diff --git a/docs/configuration/options.md b/docs/configuration/options.md index 0677724de..70cceecdd 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -803,6 +803,16 @@ Order imports by type, which is determined by case, in addition to alphabeticall - --ot - --order-by-type +## Order By Qualified Name + +Order imports by their full module path. That is, `from foo.bar import wow` would be ordered before `from foo import baz` (since `foo.bar.wow` < `foo.baz`). + +**Type:** Bool +**Default:** `False` +**Config default:** `false` +**Python & Config File Name:** order_by_qualified_name +**CLI Flags:** **Not Supported** + ## Atomic Ensures the output doesn't save if the resulting file contains syntax errors. diff --git a/isort/output.py b/isort/output.py index 1e8d5d615..c6f64995e 100644 --- a/isort/output.py +++ b/isort/output.py @@ -63,10 +63,17 @@ def sorted_imports( from_modules = parsed.imports[section]["from"] if not config.only_sections: + _from_modules = from_modules from_modules = sorting.sort( config, from_modules, - key=lambda key: sorting.module_key(key, config, section_name=section), + key=lambda key: sorting.module_key( + f"{key}.{min(_from_modules[key])}" + if config.order_by_qualified_name and _from_modules[key] + else key, + config, + section_name=section, + ), reverse=config.reverse_sort, ) diff --git a/isort/settings.py b/isort/settings.py index f5c898c0a..48889a4ac 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -173,6 +173,7 @@ class _Config: balanced_wrapping: bool = False use_parentheses: bool = False order_by_type: bool = True + order_by_qualified_name: bool = False atomic: bool = False lines_before_imports: int = -1 lines_after_imports: int = -1 diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py index 0fb972de0..86aa86168 100644 --- a/tests/unit/test_isort.py +++ b/tests/unit/test_isort.py @@ -1676,6 +1676,29 @@ def test_order_by_type() -> None: ) +def test_order_by_qualified_name() -> None: + # Without option: sorted by module name ("foo" < "foo.bar") + test_input = "from foo.bar import wow\nfrom foo import baz\n" + assert isort.code(test_input, order_by_qualified_name=False) == ( + "from foo import baz\nfrom foo.bar import wow\n" + ) + # With option: sorted by fully qualified path ("foo.bar.wow" < "foo.baz") + assert isort.code(test_input, order_by_qualified_name=True) == ( + "from foo.bar import wow\nfrom foo import baz\n" + ) + # Multiple imports from the same module use the first alphabetically + test_input = "from foo import zebra\nfrom foo.bar import wow\n" + assert isort.code(test_input, order_by_qualified_name=True) == ( + "from foo.bar import wow\nfrom foo import zebra\n" + ) + # Multiple imports on one line: key uses the first import name alphabetically + # "from foo import qux, zebra" -> key "foo.qux" > "foo.bar.wow" + test_input = "from foo import qux, zebra\nfrom foo.bar import wow\n" + assert isort.code(test_input, order_by_qualified_name=True) == ( + "from foo.bar import wow\nfrom foo import qux, zebra\n" + ) + + @pytest.mark.parametrize("has_body", [True, False]) def test_custom_lines_before_import_section(has_body: bool) -> None: """Test the case where the number of lines to output before imports has been explicitly set."""