db8f3c
From 490a2b28fa2325f9929261aa2ee398fbb4c715dd Mon Sep 17 00:00:00 2001
db8f3c
From: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
db8f3c
Date: Sat, 3 Apr 2021 16:12:19 -0400
db8f3c
Subject: [PATCH 1/2] license_files - Add support for glob patterns + add
db8f3c
 default patterns
db8f3c
db8f3c
https://github.com/pypa/setuptools/pull/2620
db8f3c
---
db8f3c
 changelog.d/2620.breaking.rst         |  4 ++
db8f3c
 changelog.d/2620.change.rst           |  1 +
db8f3c
 changelog.d/2620.deprecation.rst      |  2 +
db8f3c
 changelog.d/2620.doc.rst              |  1 +
db8f3c
 docs/references/keywords.rst          | 11 +++++
db8f3c
 docs/userguide/declarative_config.rst |  2 +-
db8f3c
 setuptools/command/sdist.py           | 55 +++++++++++++---------
db8f3c
 setuptools/tests/test_egg_info.py     | 68 +++++++++++++++++++++++++--
db8f3c
 setuptools/tests/test_manifest.py     |  1 +
db8f3c
 9 files changed, 118 insertions(+), 27 deletions(-)
db8f3c
 create mode 100644 changelog.d/2620.breaking.rst
db8f3c
 create mode 100644 changelog.d/2620.change.rst
db8f3c
 create mode 100644 changelog.d/2620.deprecation.rst
db8f3c
 create mode 100644 changelog.d/2620.doc.rst
db8f3c
db8f3c
diff --git a/changelog.d/2620.breaking.rst b/changelog.d/2620.breaking.rst
db8f3c
new file mode 100644
db8f3c
index 00000000..431e7105
db8f3c
--- /dev/null
db8f3c
+++ b/changelog.d/2620.breaking.rst
db8f3c
@@ -0,0 +1,4 @@
db8f3c
+If neither ``license_file`` nor ``license_files`` is specified, the ``sdist``
db8f3c
+option will now auto-include files that match the following patterns:
db8f3c
+``LICEN[CS]E*``, ``COPYING*``, ``NOTICE*``, ``AUTHORS*``.
db8f3c
+This matches the behavior of ``bdist_wheel``. -- by :user:`cdce8p`
db8f3c
diff --git a/changelog.d/2620.change.rst b/changelog.d/2620.change.rst
db8f3c
new file mode 100644
db8f3c
index 00000000..5470592d
db8f3c
--- /dev/null
db8f3c
+++ b/changelog.d/2620.change.rst
db8f3c
@@ -0,0 +1 @@
db8f3c
+The ``license_file`` and ``license_files`` options now support glob patterns. -- by :user:`cdce8p`
db8f3c
diff --git a/changelog.d/2620.deprecation.rst b/changelog.d/2620.deprecation.rst
db8f3c
new file mode 100644
db8f3c
index 00000000..1af5f246
db8f3c
--- /dev/null
db8f3c
+++ b/changelog.d/2620.deprecation.rst
db8f3c
@@ -0,0 +1,2 @@
db8f3c
+The ``license_file`` option is now marked as deprecated.
db8f3c
+Use ``license_files`` instead. -- by :user:`cdce8p`
db8f3c
diff --git a/changelog.d/2620.doc.rst b/changelog.d/2620.doc.rst
db8f3c
new file mode 100644
db8f3c
index 00000000..7564adac
db8f3c
--- /dev/null
db8f3c
+++ b/changelog.d/2620.doc.rst
db8f3c
@@ -0,0 +1 @@
db8f3c
+Added documentation for the ``license_files`` option. -- by :user:`cdce8p`
db8f3c
diff --git a/docs/references/keywords.rst b/docs/references/keywords.rst
db8f3c
index 03ce9fa2..619b2d14 100644
db8f3c
--- a/docs/references/keywords.rst
db8f3c
+++ b/docs/references/keywords.rst
db8f3c
@@ -76,6 +76,17 @@ Keywords
db8f3c
 ``license``
db8f3c
     A string specifying the license of the package.
db8f3c
 
db8f3c
+``license_file``
db8f3c
+
db8f3c
+    .. warning::
db8f3c
+        ``license_file`` is deprecated. Use ``license_files`` instead.
db8f3c
+
db8f3c
+``license_files``
db8f3c
+
db8f3c
+    A list of glob patterns for license related files that should be included.
db8f3c
+    If neither ``license_file`` nor ``license_files`` is specified, this option
db8f3c
+    defaults to ``LICEN[CS]E*``, ``COPYING*``, ``NOTICE*``, and ``AUTHORS*``.
db8f3c
+
db8f3c
 ``keywords``
