diff --git a/src/pyproject_fmt/cli.py b/src/pyproject_fmt/cli.py index a723377..1f8c24c 100644 --- a/src/pyproject_fmt/cli.py +++ b/src/pyproject_fmt/cli.py @@ -11,8 +11,10 @@ from pathlib import Path from typing import Sequence +from packaging.version import Version + from ._version import __version__ -from .formatter.config import DEFAULT_INDENT, Config +from .formatter.config import DEFAULT_INDENT, DEFAULT_MAX_SUPPORTED_PYTHON, Config class PyProjectFmtNamespace(Namespace): @@ -23,6 +25,7 @@ class PyProjectFmtNamespace(Namespace): indent: int check: bool keep_full_version: bool + max_supported_python: Version @property def configs(self) -> list[Config]: @@ -33,6 +36,7 @@ def configs(self) -> list[Config]: toml=toml.read_text(encoding="utf-8"), indent=self.indent, keep_full_version=self.keep_full_version, + max_supported_python=self.max_supported_python, ) for toml in self.inputs ] @@ -88,6 +92,12 @@ def _build_cli() -> ArgumentParser: default=DEFAULT_INDENT, help="number of spaces to indent", ) + parser.add_argument( + "--max-supported-python", + type=Version, + default=DEFAULT_MAX_SUPPORTED_PYTHON, + help="latest Python version the project supports (e.g. 3.13)", + ) msg = "pyproject.toml file(s) to format" parser.add_argument("inputs", nargs="+", type=pyproject_toml_path_creator, help=msg) return parser diff --git a/src/pyproject_fmt/formatter/config.py b/src/pyproject_fmt/formatter/config.py index 20db3f0..9389b9c 100644 --- a/src/pyproject_fmt/formatter/config.py +++ b/src/pyproject_fmt/formatter/config.py @@ -1,15 +1,18 @@ """Defines configuration for the formatter.""" from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import TYPE_CHECKING +from packaging.version import Version + if TYPE_CHECKING: from pathlib import Path from typing import Final DEFAULT_INDENT: Final[int] = 2 #: default indentation level +DEFAULT_MAX_SUPPORTED_PYTHON: Final[str] = "3.12" #: default maximum supported Python version @dataclass(frozen=True) @@ -20,9 +23,11 @@ class Config: toml: str #: the text to format indent: int = DEFAULT_INDENT #: indentation to apply keep_full_version: bool = False + max_supported_python: Version = field(default_factory=lambda: Version(DEFAULT_MAX_SUPPORTED_PYTHON)) __all__ = [ "Config", "DEFAULT_INDENT", + "DEFAULT_MAX_SUPPORTED_PYTHON", ] diff --git a/src/pyproject_fmt/formatter/project.py b/src/pyproject_fmt/formatter/project.py index afa4864..13f1135 100644 --- a/src/pyproject_fmt/formatter/project.py +++ b/src/pyproject_fmt/formatter/project.py @@ -20,7 +20,6 @@ from .config import Config _PY_MIN_VERSION: int = 7 -_PY_MAX_VERSION: int = 12 def fmt_project(parsed: TOMLDocument, conf: Config) -> None: # noqa: C901 @@ -45,7 +44,7 @@ def fmt_project(parsed: TOMLDocument, conf: Config) -> None: # noqa: C901 sorted_array(cast(Optional[Array], project.get("dynamic")), indent=conf.indent) if "requires-python" in project: - _add_py_classifiers(project) + _add_py_classifiers(project, py_max_version=conf.max_supported_python) sorted_array(cast(Optional[Array], project.get("classifiers")), indent=conf.indent, custom_sort="natsort") @@ -105,11 +104,11 @@ def fmt_project(parsed: TOMLDocument, conf: Config) -> None: # noqa: C901 ensure_newline_at_end(project) -def _add_py_classifiers(project: Table) -> None: +def _add_py_classifiers(project: Table, *, py_max_version: Version) -> None: specifiers = SpecifierSet(project.get("requires-python", f">=3.{_PY_MIN_VERSION}")) min_version = _get_min_version_classifier(specifiers) - max_version = _get_max_version_classifier(specifiers) + max_version = _get_max_version_classifier(specifiers, py_max_version=py_max_version) allowed_versions = list(specifiers.filter(f"3.{v}" for v in range(min_version, max_version + 1))) @@ -142,10 +141,10 @@ def _get_min_version_classifier(specifiers: SpecifierSet) -> int: min_version.append(Version(specifier.version).minor) if specifier.operator == ">": min_version.append(Version(specifier.version).minor + 1) - return min(min_version) if min_version else _PY_MIN_VERSION + return min(min_version, default=_PY_MIN_VERSION) -def _get_max_version_classifier(specifiers: SpecifierSet) -> int: +def _get_max_version_classifier(specifiers: SpecifierSet, *, py_max_version: Version) -> int: max_version: list[int] = [] for specifier in specifiers: @@ -153,7 +152,8 @@ def _get_max_version_classifier(specifiers: SpecifierSet) -> int: max_version.append(Version(specifier.version).minor) if specifier.operator == "<": max_version.append(Version(specifier.version).minor - 1) - return max(max_version) if max_version else (_get_max_version_tox() or _PY_MAX_VERSION) + + return max(max_version) if max_version else (_get_max_version_tox() or py_max_version.minor) def _get_max_version_tox() -> int | None: diff --git a/tests/formatter/test_project.py b/tests/formatter/test_project.py index d02617c..e4f37f8 100644 --- a/tests/formatter/test_project.py +++ b/tests/formatter/test_project.py @@ -6,6 +6,7 @@ from typing import TYPE_CHECKING import pytest +from packaging.version import Version from pyproject_fmt.formatter.config import Config from pyproject_fmt.formatter.project import fmt_project @@ -373,6 +374,40 @@ def test_classifier_two_upper_bounds(fmt: Fmt) -> None: fmt(fmt_project, start, expected) +def test_classifier_prerelease(fmt: Fmt) -> None: + txt = """ + [project] + requires-python = ">=3.10" + classifiers = [ + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + ] + """ + expected = """ + [project] + requires-python = ">=3.10" + classifiers = [ + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", + ] + """ + config = Config( + pyproject_toml=Path(), + toml=dedent(txt), + max_supported_python=Version("3.15"), + ) + + fmt(fmt_project, config, expected) + + def test_classifier_gt_tox(fmt: Fmt, tmp_path: Path) -> None: (tmp_path / "tox.ini").write_text("[tox]\nenv_list = py{311,312}-{magic}") start = """