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 ADDED
@@ -0,0 +1,104 @@
1
+ """
2
+ Development workflow toolkit for Python projects.
3
+
4
+ Declare configuration once in `pyprojconf.py`; pyprojkit keeps `pyproject.toml` in sync
5
+ and provides `doit` task and `nox` session factories.
6
+ """
7
+
8
+ __submodules__ = [
9
+ "config",
10
+ "versions",
11
+ "discovery",
12
+ "sync",
13
+ "tasks",
14
+ "sessions",
15
+ ]
16
+
17
+ # isort: off
18
+ # <AUTOGEN_INIT>
19
+ from .versions import (
20
+ DEFAULT_PATCH_VERSIONS,
21
+ PythonVersions,
22
+ )
23
+ from .config import (
24
+ ConfigError,
25
+ BaseToolConfig,
26
+ BaseFormatterConfig,
27
+ AutoflakeConfig,
28
+ IsortConfig,
29
+ BlackConfig,
30
+ DocformatterConfig,
31
+ TomlSortConfig,
32
+ PytestConfig,
33
+ CoverageRunConfig,
34
+ CoverageReportConfig,
35
+ DoitConfig,
36
+ NoxConfig,
37
+ MypyConfig,
38
+ ProjectConfig,
39
+ ToolsConfig,
40
+ FormattingConfig,
41
+ TestConfig,
42
+ DocConfig,
43
+ MkinitConfig,
44
+ SphinxConfig,
45
+ AnalysisConfig,
46
+ PublishConfig,
47
+ get_formatting_profile,
48
+ get_tools_profile,
49
+ register_formatting_profile,
50
+ register_tools_profile,
51
+ )
52
+ from .discovery import (
53
+ load_config,
54
+ )
55
+ from .sync import (
56
+ compute_managed_tables,
57
+ render,
58
+ sync,
59
+ )
60
+ from .tasks import (
61
+ TaskFactory,
62
+ )
63
+ from .sessions import (
64
+ NoxFactory,
65
+ )
66
+
67
+ __all__ = [
68
+ "sync",
69
+ "DEFAULT_PATCH_VERSIONS",
70
+ "PythonVersions",
71
+ "ConfigError",
72
+ "BaseToolConfig",
73
+ "BaseFormatterConfig",
74
+ "AutoflakeConfig",
75
+ "IsortConfig",
76
+ "BlackConfig",
77
+ "DocformatterConfig",
78
+ "TomlSortConfig",
79
+ "PytestConfig",
80
+ "CoverageRunConfig",
81
+ "CoverageReportConfig",
82
+ "DoitConfig",
83
+ "NoxConfig",
84
+ "MypyConfig",
85
+ "ProjectConfig",
86
+ "ToolsConfig",
87
+ "FormattingConfig",
88
+ "TestConfig",
89
+ "DocConfig",
90
+ "MkinitConfig",
91
+ "SphinxConfig",
92
+ "AnalysisConfig",
93
+ "PublishConfig",
94
+ "get_formatting_profile",
95
+ "get_tools_profile",
96
+ "register_formatting_profile",
97
+ "register_tools_profile",
98
+ "load_config",
99
+ "compute_managed_tables",
100
+ "render",
101
+ "TaskFactory",
102
+ "NoxFactory",
103
+ ]
104
+ # </AUTOGEN_INIT>
pyprojkit/_paths.py ADDED
@@ -0,0 +1,43 @@
1
+ """
2
+ Shared path conventions for caches, artifacts, and badges.
3
+
4
+ These are used by config defaults, doit task factories, and nox session factories alike,
5
+ so they must not be coupled to any one tool.
6
+ """
7
+
8
+ from pathlib import Path
9
+
10
+ # caches
11
+ CACHE_PATH = Path("__cache__")
12
+ DOIT_DB_PATH = CACHE_PATH / ".doit.db"
13
+ COVERAGE_DATA_PATH = CACHE_PATH / ".coverage"
14
+ PYTEST_CACHE_PATH = CACHE_PATH / "pytest"
15
+ NOX_ENVDIR_PATH = CACHE_PATH / "nox"
16
+
17
+ # artifact output
18
+ OUT_PATH = Path("__out__")
19
+
20
+ # test coverage results
21
+ TESTS_PATH = OUT_PATH / "test"
22
+ JUNIT_PATH = TESTS_PATH / "junit.xml"
23
+ COV_PATH = TESTS_PATH / "cov"
24
+ COV_HTML_PATH = COV_PATH / "html"
25
+ COV_XML_PATH = COV_PATH / "coverage.xml"
26
+
27
+ # documentation
28
+ DOC_PATH = OUT_PATH / "doc"
29
+ DOC_HTML_PATH = DOC_PATH / "html"
30
+
31
+ # static analysis results
32
+ ANALYSIS_PATH = OUT_PATH / "analysis"
33
+ MYPY_PATH = ANALYSIS_PATH / "mypy"
34
+ MYPY_HTML_PATH = MYPY_PATH / "html"
35
+ MYPY_XML_PATH = MYPY_PATH / "xml"
36
+
37
+ # build/publish artifacts
38
+ UV_PATH = OUT_PATH / "uv"
39
+
40
+ # badges output
41
+ BADGES_PATH = Path("badges")
42
+ PYTEST_BADGE_PATH = BADGES_PATH / "tests.svg"
43
+ COV_BADGE_PATH = BADGES_PATH / "cov.svg"
pyprojkit/_run.py ADDED
@@ -0,0 +1,36 @@
1
+ """
2
+ Subprocess helpers for task actions.
3
+ """
4
+
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ from pathlib import Path
10
+
11
+
12
+ def run(cmd: list[str], expect_rc: int | set[int] = 0):
13
+ """
14
+ Run command, exiting if the return code is not among the expected ones.
15
+
16
+ The current interpreter's bin directory is prepended to `PATH` so tools from the
17
+ active environment are found even if it isn't activated.
18
+ """
19
+ expect_rcs = expect_rc if isinstance(expect_rc, set) else {expect_rc}
20
+ env = os.environ.copy()
21
+ env["PATH"] = os.pathsep.join(
22
+ [str(Path(sys.executable).parent), env.get("PATH", "")]
23
+ )
24
+ print(f"=== Running: {cmd[0]}")
25
+ rc = subprocess.call(cmd, env=env)
26
+ if rc not in expect_rcs:
27
+ sys.exit(f"{cmd[0]} failed: rc={rc}, cmd={cmd}")
28
+
29
+
30
+ def cleanup_dir(output_dir: Path | str):
31
+ """
32
+ Remove directory if it exists.
33
+ """
34
+ path = Path(output_dir)
35
+ if path.exists():
36
+ shutil.rmtree(path)
pyprojkit/cli.py ADDED
@@ -0,0 +1,58 @@
1
+ """
2
+ Command-line interface: `pyprojkit sync [--check]`.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import argparse
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ from .config.base import ConfigError
12
+ from .discovery import load_config
13
+ from .sync import sync
14
+
15
+ __all__ = [
16
+ "main",
17
+ ]
18
+
19
+
20
+ def main(argv: list[str] | None = None) -> int:
21
+ parser = argparse.ArgumentParser(
22
+ prog="pyprojkit", description="Development workflow toolkit"
23
+ )
24
+ subparsers = parser.add_subparsers(dest="command", required=True)
25
+
26
+ sync_parser = subparsers.add_parser(
27
+ "sync", help="Sync pyproject.toml from pyprojconf.py"
28
+ )
29
+ sync_parser.add_argument(
30
+ "--check",
31
+ action="store_true",
32
+ help="Verify pyproject.toml is in sync without writing; exit 1 if not",
33
+ )
34
+ sync_parser.add_argument(
35
+ "--root",
36
+ type=Path,
37
+ default=None,
38
+ help="Project root (default: current directory)",
39
+ )
40
+
41
+ args = parser.parse_args(argv)
42
+
43
+ try:
44
+ config = load_config(args.root)
45
+ in_sync = sync(config, args.root, check=args.check)
46
+ except ConfigError as exc:
47
+ print(f"Error: {exc}", file=sys.stderr)
48
+ return 2
49
+
50
+ if args.check and not in_sync:
51
+ print("pyproject.toml is out of sync; run `pyprojkit sync`", file=sys.stderr)
52
+ return 1
53
+
54
+ return 0
55
+
56
+
57
+ if __name__ == "__main__":
58
+ sys.exit(main())
@@ -0,0 +1,79 @@
1
+ """
2
+ Configuration model for pyprojkit.
3
+ """
4
+
5
+ __submodules__ = [
6
+ "base",
7
+ "tools",
8
+ "project",
9
+ "profiles",
10
+ ]
11
+
12
+ # isort: off
13
+ # <AUTOGEN_INIT>
14
+ from .base import (
15
+ ConfigError,
16
+ BaseToolConfig,
17
+ BaseFormatterConfig,
18
+ )
19
+ from .tools import (
20
+ AutoflakeConfig,
21
+ IsortConfig,
22
+ BlackConfig,
23
+ DocformatterConfig,
24
+ TomlSortConfig,
25
+ PytestConfig,
26
+ CoverageRunConfig,
27
+ CoverageReportConfig,
28
+ DoitConfig,
29
+ NoxConfig,
30
+ MypyConfig,
31
+ )
32
+ from .project import (
33
+ ProjectConfig,
34
+ ToolsConfig,
35
+ FormattingConfig,
36
+ TestConfig,
37
+ DocConfig,
38
+ MkinitConfig,
39
+ SphinxConfig,
40
+ AnalysisConfig,
41
+ PublishConfig,
42
+ )
43
+ from .profiles import (
44
+ get_formatting_profile,
45
+ get_tools_profile,
46
+ register_formatting_profile,
47
+ register_tools_profile,
48
+ )
49
+
50
+ __all__ = [
51
+ "ConfigError",
52
+ "BaseToolConfig",
53
+ "BaseFormatterConfig",
54
+ "AutoflakeConfig",
55
+ "IsortConfig",
56
+ "BlackConfig",
57
+ "DocformatterConfig",
58
+ "TomlSortConfig",
59
+ "PytestConfig",
60
+ "CoverageRunConfig",
61
+ "CoverageReportConfig",
62
+ "DoitConfig",
63
+ "NoxConfig",
64
+ "MypyConfig",
65
+ "ProjectConfig",
66
+ "ToolsConfig",
67
+ "FormattingConfig",
68
+ "TestConfig",
69
+ "DocConfig",
70
+ "MkinitConfig",
71
+ "SphinxConfig",
72
+ "AnalysisConfig",
73
+ "PublishConfig",
74
+ "get_formatting_profile",
75
+ "get_tools_profile",
76
+ "register_formatting_profile",
77
+ "register_tools_profile",
78
+ ]
79
+ # </AUTOGEN_INIT>
@@ -0,0 +1,84 @@
1
+ """
2
+ Base classes for tool configurations.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from abc import ABC, abstractmethod
8
+ from dataclasses import dataclass, fields
9
+ from typing import TYPE_CHECKING, Any, ClassVar
10
+
11
+ if TYPE_CHECKING:
12
+ from .project import ProjectConfig
13
+
14
+ __all__ = [
15
+ "ConfigError",
16
+ "BaseToolConfig",
17
+ "BaseFormatterConfig",
18
+ ]
19
+
20
+
21
+ class ConfigError(Exception):
22
+ """
23
+ Raised for invalid or missing configuration.
24
+ """
25
+
26
+
27
+ @dataclass(kw_only=True)
28
+ class BaseToolConfig(ABC):
29
+ """
30
+ Configuration for a tool which reads its settings from a table in
31
+ `pyproject.toml`, e.g. `[tool.black]`.
32
+
33
+ Dataclass fields map to table entries; a field whose name differs from its
34
+ TOML key declares the key via `field(metadata={"toml": "some-key"})`.
35
+ Fields set to `None` are omitted. Values derived from project-wide config
36
+ (e.g. black's `target-version`) are contributed by `extra_toml()`.
37
+ """
38
+
39
+ table_path: ClassVar[str]
40
+ """
41
+ Dotted path of the tool's table, e.g. `"tool.pytest.ini_options"`.
42
+ """
43
+
44
+ def to_toml(self, project: ProjectConfig) -> dict[str, Any]:
45
+ """
46
+ Get this tool's table contents.
47
+ """
48
+ config: dict[str, Any] = {}
49
+ for field_ in fields(self):
50
+ value = getattr(self, field_.name)
51
+ if value is None:
52
+ continue
53
+ key = field_.metadata.get("toml", field_.name)
54
+ config[key] = list(value) if isinstance(value, tuple) else value
55
+ config.update(self.extra_toml(project))
56
+ return config
57
+
58
+ def extra_toml(self, project: ProjectConfig) -> dict[str, Any]:
59
+ """
60
+ Get extra table entries derived from project-wide config.
61
+ """
62
+ return {}
63
+
64
+
65
+ @dataclass(kw_only=True)
66
+ class BaseFormatterConfig(BaseToolConfig):
67
+ """
68
+ Configuration for a tool which additionally runs as part of the `format` task.
69
+
70
+ Adds the command line to invoke and the expected return codes; the order of
71
+ formatters in `FormattingConfig` is the run order.
72
+ """
73
+
74
+ expect_rc: ClassVar[frozenset[int]] = frozenset({0})
75
+ """
76
+ Return codes indicating success.
77
+ """
78
+
79
+ @abstractmethod
80
+ def command(self, py_paths: list[str], toml_paths: list[str]) -> list[str]:
81
+ """
82
+ Get the command line to run, given python and toml paths to format.
83
+ """
84
+ ...
@@ -0,0 +1,97 @@
1
+ """
2
+ Profile registries: named factories for pre-canned configurations.
3
+
4
+ A tools profile encompasses all tool categories; a formatting profile covers
5
+ just the formatter chain. Third parties can register their own profiles and
6
+ reuse them across projects:
7
+
8
+ ```python
9
+ from pyprojkit import register_tools_profile
10
+
11
+ register_tools_profile("mycompany", lambda: ToolsConfig(...))
12
+ ```
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Callable
18
+
19
+ from .base import ConfigError
20
+ from .project import FormattingConfig, ToolsConfig
21
+ from .tools import (
22
+ AutoflakeConfig,
23
+ BlackConfig,
24
+ DocformatterConfig,
25
+ IsortConfig,
26
+ TomlSortConfig,
27
+ )
28
+
29
+ __all__ = [
30
+ "get_formatting_profile",
31
+ "get_tools_profile",
32
+ "register_formatting_profile",
33
+ "register_tools_profile",
34
+ ]
35
+
36
+
37
+ def _default_formatting() -> FormattingConfig:
38
+ return FormattingConfig(
39
+ formatters=(
40
+ AutoflakeConfig(),
41
+ IsortConfig(),
42
+ BlackConfig(),
43
+ DocformatterConfig(),
44
+ TomlSortConfig(),
45
+ )
46
+ )
47
+
48
+
49
+ _FORMATTING_PROFILES: dict[str, Callable[[], FormattingConfig]] = {
50
+ "default": _default_formatting,
51
+ }
52
+
53
+ # the zero-arg ToolsConfig constructor is itself the default profile, as its
54
+ # field defaults resolve to the default sub-configs
55
+ _TOOLS_PROFILES: dict[str, Callable[[], ToolsConfig]] = {
56
+ "default": ToolsConfig,
57
+ }
58
+
59
+
60
+ def get_formatting_profile(name: str) -> FormattingConfig:
61
+ """
62
+ Get a new instance of the given formatting profile.
63
+ """
64
+ try:
65
+ factory = _FORMATTING_PROFILES[name]
66
+ except KeyError:
67
+ raise ConfigError(
68
+ f"Unknown formatting profile '{name}'; have {sorted(_FORMATTING_PROFILES)}"
69
+ ) from None
70
+ return factory()
71
+
72
+
73
+ def get_tools_profile(name: str) -> ToolsConfig:
74
+ """
75
+ Get a new instance of the given tools profile.
76
+ """
77
+ try:
78
+ factory = _TOOLS_PROFILES[name]
79
+ except KeyError:
80
+ raise ConfigError(
81
+ f"Unknown tools profile '{name}'; have {sorted(_TOOLS_PROFILES)}"
82
+ ) from None
83
+ return factory()
84
+
85
+
86
+ def register_formatting_profile(name: str, factory: Callable[[], FormattingConfig]):
87
+ """
88
+ Register a named formatting profile.
89
+ """
90
+ _FORMATTING_PROFILES[name] = factory
91
+
92
+
93
+ def register_tools_profile(name: str, factory: Callable[[], ToolsConfig]):
94
+ """
95
+ Register a named tools profile.
96
+ """
97
+ _TOOLS_PROFILES[name] = factory
@@ -0,0 +1,194 @@
1
+ """
2
+ Project-wide configuration, declared in a project's `pyprojconf.py`.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass, field
8
+ from typing import Any, Sequence
9
+
10
+ from .. import _paths
11
+ from ..versions import PythonVersions
12
+ from .base import BaseFormatterConfig
13
+ from .tools import (
14
+ CoverageReportConfig,
15
+ CoverageRunConfig,
16
+ DoitConfig,
17
+ MypyConfig,
18
+ NoxConfig,
19
+ PytestConfig,
20
+ )
21
+
22
+ __all__ = [
23
+ "ProjectConfig",
24
+ "ToolsConfig",
25
+ "FormattingConfig",
26
+ "TestConfig",
27
+ "DocConfig",
28
+ "MkinitConfig",
29
+ "SphinxConfig",
30
+ "AnalysisConfig",
31
+ "PublishConfig",
32
+ ]
33
+
34
+
35
+ @dataclass(kw_only=True)
36
+ class FormattingConfig:
37
+ """
38
+ Formatting configuration: which formatters run (in order) and their settings.
39
+ """
40
+
41
+ formatters: tuple[BaseFormatterConfig, ...]
42
+ """
43
+ Formatters in run order.
44
+ """
45
+
46
+ @classmethod
47
+ def default(cls) -> FormattingConfig:
48
+ """
49
+ Get the default formatting profile.
50
+ """
51
+ from .profiles import get_formatting_profile
52
+
53
+ return get_formatting_profile("default")
54
+
55
+
56
+ @dataclass(kw_only=True)
57
+ class TestConfig:
58
+ """
59
+ Testing configuration (pytest + coverage).
60
+ """
61
+
62
+ pytest: PytestConfig = field(default_factory=PytestConfig)
63
+ coverage_run: CoverageRunConfig = field(default_factory=CoverageRunConfig)
64
+ coverage_report: CoverageReportConfig = field(default_factory=CoverageReportConfig)
65
+
66
+
67
+ @dataclass(kw_only=True)
68
+ class MkinitConfig:
69
+ """
70
+ Configuration for generating `__init__.py` files using mkinit.
71
+ """
72
+
73
+ args: tuple[str, ...] = (
74
+ "--recursive",
75
+ "--nomods",
76
+ "--relative",
77
+ "-i",
78
+ "--source-order",
79
+ )
80
+ """
81
+ Arguments passed to mkinit.
82
+ """
83
+
84
+ @dataclass(kw_only=True)
85
+ class SphinxConfig:
86
+ """
87
+ Configuration for building documentation using sphinx.
88
+ """
89
+
90
+ source_dir: str = "doc"
91
+ """
92
+ Directory containing sphinx sources.
93
+ """
94
+
95
+ copy_env_var: str | None = None
96
+ """
97
+ Name of environment variable (or `.env` entry) giving a directory to which to copy
98
+ built documentation when the doc task is run with `--copy`.
99
+ """
100
+
101
+ @dataclass(kw_only=True)
102
+ class DocConfig:
103
+ """
104
+ Documentation tool configurations.
105
+ """
106
+
107
+ mkinit: MkinitConfig | None = field(default_factory=MkinitConfig)
108
+ """
109
+ Enables the `init` task; enabled by default.
110
+ """
111
+
112
+ sphinx: SphinxConfig | None = None
113
+ """
114
+ Enables the `doc` task; opt-in.
115
+ """
116
+
117
+ @dataclass(kw_only=True)
118
+ class AnalysisConfig:
119
+ """
120
+ Static analysis configuration.
121
+ """
122
+ mypy: MypyConfig | None = field(default_factory=MypyConfig)
123
+ pyright: bool = True
124
+
125
+
126
+ @dataclass(kw_only=True)
127
+ class PublishConfig:
128
+ """
129
+ Configuration for building and publishing via uv.
130
+ """
131
+ out_dir: str = str(_paths.UV_PATH)
132
+ """
133
+ Directory for build artifacts; cleaned before each build so stale artifacts are
134
+ never published.
135
+ """
136
+ @dataclass(kw_only=True)
137
+ class ToolsConfig:
138
+ """
139
+ Development tool configurations, grouped by broad tool category.
140
+
141
+ A "tools profile" is simply a pre-canned `ToolsConfig` factory; see
142
+ `pyprojkit.config.profiles`. The default instance corresponds to the default
143
+ profile.
144
+ """
145
+ formatting: FormattingConfig = field(default_factory=FormattingConfig.default)
146
+ test: TestConfig | None = field(default_factory=TestConfig)
147
+ doit: DoitConfig = field(default_factory=DoitConfig)
148
+ nox: NoxConfig = field(default_factory=NoxConfig)
149
+ doc: DocConfig = field(default_factory=DocConfig)
150
+ analysis: AnalysisConfig | None = None
151
+ publish: PublishConfig = field(default_factory=PublishConfig)
152
+
153
+ tool_overrides: dict[str, dict[str, Any]] = field(default_factory=dict)
154
+ """
155
+ Escape hatch: extra entries merged last into managed tables, keyed by table path,
156
+ e.g. `{"tool.pytest.ini_options": {"addopts": "..."}}`.
157
+
158
+ A key not otherwise managed creates a new managed table.
159
+ """
160
+ @classmethod
161
+ def default(cls) -> ToolsConfig:
162
+ """
163
+ Get the default tools profile.
164
+ """
165
+ from .profiles import get_tools_profile
166
+
167
+ return get_tools_profile("default")
168
+
169
+
170
+ @dataclass(kw_only=True)
171
+ class ProjectConfig:
172
+ """
173
+ Top-level project configuration.
174
+
175
+ A project's `pyprojconf.py` must define a module-level instance named `config`.
176
+ """
177
+ package: str
178
+ """
179
+ Import name of the package, e.g. `"trilium_alchemy"`.
180
+ """
181
+ python: PythonVersions
182
+ """
183
+ Supported Python versions.
184
+ """
185
+ format_paths: Sequence[str] = ("src", "test", "doc", "examples")
186
+ """
187
+ Directories to format (those which exist), in addition to `*.py` and `*.toml` files
188
+ at the project root.
189
+ """
190
+
191
+ tools: ToolsConfig = field(default_factory=ToolsConfig.default)
192
+ """
193
+ Tool configurations.
194
+ """