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

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

parents 412d2e17 52c394c1
[bumpversion]
current_version = 46.3.1
current_version = 46.4.0
commit = 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
-------
......
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
---------------------
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
......
......@@ -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
......
......@@ -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
......
......@@ -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:
with patch_path(parent_path):
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:]
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):
......
......@@ -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'
......
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