Skip to content

Commit

Permalink
Add License-File field to package metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
cdce8p committed May 22, 2021
1 parent 18d751d commit 3544de7
Show file tree
Hide file tree
Showing 7 changed files with 81 additions and 50 deletions.
4 changes: 4 additions & 0 deletions changelog.d/2645.change.rst
Original file line number Diff line number Diff line change
@@ -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`
9 changes: 8 additions & 1 deletion setuptools/command/egg_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,7 @@ def run(self):
if not os.path.exists(self.manifest):
self.write_manifest() # it must exist so it'll get in the list
self.add_defaults()
self.add_license_files()
if os.path.exists(self.template):
self.read_template()
self.prune_file_list()
Expand Down Expand Up @@ -575,7 +576,6 @@ def _should_suppress_warning(msg):

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())
Expand All @@ -592,6 +592,13 @@ def add_defaults(self):
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_computed
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()
Expand Down
46 changes: 0 additions & 46 deletions setuptools/command/sdist.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@
import sys
import io
import contextlib
from glob import iglob

from setuptools.extern import ordered_set

from .py36compat import sdist_add_defaults

Expand Down Expand Up @@ -190,46 +187,3 @@ def read_manifest(self):
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))
5 changes: 5 additions & 0 deletions setuptools/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,11 @@ def parsers(self):
'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,
Expand Down
37 changes: 35 additions & 2 deletions setuptools/dist.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@
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
from typing import List, Optional, Set, TYPE_CHECKING

from collections import defaultdict
from email import message_from_file
Expand Down Expand Up @@ -141,6 +142,8 @@ def read_pkg_file(self, file):
self.provides = None
self.obsoletes = None

self.license_files_computed = _read_list_from_msg(msg, 'license-file')


def single_line(val):
# quick and dirty validation for description pypa/setuptools#1390
Expand Down Expand Up @@ -213,6 +216,8 @@ def write_field(key, value):
for extra in self.provides_extras:
write_field('Provides-Extra', extra)

self._write_list(file, 'License-File', self.license_files_computed)

file.write("\n%s\n\n" % self.get_long_description())


Expand Down Expand Up @@ -414,7 +419,9 @@ class Distribution(_Distribution):
'long_description_content_type': lambda: None,
'project_urls': dict,
'provides_extras': ordered_set.OrderedSet,
'license_files': ordered_set.OrderedSet,
'license_file': None,
'license_files': None,
'license_files_computed': list,
}

_patched_dist = None
Expand Down Expand Up @@ -573,6 +580,31 @@ def _clean_req(self, req):
req.marker = None
return req

def _finalize_license_files(self):
"""Compute names of all license files which should be included."""
files = set()
license_files: Optional[List[str]] = self.metadata.license_files
patterns: Set[str] = set(license_files) if license_files else set()

license_file: Optional[str] = self.metadata.license_file
if license_file:
patterns.add(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*')

for pattern in patterns:
for path in iglob(pattern):
if path.endswith('~'):
continue
if path not in files and os.path.isfile(path):
files.add(path)

self.metadata.license_files_computed = sorted(files)

# FIXME: 'Distribution._parse_config_files' is too complex (14)
def _parse_config_files(self, filenames=None): # noqa: C901
"""
Expand Down Expand Up @@ -737,6 +769,7 @@ def parse_config_files(self, filenames=None, ignore_option_errors=False):
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"""
Expand Down
29 changes: 29 additions & 0 deletions setuptools/tests/test_egg_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,35 @@ def test_setup_cfg_license_file_license_files(
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 =
LICENSE*
"""),
"LICENSE-ABC": "ABC license",
"LICENSE-XYZ": "XYZ license",
"NOTICE": "not included",
})

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 'LICENSE-ABC' and 'LICENSE-XYZ' should have been matched
assert len(license_file_lines) == 2
assert "License-File: LICENSE-ABC" in license_file_lines
assert "License-File: LICENSE-XYZ" in license_file_lines

def test_metadata_version(self, tmpdir_cwd, env):
"""Make sure latest metadata version is used by default."""
self._setup_script_with_requires("")
Expand Down
1 change: 0 additions & 1 deletion setuptools/tests/test_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down

0 comments on commit 3544de7

Please sign in to comment.