From 35e1865f3050fef92d2a9f4ba0fc5894d359bd80 Mon Sep 17 00:00:00 2001 From: DongHun Kwak Date: Wed, 30 Dec 2020 07:06:13 +0900 Subject: [PATCH] Imported Upstream version 46.4.0 --- .bumpversion.cfg | 2 +- CHANGES.rst | 9 ++++++ docs/developer-guide.txt | 2 +- docs/requirements.txt | 3 ++ docs/setuptools.txt | 51 ++++++++++++++++++++++++++++- setup.cfg | 3 +- setuptools/config.py | 57 ++++++++++++++++++++++++++++----- setuptools/tests/test_config.py | 32 +++++++++++++++--- 8 files changed, 142 insertions(+), 17 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 4ee9218..72d02f2 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 fd3c16b..ea66702 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/docs/developer-guide.txt b/docs/developer-guide.txt index 0b4ae4d..e6171e4 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 -`, those dependencies +`_, 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/requirements.txt b/docs/requirements.txt index 6c35bf6..104d68f 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -2,3 +2,6 @@ sphinx jaraco.packaging>=6.1 rst.linker>=1.9 +pygments-github-lexers==0.0.5 + +setuptools>=34 diff --git a/docs/setuptools.txt b/docs/setuptools.txt index 30a30c2..c37b7ec 100644 --- a/docs/setuptools.txt +++ b/docs/setuptools.txt @@ -2193,11 +2193,54 @@ 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:``, ``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 ======================= @@ -2247,6 +2290,12 @@ Special directives: * ``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 diff --git a/setup.cfg b/setup.cfg index 467d1fa..72d4dce 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 @@ -78,3 +78,4 @@ docs = sphinx jaraco.packaging>=6.1 rst.linker>=1.9 + pygments-github-lexers==0.0.5 diff --git a/setuptools/config.py b/setuptools/config.py index 9b9a0c4..45df2e3 100644 --- a/setuptools/config.py +++ b/setuptools/config.py @@ -1,14 +1,16 @@ 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 @@ -19,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. @@ -344,15 +384,16 @@ class ConfigHandler: 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): diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py index 2fa0b37..67992c0 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 @@ -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,13 +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.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' -- 2.34.1