Skip to content

Commit

Permalink
Generate python version classifiers based on python-requires (#90)
Browse files Browse the repository at this point in the history
  • Loading branch information
gaborbernat committed Apr 28, 2023
1 parent 8f18a26 commit 2a9687d
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 4 deletions.
1 change: 1 addition & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jobs:
fail-fast: false
matrix:
py:
- "3.12.0-alpha.7"
- "3.11"
- "3.10"
- "3.9"
Expand Down
63 changes: 61 additions & 2 deletions src/pyproject_fmt/formatter/project.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,71 @@
from __future__ import annotations

import re
import subprocess
from shutil import which
from typing import Optional, cast

from packaging.utils import canonicalize_name
from tomlkit.items import Array, String, Table
from tomlkit.items import Array, String, Table, Trivia
from tomlkit.toml_document import TOMLDocument

from .config import Config
from .pep508 import normalize_pep508_array
from .util import ensure_newline_at_end, order_keys, sorted_array

_PY_MIN_VERSION: int = 7
_PY_MAX_VERSION: int = 11


def _get_max_version() -> int:
max_version = _PY_MAX_VERSION
tox = which("tox")
if tox is not None: # pragma: no branch
tox_environments = subprocess.check_output(["tox", "-aqq"], encoding="utf-8", text=True)
if not re.match(r"ROOT: No .* found, assuming empty", tox_environments):
found = set()
for env in tox_environments.split():
for part in env.split("-"):
match = re.match(r"py(\d)(\d+)", part)
if match:
found.add(int(match.groups()[1]))
if found:
max_version = max(found)
return max_version


def _add_py_classifiers(project: Table) -> None:
# update classifiers depending on requires
requires = project.get("requires-python", f">=3.{_PY_MIN_VERSION}")
if not (requires.startswith("==") or requires.startswith(">=")):
return
versions = [int(i) for i in requires[2:].split(".")[:2]]
major, minor = versions[0], versions[1] if len(versions) > 1 else _PY_MIN_VERSION
if requires.startswith(">="):
supports = [(major, i) for i in range(minor, _get_max_version() + 1)]
else:
supports = [(major, minor)]
add = [f"Programming Language :: Python :: {ma}.{mi}" for (ma, mi) in supports]
if requires.startswith(">="):
add.append("Programming Language :: Python :: 3 :: Only")
if "classifiers" in project:
classifiers: Array = cast(Array, project["classifiers"])
else:
classifiers = Array([], Trivia(), multiline=False)
project["classifiers"] = classifiers

exist = set(classifiers.unwrap())
remove = [e for e in exist if e.startswith("Programming Language :: Python ::") and e not in add]
deleted = 0
for at, item in enumerate(list(classifiers)):
if item in remove:
del classifiers[at - deleted]
deleted += 1

for entry in add:
if entry not in classifiers:
classifiers.insert(len(add), entry)


def fmt_project(parsed: TOMLDocument, conf: Config) -> None:
project = cast(Optional[Table], parsed.get("project"))
Expand All @@ -25,6 +81,10 @@ def fmt_project(parsed: TOMLDocument, conf: Config) -> None:

sorted_array(cast(Optional[Array], project.get("keywords")), indent=conf.indent)
sorted_array(cast(Optional[Array], project.get("dynamic")), indent=conf.indent)

if "requires-python" in project:
_add_py_classifiers(project)

sorted_array(cast(Optional[Array], project.get("classifiers")), indent=conf.indent, custom_sort="natsort")

normalize_pep508_array(cast(Optional[Array], project.get("dependencies")), conf.indent)
Expand All @@ -49,7 +109,6 @@ def fmt_project(parsed: TOMLDocument, conf: Config) -> None:
# license_files: Optional[LicenseFilesTable] = Field(alias="license-files")
# readme: Optional[Union[str, ReadmeTable]]
# order maintainers and authors table
# update classifiers depending on requires
# handle readme table

key_order = ["name", "version", "description", "readme", "keywords", "license", "license-files"]
Expand Down
3 changes: 2 additions & 1 deletion tests/formatter/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@


@pytest.fixture()
def fmt(mocker: MockerFixture) -> Fmt:
def fmt(monkeypatch: pytest.MonkeyPatch, mocker: MockerFixture, tmp_path: Path) -> Fmt:
def _func(formatter: Callable[[TOMLDocument, Config], None], start: str, expected: str) -> None:
mocker.patch("pyproject_fmt.formatter._perform", formatter)
opts = Config(pyproject_toml=Path(), toml=dedent(start))
monkeypatch.chdir(tmp_path)
result = format_pyproject(opts)

expected = dedent(expected)
Expand Down
101 changes: 101 additions & 0 deletions tests/formatter/test_project.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

from pathlib import Path

import pytest

from pyproject_fmt.formatter.project import fmt_project
Expand Down Expand Up @@ -149,3 +151,102 @@ def test_entry_points(fmt: Fmt) -> None:
beta = {C = "c",D = "d"}
"""
fmt(fmt_project, start, expected)


def test_classifier_lt(fmt: Fmt) -> None:
start = """
[project]
requires-python = "<=3.7"
"""
fmt(fmt_project, start, start)


def test_classifier_gt(fmt: Fmt) -> None:
start = """
[project]
requires-python = ">=3.7"
"""
expected = """
[project]
requires-python = ">=3.7"
classifiers = [
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
]
"""
fmt(fmt_project, start, expected)


def test_classifier_eq(fmt: Fmt) -> None:
start = """
[project]
requires-python="==3.11"
classifiers = [
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
]
"""
expected = """
[project]
requires-python="==3.11"
classifiers = [
"Programming Language :: Python :: 3.11",
]
"""
fmt(fmt_project, start, 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 = """
[project]
requires-python=">=3.11"
"""
expected = """
[project]
requires-python=">=3.11"
classifiers = [
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
"""
fmt(fmt_project, start, expected)


def test_classifier_gt_tox_no_py_ver(fmt: Fmt, tmp_path: Path) -> None:
(tmp_path / "tox.ini").write_text("[tox]\nenv_list = py-{magic,p12}")
start = """
[project]
requires-python=">=3.11"
"""
expected = """
[project]
requires-python=">=3.11"
classifiers = [
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.11",
]
"""
fmt(fmt_project, start, expected)


def test_classifier_gt_tox_conf_missing(fmt: Fmt) -> None:
start = """
[project]
requires-python=">=3.11"
"""
expected = """
[project]
requires-python=">=3.11"
classifiers = [
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.11",
]
"""
fmt(fmt_project, start, expected)
1 change: 1 addition & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ requires =
tox>=4.2
env_list =
fix
py312
py311
py310
py39
Expand Down
3 changes: 2 additions & 1 deletion whitelist.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Ao
ao
autoclass
autodoc
canonicalize
Expand All @@ -25,4 +25,5 @@ skipif
tofile
toml
tomlkit
tox
typehints

0 comments on commit 2a9687d

Please sign in to comment.