Skip to content

Commit

Permalink
Change min-py3-version behavior
Browse files Browse the repository at this point in the history
  • Loading branch information
mxr committed Dec 11, 2022
1 parent aff5adb commit ebc691c
Show file tree
Hide file tree
Showing 3 changed files with 173 additions and 116 deletions.
8 changes: 3 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,13 @@ 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 passed, it will be used to set `python_requires`.

Otherwise, the following sources are searched:

- the existing `python_requires` setting itself
- `envlist` in `tox.ini` if present
- python version `classifiers` that are already set
- the `--min-py3-version` argument (currently defaulting to `3.7`)

If the minimum version is detected as python2, the `--min-py3-version`
argument will be used to exclude python3.x versions (see below).
Expand All @@ -151,8 +152,6 @@ classifiers are generated based on:
name = pkg
version = 1.0
+classifiers =
+ Programming Language :: Python :: 2
+ Programming Language :: Python :: 2.7
+ Programming Language :: Python :: 3
+ Programming Language :: Python :: 3.7
+ Programming Language :: Python :: 3.8
Expand All @@ -167,7 +166,6 @@ without `--include-version-classifiers` only the major version will be included:
name = pkg
version = 1.0
+classifiers =
+ Programming Language :: Python :: 2
+ Programming Language :: Python :: 3
```

Expand Down
107 changes: 71 additions & 36 deletions setup_cfg_fmt.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ def sort_key(filename: str) -> tuple[bool, str]:
return None


def _py3_excluded(min_py3_version: tuple[int, int]) -> set[tuple[int, int]]:
_, end = min_py3_version
def _py3_excluded(min_py_version: tuple[int, int]) -> set[tuple[int, int]]:
_, end = min_py_version
return {(3, i) for i in range(end)}


Expand Down Expand Up @@ -162,6 +162,33 @@ def _parse_python_requires(
return minimum, excluded


def _parse_envlist(setup_cfg: str) -> Version | None:
return min(
(
_to_ver('.'.join(env[2:]))
for env in _tox_envlist(setup_cfg)
if (
env.startswith('py') and
len(env) == 4 and
env[2:].isdigit()
)
),
default=None,
)


def _parse_classifiers(classifiers: str) -> Version | None:
def gen() -> Generator[Version, None, None]:
for classifier in classifiers.strip().splitlines():
if classifier.startswith('Programming Language :: Python ::'):
version_part = classifier.split()[-1]
if '.' not in version_part:
continue
yield _to_ver(version_part)

return min(gen(), default=None)


def _tox_envlist(setup_cfg: str) -> Generator[str, None, None]:
tox_ini = _adjacent_filename(setup_cfg, 'tox.ini')
if os.path.exists(tox_ini):
Expand All @@ -176,46 +203,43 @@ def _tox_envlist(setup_cfg: str) -> Generator[str, None, None]:


def _python_requires(
setup_cfg: str, *, min_py3_version: tuple[int, int],
setup_cfg: str, *, min_py_version: tuple[int, int],
) -> str | None:
cfg = NoTransformConfigParser()
cfg.read(setup_cfg)
current_value = cfg.get('options', 'python_requires', fallback='')
classifiers = cfg.get('metadata', 'classifiers', fallback='')

try:
minimum, excluded = _parse_python_requires(current_value)
min_from_py_requires, excluded = _parse_python_requires(current_value)
except UnknownVersionError: # assume they know what's up with weird things
return current_value

for env in _tox_envlist(setup_cfg):
if (
env.startswith('py') and
len(env) == 4 and
env[2:].isdigit()
):
version = _to_ver('.'.join(env[2:]))
if minimum is None or version < minimum[:2]:
minimum = version

for classifier in classifiers.strip().splitlines():
if classifier.startswith('Programming Language :: Python ::'):
version_part = classifier.split()[-1]
if '.' not in version_part:
continue
version = _to_ver(version_part)
if minimum is None or version < minimum[:2]:
minimum = version

if minimum is None:
return None
elif minimum[0] == 2:
excluded.update(_py3_excluded(min_py3_version))
return _format_python_requires(minimum, excluded)
elif min_py3_version > minimum:
return _format_python_requires(min_py3_version, excluded)
if isinstance(min_py_version, ImplicitlySetVersion):
min_from_envlist = _parse_envlist(setup_cfg)
min_from_classifiers = _parse_classifiers(classifiers)

mins = tuple(
v for v in (
min_from_py_requires,
min_from_envlist,
min_from_classifiers,
)
if v
)
if not mins:
return None

min_overall = min(mins, key=lambda v: v[:2])
else:
return _format_python_requires(minimum, excluded)
min_overall = min_py_version

if min_overall[0] == 2:
excluded.update(_py3_excluded(min_py_version))

excluded.difference_update({e for e in excluded if e < min_overall})

return _format_python_requires(min_overall, excluded)


def _requires(
Expand Down Expand Up @@ -368,7 +392,7 @@ def _natural_sort(items: Sequence[str]) -> list[str]:
def format_file(
filename: str, *,
include_version_classifiers: bool,
min_py3_version: tuple[int, int],
min_py_version: tuple[int, int],
max_py_version: tuple[int, int],
) -> bool:
with open(filename) as f:
Expand Down Expand Up @@ -415,7 +439,7 @@ def format_file(
f'\n{LICENSE_TO_CLASSIFIER[license_id]}'
)

requires = _python_requires(filename, min_py3_version=min_py3_version)
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')
Expand Down Expand Up @@ -515,20 +539,31 @@ def _ver_type(s: str) -> Version:
return version


class ImplicitlySetVersion(Version):
def __new__(self, x: int, y: int) -> ImplicitlySetVersion:
return tuple.__new__(ImplicitlySetVersion, (x, y))


def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*')
parser.add_argument('--include-version-classifiers', action='store_true')
parser.add_argument('--min-py3-version', type=_ver_type, default=(3, 7))
parser.add_argument('--max-py-version', type=_ver_type, default=(3, 11))
parser.add_argument(
'--min-py-version',
type=_ver_type, default=ImplicitlySetVersion(3, 7),
)
parser.add_argument(
'--max-py-version',
type=_ver_type, default=ImplicitlySetVersion(3, 11),
)
args = parser.parse_args(argv)

retv = 0
for filename in args.filenames:
if format_file(
filename,
include_version_classifiers=args.include_version_classifiers,
min_py3_version=args.min_py3_version,
min_py_version=args.min_py_version,
max_py_version=args.max_py_version,
):
print(f'Rewriting {filename}')
Expand Down
Loading

0 comments on commit ebc691c

Please sign in to comment.