diff options
-rw-r--r-- | .bumpversion.cfg | 2 | ||||
-rw-r--r-- | CHANGES.rst | 9 | ||||
-rw-r--r-- | changelog.d/1700.change.rst | 1 | ||||
-rw-r--r-- | changelog.d/1753.change.rst | 4 | ||||
-rw-r--r-- | docs/developer-guide.txt | 2 | ||||
-rw-r--r-- | docs/userguide/commands.txt | 4 | ||||
-rw-r--r-- | setup.cfg | 2 | ||||
-rw-r--r-- | setuptools/config.py | 92 | ||||
-rw-r--r-- | setuptools/tests/test_config.py | 34 |
9 files changed, 85 insertions, 65 deletions
diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 4ee92185..72d02f2b 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 46.3.1 +current_version = 46.4.0 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index fd3c16ba..ea667028 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,12 @@ +v46.4.0 +------- + +* #1753: ``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. Works on Python 3 only. + + v46.3.1 ------- diff --git a/changelog.d/1700.change.rst b/changelog.d/1700.change.rst new file mode 100644 index 00000000..f66046a2 --- /dev/null +++ b/changelog.d/1700.change.rst @@ -0,0 +1 @@ +Document all supported keywords by migrating the ones from distutils. diff --git a/changelog.d/1753.change.rst b/changelog.d/1753.change.rst deleted file mode 100644 index c8b68026..00000000 --- a/changelog.d/1753.change.rst +++ /dev/null @@ -1,4 +0,0 @@ -``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/developer-guide.txt b/docs/developer-guide.txt index 0b4ae4d4..e6171e4e 100644 --- a/docs/developer-guide.txt +++ b/docs/developer-guide.txt @@ -139,7 +139,7 @@ Vendored Dependencies --------------------- Setuptools has some dependencies, but due to `bootstrapping issues -<https://github.com/pypa/setuptools/issues/980>`, those dependencies +<https://github.com/pypa/setuptools/issues/980>`_, those dependencies cannot be declared as they won't be resolved soon enough to build setuptools from source. Eventually, this limitation may be lifted as PEP 517/518 reach ubiquitous adoption, but for now, Setuptools diff --git a/docs/userguide/commands.txt b/docs/userguide/commands.txt index 86048416..c64f62bf 100644 --- a/docs/userguide/commands.txt +++ b/docs/userguide/commands.txt @@ -521,7 +521,7 @@ result (which must be a ``unittest.TestSuite``) is added to the tests to be run. If the named suite is a package, any submodules and subpackages are recursively added to the overall test suite. (Note: if your project specifies a ``test_loader``, the rules for processing the chosen ``test_suite`` may -differ; see the `test_loader`_ documentation for more details.) +differ; see the :ref:`test_loader <test_loader>` documentation for more details.) Note that many test systems including ``doctest`` support wrapping their non-``unittest`` tests in ``TestSuite`` objects. So, if you are using a test @@ -563,4 +563,4 @@ The ``upload`` command was deprecated in version 40.0 and removed in version For more information on the current best practices in uploading your packages to PyPI, see the Python Packaging User Guide's "Packaging Python Projects" tutorial specifically the section on `uploading the distribution archives -<https://packaging.python.org/tutorials/packaging-projects/#uploading-the-distribution-archives>`_.
\ No newline at end of file +<https://packaging.python.org/tutorials/packaging-projects/#uploading-the-distribution-archives>`_. @@ -16,7 +16,7 @@ formats = zip [metadata] name = setuptools -version = 46.3.1 +version = 46.4.0 description = Easily download, build, install, upgrade, and uninstall Python packages author = Python Packaging Authority author_email = distutils-sig@python.org diff --git a/setuptools/config.py b/setuptools/config.py index 0a2f51e2..45df2e3f 100644 --- a/setuptools/config.py +++ b/setuptools/config.py @@ -6,10 +6,11 @@ import sys 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 from setuptools.extern.packaging.version import LegacyVersion, parse @@ -20,6 +21,44 @@ from setuptools.extern.six import string_types, PY3 __metaclass__ = type +class StaticModule: + """ + Attempt to load the module by the name + """ + def __init__(self, name): + spec = importlib.util.find_spec(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 @@ class ConfigHandler: # 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: - module = import_module(module_name) - value = getattr(module, attr_name) - finally: - sys.path = sys.path[1:] - return value + # attempt to load value statically + return getattr(StaticModule(module_name), attr_name) + except Exception: + # fallback to simple import + module = importlib.import_module(module_name) + + 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 d8347c78..67992c04 100644 --- a/setuptools/tests/test_config.py +++ b/setuptools/tests/test_config.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import contextlib + import pytest from distutils.errors import DistutilsOptionError, DistutilsFileError @@ -9,6 +10,7 @@ from mock import patch 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 @@ -53,6 +55,7 @@ def fake_env( ' return [3, 4, 5, "dev"]\n' '\n' ) + return package_dir, config @@ -103,7 +106,7 @@ class TestConfigurationReader: '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( @@ -267,11 +270,23 @@ class TestMetadata: 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' @@ -289,25 +304,20 @@ class TestMetadata: 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)') - 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' - subpack.join('submodule.py').write( - 'import third_party_module\n' - 'VERSION = (2016, 11, 26)' - ) + if six.PY2: + # static version loading is unsupported on Python 2 + return config.write( '[metadata]\n' - 'version = attr: fake_package.subpackage.submodule.VERSION\n' + 'version = attr: fake_package.subpkg_b.mod.VERSION\n' ) with get_dist(tmpdir) as dist: assert dist.metadata.version == '2016.11.26' |