From 685c80c864f05ee37db31246ff125639aa38432e Mon Sep 17 00:00:00 2001 From: Karl Otness Date: Wed, 15 Jun 2022 21:05:57 -0400 Subject: [PATCH 1/5] Add support for annotated assignments to static attribute lookup. When walking the ast of a module, look for AnnAssign nodes in addition to Assign to support assignments with type annotations, for example. Since we have to read different attributes, split the generators into a for loop. Existing ast.Assign nodes follow the same processing as before. --- setuptools/config/expand.py | 29 ++++++++++++-------------- setuptools/tests/config/test_expand.py | 12 +++++++++++ 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/setuptools/config/expand.py b/setuptools/config/expand.py index be987df5b4..a0dd7c2a3f 100644 --- a/setuptools/config/expand.py +++ b/setuptools/config/expand.py @@ -69,24 +69,21 @@ def __init__(self, name: str, spec: ModuleSpec): def __getattr__(self, attr): """Attempt to load an attribute "statically", via :func:`ast.literal_eval`.""" try: - assignment_expressions = ( - statement - for statement in self.module.body - if isinstance(statement, ast.Assign) - ) - expressions_with_target = ( - (statement, target) - for statement in assignment_expressions - for target in statement.targets - ) - matching_values = ( - statement.value - for statement, target in expressions_with_target - if isinstance(target, ast.Name) and target.id == attr - ) - return next(ast.literal_eval(value) for value in matching_values) + for statement in self.module.body: + if isinstance(statement, ast.Assign): + targets = statement.targets + value = statement.value + elif isinstance(statement, ast.AnnAssign): + targets = [statement.target] + value = statement.value + else: + continue + for target in targets: + if isinstance(target, ast.Name) and target.id == attr: + return ast.literal_eval(value) except Exception as e: raise AttributeError(f"{self.name} has no attribute {attr}") from e + raise AttributeError(f"{self.name} has no attribute {attr}") def glob_relative( diff --git a/setuptools/tests/config/test_expand.py b/setuptools/tests/config/test_expand.py index 15053c8f24..a1f543861e 100644 --- a/setuptools/tests/config/test_expand.py +++ b/setuptools/tests/config/test_expand.py @@ -85,6 +85,18 @@ def test_read_attr(self, tmp_path, monkeypatch): values = expand.read_attr('lib.mod.VALUES', {'lib': 'pkg/sub'}, tmp_path) assert values['c'] == (0, 1, 1) + def test_read_annotated_attr(self, tmp_path): + files = { + "pkg/__init__.py": "", + "pkg/sub/__init__.py": ( + "VERSION: str = '0.1.1'\n" + "raise SystemExit(1)\n" + ), + } + write_files(files, tmp_path) + # Make sure this attribute can be read statically + assert expand.read_attr('pkg.sub.VERSION', root_dir=tmp_path) == '0.1.1' + def test_import_order(self, tmp_path): """ Sometimes the import machinery will import the parent package of a nested From 15af535f9bfa97c58d0a8d7efd698e10c633153b Mon Sep 17 00:00:00 2001 From: Karl Otness Date: Sat, 18 Jun 2022 22:45:47 -0400 Subject: [PATCH 2/5] Add changelog entry for `attr:` type annotation support --- changelog.d/3391.change.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/3391.change.rst diff --git a/changelog.d/3391.change.rst b/changelog.d/3391.change.rst new file mode 100644 index 0000000000..41cfea3355 --- /dev/null +++ b/changelog.d/3391.change.rst @@ -0,0 +1 @@ +Updated ``attr:`` to also extract simple constants with type annotations -- by :user:`karlotness` From 0488436c5831564a9746707ec3f54e403fffbf98 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sun, 19 Jun 2022 08:26:34 +0100 Subject: [PATCH 3/5] test_expand: Add example for annotated assignment without value --- setuptools/tests/config/test_expand.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/setuptools/tests/config/test_expand.py b/setuptools/tests/config/test_expand.py index a1f543861e..523779a8ed 100644 --- a/setuptools/tests/config/test_expand.py +++ b/setuptools/tests/config/test_expand.py @@ -85,13 +85,17 @@ def test_read_attr(self, tmp_path, monkeypatch): values = expand.read_attr('lib.mod.VALUES', {'lib': 'pkg/sub'}, tmp_path) assert values['c'] == (0, 1, 1) - def test_read_annotated_attr(self, tmp_path): + @pytest.mark.parametrize( + "example", + [ + "VERSION: str\nVERSION = '0.1.1'\nraise SystemExit(1)\n", + "VERSION: str = '0.1.1'\nraise SystemExit(1)\n", + ] + ) + def test_read_annotated_attr(self, tmp_path, example): files = { "pkg/__init__.py": "", - "pkg/sub/__init__.py": ( - "VERSION: str = '0.1.1'\n" - "raise SystemExit(1)\n" - ), + "pkg/sub/__init__.py": example, } write_files(files, tmp_path) # Make sure this attribute can be read statically From 6384f26597800c89e5605931a306fbb68127ce13 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sun, 19 Jun 2022 08:19:51 +0100 Subject: [PATCH 4/5] config.expand: Refactor StaticModule --- setuptools/config/expand.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/setuptools/config/expand.py b/setuptools/config/expand.py index a0dd7c2a3f..75af6018c8 100644 --- a/setuptools/config/expand.py +++ b/setuptools/config/expand.py @@ -66,24 +66,23 @@ def __init__(self, name: str, spec: ModuleSpec): vars(self).update(locals()) del self.self + def _find_assignments(self) -> Iterator[Tuple[ast.AST, Optional[ast.AST]]]: + for statement in self.module.body: + if isinstance(statement, ast.Assign): + yield from ((target, statement.value) for target in statement.targets) + elif isinstance(statement, ast.AnnAssign): + yield (statement.target, statement.value) + def __getattr__(self, attr): """Attempt to load an attribute "statically", via :func:`ast.literal_eval`.""" try: - for statement in self.module.body: - if isinstance(statement, ast.Assign): - targets = statement.targets - value = statement.value - elif isinstance(statement, ast.AnnAssign): - targets = [statement.target] - value = statement.value - else: - continue - for target in targets: - if isinstance(target, ast.Name) and target.id == attr: - return ast.literal_eval(value) + return next( + ast.literal_eval(value) + for target, value in self._find_assignments() + if isinstance(target, ast.Name) and target.id == attr + ) except Exception as e: raise AttributeError(f"{self.name} has no attribute {attr}") from e - raise AttributeError(f"{self.name} has no attribute {attr}") def glob_relative( From 0b1d090e48b16bff59e6e9e1d33b0d6dca65a7ee Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sun, 19 Jun 2022 08:21:25 +0100 Subject: [PATCH 5/5] config.expand.StaticModule: handle scenarios when annotated assignment does not have a value --- setuptools/config/expand.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setuptools/config/expand.py b/setuptools/config/expand.py index 75af6018c8..ed7564047a 100644 --- a/setuptools/config/expand.py +++ b/setuptools/config/expand.py @@ -66,11 +66,11 @@ def __init__(self, name: str, spec: ModuleSpec): vars(self).update(locals()) del self.self - def _find_assignments(self) -> Iterator[Tuple[ast.AST, Optional[ast.AST]]]: + def _find_assignments(self) -> Iterator[Tuple[ast.AST, ast.AST]]: for statement in self.module.body: if isinstance(statement, ast.Assign): yield from ((target, statement.value) for target in statement.targets) - elif isinstance(statement, ast.AnnAssign): + elif isinstance(statement, ast.AnnAssign) and statement.value: yield (statement.target, statement.value) def __getattr__(self, attr):