db8f3c
     A list of strings or a comma-separated string providing descriptive
db8f3c
     meta-data. See: `PEP 0314`_.
db8f3c
diff --git a/docs/userguide/declarative_config.rst b/docs/userguide/declarative_config.rst
db8f3c
index bc66869b..1d2d66e2 100644
db8f3c
--- a/docs/userguide/declarative_config.rst
db8f3c
+++ b/docs/userguide/declarative_config.rst
db8f3c
@@ -184,7 +184,7 @@ maintainer_email                maintainer-email   str
db8f3c
 classifiers                     classifier         file:, list-comma
db8f3c
 license                                            str
db8f3c
 license_file                                       str
db8f3c
-license_files                                      list-comma
db8f3c
+license_files                                      list-comma         42.0.0
db8f3c
 description                     summary            file:, str
db8f3c
 long_description                long-description   file:, str
db8f3c
 long_description_content_type                      str                38.6.0
db8f3c
diff --git a/setuptools/command/sdist.py b/setuptools/command/sdist.py
db8f3c
index 887b7efa..a6ea814a 100644
db8f3c
--- a/setuptools/command/sdist.py
db8f3c
+++ b/setuptools/command/sdist.py
db8f3c
@@ -4,6 +4,7 @@ import os
db8f3c
 import sys
db8f3c
 import io
db8f3c
 import contextlib
db8f3c
+from glob import iglob
db8f3c
 
db8f3c
 from setuptools.extern import ordered_set
db8f3c
 
db8f3c
@@ -194,29 +195,41 @@ class sdist(sdist_add_defaults, orig.sdist):
db8f3c
         """Checks if license_file' or 'license_files' is configured and adds any
db8f3c
         valid paths to 'self.filelist'.
db8f3c
         """
db8f3c
-
db8f3c
-        files = ordered_set.OrderedSet()
db8f3c
-
db8f3c
         opts = self.distribution.get_option_dict('metadata')
db8f3c
 
db8f3c
-        # ignore the source of the value
db8f3c
-        _, license_file = opts.get('license_file', (None, None))
db8f3c
-
db8f3c
-        if license_file is None:
db8f3c
-            log.debug("'license_file' option was not specified")
db8f3c
-        else:
db8f3c
-            files.add(license_file)
db8f3c
-
db8f3c
+        files = ordered_set.OrderedSet()
db8f3c
         try:
db8f3c
-            files.update(self.distribution.metadata.license_files)
db8f3c
+            license_files = self.distribution.metadata.license_files
db8f3c
         except TypeError:
db8f3c
             log.warn("warning: 'license_files' option is malformed")
db8f3c
-
db8f3c
-        for f in files:
db8f3c
-            if not os.path.exists(f):
db8f3c
-                log.warn(
db8f3c
-                    "warning: Failed to find the configured license file '%s'",
db8f3c
-                    f)
db8f3c
-                files.remove(f)
db8f3c
-
db8f3c
-        self.filelist.extend(files)
db8f3c
+            license_files = ordered_set.OrderedSet()
db8f3c
+        patterns = license_files if isinstance(license_files, ordered_set.OrderedSet) \
db8f3c
+            else ordered_set.OrderedSet(license_files)
db8f3c
+
db8f3c
+        if 'license_file' in opts:
db8f3c
+            log.warn(
db8f3c
+                "warning: the 'license_file' option is deprecated, "
db8f3c
+                "use 'license_files' instead")
db8f3c
+            patterns.append(opts['license_file'][1])
db8f3c
+
db8f3c
+        if 'license_file' not in opts and 'license_files' not in opts:
db8f3c
+            # Default patterns match the ones wheel uses
db8f3c
+            # See https://wheel.readthedocs.io/en/stable/user_guide.html
db8f3c
+            # -> 'Including license files in the generated wheel file'
db8f3c
+            patterns = ('LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*')
db8f3c
+
db8f3c
+        for pattern in patterns:
db8f3c
+            for path in iglob(pattern):
db8f3c
+                if path.endswith('~'):
db8f3c
+                    log.debug(
db8f3c
+                        "ignoring license file '%s' as it looks like a backup",
db8f3c
+                        path)
db8f3c
+                    continue
db8f3c
+
db8f3c
+                if path not in files and os.path.isfile(path):
db8f3c
+                    log.info(
db8f3c
+                        "adding license file '%s' (matched pattern '%s')",
db8f3c
+                        path, pattern)
db8f3c
+                    files.add(path)
db8f3c
+
db8f3c
+        self.filelist.extend(sorted(files))
db8f3c
diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py
db8f3c
index 1047468b..c93ed020 100644
db8f3c
--- a/setuptools/tests/test_egg_info.py
db8f3c
+++ b/setuptools/tests/test_egg_info.py
db8f3c
@@ -533,7 +533,7 @@ class TestEggInfo:
db8f3c
             'setup.cfg': DALS("""
db8f3c
                               """),
