Imported Upstream version 38.6.1 upstream/38.6.1
authorDongHun Kwak <dh0128.kwak@samsung.com>
Mon, 14 Jan 2019 01:41:31 +0000 (10:41 +0900)
committerDongHun Kwak <dh0128.kwak@samsung.com>
Mon, 14 Jan 2019 01:41:31 +0000 (10:41 +0900)
25 files changed:
.travis.yml
CHANGES.rst
appveyor.yml
docs/developer-guide.txt
docs/pkg_resources.txt
docs/setuptools.txt
pkg_resources/__init__.py
pkg_resources/tests/test_find_distributions.py
pkg_resources/tests/test_pkg_resources.py
pkg_resources/tests/test_resources.py
pkg_resources/tests/test_working_set.py
setup.cfg
setup.py
setuptools/command/bdist_egg.py
setuptools/command/build_ext.py
setuptools/command/easy_install.py
setuptools/command/egg_info.py
setuptools/config.py
setuptools/dist.py
setuptools/tests/test_config.py
setuptools/tests/test_easy_install.py
setuptools/tests/test_egg_info.py
setuptools/tests/test_sandbox.py
setuptools/tests/test_virtualenv.py
tests/requirements.txt

index 1fd73aa..ffbd72a 100644 (file)
@@ -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
index 7227b7f..01fb448 100644 (file)
@@ -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
 -------
 
index 9313a48..7c61455 100644 (file)
@@ -14,7 +14,12 @@ install:
 
 build: off
 
+cache:
+  - '%LOCALAPPDATA%\pip\Cache'
+
 test_script:
   - "python bootstrap.py"
   - "python -m pip install tox"
   - "tox"
+
+version: '{build}'
index 8a13638..b2c1a0c 100644 (file)
@@ -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
-<https://bitbucket.org/pypa/setuptools>`_ is available.
-
 -------
 Testing
 -------
index 8d337cb..b40a209 100644 (file)
@@ -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.
 
index c2822c4..65080a0 100644 (file)
@@ -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
index 08f9bbe..92272f2 100644 (file)
@@ -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)
 
 
index 97999b3..d735c59 100644 (file)
@@ -13,6 +13,7 @@ setuptools.setup(
 )
 """.lstrip()
 
+
 class TestFindDistributions:
 
     @pytest.fixture
index f2c00b2..7442b79 100644 (file)
@@ -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)
index dcd2f42..0543821 100644 (file)
@@ -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
index 422a728..42ddcc8 100644 (file)
@@ -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,
     )
index 0a33329..5a9a875 100755 (executable)
--- 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
 
index af799e4..a59ba44 100755 (executable)
--- 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,
index 5fdb62d..423b818 100644 (file)
@@ -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<name>.+)\.(?P<magic>[^.]+)\.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)
index 36f53f0..ea97b37 100644 (file)
@@ -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
 
index 12e2231..a6f6143 100755 (executable)
@@ -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
index 103c5f2..befa090 100755 (executable)
@@ -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
index 5382844..8eddcae 100644 (file)
@@ -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):
index 477f93d..d24958d 100644 (file)
@@ -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()
 
index cdfa5af..abb953a 100644 (file)
@@ -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(
index 834710e..57339c8 100644 (file)
@@ -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):
index 66ca916..d221167 100644 (file)
@@ -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 `<distribution>.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(
index a3f1206..d867542 100644 (file)
@@ -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')
index 9dbd3c8..b66a311 100644 (file)
@@ -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)
index 9c8bcff..38b6924 100644 (file)
@@ -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