Repository: facebookexperimental/usort (python)

Issue:
AssertionError when formatting semicolon-delimited import lines
Originally noticed via omnilib/ufmt#115:

AssertionError is raised when formatting a file with multiple statements on a line that includes an import. The following example code will trigger this error in 1.0.4:

```python
import sys; print(sys)
```

```shell-session
$ usort -V
usort, version 1.0.4

$ usort check foo.py
Error sorting foo.py:
Would sort foo.py
```

As seen from the API:

```ipython
Python 3.9.9 (main, Nov 28 2021, 12:38:04)
Type 'copyright', 'credits' or 'license' for more information
IPython 8.5.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: import usort

In [2]: from pathlib import Path

In [3]: foo = Path("foo.py")

In [4]: content = b"import sys; print(sys)\n\n"

In [5]: config = usort.Config()

In [6]: result = usort.usort(content, config, foo)

In [7]: result.error
Out[7]: AssertionError()

In [8]: print(result.trace)
Traceback (most recent call last):
  File "/Users/amethyst/workspace/ufmt/.venv/lib/python3.9/site-packages/usort/api.py", line 33, in usort
    new_mod = sorter.sort_module()
  File "/Users/amethyst/workspace/ufmt/.venv/lib/python3.9/site-packages/usort/sorting.py", line 305, in sort_module
    new_module = self.wrapper.visit(self.transformer)
  File "/Users/amethyst/workspace/ufmt/.venv/lib/python3.9/site-packages/libcst/metadata/wrapper.py", line 204, in visit
    return self.module.visit(visitor)
  File "/Users/amethyst/workspace/ufmt/.venv/lib/python3.9/site-packages/libcst/_nodes/module.py", line 90, in visit
    result = super(Module, self).visit(visitor)
  File "/Users/amethyst/workspace/ufmt/.venv/lib/python3.9/site-packages/libcst/_nodes/base.py", line 237, in visit
    leave_result = visitor.on_leave(self, with_updated_children)
  File "/Users/amethyst/workspace/ufmt/.venv/lib/python3.9/site-packages/libcst/_visitors.py", line 71, in on_leave
    updated_node = leave_func(original_node, updated_node)
  File "/Users/amethyst/workspace/ufmt/.venv/lib/python3.9/site-packages/usort/sorting.py", line 348, in leave_Module
    sorted_body = self.sorter.find_and_sort_blocks(
  File "/Users/amethyst/workspace/ufmt/.venv/lib/python3.9/site-packages/usort/sorting.py", line 278, in find_and_sort_blocks
    blocks = list(self.sortable_blocks(body))
  File "/Users/amethyst/workspace/ufmt/.venv/lib/python3.9/site-packages/usort/sorting.py", line 180, in sortable_blocks
    imp = import_from_node(stmt, self.config)
  File "/Users/amethyst/workspace/ufmt/.venv/lib/python3.9/site-packages/usort/translate.py", line 169, in import_from_node
    comments = import_comments_from_node(node)
  File "/Users/amethyst/workspace/ufmt/.venv/lib/python3.9/site-packages/usort/translate.py", line 39, in import_comments_from_node
    assert len(node.body) == 1
AssertionError
```

Pull request description:
Treat lines with multiple statements as barriers
Since we can't easily reason about or handle lines with multiple
statements (separated by semicolons), this improves usort's behavior by
treating them as unsortable, effectively becoming barriers between
blocks of imports. This fixes an issue where assertions failed when
transforming nodes into dataclasses.

Fixes #203

Patch:
diff --git a/docs/guide.rst b/docs/guide.rst
index 350188e..a96b325 100644
--- a/docs/guide.rst
+++ b/docs/guide.rst
@@ -383,6 +383,14 @@ logging::
 
     import other_module
 
+Similarly, any line with multiple statements separated by semicolons will also
+create an implicit block separator. µsort will defer to a dedicated formatter
+for correctly splitting those statements onto separate lines::
+
+    import zipfile
+    import sys; import re  # <-- implicit block separator
+    import ast
+
 Shadowed Imports
 ^^^^^^^^^^^^^^^^
 
diff --git a/usort/sorting.py b/usort/sorting.py
index 4c5f336..a220e03 100644
--- a/usort/sorting.py
+++ b/usort/sorting.py
@@ -56,14 +56,16 @@ class ImportSorter:
                 return False
 
             # N.b. `body` is a list, because the SimpleStatementLine might have
-            # semicolons.  We only look at the first, which is probably the most
-            # dangerous thing in here.
+            # semicolons. We treat lines with multiple statements as unsortable.
+            # This is the safest and easiest thing to do.
             #
             # If black is run, it will put the semicolon pieces on different lines,
             # but we don't want to do that until we reflow and handle directives
             # like noqa.  TODO do that before calling is_sortable_import, and assert
             # that it's not a compound statement line.
-            if isinstance(stmt.body[0], cst.ImportFrom):
+            if len(stmt.body) > 1:
+                return False
+            elif isinstance(stmt.body[0], cst.ImportFrom):
                 # from foo import (  # usort:skip
                 #     bar,
                 # )
diff --git a/usort/translate.py b/usort/translate.py
index ead4eb3..43921e3 100644
--- a/usort/translate.py
+++ b/usort/translate.py
@@ -36,7 +36,7 @@ def name_to_node(name: str) -> Union[cst.Name, cst.Attribute]:
 def import_comments_from_node(node: cst.SimpleStatementLine) -> ImportComments:
     comments = ImportComments()
 
-    assert len(node.body) == 1
+    assert len(node.body) == 1, "lines with multiple statements are unsupported"
     assert isinstance(node.body[0], (cst.Import, cst.ImportFrom))
     imp: Union[cst.Import, cst.ImportFrom] = node.body[0]
 


Test patch:
diff --git a/usort/tests/functional.py b/usort/tests/functional.py
index b48d1cd..1825602 100644
--- a/usort/tests/functional.py
+++ b/usort/tests/functional.py
@@ -186,6 +186,24 @@ class UsortStringFunctionalTest(unittest.TestCase):
             """,
         )
 
+    def test_semicolons(self) -> None:
+        self.assertUsortResult(
+            """
+                import zipfile
+                import re
+                import sys; print(sys)
+                import urllib
+                import difflib
+            """,
+            """
+                import re
+                import zipfile
+                import sys; print(sys)
+                import difflib
+                import urllib
+            """,
+        )
+
     def test_dot_handling(self) -> None:
         # Test that 'from .. import b' comes before 'from ..a import foo'
         self.assertUsortResult(