db8f3c
             'LICENSE': "Test license"
db8f3c
-        }, False),  # no license_file attribute
db8f3c
+        }, True),  # no license_file attribute, LICENSE auto-included
db8f3c
         ({
db8f3c
             'setup.cfg': DALS("""
db8f3c
                               [metadata]
db8f3c
@@ -541,7 +541,15 @@ class TestEggInfo:
db8f3c
                               """),
db8f3c
             'MANIFEST.in': "exclude LICENSE",
db8f3c
             'LICENSE': "Test license"
db8f3c
-        }, False)  # license file is manually excluded
db8f3c
+        }, False),  # license file is manually excluded
db8f3c
+        pytest.param({
db8f3c
+            'setup.cfg': DALS("""
db8f3c
+                              [metadata]
db8f3c
+                              license_file = LICEN[CS]E*
db8f3c
+                              """),
db8f3c
+            'LICENSE': "Test license",
db8f3c
+            }, True,
db8f3c
+            id="glob_pattern"),
db8f3c
     ])
db8f3c
     def test_setup_cfg_license_file(
db8f3c
             self, tmpdir_cwd, env, files, license_in_sources):
db8f3c
@@ -621,7 +629,7 @@ class TestEggInfo:
db8f3c
             'setup.cfg': DALS("""
db8f3c
                               """),
db8f3c
             'LICENSE': "Test license"
