Repository: pypa/setuptools (python)

Issue:
The 'python_requires' field is ignored in setup.cfg (not in setup.py).
I'm using setuptools version '41.0.1.post20190616' (commit 8aeff6b2c9a64e47ad2a22533d7e65c08cd4103f). 

$ cd /tmp/setup/py
$ cat setup.py
from setuptools import setup
setup(python_requires='invalid')
$ python setup.py install
error in setup command: 'python_requires' must be a string containing valid version specifiers; Invalid specifier: 'invalid'


$ cd /tmp/setup/cfg
$ cat setup.cfg 
[metadata]
python_requires=invalid
$ python setup.py install
running install
running bdist_egg
running egg_info
writing UNKNOWN.egg-info/PKG-INFO
writing dependency_links to UNKNOWN.egg-info/dependency_links.txt
writing top-level names to UNKNOWN.egg-info/top_level.txt
reading manifest file 'UNKNOWN.egg-info/SOURCES.txt'
writing manifest file 'UNKNOWN.egg-info/SOURCES.txt'
installing library code to build/bdist.linux-x86_64/egg
running install_lib
warning: install_lib: 'build/lib' does not exist -- no Python modules to install

creating build/bdist.linux-x86_64/egg
creating build/bdist.linux-x86_64/egg/EGG-INFO
copying UNKNOWN.egg-info/PKG-INFO -> build/bdist.linux-x86_64/egg/EGG-INFO
copying UNKNOWN.egg-info/SOURCES.txt -> build/bdist.linux-x86_64/egg/EGG-INFO
copying UNKNOWN.egg-info/dependency_links.txt -> build/bdist.linux-x86_64/egg/EGG-INFO
copying UNKNOWN.egg-info/top_level.txt -> build/bdist.linux-x86_64/egg/EGG-INFO
zip_safe flag not set; analyzing archive contents...
creating 'dist/UNKNOWN-0.0.0-py3.7.egg' and adding 'build/bdist.linux-x86_64/egg' to it
removing 'build/bdist.linux-x86_64/egg' (and everything under it)
Processing UNKNOWN-0.0.0-py3.7.egg
Copying UNKNOWN-0.0.0-py3.7.egg to /tmp/setup/cfg/v/lib/python3.7/site-packages
Adding UNKNOWN 0.0.0 to easy-install.pth file

Installed /tmp/setup/cfg/v/lib/python3.7/site-packages/UNKNOWN-0.0.0-py3.7.egg
Processing dependencies for UNKNOWN==0.0.0
Finished processing dependencies for UNKNOWN==0.0.0
$ echo $?
0


I thing "python setup.py install" should also fail when using setup.cfg.

Pull request description:
Crash when invalid python_requires indicated in setup.cfg
Closes #1787.

### Pull Request Checklist
- [X] Changes have tests
- [X] News fragment added in changelog.d. See [documentation](http://setuptools.readthedocs.io/en/latest/developer-guide.html#making-a-pull-request) for details


Patch:
diff --git a/changelog.d/1847.change.rst b/changelog.d/1847.change.rst
new file mode 100644
index 000000000..d3f7724e2
--- /dev/null
+++ b/changelog.d/1847.change.rst
@@ -0,0 +1,1 @@
+In declarative config, now traps errors when invalid ``python_requires`` values are supplied.
diff --git a/setuptools/config.py b/setuptools/config.py
index b6626043f..2d50e25e8 100644
--- a/setuptools/config.py
+++ b/setuptools/config.py
@@ -12,6 +12,7 @@ from importlib import import_module
 
 from distutils.errors import DistutilsOptionError, DistutilsFileError
 from setuptools.extern.packaging.version import LegacyVersion, parse
+from setuptools.extern.packaging.specifiers import SpecifierSet
 from setuptools.extern.six import string_types, PY3
 
 
@@ -554,6 +555,7 @@ class ConfigOptionsHandler(ConfigHandler):
             'packages': self._parse_packages,
             'entry_points': self._parse_file,
             'py_modules': parse_list,
+            'python_requires': SpecifierSet,
         }
 
     def _parse_packages(self, value):


Test patch:
diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py
index bc97664d4..1b94a5868 100644
--- a/setuptools/tests/test_config.py
+++ b/setuptools/tests/test_config.py
@@ -819,6 +819,40 @@ class TestOptions:
             ]
             assert sorted(dist.data_files) == sorted(expected)
 
+    def test_python_requires_simple(self, tmpdir):
+        fake_env(
+            tmpdir,
+            DALS("""
+            [options]
+            python_requires=>=2.7
+            """),
+        )
+        with get_dist(tmpdir) as dist:
+            dist.parse_config_files()
+
+    def test_python_requires_compound(self, tmpdir):
+        fake_env(
+            tmpdir,
+            DALS("""
+            [options]
+            python_requires=>=2.7,!=3.0.*
+            """),
+        )
+        with get_dist(tmpdir) as dist:
+            dist.parse_config_files()
+
+    def test_python_requires_invalid(self, tmpdir):
+        fake_env(
+            tmpdir,
+            DALS("""
+            [options]
+            python_requires=invalid
+            """),
+        )
+        with pytest.raises(Exception):
+            with get_dist(tmpdir) as dist:
+                dist.parse_config_files()
+
 
 saved_dist_init = _Distribution.__init__