Skip to content

Commit

Permalink
ENH: define TypedDict for pyproject.toml (#317)
Browse files Browse the repository at this point in the history
  • Loading branch information
redeboer authored Mar 10, 2024
1 parent 5348a24 commit 17635bb
Show file tree
Hide file tree
Showing 11 changed files with 178 additions and 88 deletions.
15 changes: 11 additions & 4 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,24 @@
"obj",
"compwa_policy.check_dev_files.dependabot.DependabotOption",
),
"IO": "typing.IO",
"Iterable": "typing.Iterable",
"K": "typing.TypeVar",
"Mapping": "collections.abc.Mapping",
"NotRequired": ("obj", "typing.NotRequired"),
"P": "typing.ParamSpec",
"P.args": ("attr", "typing.ParamSpec.args"),
"P.kwargs": ("attr", "typing.ParamSpec.kwargs"),
"P": "typing.ParamSpec",
"Path": "pathlib.Path",
"PythonVersion": "typing.TypeVar",
"ProjectURLs": "list",
"PyprojectTOML": "dict",
"PythonVersion": "str",
"Sequence": "typing.Sequence",
"T": "typing.TypeVar",
"TOMLDocument": "tomlkit.TOMLDocument",
"Table": "tomlkit.items.Table",
"V": "typing.TypeVar",
"TOMLDocument": "tomlkit.TOMLDocument",
"typing_extensions.NotRequired": ("obj", "typing.NotRequired"),
"V": "typing.TypeVar",
}
author = "Common Partial Wave Analysis"
autodoc_member_order = "bysource"
Expand Down Expand Up @@ -102,6 +108,7 @@
]
nitpick_ignore = [
("py:class", "CommentedMap"),
("py:class", "ProjectURLs"),
]
nitpick_ignore_regex = [
("py:class", r"^.*.[A-Z]$"),
Expand Down
2 changes: 1 addition & 1 deletion src/compwa_policy/check_dev_files/black.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ def _remove_outdated_settings(pyproject: ModifiablePyproject) -> None:
removed_options = set()
for option in forbidden_options:
if option in settings:
settings.pop(option)
removed_options.add(option)
settings.remove(option)
if removed_options:
msg = (
f"Removed {', '.join(sorted(removed_options))} option from black"
Expand Down
4 changes: 2 additions & 2 deletions src/compwa_policy/check_dev_files/pytest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Iterable

import tomlkit
from ini2toml.api import Translator
Expand Down Expand Up @@ -70,7 +70,7 @@ def _update_settings(pyproject: ModifiablePyproject) -> None:
pyproject.append_to_changelog(msg)


def __get_expected_addopts(existing: str | Array) -> Array:
def __get_expected_addopts(existing: str | Iterable) -> Array:
if isinstance(existing, str):
options = {opt.strip() for opt in __split_options(existing)}
else:
Expand Down
28 changes: 16 additions & 12 deletions src/compwa_policy/check_dev_files/ruff.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
from __future__ import annotations

import os
from typing import Iterable
from collections import abc
from typing import TYPE_CHECKING, Any, Iterable, Mapping

from ruamel.yaml import YAML
from tomlkit.items import Array, Table

from compwa_policy.utilities import CONFIG_PATH, natural_sorting, remove_configs, vscode
from compwa_policy.utilities.executor import Executor
Expand All @@ -25,6 +25,9 @@
from compwa_policy.utilities.readme import add_badge, remove_badge
from compwa_policy.utilities.toml import to_toml_array

if TYPE_CHECKING:
from tomlkit.items import Array


def main(has_notebooks: bool) -> None:
with Executor() as do, ModifiablePyproject.load() as pyproject:
Expand Down Expand Up @@ -98,18 +101,17 @@ def __remove_nbqa_option(pyproject: ModifiablePyproject, option: str) -> None:
nbqa_table = pyproject.get_table(table_key)
if option not in nbqa_table:
return
nbqa_table.remove(option)
nbqa_table.pop(option)
msg = f"Removed {option!r} nbQA options from {CONFIG_PATH.pyproject}"
pyproject.append_to_changelog(msg)


def __remove_tool_table(pyproject: ModifiablePyproject, tool_table: str) -> None:
table_key = f"tool.{tool_table}"
if not pyproject.has_table(table_key):
return
pyproject._document["tool"].remove(tool_table) # type: ignore[union-attr]
msg = f"Removed [tool.{tool_table}] section from {CONFIG_PATH.pyproject}"
pyproject.append_to_changelog(msg)
tools = pyproject._document.get("tool")
if isinstance(tools, dict) and tool_table in tools:
tools.pop(tool_table)
msg = f"Removed [tool.{tool_table}] section from {CONFIG_PATH.pyproject}"
pyproject.append_to_changelog(msg)


def _remove_pydocstyle(pyproject: ModifiablePyproject) -> None:
Expand Down Expand Up @@ -153,11 +155,13 @@ def _move_ruff_lint_config(pyproject: ModifiablePyproject) -> None:
}
global_settings = pyproject.get_table("tool.ruff", create=True)
lint_settings = {k: v for k, v in global_settings.items() if k in lint_option_keys}
lint_arrays = {k: v for k, v in lint_settings.items() if isinstance(v, Array)}
lint_arrays = {
k: v for k, v in lint_settings.items() if isinstance(v, abc.Sequence)
}
if lint_arrays:
lint_config = pyproject.get_table("tool.ruff.lint", create=True)
lint_config.update(lint_arrays)
lint_tables = {k: v for k, v in lint_settings.items() if isinstance(v, Table)}
lint_tables = {k: v for k, v in lint_settings.items() if isinstance(v, abc.Mapping)}
for table in lint_tables:
lint_config = pyproject.get_table(f"tool.ruff.lint.{table}", create=True)
lint_config.update(lint_tables[table])
Expand Down Expand Up @@ -322,7 +326,7 @@ def ___get_selected_ruff_rules(pyproject: Pyproject) -> Array:
return to_toml_array(sorted(rules))


def ___get_task_tags(ruff_settings: Table) -> Array:
def ___get_task_tags(ruff_settings: Mapping[str, Any]) -> Array:
existing: set[str] = set(ruff_settings.get("task-tags", set()))
expected = {
"cspell",
Expand Down
44 changes: 23 additions & 21 deletions src/compwa_policy/utilities/pyproject/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,17 @@
from contextlib import AbstractContextManager
from pathlib import Path
from textwrap import indent
from typing import IO, TYPE_CHECKING, Iterable, Sequence, TypeVar, overload
from typing import (
IO,
TYPE_CHECKING,
Any,
Iterable,
Mapping,
MutableMapping,
Sequence,
TypeVar,
overload,
)

import tomlkit
from attrs import field, frozen
Expand All @@ -22,6 +32,7 @@
get_sub_table,
get_supported_python_versions,
has_sub_table,
load_pyproject_toml,
)
from compwa_policy.utilities.pyproject.setters import (
add_dependency,
Expand All @@ -40,8 +51,7 @@
if TYPE_CHECKING:
from types import TracebackType

from tomlkit.items import Table
from tomlkit.toml_document import TOMLDocument
from compwa_policy.utilities.pyproject._struct import PyprojectTOML

T = TypeVar("T", bound="Pyproject")

Expand All @@ -50,34 +60,24 @@
class Pyproject:
"""Read-only representation of a :code:`pyproject.toml` file."""

_document: TOMLDocument
_document: PyprojectTOML
_source: IO | Path | None = field(default=None)

@final
@classmethod
def load(cls: type[T], source: IO | Path | str = CONFIG_PATH.pyproject) -> T:
"""Load a :code:`pyproject.toml` file from a file, I/O stream, or `str`."""
if isinstance(source, io.IOBase):
current_position = source.tell()
source.seek(0)
document = tomlkit.load(source) # type:ignore[arg-type]
source.seek(current_position)
return cls(document, source)
if isinstance(source, Path):
with open(source) as stream:
document = tomlkit.load(stream)
return cls(document, source)
document = load_pyproject_toml(source)
if isinstance(source, str):
return cls(tomlkit.loads(source))
msg = f"Source of type {type(source).__name__} is not supported"
raise TypeError(msg)
return cls(document)
return cls(document, source)

@final
def dumps(self) -> str:
src = tomlkit.dumps(self._document, sort_keys=True)
return f"{src.strip()}\n"

def get_table(self, dotted_header: str, create: bool = False) -> Table:
def get_table(self, dotted_header: str, create: bool = False) -> Mapping[str, Any]:
if create:
msg = "Cannot create sub-tables in a read-only pyproject.toml"
raise TypeError(msg)
Expand Down Expand Up @@ -166,11 +166,13 @@ def dump(self, target: IO | Path | str | None = None) -> None:
raise TypeError(msg)

@override
def get_table(self, dotted_header: str, create: bool = False) -> Table:
def get_table(
self, dotted_header: str, create: bool = False
) -> MutableMapping[str, Any]:
self.__assert_is_in_context()
if create:
create_sub_table(self._document, dotted_header)
return super().get_table(dotted_header)
return super().get_table(dotted_header) # type:ignore[return-value]

def add_dependency(
self, package: str, optional_key: str | Sequence[str] | None = None
Expand Down Expand Up @@ -200,7 +202,7 @@ def append_to_changelog(self, message: str) -> None:
self._changelog.append(message)


def complies_with_subset(settings: dict, minimal_settings: dict) -> bool:
def complies_with_subset(settings: Mapping, minimal_settings: Mapping) -> bool:
return all(settings.get(key) == value for key, value in minimal_settings.items())


Expand Down
63 changes: 63 additions & 0 deletions src/compwa_policy/utilities/pyproject/_struct.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""This module is hidden Sphinx can't handle `typing.TypedDict` with hyphens.
See https://github.com/sphinx-doc/sphinx/issues/11039.
"""

import sys
from typing import Dict, List

if sys.version_info < (3, 8):
from typing_extensions import TypedDict
else:
from typing import TypedDict
if sys.version_info < (3, 11):
from typing_extensions import NotRequired
else:
from typing import NotRequired

PyprojectTOML = TypedDict(
"PyprojectTOML",
{
"build-system": NotRequired["BuildSystem"],
"project": "Project",
"tool": NotRequired[Dict[str, Dict[str, str]]],
},
)
"""Structure of a `pyproject.toml` file.
See [pyproject.toml
specification](https://packaging.python.org/en/latest/specifications/pyproject-toml).
"""


BuildSystem = TypedDict(
"BuildSystem",
{
"requires": List[str],
"build-backend": str,
},
)


Project = TypedDict(
"Project",
{
"name": str,
"version": NotRequired[str],
"dependencies": NotRequired[List[str]],
"optional-dependencies": NotRequired[Dict[str, List[str]]],
"urls": NotRequired["ProjectURLs"],
},
)


class ProjectURLs(TypedDict):
"""Project for PyPI."""

Changelog: NotRequired[str]
Documentation: NotRequired[str]
Homepage: NotRequired[str]
Issues: NotRequired[str]
Repository: NotRequired[str]
Source: NotRequired[str]
Tracker: NotRequired[str]
Loading

0 comments on commit 17635bb

Please sign in to comment.