db8f3c
-        }, [], ['LICENSE']),  # no license_files attribute
db8f3c
+        }, ['LICENSE'], []),  # no license_files attribute, LICENSE auto-included
db8f3c
         ({
db8f3c
             'setup.cfg': DALS("""
db8f3c
                               [metadata]
db8f3c
@@ -640,7 +648,36 @@ class TestEggInfo:
db8f3c
             'MANIFEST.in': "exclude LICENSE-XYZ",
db8f3c
             'LICENSE-ABC': "ABC license",
db8f3c
             'LICENSE-XYZ': "XYZ license"
db8f3c
-        }, ['LICENSE-ABC'], ['LICENSE-XYZ'])  # subset is manually excluded
db8f3c
+        }, ['LICENSE-ABC'], ['LICENSE-XYZ']),  # subset is manually excluded
db8f3c
+        pytest.param({
db8f3c
+            'setup.cfg': "",
db8f3c
+            'LICENSE-ABC': "ABC license",
db8f3c
+            'COPYING-ABC': "ABC copying",
db8f3c
+            'NOTICE-ABC': "ABC notice",
db8f3c
+            'AUTHORS-ABC': "ABC authors",
db8f3c
+            'LICENCE-XYZ': "XYZ license",
db8f3c
+            'LICENSE': "License",
db8f3c
+            'INVALID-LICENSE': "Invalid license",
db8f3c
+            }, [
db8f3c
+            'LICENSE-ABC',
db8f3c
+            'COPYING-ABC',
db8f3c
+            'NOTICE-ABC',
db8f3c
+            'AUTHORS-ABC',
db8f3c
+            'LICENCE-XYZ',
db8f3c
+            'LICENSE',
db8f3c
+            ], ['INVALID-LICENSE'],
db8f3c
+            # ('LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*')
db8f3c
+            id="default_glob_patterns"),
db8f3c
+        pytest.param({
db8f3c
+            'setup.cfg': DALS("""
db8f3c
+                              [metadata]
db8f3c
+                              license_files =
db8f3c
+                                  LICENSE*
db8f3c
+                              """),
db8f3c
+            'LICENSE-ABC': "ABC license",
db8f3c
+            'NOTICE-XYZ': "XYZ notice",
db8f3c
+            }, ['LICENSE-ABC'], ['NOTICE-XYZ'],
db8f3c
+            id="no_default_glob_patterns"),
db8f3c
     ])
db8f3c
     def test_setup_cfg_license_files(
db8f3c
             self, tmpdir_cwd, env, files, incl_licenses, excl_licenses):
db8f3c
@@ -745,7 +782,28 @@ class TestEggInfo:
db8f3c
             'LICENSE-PQR': "PQR license",
db8f3c
             'LICENSE-XYZ': "XYZ license"
db8f3c
             # manually excluded
db8f3c
-        }, ['LICENSE-XYZ'], ['LICENSE-ABC', 'LICENSE-PQR'])
db8f3c
+        }, ['LICENSE-XYZ'], ['LICENSE-ABC', 'LICENSE-PQR']),
db8f3c
+        pytest.param({
db8f3c
+            'setup.cfg': DALS("""
db8f3c
+                              [metadata]
db8f3c
+                              license_file = LICENSE*
db8f3c
+                              """),
db8f3c
+            'LICENSE-ABC': "ABC license",
db8f3c
+            'NOTICE-XYZ': "XYZ notice",
db8f3c
+            }, ['LICENSE-ABC'], ['NOTICE-XYZ'],
db8f3c
+            id="no_default_glob_patterns"),
db8f3c
+        pytest.param({
db8f3c
+            'setup.cfg': DALS("""
db8f3c
+                              [metadata]
db8f3c
+                              license_file = LICENSE*
db8f3c
+                              license_files =
db8f3c
+                                NOTICE*
db8f3c
+                              """),
db8f3c
+            'LICENSE-ABC': "ABC license",
db8f3c
+            'NOTICE-ABC': "ABC notice",
db8f3c
+            'AUTHORS-ABC': "ABC authors",
db8f3c
+            }, ['LICENSE-ABC', 'NOTICE-ABC'], ['AUTHORS-ABC'],
db8f3c
+            id="combined_glob_patterrns"),
db8f3c
     ])
db8f3c
     def test_setup_cfg_license_file_license_files(
db8f3c
             self, tmpdir_cwd, env, files, incl_licenses, excl_licenses):
db8f3c
diff --git a/setuptools/tests/test_manifest.py b/setuptools/tests/test_manifest.py
db8f3c
index 82bdb9c6..589cefb2 100644
db8f3c
--- a/setuptools/tests/test_manifest.py
db8f3c
+++ b/setuptools/tests/test_manifest.py
db8f3c
@@ -55,6 +55,7 @@ def touch(filename):
db8f3c
 default_files = frozenset(map(make_local_path, [
db8f3c
     'README.rst',
db8f3c
     'MANIFEST.in',
db8f3c
+    'LICENSE',
db8f3c
     'setup.py',
db8f3c
     'app.egg-info/PKG-INFO',
db8f3c
     'app.egg-info/SOURCES.txt',
db8f3c
db8f3c
From e1aa3949d2b0d610f6d83bc3c85d96c5c4cabd3a Mon Sep 17 00:00:00 2001
db8f3c
From: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
db8f3c
Date: Sat, 22 May 2021 20:00:24 -0400
db8f3c
Subject: [PATCH 2/2] Add License-File field to package metadata
db8f3c
db8f3c
https://github.com/pypa/setuptools/pull/2645
db8f3c
---
db8f3c
 changelog.d/2645.breaking.rst     |  3 ++
db8f3c
 changelog.d/2645.change.rst       |  4 +++
db8f3c
 setuptools/command/egg_info.py    |  9 +++++-
db8f3c
 setuptools/command/sdist.py       | 46 --------------------------
db8f3c
 setuptools/config.py              |  5 +++
db8f3c
 setuptools/dist.py                | 54 ++++++++++++++++++++++++++++++-
db8f3c
 setuptools/tests/test_egg_info.py | 54 ++++++++++++++++++++++++++++---
db8f3c
 setuptools/tests/test_manifest.py |  1 -
db8f3c
 8 files changed, 122 insertions(+), 54 deletions(-)
db8f3c
 create mode 100644 changelog.d/2645.breaking.rst
db8f3c
 create mode 100644 changelog.d/2645.change.rst
db8f3c
db8f3c
diff --git a/changelog.d/2645.breaking.rst b/changelog.d/2645.breaking.rst
db8f3c
new file mode 100644
db8f3c
index 00000000..b96b492a
db8f3c
--- /dev/null
db8f3c
+++ b/changelog.d/2645.breaking.rst
db8f3c
@@ -0,0 +1,3 @@
db8f3c
+License files excluded via the ``MANIFEST.in`` but matched by either
db8f3c
+the ``license_file`` (deprecated) or ``license_files`` options,
db8f3c
+will be nevertheless included in the source distribution. - by :user:`cdce8p`
db8f3c
diff --git a/changelog.d/2645.change.rst b/changelog.d/2645.change.rst
db8f3c
new file mode 100644
db8f3c
index 00000000..b22385c1
db8f3c
--- /dev/null
db8f3c
+++ b/changelog.d/2645.change.rst
db8f3c
@@ -0,0 +1,4 @@
db8f3c
+Added ``License-File`` (multiple) to the output package metadata.
db8f3c
+The field will contain the path of a license file, matched by the
db8f3c
+``license_file`` (deprecated) and ``license_files`` options,
db8f3c
+relative to ``.dist-info``. - by :user:`cdce8p`
db8f3c
diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py
db8f3c
index 1f120b67..18b81340 100644
db8f3c
--- a/setuptools/command/egg_info.py
db8f3c
+++ b/setuptools/command/egg_info.py
db8f3c
@@ -541,6 +541,7 @@ class manifest_maker(sdist):
db8f3c
         self.add_defaults()
db8f3c
         if os.path.exists(self.template):
db8f3c
             self.read_template()
db8f3c
+        self.add_license_files()
db8f3c
         self.prune_file_list()
db8f3c
         self.filelist.sort()
db8f3c
         self.filelist.remove_duplicates()
db8f3c
@@ -575,7 +576,6 @@ class manifest_maker(sdist):
db8f3c
 
db8f3c
     def add_defaults(self):
db8f3c
         sdist.add_defaults(self)
db8f3c
-        self.check_license()
db8f3c
         self.filelist.append(self.template)
db8f3c
         self.filelist.append(self.manifest)
db8f3c
         rcfiles = list(walk_revctrl())
db8f3c
@@ -592,6 +592,13 @@ class manifest_maker(sdist):
db8f3c
         ei_cmd = self.get_finalized_command('egg_info')
db8f3c
         self.filelist.graft(ei_cmd.egg_info)
db8f3c
 
db8f3c
+    def add_license_files(self):
db8f3c
+        license_files = self.distribution.metadata.license_files or []
db8f3c
+        for lf in license_files:
db8f3c
+            log.info("adding license file '%s'", lf)
db8f3c
+            pass
db8f3c
+        self.filelist.extend(license_files)
db8f3c
+
db8f3c
     def prune_file_list(self):
db8f3c
         build = self.get_finalized_command('build')
db8f3c
         base_dir = self.distribution.get_fullname()
db8f3c
diff --git a/setuptools/command/sdist.py b/setuptools/command/sdist.py
db8f3c
index a6ea814a..4a014283 100644
db8f3c
--- a/setuptools/command/sdist.py
db8f3c
+++ b/setuptools/command/sdist.py
db8f3c
@@ -4,9 +4,6 @@ import os
db8f3c
 import sys
db8f3c
 import io
db8f3c
 import contextlib
db8f3c
-from glob import iglob
db8f3c
-
db8f3c
-from setuptools.extern import ordered_set
db8f3c
 
db8f3c
 from .py36compat import sdist_add_defaults
db8f3c
 
db8f3c
@@ -190,46 +187,3 @@ class sdist(sdist_add_defaults, orig.sdist):
db8f3c
                 continue
db8f3c
             self.filelist.append(line)
db8f3c
         manifest.close()
db8f3c
-
db8f3c
-    def check_license(self):
db8f3c
-        """Checks if license_file' or 'license_files' is configured and adds any
db8f3c
-        valid paths to 'self.filelist'.
db8f3c
-        """
db8f3c
-        opts = self.distribution.get_option_dict('metadata')
db8f3c
-
db8f3c
-        files = ordered_set.OrderedSet()
db8f3c
-        try:
db8f3c
-            license_files = self.distribution.metadata.license_files
db8f3c
-        except TypeError:
db8f3c
-            log.warn("warning: 'license_files' option is malformed")
db8f3c
-            license_files = ordered_set.OrderedSet()
db8f3c
-        patterns = license_files if isinstance(license_files, ordered_set.OrderedSet) \
db8f3c
-            else ordered_set.OrderedSet(license_files)
db8f3c
-
db8f3c
-        if 'license_file' in opts:
db8f3c
-            log.warn(
db8f3c
-                "warning: the 'license_file' option is deprecated, "
db8f3c
-                "use 'license_files' instead")
db8f3c
-            patterns.append(opts['license_file'][1])
db8f3c
-
db8f3c
-        if 'license_file' not in opts and 'license_files' not in opts:
db8f3c
-            # Default patterns match the ones wheel uses
db8f3c
-            # See https://wheel.readthedocs.io/en/stable/user_guide.html
db8f3c
-            # -> 'Including license files in the generated wheel file'
db8f3c
-            patterns = ('LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*')
db8f3c
-
db8f3c
-        for pattern in patterns:
db8f3c
-            for path in iglob(pattern):
db8f3c
-                if path.endswith('~'):
db8f3c
-                    log.debug(
db8f3c
-                        "ignoring license file '%s' as it looks like a backup",
db8f3c
-                        path)
db8f3c
-                    continue
db8f3c
-
db8f3c
-                if path not in files and os.path.isfile(path):
db8f3c
-                    log.info(
db8f3c
-                        "adding license file '%s' (matched pattern '%s')",
db8f3c
-                        path, pattern)
db8f3c
-                    files.add(path)
db8f3c
-
db8f3c
-        self.filelist.extend(sorted(files))
db8f3c
diff --git a/setuptools/config.py b/setuptools/config.py
db8f3c
index af3a3bcb..ece325e2 100644
db8f3c
--- a/setuptools/config.py
db8f3c
+++ b/setuptools/config.py
db8f3c
@@ -520,6 +520,11 @@ class ConfigMetadataHandler(ConfigHandler):
db8f3c
             'obsoletes': parse_list,
db8f3c
             'classifiers': self._get_parser_compound(parse_file, parse_list),
db8f3c
             'license': exclude_files_parser('license'),
db8f3c
+            'license_file': self._deprecated_config_handler(
db8f3c
+                exclude_files_parser('license_file'),
db8f3c
+                "The license_file parameter is deprecated, "
db8f3c
+                "use license_files instead.",
db8f3c
+                DeprecationWarning),
db8f3c
             'license_files': parse_list,
db8f3c
             'description': parse_file,
db8f3c
             'long_description': parse_file,
db8f3c
diff --git a/setuptools/dist.py b/setuptools/dist.py
db8f3c
index 050388de..bc663e63 100644
db8f3c
--- a/setuptools/dist.py
db8f3c
+++ b/setuptools/dist.py
db8f3c
@@ -14,6 +14,7 @@ import distutils.dist
db8f3c
 from distutils.util import strtobool
db8f3c
 from distutils.debug import DEBUG
db8f3c
 from distutils.fancy_getopt import translate_longopt
db8f3c
+from glob import iglob
db8f3c
 import itertools
db8f3c
 
db8f3c
 from collections import defaultdict
db8f3c
@@ -117,6 +118,8 @@ def read_pkg_file(self, file):
db8f3c
         self.provides = None
db8f3c
         self.obsoletes = None
db8f3c
 
db8f3c
+    self.license_files = _read_list('license-file')
db8f3c
+
db8f3c
 
db8f3c
 def single_line(val):
db8f3c
     # quick and dirty validation for description pypa/setuptools#1390
db8f3c
@@ -199,6 +202,7 @@ def write_pkg_file(self, file):  # noqa: C901  # is too complex (14)  # FIXME
db8f3c
         for extra in self.provides_extras:
db8f3c
             write_field('Provides-Extra', extra)
db8f3c
 
db8f3c
+    self._write_list(file, 'License-File', self.license_files or [])
db8f3c
 
db8f3c
 sequence = tuple, list
db8f3c
 
db8f3c
@@ -398,7 +402,8 @@ class Distribution(_Distribution):
db8f3c
         'long_description_content_type': None,
db8f3c
         'project_urls': dict,
db8f3c
         'provides_extras': ordered_set.OrderedSet,
db8f3c
-        'license_files': ordered_set.OrderedSet,
db8f3c
+        'license_file': lambda: None,
db8f3c
+        'license_files': lambda: None,
db8f3c
     }
db8f3c
 
db8f3c
     _patched_dist = None
db8f3c
@@ -557,6 +562,34 @@ class Distribution(_Distribution):
db8f3c
         req.marker = None
db8f3c
         return req
db8f3c
 
db8f3c
+    def _finalize_license_files(self):
db8f3c
+        """Compute names of all license files which should be included."""
db8f3c
+        license_files: Optional[List[str]] = self.metadata.license_files
db8f3c
+        patterns: List[str] = license_files if license_files else []
db8f3c
+
db8f3c
+        license_file: Optional[str] = self.metadata.license_file
db8f3c
+        if license_file and license_file not in patterns:
db8f3c
+            patterns.append(license_file)
db8f3c
+
db8f3c
+        if license_files is None and license_file is None:
db8f3c
+            # Default patterns match the ones wheel uses
db8f3c
+            # See https://wheel.readthedocs.io/en/stable/user_guide.html
db8f3c
+            # -> 'Including license files in the generated wheel file'
db8f3c
+            patterns = ('LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*')
db8f3c
+
db8f3c
+        self.metadata.license_files = list(
db8f3c
+            unique_everseen(self._expand_patterns(patterns)))
db8f3c
+
db8f3c
+    @staticmethod
db8f3c
+    def _expand_patterns(patterns):
db8f3c
+        return (
db8f3c
+            path
db8f3c
+            for pattern in patterns
db8f3c
+            for path in iglob(pattern)
db8f3c
+            if not path.endswith('~')
db8f3c
+            and os.path.isfile(path)
db8f3c
+        )
db8f3c
+
db8f3c
     # FIXME: 'Distribution._parse_config_files' is too complex (14)
db8f3c
     def _parse_config_files(self, filenames=None):  # noqa: C901
db8f3c
         """
db8f3c
@@ -680,6 +713,7 @@ class Distribution(_Distribution):
db8f3c
         parse_configuration(self, self.command_options,
db8f3c
                             ignore_option_errors=ignore_option_errors)
db8f3c
         self._finalize_requires()
db8f3c
+        self._finalize_license_files()
db8f3c
 
db8f3c
     def fetch_build_eggs(self, requires):
db8f3c
         """Resolve pre-setup requirements"""
db8f3c
@@ -1020,3 +1054,21 @@ class Distribution(_Distribution):
db8f3c
 class DistDeprecationWarning(SetuptoolsDeprecationWarning):
db8f3c
     """Class for warning about deprecations in dist in
db8f3c
     setuptools. Not ignored by default, unlike DeprecationWarning."""
db8f3c
+
db8f3c
+
db8f3c
+def unique_everseen(iterable, key=None):
db8f3c
+    "List unique elements, preserving order. Remember all elements ever seen."
db8f3c
+    # unique_everseen('AAAABBBCCDAABBB') --> A B C D
db8f3c
+    # unique_everseen('ABBCcAD', str.lower) --> A B C D
db8f3c
+    seen = set()
db8f3c
+    seen_add = seen.add
db8f3c
+    if key is None:
db8f3c
+        for element in itertools.filterfalse(seen.__contains__, iterable):
db8f3c
+            seen_add(element)
db8f3c
+            yield element
db8f3c
+    else:
db8f3c
+        for element in iterable:
db8f3c
+            k = key(element)
db8f3c
+            if k not in seen:
db8f3c
+                seen_add(k)
db8f3c
+                yield element
db8f3c
diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py
db8f3c
index c93ed020..e8b49732 100644
db8f3c
--- a/setuptools/tests/test_egg_info.py
db8f3c
+++ b/setuptools/tests/test_egg_info.py
db8f3c
@@ -541,7 +541,7 @@ class TestEggInfo:
db8f3c
                               """),
db8f3c
             'MANIFEST.in': "exclude LICENSE",
db8f3c
             'LICENSE': "Test license"
db8f3c
-        }, False),  # license file is manually excluded
db8f3c
+        }, True),  # manifest is overwritten by license_file
db8f3c
         pytest.param({
db8f3c
             'setup.cfg': DALS("""
db8f3c
                               [metadata]
db8f3c
@@ -637,7 +637,7 @@ class TestEggInfo:
db8f3c
                               """),
db8f3c
             'MANIFEST.in': "exclude LICENSE",
db8f3c
             'LICENSE': "Test license"
db8f3c
-        }, [], ['LICENSE']),  # license file is manually excluded
db8f3c
+        }, ['LICENSE'], []),  # manifest is overwritten by license_files
db8f3c
         ({
db8f3c
             'setup.cfg': DALS("""
db8f3c
                               [metadata]
db8f3c
@@ -648,7 +648,8 @@ class TestEggInfo:
db8f3c
             'MANIFEST.in': "exclude LICENSE-XYZ",
db8f3c
             'LICENSE-ABC': "ABC license",
db8f3c
             'LICENSE-XYZ': "XYZ license"
db8f3c
-        }, ['LICENSE-ABC'], ['LICENSE-XYZ']),  # subset is manually excluded
db8f3c
+            # manifest is overwritten by license_files
db8f3c
+        }, ['LICENSE-ABC', 'LICENSE-XYZ'], []),
db8f3c
         pytest.param({
db8f3c
             'setup.cfg': "",
db8f3c
             'LICENSE-ABC': "ABC license",
db8f3c
@@ -678,6 +679,17 @@ class TestEggInfo:
db8f3c
             'NOTICE-XYZ': "XYZ notice",
db8f3c
             }, ['LICENSE-ABC'], ['NOTICE-XYZ'],
db8f3c
             id="no_default_glob_patterns"),
