diff options
| -rw-r--r-- | changelog.d/2628.change.rst | 1 | ||||
| -rw-r--r-- | changelog.d/2645.breaking.rst | 3 | ||||
| -rw-r--r-- | changelog.d/2645.change.rst | 4 | ||||
| -rw-r--r-- | setuptools/command/egg_info.py | 9 | ||||
| -rw-r--r-- | setuptools/command/sdist.py | 46 | ||||
| -rw-r--r-- | setuptools/config.py | 5 | ||||
| -rw-r--r-- | setuptools/dist.py | 52 | ||||
| -rw-r--r-- | setuptools/tests/test_dist.py | 4 | ||||
| -rw-r--r-- | setuptools/tests/test_egg_info.py | 77 | ||||
| -rw-r--r-- | setuptools/tests/test_manifest.py | 1 |
10 files changed, 143 insertions, 59 deletions
diff --git a/changelog.d/2628.change.rst b/changelog.d/2628.change.rst new file mode 100644 index 00000000..53619f89 --- /dev/null +++ b/changelog.d/2628.change.rst @@ -0,0 +1 @@ +Write long description in message payload of PKG-INFO file. - by :user:`cdce8p` diff --git a/changelog.d/2645.breaking.rst b/changelog.d/2645.breaking.rst new file mode 100644 index 00000000..b96b492a --- /dev/null +++ b/changelog.d/2645.breaking.rst @@ -0,0 +1,3 @@ +License files excluded via the ``MANIFEST.in`` but matched by either +the ``license_file`` (deprecated) or ``license_files`` options, +will be nevertheless included in the source distribution. - by :user:`cdce8p` diff --git a/changelog.d/2645.change.rst b/changelog.d/2645.change.rst new file mode 100644 index 00000000..b22385c1 --- /dev/null +++ b/changelog.d/2645.change.rst @@ -0,0 +1,4 @@ +Added ``License-File`` (multiple) to the output package metadata. +The field will contain the path of a license file, matched by the +``license_file`` (deprecated) and ``license_files`` options, +relative to ``.dist-info``. - by :user:`cdce8p` diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py index 1f120b67..18b81340 100644 --- a/setuptools/command/egg_info.py +++ b/setuptools/command/egg_info.py @@ -541,6 +541,7 @@ class manifest_maker(sdist): self.add_defaults() if os.path.exists(self.template): self.read_template() + self.add_license_files() self.prune_file_list() self.filelist.sort() self.filelist.remove_duplicates() @@ -575,7 +576,6 @@ class manifest_maker(sdist): def add_defaults(self): sdist.add_defaults(self) - self.check_license() self.filelist.append(self.template) self.filelist.append(self.manifest) rcfiles = list(walk_revctrl()) @@ -592,6 +592,13 @@ class manifest_maker(sdist): ei_cmd = self.get_finalized_command('egg_info') self.filelist.graft(ei_cmd.egg_info) + def add_license_files(self): + license_files = self.distribution.metadata.license_files or [] + for lf in license_files: + log.info("adding license file '%s'", lf) + pass + self.filelist.extend(license_files) + def prune_file_list(self): build = self.get_finalized_command('build') base_dir = self.distribution.get_fullname() diff --git a/setuptools/command/sdist.py b/setuptools/command/sdist.py index a6ea814a..4a014283 100644 --- a/setuptools/command/sdist.py +++ b/setuptools/command/sdist.py @@ -4,9 +4,6 @@ import os import sys import io import contextlib -from glob import iglob - -from setuptools.extern import ordered_set from .py36compat import sdist_add_defaults @@ -190,46 +187,3 @@ class sdist(sdist_add_defaults, orig.sdist): continue self.filelist.append(line) manifest.close() - - def check_license(self): - """Checks if license_file' or 'license_files' is configured and adds any - valid paths to 'self.filelist'. - """ - opts = self.distribution.get_option_dict('metadata') - - files = ordered_set.OrderedSet() - try: - license_files = self.distribution.metadata.license_files - except TypeError: - log.warn("warning: 'license_files' option is malformed") - license_files = ordered_set.OrderedSet() - patterns = license_files if isinstance(license_files, ordered_set.OrderedSet) \ - else ordered_set.OrderedSet(license_files) - - if 'license_file' in opts: - log.warn( - "warning: the 'license_file' option is deprecated, " - "use 'license_files' instead") - patterns.append(opts['license_file'][1]) - - if 'license_file' not in opts and 'license_files' not in opts: - # Default patterns match the ones wheel uses - # See https://wheel.readthedocs.io/en/stable/user_guide.html - # -> 'Including license files in the generated wheel file' - patterns = ('LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*') - - for pattern in patterns: - for path in iglob(pattern): - if path.endswith('~'): - log.debug( - "ignoring license file '%s' as it looks like a backup", - path) - continue - - if path not in files and os.path.isfile(path): - log.info( - "adding license file '%s' (matched pattern '%s')", - path, pattern) - files.add(path) - - self.filelist.extend(sorted(files)) diff --git a/setuptools/config.py b/setuptools/config.py index 4a6cd469..44de7cf5 100644 --- a/setuptools/config.py +++ b/setuptools/config.py @@ -520,6 +520,11 @@ class ConfigMetadataHandler(ConfigHandler): 'obsoletes': parse_list, 'classifiers': self._get_parser_compound(parse_file, parse_list), 'license': exclude_files_parser('license'), + 'license_file': self._deprecated_config_handler( + exclude_files_parser('license_file'), + "The license_file parameter is deprecated, " + "use license_files instead.", + DeprecationWarning), 'license_files': parse_list, 'description': parse_file, 'long_description': parse_file, diff --git a/setuptools/dist.py b/setuptools/dist.py index a1b7e832..6e3f25f9 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -15,6 +15,7 @@ import distutils.command from distutils.util import strtobool from distutils.debug import DEBUG from distutils.fancy_getopt import translate_longopt +from glob import iglob import itertools import textwrap from typing import List, Optional, TYPE_CHECKING @@ -28,6 +29,7 @@ from distutils.version import StrictVersion from setuptools.extern import packaging from setuptools.extern import ordered_set +from setuptools.extern.more_itertools import unique_everseen from . import SetuptoolsDeprecationWarning @@ -92,6 +94,13 @@ def _read_list_from_msg(msg: "Message", field: str) -> Optional[List[str]]: return values +def _read_payload_from_msg(msg: "Message") -> Optional[str]: + value = msg.get_payload().strip() + if value == 'UNKNOWN': + return None + return value + + def read_pkg_file(self, file): """Reads the metadata values from a file object.""" msg = message_from_file(file) @@ -114,6 +123,8 @@ def read_pkg_file(self, file): self.download_url = None self.long_description = _read_field_unescaped_from_msg(msg, 'description') + if self.long_description is None and self.metadata_version >= StrictVersion('2.1'): + self.long_description = _read_payload_from_msg(msg) self.description = _read_field_from_msg(msg, 'summary') if 'keywords' in msg: @@ -132,6 +143,8 @@ def read_pkg_file(self, file): self.provides = None self.obsoletes = None + self.license_files = _read_list_from_msg(msg, 'license-file') + def single_line(val): # quick and dirty validation for description pypa/setuptools#1390 @@ -176,9 +189,6 @@ def write_pkg_file(self, file): # noqa: C901 # is too complex (14) # FIXME for project_url in self.project_urls.items(): write_field('Project-URL', '%s, %s' % project_url) - long_desc = rfc822_escape(self.get_long_description()) - write_field('Description', long_desc) - keywords = ','.join(self.get_keywords()) if keywords: write_field('Keywords', keywords) @@ -207,6 +217,10 @@ def write_pkg_file(self, file): # noqa: C901 # is too complex (14) # FIXME for extra in self.provides_extras: write_field('Provides-Extra', extra) + self._write_list(file, 'License-File', self.license_files or []) + + file.write("\n%s\n\n" % self.get_long_description()) + sequence = tuple, list @@ -406,7 +420,8 @@ class Distribution(_Distribution): 'long_description_content_type': lambda: None, 'project_urls': dict, 'provides_extras': ordered_set.OrderedSet, - 'license_files': ordered_set.OrderedSet, + 'license_file': lambda: None, + 'license_files': lambda: None, } _patched_dist = None @@ -565,6 +580,34 @@ class Distribution(_Distribution): req.marker = None return req + def _finalize_license_files(self): + """Compute names of all license files which should be included.""" + license_files: Optional[List[str]] = self.metadata.license_files + patterns: List[str] = license_files if license_files else [] + + license_file: Optional[str] = self.metadata.license_file + if license_file and license_file not in patterns: + patterns.append(license_file) + + if license_files is None and license_file is None: + # Default patterns match the ones wheel uses + # See https://wheel.readthedocs.io/en/stable/user_guide.html + # -> 'Including license files in the generated wheel file' + patterns = ('LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*') + + self.metadata.license_files = list( + unique_everseen(self._expand_patterns(patterns))) + + @staticmethod + def _expand_patterns(patterns): + return ( + path + for pattern in patterns + for path in iglob(pattern) + if not path.endswith('~') + and os.path.isfile(path) + ) + # FIXME: 'Distribution._parse_config_files' is too complex (14) def _parse_config_files(self, filenames=None): # noqa: C901 """ @@ -729,6 +772,7 @@ class Distribution(_Distribution): parse_configuration(self, self.command_options, ignore_option_errors=ignore_option_errors) self._finalize_requires() + self._finalize_license_files() def fetch_build_eggs(self, requires): """Resolve pre-setup requirements""" diff --git a/setuptools/tests/test_dist.py b/setuptools/tests/test_dist.py index 6378caef..c4279f0b 100644 --- a/setuptools/tests/test_dist.py +++ b/setuptools/tests/test_dist.py @@ -251,8 +251,8 @@ def test_maintainer_author(name, attrs, tmpdir): with io.open(str(fn.join('PKG-INFO')), 'r', encoding='utf-8') as f: raw_pkg_lines = f.readlines() - # Drop blank lines - pkg_lines = list(filter(None, raw_pkg_lines)) + # Drop blank lines and strip lines from default description + pkg_lines = list(filter(None, raw_pkg_lines[:-2])) pkg_lines_set = set(pkg_lines) diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py index 0d595ad0..ee07b5a1 100644 --- a/setuptools/tests/test_egg_info.py +++ b/setuptools/tests/test_egg_info.py @@ -551,7 +551,7 @@ class TestEggInfo: """), 'MANIFEST.in': "exclude LICENSE", 'LICENSE': "Test license" - }, False), # license file is manually excluded + }, True), # manifest is overwritten by license_file pytest.param({ 'setup.cfg': DALS(""" [metadata] @@ -647,7 +647,7 @@ class TestEggInfo: """), 'MANIFEST.in': "exclude LICENSE", 'LICENSE': "Test license" - }, [], ['LICENSE']), # license file is manually excluded + }, ['LICENSE'], []), # manifest is overwritten by license_files ({ 'setup.cfg': DALS(""" [metadata] @@ -658,7 +658,8 @@ class TestEggInfo: 'MANIFEST.in': "exclude LICENSE-XYZ", 'LICENSE-ABC': "ABC license", 'LICENSE-XYZ': "XYZ license" - }, ['LICENSE-ABC'], ['LICENSE-XYZ']), # subset is manually excluded + # manifest is overwritten by license_files + }, ['LICENSE-ABC', 'LICENSE-XYZ'], []), pytest.param({ 'setup.cfg': "", 'LICENSE-ABC': "ABC license", @@ -688,6 +689,17 @@ class TestEggInfo: 'NOTICE-XYZ': "XYZ notice", }, ['LICENSE-ABC'], ['NOTICE-XYZ'], id="no_default_glob_patterns"), + pytest.param({ + 'setup.cfg': DALS(""" + [metadata] + license_files = + LICENSE-ABC + LICENSE* + """), + 'LICENSE-ABC': "ABC license", + }, ['LICENSE-ABC'], [], + id="files_only_added_once", + ), ]) def test_setup_cfg_license_files( self, tmpdir_cwd, env, files, incl_licenses, excl_licenses): @@ -791,8 +803,8 @@ class TestEggInfo: 'LICENSE-ABC': "ABC license", 'LICENSE-PQR': "PQR license", 'LICENSE-XYZ': "XYZ license" - # manually excluded - }, ['LICENSE-XYZ'], ['LICENSE-ABC', 'LICENSE-PQR']), + # manifest is overwritten + }, ['LICENSE-ABC', 'LICENSE-PQR', 'LICENSE-XYZ'], []), pytest.param({ 'setup.cfg': DALS(""" [metadata] @@ -835,6 +847,38 @@ class TestEggInfo: for lf in excl_licenses: assert sources_lines.count(lf) == 0 + def test_license_file_attr_pkg_info(self, tmpdir_cwd, env): + """All matched license files should have a corresponding License-File.""" + self._create_project() + path.build({ + "setup.cfg": DALS(""" + [metadata] + license_files = + NOTICE* + LICENSE* + """), + "LICENSE-ABC": "ABC license", + "LICENSE-XYZ": "XYZ license", + "NOTICE": "included", + "IGNORE": "not include", + }) + + environment.run_setup_py( + cmd=['egg_info'], + pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]) + ) + 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') + license_file_lines = [ + line for line in pkg_info_lines if line.startswith('License-File:')] + + # Only 'NOTICE', LICENSE-ABC', and 'LICENSE-XYZ' should have been matched + # Also assert that order from license_files is keeped + assert "License-File: NOTICE" == license_file_lines[0] + assert "License-File: LICENSE-ABC" in license_file_lines[1:] + assert "License-File: LICENSE-XYZ" in license_file_lines[1:] + def test_metadata_version(self, tmpdir_cwd, env): """Make sure latest metadata version is used by default.""" self._setup_script_with_requires("") @@ -875,6 +919,29 @@ class TestEggInfo: assert expected_line in pkg_info_lines assert 'Metadata-Version: 2.1' in pkg_info_lines + def test_long_description(self, tmpdir_cwd, env): + # Test that specifying `long_description` and `long_description_content_type` + # keyword args to the `setup` function results in writing + # the description in the message payload of the `PKG-INFO` file + # in the `<distribution>.egg-info` directory. + self._setup_script_with_requires( + "long_description='This is a long description\\nover multiple lines'," + "long_description_content_type='text/markdown'," + ) + code, data = environment.run_setup_py( + cmd=['egg_info'], + pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), + data_stream=1, + ) + 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 'Metadata-Version: 2.1' in pkg_info_lines + assert '' == pkg_info_lines[-1] # last line should be empty + long_desc_lines = pkg_info_lines[pkg_info_lines.index(''):] + assert 'This is a long description' in long_desc_lines + assert 'over multiple lines' in long_desc_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 diff --git a/setuptools/tests/test_manifest.py b/setuptools/tests/test_manifest.py index 589cefb2..82bdb9c6 100644 --- a/setuptools/tests/test_manifest.py +++ b/setuptools/tests/test_manifest.py @@ -55,7 +55,6 @@ def touch(filename): default_files = frozenset(map(make_local_path, [ 'README.rst', 'MANIFEST.in', - 'LICENSE', 'setup.py', 'app.egg-info/PKG-INFO', 'app.egg-info/SOURCES.txt', |
