Repository: facebookexperimental/usort (python)

Issue:
Unstable sorting with mixed category imports
When basic imports include modules from multiple categories, sorting can be unstable because the sort_key isn't updated when changing order of the import names:

Test case:

```py
    def test_sorting_mixed_category_imports(self) -> None:
        self.assertUsortResult(
            """
                import os, attr
                import difflib
                import third_party
            """,
            """
                import difflib

                import attr, os
                import third_party
            """,
        )

```

Produces the following test failure:

```
(.venv) jreese@butterfree ~/workspace/usort mixed± » make format test lint
python -m ufmt format usort setup.py
python -m coverage run -m usort.tests
.......................................................................F.............
======================================================================
FAIL: test_sorting_mixed_category_imports (usort.tests.functional.UsortStringFunctionalTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/jreese/workspace/usort/usort/tests/functional.py", line 632, in test_sorting_mixed_category_imports
    self.assertUsortResult(
  File "/Users/jreese/workspace/usort/usort/tests/functional.py", line 58, in assertUsortResult
    self.fail(
AssertionError: µsort result was not stable on second pass:

Before:
-------

import os, attr
import difflib
import third_party

First Pass:
-----------

import attr, os
import difflib

import third_party

Second Pass:
------------

import difflib

import attr, os

import third_party


----------------------------------------------------------------------
Ran 85 tests in 4.119s

FAILED (failures=1)
make: *** [test] Error 1
```

Pull request:
Recalculate sort_key after sorting import items
Fixes #145 when sorting basic imports with multiple items from
different categories.

Patch:
diff --git a/docs/guide.rst b/docs/guide.rst
index 065f9d5..6c9b3b0 100644
--- a/docs/guide.rst
+++ b/docs/guide.rst
@@ -505,7 +505,7 @@ as adding the :mod:`example` module to the "first_party" category:
 .. code-block:: toml
 
     [tool.usort]
-    categories = ["future", "standard_library", "numpy", "third_party", "first_party"]
+    categories = ["future", "standard_library", numpy", "third_party", "first_party"]
     default_category = "third_party"
 
     [tool.usort.known]
diff --git a/requirements-dev.txt b/requirements-dev.txt
index c2a494d..b1523c6 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,14 +1,13 @@
-black==22.1.0
+black==21.12b0
 coverage==6.2
 flake8==4.0.1
-mistune==2.0.2
 mypy==0.931
 pessimist==0.9.1
 setuptools_scm==6.4.2
 sphinx==4.3.2
-sphinx-mdinclude==0.5.1
+sphinx-mdinclude==0.4.0b1
 tox==3.24.5
-twine==3.8.0
+twine==3.7.1
 ufmt==1.3.1.post1
 volatile==2.1.0
 wheel==0.37.1
diff --git a/requirements.txt b/requirements.txt
index 034a717..092a1ee 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,7 @@
 attrs==21.4.0
-click==8.0.4
-LibCST==0.4.1
-stdlibs==2022.2.2
+click==8.0.3
+LibCST==0.4.0
+stdlibs==2021.4.1
 moreorless==0.4.0
 toml==0.10.2
 trailrunner==1.1.1
diff --git a/usort/sorting.py b/usort/sorting.py
index 416c7f8..35db215 100644
--- a/usort/sorting.py
+++ b/usort/sorting.py
@@ -261,6 +261,7 @@ class ImportSorter:
         # Sort items within each remaining statement
         for imp in imports:
             imp.items.sort()
+            imp.calculate_sort_key()
 
         return imports
 
diff --git a/usort/types.py b/usort/types.py
index 1e4e66c..0f88a8c 100644
--- a/usort/types.py
+++ b/usort/types.py
@@ -218,7 +218,7 @@ class SortableImport:
 
         return results
 
-    def __attrs_post_init__(self) -> None:
+    def calculate_sort_key(self) -> None:
         top: Optional[str] = None
         ndots = 0
 
@@ -239,6 +239,9 @@ class SortableImport:
             ndots=ndots,
         )
 
+    def __attrs_post_init__(self) -> None:
+        self.calculate_sort_key()
+
 
 @dataclass(repr=False)
 class SortableBlock:


Test patch:
diff --git a/usort/tests/functional.py b/usort/tests/functional.py
index 5737dbe..1c2e4b7 100644
--- a/usort/tests/functional.py
+++ b/usort/tests/functional.py
@@ -628,6 +628,21 @@ numpy = ["numpy", "pandas"]
             """,
         )
 
+    def test_sorting_mixed_category_imports(self) -> None:
+        self.assertUsortResult(
+            """
+                import os, attr
+                import difflib
+                import third_party
+            """,
+            """
+                import difflib
+
+                import attr, os
+                import third_party
+            """,
+        )
+
     def test_merging_import_items(self) -> None:
         self.assertUsortResult(
             """