Repository: facebookexperimental/usort (python)

Issue:
KeyError while sorting duplicate imports
On this file:
```
from a import x
from b import y
from b import y
from c import x
```

`usort` fails with
```
Traceback (most recent call last):
  File "usort/cli.py", line 80, in list_imports
    blocks = sorter.sortable_blocks(mod.body)
  File "usort/sorting.py", line 192, in sortable_blocks
    current = self.split_inplace(current, overlap)
  File "usort/sorting.py", line 169, in split_inplace
    new.imported_names[key] = block.imported_names.pop(key)
KeyError: 'y'
```

Pull request description:
Avoid KeyError when imports are duplicated.
Because we do block splitting before deduplication, it could previously try to pop the same name twice.

Fixes #194

Patch:
diff --git a/setup.cfg b/setup.cfg
index 2d83454..ccf78c1 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -37,8 +37,8 @@ include = usort/*
 omit = usort/tests/*
 
 [coverage:report]
-fail_under = 70
-precision = 1
+fail_under = 92
+precision = 0
 show_missing = True
 skip_covered = True
 
diff --git a/usort/sorting.py b/usort/sorting.py
index 666728c..4c5f336 100644
--- a/usort/sorting.py
+++ b/usort/sorting.py
@@ -163,7 +163,14 @@ class ImportSorter:
             # move imported names metadata
             for imp in new.imports:
                 for key in list(imp.imported_names):
-                    new.imported_names[key] = block.imported_names.pop(key)
+                    name = block.imported_names.pop(key, None)
+                    if name is not None:
+                        # Normally we wouldn't see multiple copies of a key because that
+                        # would have caused a block split already, but if an import is
+                        # just verbatim repeated, it's not technically shadowing.
+                        # If that's the case, we've already popped it and achieved the
+                        # goal of this block.
+                        new.imported_names[key] = name
 
         return new
 


Test patch:
diff --git a/usort/tests/__init__.py b/usort/tests/__init__.py
index 5ca0e12..67dfb7d 100644
--- a/usort/tests/__init__.py
+++ b/usort/tests/__init__.py
@@ -6,6 +6,7 @@
 from .cli import CliTest
 from .config import ConfigTest
 from .functional import BasicOrderingTest, UsortStringFunctionalTest
+from .sorting import SplitTest
 from .stdlibs import StdlibsTest
 from .translate import IsSortableTest, SortableImportTest
 from .types import TypesTest
@@ -21,4 +22,5 @@ __all__ = [
     "StdlibsTest",
     "TypesTest",
     "UtilTest",
+    "SplitTest",
 ]
diff --git a/usort/tests/functional.py b/usort/tests/functional.py
index c486cf1..b48d1cd 100644
--- a/usort/tests/functional.py
+++ b/usort/tests/functional.py
@@ -897,6 +897,21 @@ numpy = ["numpy", "pandas"]
             """,
         )
 
+    def test_sort_merging_and_implicit_blocks(self) -> None:
+        self.assertUsortResult(
+            """
+                from a import x
+                from b import y
+                from b import y
+                from c import x
+            """,
+            """
+                from a import x
+                from b import y
+                from c import x
+            """,
+        )
+
     def test_skip_directives(self) -> None:
         """Test both usort:skip and isort:skip on single line imports"""
         self.assertUsortResult(
diff --git a/usort/tests/sorting.py b/usort/tests/sorting.py
new file mode 100644
index 0000000..446740b
--- /dev/null
+++ b/usort/tests/sorting.py
@@ -0,0 +1,30 @@
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+import unittest
+from pathlib import Path
+
+import libcst as cst
+
+from ..config import Config
+
+from ..sorting import ImportSorter
+
+
+class SplitTest(unittest.TestCase):
+    def test_split_block(self) -> None:
+        mod = cst.parse_module(
+            b"""\
+from a import x
+from b import y
+from b import y
+from c import x
+"""
+        )
+        x = ImportSorter(module=mod, path=Path(), config=Config())
+        blocks = x.sortable_blocks(mod.children)  # type: ignore
+        self.assertEqual(2, len(blocks))
+        self.assertEqual({"x": "a.x"}, blocks[0].imported_names)
+        self.assertEqual({"y": "b.y", "x": "c.x"}, blocks[1].imported_names)