[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
-------
---------------------
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
sphinx
jaraco.packaging>=6.1
rst.linker>=1.9
+pygments-github-lexers==0.0.5
+
+setuptools>=34
* 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:``, ``find:``, and ``find_namespace:`` directives in
order to cover common usecases.
* Unknown keys are ignored.
+setup.cfg-only projects
+=======================
+
+.. versionadded:: 40.9.0
+
+If ``setup.py`` is missing from the project directory when a :pep:`517`
+build is invoked, ``setuptools`` emulates a dummy ``setup.py`` file containing
+only a ``setuptools.setup()`` call.
+
+.. note::
+
+ :pep:`517` doesn't support editable installs so this is currently
+ incompatible with ``pip install -e .``, as :pep:`517` does not support editable installs.
+
+This means that you can have a Python project with all build configuration
+specified in ``setup.cfg``, without a ``setup.py`` file, if you **can rely
+on** your project always being built by a :pep:`517`/:pep:`518` compatible
+frontend.
+
+To use this feature:
+
+* Specify build requirements and :pep:`517` build backend in
+ ``pyproject.toml``.
+ For example:
+
+ .. code-block:: toml
+
+ [build-system]
+ requires = [
+ "setuptools >= 40.9.0",
+ "wheel",
+ ]
+ build-backend = "setuptools.build_meta"
+
+* Use a :pep:`517` compatible build frontend, such as ``pip >= 19`` or ``pep517``.
+
+ .. warning::
+
+ As :pep:`517` is new, support is not universal, and frontends that
+ do support it may still have bugs. For compatibility, you may want to
+ put a ``setup.py`` file containing only a ``setuptools.setup()``
+ invocation.
+
Using a ``src/`` layout
=======================
* ``attr:`` - Value is read from a module attribute. ``attr:`` supports
callables and iterables; unsupported types are cast using ``str()``.
+
+ 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
[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
sphinx
jaraco.packaging>=6.1
rst.linker>=1.9
+ pygments-github-lexers==0.0.5
from __future__ import absolute_import, unicode_literals
+import ast
import io
import os
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
__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.
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:]
+ 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 = importlib.import_module(module_name)
- return value
+ return getattr(module, attr_name)
@classmethod
def _get_parser_compound(cls, *parse_methods):
from __future__ import unicode_literals
import contextlib
+
import pytest
from distutils.errors import DistutilsOptionError, DistutilsFileError
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
' return [3, 4, 5, "dev"]\n'
'\n'
)
+
return package_dir, config
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'
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.subpkg_a.mod.VERSION\n'
+ )
+ 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.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'