diff --git a/docs/changelog.rst b/docs/changelog.rst index 037e1d1..b06a831 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ NEXT ---- * Handle ``project`` section in first iteration - by :user:`gaborbernat`. * Add documentation build to the project - by :user:`gaborbernat`. +* Add a programmatic API as :meth:`format_pyproject ` - by :user:`gaborbernat`. v0.2.0 (2022-02-21) ------------------- diff --git a/docs/index.rst b/docs/index.rst index 58547eb..49ff2d4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -36,6 +36,12 @@ Command line interface :prog: pyproject-fmt :title: +API +--- + +.. automodule:: pyproject_fmt + :members: + .. toctree:: :hidden: diff --git a/src/pyproject_fmt/__init__.py b/src/pyproject_fmt/__init__.py index 1032ca4..b38bee8 100644 --- a/src/pyproject_fmt/__init__.py +++ b/src/pyproject_fmt/__init__.py @@ -1,7 +1,11 @@ from __future__ import annotations from ._version import version as __version__ +from .formatter import format_pyproject +from .formatter.config import Config __all__ = [ "__version__", + "Config", + "format_pyproject", ] diff --git a/src/pyproject_fmt/__main__.py b/src/pyproject_fmt/__main__.py index 303d8fd..328d0f5 100644 --- a/src/pyproject_fmt/__main__.py +++ b/src/pyproject_fmt/__main__.py @@ -25,14 +25,15 @@ def color_diff(diff: Iterable[str]) -> Iterable[str]: def run(args: Sequence[str] | None = None) -> int: opts = cli_args(sys.argv[1:] if args is None else args) - formatted = format_pyproject(opts) + config = opts.as_config + formatted = format_pyproject(config) toml = opts.pyproject_toml - before = toml.read_text() + before = config.toml changed = before != formatted if opts.stdout: # stdout just prints new format to stdout print(formatted, end="") else: - toml.write_text(formatted) + toml.write_text(formatted, encoding="utf-8") try: name = str(toml.relative_to(Path.cwd())) except ValueError: diff --git a/src/pyproject_fmt/cli.py b/src/pyproject_fmt/cli.py index e9960ff..129c0e0 100644 --- a/src/pyproject_fmt/cli.py +++ b/src/pyproject_fmt/cli.py @@ -1,17 +1,31 @@ from __future__ import annotations import os -from argparse import ArgumentParser, ArgumentTypeError, Namespace +from argparse import ( + ArgumentDefaultsHelpFormatter, + ArgumentParser, + ArgumentTypeError, + Namespace, +) from pathlib import Path from typing import Sequence +from pyproject_fmt.formatter.config import DEFAULT_INDENT, Config + class PyProjectFmtNamespace(Namespace): """Options for pyproject-fmt tool""" pyproject_toml: Path stdout: bool - indent = 2 + indent: int + + @property + def as_config(self) -> Config: + return Config( + toml=self.pyproject_toml.read_text(encoding="utf-8"), + indent=self.indent, + ) def pyproject_toml_path_creator(argument: str) -> Path: @@ -33,9 +47,10 @@ def pyproject_toml_path_creator(argument: str) -> Path: def _build_cli() -> ArgumentParser: - parser = ArgumentParser() + parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) msg = "print the formatted text to the stdout (instead of update in-place)" parser.add_argument("-s", "--stdout", action="store_true", help=msg) + parser.add_argument("--indent", type=int, default=DEFAULT_INDENT, help="number of spaces to indent") parser.add_argument("pyproject_toml", type=pyproject_toml_path_creator, help="tox ini file to format") return parser diff --git a/src/pyproject_fmt/formatter/__init__.py b/src/pyproject_fmt/formatter/__init__.py index 990532f..f92fb7d 100644 --- a/src/pyproject_fmt/formatter/__init__.py +++ b/src/pyproject_fmt/formatter/__init__.py @@ -3,20 +3,25 @@ from tomlkit import parse from tomlkit.toml_document import TOMLDocument -from ..cli import PyProjectFmtNamespace from .build_system import fmt_build_system +from .config import Config from .project import fmt_project -def _perform(parsed: TOMLDocument, opts: PyProjectFmtNamespace) -> None: - fmt_build_system(parsed, opts) - fmt_project(parsed, opts) +def _perform(parsed: TOMLDocument, conf: Config) -> None: + fmt_build_system(parsed, conf) + fmt_project(parsed, conf) -def format_pyproject(opts: PyProjectFmtNamespace) -> str: - text = opts.pyproject_toml.read_text() - parsed: TOMLDocument = parse(text) - _perform(parsed, opts) +def format_pyproject(conf: Config) -> str: + """ + Format a ``pyproject.toml`` text. + + :param conf: the formatting configuration + :return: the formatted text + """ + parsed: TOMLDocument = parse(conf.toml) + _perform(parsed, conf) result = parsed.as_string().rstrip("\n") return f"{result}\n" diff --git a/src/pyproject_fmt/formatter/build_system.py b/src/pyproject_fmt/formatter/build_system.py index 6699cbe..1074181 100644 --- a/src/pyproject_fmt/formatter/build_system.py +++ b/src/pyproject_fmt/formatter/build_system.py @@ -5,16 +5,16 @@ from tomlkit.items import Array, Table from tomlkit.toml_document import TOMLDocument -from ..cli import PyProjectFmtNamespace +from .config import Config from .pep508 import normalize_pep508_array from .util import order_keys, sorted_array -def fmt_build_system(parsed: TOMLDocument, opts: PyProjectFmtNamespace) -> None: +def fmt_build_system(parsed: TOMLDocument, conf: Config) -> None: system = cast(Optional[Table], parsed.get("build-system")) if system is not None: - normalize_pep508_array(cast(Optional[Array], system.get("requires")), opts.indent) - sorted_array(cast(Optional[Array], system.get("backend-path")), indent=opts.indent) + normalize_pep508_array(cast(Optional[Array], system.get("requires")), conf.indent) + sorted_array(cast(Optional[Array], system.get("backend-path")), indent=conf.indent) order_keys(system.value.body, ("build-backend", "requires", "backend-path")) diff --git a/src/pyproject_fmt/formatter/config.py b/src/pyproject_fmt/formatter/config.py new file mode 100644 index 0000000..171f191 --- /dev/null +++ b/src/pyproject_fmt/formatter/config.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +import sys +from dataclasses import dataclass + +if sys.version_info >= (3, 8): # pragma: no cover (py38+) + from typing import Final +else: # pragma: no cover ( None: +def fmt_project(parsed: TOMLDocument, conf: Config) -> None: project = cast(Optional[Table], parsed.get("project")) if project is None: return @@ -22,14 +22,14 @@ def fmt_project(parsed: TOMLDocument, opts: PyProjectFmtNamespace) -> None: if "description" in project: project["description"] = String.from_raw(str(project["description"]).strip()) - sorted_array(cast(Optional[Array], project.get("keywords")), indent=opts.indent) - sorted_array(cast(Optional[Array], project.get("dynamic")), indent=opts.indent) + sorted_array(cast(Optional[Array], project.get("keywords")), indent=conf.indent) + sorted_array(cast(Optional[Array], project.get("dynamic")), indent=conf.indent) - normalize_pep508_array(cast(Optional[Array], project.get("dependencies")), opts.indent) + normalize_pep508_array(cast(Optional[Array], project.get("dependencies")), conf.indent) if "optional-dependencies" in project: opt_deps = cast(Table, project["optional-dependencies"]) for value in opt_deps.values(): - normalize_pep508_array(cast(Array, value), opts.indent) + normalize_pep508_array(cast(Array, value), conf.indent) order_keys(opt_deps.value.body, (), sort_key=lambda k: k[0]) for of_type in ("scripts", "gui-scripts", "entry-points", "urls"): diff --git a/tests/__init__.py b/tests/__init__.py index 4cf9c57..d3cf850 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -4,8 +4,8 @@ from tomlkit.toml_document import TOMLDocument -from pyproject_fmt.cli import PyProjectFmtNamespace +from pyproject_fmt.formatter.config import Config -Fmt = Callable[[Callable[[TOMLDocument, PyProjectFmtNamespace], None], str, str], None] +Fmt = Callable[[Callable[[TOMLDocument, Config], None], str, str], None] __all__ = ["Fmt"] diff --git a/tests/formatter/conftest.py b/tests/formatter/conftest.py index a91693e..98be187 100644 --- a/tests/formatter/conftest.py +++ b/tests/formatter/conftest.py @@ -1,6 +1,5 @@ from __future__ import annotations -from pathlib import Path from textwrap import dedent from typing import Callable @@ -8,18 +7,16 @@ from pytest_mock import MockerFixture from tomlkit.toml_document import TOMLDocument -from pyproject_fmt.cli import PyProjectFmtNamespace from pyproject_fmt.formatter import format_pyproject +from pyproject_fmt.formatter.config import Config from tests import Fmt @pytest.fixture() -def fmt(tmp_path: Path, mocker: MockerFixture) -> Fmt: - def _func(formatter: Callable[[TOMLDocument, PyProjectFmtNamespace], None], start: str, expected: str) -> None: +def fmt(mocker: MockerFixture) -> Fmt: + def _func(formatter: Callable[[TOMLDocument, Config], None], start: str, expected: str) -> None: mocker.patch("pyproject_fmt.formatter._perform", formatter) - toml = tmp_path / "a.toml" - toml.write_text(dedent(start)) - opts = PyProjectFmtNamespace(pyproject_toml=tmp_path / "a.toml") + opts = Config(toml=dedent(start)) result = format_pyproject(opts) expected = dedent(expected) diff --git a/whitelist.txt b/whitelist.txt index 3f1e516..6f2ede5 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -2,6 +2,7 @@ autoclass autodoc capsys chdir +conf dedent deps difflib