From 51213c79849a4543fe00ade676da143bd370ff0b Mon Sep 17 00:00:00 2001 From: DongHun Kwak Date: Wed, 30 Dec 2020 07:03:58 +0900 Subject: [PATCH] Imported Upstream version 40.9.0 --- .bumpversion.cfg | 7 +++ .travis.yml | 4 +- CHANGES.rst | 10 ++++ README.rst | 18 ++++--- appveyor.yml | 1 + docs/setuptools.txt | 36 ------------- pkg_resources/__init__.py | 60 ++++++++++++++++++++-- pkg_resources/tests/test_pkg_resources.py | 84 +++++++++++++++++++++++++++++++ setup.cfg | 9 +--- setup.py | 1 - setuptools/build_meta.py | 22 ++++++-- setuptools/dist.py | 3 +- setuptools/tests/test_build_meta.py | 74 ++++++++++++++++++++++++++- setuptools/tests/test_virtualenv.py | 47 ++++++++++++++++- tox.ini | 2 +- 15 files changed, 310 insertions(+), 68 deletions(-) create mode 100644 .bumpversion.cfg diff --git a/.bumpversion.cfg b/.bumpversion.cfg new file mode 100644 index 0000000..9840c08 --- /dev/null +++ b/.bumpversion.cfg @@ -0,0 +1,7 @@ +[bumpversion] +current_version = 40.9.0 +commit = True +tag = True + +[bumpversion:file:setup.cfg] + diff --git a/.travis.yml b/.travis.yml index d1febcc..a5b670e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,8 @@ jobs: python: 2.7 - <<: *latest_py2 env: LANG=C - - python: pypy + - python: pypy2.7-6.0.0 + dist: xenial env: DISABLE_COVERAGE=1 # Don't run coverage on pypy (too slow). - python: pypy3 env: DISABLE_COVERAGE=1 @@ -63,6 +64,7 @@ install: - "! grep pyc setuptools.egg-info/SOURCES.txt" script: + - export NETWORK_REQUIRED=1 - | ( # Run testsuite. if [ -z "$DISABLE_COVERAGE" ] diff --git a/CHANGES.rst b/CHANGES.rst index b043e44..2a8d432 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,13 @@ +v40.9.0 +------- + +* #1675: Added support for ``setup.cfg``-only projects when using the ``setuptools.build_meta`` backend. Projects that have enabled PEP 517 no longer need to have a ``setup.py`` and can use the purely declarative ``setup.cfg`` configuration file instead. +* #1720: Added support for ``pkg_resources.parse_requirements``-style requirements in ``setup_requires`` when ``setup.py`` is invoked from the ``setuptools.build_meta`` build backend. +* #1664: Added the path to the ``PKG-INFO`` or ``METADATA`` file in the exception + text when the ``Version:`` header can't be found. +* #1705: Removed some placeholder documentation sections referring to deprecated features. + + v40.8.0 ------- diff --git a/README.rst b/README.rst index 0454f2e..dac8a46 100644 --- a/README.rst +++ b/README.rst @@ -1,23 +1,23 @@ .. image:: https://img.shields.io/pypi/v/setuptools.svg :target: https://pypi.org/project/setuptools -.. image:: https://readthedocs.org/projects/setuptools/badge/?version=latest +.. image:: https://img.shields.io/readthedocs/setuptools/latest.svg :target: https://setuptools.readthedocs.io -.. image:: https://img.shields.io/travis/pypa/setuptools/master.svg?label=Linux%20build%20%40%20Travis%20CI +.. image:: https://img.shields.io/travis/pypa/setuptools/master.svg?label=Linux%20CI&logo=travis&logoColor=white :target: https://travis-ci.org/pypa/setuptools -.. image:: https://img.shields.io/appveyor/ci/pypa/setuptools/master.svg?label=Windows%20build%20%40%20Appveyor +.. image:: https://img.shields.io/appveyor/ci/pypa/setuptools/master.svg?label=Windows%20CI&logo=appveyor&logoColor=white :target: https://ci.appveyor.com/project/pypa/setuptools/branch/master -.. image:: https://img.shields.io/codecov/c/github/pypa/setuptools/master.svg +.. image:: https://img.shields.io/codecov/c/github/pypa/setuptools/master.svg?logo=codecov&logoColor=white :target: https://codecov.io/gh/pypa/setuptools -.. image:: https://img.shields.io/pypi/pyversions/setuptools.svg - -.. image:: https://tidelift.com/badges/github/pypa/setuptools +.. image:: https://tidelift.com/badges/github/pypa/setuptools?style=flat :target: https://tidelift.com/subscription/pkg/pypi-setuptools?utm_source=pypi-setuptools&utm_medium=readme +.. image:: https://img.shields.io/pypi/pyversions/setuptools.svg + See the `Installation Instructions `_ in the Python Packaging User's Guide for instructions on installing, upgrading, and uninstalling @@ -29,6 +29,10 @@ Bug reports and especially tested patches may be submitted directly to the `bug tracker `_. +To report a security vulnerability, please use the +`Tidelift security contact `_. +Tidelift will coordinate the fix and disclosure. + Code of Conduct --------------- diff --git a/appveyor.yml b/appveyor.yml index ef4a9f7..7a3d174 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,6 +3,7 @@ clone_depth: 50 environment: APPVEYOR: True + NETWORK_REQUIRED: True CODECOV_ENV: APPVEYOR_JOB_NAME matrix: diff --git a/docs/setuptools.txt b/docs/setuptools.txt index a4e05d7..64b385c 100644 --- a/docs/setuptools.txt +++ b/docs/setuptools.txt @@ -2679,42 +2679,6 @@ A few important points for writing revision control file finders: inform the user of the missing program(s). -Subclassing ``Command`` ------------------------ - -Sorry, this section isn't written yet, and neither is a lot of what's below -this point. - -XXX - - -Reusing ``setuptools`` Code -=========================== - -``ez_setup`` ------------- - -XXX - - -``setuptools.archive_util`` ---------------------------- - -XXX - - -``setuptools.sandbox`` ----------------------- - -XXX - - -``setuptools.package_index`` ----------------------------- - -XXX - - Mailing List and Bug Tracker ============================ diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index dcfa1d0..97e08d6 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -1403,8 +1403,15 @@ class NullProvider: def has_resource(self, resource_name): return self._has(self._fn(self.module_path, resource_name)) + def _get_metadata_path(self, name): + return self._fn(self.egg_info, name) + def has_metadata(self, name): - return self.egg_info and self._has(self._fn(self.egg_info, name)) + if not self.egg_info: + return self.egg_info + + path = self._get_metadata_path(name) + return self._has(path) def get_metadata(self, name): if not self.egg_info: @@ -1512,6 +1519,19 @@ is not allowed. ... ValueError: Use of .. or absolute path in a resource path \ is not allowed. + + Blank values are allowed + + >>> vrp('') + >>> bool(warned) + False + + Non-string values are not. + + >>> vrp(None) + Traceback (most recent call last): + ... + AttributeError: ... """ invalid = ( os.path.pardir in path.split(posixpath.sep) or @@ -1855,6 +1875,9 @@ class FileMetadata(EmptyProvider): def __init__(self, path): self.path = path + def _get_metadata_path(self, name): + return self.path + def has_metadata(self, name): return name == 'PKG-INFO' and os.path.isfile(self.path) @@ -2648,10 +2671,14 @@ class Distribution: try: return self._version except AttributeError: - version = _version_from_file(self._get_metadata(self.PKG_INFO)) + version = self._get_version() if version is None: - tmpl = "Missing 'Version:' header and/or %s file" - raise ValueError(tmpl % self.PKG_INFO, self) + path = self._get_metadata_path_for_display(self.PKG_INFO) + msg = ( + "Missing 'Version:' header and/or {} file at path: {}" + ).format(self.PKG_INFO, path) + raise ValueError(msg, self) + return version @property @@ -2709,11 +2736,34 @@ class Distribution: ) return deps + def _get_metadata_path_for_display(self, name): + """ + Return the path to the given metadata file, if available. + """ + try: + # We need to access _get_metadata_path() on the provider object + # directly rather than through this class's __getattr__() + # since _get_metadata_path() is marked private. + path = self._provider._get_metadata_path(name) + + # Handle exceptions e.g. in case the distribution's metadata + # provider doesn't support _get_metadata_path(). + except Exception: + return '[could not detect]' + + return path + def _get_metadata(self, name): if self.has_metadata(name): for line in self.get_metadata_lines(name): yield line + def _get_version(self): + lines = self._get_metadata(self.PKG_INFO) + version = _version_from_file(lines) + + return version + def activate(self, path=None, replace=False): """Ensure distribution is importable on `path` (default=sys.path)""" if path is None: @@ -2932,7 +2982,7 @@ class EggInfoDistribution(Distribution): take an extra step and try to get the version number from the metadata file itself instead of the filename. """ - md_version = _version_from_file(self._get_metadata(self.PKG_INFO)) + md_version = self._get_version() if md_version: self._version = md_version return self diff --git a/pkg_resources/tests/test_pkg_resources.py b/pkg_resources/tests/test_pkg_resources.py index 2c2c9c7..fb77c68 100644 --- a/pkg_resources/tests/test_pkg_resources.py +++ b/pkg_resources/tests/test_pkg_resources.py @@ -17,6 +17,7 @@ try: except ImportError: import mock +from pkg_resources import DistInfoDistribution, Distribution, EggInfoDistribution from pkg_resources.extern.six.moves import map from pkg_resources.extern.six import text_type, string_types @@ -190,6 +191,89 @@ class TestResourceManager: subprocess.check_call(cmd) +# TODO: remove this in favor of Path.touch() when Python 2 is dropped. +def touch_file(path): + """ + Create an empty file. + """ + with open(path, 'w'): + pass + + +def make_distribution_no_version(tmpdir, basename): + """ + Create a distribution directory with no file containing the version. + """ + # Convert the LocalPath object to a string before joining. + dist_dir = os.path.join(str(tmpdir), basename) + os.mkdir(dist_dir) + # Make the directory non-empty so distributions_from_metadata() + # will detect it and yield it. + touch_file(os.path.join(dist_dir, 'temp.txt')) + + dists = list(pkg_resources.distributions_from_metadata(dist_dir)) + assert len(dists) == 1 + dist, = dists + + return dist, dist_dir + + +@pytest.mark.parametrize( + 'suffix, expected_filename, expected_dist_type', + [ + ('egg-info', 'PKG-INFO', EggInfoDistribution), + ('dist-info', 'METADATA', DistInfoDistribution), + ], +) +def test_distribution_version_missing(tmpdir, suffix, expected_filename, + expected_dist_type): + """ + Test Distribution.version when the "Version" header is missing. + """ + basename = 'foo.{}'.format(suffix) + dist, dist_dir = make_distribution_no_version(tmpdir, basename) + + expected_text = ( + "Missing 'Version:' header and/or {} file at path: " + ).format(expected_filename) + metadata_path = os.path.join(dist_dir, expected_filename) + + # Now check the exception raised when the "version" attribute is accessed. + with pytest.raises(ValueError) as excinfo: + dist.version + + err = str(excinfo) + # Include a string expression after the assert so the full strings + # will be visible for inspection on failure. + assert expected_text in err, str((expected_text, err)) + + # Also check the args passed to the ValueError. + msg, dist = excinfo.value.args + assert expected_text in msg + # Check that the message portion contains the path. + assert metadata_path in msg, str((metadata_path, msg)) + assert type(dist) == expected_dist_type + + +def test_distribution_version_missing_undetected_path(): + """ + Test Distribution.version when the "Version" header is missing and + the path can't be detected. + """ + # Create a Distribution object with no metadata argument, which results + # in an empty metadata provider. + dist = Distribution('/foo') + with pytest.raises(ValueError) as excinfo: + dist.version + + msg, dist = excinfo.value.args + expected = ( + "Missing 'Version:' header and/or PKG-INFO file at path: " + '[could not detect]' + ) + assert msg == expected + + class TestDeepVersionLookupDistutils: @pytest.fixture def env(self, tmpdir): diff --git a/setup.cfg b/setup.cfg index f0932e1..01fe6ae 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,8 +1,3 @@ -[bumpversion] -current_version = 40.8.0 -commit = True -tag = True - [egg_info] tag_build = .post tag_date = 1 @@ -24,6 +19,4 @@ universal = 1 [metadata] license_file = LICENSE - -[bumpversion:file:setup.py] - +version = 40.9.0 diff --git a/setup.py b/setup.py index 8ec7ce0..78d7018 100755 --- a/setup.py +++ b/setup.py @@ -89,7 +89,6 @@ def pypi_link(pkg_filename): setup_params = dict( name="setuptools", - version="40.8.0", description=( "Easily download, build, install, upgrade, and uninstall " "Python packages" diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index 70b7ab2..47cbcbf 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -26,6 +26,7 @@ bug reports or API stability): Again, this is not a formal definition! Just a "taste" of the module. """ +import io import os import sys import tokenize @@ -35,6 +36,8 @@ import contextlib import setuptools import distutils +from pkg_resources import parse_requirements + __all__ = ['get_requires_for_build_sdist', 'get_requires_for_build_wheel', 'prepare_metadata_for_build_wheel', @@ -50,7 +53,9 @@ class SetupRequirementsError(BaseException): class Distribution(setuptools.dist.Distribution): def fetch_build_eggs(self, specifiers): - raise SetupRequirementsError(specifiers) + specifier_list = list(map(str, parse_requirements(specifiers))) + + raise SetupRequirementsError(specifier_list) @classmethod @contextlib.contextmanager @@ -95,6 +100,14 @@ def _file_with_extension(directory, extension): return file +def _open_setup_script(setup_script): + if not os.path.exists(setup_script): + # Supply a default setup.py + return io.StringIO(u"from setuptools import setup; setup()") + + return getattr(tokenize, 'open', open)(setup_script) + + class _BuildMetaBackend(object): def _fix_config(self, config_settings): @@ -120,9 +133,10 @@ class _BuildMetaBackend(object): # Correctness comes first, then optimization later __file__ = setup_script __name__ = '__main__' - f = getattr(tokenize, 'open', open)(__file__) - code = f.read().replace('\\r\\n', '\\n') - f.close() + + with _open_setup_script(__file__) as f: + code = f.read().replace(r'\r\n', r'\n') + exec(compile(code, __file__, 'exec'), locals()) def get_requires_for_build_wheel(self, config_settings=None): diff --git a/setuptools/dist.py b/setuptools/dist.py index ddb1787..e6d08b9 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -885,7 +885,7 @@ class Distribution(_Distribution): def include(self, **attrs): """Add items to distribution that are named in keyword arguments - For example, 'dist.exclude(py_modules=["x"])' would add 'x' to + For example, 'dist.include(py_modules=["x"])' would add 'x' to the distribution's 'py_modules' attribute, if it was not already there. @@ -1103,7 +1103,6 @@ class Distribution(_Distribution): return _Distribution.handle_display_options(self, option_order) # Stdout may be StringIO (e.g. in tests) - import io if not isinstance(sys.stdout, io.TextIOWrapper): return _Distribution.handle_display_options(self, option_order) diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index 6236b9f..0bdea2d 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -110,6 +110,21 @@ defns = [ print('hello') """), }, + { + 'setup.cfg': DALS(""" + [metadata] + name = foo + version='0.0.0' + + [options] + py_modules=hello + setup_requires=six + """), + 'hello.py': DALS(""" + def run(): + print('hello') + """) + }, ] @@ -183,9 +198,13 @@ class TestBuildMetaBackend: # if the setup.py changes subsequent call of the build meta # should still succeed, given the # sdist_directory the frontend specifies is empty - with open(os.path.abspath("setup.py"), 'rt') as file_handler: + setup_loc = os.path.abspath("setup.py") + if not os.path.exists(setup_loc): + setup_loc = os.path.abspath("setup.cfg") + + with open(setup_loc, 'rt') as file_handler: content = file_handler.read() - with open(os.path.abspath("setup.py"), 'wt') as file_handler: + with open(setup_loc, 'wt') as file_handler: file_handler.write( content.replace("version='0.0.0'", "version='0.0.1'")) @@ -268,6 +287,57 @@ class TestBuildMetaBackend: with pytest.raises(ImportError): build_backend.build_sdist("temp") + @pytest.mark.parametrize('setup_literal, requirements', [ + ("'foo'", ['foo']), + ("['foo']", ['foo']), + (r"'foo\n'", ['foo']), + (r"'foo\n\n'", ['foo']), + ("['foo', 'bar']", ['foo', 'bar']), + (r"'# Has a comment line\nfoo'", ['foo']), + (r"'foo # Has an inline comment'", ['foo']), + (r"'foo \\\n >=3.0'", ['foo>=3.0']), + (r"'foo\nbar'", ['foo', 'bar']), + (r"'foo\nbar\n'", ['foo', 'bar']), + (r"['foo\n', 'bar\n']", ['foo', 'bar']), + ]) + @pytest.mark.parametrize('use_wheel', [True, False]) + def test_setup_requires(self, setup_literal, requirements, use_wheel, + tmpdir_cwd): + + files = { + 'setup.py': DALS(""" + from setuptools import setup + + setup( + name="qux", + version="0.0.0", + py_modules=["hello.py"], + setup_requires={setup_literal}, + ) + """).format(setup_literal=setup_literal), + 'hello.py': DALS(""" + def run(): + print('hello') + """), + } + + build_files(files) + + build_backend = self.get_build_backend() + + if use_wheel: + base_requirements = ['wheel'] + get_requires = build_backend.get_requires_for_build_wheel + else: + base_requirements = [] + get_requires = build_backend.get_requires_for_build_sdist + + # Ensure that the build requirements are properly parsed + expected = sorted(base_requirements + requirements) + actual = get_requires() + + assert expected == sorted(actual) + class TestBuildMetaLegacyBackend(TestBuildMetaBackend): backend_name = 'setuptools.build_meta:__legacy__' diff --git a/setuptools/tests/test_virtualenv.py b/setuptools/tests/test_virtualenv.py index 3d5c84b..d7b98c7 100644 --- a/setuptools/tests/test_virtualenv.py +++ b/setuptools/tests/test_virtualenv.py @@ -52,10 +52,55 @@ def test_clean_env_install(bare_virtualenv): )).format(source=SOURCE_DIR)) -def test_pip_upgrade_from_source(virtualenv): +def _get_pip_versions(): + # This fixture will attempt to detect if tests are being run without + # network connectivity and if so skip some tests + + network = True + if not os.environ.get('NETWORK_REQUIRED', False): # pragma: nocover + try: + from urllib.request import urlopen + from urllib.error import URLError + except ImportError: + from urllib2 import urlopen, URLError # Python 2.7 compat + + try: + urlopen('https://pypi.org', timeout=1) + except URLError: + # No network, disable most of these tests + network = False + + network_versions = [ + 'pip==9.0.3', + 'pip==10.0.1', + 'pip==18.1', + 'pip==19.0.1', + 'https://github.com/pypa/pip/archive/master.zip', + ] + + versions = [None] + [ + pytest.param(v, **({} if network else {'marks': pytest.mark.skip})) + for v in network_versions + ] + + return versions + + +@pytest.mark.parametrize('pip_version', _get_pip_versions()) +def test_pip_upgrade_from_source(pip_version, virtualenv): """ Check pip can upgrade setuptools from source. """ + # Install pip/wheel, and remove setuptools (as it + # should not be needed for bootstraping from source) + if pip_version is None: + upgrade_pip = () + else: + upgrade_pip = ('python -m pip install -U {pip_version} --retries=1',) + virtualenv.run(' && '.join(( + 'pip uninstall -y setuptools', + 'pip install -U wheel', + ) + upgrade_pip).format(pip_version=pip_version)) dist_dir = virtualenv.workspace # Generate source distribution / wheel. virtualenv.run(' && '.join(( diff --git a/tox.ini b/tox.ini index a31cb1c..bb940ac 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,7 @@ list_dependencies_command={envbindir}/pip freeze setenv=COVERAGE_FILE={toxworkdir}/.coverage.{envname} # TODO: The passed environment variables came from copying other tox.ini files # These should probably be individually annotated to explain what needs them. -passenv=APPDATA HOMEDRIVE HOMEPATH windir APPVEYOR APPVEYOR_* CI CODECOV_* TRAVIS TRAVIS_* +passenv=APPDATA HOMEDRIVE HOMEPATH windir APPVEYOR APPVEYOR_* CI CODECOV_* TRAVIS TRAVIS_* NETWORK_REQUIRED commands=pytest --cov-config={toxinidir}/tox.ini --cov-report= {posargs} usedevelop=True -- 2.7.4