From b9fa500c8b1229dbfe9a254a2032b9a487041a45 Mon Sep 17 00:00:00 2001 From: DongHun Kwak Date: Mon, 14 Jan 2019 10:41:31 +0900 Subject: [PATCH] Imported Upstream version 38.6.1 --- .travis.yml | 2 +- CHANGES.rst | 49 ++++++++++++++ appveyor.yml | 5 ++ docs/developer-guide.txt | 3 - docs/pkg_resources.txt | 2 +- docs/setuptools.txt | 29 ++++++-- pkg_resources/__init__.py | 94 +++++++++++++++++++------- pkg_resources/tests/test_find_distributions.py | 1 + pkg_resources/tests/test_pkg_resources.py | 6 +- pkg_resources/tests/test_resources.py | 41 +++++++---- pkg_resources/tests/test_working_set.py | 10 ++- setup.cfg | 2 +- setup.py | 5 +- setuptools/command/bdist_egg.py | 14 ++-- setuptools/command/build_ext.py | 3 + setuptools/command/easy_install.py | 17 ++--- setuptools/command/egg_info.py | 5 +- setuptools/config.py | 4 +- setuptools/dist.py | 49 +++++++++++--- setuptools/tests/test_config.py | 19 ++++++ setuptools/tests/test_easy_install.py | 45 ++++++++++++ setuptools/tests/test_egg_info.py | 66 ++++++++++++++++++ setuptools/tests/test_sandbox.py | 2 + setuptools/tests/test_virtualenv.py | 19 ++++++ tests/requirements.txt | 2 +- 25 files changed, 413 insertions(+), 81 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1fd73aa..ffbd72a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -40,7 +40,7 @@ cache: pip install: # need tox to get started -- pip install tox +- pip install tox 'tox-venv; python_version!="3.3"' # Output the env, to verify behavior - env diff --git a/CHANGES.rst b/CHANGES.rst index 7227b7f..01fb448 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,52 @@ +v38.6.1 +------- + +* #1292: Avoid generating ``Provides-Extra`` in metadata when + no extra is present (but environment markers are). + +v38.6.0 +------- + +* #1286: Add support for Metadata 2.1 (PEP 566). + +v38.5.2 +------- + +* #1285: Fixed RuntimeError in pkg_resources.parse_requirements + on Python 3.7 (stemming from PEP 479). + +v38.5.1 +------- + +* #1271: Revert to Cython legacy ``build_ext`` behavior for + compatibility. + +v38.5.0 +------- + +* #1229: Expand imports in ``build_ext`` to refine detection of + Cython availability. + +* #1270: When Cython is available, ``build_ext`` now uses the + new_build_ext. + +v38.4.1 +------- + +* #1257: In bdist_egg.scan_module, fix ValueError on Python 3.7. + +v38.4.0 +------- + +* #1231: Removed warning when PYTHONDONTWRITEBYTECODE is enabled. + +v38.3.0 +------- + +* #1210: Add support for PEP 345 Project-URL metadata. +* #1207: Add support for ``long_description_type`` to setup.cfg + declarative config as intended and documented. + v38.2.5 ------- diff --git a/appveyor.yml b/appveyor.yml index 9313a48..7c61455 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -14,7 +14,12 @@ install: build: off +cache: + - '%LOCALAPPDATA%\pip\Cache' + test_script: - "python bootstrap.py" - "python -m pip install tox" - "tox" + +version: '{build}' diff --git a/docs/developer-guide.txt b/docs/developer-guide.txt index 8a13638..b2c1a0c 100644 --- a/docs/developer-guide.txt +++ b/docs/developer-guide.txt @@ -82,9 +82,6 @@ branches or multiple forks to maintain separate efforts. The Continuous Integration tests that validate every release are run from this repository. -For posterity, the old `Bitbucket mirror -`_ is available. - ------- Testing ------- diff --git a/docs/pkg_resources.txt b/docs/pkg_resources.txt index 8d337cb..b40a209 100644 --- a/docs/pkg_resources.txt +++ b/docs/pkg_resources.txt @@ -638,7 +638,7 @@ Requirements Parsing sorted into ascending version order, and used to establish what ranges of versions are acceptable. Adjacent redundant conditions are effectively consolidated (e.g. ``">1, >2"`` produces the same results as ``">2"``, and - ``"<2,<3"`` produces the same results as``"<2"``). ``"!="`` versions are + ``"<2,<3"`` produces the same results as ``"<2"``). ``"!="`` versions are excised from the ranges they fall within. The version being tested for acceptability is then checked for membership in the resulting ranges. diff --git a/docs/setuptools.txt b/docs/setuptools.txt index c2822c4..65080a0 100644 --- a/docs/setuptools.txt +++ b/docs/setuptools.txt @@ -145,6 +145,11 @@ dependencies, and perhaps some data files and scripts:: license="PSF", keywords="hello world example examples", url="http://example.com/HelloWorld/", # project home page, if any + project_urls={ + "Bug Tracker": "https://bugs.example.com/HelloWorld/", + "Documentation": "https://docs.example.com/HelloWorld/", + "Source Code": "https://code.example.com/HelloWorld/", + } # could also include long_description, download_url, classifiers, etc. ) @@ -408,6 +413,11 @@ unless you need the associated ``setuptools`` feature. A list of modules to search for additional fixers to be used during the 2to3 conversion. See :doc:`python3` for more details. +``project_urls`` + An arbitrary map of URL names to hyperlinks, allowing more extensible + documentation of where various resources can be found than the simple + ``url`` and ``download_url`` options provide. + Using ``find_packages()`` ------------------------- @@ -427,9 +437,9 @@ such projects also need something like ``package_dir={'':'src'}`` in their ``setup()`` arguments, but that's just a normal distutils thing.) Anyway, ``find_packages()`` walks the target directory, filtering by inclusion -patterns, and finds Python packages (any directory). On Python 3.2 and -earlier, packages are only recognized if they include an ``__init__.py`` file. -Finally, exclusion patterns are applied to remove matching packages. +patterns, and finds Python packages (any directory). Packages are only +recognized if they include an ``__init__.py`` file. Finally, exclusion +patterns are applied to remove matching packages. Inclusion and exclusion patterns are package names, optionally including wildcards. For @@ -561,7 +571,7 @@ project name or version identifier must be replaced with ``-``. Version specifiers for a given project are internally sorted into ascending version order, and used to establish what ranges of versions are acceptable. Adjacent redundant conditions are also consolidated (e.g. ``">1, >2"`` becomes -``">1"``, and ``"<2,<3"`` becomes ``"<3"``). ``"!="`` versions are excised from +``">2"``, and ``"<2,<3"`` becomes ``"<2"``). ``"!="`` versions are excised from the ranges they fall within. A project's version is then checked for membership in the resulting ranges. (Note that providing conflicting conditions for the same version (e.g. "<2,>=2" or "==2,!=2") is meaningless and may @@ -875,6 +885,14 @@ Also notice that if you use paths, you *must* use a forward slash (``/``) as the path separator, even if you are on Windows. Setuptools automatically converts slashes to appropriate platform-specific separators at build time. +If datafiles are contained in a subdirectory of a package that isn't a package +itself (no ``__init__.py``), then the subdirectory names (or ``*``) are required +in the ``package_data`` argument (as shown above with ``'data/*.dat'``). + +When building an ``sdist``, the datafiles are also drawn from the +``package_name.egg-info/SOURCES.txt`` file, so make sure that this is removed if +the ``setup.py`` ``package_data`` list is updated before calling ``setup.py``. + (Note: although the ``package_data`` argument was previously only available in ``setuptools``, it was also added to the Python ``distutils`` package as of Python 2.4; there is `some documentation for the feature`__ available on the @@ -916,7 +934,7 @@ In summary, the three options allow you to: Accept all data files and directories matched by ``MANIFEST.in``. ``package_data`` - Specify additional patterns to match files and directories that may or may + Specify additional patterns to match files that may or may not be matched by ``MANIFEST.in`` or found in source control. ``exclude_package_data`` @@ -2406,6 +2424,7 @@ name str version attr:, str url home-page str download_url download-url str +project_urls dict author str author_email author-email str maintainer str diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index 08f9bbe..92272f2 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -90,6 +90,21 @@ if six.PY2: # satisfy the linters. require = None working_set = None +add_activation_listener = None +resources_stream = None +cleanup_resources = None +resource_dir = None +resource_stream = None +set_extraction_path = None +resource_isdir = None +resource_string = None +iter_entry_points = None +resource_listdir = None +resource_filename = None +resource_exists = None +_distribution_finders = None +_namespace_handlers = None +_namespace_packages = None class PEP440Warning(RuntimeWarning): @@ -1074,9 +1089,12 @@ class Environment(object): requirements specified when this environment was created, or False is returned. """ - return (self.python is None or dist.py_version is None - or dist.py_version == self.python) \ - and compatible_platforms(dist.platform, self.platform) + py_compat = ( + self.python is None + or dist.py_version is None + or dist.py_version == self.python + ) + return py_compat and compatible_platforms(dist.platform, self.platform) def remove(self, dist): """Remove `dist` from the environment""" @@ -1289,7 +1307,7 @@ class ResourceManager: target_path = os.path.join(extract_path, archive_name + '-tmp', *names) try: _bypass_ensure_directory(target_path) - except: + except Exception: self.extraction_error() self._warn_unsafe_extraction_path(extract_path) @@ -1621,11 +1639,16 @@ DefaultProvider._register() class EmptyProvider(NullProvider): """Provider that returns nothing for all requests""" - _isdir = _has = lambda self, path: False - _get = lambda self, path: '' - _listdir = lambda self, path: [] module_path = None + _isdir = _has = lambda self, path: False + + def _get(self, path): + return '' + + def _listdir(self, path): + return [] + def __init__(self): pass @@ -2515,7 +2538,8 @@ def _version_from_file(lines): Given an iterable of lines from a Metadata file, return the value of the Version field, if present, or None otherwise. """ - is_version_line = lambda line: line.lower().startswith('version:') + def is_version_line(line): + return line.lower().startswith('version:') version_lines = filter(is_version_line, lines) line = next(iter(version_lines), '') _, _, value = line.partition(':') @@ -2652,23 +2676,44 @@ class Distribution(object): @property def _dep_map(self): + """ + A map of extra to its list of (direct) requirements + for this distribution, including the null extra. + """ try: return self.__dep_map except AttributeError: - dm = self.__dep_map = {None: []} - for name in 'requires.txt', 'depends.txt': - for extra, reqs in split_sections(self._get_metadata(name)): - if extra: - if ':' in extra: - extra, marker = extra.split(':', 1) - if invalid_marker(marker): - # XXX warn - reqs = [] - elif not evaluate_marker(marker): - reqs = [] - extra = safe_extra(extra) or None - dm.setdefault(extra, []).extend(parse_requirements(reqs)) - return dm + self.__dep_map = self._filter_extras(self._build_dep_map()) + return self.__dep_map + + @staticmethod + def _filter_extras(dm): + """ + Given a mapping of extras to dependencies, strip off + environment markers and filter out any dependencies + not matching the markers. + """ + for extra in list(filter(None, dm)): + new_extra = extra + reqs = dm.pop(extra) + new_extra, _, marker = extra.partition(':') + fails_marker = marker and ( + invalid_marker(marker) + or not evaluate_marker(marker) + ) + if fails_marker: + reqs = [] + new_extra = safe_extra(new_extra) or None + + dm.setdefault(new_extra, []).extend(reqs) + return dm + + def _build_dep_map(self): + dm = {} + for name in 'requires.txt', 'depends.txt': + for extra, reqs in split_sections(self._get_metadata(name)): + dm.setdefault(extra, []).extend(parse_requirements(reqs)) + return dm def requires(self, extras=()): """List of Requirements needed for this distro if `extras` are used""" @@ -2990,7 +3035,10 @@ def parse_requirements(strs): # If there is a line continuation, drop it, and append the next line. if line.endswith('\\'): line = line[:-2].strip() - line += next(lines) + try: + line += next(lines) + except StopIteration: + return yield Requirement(line) diff --git a/pkg_resources/tests/test_find_distributions.py b/pkg_resources/tests/test_find_distributions.py index 97999b3..d735c59 100644 --- a/pkg_resources/tests/test_find_distributions.py +++ b/pkg_resources/tests/test_find_distributions.py @@ -13,6 +13,7 @@ setuptools.setup( ) """.lstrip() + class TestFindDistributions: @pytest.fixture diff --git a/pkg_resources/tests/test_pkg_resources.py b/pkg_resources/tests/test_pkg_resources.py index f2c00b2..7442b79 100644 --- a/pkg_resources/tests/test_pkg_resources.py +++ b/pkg_resources/tests/test_pkg_resources.py @@ -154,8 +154,10 @@ class TestIndependence: lines = ( 'import pkg_resources', 'import sys', - 'assert "setuptools" not in sys.modules, ' - '"setuptools was imported"', + ( + 'assert "setuptools" not in sys.modules, ' + '"setuptools was imported"' + ), ) cmd = [sys.executable, '-c', '; '.join(lines)] subprocess.check_call(cmd) diff --git a/pkg_resources/tests/test_resources.py b/pkg_resources/tests/test_resources.py index dcd2f42..0543821 100644 --- a/pkg_resources/tests/test_resources.py +++ b/pkg_resources/tests/test_resources.py @@ -11,7 +11,8 @@ import pytest from pkg_resources.extern import packaging import pkg_resources -from pkg_resources import (parse_requirements, VersionConflict, parse_version, +from pkg_resources import ( + parse_requirements, VersionConflict, parse_version, Distribution, EntryPoint, Requirement, safe_version, safe_name, WorkingSet) @@ -51,7 +52,8 @@ class TestDistro: assert list(ad) == ['foopkg'] # Distributions sort by version - assert [dist.version for dist in ad['FooPkg']] == ['1.4', '1.3-1', '1.2'] + expected = ['1.4', '1.3-1', '1.2'] + assert [dist.version for dist in ad['FooPkg']] == expected # Removing a distribution leaves sequence alone ad.remove(ad['FooPkg'][1]) @@ -97,7 +99,10 @@ class TestDistro: def testDistroBasics(self): d = Distribution( "/some/path", - project_name="FooPkg", version="1.3-1", py_version="2.4", platform="win32" + project_name="FooPkg", + version="1.3-1", + py_version="2.4", + platform="win32", ) self.checkFooPkg(d) @@ -113,10 +118,11 @@ class TestDistro: def testDistroMetadata(self): d = Distribution( - "/some/path", project_name="FooPkg", py_version="2.4", platform="win32", + "/some/path", project_name="FooPkg", + py_version="2.4", platform="win32", metadata=Metadata( ('PKG-INFO', "Metadata-Version: 1.0\nVersion: 1.3-1\n") - ) + ), ) self.checkFooPkg(d) @@ -164,7 +170,10 @@ class TestDistro: ad.add(Baz) # Activation list now includes resolved dependency - assert list(ws.resolve(parse_requirements("Foo[bar]"), ad)) == [Foo, Baz] + assert ( + list(ws.resolve(parse_requirements("Foo[bar]"), ad)) + == [Foo, Baz] + ) # Requests for conflicting versions produce VersionConflict with pytest.raises(VersionConflict) as vc: ws.resolve(parse_requirements("Foo==1.2\nFoo!=1.2"), ad) @@ -415,7 +424,8 @@ class TestEntryPoints: submap_expect = dict( feature1=EntryPoint('feature1', 'somemodule', ['somefunction']), - feature2=EntryPoint('feature2', 'another.module', ['SomeClass'], ['extra1', 'extra2']), + feature2=EntryPoint( + 'feature2', 'another.module', ['SomeClass'], ['extra1', 'extra2']), feature3=EntryPoint('feature3', 'this.module', extras=['something']) ) submap_str = """ @@ -518,11 +528,17 @@ class TestRequirements: Requirement.parse('setuptools').project_name == 'setuptools') # setuptools 0.7 and higher means setuptools. assert ( - Requirement.parse('setuptools == 0.7').project_name == 'setuptools') + Requirement.parse('setuptools == 0.7').project_name + == 'setuptools' + ) assert ( - Requirement.parse('setuptools == 0.7a1').project_name == 'setuptools') + Requirement.parse('setuptools == 0.7a1').project_name + == 'setuptools' + ) assert ( - Requirement.parse('setuptools >= 0.7').project_name == 'setuptools') + Requirement.parse('setuptools >= 0.7').project_name + == 'setuptools' + ) class TestParsing: @@ -552,7 +568,7 @@ class TestParsing: """ assert ( list(pkg_resources.split_sections(sample)) - == + == [ (None, ["x"]), ("Y", ["z", "a"]), @@ -838,7 +854,8 @@ class TestNamespaces: subpkg = nspkg / 'subpkg' subpkg.ensure_dir() (nspkg / '__init__.py').write_text(self.ns_str, encoding='utf-8') - (subpkg / '__init__.py').write_text(vers_str % number, encoding='utf-8') + (subpkg / '__init__.py').write_text( + vers_str % number, encoding='utf-8') import nspkg.subpkg import nspkg diff --git a/pkg_resources/tests/test_working_set.py b/pkg_resources/tests/test_working_set.py index 422a728..42ddcc8 100644 --- a/pkg_resources/tests/test_working_set.py +++ b/pkg_resources/tests/test_working_set.py @@ -1,6 +1,7 @@ import inspect import re import textwrap +import functools import pytest @@ -15,6 +16,7 @@ def strip_comments(s): if l.strip() and not l.strip().startswith('#') ) + def parse_distributions(s): ''' Parse a series of distribution specs of the form: @@ -31,7 +33,8 @@ def parse_distributions(s): yield 2 distributions: - project_name=foo, version=0.2 - - project_name=bar, version=1.0, requires=['foo>=3.0', 'baz; extra=="feature"'] + - project_name=bar, version=1.0, + requires=['foo>=3.0', 'baz; extra=="feature"'] ''' s = s.strip() for spec in re.split('\n(?=[^\s])', s): @@ -42,7 +45,7 @@ def parse_distributions(s): name, version = fields.pop(0).split('-') if fields: requires = textwrap.dedent(fields.pop(0)) - metadata=Metadata(('requires.txt', requires)) + metadata = Metadata(('requires.txt', requires)) else: metadata = None dist = pkg_resources.Distribution(project_name=name, @@ -467,7 +470,8 @@ def test_working_set_resolve(installed_dists, installable_dists, requirements, replace_conflicting, resolved_dists_or_exception): ws = pkg_resources.WorkingSet([]) list(map(ws.add, installed_dists)) - resolve_call = lambda: ws.resolve( + resolve_call = functools.partial( + ws.resolve, requirements, installer=FakeInstaller(installable_dists), replace_conflicting=replace_conflicting, ) diff --git a/setup.cfg b/setup.cfg index 0a33329..5a9a875 100755 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 38.2.5 +current_version = 38.6.1 commit = True tag = True diff --git a/setup.py b/setup.py index af799e4..a59ba44 100755 --- a/setup.py +++ b/setup.py @@ -89,7 +89,7 @@ def pypi_link(pkg_filename): setup_params = dict( name="setuptools", - version="38.2.5", + version="38.6.1", description="Easily download, build, install, upgrade, and uninstall " "Python packages", author="Python Packaging Authority", @@ -98,6 +98,9 @@ setup_params = dict( long_description_content_type='text/x-rst; charset=UTF-8', keywords="CPAN PyPI distutils eggs package management", url="https://github.com/pypa/setuptools", + project_urls={ + "Documentation": "https://setuptools.readthedocs.io/", + }, src_root=None, packages=setuptools.find_packages(exclude=['*.tests']), package_data=package_data, diff --git a/setuptools/command/bdist_egg.py b/setuptools/command/bdist_egg.py index 5fdb62d..423b818 100644 --- a/setuptools/command/bdist_egg.py +++ b/setuptools/command/bdist_egg.py @@ -39,6 +39,7 @@ def strip_module(filename): filename = filename[:-6] return filename + def sorted_walk(dir): """Do os.walk in a reproducible way, independent of indeterministic filesystem readdir order @@ -48,6 +49,7 @@ def sorted_walk(dir): files.sort() yield base, dirs, files + def write_stub(resource, pyfile): _stub_template = textwrap.dedent(""" def __bootstrap__(): @@ -252,15 +254,17 @@ class bdist_egg(Command): pattern = r'(?P.+)\.(?P[^.]+)\.pyc' m = re.match(pattern, name) - path_new = os.path.join(base, os.pardir, m.group('name') + '.pyc') - log.info("Renaming file from [%s] to [%s]" % (path_old, path_new)) + path_new = os.path.join( + base, os.pardir, m.group('name') + '.pyc') + log.info( + "Renaming file from [%s] to [%s]" + % (path_old, path_new)) try: os.remove(path_new) except OSError: pass os.rename(path_old, path_new) - def zip_safe(self): safe = getattr(self.distribution, 'zip_safe', None) if safe is not None: @@ -409,8 +413,10 @@ def scan_module(egg_dir, base, name, stubs): module = pkg + (pkg and '.' or '') + os.path.splitext(name)[0] if sys.version_info < (3, 3): skip = 8 # skip magic & date - else: + elif sys.version_info < (3, 7): skip = 12 # skip magic & date & file size + else: + skip = 16 # skip magic & reserved? & date & file size f = open(filename, 'rb') f.read(skip) code = marshal.load(f) diff --git a/setuptools/command/build_ext.py b/setuptools/command/build_ext.py index 36f53f0..ea97b37 100644 --- a/setuptools/command/build_ext.py +++ b/setuptools/command/build_ext.py @@ -15,6 +15,9 @@ from setuptools.extern import six try: # Attempt to use Cython for building extensions, if available from Cython.Distutils.build_ext import build_ext as _build_ext + # Additionally, assert that the compiler module will load + # also. Ref #1229. + __import__('Cython.Compiler.Main') except ImportError: _build_ext = _du_build_ext diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index 12e2231..a6f6143 100755 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -828,14 +828,16 @@ class easy_install(Command): target = os.path.join(self.script_dir, script_name) self.add_output(target) + if self.dry_run: + return + mask = current_umask() - if not self.dry_run: - ensure_directory(target) - if os.path.exists(target): - os.unlink(target) - with open(target, "w" + mode) as f: - f.write(contents) - chmod(target, 0o777 - mask) + ensure_directory(target) + if os.path.exists(target): + os.unlink(target) + with open(target, "w" + mode) as f: + f.write(contents) + chmod(target, 0o777 - mask) def install_eggs(self, spec, dist_filename, tmpdir): # .egg dirs or files are already built, so just return them @@ -1248,7 +1250,6 @@ class easy_install(Command): def byte_compile(self, to_compile): if sys.dont_write_bytecode: - self.warn('byte-compiling is disabled, skipping.') return from distutils.util import byte_compile diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py index 103c5f2..befa090 100755 --- a/setuptools/command/egg_info.py +++ b/setuptools/command/egg_info.py @@ -597,10 +597,7 @@ def write_pkg_info(cmd, basename, filename): metadata = cmd.distribution.metadata metadata.version, oldver = cmd.egg_version, metadata.version metadata.name, oldname = cmd.egg_name, metadata.name - metadata.long_description_content_type = getattr( - cmd.distribution, - 'long_description_content_type' - ) + try: # write unescaped data to PKG-INFO, so older pkg_resources # can still parse it diff --git a/setuptools/config.py b/setuptools/config.py index 5382844..8eddcae 100644 --- a/setuptools/config.py +++ b/setuptools/config.py @@ -109,7 +109,7 @@ def parse_configuration( distribution, command_options, ignore_option_errors) options.parse() - return [meta, options] + return meta, options class ConfigHandler(object): @@ -404,6 +404,7 @@ class ConfigMetadataHandler(ConfigHandler): """Metadata item name to parser function mapping.""" parse_list = self._parse_list parse_file = self._parse_file + parse_dict = self._parse_dict return { 'platforms': parse_list, @@ -416,6 +417,7 @@ class ConfigMetadataHandler(ConfigHandler): 'description': parse_file, 'long_description': parse_file, 'version': self._parse_version, + 'project_urls': parse_dict, } def _parse_version(self, value): diff --git a/setuptools/dist.py b/setuptools/dist.py index 477f93d..d24958d 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -44,8 +44,10 @@ def write_pkg_file(self, file): self.classifiers or self.download_url): version = '1.1' # Setuptools specific for PEP 345 - if hasattr(self, 'python_requires'): + if hasattr(self, 'python_requires') or self.project_urls: version = '1.2' + if self.long_description_content_type or self.provides_extras: + version = '2.1' file.write('Metadata-Version: %s\n' % version) file.write('Name: %s\n' % self.get_name()) @@ -57,13 +59,8 @@ def write_pkg_file(self, file): file.write('License: %s\n' % self.get_license()) if self.download_url: file.write('Download-URL: %s\n' % self.download_url) - - long_desc_content_type = getattr( - self, - 'long_description_content_type', - None - ) or 'UNKNOWN' - file.write('Description-Content-Type: %s\n' % long_desc_content_type) + for project_url in self.project_urls.items(): + file.write('Project-URL: %s, %s\n' % project_url) long_desc = rfc822_escape(self.get_long_description()) file.write('Description: %s\n' % long_desc) @@ -84,6 +81,16 @@ def write_pkg_file(self, file): if hasattr(self, 'python_requires'): file.write('Requires-Python: %s\n' % self.python_requires) + # PEP 566 + if self.long_description_content_type: + file.write( + 'Description-Content-Type: %s\n' % + self.long_description_content_type + ) + if self.provides_extras: + for extra in self.provides_extras: + file.write('Provides-Extra: %s\n' % extra) + # from Python 3.4 def write_pkg_info(self, base_dir): @@ -326,14 +333,24 @@ class Distribution(Distribution_parse_config_files, _Distribution): self.dist_files = [] self.src_root = attrs.pop("src_root", None) self.patch_missing_pkg_info(attrs) - self.long_description_content_type = attrs.get( - 'long_description_content_type' - ) + self.project_urls = attrs.get('project_urls', {}) self.dependency_links = attrs.pop('dependency_links', []) self.setup_requires = attrs.pop('setup_requires', []) for ep in pkg_resources.iter_entry_points('distutils.setup_keywords'): vars(self).setdefault(ep.name, None) _Distribution.__init__(self, attrs) + + # The project_urls attribute may not be supported in distutils, so + # prime it here from our value if not automatically set + self.metadata.project_urls = getattr( + self.metadata, 'project_urls', self.project_urls) + self.metadata.long_description_content_type = attrs.get( + 'long_description_content_type' + ) + self.metadata.provides_extras = getattr( + self.metadata, 'provides_extras', set() + ) + if isinstance(self.metadata.version, numbers.Number): # Some people apparently take "version number" too literally :) self.metadata.version = str(self.metadata.version) @@ -366,6 +383,16 @@ class Distribution(Distribution_parse_config_files, _Distribution): """ if getattr(self, 'python_requires', None): self.metadata.python_requires = self.python_requires + + if getattr(self, 'extras_require', None): + for extra in self.extras_require.keys(): + # Since this gets called multiple times at points where the + # keys have become 'converted' extras, ensure that we are only + # truly adding extras we haven't seen before here. + extra = extra.split(':')[0] + if extra: + self.metadata.provides_extras.add(extra) + self._convert_extras_requirements() self._move_install_requirements_markers() diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py index cdfa5af..abb953a 100644 --- a/setuptools/tests/test_config.py +++ b/setuptools/tests/test_config.py @@ -110,6 +110,7 @@ class TestMetadata: '[metadata]\n' 'version = 10.1.1\n' 'description = Some description\n' + 'long_description_content_type = text/something\n' 'long_description = file: README\n' 'name = fake_name\n' 'keywords = one, two\n' @@ -131,6 +132,7 @@ class TestMetadata: assert metadata.version == '10.1.1' assert metadata.description == 'Some description' + assert metadata.long_description_content_type == 'text/something' assert metadata.long_description == 'readme contents\nline2' assert metadata.provides == ['package', 'package.sub'] assert metadata.license == 'BSD 3-Clause License' @@ -215,6 +217,22 @@ class TestMetadata: 'Programming Language :: Python :: 3.5', ] + def test_dict(self, tmpdir): + + fake_env( + tmpdir, + '[metadata]\n' + 'project_urls =\n' + ' Link One = https://example.com/one/\n' + ' Link Two = https://example.com/two/\n' + ) + with get_dist(tmpdir) as dist: + metadata = dist.metadata + assert metadata.project_urls == { + 'Link One': 'https://example.com/one/', + 'Link Two': 'https://example.com/two/', + } + def test_version(self, tmpdir): _, config = fake_env( @@ -528,6 +546,7 @@ class TestOptions: 'pdf': ['ReportLab>=1.2', 'RXP'], 'rest': ['docutils>=0.3', 'pack==1.1,==1.3'] } + assert dist.metadata.provides_extras == set(['pdf', 'rest']) def test_entry_points(self, tmpdir): _, config = fake_env( diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py index 834710e..57339c8 100644 --- a/setuptools/tests/test_easy_install.py +++ b/setuptools/tests/test_easy_install.py @@ -183,6 +183,51 @@ class TestEasyInstallTest: cmd.ensure_finalized() cmd.easy_install(sdist_unicode) + @pytest.fixture + def sdist_script(self, tmpdir): + files = [ + ( + 'setup.py', + DALS(""" + import setuptools + setuptools.setup( + name="setuptools-test-script", + version="1.0", + scripts=["mypkg_script"], + ) + """), + ), + ( + u'mypkg_script', + DALS(""" + #/usr/bin/python + print('mypkg_script') + """), + ), + ] + sdist_name = 'setuptools-test-script-1.0.zip' + sdist = str(tmpdir / sdist_name) + make_sdist(sdist, files) + return sdist + + @pytest.mark.skipif(not sys.platform.startswith('linux'), + reason="Test can only be run on Linux") + def test_script_install(self, sdist_script, tmpdir, monkeypatch): + """ + Check scripts are installed. + """ + dist = Distribution({'script_args': ['easy_install']}) + target = (tmpdir / 'target').ensure_dir() + cmd = ei.easy_install( + dist, + install_dir=str(target), + args=['x'], + ) + monkeypatch.setitem(os.environ, 'PYTHONPATH', str(target)) + cmd.ensure_finalized() + cmd.easy_install(sdist_script) + assert (target / 'mypkg_script').exists() + class TestPTHFileWriter: def test_add_from_cwd_site_sets_dirty(self): diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py index 66ca916..d221167 100644 --- a/setuptools/tests/test_egg_info.py +++ b/setuptools/tests/test_egg_info.py @@ -420,6 +420,41 @@ class TestEggInfo(object): self._run_install_command(tmpdir_cwd, env) assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == [] + def test_provides_extra(self, tmpdir_cwd, env): + self._setup_script_with_requires( + 'extras_require={"foobar": ["barbazquux"]},') + environ = os.environ.copy().update( + HOME=env.paths['home'], + ) + code, data = environment.run_setup_py( + cmd=['egg_info'], + pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), + data_stream=1, + env=environ, + ) + egg_info_dir = os.path.join('.', 'foo.egg-info') + with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: + pkg_info_lines = pkginfo_file.read().split('\n') + assert 'Provides-Extra: foobar' in pkg_info_lines + assert 'Metadata-Version: 2.1' in pkg_info_lines + + def test_doesnt_provides_extra(self, tmpdir_cwd, env): + self._setup_script_with_requires( + '''install_requires=["spam ; python_version<'3.3'"]''') + environ = os.environ.copy().update( + HOME=env.paths['home'], + ) + environment.run_setup_py( + cmd=['egg_info'], + pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), + data_stream=1, + env=environ, + ) + egg_info_dir = os.path.join('.', 'foo.egg-info') + with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: + pkg_info_text = pkginfo_file.read() + assert 'Provides-Extra:' not in pkg_info_text + def test_long_description_content_type(self, tmpdir_cwd, env): # Test that specifying a `long_description_content_type` keyword arg to # the `setup` function results in writing a `Description-Content-Type` @@ -444,6 +479,37 @@ class TestEggInfo(object): pkg_info_lines = pkginfo_file.read().split('\n') expected_line = 'Description-Content-Type: text/markdown' assert expected_line in pkg_info_lines + assert 'Metadata-Version: 2.1' in pkg_info_lines + + def test_project_urls(self, tmpdir_cwd, env): + # Test that specifying a `project_urls` dict to the `setup` + # function results in writing multiple `Project-URL` lines to + # the `PKG-INFO` file in the `.egg-info` + # directory. + # `Project-URL` is described at https://packaging.python.org + # /specifications/core-metadata/#project-url-multiple-use + + self._setup_script_with_requires( + """project_urls={ + 'Link One': 'https://example.com/one/', + 'Link Two': 'https://example.com/two/', + },""") + environ = os.environ.copy().update( + HOME=env.paths['home'], + ) + code, data = environment.run_setup_py( + cmd=['egg_info'], + pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), + data_stream=1, + env=environ, + ) + egg_info_dir = os.path.join('.', 'foo.egg-info') + with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: + pkg_info_lines = pkginfo_file.read().split('\n') + expected_line = 'Project-URL: Link One, https://example.com/one/' + assert expected_line in pkg_info_lines + expected_line = 'Project-URL: Link Two, https://example.com/two/' + assert expected_line in pkg_info_lines def test_python_requires_egg_info(self, tmpdir_cwd, env): self._setup_script_with_requires( diff --git a/setuptools/tests/test_sandbox.py b/setuptools/tests/test_sandbox.py index a3f1206..d867542 100644 --- a/setuptools/tests/test_sandbox.py +++ b/setuptools/tests/test_sandbox.py @@ -75,6 +75,8 @@ class TestExceptionSaver: def test_unpickleable_exception(self): class CantPickleThis(Exception): "This Exception is unpickleable because it's not in globals" + def __repr__(self): + return 'CantPickleThis%r' % (self.args,) with setuptools.sandbox.ExceptionSaver() as saved_exc: raise CantPickleThis('detail') diff --git a/setuptools/tests/test_virtualenv.py b/setuptools/tests/test_virtualenv.py index 9dbd3c8..b66a311 100644 --- a/setuptools/tests/test_virtualenv.py +++ b/setuptools/tests/test_virtualenv.py @@ -2,6 +2,7 @@ import glob import os import sys +import pytest from pytest import yield_fixture from pytest_fixture_config import yield_requires_config @@ -11,6 +12,20 @@ from .textwrap import DALS from .test_easy_install import make_nspkg_sdist +@pytest.fixture(autouse=True) +def pytest_virtualenv_works(virtualenv): + """ + pytest_virtualenv may not work. if it doesn't, skip these + tests. See #1284. + """ + venv_prefix = virtualenv.run( + 'python -c "import sys; print(sys.prefix)"', + capture=True, + ).strip() + if venv_prefix == sys.prefix: + pytest.skip("virtualenv is broken (see pypa/setuptools#1284)") + + @yield_requires_config(pytest_virtualenv.CONFIG, ['virtualenv_executable']) @yield_fixture(scope='function') def bare_virtualenv(): @@ -26,6 +41,7 @@ def bare_virtualenv(): SOURCE_DIR = os.path.join(os.path.dirname(__file__), '../..') + def test_clean_env_install(bare_virtualenv): """ Check setuptools can be installed in a clean environment. @@ -35,6 +51,7 @@ def test_clean_env_install(bare_virtualenv): 'python setup.py install', )).format(source=SOURCE_DIR)) + def test_pip_upgrade_from_source(virtualenv): """ Check pip can upgrade setuptools from source. @@ -56,6 +73,7 @@ def test_pip_upgrade_from_source(virtualenv): # And finally try to upgrade from source. virtualenv.run('pip install --no-cache-dir --upgrade ' + sdist) + def test_test_command_install_requirements(bare_virtualenv, tmpdir): """ Check the test command will install all required dependencies. @@ -64,6 +82,7 @@ def test_test_command_install_requirements(bare_virtualenv, tmpdir): 'cd {source}', 'python setup.py develop', )).format(source=SOURCE_DIR)) + def sdist(distname, version): dist_path = tmpdir.join('%s-%s.tar.gz' % (distname, version)) make_nspkg_sdist(str(dist_path), distname, version) diff --git a/tests/requirements.txt b/tests/requirements.txt index 9c8bcff..38b6924 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -3,5 +3,5 @@ mock pytest-flake8; python_version>="2.7" virtualenv>=13.0.0 pytest-virtualenv>=1.2.7 -pytest>=3.0.2,<=3.2.5 +pytest>=3.0.2 wheel -- 2.7.4