From 6de971f158553c47ce11e7be9d38d268e0398193 Mon Sep 17 00:00:00 2001 From: "John T. Wodder II" Date: Sat, 27 Apr 2019 19:21:50 +0000 Subject: [PATCH 01/12] Implement a "literal_attr:" config directive --- setuptools/config.py | 62 +++++++++++++++++++++++++++----- setuptools/tests/test_config.py | 64 +++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 9 deletions(-) diff --git a/setuptools/config.py b/setuptools/config.py index 9b9a0c45e7..d1456cac18 100644 --- a/setuptools/config.py +++ b/setuptools/config.py @@ -1,4 +1,5 @@ from __future__ import absolute_import, unicode_literals +import ast import io import os import sys @@ -316,15 +317,22 @@ def _parse_attr(cls, value, package_dir=None): Examples: attr: package.attr attr: package.module.attr + literal_attr: package.attr + literal_attr: package.module.attr :param str value: :rtype: str """ attr_directive = 'attr:' - if not value.startswith(attr_directive): + literal_attr_directive = 'literal_attr:' + if value.startswith(attr_directive): + directive = attr_directive + elif value.startswith(literal_attr_directive): + directive = literal_attr_directive + else: return value - attrs_path = value.replace(attr_directive, '').strip().split('.') + attrs_path = value.replace(directive, '').strip().split('.') attr_name = attrs_path.pop() module_name = '.'.join(attrs_path) @@ -344,13 +352,49 @@ def _parse_attr(cls, value, package_dir=None): elif '' in package_dir: # A custom parent directory was specified for all root modules parent_path = os.path.join(os.getcwd(), package_dir['']) - sys.path.insert(0, parent_path) - try: - module = import_module(module_name) - value = getattr(module, attr_name) - - finally: - sys.path = sys.path[1:] + if directive == attr_directive: + sys.path.insert(0, parent_path) + try: + module = import_module(module_name) + value = getattr(module, attr_name) + finally: + sys.path = sys.path[1:] + + elif directive == literal_attr_directive: + fpath = os.path.join(parent_path, *module_name.split('.')) + if os.path.exists(fpath + '.py'): + fpath += '.py' + elif os.path.isdir(fpath): + fpath = os.path.join(fpath, '__init__.py') + else: + raise DistutilsOptionError( + 'Could not find module ' + module_name + ) + with open(fpath, 'rb') as fp: + src = fp.read() + found = False + top_level = ast.parse(src) + for statement in top_level.body: + if isinstance(statement, ast.Assign): + for target in statement.targets: + if isinstance(target, ast.Name) \ + and target.id == attr_name: + value = ast.literal_eval(statement.value) + found = True + elif isinstance(target, ast.Tuple) \ + and any(isinstance(t, ast.Name) and t.id==attr_name + for t in target.elts): + stmnt_value = ast.literal_eval(statement.value) + for t,v in zip(target.elts, stmnt_value): + if isinstance(t, ast.Name) \ + and t.id == attr_name: + value = v + found = True + if not found: + raise DistutilsOptionError( + 'No literal assignment to {!r} found in file' + .format(attr_name) + ) return value diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py index 2fa0b374e2..03e6916bd9 100644 --- a/setuptools/tests/test_config.py +++ b/setuptools/tests/test_config.py @@ -300,6 +300,37 @@ def test_version(self, tmpdir): with get_dist(tmpdir) as dist: assert dist.metadata.version == '2016.11.26' + def test_literal_version(self, tmpdir): + + _, config = fake_env( + tmpdir, + '[metadata]\n' + 'version = literal_attr: fake_package.VERSION\n' + ) + with get_dist(tmpdir) as dist: + assert dist.metadata.version == '1.2.3' + + config.write( + '[metadata]\n' + 'version = literal_attr: fake_package.VERSION_MAJOR\n' + ) + with get_dist(tmpdir) as dist: + assert dist.metadata.version == '1' + + subpack = tmpdir.join('fake_package').mkdir('subpackage') + subpack.join('__init__.py').write('') + subpack.join('submodule.py').write( + 'import third_party_module\n' + 'VERSION = (2016, 11, 26)' + ) + + config.write( + '[metadata]\n' + 'version = attr: fake_package.subpackage.submodule.VERSION\n' + ) + with get_dist(tmpdir) as dist: + assert dist.metadata.version == '2016.11.26' + def test_version_file(self, tmpdir): _, config = fake_env( @@ -332,6 +363,17 @@ def test_version_with_package_dir_simple(self, tmpdir): with get_dist(tmpdir) as dist: assert dist.metadata.version == '1.2.3' + config.write( + '[metadata]\n' + 'version = literal_attr: fake_package_simple.VERSION\n' + '[options]\n' + 'package_dir =\n' + ' = src\n' + ) + + with get_dist(tmpdir) as dist: + assert dist.metadata.version == '1.2.3' + def test_version_with_package_dir_rename(self, tmpdir): _, config = fake_env( @@ -347,6 +389,17 @@ def test_version_with_package_dir_rename(self, tmpdir): with get_dist(tmpdir) as dist: assert dist.metadata.version == '1.2.3' + config.write( + '[metadata]\n' + 'version = literal_attr: fake_package_rename.VERSION\n' + '[options]\n' + 'package_dir =\n' + ' fake_package_rename = fake_dir\n' + ) + + with get_dist(tmpdir) as dist: + assert dist.metadata.version == '1.2.3' + def test_version_with_package_dir_complex(self, tmpdir): _, config = fake_env( @@ -362,6 +415,17 @@ def test_version_with_package_dir_complex(self, tmpdir): with get_dist(tmpdir) as dist: assert dist.metadata.version == '1.2.3' + config.write( + '[metadata]\n' + 'version = literal_attr: fake_package_complex.VERSION\n' + '[options]\n' + 'package_dir =\n' + ' fake_package_complex = src/fake_dir\n' + ) + + with get_dist(tmpdir) as dist: + assert dist.metadata.version == '1.2.3' + def test_unknown_meta_item(self, tmpdir): fake_env( From 2bbd4d72225ad4f717be65460940292b50bd781e Mon Sep 17 00:00:00 2001 From: "John T. Wodder II" Date: Sat, 27 Apr 2019 19:31:21 +0000 Subject: [PATCH 02/12] Update documentation --- docs/setuptools.txt | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/docs/setuptools.txt b/docs/setuptools.txt index ec58b754fc..3e616582ff 100644 --- a/docs/setuptools.txt +++ b/docs/setuptools.txt @@ -2193,7 +2193,7 @@ Metadata and options are set in the config sections of the same name. * In some cases, complex values can be provided in dedicated subsections for clarity. -* Some keys allow ``file:``, ``attr:``, and ``find:`` and ``find_namespace:`` directives in +* Some keys allow ``file:``, ``attr:``, ``literal_attr:``, ``find:``, and ``find_namespace:`` directives in order to cover common usecases. * Unknown keys are ignored. @@ -2290,6 +2290,15 @@ Special directives: * ``attr:`` - Value is read from a module attribute. ``attr:`` supports callables and iterables; unsupported types are cast using ``str()``. + +* ``literal_attr:`` — Like ``attr:``, except that the value is parsed using + ``ast.literal_eval()`` instead of by importing the module. This allows one + to specify an attribute of a module that imports one or more third-party + modules without having to install those modules first; as a downside, + ``literal_attr:`` only supports variables that are assigned constant + expressions, not more complex assignments like ``__version__ = + '.'.join(map(str, (VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH)))``. + * ``file:`` - Value is read from a list of files and then concatenated @@ -2305,14 +2314,14 @@ Metadata The aliases given below are supported for compatibility reasons, but their use is not advised. -============================== ================= ================= =============== ===== -Key Aliases Type Minimum Version Notes -============================== ================= ================= =============== ===== +============================== ================= ================================ =============== ===== +Key Aliases Type Minimum Version Notes +============================== ================= ================================ =============== ===== name str -version attr:, file:, str 39.2.0 (1) +version attr:, literal_attr:, file:, str 39.2.0 (1) url home-page str download_url download-url str -project_urls dict 38.3.0 +project_urls dict 38.3.0 author str author_email author-email str maintainer str @@ -2323,13 +2332,13 @@ license_file str license_files list-comma description summary file:, str long_description long-description file:, str -long_description_content_type str 38.6.0 +long_description_content_type str 38.6.0 keywords list-comma platforms platform list-comma provides list-comma requires list-comma obsoletes list-comma -============================== ================= ================= =============== ===== +============================== ================= ================================ =============== ===== .. note:: A version loaded using the ``file:`` directive must comply with PEP 440. From 130cbede42d3d351fc21bb35c18c1be7e108df46 Mon Sep 17 00:00:00 2001 From: "John T. Wodder II" Date: Sat, 27 Apr 2019 19:54:51 +0000 Subject: [PATCH 03/12] Added changelog.d news fragment --- changelog.d/1753.change.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/1753.change.rst diff --git a/changelog.d/1753.change.rst b/changelog.d/1753.change.rst new file mode 100644 index 0000000000..0f27bb2e46 --- /dev/null +++ b/changelog.d/1753.change.rst @@ -0,0 +1 @@ +Added a ``literal_attr:`` config directive to support reading versions from attributes of modules that import third-party modules From d6bcf5e89ef6a523c2476b249aba810af9808d8b Mon Sep 17 00:00:00 2001 From: "John T. Wodder II" Date: Fri, 15 May 2020 21:17:49 +0000 Subject: [PATCH 04/12] Merge `literal_attr:` functionality into `attr:` --- changelog.d/1753.change.rst | 5 +- docs/setuptools.txt | 27 +++++----- setuptools/config.py | 87 +++++++++++++++------------------ setuptools/tests/test_config.py | 54 +------------------- 4 files changed, 57 insertions(+), 116 deletions(-) diff --git a/changelog.d/1753.change.rst b/changelog.d/1753.change.rst index 0f27bb2e46..c8b68026c2 100644 --- a/changelog.d/1753.change.rst +++ b/changelog.d/1753.change.rst @@ -1 +1,4 @@ -Added a ``literal_attr:`` config directive to support reading versions from attributes of modules that import third-party modules +``attr:`` now extracts variables through rudimentary examination of the AST, +thereby supporting modules with third-party imports. If examining the AST +fails to find the variable, ``attr:`` falls back to the old behavior of +importing the module. diff --git a/docs/setuptools.txt b/docs/setuptools.txt index 3e616582ff..c37b7ec5c5 100644 --- a/docs/setuptools.txt +++ b/docs/setuptools.txt @@ -2193,7 +2193,7 @@ Metadata and options are set in the config sections of the same name. * In some cases, complex values can be provided in dedicated subsections for clarity. -* Some keys allow ``file:``, ``attr:``, ``literal_attr:``, ``find:``, and ``find_namespace:`` directives in +* Some keys allow ``file:``, ``attr:``, ``find:``, and ``find_namespace:`` directives in order to cover common usecases. * Unknown keys are ignored. @@ -2291,13 +2291,10 @@ Special directives: * ``attr:`` - Value is read from a module attribute. ``attr:`` supports callables and iterables; unsupported types are cast using ``str()``. -* ``literal_attr:`` — Like ``attr:``, except that the value is parsed using - ``ast.literal_eval()`` instead of by importing the module. This allows one - to specify an attribute of a module that imports one or more third-party - modules without having to install those modules first; as a downside, - ``literal_attr:`` only supports variables that are assigned constant - expressions, not more complex assignments like ``__version__ = - '.'.join(map(str, (VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH)))``. + In order to support the common case of a literal value assigned to a variable + in a module containing (directly or indirectly) third-party imports, + ``attr:`` first tries to read the value from the module by examining the + module's AST. If that fails, ``attr:`` falls back to importing the module. * ``file:`` - Value is read from a list of files and then concatenated @@ -2314,14 +2311,14 @@ Metadata The aliases given below are supported for compatibility reasons, but their use is not advised. -============================== ================= ================================ =============== ===== -Key Aliases Type Minimum Version Notes -============================== ================= ================================ =============== ===== +============================== ================= ================= =============== ===== +Key Aliases Type Minimum Version Notes +============================== ================= ================= =============== ===== name str -version attr:, literal_attr:, file:, str 39.2.0 (1) +version attr:, file:, str 39.2.0 (1) url home-page str download_url download-url str -project_urls dict 38.3.0 +project_urls dict 38.3.0 author str author_email author-email str maintainer str @@ -2332,13 +2329,13 @@ license_file str license_files list-comma description summary file:, str long_description long-description file:, str -long_description_content_type str 38.6.0 +long_description_content_type str 38.6.0 keywords list-comma platforms platform list-comma provides list-comma requires list-comma obsoletes list-comma -============================== ================= ================================ =============== ===== +============================== ================= ================= =============== ===== .. note:: A version loaded using the ``file:`` directive must comply with PEP 440. diff --git a/setuptools/config.py b/setuptools/config.py index d1456cac18..0a2f51e212 100644 --- a/setuptools/config.py +++ b/setuptools/config.py @@ -317,22 +317,15 @@ def _parse_attr(cls, value, package_dir=None): Examples: attr: package.attr attr: package.module.attr - literal_attr: package.attr - literal_attr: package.module.attr :param str value: :rtype: str """ attr_directive = 'attr:' - literal_attr_directive = 'literal_attr:' - if value.startswith(attr_directive): - directive = attr_directive - elif value.startswith(literal_attr_directive): - directive = literal_attr_directive - else: + if not value.startswith(attr_directive): return value - attrs_path = value.replace(directive, '').strip().split('.') + attrs_path = value.replace(attr_directive, '').strip().split('.') attr_name = attrs_path.pop() module_name = '.'.join(attrs_path) @@ -352,50 +345,50 @@ def _parse_attr(cls, value, package_dir=None): elif '' in package_dir: # A custom parent directory was specified for all root modules parent_path = os.path.join(os.getcwd(), package_dir['']) - if directive == attr_directive: + + fpath = os.path.join(parent_path, *module_name.split('.')) + if os.path.exists(fpath + '.py'): + fpath += '.py' + elif os.path.isdir(fpath): + fpath = os.path.join(fpath, '__init__.py') + else: + raise DistutilsOptionError('Could not find module ' + module_name) + with open(fpath, 'rb') as fp: + src = fp.read() + found = False + top_level = ast.parse(src) + for statement in top_level.body: + if isinstance(statement, ast.Assign): + for target in statement.targets: + if isinstance(target, ast.Name) \ + and target.id == attr_name: + try: + value = ast.literal_eval(statement.value) + except ValueError: + found = False + else: + found = True + elif isinstance(target, ast.Tuple) \ + and any(isinstance(t, ast.Name) and t.id == attr_name + for t in target.elts): + try: + stmnt_value = ast.literal_eval(statement.value) + except ValueError: + found = False + else: + for t, v in zip(target.elts, stmnt_value): + if isinstance(t, ast.Name) \ + and t.id == attr_name: + value = v + found = True + if not found: + # Fall back to extracting attribute via importing sys.path.insert(0, parent_path) try: module = import_module(module_name) value = getattr(module, attr_name) finally: sys.path = sys.path[1:] - - elif directive == literal_attr_directive: - fpath = os.path.join(parent_path, *module_name.split('.')) - if os.path.exists(fpath + '.py'): - fpath += '.py' - elif os.path.isdir(fpath): - fpath = os.path.join(fpath, '__init__.py') - else: - raise DistutilsOptionError( - 'Could not find module ' + module_name - ) - with open(fpath, 'rb') as fp: - src = fp.read() - found = False - top_level = ast.parse(src) - for statement in top_level.body: - if isinstance(statement, ast.Assign): - for target in statement.targets: - if isinstance(target, ast.Name) \ - and target.id == attr_name: - value = ast.literal_eval(statement.value) - found = True - elif isinstance(target, ast.Tuple) \ - and any(isinstance(t, ast.Name) and t.id==attr_name - for t in target.elts): - stmnt_value = ast.literal_eval(statement.value) - for t,v in zip(target.elts, stmnt_value): - if isinstance(t, ast.Name) \ - and t.id == attr_name: - value = v - found = True - if not found: - raise DistutilsOptionError( - 'No literal assignment to {!r} found in file' - .format(attr_name) - ) - return value @classmethod diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py index 03e6916bd9..d8347c78b9 100644 --- a/setuptools/tests/test_config.py +++ b/setuptools/tests/test_config.py @@ -103,7 +103,7 @@ def test_ignore_errors(self, tmpdir): 'version = attr: none.VERSION\n' 'keywords = one, two\n' ) - with pytest.raises(ImportError): + with pytest.raises(DistutilsOptionError): read_configuration('%s' % config) config_dict = read_configuration( @@ -300,25 +300,6 @@ def test_version(self, tmpdir): with get_dist(tmpdir) as dist: assert dist.metadata.version == '2016.11.26' - def test_literal_version(self, tmpdir): - - _, config = fake_env( - tmpdir, - '[metadata]\n' - 'version = literal_attr: fake_package.VERSION\n' - ) - with get_dist(tmpdir) as dist: - assert dist.metadata.version == '1.2.3' - - config.write( - '[metadata]\n' - 'version = literal_attr: fake_package.VERSION_MAJOR\n' - ) - with get_dist(tmpdir) as dist: - assert dist.metadata.version == '1' - - subpack = tmpdir.join('fake_package').mkdir('subpackage') - subpack.join('__init__.py').write('') subpack.join('submodule.py').write( 'import third_party_module\n' 'VERSION = (2016, 11, 26)' @@ -363,17 +344,6 @@ def test_version_with_package_dir_simple(self, tmpdir): with get_dist(tmpdir) as dist: assert dist.metadata.version == '1.2.3' - config.write( - '[metadata]\n' - 'version = literal_attr: fake_package_simple.VERSION\n' - '[options]\n' - 'package_dir =\n' - ' = src\n' - ) - - with get_dist(tmpdir) as dist: - assert dist.metadata.version == '1.2.3' - def test_version_with_package_dir_rename(self, tmpdir): _, config = fake_env( @@ -389,17 +359,6 @@ def test_version_with_package_dir_rename(self, tmpdir): with get_dist(tmpdir) as dist: assert dist.metadata.version == '1.2.3' - config.write( - '[metadata]\n' - 'version = literal_attr: fake_package_rename.VERSION\n' - '[options]\n' - 'package_dir =\n' - ' fake_package_rename = fake_dir\n' - ) - - with get_dist(tmpdir) as dist: - assert dist.metadata.version == '1.2.3' - def test_version_with_package_dir_complex(self, tmpdir): _, config = fake_env( @@ -415,17 +374,6 @@ def test_version_with_package_dir_complex(self, tmpdir): with get_dist(tmpdir) as dist: assert dist.metadata.version == '1.2.3' - config.write( - '[metadata]\n' - 'version = literal_attr: fake_package_complex.VERSION\n' - '[options]\n' - 'package_dir =\n' - ' fake_package_complex = src/fake_dir\n' - ) - - with get_dist(tmpdir) as dist: - assert dist.metadata.version == '1.2.3' - def test_unknown_meta_item(self, tmpdir): fake_env( From e1824c093bf89e8875ddd329f316b9ed3e7dd533 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 16 May 2020 19:54:13 -0400 Subject: [PATCH 05/12] Extract StaticModule and patch_path helpers. --- setuptools/config.py | 88 +++++++++++++++++---------------- setuptools/tests/test_config.py | 2 +- 2 files changed, 47 insertions(+), 43 deletions(-) diff --git a/setuptools/config.py b/setuptools/config.py index 0a2f51e212..cd1b115e16 100644 --- a/setuptools/config.py +++ b/setuptools/config.py @@ -10,6 +10,7 @@ from functools import partial from functools import wraps from importlib import import_module +import contextlib from distutils.errors import DistutilsOptionError, DistutilsFileError from setuptools.extern.packaging.version import LegacyVersion, parse @@ -20,6 +21,44 @@ __metaclass__ = type +class StaticModule: + """ + Attempt to load the module by the name + """ + def __init__(self, name): + spec = importlib.util.find_spec(module_name) + with open(spec.origin) as strm: + src = strm.read() + module = ast.parse(src) + vars(self).update(locals()) + del self.self + + def __getattr__(self, attr): + try: + return next( + ast.literal_eval(statement.value) + for statement in self.module.body + if isinstance(statement, ast.Assign) + for target in statement.targets + if isinstance(target, ast.Name) and target.id == attr + ) + except Exception: + raise AttributeError( + "{self.name} has no attribute {attr}".format(**locals())) + + +@contextlib.contextmanager +def patch_path(path): + """ + Add path to front of sys.path for the duration of the context. + """ + try: + sys.path.insert(0, path) + yield + finally: + sys.path.remove(path) + + def read_configuration( filepath, find_others=False, ignore_option_errors=False): """Read given configuration file and returns options from it as a dict. @@ -346,50 +385,15 @@ def _parse_attr(cls, value, package_dir=None): # A custom parent directory was specified for all root modules parent_path = os.path.join(os.getcwd(), package_dir['']) - fpath = os.path.join(parent_path, *module_name.split('.')) - if os.path.exists(fpath + '.py'): - fpath += '.py' - elif os.path.isdir(fpath): - fpath = os.path.join(fpath, '__init__.py') - else: - raise DistutilsOptionError('Could not find module ' + module_name) - with open(fpath, 'rb') as fp: - src = fp.read() - found = False - top_level = ast.parse(src) - for statement in top_level.body: - if isinstance(statement, ast.Assign): - for target in statement.targets: - if isinstance(target, ast.Name) \ - and target.id == attr_name: - try: - value = ast.literal_eval(statement.value) - except ValueError: - found = False - else: - found = True - elif isinstance(target, ast.Tuple) \ - and any(isinstance(t, ast.Name) and t.id == attr_name - for t in target.elts): - try: - stmnt_value = ast.literal_eval(statement.value) - except ValueError: - found = False - else: - for t, v in zip(target.elts, stmnt_value): - if isinstance(t, ast.Name) \ - and t.id == attr_name: - value = v - found = True - if not found: - # Fall back to extracting attribute via importing - sys.path.insert(0, parent_path) + with patch_path(parent_path): try: + # attempt to load value statically + return getattr(StaticModule(module_name), attr_name) + except Exception: + # fallback to simple import module = import_module(module_name) - value = getattr(module, attr_name) - finally: - sys.path = sys.path[1:] - return value + + return getattr(module, attr_name) @classmethod def _get_parser_compound(cls, *parse_methods): diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py index d8347c78b9..961f8d4217 100644 --- a/setuptools/tests/test_config.py +++ b/setuptools/tests/test_config.py @@ -103,7 +103,7 @@ def test_ignore_errors(self, tmpdir): 'version = attr: none.VERSION\n' 'keywords = one, two\n' ) - with pytest.raises(DistutilsOptionError): + with pytest.raises(ImportError): read_configuration('%s' % config) config_dict = read_configuration( From 43bbaa5827d38eede4ca8c837c9fc4994f9ab665 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 16 May 2020 20:13:05 -0400 Subject: [PATCH 06/12] Fix imports --- setuptools/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setuptools/config.py b/setuptools/config.py index cd1b115e16..b39ac7183d 100644 --- a/setuptools/config.py +++ b/setuptools/config.py @@ -6,10 +6,10 @@ import warnings import functools +import importlib from collections import defaultdict from functools import partial from functools import wraps -from importlib import import_module import contextlib from distutils.errors import DistutilsOptionError, DistutilsFileError @@ -391,7 +391,7 @@ def _parse_attr(cls, value, package_dir=None): return getattr(StaticModule(module_name), attr_name) except Exception: # fallback to simple import - module = import_module(module_name) + module = importlib.import_module(module_name) return getattr(module, attr_name) From a11c8eac4bf7e1b97f489395565d96076285617d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 16 May 2020 20:18:54 -0400 Subject: [PATCH 07/12] Alter test so it actually triggers the intended behavior. --- setuptools/tests/test_config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py index 961f8d4217..6840a8e283 100644 --- a/setuptools/tests/test_config.py +++ b/setuptools/tests/test_config.py @@ -300,14 +300,14 @@ def test_version(self, tmpdir): with get_dist(tmpdir) as dist: assert dist.metadata.version == '2016.11.26' - subpack.join('submodule.py').write( + subpack.join('othersub.py').write( 'import third_party_module\n' 'VERSION = (2016, 11, 26)' ) config.write( '[metadata]\n' - 'version = attr: fake_package.subpackage.submodule.VERSION\n' + 'version = attr: fake_package.subpackage.othersub.VERSION\n' ) with get_dist(tmpdir) as dist: assert dist.metadata.version == '2016.11.26' From af199e99549df07f9457567a26a0da8af069f513 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 16 May 2020 20:23:25 -0400 Subject: [PATCH 08/12] Fix error in StaticModule implementation. --- setuptools/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setuptools/config.py b/setuptools/config.py index b39ac7183d..45df2e3f2e 100644 --- a/setuptools/config.py +++ b/setuptools/config.py @@ -26,7 +26,7 @@ class StaticModule: Attempt to load the module by the name """ def __init__(self, name): - spec = importlib.util.find_spec(module_name) + spec = importlib.util.find_spec(name) with open(spec.origin) as strm: src = strm.read() module = ast.parse(src) From 7681ff9f70f33651f40c7b64a8186471a7014515 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 16 May 2020 20:57:15 -0400 Subject: [PATCH 09/12] Delete packages from sys.modules --- setuptools/tests/test_config.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py index 6840a8e283..942050e0d7 100644 --- a/setuptools/tests/test_config.py +++ b/setuptools/tests/test_config.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +import sys import contextlib + import pytest from distutils.errors import DistutilsOptionError, DistutilsFileError @@ -300,6 +302,9 @@ def test_version(self, tmpdir): with get_dist(tmpdir) as dist: assert dist.metadata.version == '2016.11.26' + del sys.modules['fake_package'] + del sys.modules['fake_package.subpackage'] + subpack.join('othersub.py').write( 'import third_party_module\n' 'VERSION = (2016, 11, 26)' From 4c62d634784a935eb0fbeedc174a25b82f05e1d6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 16 May 2020 21:21:30 -0400 Subject: [PATCH 10/12] Update test to create separate subpackages. Hoping that avoids issues with caching. --- setuptools/tests/test_config.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py index 942050e0d7..eeac32cecd 100644 --- a/setuptools/tests/test_config.py +++ b/setuptools/tests/test_config.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -import sys import contextlib import pytest @@ -291,28 +290,27 @@ def test_version(self, tmpdir): with get_dist(tmpdir) as dist: assert dist.metadata.version == '1' - subpack = tmpdir.join('fake_package').mkdir('subpackage') - subpack.join('__init__.py').write('') - subpack.join('submodule.py').write('VERSION = (2016, 11, 26)') + sub_a = tmpdir.join('fake_package').mkdir('subpkg_a') + sub_a.join('__init__.py').write('') + sub_a.join('mod.py').write('VERSION = (2016, 11, 26)') config.write( '[metadata]\n' - 'version = attr: fake_package.subpackage.submodule.VERSION\n' + 'version = attr: fake_package.subpkg_a.mod.VERSION\n' ) with get_dist(tmpdir) as dist: assert dist.metadata.version == '2016.11.26' - del sys.modules['fake_package'] - del sys.modules['fake_package.subpackage'] - - subpack.join('othersub.py').write( + sub_b = tmpdir.join('fake_package').mkdir('subpkg_b') + sub_b.join('__init__.py').write('') + sub_b.join('mod.py').write( 'import third_party_module\n' 'VERSION = (2016, 11, 26)' ) config.write( '[metadata]\n' - 'version = attr: fake_package.subpackage.othersub.VERSION\n' + 'version = attr: fake_package.subpkg_b.mod.VERSION\n' ) with get_dist(tmpdir) as dist: assert dist.metadata.version == '2016.11.26' From 55456fe32ab2b5d7a4d476149ba935dbfd6e5fca Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 16 May 2020 21:45:40 -0400 Subject: [PATCH 11/12] Try constructing the fake package at the beginning of the test. --- setuptools/tests/test_config.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py index eeac32cecd..77b853eba9 100644 --- a/setuptools/tests/test_config.py +++ b/setuptools/tests/test_config.py @@ -54,6 +54,7 @@ def fake_env( ' return [3, 4, 5, "dev"]\n' '\n' ) + return package_dir, config @@ -268,11 +269,23 @@ def test_dict(self, tmpdir): def test_version(self, tmpdir): - _, config = fake_env( + package_dir, config = fake_env( tmpdir, '[metadata]\n' 'version = attr: fake_package.VERSION\n' ) + + sub_a = package_dir.mkdir('subpkg_a') + sub_a.join('__init__.py').write('') + sub_a.join('mod.py').write('VERSION = (2016, 11, 26)') + + sub_b = package_dir.mkdir('subpkg_b') + sub_b.join('__init__.py').write('') + sub_b.join('mod.py').write( + 'import third_party_module\n' + 'VERSION = (2016, 11, 26)' + ) + with get_dist(tmpdir) as dist: assert dist.metadata.version == '1.2.3' @@ -290,10 +303,6 @@ def test_version(self, tmpdir): with get_dist(tmpdir) as dist: assert dist.metadata.version == '1' - sub_a = tmpdir.join('fake_package').mkdir('subpkg_a') - sub_a.join('__init__.py').write('') - sub_a.join('mod.py').write('VERSION = (2016, 11, 26)') - config.write( '[metadata]\n' 'version = attr: fake_package.subpkg_a.mod.VERSION\n' @@ -301,13 +310,6 @@ def test_version(self, tmpdir): with get_dist(tmpdir) as dist: assert dist.metadata.version == '2016.11.26' - sub_b = tmpdir.join('fake_package').mkdir('subpkg_b') - sub_b.join('__init__.py').write('') - sub_b.join('mod.py').write( - 'import third_party_module\n' - 'VERSION = (2016, 11, 26)' - ) - config.write( '[metadata]\n' 'version = attr: fake_package.subpkg_b.mod.VERSION\n' From 39a37c0758f43b130e5163156facffbbe89cf9fa Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 16 May 2020 22:18:25 -0400 Subject: [PATCH 12/12] Disable test on Python 2. --- changelog.d/1753.change.rst | 4 ++-- setuptools/tests/test_config.py | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/changelog.d/1753.change.rst b/changelog.d/1753.change.rst index c8b68026c2..08fa9ea499 100644 --- a/changelog.d/1753.change.rst +++ b/changelog.d/1753.change.rst @@ -1,4 +1,4 @@ ``attr:`` now extracts variables through rudimentary examination of the AST, -thereby supporting modules with third-party imports. If examining the AST +thereby supporting modules with third-party imports. If examining the AST fails to find the variable, ``attr:`` falls back to the old behavior of -importing the module. +importing the module. Works on Python 3 only. diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py index 77b853eba9..67992c041f 100644 --- a/setuptools/tests/test_config.py +++ b/setuptools/tests/test_config.py @@ -10,6 +10,7 @@ from setuptools.dist import Distribution, _Distribution from setuptools.config import ConfigHandler, read_configuration from setuptools.extern.six.moves import configparser +from setuptools.extern import six from . import py2_only, py3_only from .textwrap import DALS @@ -310,6 +311,10 @@ def test_version(self, tmpdir): with get_dist(tmpdir) as dist: assert dist.metadata.version == '2016.11.26' + if six.PY2: + # static version loading is unsupported on Python 2 + return + config.write( '[metadata]\n' 'version = attr: fake_package.subpkg_b.mod.VERSION\n'