summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--changelog.d/2628.change.rst1
-rw-r--r--changelog.d/2645.breaking.rst3
-rw-r--r--changelog.d/2645.change.rst4
-rw-r--r--setuptools/command/egg_info.py9
-rw-r--r--setuptools/command/sdist.py46
-rw-r--r--setuptools/config.py5
-rw-r--r--setuptools/dist.py52
-rw-r--r--setuptools/tests/test_dist.py4
-rw-r--r--setuptools/tests/test_egg_info.py77
-rw-r--r--setuptools/tests/test_manifest.py1
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',