db8f3c
+        pytest.param({
db8f3c
+            'setup.cfg': DALS("""
db8f3c
+                              [metadata]
db8f3c
+                              license_files =
db8f3c
+                                  LICENSE-ABC
db8f3c
+                                  LICENSE*
db8f3c
+                              """),
db8f3c
+            'LICENSE-ABC': "ABC license",
db8f3c
+            }, ['LICENSE-ABC'], [],
db8f3c
+            id="files_only_added_once",
db8f3c
+        ),
db8f3c
     ])
db8f3c
     def test_setup_cfg_license_files(
db8f3c
             self, tmpdir_cwd, env, files, incl_licenses, excl_licenses):
db8f3c
@@ -781,8 +793,8 @@ class TestEggInfo:
db8f3c
             'LICENSE-ABC': "ABC license",
db8f3c
             'LICENSE-PQR': "PQR license",
db8f3c
             'LICENSE-XYZ': "XYZ license"
db8f3c
-            # manually excluded
db8f3c
-        }, ['LICENSE-XYZ'], ['LICENSE-ABC', 'LICENSE-PQR']),
db8f3c
+            # manifest is overwritten
db8f3c
+        }, ['LICENSE-ABC', 'LICENSE-PQR', 'LICENSE-XYZ'], []),
db8f3c
         pytest.param({
db8f3c
             'setup.cfg': DALS("""
db8f3c
                               [metadata]
db8f3c
@@ -825,6 +837,38 @@ class TestEggInfo:
db8f3c
         for lf in excl_licenses:
db8f3c
             assert sources_lines.count(lf) == 0
db8f3c
 
db8f3c
+    def test_license_file_attr_pkg_info(self, tmpdir_cwd, env):
db8f3c
+        """All matched license files should have a corresponding License-File."""
db8f3c
+        self._create_project()
db8f3c
+        build_files({
db8f3c
+            "setup.cfg": DALS("""
db8f3c
+                              [metadata]
db8f3c
+                              license_files =
db8f3c
+                                  NOTICE*
db8f3c
+                                  LICENSE*
db8f3c
+                              """),
db8f3c
+            "LICENSE-ABC": "ABC license",
db8f3c
+            "LICENSE-XYZ": "XYZ license",
db8f3c
+            "NOTICE": "included",
db8f3c
+            "IGNORE": "not include",
db8f3c
+        })
db8f3c
+
db8f3c
+        environment.run_setup_py(
db8f3c
+            cmd=['egg_info'],
db8f3c
+            pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)])
db8f3c
+        )
db8f3c
+        egg_info_dir = os.path.join('.', 'foo.egg-info')
db8f3c
+        with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file:
db8f3c
+            pkg_info_lines = pkginfo_file.read().split('\n')
db8f3c
+        license_file_lines = [
db8f3c
+            line for line in pkg_info_lines if line.startswith('License-File:')]
db8f3c
+
db8f3c
+        # Only 'NOTICE', LICENSE-ABC', and 'LICENSE-XYZ' should have been matched
db8f3c
+        # Also assert that order from license_files is keeped
db8f3c
+        assert "License-File: NOTICE" == license_file_lines[0]
db8f3c
+        assert "License-File: LICENSE-ABC" in license_file_lines[1:]
db8f3c
+        assert "License-File: LICENSE-XYZ" in license_file_lines[1:]
db8f3c
+
db8f3c
     def test_long_description_content_type(self, tmpdir_cwd, env):
db8f3c
         # Test that specifying a `long_description_content_type` keyword arg to
db8f3c
         # the `setup` function results in writing a `Description-Content-Type`
db8f3c
diff --git a/setuptools/tests/test_manifest.py b/setuptools/tests/test_manifest.py
db8f3c
index 589cefb2..82bdb9c6 100644
db8f3c
--- a/setuptools/tests/test_manifest.py
db8f3c
+++ b/setuptools/tests/test_manifest.py
db8f3c
@@ -55,7 +55,6 @@ def touch(filename):
db8f3c
 default_files = frozenset(map(make_local_path, [
db8f3c
     'README.rst',
db8f3c
     'MANIFEST.in',
db8f3c
-    'LICENSE',
db8f3c
     'setup.py',
db8f3c
     'app.egg-info/PKG-INFO',
db8f3c
     'app.egg-info/SOURCES.txt',