diff --git a/CHANGELOG.md b/CHANGELOG.md index ced2475a3..587259041 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,10 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. ### 5.1.0 July TBD, 2020 - isort now throws an exception if an invalid settings path is given (issue #1174). + - Implemented support for automatic redundant alias removal (issue #1281). - Fixed #1178: support for semicolons in decorators. - Fixed #1315: Extra newline before comment with -n + --fss. + **Formatting changes implied:** - Fixed #1280: rewrite of as imports changes the behavior of the imports. diff --git a/isort/hooks.py b/isort/hooks.py index 2f3decd4c..892bd312c 100644 --- a/isort/hooks.py +++ b/isort/hooks.py @@ -62,13 +62,13 @@ def git_hook(strict: bool = False, modify: bool = False) -> int: staged_contents = get_output(staged_cmd) try: - if not api.check_code_string(staged_contents, - file_path=Path(filename), - config=config): + if not api.check_code_string( + staged_contents, file_path=Path(filename), config=config + ): errors += 1 if modify: api.sort_file(filename, config=config) - except exceptions.FileSkipComment: + except exceptions.FileSkipped: # pragma: no cover pass return errors if strict else 0 diff --git a/isort/main.py b/isort/main.py index 0d0418636..320e3f898 100644 --- a/isort/main.py +++ b/isort/main.py @@ -585,6 +585,16 @@ def _build_arg_parser() -> argparse.ArgumentParser: action="store_true", help="Tells isort to honor noqa comments to enforce skipping those comments.", ) + parser.add_argument( + "--remove-redundant-aliases", + dest="remove_redundant_aliases", + action="store_true", + help=( + "Tells isort to remove redundant aliases from imports, such as import os as os." + " This defaults to false simply because some projects use these seemingly useless alias" + " to signify intent and change behaviour." + ), + ) # deprecated options parser.add_argument( diff --git a/isort/parse.py b/isort/parse.py index 7f112fc14..94b1e3161 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -319,14 +319,19 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte while "as" in just_imports: as_index = just_imports.index("as") if type_of_import == "from": - module = just_imports[0] + "." + just_imports[as_index - 1] + nested_module = just_imports[as_index - 1] + module = just_imports[0] + "." + nested_module as_name = just_imports[as_index + 1] - if as_name not in as_map["from"][module]: + if nested_module == as_name and config.remove_redundant_aliases: + pass + elif as_name not in as_map["from"][module]: as_map["from"][module].append(as_name) else: module = just_imports[as_index - 1] as_name = just_imports[as_index + 1] - if as_name not in as_map["straight"][module]: + if module == as_name and config.remove_redundant_aliases: + pass + elif as_name not in as_map["straight"][module]: as_map["straight"][module].append(as_name) if not config.combine_as_imports: categorized_comments["straight"][module] = comments diff --git a/isort/settings.py b/isort/settings.py index a3738c17c..80938dbd5 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -166,6 +166,7 @@ class _Config: honor_noqa: bool = False src_paths: FrozenSet[Path] = frozenset() old_finders: bool = False + remove_redundant_aliases: bool = False def __post_init__(self): py_version = self.py_version diff --git a/tests/test_hooks.py b/tests/test_hooks.py index 49ba8b45b..c84d6d5c9 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -2,7 +2,7 @@ from io import BytesIO from unittest.mock import MagicMock, patch -from isort import hooks +from isort import exceptions, hooks def test_git_hook(src_dir): @@ -26,3 +26,15 @@ class FakeProcessResponse(object): with patch("subprocess.run", MagicMock(return_value=FakeProcessResponse())) as run_mock: with patch("isort.api", MagicMock(return_value=False)): hooks.git_hook(modify=True) + + # Test with skipped file returned from git + with patch( + "isort.hooks.get_lines", MagicMock(return_value=[os.path.join(src_dir, "main.py")]) + ) as run_mock: + + class FakeProcessResponse(object): + stdout = b"# isort: skip-file\nimport b\nimport a\n" + + with patch("subprocess.run", MagicMock(return_value=FakeProcessResponse())) as run_mock: + with patch("isort.api", MagicMock(side_effect=exceptions.FileSkipped("", ""))): + hooks.git_hook(modify=True) diff --git a/tests/test_ticketed_features.py b/tests/test_ticketed_features.py index 283d7c1b8..2709a7690 100644 --- a/tests/test_ticketed_features.py +++ b/tests/test_ticketed_features.py @@ -19,3 +19,16 @@ def test_thing(): pass """, show_diff=True, ) + + +def test_isort_automatically_removes_duplicate_aliases_issue_1193(): + """Test to ensure isort can automatically remove duplicate aliases. + See: https://github.com/timothycrosley/isort/issues/1281 + """ + assert isort.check_code("from urllib import parse as parse\n", show_diff=True) + assert ( + isort.code("from urllib import parse as parse", remove_redundant_aliases=True) + == "from urllib import parse\n" + ) + assert isort.check_code("import os as os\n", show_diff=True) + assert isort.code("import os as os", remove_redundant_aliases=True) == "import os\n"