From 9c94d9cd9fe4b681cdf140f14a1a58b03c0903f9 Mon Sep 17 00:00:00 2001 From: Max R Date: Fri, 16 Dec 2022 14:32:25 -0500 Subject: [PATCH] Change `--min-py-version` behavior & always set `python_requires` --- README.md | 6 +- setup_cfg_fmt.py | 42 +++-- tests/setup_cfg_fmt_test.py | 295 +++++++++++++++++++++++++++--------- 3 files changed, 245 insertions(+), 98 deletions(-) diff --git a/README.md b/README.md index cb7691c..491cebf 100644 --- a/README.md +++ b/README.md @@ -115,12 +115,14 @@ This will show up on the pypi project page ### set `python_requires` -A few sources are searched for guessing `python_requires`: +If `--min-py-version` is set, it will be used as the source of truth +for `python_requires`. +Otherwise, a few sources are searched for guessing `python_requires`: - the existing `python_requires` setting itself - `envlist` in `tox.ini` if present - python version `classifiers` that are already set -- the `--min-py-version` argument (currently defaulting to `3.7`) +- the default for the `--min-py-version` argument (currently `3.7`) ### adds python version classifiers diff --git a/setup_cfg_fmt.py b/setup_cfg_fmt.py index fddfd5d..16611c3 100644 --- a/setup_cfg_fmt.py +++ b/setup_cfg_fmt.py @@ -141,7 +141,7 @@ def _v(x: Version) -> str: def _parse_python_requires( python_requires: str | None, -) -> tuple[Version | None, set[Version]]: +) -> tuple[Version, set[Version]]: minimum = None excluded = set() @@ -155,7 +155,7 @@ def _parse_python_requires( else: raise UnknownVersionError() - return minimum, excluded + return minimum or (3, 7), excluded def _tox_envlist(setup_cfg: str) -> Generator[str, None, None]: @@ -175,8 +175,8 @@ def _tox_envlist(setup_cfg: str) -> Generator[str, None, None]: def _python_requires( - setup_cfg: str, *, min_py_version: tuple[int, int], -) -> str | None: + setup_cfg: str, *, min_py_version: tuple[int, int] | None, +) -> str: cfg = NoTransformConfigParser() cfg.read(setup_cfg) current_value = cfg.get('options', 'python_requires', fallback='') @@ -187,11 +187,14 @@ def _python_requires( except UnknownVersionError: # assume they know what's up with weird things return current_value + if min_py_version is not None: + return _format_python_requires(min_py_version, excluded) + for env in _tox_envlist(setup_cfg): match = TOX_ENV.match(env) if match: version = _to_ver(f'3.{match[1]}') - if minimum is None or version < minimum[:2]: + if version < minimum[:2]: minimum = version for classifier in classifiers.strip().splitlines(): @@ -200,15 +203,10 @@ def _python_requires( if '.' not in version_part: continue version = _to_ver(version_part) - if minimum is None or version < minimum[:2]: + if version < minimum[:2]: minimum = version - if minimum is None: - return None - elif min_py_version > minimum: - return _format_python_requires(min_py_version, excluded) - else: - return _format_python_requires(minimum, excluded) + return _format_python_requires(minimum, excluded) def _requires( @@ -274,11 +272,8 @@ def _py_classifiers( except UnknownVersionError: return None - 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] + # classifiers only use the first two segments of version + minimum = minimum[:2] versions: set[Version] = set() while minimum <= max_py_version: @@ -310,8 +305,6 @@ def _trim_py_classifiers( def _is_ok_classifier(s: str) -> bool: parts = s.split(' :: ') if ( - # can't know if it applies without a minimum - minimum is None or # handle Python :: 3 :: Only len(parts) != 3 or not s.startswith('Programming Language :: Python :: ') @@ -357,7 +350,7 @@ def _natural_sort(items: Sequence[str]) -> list[str]: def format_file( filename: str, *, include_version_classifiers: bool, - min_py_version: tuple[int, int], + min_py_version: tuple[int, int] | None, max_py_version: tuple[int, int], ) -> bool: with open(filename) as f: @@ -405,10 +398,9 @@ def format_file( ) requires = _python_requires(filename, min_py_version=min_py_version) - if requires is not None: - if not cfg.has_section('options'): - cfg.add_section('options') - cfg['options']['python_requires'] = requires + if not cfg.has_section('options'): + cfg.add_section('options') + cfg['options']['python_requires'] = requires install_requires = _requires(cfg, 'install_requires') if install_requires: @@ -514,7 +506,7 @@ def main(argv: Sequence[str] | None = None) -> int: '--min-py3-version', type=_ver_type, default=None, help=argparse.SUPPRESS, ) - parser.add_argument('--min-py-version', type=_ver_type, default=(3, 7)) + parser.add_argument('--min-py-version', type=_ver_type, default=None) parser.add_argument('--max-py-version', type=_ver_type, default=(3, 11)) args = parser.parse_args(argv) diff --git a/tests/setup_cfg_fmt_test.py b/tests/setup_cfg_fmt_test.py index 85d7004..7a35c0f 100644 --- a/tests/setup_cfg_fmt_test.py +++ b/tests/setup_cfg_fmt_test.py @@ -10,6 +10,7 @@ from setup_cfg_fmt import _normalize_lib from setup_cfg_fmt import _ver_type from setup_cfg_fmt import main +from setup_cfg_fmt import NoTransformConfigParser def test_ver_type_ok(): @@ -45,29 +46,29 @@ def test_case_insensitive_glob(s, expected): def test_noop(tmpdir): setup_cfg = tmpdir.join('setup.cfg') - setup_cfg.write( + s = ( '[metadata]\n' 'name = pkg\n' 'version = 1.0\n' + 'classifiers =\n' + ' Programming Language :: Python :: 3\n' + ' Programming Language :: Python :: 3 :: Only\n' + '\n' + '[options]\n' + 'python_requires = >=3.7\n' '\n' '[bdist_wheel]\n' - 'universal = true\n', + 'universal = true\n' ) + setup_cfg.write(s) assert not main((str(setup_cfg),)) - assert setup_cfg.read() == ( - '[metadata]\n' - 'name = pkg\n' - 'version = 1.0\n' - '\n' - '[bdist_wheel]\n' - 'universal = true\n' - ) + assert setup_cfg.read() == s @pytest.mark.parametrize( - ('input_tpl', 'expected_tpl'), + ('input_tpl', 'expected'), ( pytest.param( '[metadata]\n' @@ -94,30 +95,25 @@ def test_noop(tmpdir): ' req15 @ git+https://github.com/foo/bar.git@main\n' ' req16@git+https://github.com/biz/womp.git@tag\n', - '[metadata]\n' - 'name = pkg\n' - 'version = 1.0\n' '\n' - '[options]\n' - '{} =\n' - ' req>=2\n' - ' req-req\n' - ' req01\n' - ' req02\n' - ' req03\n' - ' req04>=1\n' - ' req05!=1,<=2\n' - ' req08==2\n' - ' req09~=7\n' - ' req10===8\n' - ' req12\n' - ' req13!=2,>=7\n' - ' req14>=1,<=2\n' - ' req15@git+https://github.com/foo/bar.git@main\n' - ' req16@git+https://github.com/biz/womp.git@tag\n' - ' req06;python_version==3.7\n' - ' req07;os_version!=windows\n' - ' req11;python_version=="3.7"\n', + 'req>=2\n' + 'req-req\n' + 'req01\n' + 'req02\n' + 'req03\n' + 'req04>=1\n' + 'req05!=1,<=2\n' + 'req08==2\n' + 'req09~=7\n' + 'req10===8\n' + 'req12\n' + 'req13!=2,>=7\n' + 'req14>=1,<=2\n' + 'req15@git+https://github.com/foo/bar.git@main\n' + 'req16@git+https://github.com/biz/womp.git@tag\n' + 'req06;python_version==3.7\n' + 'req07;os_version!=windows\n' + 'req11;python_version=="3.7"', id='normalizes requires', ), @@ -127,13 +123,17 @@ def test_noop(tmpdir): 'which', ('install_requires', 'setup_requires'), ) -def test_rewrite_requires(which, input_tpl, expected_tpl, tmpdir): +def test_rewrite_requires(which, input_tpl, expected, tmpdir): setup_cfg = tmpdir.join('setup.cfg') setup_cfg.write(input_tpl.format(which)) assert main((str(setup_cfg),)) - assert setup_cfg.read() == expected_tpl.format(which) + cfg = NoTransformConfigParser() + cfg.read(setup_cfg) + requires = cfg.get('options', which) + + assert requires == expected @pytest.mark.parametrize( @@ -150,6 +150,12 @@ def test_rewrite_requires(which, input_tpl, expected_tpl, tmpdir): '[metadata]\n' 'name = pkg\n' 'version = 1.0\n' + 'classifiers =\n' + ' Programming Language :: Python :: 3\n' + ' Programming Language :: Python :: 3 :: Only\n' + '\n' + '[options]\n' + 'python_requires = >=3.7\n' '\n' '[bdist_wheel]\n' 'universal = true\n', @@ -170,6 +176,12 @@ def test_rewrite_requires(which, input_tpl, expected_tpl, tmpdir): '[metadata]\n' 'name = pkg\n' 'version = 1.0\n' + 'classifiers =\n' + ' Programming Language :: Python :: 3\n' + ' Programming Language :: Python :: 3 :: Only\n' + '\n' + '[options]\n' + 'python_requires = >=3.7\n' '\n' '[options.packages.find]\n' 'where = src\n' @@ -186,7 +198,13 @@ def test_rewrite_requires(which, input_tpl, expected_tpl, tmpdir): '[metadata]\n' 'name = pkg_name\n' - 'version = 1.0\n', + 'version = 1.0\n' + 'classifiers =\n' + ' Programming Language :: Python :: 3\n' + ' Programming Language :: Python :: 3 :: Only\n' + '\n' + '[options]\n' + 'python_requires = >=3.7\n', id='normalizes names dashes -> underscores', ), @@ -197,15 +215,18 @@ def test_rewrite_requires(which, input_tpl, expected_tpl, tmpdir): 'classifiers =\n' ' Programming Language :: Python :: 3\n' ' License :: OSI Approved :: MIT License\n' - ' Programming Language :: Python :: 2\n', + ' Programming Language :: Python :: 3 :: Only\n', '[metadata]\n' 'name = pkg\n' 'version = 1.0\n' 'classifiers =\n' ' License :: OSI Approved :: MIT License\n' - ' Programming Language :: Python :: 2\n' - ' Programming Language :: Python :: 3\n', + ' Programming Language :: Python :: 3\n' + ' Programming Language :: Python :: 3 :: Only\n' + '\n' + '[options]\n' + 'python_requires = >=3.7\n', id='sorts classifiers', ), @@ -224,7 +245,13 @@ def test_rewrite_requires(which, input_tpl, expected_tpl, tmpdir): 'author_email = john@example.com\n' 'maintainer = jane\n' 'maintainer_email = jane@example.com\n' - 'license = foo\n', + 'license = foo\n' + 'classifiers =\n' + ' Programming Language :: Python :: 3\n' + ' Programming Language :: Python :: 3 :: Only\n' + '\n' + '[options]\n' + 'python_requires = >=3.7\n', id='orders authors and maintainers', ), @@ -237,7 +264,13 @@ def test_rewrite_requires(which, input_tpl, expected_tpl, tmpdir): '[metadata]\n' 'name = pkg\n' 'author_email = john@example.com\n' - 'maintainer_email = jane@example.com\n', + 'maintainer_email = jane@example.com\n' + 'classifiers =\n' + ' Programming Language :: Python :: 3\n' + ' Programming Language :: Python :: 3 :: Only\n' + '\n' + '[options]\n' + 'python_requires = >=3.7\n', id='normalize dashes to underscores in keys', ), @@ -294,6 +327,12 @@ def test_adds_long_description_with_readme(filename, content_type, tmpdir): f'version = 1.0\n' f'long_description = file: {filename}\n' f'long_description_content_type = {content_type}\n' + f'classifiers =\n' + f' Programming Language :: Python :: 3\n' + f' Programming Language :: Python :: 3 :: Only\n' + f'\n' + f'[options]\n' + f'python_requires = >=3.7\n' ) @@ -328,6 +367,12 @@ def test_readme_discover_does_not_prefer_adoc(filename, content_type, tmpdir): f'version = 1.0\n' f'long_description = file: {filename}\n' f'long_description_content_type = {content_type}\n' + f'classifiers =\n' + f' Programming Language :: Python :: 3\n' + f' Programming Language :: Python :: 3 :: Only\n' + f'\n' + f'[options]\n' + f'python_requires = >=3.7\n' ) @@ -356,6 +401,12 @@ def test_readme_discover_uses_asciidoc_if_none_other_found(filename, tmpdir): f'version = 1.0\n' f'long_description = file: {filename}\n' f'long_description_content_type = text/plain\n' + f'classifiers =\n' + f' Programming Language :: Python :: 3\n' + f' Programming Language :: Python :: 3 :: Only\n' + f'\n' + f'[options]\n' + f'python_requires = >=3.7\n' ) @@ -365,15 +416,29 @@ def test_readme_discover_prefers_file_over_directory(tmpdir): setup_cfg = tmpdir.join('setup.cfg') setup_cfg.write( '[metadata]\n' - 'name = pkg\n', + 'name = pkg\n' + 'version = 1.0\n' + 'classifiers =\n' + ' Programming Language :: Python :: 3\n' + ' Programming Language :: Python :: 3 :: Only\n' + '\n' + '[options]\n' + 'python_requires = >=3.7\n', ) assert main((str(setup_cfg),)) assert setup_cfg.read() == ( '[metadata]\n' 'name = pkg\n' + 'version = 1.0\n' 'long_description = file: README.md\n' 'long_description_content_type = text/markdown\n' + 'classifiers =\n' + ' Programming Language :: Python :: 3\n' + ' Programming Language :: Python :: 3 :: Only\n' + '\n' + '[options]\n' + 'python_requires = >=3.7\n' ) @@ -386,7 +451,13 @@ def test_sets_license_file_if_license_exists(filename, tmpdir): setup_cfg.write( '[metadata]\n' 'name = pkg\n' - 'version = 1.0\n', + 'version = 1.0\n' + 'classifiers =\n' + ' Programming Language :: Python :: 3\n' + ' Programming Language :: Python :: 3 :: Only\n' + '\n' + '[options]\n' + 'python_requires = >=3.7\n', ) assert main((str(setup_cfg),)) @@ -396,6 +467,12 @@ def test_sets_license_file_if_license_exists(filename, tmpdir): f'name = pkg\n' f'version = 1.0\n' f'license_file = {filename}\n' + f'classifiers =\n' + f' Programming Language :: Python :: 3\n' + f' Programming Language :: Python :: 3 :: Only\n' + f'\n' + f'[options]\n' + f'python_requires = >=3.7\n' ) @@ -405,24 +482,23 @@ def test_license_does_not_match_directories(tmpdir): def test_license_does_not_set_when_licenses_matches(tmpdir): + s = '[metadata]\n' \ + 'name = pkg\n' \ + 'version = 1.0\n' \ + 'license_files = LICENSE\n' \ + 'classifiers =\n' \ + ' Programming Language :: Python :: 3\n' \ + ' Programming Language :: Python :: 3 :: Only\n' \ + '\n' \ + '[options]\n' \ + 'python_requires = >=3.7\n' + tmpdir.join('LICENSE').write('COPYRIGHT (C) 2019 ME') setup_cfg = tmpdir.join('setup.cfg') - setup_cfg.write( - '[metadata]\n' - 'name = pkg\n' - 'version = 1.0\n' - 'license_files = LICENSE\n', - ) + setup_cfg.write(s) assert not main((str(setup_cfg),)) - assert setup_cfg.read() == ( - '[metadata]\n' - 'name = pkg\n' - 'version = 1.0\n' - 'license_files = LICENSE\n' - ) - def test_license_does_set_when_licenses_mismatches(tmpdir): tmpdir.join('LICENSE').write('COPYRIGHT (C) 2019 ME') @@ -442,6 +518,12 @@ def test_license_does_set_when_licenses_mismatches(tmpdir): 'version = 1.0\n' 'license_file = LICENSE\n' 'license_files = LICENSES\n' + 'classifiers =\n' + ' Programming Language :: Python :: 3\n' + ' Programming Language :: Python :: 3 :: Only\n' + '\n' + '[options]\n' + 'python_requires = >=3.7\n' ) @@ -467,6 +549,11 @@ def test_rewrite_sets_license_type_and_classifier(tmpdir): 'license_file = LICENSE\n' 'classifiers =\n' ' License :: OSI Approved :: MIT License\n' + ' Programming Language :: Python :: 3\n' + ' Programming Language :: Python :: 3 :: Only\n' + '\n' + '[options]\n' + 'python_requires = >=3.7\n' ) @@ -510,6 +597,11 @@ def test_rewrite_identifies_license(tmpdir): 'license_file = LICENSE\n' 'classifiers =\n' ' License :: OSI Approved :: zlib/libpng License\n' + ' Programming Language :: Python :: 3\n' + ' Programming Language :: Python :: 3 :: Only\n' + '\n' + '[options]\n' + 'python_requires = >=3.7\n' ) @@ -535,7 +627,7 @@ def test_python_requires_left_alone(tmpdir, s): assert not main(( str(setup_cfg), '--include-version-classifiers', - '--min-py3-version=3.2', + '--min-py-version=3.2', )) assert setup_cfg.read() == ( @@ -558,14 +650,19 @@ def test_python_requires_left_alone(tmpdir, s): 'py_modules = pkg\n', '\n' '[options]\n' - 'py_modules = pkg\n', + 'py_modules = pkg\n' + 'python_requires = >=3.7\n', id='only empty options removed', ), pytest.param( '\n' '[options]\n' - 'dependency_links = \n', - '', + 'python_requires = >=3.7\n' + '\n' + '[empty]\n', + '\n' + '[options]\n' + 'python_requires = >=3.7\n', id='entire section removed if all empty options are removed', ), ), @@ -576,6 +673,9 @@ def test_strips_empty_options_and_sections(tmpdir, section, expected): '[metadata]\n' 'name = pkg\n' 'version = 1.0\n' + 'classifiers =\n' + ' Programming Language :: Python :: 3\n' + ' Programming Language :: Python :: 3 :: Only\n' f'{section}', ) @@ -584,6 +684,9 @@ def test_strips_empty_options_and_sections(tmpdir, section, expected): '[metadata]\n' 'name = pkg\n' 'version = 1.0\n' + 'classifiers =\n' + ' Programming Language :: Python :: 3\n' + ' Programming Language :: Python :: 3 :: Only\n' f'{expected}' ) @@ -600,7 +703,6 @@ def test_guess_python_requires_python2_tox_ini(tmpdir): assert main(( str(setup_cfg), '--include-version-classifiers', - '--min-py-version=3.4', '--max-py-version=3.7', )) @@ -633,7 +735,6 @@ def test_guess_python_requires_tox_ini_dashed_name(tmpdir): assert main(( str(setup_cfg), '--include-version-classifiers', - '--min-py-version=3.4', '--max-py-version=3.7', )) @@ -664,7 +765,6 @@ def test_guess_python_requires_tox_ini_py310(tmpdir): assert main(( str(setup_cfg), '--include-version-classifiers', - '--min-py3-version=3.4', )) assert setup_cfg.read() == ( @@ -674,12 +774,15 @@ def test_guess_python_requires_tox_ini_py310(tmpdir): 'classifiers =\n' ' Programming Language :: Python :: 3\n' ' Programming Language :: Python :: 3 :: Only\n' + ' Programming Language :: Python :: 3.7\n' + ' Programming Language :: Python :: 3.8\n' + ' Programming Language :: Python :: 3.9\n' ' Programming Language :: Python :: 3.10\n' ' Programming Language :: Python :: 3.11\n' ' Programming Language :: Python :: Implementation :: CPython\n' '\n' '[options]\n' - 'python_requires = >=3.10\n' + 'python_requires = >=3.7\n' ) @@ -694,16 +797,19 @@ def test_guess_python_requires_ignores_insufficient_version_envs(tmpdir): ' Programming Language :: Python :: Implementation :: CPython\n', ) - assert not main(( - str(setup_cfg), '--min-py-version=3.4', '--max-py-version=3.7', - )) + assert main((str(setup_cfg), '--max-py-version=3.7')) 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 :: Implementation :: CPython\n' + '\n' + '[options]\n' + 'python_requires = >=3.7\n' ) @@ -721,7 +827,6 @@ def test_guess_python_requires_from_classifiers(tmpdir): assert main(( str(setup_cfg), '--include-version-classifiers', - '--min-py-version=3.4', '--max-py-version=3.7', )) @@ -841,7 +946,6 @@ def test_python_requires_with_patch_version(tmpdir): assert main(( str(setup_cfg), '--include-version-classifiers', - '--min-py-version=3.4', '--max-py-version=3.8', )) @@ -927,12 +1031,14 @@ def test_min_py3_version_less_than_minimum(tmpdir): 'classifiers =\n' ' Programming Language :: Python :: 3\n' ' Programming Language :: Python :: 3 :: Only\n' + ' Programming Language :: Python :: 3.4\n' + ' Programming Language :: Python :: 3.5\n' ' Programming Language :: Python :: 3.6\n' ' Programming Language :: Python :: 3.7\n' ' Programming Language :: Python :: Implementation :: CPython\n' '\n' '[options]\n' - 'python_requires = >=3.6\n' + 'python_requires = >=3.4\n' ) @@ -940,6 +1046,12 @@ def test_rewrite_extras(tmpdir): setup_cfg_content = ( '[metadata]\n' 'name = test\n' + 'classifiers =\n' + ' Programming Language :: Python :: 3\n' + ' Programming Language :: Python :: 3 :: Only\n' + '\n' + '[options]\n' + 'python_requires = >=3.7\n' '[options.extras_require]\n' 'dev =\n' ' pytest\n' @@ -957,6 +1069,12 @@ def test_rewrite_extras(tmpdir): assert setup_cfg.read() == ( '[metadata]\n' 'name = test\n' + 'classifiers =\n' + ' Programming Language :: Python :: 3\n' + ' Programming Language :: Python :: 3 :: Only\n' + '\n' + '[options]\n' + 'python_requires = >=3.7\n' '\n' '[options.extras_require]\n' 'ci =\n' @@ -1115,6 +1233,36 @@ def test_version_classifiers_removed_by_default(tmpdir): ) +def test_deprecated_min_py3_version(tmpdir, capsys): + setup_cfg = tmpdir.join('setup.cfg') + setup_cfg.write( + '[metadata]\n' + 'name = test\n' + 'version = 1.0\n', + ) + + assert main((str(setup_cfg), '--min-py3-version=3.7')) + + assert setup_cfg.read() == ( + '[metadata]\n' + 'name = test\n' + 'version = 1.0\n' + 'classifiers =\n' + ' Programming Language :: Python :: 3\n' + ' Programming Language :: Python :: 3 :: Only\n' + '\n' + '[options]\n' + 'python_requires = >=3.7\n' + ) + + out, err = capsys.readouterr() + assert out == f'Rewriting {setup_cfg}\n' + assert err == ( + 'WARNING: setup-cfg-fmt will replace --min-py3-version with ' + '--min-py-version in a future release\n' + ) + + def test_leaves_casing_of_unrelated_settings(tmpdir): setup_cfg = tmpdir.join('setup.cfg') setup_cfg.write( @@ -1122,8 +1270,13 @@ def test_leaves_casing_of_unrelated_settings(tmpdir): 'name = pkg\n' 'version = 1.0\n' 'classifiers =\n' + ' Programming Language :: Python :: 3\n' + ' Programming Language :: Python :: 3 :: Only\n' ' Programming Language :: Python :: Implementation :: CPython\n' '\n' + '[options]\n' + 'python_requires = >=3.7\n' + '\n' '[tool:pytest]\n' 'DJANGO_SETTINGS_MODULE = test.test\n', )