diff --git a/CHANGES.rst b/CHANGES.rst index f9f1ea038b8bcb05beea1d1f3b65c828f2f628b4..df50fc8225c3e65c223c57c8c36ff47524d7b0ea 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,7 +1,19 @@ +v38.3.1 +------- + +* #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 ------- -* Removed warning when PYTHONDONTWRITEBYTECODE is enabled +* #1232: Fix trailing slash handling in ``pkg_resources.ZipProvider``. v38.2.4 ------- diff --git a/docs/setuptools.txt b/docs/setuptools.txt index c2822c4f9e8cf164f2128fed6b91eef2f46cd321..bea8018195910801fdd4cc1efb3e4af583014c0e 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()`` ------------------------- @@ -2406,6 +2416,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 73334641b5675510711b30d26ab7834b7990fd1a..08f9bbe7ef55e38f910e7a79af2d6da290488f9b 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -1693,6 +1693,9 @@ class ZipProvider(EggProvider): def _zipinfo_name(self, fspath): # Convert a virtual filename (full path to file) into a zipfile subpath # usable with the zipimport directory cache for our target archive + fspath = fspath.rstrip(os.sep) + if fspath == self.loader.archive: + return '' if fspath.startswith(self.zip_pre): return fspath[len(self.zip_pre):] raise AssertionError( diff --git a/pkg_resources/tests/test_pkg_resources.py b/pkg_resources/tests/test_pkg_resources.py index c6a7ac97e93467b60dbd8e419bf53142881c8cc8..f2c00b297b6ebe66b7ee0df44b0f562b75cbdc0d 100644 --- a/pkg_resources/tests/test_pkg_resources.py +++ b/pkg_resources/tests/test_pkg_resources.py @@ -62,10 +62,21 @@ class TestZipProvider(object): zip_info.filename = 'data.dat' zip_info.date_time = cls.ref_time.timetuple() zip_egg.writestr(zip_info, 'hello, world!') + zip_info = zipfile.ZipInfo() + zip_info.filename = 'subdir/mod2.py' + zip_info.date_time = cls.ref_time.timetuple() + zip_egg.writestr(zip_info, 'x = 6\n') + zip_info = zipfile.ZipInfo() + zip_info.filename = 'subdir/data2.dat' + zip_info.date_time = cls.ref_time.timetuple() + zip_egg.writestr(zip_info, 'goodbye, world!') zip_egg.close() egg.close() sys.path.append(egg.name) + subdir = os.path.join(egg.name, 'subdir') + sys.path.append(subdir) + cls.finalizers.append(EggRemover(subdir)) cls.finalizers.append(EggRemover(egg.name)) @classmethod @@ -73,6 +84,30 @@ class TestZipProvider(object): for finalizer in cls.finalizers: finalizer() + def test_resource_listdir(self): + import mod + zp = pkg_resources.ZipProvider(mod) + + expected_root = ['data.dat', 'mod.py', 'subdir'] + assert sorted(zp.resource_listdir('')) == expected_root + assert sorted(zp.resource_listdir('/')) == expected_root + + expected_subdir = ['data2.dat', 'mod2.py'] + assert sorted(zp.resource_listdir('subdir')) == expected_subdir + assert sorted(zp.resource_listdir('subdir/')) == expected_subdir + + assert zp.resource_listdir('nonexistent') == [] + assert zp.resource_listdir('nonexistent/') == [] + + import mod2 + zp2 = pkg_resources.ZipProvider(mod2) + + assert sorted(zp2.resource_listdir('')) == expected_subdir + assert sorted(zp2.resource_listdir('/')) == expected_subdir + + assert zp2.resource_listdir('subdir') == [] + assert zp2.resource_listdir('subdir/') == [] + def test_resource_filename_rewrites_on_change(self): """ If a previous call to get_resource_filename has saved the file, but diff --git a/setup.cfg b/setup.cfg index d6f1a195b539a529c078f92c03bff70f6da30fc3..13b520a898cff5521a71506cda0e6d0e4b887e7a 100755 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 38.2.4 +current_version = 38.3.0 commit = True tag = True diff --git a/setup.py b/setup.py index 0e3e42c5367b91d25c9313b7d99a8b71f6d3ad4f..e2b5dc90a22e765adce127df7fcc1677cd13816e 100755 --- a/setup.py +++ b/setup.py @@ -89,7 +89,7 @@ def pypi_link(pkg_filename): setup_params = dict( name="setuptools", - version="38.2.4", + version="38.3.0", 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/egg_info.py b/setuptools/command/egg_info.py index 103c5f20b97b032ba8ca5bca21733358858cb535..befa09043a9fd73bd7a6bdfecef2b076e6402883 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 5382844769eb5e25ff9a0afe0d5c49ba7dfcbd7f..a70794a42ef0068685c9788e02aa70d12e6a14e5 100644 --- a/setuptools/config.py +++ b/setuptools/config.py @@ -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 477f93ddbaade73c69b64b578d08903d5142b3d7..c2bfdbc7c66090d096d0515b5ca7454f7c2cf72c 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -44,7 +44,7 @@ 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' file.write('Metadata-Version: %s\n' % version) @@ -57,12 +57,11 @@ 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) + for project_url in self.project_urls.items(): + file.write('Project-URL: %s, %s\n' % project_url) - long_desc_content_type = getattr( - self, - 'long_description_content_type', - None - ) or 'UNKNOWN' + long_desc_content_type = \ + self.long_description_content_type or 'UNKNOWN' file.write('Description-Content-Type: %s\n' % long_desc_content_type) long_desc = rfc822_escape(self.get_long_description()) @@ -326,14 +325,21 @@ 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' + ) + if isinstance(self.metadata.version, numbers.Number): # Some people apparently take "version number" too literally :) self.metadata.version = str(self.metadata.version) diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py index cdfa5af43bec84ad484b85110a071360f7985bf5..383e0d3071c579b8ddc6c2cfec5eef4c3bf004a1 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( diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py index 66ca9164d5ee8d9fb8fd7c2b5547b98fe0455dd2..7d12434e1f0d6b1dccb3e03a4e3f980bd5881635 100644 --- a/setuptools/tests/test_egg_info.py +++ b/setuptools/tests/test_egg_info.py @@ -445,6 +445,36 @@ class TestEggInfo(object): expected_line = 'Description-Content-Type: text/markdown' assert expected_line 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( """python_requires='>=2.7.12',""")