From e49c133b484fa464449f571976ed55e4663ca27b Mon Sep 17 00:00:00 2001 From: Hadi Alqattan Date: Wed, 14 Oct 2020 22:27:07 +0300 Subject: [PATCH] Support implicit imports from sub-packages. (#37) --- docs/CHANGELOG.md | 1 + docs/README.md | 12 ++++++++++++ pycln/utils/refactor.py | 19 ++++++++++++++++++- tests/test_refactor.py | 18 ++++++++++++++++++ 4 files changed, 49 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index c940134..e8d8711 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -12,6 +12,7 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Added +- [Support implicit imports from sub-packages by @hadialqattan](https://github.com/hadialqattan/pycln/pull/36) - [Support semi string type hint by @hadialqattan](https://github.com/hadialqattan/pycln/pull/35) - [Support casting case by @hadialqattan](https://github.com/hadialqattan/pycln/pull/34) diff --git a/docs/README.md b/docs/README.md index 68a0c47..1e7d9d7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -716,6 +716,18 @@ All bellow imports are considered as used: import y ``` +### Implicit Imports From Sub-Packages + +> Pycln can deal with implicit imports from sub-packages. + +For example: + +```python +import os.path # marked as used. + +print(os.getpid()) +``` + ### Import With Importlib > Not supported yet, on the roadmap: diff --git a/pycln/utils/refactor.py b/pycln/utils/refactor.py index e22a8bd..9ec7ace 100644 --- a/pycln/utils/refactor.py +++ b/pycln/utils/refactor.py @@ -246,7 +246,9 @@ def _get_used_names( """ used_names: Set[str] = set() for alias in node.names: - if self._should_remove(node, alias, is_star): + if self._should_remove( + node, alias, is_star + ) and not self._is_partially_used(alias, is_star): if not is_star: self.reporter.removed_import(self._path, node, alias) continue @@ -308,6 +310,21 @@ def _expand_import_star( self.reporter.ignored_import(self._path, node, is_star=True) return node, None + def _is_partially_used(self, alias: ast.alias, is_star: bool) -> bool: + """Determine if the alias name partially used or not. + + :param alias: an `ast.alias` node. + :param is_star: is it a '*' import. + :returns: whather the alias name partially used or not. + """ + if not alias.asname and "." in alias.name: + names = alias.name.split(".")[1:] + for name in reversed(names): + alias.name = alias.name.rstrip("." + name) + if self._has_used(alias.name, is_star): + return True + return False + def _should_remove( self, node: Union[Import, ImportFrom], alias: ast.alias, is_star: bool ) -> bool: diff --git a/tests/test_refactor.py b/tests/test_refactor.py index 25cb735..bc71581 100644 --- a/tests/test_refactor.py +++ b/tests/test_refactor.py @@ -533,6 +533,24 @@ def test_expand_import_star( enode, is_star = self.session_maker._expand_import_star(node) assert (enode, is_star) == (node, expec_is_star) + @pytest.mark.parametrize( + "_has_used_return, name, asname, expec_val", + [ + pytest.param(True, "os.path.join", None, True, id="used"), + pytest.param(False, "os.path.join", None, False, id="unused"), + pytest.param(None, "os.path.join", "asname", False, id="as alias"), + pytest.param(None, "os", None, False, id="single name"), + ], + ) + @mock.patch(MOCK % "Refactor._has_used") + def test_is_partially_used( + self, _has_used, _has_used_return, name, asname, expec_val + ): + _has_used.return_value = _has_used_return + alias = ast.alias(name=name, asname=asname) + val = self.session_maker._is_partially_used(alias, False) + assert val == expec_val + @pytest.mark.parametrize( "_has_used_return, _has_side_effects_return, all_, name, expec_val", [