diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 1fbb688..4f2d813 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -85,7 +85,7 @@ jobs: - name: build package run: python -m build --sdist --wheel . -o dist - name: publish to PyPI - uses: pypa/gh-action-pypi-publish@v1.6.4 + uses: pypa/gh-action-pypi-publish@v1.8.5 with: skip_existing: true user: __token__ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8429cc3..15f96f8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: hooks: - id: isort - repo: https://github.com/psf/black - rev: 23.1.0 + rev: 23.3.0 hooks: - id: black args: [ --safe ] @@ -33,22 +33,30 @@ repos: rev: 1.13.0 hooks: - id: blacken-docs - additional_dependencies: [ black==23.1 ] + additional_dependencies: [ black==23.3 ] - repo: https://github.com/tox-dev/tox-ini-fmt - rev: "0.6.1" + rev: "1.3.0" hooks: - id: tox-ini-fmt args: [ "-p", "fix" ] + - repo: https://github.com/tox-dev/pyproject-fmt + rev: "0.9.2" + hooks: + - id: pyproject-fmt - repo: https://github.com/PyCQA/flake8 rev: 6.0.0 hooks: - id: flake8 additional_dependencies: - - flake8-bugbear==23.1.20 - - flake8-comprehensions==3.10.1 - - flake8-pytest-style==1.7 + - flake8-bugbear==23.3.23 + - flake8-comprehensions==3.12 + - flake8-pytest-style==1.7.2 - flake8-spellcheck==0.28 - flake8-unused-arguments==0.0.13 - - flake8-noqa==1.3 + - flake8-noqa==1.3.1 - pep8-naming==0.13.3 - - flake8-pyproject==1.2.2 + - flake8-pyproject==1.2.3 + - repo: https://github.com/tox-dev/pyproject-fmt + rev: "0.9.2" + hooks: + - id: pyproject-fmt diff --git a/README.md b/README.md index b2a3cf6..09a8812 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Apply a consistent format to `pyproject.toml` files. ```yaml - repo: https://github.com/tox-dev/pyproject-fmt - rev: "0.9.0" + rev: "0.9.2" hooks: - id: pyproject-fmt ``` diff --git a/pyproject.toml b/pyproject.toml index 8aa05be..bdbfb6a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,11 +2,11 @@ build-backend = "hatchling.build" requires = [ "hatch-vcs>=0.3", - "hatchling>=1.13", + "hatchling>=1.14", ] [project] -name = "pyproject_fmt" +name = "pyproject-fmt" description = "Format your pyproject.toml file" readme = "README.md" keywords = [ @@ -31,27 +31,28 @@ dynamic = [ "version", ] dependencies = [ - "packaging>=23", - "tomlkit>=0.11.6", - 'typing-extensions>=4.4; python_version < "3.8"', + "natsort>=8.3.1", + "packaging>=23.1", + "tomlkit>=0.11.7", + 'typing-extensions>=4.5; python_version < "3.8"', ] optional-dependencies.docs = [ - "furo>=2022.12.7", + "furo>=2023.3.27", "sphinx>=6.1.3", "sphinx-argparse-cli>=1.11", - "sphinx-autodoc-typehints!=1.23.4,>=1.22", - "sphinx-copybutton>=0.5.1", + "sphinx-autodoc-typehints!=1.23.4,>=1.23", + "sphinx-copybutton>=0.5.2", ] optional-dependencies.test = [ - "covdefaults>=2.2.2", - "pytest>=7.2.1", + "covdefaults>=2.3", + "pytest>=7.3.1", "pytest-cov>=4", "pytest-mock>=3.10", ] urls."Bug Tracker" = "https://github.com/tox-dev/pyproject-fmt/issues" +urls."Changelog" = "https://github.com/tox-dev/pyproject-fmt/releases" urls.Documentation = "https://github.com/tox-dev/pyproject-fmt/" urls."Source Code" = "https://github.com/tox-dev/pyproject-fmt" -urls."Changelog" = "https://github.com/tox-dev/pyproject-fmt/releases" scripts.pyproject-fmt = "pyproject_fmt.__main__:run" [tool.hatch] diff --git a/src/pyproject_fmt/formatter/project.py b/src/pyproject_fmt/formatter/project.py index bcefd5d..5d3dfa0 100644 --- a/src/pyproject_fmt/formatter/project.py +++ b/src/pyproject_fmt/formatter/project.py @@ -25,6 +25,7 @@ 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) + 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) if "optional-dependencies" in project: diff --git a/src/pyproject_fmt/formatter/util.py b/src/pyproject_fmt/formatter/util.py index 32383f5..1216b77 100644 --- a/src/pyproject_fmt/formatter/util.py +++ b/src/pyproject_fmt/formatter/util.py @@ -3,8 +3,9 @@ import sys from collections import defaultdict from dataclasses import dataclass, field -from typing import Any, Callable, Sequence +from typing import Any, Callable, Iterable, Sequence, TypeVar +from natsort import natsorted from tomlkit.container import OutOfOrderTableProxy from tomlkit.items import ( AbstractTable, @@ -36,6 +37,14 @@ def __gt__(self, __other: Any) -> bool: # noqa: U101 ... +T = TypeVar("T") + + +class SortingFunction(Protocol[T]): + def __call__(self, __seq: Iterable[T], key: Callable[[T], SupportsDunderLT]) -> list[T]: # noqa: U100, U101 + ... + + def sort_inline_table(item: tuple[str, Any | Table]) -> str: key, value = item return f"{key}{'-'.join(value) if isinstance(value, Table) else ''}" @@ -75,7 +84,10 @@ class ArrayEntries: def sorted_array( - array: Array | None, indent: int, key: Callable[[ArrayEntries], str] = lambda e: str(e.text).lower() + array: Array | None, + indent: int, + key: Callable[[ArrayEntries], str] = lambda e: str(e.text).lower(), + custom_sort: str | None = None, ) -> None: if array is None: return @@ -94,7 +106,12 @@ def sorted_array( indent_text = " " * indent for start_entry in start: body.append(_ArrayItemGroup(indent=Whitespace(f"\n{indent_text}"), comment=start_entry)) - for element in sorted(entries, key=key): + sort_method: SortingFunction[ArrayEntries] + if custom_sort == "natsort": + sort_method = natsorted + else: + sort_method = sorted # type: ignore[assignment] + for element in sort_method(entries, key=key): if element.comments: com = " ".join(i.trivia.comment[1:].strip() for i in element.comments) comment = Comment(Trivia(comment=f" # {com}", trail="")) @@ -108,12 +125,12 @@ def sorted_array( def ensure_newline_at_end(body: Table) -> None: - content = body + content: Table = body while True: if isinstance(content, AoT) and content.value and isinstance(content[-1], (AoT, Table)): content = content[-1] elif isinstance(content, Table) and content.value.body and isinstance(content.value.body[-1][1], (AoT, Table)): - content = content.value.body[-1][1] + content = content.value.body[-1][1] # type: ignore # can be AoT temporarily else: # pragma: no cover # coverage has a bug on python < 3.10, seeing this line as uncovered # https://github.com/nedbat/coveragepy/issues/1480 diff --git a/tests/formatter/test_project.py b/tests/formatter/test_project.py index 5e3e97c..9a36d8f 100644 --- a/tests/formatter/test_project.py +++ b/tests/formatter/test_project.py @@ -18,6 +18,40 @@ def test_project_name(fmt: Fmt, value: str) -> None: fmt(fmt_project, value, '[project]\nname="a-b"\n') +def test_project_classifiers(fmt: Fmt) -> None: + start = """ + [project] + classifiers = [ + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3 :: Only", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.11", + ] + """ + expected = """ + [project] + classifiers = [ + "License :: OSI Approved :: MIT License", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "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_project_dependencies(fmt: Fmt) -> None: start = '[project]\ndependencies=["pytest","pytest-cov",]' expected = '[project]\ndependencies=[\n "pytest",\n "pytest-cov",\n]\n' diff --git a/tests/formatter/test_tools.py b/tests/formatter/test_tools.py index 0497daf..0ef8ec4 100644 --- a/tests/formatter/test_tools.py +++ b/tests/formatter/test_tools.py @@ -54,7 +54,6 @@ def test_sub_table_newline(fmt: Fmt) -> None: content = """ [tool.mypy] a = 0 - [[tool.mypy.overrides]] a = 1 [tool.something-else] @@ -63,7 +62,6 @@ def test_sub_table_newline(fmt: Fmt) -> None: expected = """ [tool.mypy] a = 0 - [[tool.mypy.overrides]] a = 1 diff --git a/tox.ini b/tox.ini index 070de9e..d6b4c78 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,7 @@ [tox] -envlist = +requires = + tox>=4.2 +env_list = fix py311 py310 @@ -10,16 +12,17 @@ envlist = readme docs skip_missing_interpreters = true -requires = tox>=4.2 [testenv] description = run the unit tests with pytest under {basepython} -setenv = +package = wheel +wheel_build_env = .pkg +extras = + test +set_env = COVERAGE_FILE = {toxworkdir}/.coverage.{envname} COVERAGE_PROCESS_START = {toxinidir}/setup.cfg _COVERAGE_SRC = {envsitepackagesdir}/sphinx_argparse_cli -extras = - test commands = python -m pytest {tty:--color=yes} {posargs: \ --junitxml {toxworkdir}{/}junit.{envname}.xml --cov {envsitepackagesdir}{/}pyproject_fmt \ @@ -27,26 +30,25 @@ commands = --cov-config=pyproject.toml --no-cov-on-fail --cov-report term-missing:skip-covered --cov-context=test \ --cov-report html:{envtmpdir}{/}htmlcov --cov-report xml:{toxworkdir}{/}coverage.{envname}.xml \ tests} -package = wheel -wheel_build_env = .pkg [testenv:fix] description = run static analysis and style check using flake8 skip_install = true deps = - pre-commit>=3.0.4 + pre-commit>=3.2.2 commands = pre-commit run --all-files --show-diff-on-failure python -c 'print("hint: run {envdir}/bin/pre-commit install to add checks as pre-commit hook")' [testenv:type] description = run type check on code base -setenv = - {tty:MYPY_FORCE_COLOR = 1} deps = - mypy==1 + mypy==1.2 +set_env = + {tty:MYPY_FORCE_COLOR = 1} commands = - mypy --strict --python-version 3.10 src tests + mypy src + mypy tests [testenv:readme] description = check that the long description is valid @@ -68,10 +70,10 @@ commands = [testenv:dev] description = generate a DEV environment +package = editable extras = docs test commands = python -m pip list --format=columns python -c 'import sys; print(sys.executable)' -package = editable diff --git a/whitelist.txt b/whitelist.txt index e1097a0..8eff61c 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -11,6 +11,8 @@ intersphinx iread iwrite mkfifo +natsort +natsorted nitpicky pep508 py38