Repository: asottile/setup-cfg-fmt

Issue:
python_requires>=3.6.1 should be detected as needing classifiers for 3.6+
currently I think `setup-cfg-fmt` ignores this?

Discussion:

Patch:
diff --git a/setup.cfg b/setup.cfg
index d93b1b6..a8c0cb8 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -16,6 +16,7 @@ classifiers =
     Programming Language :: Python :: 3.6
     Programming Language :: Python :: 3.7
     Programming Language :: Python :: 3.8
+    Programming Language :: Python :: 3.9
 
 [options]
 py_modules = setup_cfg_fmt
diff --git a/setup_cfg_fmt.py b/setup_cfg_fmt.py
index 75b0c2c..d12d92c 100644
--- a/setup_cfg_fmt.py
+++ b/setup_cfg_fmt.py
@@ -14,6 +14,7 @@ from typing import Tuple
 
 from identify import identify
 
+Version = Tuple[int, ...]
 
 KEYS_ORDER: Tuple[Tuple[str, Tuple[str, ...]], ...] = (
     (
@@ -103,10 +104,7 @@ def _py3_excluded(min_py3_version: Tuple[int, int]) -> Set[Tuple[int, int]]:
     return {(3, i) for i in range(end)}
 
 
-def _format_python_requires(
-        minimum: Tuple[int, int],
-        excluded: Set[Tuple[int, int]],
-) -> str:
+def _format_python_requires(minimum: Version, excluded: Set[Version]) -> str:
     return ', '.join((
         f'>={_v(minimum)}', *(f'!={_v(v)}.*' for v in sorted(excluded)),
     ))
@@ -116,21 +114,21 @@ class UnknownVersionError(ValueError):
     pass
 
 
-def _to_ver(s: str) -> Tuple[int, int]:
+def _to_ver(s: str) -> Version:
     parts = [part for part in s.split('.') if part != '*']
-    if len(parts) != 2:
+    if len(parts) < 2:
         raise UnknownVersionError()
     else:
-        return int(parts[0]), int(parts[1])
+        return tuple(int(part) for part in parts)
 
 
-def _v(x: Tuple[int, ...]) -> str:
+def _v(x: Version) -> str:
     return '.'.join(str(p) for p in x)
 
 
 def _parse_python_requires(
-    python_requires: Optional[str],
-) -> Tuple[Optional[Tuple[int, int]], Set[Tuple[int, int]]]:
+        python_requires: Optional[str],
+) -> Tuple[Optional[Version], Set[Version]]:
     minimum = None
     excluded = set()
 
@@ -176,7 +174,7 @@ def _python_requires(
                         env[2:].isdigit()
                 ):
                     version = _to_ver('.'.join(env[2:]))
-                    if minimum is None or version < minimum:
+                    if minimum is None or version < minimum[:2]:
                         minimum = version
 
     for classifier in classifiers.strip().splitlines():
@@ -185,7 +183,7 @@ def _python_requires(
             if '.' not in version_part:
                 continue
             version = _to_ver(version_part)
-            if minimum is None or version < minimum:
+            if minimum is None or version < minimum[:2]:
                 minimum = version
 
     if minimum is None:
@@ -263,8 +261,11 @@ def _py_classifiers(
 
     if minimum is None:  # don't have a sequence of versions to iterate over
         return None
+    else:
+        # classifiers only use the first two segments of version
+        minimum = minimum[:2]
 
-    versions: Set[Tuple[int, ...]] = set()
+    versions: Set[Version] = set()
     while minimum <= max_py_version:
         if minimum not in exclude:
             versions.add(minimum)
@@ -437,11 +438,16 @@ def _clean_sections(cfg: configparser.ConfigParser) -> None:
             cfg.pop(section)
 
 
-def _ver_type(s: str) -> Tuple[int, int]:
+def _ver_type(s: str) -> Version:
     try:
-        return _to_ver(s)
+        version = _to_ver(s)
     except UnknownVersionError:
+        version = ()
+
+    if len(version) != 2:
         raise argparse.ArgumentTypeError(f'expected #.#, got {s!r}')
+    else:
+        return version
 
 
 def main(argv: Optional[Sequence[str]] = None) -> int:


Test patch:
diff --git a/tests/setup_cfg_fmt_test.py b/tests/setup_cfg_fmt_test.py
index 677291b..9cfa690 100644
--- a/tests/setup_cfg_fmt_test.py
+++ b/tests/setup_cfg_fmt_test.py
@@ -20,6 +20,13 @@ def test_ver_type_error():
     assert msg == "expected #.#, got '1.2.3'"
 
 
+def test_ver_type_not_a_version():
+    with pytest.raises(argparse.ArgumentTypeError) as excinfo:
+        _ver_type('wat')
+    msg, = excinfo.value.args
+    assert msg == "expected #.#, got 'wat'"
+
+
 @pytest.mark.parametrize(
     ('s', 'expected'),
     (
@@ -381,6 +388,7 @@ freely, subject to the following restrictions:
             id='already correct',
         ),
         pytest.param('~=3.6', id='weird comparator'),
+        pytest.param('>=3', id='not enough version segments'),
     ),
 )
 def test_python_requires_left_alone(tmpdir, s):
@@ -662,6 +670,45 @@ def test_min_version_removes_classifiers(tmpdir):
     )
 
 
+def test_python_requires_with_patch_version(tmpdir):
+    setup_cfg = tmpdir.join('setup.cfg')
+    setup_cfg.write(
+        '[metadata]\n'
+        'name = pkg\n'
+        'version = 1.0\n'
+        'classifiers =\n'
+        '    Programming Language :: Python :: 3\n'
+        '    Programming Language :: Python :: 3 :: Only\n'
+        # added this to make sure that it doesn't revert to 3.6
+        '    Programming Language :: Python :: 3.6\n'
+        '    Programming Language :: Python :: 3.7\n'
+        '\n'
+        '[options]\n'
+        'python_requires = >=3.6.1\n',
+    )
+
+    # added this to make sure it doesn't revert to 3.6
+    tmpdir.join('tox.ini').write('[tox]\nenvlist=py36\n')
+
+    args = (str(setup_cfg), '--min-py3-version=3.4', '--max-py-version=3.8')
+    assert main(args)
+
+    assert setup_cfg.read() == (
+        '[metadata]\n'
+        'name = pkg\n'
+        'version = 1.0\n'
+        'classifiers =\n'
+        '    Programming Language :: Python :: 3\n'
+        '    Programming Language :: Python :: 3 :: Only\n'
+        '    Programming Language :: Python :: 3.6\n'
+        '    Programming Language :: Python :: 3.7\n'
+        '    Programming Language :: Python :: 3.8\n'
+        '\n'
+        '[options]\n'
+        'python_requires = >=3.6.1\n'
+    )
+
+
 def test_classifiers_left_alone_for_odd_python_requires(tmpdir):
     setup_cfg = tmpdir.join('setup.cfg')
     setup_cfg.write(