Repository: pypa/setuptools

Issue:
license field in setup.cfg should not accept file:
As brought to my attention by [this comment](https://github.com/pypa/setuptools/pull/1536#issuecomment-433707087), it seems that in `setup.cfg`, `license` accepts a `file:`, and the docstring of `_parse_file` even mentions `file: LICENSE`. I think this was based on a misunderstanding of what the `license` file does, and I think will lead to problems with #1390 if actually used this way.

We should remove support for `file:` in the license field.

To do on this:

- [x] Remove `file:` support [from the code](https://github.com/pypa/setuptools/blob/d6d9da82f6437d549ddc3d4ece0e2011abc5425a/setuptools/config.py#L440). I'd start with just removing it and seeing what error is raised if you have `file:` in there, but if the error message is confusing, we need to add a "throw error if it's `file:`" parser.
- [x] Remove `file:` from the [setup.cfg](https://setuptools.readthedocs.io/en/latest/setuptools.html#metadata) documentation.
- [x] Make sure there's a test using something like `license: Apache 2.0`
- [x] Add a test that `license: file:` throws an exception (Maybe?)


Discussion:
I'd like to work on this as a part of the bloomberg pypa sprint

Resolving patch:
diff --git a/setuptools/config.py b/setuptools/config.py
--- a/setuptools/config.py
+++ b/setuptools/config.py
@@ -246,6 +246,24 @@ def _parse_bool(cls, value):
         value = value.lower()
         return value in ('1', 'true', 'yes')
 
+    @classmethod
+    def _exclude_files_parser(cls, key):
+        """Returns a parser function to make sure field inputs
+        are not files.
+
+        Parses a value after getting the key so error messages are
+        more informative.
+
+        :param key:
+        :rtype: callable
+        """
+        def parser(value):
+            exclude_directive = 'file:'
+            if value.startswith(exclude_directive):
+                raise ValueError('Only strings are accepted for the {0} field, files are not accepted'.format(key))
+            return value
+        return parser
+
     @classmethod
     def _parse_file(cls, value):
         """Represents value as a string, allowing including text
@@ -255,7 +273,6 @@ def _parse_file(cls, value):
         directory with setup.py.
 
         Examples:
-            file: LICENSE
             file: README.rst, CHANGELOG.md, src/file.txt
 
         :param str value:
@@ -449,6 +466,7 @@ def parsers(self):
         parse_list = self._parse_list
         parse_file = self._parse_file
         parse_dict = self._parse_dict
+        exclude_files_parser = self._exclude_files_parser
 
         return {
             'platforms': parse_list,
@@ -460,7 +478,7 @@ def parsers(self):
                 DeprecationWarning),
             'obsoletes': parse_list,
             'classifiers': self._get_parser_compound(parse_file, parse_list),
-            'license': parse_file,
+            'license': exclude_files_parser('license'),
             'description': parse_file,
             'long_description': parse_file,
             'version': self._parse_version,


Test patch:
diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py
--- a/setuptools/tests/test_config.py
+++ b/setuptools/tests/test_config.py
@@ -1,10 +1,12 @@
 import contextlib
 import pytest
+
 from distutils.errors import DistutilsOptionError, DistutilsFileError
 from mock import patch
 from setuptools.dist import Distribution, _Distribution
 from setuptools.config import ConfigHandler, read_configuration
 from . import py2_only, py3_only
+from .textwrap import DALS
 
 class ErrConfigHandler(ConfigHandler):
     """Erroneous handler. Fails to implement required methods."""
@@ -146,6 +148,24 @@ def test_basic(self, tmpdir):
             assert metadata.download_url == 'http://test.test.com/test/'
             assert metadata.maintainer_email == 'test@test.com'
 
+    def test_license_cfg(self, tmpdir):
+        fake_env(
+            tmpdir,
+            DALS("""
+            [metadata]
+            name=foo
+            version=0.0.1
+            license=Apache 2.0
+            """)
+        )
+
+        with get_dist(tmpdir) as dist:
+            metadata = dist.metadata
+
+            assert metadata.name == "foo"
+            assert metadata.version == "0.0.1"
+            assert metadata.license == "Apache 2.0"
+
     def test_file_mixed(self, tmpdir):
 
         fake_env(
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
@@ -148,6 +148,37 @@ def test_expected_files_produced(self, tmpdir_cwd, env):
         ]
         assert sorted(actual) == expected
 
+    def test_license_is_a_string(self, tmpdir_cwd, env):
+        setup_config = DALS("""
+            [metadata]
+            name=foo
+            version=0.0.1
+            license=file:MIT
+            """)
+
+        setup_script = DALS("""
+            from setuptools import setup
+
+            setup()
+            """)
+
+        build_files({'setup.py': setup_script,
+                     'setup.cfg': setup_config})
+
+        # This command should fail with a ValueError, but because it's
+        # currently configured to use a subprocess, the actual traceback
+        # object is lost and we need to parse it from stderr
+        with pytest.raises(AssertionError) as exc:
+            self._run_egg_info_command(tmpdir_cwd, env)
+
+        # Hopefully this is not too fragile: the only argument to the
+        # assertion error should be a traceback, ending with:
+        #     ValueError: ....
+        #
+        #     assert not 1
+        tb = exc.value.args[0].split('\n')
+        assert tb[-3].lstrip().startswith('ValueError')
+
     def test_rebuilt(self, tmpdir_cwd, env):
         """Ensure timestamps are updated when the command is re-run."""
         self._create_project()
@@ -597,8 +628,8 @@ def _run_egg_info_command(self, tmpdir_cwd, env, cmd=None, output=None):
             data_stream=1,
             env=environ,
         )
-        if code:
-            raise AssertionError(data)
+        assert not code, data
+
         if output:
             assert output in data