pyprojkit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyprojkit/__init__.py +104 -0
- pyprojkit/_paths.py +43 -0
- pyprojkit/_run.py +36 -0
- pyprojkit/cli.py +58 -0
- pyprojkit/config/__init__.py +79 -0
- pyprojkit/config/base.py +84 -0
- pyprojkit/config/profiles.py +97 -0
- pyprojkit/config/project.py +194 -0
- pyprojkit/config/tools.py +170 -0
- pyprojkit/discovery.py +51 -0
- pyprojkit/sessions.py +71 -0
- pyprojkit/sync.py +243 -0
- pyprojkit/tasks.py +375 -0
- pyprojkit/versions.py +114 -0
- pyprojkit-0.1.0.dist-info/METADATA +310 -0
- pyprojkit-0.1.0.dist-info/RECORD +19 -0
- pyprojkit-0.1.0.dist-info/WHEEL +4 -0
- pyprojkit-0.1.0.dist-info/entry_points.txt +3 -0
- pyprojkit-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configurations for all supported tools.
|
|
3
|
+
|
|
4
|
+
Formatters are tools which additionally run as part of the `format` task.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import TYPE_CHECKING, Any, ClassVar
|
|
11
|
+
|
|
12
|
+
from .. import _paths
|
|
13
|
+
from .base import BaseFormatterConfig, BaseToolConfig
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from .project import ProjectConfig
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"AutoflakeConfig",
|
|
20
|
+
"IsortConfig",
|
|
21
|
+
"BlackConfig",
|
|
22
|
+
"DocformatterConfig",
|
|
23
|
+
"TomlSortConfig",
|
|
24
|
+
"PytestConfig",
|
|
25
|
+
"CoverageRunConfig",
|
|
26
|
+
"CoverageReportConfig",
|
|
27
|
+
"DoitConfig",
|
|
28
|
+
"NoxConfig",
|
|
29
|
+
"MypyConfig",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# --- formatters (run order defined by FormattingConfig) ---
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(kw_only=True)
|
|
37
|
+
class AutoflakeConfig(BaseFormatterConfig):
|
|
38
|
+
table_path: ClassVar[str] = "tool.autoflake"
|
|
39
|
+
|
|
40
|
+
in_place: bool | None = True
|
|
41
|
+
recursive: bool | None = True
|
|
42
|
+
remove_all_unused_imports: bool | None = field(
|
|
43
|
+
default=True, metadata={"toml": "remove-all-unused-imports"}
|
|
44
|
+
)
|
|
45
|
+
remove_unused_variables: bool | None = field(
|
|
46
|
+
default=True, metadata={"toml": "remove-unused-variables"}
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def command(self, py_paths: list[str], toml_paths: list[str]) -> list[str]:
|
|
50
|
+
return ["autoflake"] + py_paths
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(kw_only=True)
|
|
54
|
+
class IsortConfig(BaseFormatterConfig):
|
|
55
|
+
table_path: ClassVar[str] = "tool.isort"
|
|
56
|
+
|
|
57
|
+
profile: str | None = "black"
|
|
58
|
+
quiet: bool | None = True
|
|
59
|
+
|
|
60
|
+
def command(self, py_paths: list[str], toml_paths: list[str]) -> list[str]:
|
|
61
|
+
return ["isort"] + py_paths
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(kw_only=True)
|
|
65
|
+
class BlackConfig(BaseFormatterConfig):
|
|
66
|
+
table_path: ClassVar[str] = "tool.black"
|
|
67
|
+
|
|
68
|
+
quiet: bool | None = True
|
|
69
|
+
|
|
70
|
+
def extra_toml(self, project: ProjectConfig) -> dict[str, Any]:
|
|
71
|
+
return {"target-version": project.python.black_targets}
|
|
72
|
+
|
|
73
|
+
def command(self, py_paths: list[str], toml_paths: list[str]) -> list[str]:
|
|
74
|
+
return ["black"] + py_paths
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(kw_only=True)
|
|
78
|
+
class DocformatterConfig(BaseFormatterConfig):
|
|
79
|
+
table_path: ClassVar[str] = "tool.docformatter"
|
|
80
|
+
|
|
81
|
+
# docformatter returns 3 when files were changed
|
|
82
|
+
expect_rc: ClassVar[frozenset[int]] = frozenset({0, 3})
|
|
83
|
+
|
|
84
|
+
black: bool | None = True
|
|
85
|
+
in_place: bool | None = field(default=True, metadata={"toml": "in-place"})
|
|
86
|
+
make_summary_multi_line: bool | None = field(
|
|
87
|
+
default=True, metadata={"toml": "make-summary-multi-line"}
|
|
88
|
+
)
|
|
89
|
+
non_strict: bool | None = field(default=True, metadata={"toml": "non-strict"})
|
|
90
|
+
pre_summary_newline: bool | None = field(
|
|
91
|
+
default=True, metadata={"toml": "pre-summary-newline"}
|
|
92
|
+
)
|
|
93
|
+
recursive: bool | None = True
|
|
94
|
+
|
|
95
|
+
def command(self, py_paths: list[str], toml_paths: list[str]) -> list[str]:
|
|
96
|
+
return ["docformatter"] + py_paths
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass(kw_only=True)
|
|
100
|
+
class TomlSortConfig(BaseFormatterConfig):
|
|
101
|
+
table_path: ClassVar[str] = "tool.tomlsort"
|
|
102
|
+
|
|
103
|
+
sort_first: tuple[str, ...] | None = (
|
|
104
|
+
"project",
|
|
105
|
+
"dependency-groups",
|
|
106
|
+
"build-system",
|
|
107
|
+
)
|
|
108
|
+
sort_table_keys: bool | None = True
|
|
109
|
+
|
|
110
|
+
# explicit since the toml-sort CLI and library defaults disagree; must be
|
|
111
|
+
# pinned so the sync engine's normalization and the format task agree
|
|
112
|
+
spaces_before_inline_comment: int | None = 2
|
|
113
|
+
|
|
114
|
+
def command(self, py_paths: list[str], toml_paths: list[str]) -> list[str]:
|
|
115
|
+
return ["toml-sort", "-i"] + toml_paths
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# --- non-formatter tools ---
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass(kw_only=True)
|
|
122
|
+
class PytestConfig(BaseToolConfig):
|
|
123
|
+
table_path: ClassVar[str] = "tool.pytest.ini_options"
|
|
124
|
+
|
|
125
|
+
addopts: str | None = "--import-mode=importlib -s -v -rA"
|
|
126
|
+
cache_dir: str | None = str(_paths.PYTEST_CACHE_PATH)
|
|
127
|
+
testpaths: str | None = "test"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass(kw_only=True)
|
|
131
|
+
class CoverageRunConfig(BaseToolConfig):
|
|
132
|
+
table_path: ClassVar[str] = "tool.coverage.run"
|
|
133
|
+
|
|
134
|
+
data_file: str | None = str(_paths.COVERAGE_DATA_PATH)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass(kw_only=True)
|
|
138
|
+
class CoverageReportConfig(BaseToolConfig):
|
|
139
|
+
table_path: ClassVar[str] = "tool.coverage.report"
|
|
140
|
+
|
|
141
|
+
exclude_lines: tuple[str, ...] | None = (
|
|
142
|
+
"if TYPE_CHECKING:",
|
|
143
|
+
"\\.\\.\\.$",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass(kw_only=True)
|
|
148
|
+
class DoitConfig(BaseToolConfig):
|
|
149
|
+
table_path: ClassVar[str] = "tool.doit"
|
|
150
|
+
|
|
151
|
+
dep_file: str | None = str(_paths.DOIT_DB_PATH)
|
|
152
|
+
verbosity: int | None = 2
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@dataclass(kw_only=True)
|
|
156
|
+
class NoxConfig(BaseToolConfig):
|
|
157
|
+
table_path: ClassVar[str] = "tool.nox"
|
|
158
|
+
|
|
159
|
+
default_venv_backend: str | None = "uv"
|
|
160
|
+
envdir: str | None = str(_paths.NOX_ENVDIR_PATH)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass(kw_only=True)
|
|
164
|
+
class MypyConfig(BaseToolConfig):
|
|
165
|
+
table_path: ClassVar[str] = "tool.mypy"
|
|
166
|
+
|
|
167
|
+
ignore_missing_imports: bool | None = True
|
|
168
|
+
|
|
169
|
+
def extra_toml(self, project: ProjectConfig) -> dict[str, Any]:
|
|
170
|
+
return {"python_version": project.python.mypy_version}
|
pyprojkit/discovery.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Discovery of a project's `pyprojconf.py`.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import importlib.util
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .config.base import ConfigError
|
|
12
|
+
from .config.project import ProjectConfig
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"load_config",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
CONF_FILENAME = "pyprojconf.py"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def load_config(root: Path | str | None = None) -> ProjectConfig:
|
|
22
|
+
"""
|
|
23
|
+
Load `pyprojconf.py` from the given project root (defaulting to the current
|
|
24
|
+
directory) and return its `config`.
|
|
25
|
+
"""
|
|
26
|
+
root_path = Path(root) if root else Path.cwd()
|
|
27
|
+
conf_path = root_path / CONF_FILENAME
|
|
28
|
+
|
|
29
|
+
if not conf_path.is_file():
|
|
30
|
+
raise ConfigError(f"'{conf_path}' not found")
|
|
31
|
+
|
|
32
|
+
module_name = f"_pyprojconf_{abs(hash(str(conf_path.resolve())))}"
|
|
33
|
+
spec = importlib.util.spec_from_file_location(module_name, conf_path)
|
|
34
|
+
assert spec and spec.loader
|
|
35
|
+
|
|
36
|
+
module = importlib.util.module_from_spec(spec)
|
|
37
|
+
sys.modules[module_name] = module
|
|
38
|
+
try:
|
|
39
|
+
spec.loader.exec_module(module)
|
|
40
|
+
except Exception as exc:
|
|
41
|
+
raise ConfigError(f"Failed to import '{conf_path}': {exc}") from exc
|
|
42
|
+
finally:
|
|
43
|
+
sys.modules.pop(module_name, None)
|
|
44
|
+
|
|
45
|
+
config = getattr(module, "config", None)
|
|
46
|
+
if not isinstance(config, ProjectConfig):
|
|
47
|
+
raise ConfigError(
|
|
48
|
+
f"'{conf_path}' must define a module-level `config: ProjectConfig`"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
return config
|
pyprojkit/sessions.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
`nox` session factories.
|
|
3
|
+
|
|
4
|
+
Usage in a project's `noxfile.py`:
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
from pyprojkit import NoxFactory
|
|
8
|
+
|
|
9
|
+
test = NoxFactory().create_test_session()
|
|
10
|
+
```
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Callable
|
|
17
|
+
|
|
18
|
+
import nox
|
|
19
|
+
from nox import Session
|
|
20
|
+
|
|
21
|
+
from .config.base import ConfigError
|
|
22
|
+
from .config.project import ProjectConfig
|
|
23
|
+
from .discovery import load_config
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"NoxFactory",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class NoxFactory:
|
|
31
|
+
"""
|
|
32
|
+
Factory for `nox` sessions, configured by the project's `pyprojconf.py`.
|
|
33
|
+
|
|
34
|
+
Sessions are registered with nox upon creation, so `create_*_session()` must be
|
|
35
|
+
called at `noxfile.py` import time.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
_config: ProjectConfig
|
|
39
|
+
_registered: set[str]
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
config: ProjectConfig | None = None,
|
|
44
|
+
root: Path | str | None = None,
|
|
45
|
+
):
|
|
46
|
+
self._config = config or load_config(root)
|
|
47
|
+
self._registered = set()
|
|
48
|
+
nox.options.envdir = self._config.tools.nox.envdir
|
|
49
|
+
|
|
50
|
+
def create_test_session(self, *, group: str = "dev") -> Callable[[Session], None]:
|
|
51
|
+
"""
|
|
52
|
+
Create and register `test` session which runs pytest over all supported Python
|
|
53
|
+
versions, installing dependencies from the given dependency group via uv.
|
|
54
|
+
"""
|
|
55
|
+
if "test" in self._registered:
|
|
56
|
+
raise ConfigError("Session 'test' already created")
|
|
57
|
+
self._registered.add("test")
|
|
58
|
+
|
|
59
|
+
@nox.session(python=self._config.python.pins, name="test")
|
|
60
|
+
def test(session: Session):
|
|
61
|
+
session.run_install(
|
|
62
|
+
"uv",
|
|
63
|
+
"sync",
|
|
64
|
+
f"--group={group}",
|
|
65
|
+
"--frozen",
|
|
66
|
+
f"--python={session.python}", # explicitly pin the version
|
|
67
|
+
env={"UV_PROJECT_ENVIRONMENT": session.virtualenv.location},
|
|
68
|
+
)
|
|
69
|
+
session.run("pytest", *session.posargs)
|
|
70
|
+
|
|
71
|
+
return test
|
pyprojkit/sync.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sync engine: writes managed parts of `pyproject.toml` from a project's `pyprojconf.py`.
|
|
3
|
+
|
|
4
|
+
Managed content:
|
|
5
|
+
|
|
6
|
+
- `project.requires-python`
|
|
7
|
+
- Python version classifiers (`Programming Language :: Python :: 3[.X]`);
|
|
8
|
+
other classifiers are left untouched
|
|
9
|
+
- One `[tool.X]` table per enabled tool (fully owned — any hand edits or
|
|
10
|
+
comments inside are overwritten)
|
|
11
|
+
- `[tool.pyprojkit].managed`: bookkeeping list of owned tables, enabling safe
|
|
12
|
+
removal of tables for tools dropped from the configuration
|
|
13
|
+
|
|
14
|
+
Everything else (dependencies, build-system, urls, unmanaged tool tables,
|
|
15
|
+
etc.) is preserved. Output is normalized with toml-sort (as a library, using
|
|
16
|
+
the same settings as the managed `[tool.tomlsort]` table), so a subsequent
|
|
17
|
+
`toml-sort` run in the format task is a no-op.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import difflib
|
|
23
|
+
import re
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any, cast
|
|
26
|
+
|
|
27
|
+
import tomlkit
|
|
28
|
+
from tomlkit import TOMLDocument
|
|
29
|
+
from tomlkit.items import Table
|
|
30
|
+
|
|
31
|
+
from .config.base import ConfigError
|
|
32
|
+
from .config.project import ProjectConfig
|
|
33
|
+
from .config.tools import TomlSortConfig
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"compute_managed_tables",
|
|
37
|
+
"render",
|
|
38
|
+
"sync",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
_CLASSIFIER_RE = re.compile(r"^Programming Language :: Python :: \d+(\.\d+)?$")
|
|
42
|
+
|
|
43
|
+
_MANAGED_COMMENT = "managed by pyprojkit"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def compute_managed_tables(config: ProjectConfig) -> dict[str, dict[str, Any]]:
|
|
47
|
+
"""
|
|
48
|
+
Compute contents of all managed tables, keyed by dotted table path.
|
|
49
|
+
"""
|
|
50
|
+
tables: dict[str, dict[str, Any]] = {}
|
|
51
|
+
tools = config.tools
|
|
52
|
+
|
|
53
|
+
for formatter in tools.formatting.formatters:
|
|
54
|
+
tables[formatter.table_path] = formatter.to_toml(config)
|
|
55
|
+
|
|
56
|
+
if test := tools.test:
|
|
57
|
+
for tool in (test.pytest, test.coverage_run, test.coverage_report):
|
|
58
|
+
tables[tool.table_path] = tool.to_toml(config)
|
|
59
|
+
|
|
60
|
+
tables[tools.doit.table_path] = tools.doit.to_toml(config)
|
|
61
|
+
tables[tools.nox.table_path] = tools.nox.to_toml(config)
|
|
62
|
+
|
|
63
|
+
if (analysis := tools.analysis) and analysis.mypy:
|
|
64
|
+
tables[analysis.mypy.table_path] = analysis.mypy.to_toml(config)
|
|
65
|
+
|
|
66
|
+
# merge escape-hatch overrides last; unknown paths become managed tables
|
|
67
|
+
for path, overrides in tools.tool_overrides.items():
|
|
68
|
+
tables.setdefault(path, {}).update(overrides)
|
|
69
|
+
|
|
70
|
+
return tables
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def render(config: ProjectConfig, text: str) -> str:
|
|
74
|
+
"""
|
|
75
|
+
Render the synced `pyproject.toml` contents from existing contents.
|
|
76
|
+
"""
|
|
77
|
+
doc = tomlkit.parse(text)
|
|
78
|
+
|
|
79
|
+
project = doc.get("project")
|
|
80
|
+
if project is None:
|
|
81
|
+
raise ConfigError("pyproject.toml has no [project] table")
|
|
82
|
+
|
|
83
|
+
project["requires-python"] = config.python.requires_python
|
|
84
|
+
project["requires-python"].comment(_MANAGED_COMMENT)
|
|
85
|
+
_update_classifiers(project, config)
|
|
86
|
+
|
|
87
|
+
tables = compute_managed_tables(config)
|
|
88
|
+
|
|
89
|
+
prev_managed = _get_managed_list(doc)
|
|
90
|
+
for path in prev_managed:
|
|
91
|
+
if path not in tables:
|
|
92
|
+
_delete_table(doc, path)
|
|
93
|
+
|
|
94
|
+
for path, content in tables.items():
|
|
95
|
+
_set_table(doc, path, content)
|
|
96
|
+
|
|
97
|
+
_set_table(doc, "tool.pyprojkit", {"managed": sorted(tables)})
|
|
98
|
+
|
|
99
|
+
return _normalize(config, tomlkit.dumps(doc))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def sync(
|
|
103
|
+
config: ProjectConfig,
|
|
104
|
+
root: Path | str | None = None,
|
|
105
|
+
*,
|
|
106
|
+
check: bool = False,
|
|
107
|
+
) -> bool:
|
|
108
|
+
"""
|
|
109
|
+
Sync `pyproject.toml` under the given project root (defaulting to the current
|
|
110
|
+
directory).
|
|
111
|
+
|
|
112
|
+
In check mode, nothing is written; prints a diff and returns `False` if out of sync.
|
|
113
|
+
In write mode, returns `True` (having updated the file if needed).
|
|
114
|
+
"""
|
|
115
|
+
path = (Path(root) if root else Path.cwd()) / "pyproject.toml"
|
|
116
|
+
if not path.is_file():
|
|
117
|
+
raise ConfigError(f"'{path}' not found")
|
|
118
|
+
|
|
119
|
+
old = path.read_text()
|
|
120
|
+
new = render(config, old)
|
|
121
|
+
|
|
122
|
+
if old == new:
|
|
123
|
+
return True
|
|
124
|
+
|
|
125
|
+
if check:
|
|
126
|
+
diff = difflib.unified_diff(
|
|
127
|
+
old.splitlines(keepends=True),
|
|
128
|
+
new.splitlines(keepends=True),
|
|
129
|
+
fromfile="pyproject.toml (on disk)",
|
|
130
|
+
tofile="pyproject.toml (synced)",
|
|
131
|
+
)
|
|
132
|
+
print("".join(diff), end="")
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
path.write_text(new)
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _update_classifiers(project: Table, config: ProjectConfig):
|
|
140
|
+
"""
|
|
141
|
+
Replace python version classifiers with those derived from config, preserving all
|
|
142
|
+
others; result is sorted, with managed entries marked by an inline comment.
|
|
143
|
+
"""
|
|
144
|
+
existing = cast(list[str], [str(c) for c in project.get("classifiers", [])])
|
|
145
|
+
kept = [c for c in existing if not _CLASSIFIER_RE.match(c)]
|
|
146
|
+
managed = set(config.python.classifiers)
|
|
147
|
+
|
|
148
|
+
array = tomlkit.array()
|
|
149
|
+
for entry in sorted(set(kept) | managed):
|
|
150
|
+
array.add_line(
|
|
151
|
+
entry,
|
|
152
|
+
indent=" ",
|
|
153
|
+
comment=_MANAGED_COMMENT if entry in managed else None,
|
|
154
|
+
)
|
|
155
|
+
array.add_line(indent="")
|
|
156
|
+
project["classifiers"] = array
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _get_managed_list(doc: TOMLDocument) -> list[str]:
|
|
160
|
+
try:
|
|
161
|
+
return list(doc["tool"]["pyprojkit"]["managed"]) # type: ignore[index]
|
|
162
|
+
except (KeyError, TypeError):
|
|
163
|
+
return []
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _set_table(doc: TOMLDocument, path: str, content: dict[str, Any]):
|
|
167
|
+
parts = path.split(".")
|
|
168
|
+
container: Any = doc
|
|
169
|
+
for part in parts[:-1]:
|
|
170
|
+
if part in container:
|
|
171
|
+
container = container[part]
|
|
172
|
+
else:
|
|
173
|
+
table = tomlkit.table(is_super_table=True)
|
|
174
|
+
container[part] = table
|
|
175
|
+
container = table
|
|
176
|
+
|
|
177
|
+
leaf = tomlkit.table()
|
|
178
|
+
leaf.comment(_MANAGED_COMMENT)
|
|
179
|
+
for key, value in content.items():
|
|
180
|
+
leaf[key] = _to_item(value)
|
|
181
|
+
container[parts[-1]] = leaf
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _delete_table(doc: TOMLDocument, path: str):
|
|
185
|
+
parts = path.split(".")
|
|
186
|
+
|
|
187
|
+
# walk to leaf's parent
|
|
188
|
+
containers: list[Any] = [doc]
|
|
189
|
+
for part in parts[:-1]:
|
|
190
|
+
container = containers[-1].get(part)
|
|
191
|
+
if container is None:
|
|
192
|
+
return
|
|
193
|
+
containers.append(container)
|
|
194
|
+
|
|
195
|
+
if parts[-1] not in containers[-1]:
|
|
196
|
+
return
|
|
197
|
+
del containers[-1][parts[-1]]
|
|
198
|
+
|
|
199
|
+
# prune emptied parents (but never the document itself)
|
|
200
|
+
for i in range(len(containers) - 1, 0, -1):
|
|
201
|
+
if len(containers[i]) == 0:
|
|
202
|
+
del containers[i - 1][parts[i - 1]]
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _to_item(value: Any) -> Any:
|
|
206
|
+
if isinstance(value, (list, tuple)):
|
|
207
|
+
array = tomlkit.array()
|
|
208
|
+
array.extend(value)
|
|
209
|
+
if len(array) >= 2:
|
|
210
|
+
array.multiline(True)
|
|
211
|
+
return array
|
|
212
|
+
return value
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _normalize(config: ProjectConfig, text: str) -> str:
|
|
216
|
+
"""
|
|
217
|
+
Normalize with toml-sort as a library, driven by the project's `TomlSortConfig`
|
|
218
|
+
(skipped if toml-sort is not among the formatters).
|
|
219
|
+
"""
|
|
220
|
+
tomlsort_config = next(
|
|
221
|
+
(
|
|
222
|
+
f
|
|
223
|
+
for f in config.tools.formatting.formatters
|
|
224
|
+
if isinstance(f, TomlSortConfig)
|
|
225
|
+
),
|
|
226
|
+
None,
|
|
227
|
+
)
|
|
228
|
+
if tomlsort_config is None:
|
|
229
|
+
return text
|
|
230
|
+
|
|
231
|
+
from toml_sort import TomlSort
|
|
232
|
+
from toml_sort.tomlsort import FormattingConfiguration, SortConfiguration
|
|
233
|
+
|
|
234
|
+
sort_config = SortConfiguration(
|
|
235
|
+
table_keys=bool(tomlsort_config.sort_table_keys),
|
|
236
|
+
first=list(tomlsort_config.sort_first or []),
|
|
237
|
+
)
|
|
238
|
+
format_config = FormattingConfiguration(
|
|
239
|
+
spaces_before_inline_comment=tomlsort_config.spaces_before_inline_comment or 1,
|
|
240
|
+
)
|
|
241
|
+
return TomlSort(
|
|
242
|
+
input_toml=text, sort_config=sort_config, format_config=format_config
|
|
243
|
+
).sorted()
|