Commit f852d159 authored by Jason R. Coombs's avatar Jason R. Coombs

Merge commit '52c394c1' into feature/2093-docs-revamp

parents 412d2e17 52c394c1
[bumpversion] [bumpversion]
current_version = 46.3.1 current_version = 46.4.0
commit = True commit = True
tag = True tag = True
......
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 v46.3.1
------- -------
......
Document all supported keywords by migrating the ones from distutils.
``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.
...@@ -139,7 +139,7 @@ Vendored Dependencies ...@@ -139,7 +139,7 @@ Vendored Dependencies
--------------------- ---------------------
Setuptools has some dependencies, but due to `bootstrapping issues 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 cannot be declared as they won't be resolved soon enough to build
setuptools from source. Eventually, this limitation may be lifted as setuptools from source. Eventually, this limitation may be lifted as
PEP 517/518 reach ubiquitous adoption, but for now, Setuptools PEP 517/518 reach ubiquitous adoption, but for now, Setuptools
......
...@@ -521,7 +521,7 @@ result (which must be a ``unittest.TestSuite``) is added to the tests to be ...@@ -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 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 recursively added to the overall test suite. (Note: if your project specifies
a ``test_loader``, the rules for processing the chosen ``test_suite`` may 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 Note that many test systems including ``doctest`` support wrapping their
non-``unittest`` tests in ``TestSuite`` objects. So, if you are using a test non-``unittest`` tests in ``TestSuite`` objects. So, if you are using a test
......
...@@ -16,7 +16,7 @@ formats = zip ...@@ -16,7 +16,7 @@ formats = zip
[metadata] [metadata]
name = setuptools name = setuptools
version = 46.3.1 version = 46.4.0
description = Easily download, build, install, upgrade, and uninstall Python packages description = Easily download, build, install, upgrade, and uninstall Python packages
author = Python Packaging Authority author = Python Packaging Authority
author_email = distutils-sig@python.org author_email = distutils-sig@python.org
......
...@@ -6,10 +6,11 @@ import sys ...@@ -6,10 +6,11 @@ import sys
import warnings import warnings
import functools import functools
import importlib
from collections import defaultdict from collections import defaultdict
from functools import partial from functools import partial
from functools import wraps from functools import wraps
from importlib import import_module import contextlib
from distutils.errors import DistutilsOptionError, DistutilsFileError from distutils.errors import DistutilsOptionError, DistutilsFileError
from setuptools.extern.packaging.version import LegacyVersion, parse from setuptools.extern.packaging.version import LegacyVersion, parse
...@@ -20,6 +21,44 @@ from setuptools.extern.six import string_types, PY3 ...@@ -20,6 +21,44 @@ from setuptools.extern.six import string_types, PY3
__metaclass__ = type __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( def read_configuration(
filepath, find_others=False, ignore_option_errors=False): filepath, find_others=False, ignore_option_errors=False):
"""Read given configuration file and returns options from it as a dict. """Read given configuration file and returns options from it as a dict.
...@@ -346,50 +385,15 @@ class ConfigHandler: ...@@ -346,50 +385,15 @@ class ConfigHandler:
# A custom parent directory was specified for all root modules # A custom parent directory was specified for all root modules
parent_path = os.path.join(os.getcwd(), package_dir['']) parent_path = os.path.join(os.getcwd(), package_dir[''])
fpath = os.path.join(parent_path, *module_name.split('.')) with patch_path(parent_path):
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: try:
value = ast.literal_eval(statement.value) # attempt to load value statically
except ValueError: return getattr(StaticModule(module_name), attr_name)
found = False except Exception:
else: # fallback to simple import
found = True module = importlib.import_module(module_name)
elif isinstance(target, ast.Tuple) \
and any(isinstance(t, ast.Name) and t.id == attr_name return getattr(module, 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:]
return value
@classmethod @classmethod
def _get_parser_compound(cls, *parse_methods): def _get_parser_compound(cls, *parse_methods):
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import contextlib import contextlib
import pytest import pytest
from distutils.errors import DistutilsOptionError, DistutilsFileError from distutils.errors import DistutilsOptionError, DistutilsFileError
...@@ -9,6 +10,7 @@ from mock import patch ...@@ -9,6 +10,7 @@ from mock import patch
from setuptools.dist import Distribution, _Distribution from setuptools.dist import Distribution, _Distribution
from setuptools.config import ConfigHandler, read_configuration from setuptools.config import ConfigHandler, read_configuration
from setuptools.extern.six.moves import configparser from setuptools.extern.six.moves import configparser
from setuptools.extern import six
from . import py2_only, py3_only from . import py2_only, py3_only
from .textwrap import DALS from .textwrap import DALS
...@@ -53,6 +55,7 @@ def fake_env( ...@@ -53,6 +55,7 @@ def fake_env(
' return [3, 4, 5, "dev"]\n' ' return [3, 4, 5, "dev"]\n'
'\n' '\n'
) )
return package_dir, config return package_dir, config
...@@ -103,7 +106,7 @@ class TestConfigurationReader: ...@@ -103,7 +106,7 @@ class TestConfigurationReader:
'version = attr: none.VERSION\n' 'version = attr: none.VERSION\n'
'keywords = one, two\n' 'keywords = one, two\n'
) )
with pytest.raises(DistutilsOptionError): with pytest.raises(ImportError):
read_configuration('%s' % config) read_configuration('%s' % config)
config_dict = read_configuration( config_dict = read_configuration(
...@@ -267,11 +270,23 @@ class TestMetadata: ...@@ -267,11 +270,23 @@ class TestMetadata:
def test_version(self, tmpdir): def test_version(self, tmpdir):
_, config = fake_env( package_dir, config = fake_env(
tmpdir, tmpdir,
'[metadata]\n' '[metadata]\n'
'version = attr: fake_package.VERSION\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: with get_dist(tmpdir) as dist:
assert dist.metadata.version == '1.2.3' assert dist.metadata.version == '1.2.3'
...@@ -289,25 +304,20 @@ class TestMetadata: ...@@ -289,25 +304,20 @@ class TestMetadata:
with get_dist(tmpdir) as dist: with get_dist(tmpdir) as dist:
assert dist.metadata.version == '1' 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( config.write(
'[metadata]\n' '[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: with get_dist(tmpdir) as dist:
assert dist.metadata.version == '2016.11.26' assert dist.metadata.version == '2016.11.26'
subpack.join('submodule.py').write( if six.PY2:
'import third_party_module\n' # static version loading is unsupported on Python 2
'VERSION = (2016, 11, 26)' return
)
config.write( config.write(
'[metadata]\n' '[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: with get_dist(tmpdir) as dist:
assert dist.metadata.version == '2016.11.26' assert dist.metadata.version == '2016.11.26'
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment