Repository: pypa/setuptools

Issue:
setuptools doesn't install licenses
Originally reported by: **notting (Bitbucket: [notting](http://bitbucket.org/notting), GitHub: [notting](http://github.com/notting))**

---
1. Most python software is licensed under MIT, BSD, or similar licenses
2. These licenses require shipping a copy of the license with distributions of the software.

Ergo, setuptools should add the LICENSE or COPYING file to the egginfo/distinfo directory by default, so licenses get distributed automatically.

---
- Bitbucket: https://bitbucket.org/pypa/setuptools/issue/357



Discussion:
Would it be possible to have a `license_file` keyword argument for `setup`? This file (or directory or `list` if multiple) could then just get copied over.

The license file already comes included in the sdist (barring any mishaps by the author). As for wheels, you can add `license_file = LICENSE` option in the `[metadata]` section in `setup.cfg`. This feature has not been documented but I will rectify that once I overhaul wheel's documentation.
It appears that unless the license file is explicitly mentioned in the manifest, it will NOT be included (just tested with setuptools 36.5.0 / distutils from Py 3.6).  This is also the behavior documented in https://docs.python.org/3.6/distutils/sourcedist.html#specifying-the-files-to-distribute.

But as you point out yourself, it is easy enough for setuptools to find that information: it should read it from setup.cfg's metadata.license_file, to avoid having to duplicate it.
I think this is a reasonable enhancement. The new behavior should be that `python setup.py sdist` should include the license file if `license_file` is included in `setup.cfg` *unless* it is explicitly excluded in `MANIFEST.in`.

There's a case to be made for including anything called `LICENSE` or `LICENSE.txt` or `LICENSE.md` by default, as well, but I think that needs further discussion.
Working on this as part of PyPA Sprint Weekend at Bloomberg (2018)

Resolving patch:
diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py
--- a/setuptools/command/egg_info.py
+++ b/setuptools/command/egg_info.py
@@ -568,6 +568,7 @@ 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())
diff --git a/setuptools/command/sdist.py b/setuptools/command/sdist.py
--- a/setuptools/command/sdist.py
+++ b/setuptools/command/sdist.py
@@ -198,3 +198,24 @@ def read_manifest(self):
                 continue
             self.filelist.append(line)
         manifest.close()
+
+    def check_license(self):
+        """Checks if license_file' is configured and adds it to
+        'self.filelist' if the value contains a valid path.
+        """
+
+        opts = self.distribution.get_option_dict('metadata')
+
+        # ignore the source of the value
+        _, license_file = opts.get('license_file', (None, None))
+
+        if license_file is None:
+            log.debug("'license_file' option was not specified")
+            return
+
+        if not os.path.exists(license_file):
+            log.warn("warning: Failed to find the configured license file '%s'",
+                    license_file)
+            return
+
+        self.filelist.append(license_file)


Test patch:
diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py
--- a/setuptools/tests/test_egg_info.py
+++ b/setuptools/tests/test_egg_info.py
@@ -518,6 +518,55 @@ def test_doesnt_provides_extra(self, tmpdir_cwd, env):
             pkg_info_text = pkginfo_file.read()
         assert 'Provides-Extra:' not in pkg_info_text
 
+    @pytest.mark.parametrize("files, license_in_sources", [
+        ({
+            'setup.cfg': DALS("""
+                              [metadata]
+                              license_file = LICENSE
+                              """),
+            'LICENSE': DALS("Test license")
+        }, True), # with license
+        ({
+            'setup.cfg': DALS("""
+                              [metadata]
+                              license_file = INVALID_LICENSE
+                              """),
+            'LICENSE': DALS("Test license")
+        }, False), # with an invalid license
+        ({
+            'setup.cfg': DALS("""
+                              """),
+            'LICENSE': DALS("Test license")
+        }, False), # no license_file attribute
+        ({
+            'setup.cfg': DALS("""
+                              [metadata]
+                              license_file = LICENSE
+                              """),
+            'MANIFEST.in': DALS("exclude LICENSE"),
+            'LICENSE': DALS("Test license")
+        }, False) # license file is manually excluded
+    ])
+    def test_setup_cfg_license_file(
+            self, tmpdir_cwd, env, files, license_in_sources):
+        self._create_project()
+        build_files(files)
+
+        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, 'SOURCES.txt')) as sources_file:
+            sources_text = sources_file.read()
+
+        if license_in_sources:
+            assert 'LICENSE' in sources_text
+        else:
+            assert 'LICENSE' not in sources_text
+            assert 'INVALID_LICENSE' not in sources_text # for invalid license test
+
     def test_long_description_content_type(self, tmpdir_cwd, env):
         # Test that specifying a `long_description_content_type` keyword arg to
         # the `setup` function results in writing a `Description-Content-Type`