Skip to content

Commit

Permalink
Add programmatic API (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
gaborbernat authored Feb 27, 2022
1 parent 199325a commit 704637a
Show file tree
Hide file tree
Showing 12 changed files with 89 additions and 33 deletions.
1 change: 1 addition & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <pyproject_fmt.format_pyproject>` - by :user:`gaborbernat`.

v0.2.0 (2022-02-21)
-------------------
Expand Down
6 changes: 6 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ Command line interface
:prog: pyproject-fmt
:title:

API
---

.. automodule:: pyproject_fmt
:members:

.. toctree::
:hidden:

Expand Down
4 changes: 4 additions & 0 deletions src/pyproject_fmt/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
]
7 changes: 4 additions & 3 deletions src/pyproject_fmt/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
21 changes: 18 additions & 3 deletions src/pyproject_fmt/cli.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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

Expand Down
21 changes: 13 additions & 8 deletions src/pyproject_fmt/formatter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
8 changes: 4 additions & 4 deletions src/pyproject_fmt/formatter/build_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"))


Expand Down
26 changes: 26 additions & 0 deletions src/pyproject_fmt/formatter/config.py
Original file line number Diff line number Diff line change
@@ -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 (<py38)
from typing_extensions import Final


DEFAULT_INDENT: Final[int] = 2 #: default indentation level


@dataclass(frozen=True)
class Config:
"""Configuration flags for the formatting."""

toml: str #: the text to format
indent: int = DEFAULT_INDENT #: indentation to apply


__all__ = [
"Config",
"DEFAULT_INDENT",
]
12 changes: 6 additions & 6 deletions src/pyproject_fmt/formatter/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
from tomlkit.items import Array, String, 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_project(parsed: TOMLDocument, opts: PyProjectFmtNamespace) -> None:
def fmt_project(parsed: TOMLDocument, conf: Config) -> None:
project = cast(Optional[Table], parsed.get("project"))
if project is None:
return
Expand All @@ -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"):
Expand Down
4 changes: 2 additions & 2 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
11 changes: 4 additions & 7 deletions tests/formatter/conftest.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
from __future__ import annotations

from pathlib import Path
from textwrap import dedent
from typing import Callable

import pytest
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)
Expand Down
1 change: 1 addition & 0 deletions whitelist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ autoclass
autodoc
capsys
chdir
conf
dedent
deps
difflib
Expand Down

0 comments on commit 704637a

Please sign in to comment.