pyprojkit 0.3.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
+ DocfmtConfig,
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
+ "DocfmtConfig",
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
+ DocfmtConfig,
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
+ "DocfmtConfig",
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
+ DocfmtConfig,
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
+ DocfmtConfig(),
43
+ BlackConfig(),
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