pyprojkit 0.4.2__tar.gz → 0.5.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyprojkit
3
- Version: 0.4.2
3
+ Version: 0.5.0
4
4
  Summary: Development workflow toolkit for Python projects: pyproject.toml sync, doit tasks, nox sessions
5
5
  Author: mm21
6
6
  Author-email: mm21 <mm21.dev@gmail.com>
@@ -24,7 +24,7 @@ Requires-Dist: tomlkit>=0.13.3,<0.14
24
24
  Requires-Dist: pyprojkit[analysis,doc,format,test] ; extra == 'all'
25
25
  Requires-Dist: mypy[reports]>=1.5.1,<2 ; extra == 'analysis'
26
26
  Requires-Dist: pyright>=1.1.329,<2 ; extra == 'analysis'
27
- Requires-Dist: mkinit ; extra == 'doc'
27
+ Requires-Dist: mkinit>=1.3.0,<2 ; extra == 'doc'
28
28
  Requires-Dist: autoflake>=2.3.1,<3 ; extra == 'format'
29
29
  Requires-Dist: black>=25.1.0,<26 ; extra == 'format'
30
30
  Requires-Dist: docfmt>=0.3.0,<0.4.0 ; extra == 'format'
@@ -29,7 +29,7 @@ license-files = ["LICENSE"]
29
29
  name = "pyprojkit"
30
30
  readme = "README.md"
31
31
  requires-python = ">=3.12,<4.0" # pyprojkit-managed
32
- version = "0.4.2"
32
+ version = "0.5.0"
33
33
 
34
34
  [project.optional-dependencies]
35
35
  all = [
@@ -40,7 +40,7 @@ analysis = [
40
40
  "pyright>=1.1.329,<2"
41
41
  ]
42
42
  doc = [
43
- "mkinit"
43
+ "mkinit>=1.3.0,<2"
44
44
  ]
45
45
  format = [
46
46
  "autoflake>=2.3.1,<3",
@@ -107,6 +107,7 @@ profile = "black" # pyprojkit-managed
107
107
  quiet = true # pyprojkit-managed
108
108
 
109
109
  [tool.mypy]
110
+ check_untyped_defs = true # pyprojkit-managed
110
111
  ignore_missing_imports = true # pyprojkit-managed
111
112
  python_version = "3.12" # pyprojkit-managed
112
113
 
@@ -114,6 +115,9 @@ python_version = "3.12" # pyprojkit-managed
114
115
  default_venv_backend = "uv" # pyprojkit-managed
115
116
  envdir = "__cache__/nox" # pyprojkit-managed
116
117
 
118
+ [tool.pyright]
119
+ pythonVersion = "3.12" # pyprojkit-managed
120
+
117
121
  [tool.pytest.ini_options]
118
122
  addopts = "--import-mode=importlib -s -v -rA" # pyprojkit-managed
119
123
  cache_dir = "__cache__/pytest" # pyprojkit-managed
@@ -127,6 +131,3 @@ sort_first = [
127
131
  ] # pyprojkit-managed
128
132
  sort_table_keys = true # pyprojkit-managed
129
133
  spaces_before_inline_comment = 2 # pyprojkit-managed
130
-
131
- [tool.uv.sources]
132
- mkinit = {git = "https://github.com/mm21/mkinit.git"}
@@ -19,6 +19,7 @@ __submodules__ = [
19
19
  # <AUTOGEN_INIT>
20
20
  from .config import (
21
21
  ConfigError,
22
+ BaseConfig,
22
23
  BaseToolConfig,
23
24
  BaseFormatterConfig,
24
25
  AutoflakeConfig,
@@ -32,6 +33,7 @@ from .config import (
32
33
  DoitConfig,
33
34
  NoxConfig,
34
35
  MypyConfig,
36
+ PyrightConfig,
35
37
  ProjectConfig,
36
38
  ToolsConfig,
37
39
  FormattingConfig,
@@ -71,6 +73,7 @@ from .sessions import (
71
73
  __all__ = [
72
74
  "sync",
73
75
  "ConfigError",
76
+ "BaseConfig",
74
77
  "BaseToolConfig",
75
78
  "BaseFormatterConfig",
76
79
  "AutoflakeConfig",
@@ -84,6 +87,7 @@ __all__ = [
84
87
  "DoitConfig",
85
88
  "NoxConfig",
86
89
  "MypyConfig",
90
+ "PyrightConfig",
87
91
  "ProjectConfig",
88
92
  "ToolsConfig",
89
93
  "FormattingConfig",
@@ -13,6 +13,7 @@ __submodules__ = [
13
13
  # <AUTOGEN_INIT>
14
14
  from .base import (
15
15
  ConfigError,
16
+ BaseConfig,
16
17
  BaseToolConfig,
17
18
  BaseFormatterConfig,
18
19
  )
@@ -28,6 +29,7 @@ from .tools import (
28
29
  DoitConfig,
29
30
  NoxConfig,
30
31
  MypyConfig,
32
+ PyrightConfig,
31
33
  )
32
34
  from .project import (
33
35
  ProjectConfig,
@@ -47,6 +49,7 @@ from .profiles import (
47
49
 
48
50
  __all__ = [
49
51
  "ConfigError",
52
+ "BaseConfig",
50
53
  "BaseToolConfig",
51
54
  "BaseFormatterConfig",
52
55
  "AutoflakeConfig",
@@ -60,6 +63,7 @@ __all__ = [
60
63
  "DoitConfig",
61
64
  "NoxConfig",
62
65
  "MypyConfig",
66
+ "PyrightConfig",
63
67
  "ProjectConfig",
64
68
  "ToolsConfig",
65
69
  "FormattingConfig",
@@ -6,13 +6,14 @@ from __future__ import annotations
6
6
 
7
7
  from abc import ABC, abstractmethod
8
8
  from dataclasses import dataclass, fields
9
- from typing import TYPE_CHECKING, Any, ClassVar
9
+ from typing import TYPE_CHECKING, Any, ClassVar, Self
10
10
 
11
11
  if TYPE_CHECKING:
12
12
  from .project import ProjectConfig
13
13
 
14
14
  __all__ = [
15
15
  "ConfigError",
16
+ "BaseConfig",
16
17
  "BaseToolConfig",
17
18
  "BaseFormatterConfig",
18
19
  ]
@@ -24,16 +25,36 @@ class ConfigError(Exception):
24
25
  """
25
26
 
26
27
 
28
+ class BaseConfig(ABC):
29
+ """
30
+ Base for all tool configurations: both tool categories (e.g. `TestConfig`) and
31
+ individual tools' `pyproject.toml` tables (e.g. `PytestConfig`).
32
+
33
+ A default-constructed config is empty: every setting is unset and every optional
34
+ sub-config is disabled. Opinionated settings are provided by
35
+ `create_kitchen_sink()`.
36
+ """
37
+
38
+ @classmethod
39
+ @abstractmethod
40
+ def create_kitchen_sink(cls) -> Self:
41
+ """
42
+ Get a new instance with opinionated settings, as used by
43
+ `create_kitchen_sink_tools()`.
44
+ """
45
+ ...
46
+
47
+
27
48
  @dataclass(kw_only=True)
28
- class BaseToolConfig(ABC):
49
+ class BaseToolConfig(BaseConfig):
29
50
  """
30
51
  Configuration for a tool which reads its settings from a table in `pyproject.toml`,
31
52
  e.g. `[tool.black]`.
32
53
 
33
54
  Dataclass fields map to table entries; a field whose name differs from its TOML key
34
55
  declares the key via `field(metadata={"toml": "some-key"})`. Fields set to `None`
35
- are omitted. Values derived from project-wide config (e.g. black's `target-version`)
36
- are contributed by `extra_toml()`.
56
+ (the default for every field) are omitted. Values derived from project-wide config
57
+ (e.g. black's `target-version`) are contributed by `extra_toml()`.
37
58
  """
38
59
 
39
60
  table_path: ClassVar[str]
@@ -0,0 +1,51 @@
1
+ """
2
+ Built-in tool configurations.
3
+
4
+ Configurations are plain `ToolsConfig` values: define your own factory function or
5
+ global in a module shared across projects, enabling just the tools you want:
6
+
7
+ ```python
8
+ from pyprojkit import (
9
+ AnalysisConfig,
10
+ BlackConfig,
11
+ FormattingConfig,
12
+ IsortConfig,
13
+ PyrightConfig,
14
+ PytestConfig,
15
+ TestConfig,
16
+ ToolsConfig,
17
+ )
18
+
19
+
20
+ def create_mycompany_tools() -> ToolsConfig:
21
+ return ToolsConfig(
22
+ formatting=FormattingConfig(
23
+ formatters=(
24
+ IsortConfig(profile="black"),
25
+ BlackConfig(),
26
+ )
27
+ ),
28
+ test=TestConfig(pytest=PytestConfig(testpaths="tests")),
29
+ analysis=AnalysisConfig(pyright=PyrightConfig()),
30
+ )
31
+ ```
32
+
33
+ Alternatively, start from `create_kitchen_sink_tools()` and override parts of it with
34
+ `dataclasses.replace()`.
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ from .project import ToolsConfig
40
+
41
+ __all__ = [
42
+ "create_kitchen_sink_tools",
43
+ ]
44
+
45
+
46
+ def create_kitchen_sink_tools() -> ToolsConfig:
47
+ """
48
+ Get a new instance of the configuration which enables every tool; equivalent to
49
+ `ToolsConfig.create_kitchen_sink()`.
50
+ """
51
+ return ToolsConfig.create_kitchen_sink()
@@ -6,18 +6,24 @@ from __future__ import annotations
6
6
 
7
7
  from dataclasses import dataclass, field
8
8
  from pathlib import Path
9
- from typing import Literal, Sequence
9
+ from typing import Literal, Self, Sequence
10
10
 
11
11
  from .. import _paths
12
12
  from ..versions import PythonVersions, PythonVersionSet, RequiresPython
13
- from .base import BaseFormatterConfig
13
+ from .base import BaseConfig, BaseFormatterConfig
14
14
  from .tools import (
15
+ AutoflakeConfig,
16
+ BlackConfig,
15
17
  CoverageReportConfig,
16
18
  CoverageRunConfig,
19
+ DocfmtConfig,
17
20
  DoitConfig,
21
+ IsortConfig,
18
22
  MypyConfig,
19
23
  NoxConfig,
24
+ PyrightConfig,
20
25
  PytestConfig,
26
+ TomlSortConfig,
21
27
  )
22
28
 
23
29
  __all__ = [
@@ -35,19 +41,31 @@ __all__ = [
35
41
 
36
42
 
37
43
  @dataclass(kw_only=True)
38
- class FormattingConfig:
44
+ class FormattingConfig(BaseConfig):
39
45
  """
40
46
  Formatting configuration: which formatters run (in order) and their settings.
41
47
  """
42
48
 
43
- formatters: tuple[BaseFormatterConfig, ...]
49
+ formatters: tuple[BaseFormatterConfig, ...] = ()
44
50
  """
45
51
  Formatters in run order.
46
52
  """
47
53
 
54
+ @classmethod
55
+ def create_kitchen_sink(cls) -> Self:
56
+ return cls(
57
+ formatters=(
58
+ AutoflakeConfig.create_kitchen_sink(),
59
+ IsortConfig.create_kitchen_sink(),
60
+ DocfmtConfig.create_kitchen_sink(),
61
+ BlackConfig.create_kitchen_sink(),
62
+ TomlSortConfig.create_kitchen_sink(),
63
+ )
64
+ )
65
+
48
66
 
49
67
  @dataclass(kw_only=True)
50
- class TestConfig:
68
+ class TestConfig(BaseConfig):
51
69
  """
52
70
  Testing configuration (pytest + coverage).
53
71
  """
@@ -56,27 +74,35 @@ class TestConfig:
56
74
  coverage_run: CoverageRunConfig = field(default_factory=CoverageRunConfig)
57
75
  coverage_report: CoverageReportConfig = field(default_factory=CoverageReportConfig)
58
76
 
77
+ @classmethod
78
+ def create_kitchen_sink(cls) -> Self:
79
+ return cls(
80
+ pytest=PytestConfig.create_kitchen_sink(),
81
+ coverage_run=CoverageRunConfig.create_kitchen_sink(),
82
+ coverage_report=CoverageReportConfig.create_kitchen_sink(),
83
+ )
84
+
59
85
 
60
86
  @dataclass(kw_only=True)
61
- class MkinitConfig:
87
+ class MkinitConfig(BaseConfig):
62
88
  """
63
89
  Configuration for generating `__init__.py` files using mkinit.
64
90
  """
65
91
 
66
- args: tuple[str, ...] = (
67
- "--recursive",
68
- "--nomods",
69
- "--relative",
70
- "-i",
71
- "--source-order",
72
- )
92
+ args: tuple[str, ...] = ()
73
93
  """
74
94
  Arguments passed to mkinit.
75
95
  """
76
96
 
97
+ @classmethod
98
+ def create_kitchen_sink(cls) -> Self:
99
+ return cls(
100
+ args=("--recursive", "--nomods", "--relative", "-i", "--source-order")
101
+ )
102
+
77
103
 
78
104
  @dataclass(kw_only=True)
79
- class SphinxConfig:
105
+ class SphinxConfig(BaseConfig):
80
106
  """
81
107
  Configuration for building documentation using sphinx.
82
108
  """
@@ -92,49 +118,72 @@ class SphinxConfig:
92
118
  built documentation when the doc task is run with `--copy`.
93
119
  """
94
120
 
121
+ @classmethod
122
+ def create_kitchen_sink(cls) -> Self:
123
+ return cls()
124
+
95
125
 
96
126
  @dataclass(kw_only=True)
97
- class DocConfig:
127
+ class DocConfig(BaseConfig):
98
128
  """
99
129
  Documentation tool configurations.
100
130
  """
101
131
 
102
- mkinit: MkinitConfig | None = field(default_factory=MkinitConfig)
132
+ mkinit: MkinitConfig | None = None
103
133
  """
104
- Enables the `init` task; enabled by default.
134
+ Enables the `init` task.
105
135
  """
106
136
 
107
137
  sphinx: SphinxConfig | None = None
108
138
  """
109
- Enables the `doc` task; opt-in.
139
+ Enables the `doc` task.
110
140
  """
111
141
 
142
+ @classmethod
143
+ def create_kitchen_sink(cls) -> Self:
144
+ """
145
+ Enables mkinit; sphinx is left disabled since it needs project-specific
146
+ settings.
147
+ """
148
+ return cls(mkinit=MkinitConfig.create_kitchen_sink())
149
+
112
150
 
113
151
  @dataclass(kw_only=True)
114
- class AnalysisConfig:
152
+ class AnalysisConfig(BaseConfig):
115
153
  """
116
154
  Static analysis configuration.
117
155
  """
118
156
 
119
- mypy: MypyConfig | None = field(default_factory=MypyConfig)
120
- pyright: bool = True
157
+ mypy: MypyConfig | None = None
158
+ pyright: PyrightConfig | None = None
159
+
160
+ @classmethod
161
+ def create_kitchen_sink(cls) -> Self:
162
+ return cls(
163
+ mypy=MypyConfig.create_kitchen_sink(),
164
+ pyright=PyrightConfig.create_kitchen_sink(),
165
+ )
121
166
 
122
167
 
123
168
  @dataclass(kw_only=True)
124
- class PublishConfig:
169
+ class PublishConfig(BaseConfig):
125
170
  """
126
171
  Configuration for building and publishing via uv.
127
172
  """
128
173
 
129
- out_dir: str = str(_paths.UV_PATH)
174
+ out_dir: str | None = None
130
175
  """
131
- Directory for build artifacts; cleaned before each build so stale artifacts are
132
- never published.
176
+ Directory for build artifacts, defaulting to uv's `dist`; cleaned before each build
177
+ so stale artifacts are never published.
133
178
  """
134
179
 
180
+ @classmethod
181
+ def create_kitchen_sink(cls) -> Self:
182
+ return cls(out_dir=str(_paths.UV_PATH))
183
+
135
184
 
136
185
  @dataclass(kw_only=True)
137
- class SkillConfig:
186
+ class SkillConfig(BaseConfig):
138
187
  """
139
188
  Agent skill integration: a `pyprojkit-python` skill in the
140
189
  [Agent Skills](https://agentskills.io) format, generated from the project's
@@ -148,16 +197,20 @@ class SkillConfig:
148
197
  `<skills_dir>/pyprojkit-python/SKILL.md`.
149
198
  """
150
199
 
200
+ @classmethod
201
+ def create_kitchen_sink(cls) -> Self:
202
+ return cls()
203
+
151
204
 
152
205
  @dataclass(kw_only=True)
153
- class ToolsConfig:
206
+ class ToolsConfig(BaseConfig):
154
207
  """
155
208
  Development tool configurations, grouped by broad tool category.
156
209
 
157
210
  Every category is opt-in: `None` (the default) means the tool is neither managed nor
158
- given a task. For a pre-canned set of everything, see `create_kitchen_sink_tools()`;
159
- to share a configuration across projects, define your own factory function or
160
- global.
211
+ given a task. For a pre-canned set of everything, see `create_kitchen_sink()` (also
212
+ available as `create_kitchen_sink_tools()`); to share a configuration across
213
+ projects, define your own factory function or global.
161
214
  """
162
215
 
163
216
  workflow_tool: Literal["uv", "poetry"] | None = None
@@ -179,6 +232,26 @@ class ToolsConfig:
179
232
  sync.
180
233
  """
181
234
 
235
+ @classmethod
236
+ def create_kitchen_sink(cls) -> Self:
237
+ """
238
+ Enables every tool category, each with its opinionated settings: uv as the
239
+ workflow tool, formatting, testing, doit, nox, mkinit, static analysis,
240
+ publishing and the agent skill. Sphinx is left disabled since it needs
241
+ project-specific settings.
242
+ """
243
+ return cls(
244
+ workflow_tool="uv",
245
+ formatting=FormattingConfig.create_kitchen_sink(),
246
+ test=TestConfig.create_kitchen_sink(),
247
+ doit=DoitConfig.create_kitchen_sink(),
248
+ nox=NoxConfig.create_kitchen_sink(),
249
+ doc=DocConfig.create_kitchen_sink(),
250
+ analysis=AnalysisConfig.create_kitchen_sink(),
251
+ publish=PublishConfig.create_kitchen_sink(),
252
+ skill=SkillConfig.create_kitchen_sink(),
253
+ )
254
+
182
255
 
183
256
  @dataclass(kw_only=True)
184
257
  class ProjectConfig:
@@ -0,0 +1,247 @@
1
+ """
2
+ Configurations for all supported tools.
3
+
4
+ Formatters are tools which additionally run as part of the `format` task. Every field
5
+ defaults to `None` (unset); each tool's opinionated settings are provided by its
6
+ `create_kitchen_sink()`.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass, field
12
+ from typing import TYPE_CHECKING, Any, ClassVar, Self
13
+
14
+ from .. import _paths
15
+ from .base import BaseFormatterConfig, BaseToolConfig
16
+
17
+ if TYPE_CHECKING:
18
+ from .project import ProjectConfig
19
+
20
+ __all__ = [
21
+ "AutoflakeConfig",
22
+ "IsortConfig",
23
+ "BlackConfig",
24
+ "DocfmtConfig",
25
+ "TomlSortConfig",
26
+ "PytestConfig",
27
+ "CoverageRunConfig",
28
+ "CoverageReportConfig",
29
+ "DoitConfig",
30
+ "NoxConfig",
31
+ "MypyConfig",
32
+ "PyrightConfig",
33
+ ]
34
+
35
+
36
+ # --- formatters (run order defined by FormattingConfig) ---
37
+
38
+
39
+ @dataclass(kw_only=True)
40
+ class AutoflakeConfig(BaseFormatterConfig):
41
+ table_path: ClassVar[str] = "tool.autoflake"
42
+
43
+ in_place: bool | None = None
44
+ recursive: bool | None = None
45
+ remove_all_unused_imports: bool | None = field(
46
+ default=None, metadata={"toml": "remove-all-unused-imports"}
47
+ )
48
+ remove_unused_variables: bool | None = field(
49
+ default=None, metadata={"toml": "remove-unused-variables"}
50
+ )
51
+
52
+ @classmethod
53
+ def create_kitchen_sink(cls) -> Self:
54
+ return cls(
55
+ in_place=True,
56
+ recursive=True,
57
+ remove_all_unused_imports=True,
58
+ remove_unused_variables=True,
59
+ )
60
+
61
+ def command(self, py_paths: list[str], toml_paths: list[str]) -> list[str]:
62
+ return ["autoflake"] + py_paths
63
+
64
+
65
+ @dataclass(kw_only=True)
66
+ class IsortConfig(BaseFormatterConfig):
67
+ table_path: ClassVar[str] = "tool.isort"
68
+
69
+ profile: str | None = None
70
+ quiet: bool | None = None
71
+
72
+ @classmethod
73
+ def create_kitchen_sink(cls) -> Self:
74
+ return cls(profile="black", quiet=True)
75
+
76
+ def command(self, py_paths: list[str], toml_paths: list[str]) -> list[str]:
77
+ return ["isort"] + py_paths
78
+
79
+
80
+ @dataclass(kw_only=True)
81
+ class BlackConfig(BaseFormatterConfig):
82
+ table_path: ClassVar[str] = "tool.black"
83
+
84
+ quiet: bool | None = None
85
+
86
+ @classmethod
87
+ def create_kitchen_sink(cls) -> Self:
88
+ return cls(quiet=True)
89
+
90
+ def extra_toml(self, project: ProjectConfig) -> dict[str, Any]:
91
+ versions = project.pinned_versions
92
+ if versions is None:
93
+ return {}
94
+ return {"target-version": [f"py{maj}{min}" for maj, min in versions.versions]}
95
+
96
+ def command(self, py_paths: list[str], toml_paths: list[str]) -> list[str]:
97
+ return ["black"] + py_paths
98
+
99
+
100
+ @dataclass(kw_only=True)
101
+ class DocfmtConfig(BaseFormatterConfig):
102
+ table_path: ClassVar[str] = "tool.docfmt"
103
+
104
+ # docfmt returns 0 when it writes changes in-place; 1 means "would change"
105
+ # in check mode and 2 is an error, so only 0 is expected here
106
+ expect_rc: ClassVar[frozenset[int]] = frozenset({0})
107
+
108
+ # line length is left unset: docfmt picks it up from `[tool.black]`
109
+ in_place: bool | None = field(default=None, metadata={"toml": "in-place"})
110
+ recursive: bool | None = None
111
+ summary_on_own_line: bool | None = field(
112
+ default=None, metadata={"toml": "summary-on-own-line"}
113
+ )
114
+ force_reflow: bool | None = field(default=None, metadata={"toml": "force-reflow"})
115
+
116
+ @classmethod
117
+ def create_kitchen_sink(cls) -> Self:
118
+ return cls(
119
+ in_place=True, recursive=True, summary_on_own_line=True, force_reflow=True
120
+ )
121
+
122
+ def command(self, py_paths: list[str], toml_paths: list[str]) -> list[str]:
123
+ return ["docfmt"] + py_paths
124
+
125
+
126
+ @dataclass(kw_only=True)
127
+ class TomlSortConfig(BaseFormatterConfig):
128
+ table_path: ClassVar[str] = "tool.tomlsort"
129
+
130
+ sort_first: tuple[str, ...] | None = None
131
+ sort_table_keys: bool | None = None
132
+
133
+ # the toml-sort CLI and library defaults disagree; when unset, the sync engine's
134
+ # normalization falls back to the CLI default so it agrees with the format task
135
+ spaces_before_inline_comment: int | None = None
136
+
137
+ @classmethod
138
+ def create_kitchen_sink(cls) -> Self:
139
+ return cls(
140
+ sort_first=("project", "dependency-groups", "build-system"),
141
+ sort_table_keys=True,
142
+ spaces_before_inline_comment=2,
143
+ )
144
+
145
+ def command(self, py_paths: list[str], toml_paths: list[str]) -> list[str]:
146
+ return ["toml-sort", "-i"] + toml_paths
147
+
148
+
149
+ # --- non-formatter tools ---
150
+
151
+
152
+ @dataclass(kw_only=True)
153
+ class PytestConfig(BaseToolConfig):
154
+ table_path: ClassVar[str] = "tool.pytest.ini_options"
155
+
156
+ addopts: str | None = None
157
+ cache_dir: str | None = None
158
+ testpaths: str | None = None
159
+
160
+ @classmethod
161
+ def create_kitchen_sink(cls) -> Self:
162
+ return cls(
163
+ addopts="--import-mode=importlib -s -v -rA",
164
+ cache_dir=str(_paths.PYTEST_CACHE_PATH),
165
+ testpaths="test",
166
+ )
167
+
168
+
169
+ @dataclass(kw_only=True)
170
+ class CoverageRunConfig(BaseToolConfig):
171
+ table_path: ClassVar[str] = "tool.coverage.run"
172
+
173
+ data_file: str | None = None
174
+
175
+ @classmethod
176
+ def create_kitchen_sink(cls) -> Self:
177
+ return cls(data_file=str(_paths.COVERAGE_DATA_PATH))
178
+
179
+
180
+ @dataclass(kw_only=True)
181
+ class CoverageReportConfig(BaseToolConfig):
182
+ table_path: ClassVar[str] = "tool.coverage.report"
183
+
184
+ exclude_lines: tuple[str, ...] | None = None
185
+
186
+ @classmethod
187
+ def create_kitchen_sink(cls) -> Self:
188
+ return cls(exclude_lines=("if TYPE_CHECKING:", "\\.\\.\\.$"))
189
+
190
+
191
+ @dataclass(kw_only=True)
192
+ class DoitConfig(BaseToolConfig):
193
+ table_path: ClassVar[str] = "tool.doit"
194
+
195
+ dep_file: str | None = None
196
+ verbosity: int | None = None
197
+
198
+ @classmethod
199
+ def create_kitchen_sink(cls) -> Self:
200
+ return cls(dep_file=str(_paths.DOIT_DB_PATH), verbosity=2)
201
+
202
+
203
+ @dataclass(kw_only=True)
204
+ class NoxConfig(BaseToolConfig):
205
+ table_path: ClassVar[str] = "tool.nox"
206
+
207
+ default_venv_backend: str | None = None
208
+ envdir: str | None = None
209
+
210
+ @classmethod
211
+ def create_kitchen_sink(cls) -> Self:
212
+ return cls(default_venv_backend="uv", envdir=str(_paths.NOX_ENVDIR_PATH))
213
+
214
+
215
+ @dataclass(kw_only=True)
216
+ class MypyConfig(BaseToolConfig):
217
+ table_path: ClassVar[str] = "tool.mypy"
218
+
219
+ ignore_missing_imports: bool | None = None
220
+ check_untyped_defs: bool | None = None
221
+
222
+ @classmethod
223
+ def create_kitchen_sink(cls) -> Self:
224
+ return cls(ignore_missing_imports=True, check_untyped_defs=True)
225
+
226
+ def extra_toml(self, project: ProjectConfig) -> dict[str, Any]:
227
+ versions = project.pinned_versions
228
+ if versions is None:
229
+ return {}
230
+ maj, min = versions.versions[0]
231
+ return {"python_version": f"{maj}.{min}"}
232
+
233
+
234
+ @dataclass(kw_only=True)
235
+ class PyrightConfig(BaseToolConfig):
236
+ table_path: ClassVar[str] = "tool.pyright"
237
+
238
+ @classmethod
239
+ def create_kitchen_sink(cls) -> Self:
240
+ return cls()
241
+
242
+ def extra_toml(self, project: ProjectConfig) -> dict[str, Any]:
243
+ versions = project.pinned_versions
244
+ if versions is None:
245
+ return {}
246
+ maj, min = versions.versions[0]
247
+ return {"pythonVersion": f"{maj}.{min}"}
@@ -45,7 +45,7 @@ class NoxFactory:
45
45
  ):
46
46
  self._config = config or load_config(root)
47
47
  self._registered = set()
48
- if nox_config := self._config.tools.nox:
48
+ if (nox_config := self._config.tools.nox) and nox_config.envdir is not None:
49
49
  nox.options.envdir = nox_config.envdir
50
50
 
51
51
  def create_test_session(self, *, group: str = "dev") -> Callable[[Session], None]:
@@ -45,7 +45,7 @@ from tomlkit import TOMLDocument
45
45
  from tomlkit.items import AoT, Null, Table
46
46
 
47
47
  from ._run import run
48
- from .config.base import ConfigError
48
+ from .config.base import BaseToolConfig, ConfigError
49
49
  from .config.project import ProjectConfig, SkillConfig
50
50
  from .config.tools import TomlSortConfig
51
51
  from .skill import SKILL_NAME, render_skill
@@ -70,25 +70,21 @@ def compute_managed_fields(config: ProjectConfig) -> dict[str, dict[str, Any]]:
70
70
  """
71
71
  Compute all managed fields, keyed by dotted table path then key.
72
72
  """
73
- tables: dict[str, dict[str, Any]] = {}
73
+ tool_configs: list[BaseToolConfig] = []
74
74
  tools = config.tools
75
75
 
76
76
  if tools.formatting:
77
- for formatter in tools.formatting.formatters:
78
- tables[formatter.table_path] = formatter.to_toml(config)
77
+ tool_configs += tools.formatting.formatters
79
78
 
80
79
  if test := tools.test:
81
- for test_tool in (test.pytest, test.coverage_run, test.coverage_report):
82
- tables[test_tool.table_path] = test_tool.to_toml(config)
80
+ tool_configs += [test.pytest, test.coverage_run, test.coverage_report]
83
81
 
84
- for runner in (tools.doit, tools.nox):
85
- if runner:
86
- tables[runner.table_path] = runner.to_toml(config)
82
+ tool_configs += [t for t in (tools.doit, tools.nox) if t]
87
83
 
88
- if (analysis := tools.analysis) and analysis.mypy:
89
- tables[analysis.mypy.table_path] = analysis.mypy.to_toml(config)
84
+ if analysis := tools.analysis:
85
+ tool_configs += [t for t in (analysis.mypy, analysis.pyright) if t]
90
86
 
91
- return tables
87
+ return {t.table_path: t.to_toml(config) for t in tool_configs}
92
88
 
93
89
 
94
90
  def render(config: ProjectConfig, text: str) -> str:
@@ -347,7 +347,7 @@ class TaskFactory:
347
347
  Create `publish` task which builds and publishes the package via uv.
348
348
  """
349
349
  publish = self._require(self._config.tools.publish, "tools.publish")
350
- out_dir = Path(publish.out_dir)
350
+ out_dir = Path(publish.out_dir or "dist")
351
351
 
352
352
  def _publish():
353
353
  artifacts = [str(p) for p in sorted(out_dir.iterdir())]
@@ -1,69 +0,0 @@
1
- """
2
- Built-in tool configurations.
3
-
4
- Configurations are plain `ToolsConfig` values: define your own factory function or
5
- global in a module shared across projects, optionally building on
6
- `create_kitchen_sink_tools()`:
7
-
8
- ```python
9
- from dataclasses import replace
10
-
11
- from pyprojkit import AnalysisConfig, ToolsConfig, create_kitchen_sink_tools
12
-
13
-
14
- def create_mycompany_tools() -> ToolsConfig:
15
- return replace(create_kitchen_sink_tools(), analysis=AnalysisConfig())
16
- ```
17
- """
18
-
19
- from __future__ import annotations
20
-
21
- from .project import (
22
- AnalysisConfig,
23
- DocConfig,
24
- FormattingConfig,
25
- PublishConfig,
26
- SkillConfig,
27
- TestConfig,
28
- ToolsConfig,
29
- )
30
- from .tools import (
31
- AutoflakeConfig,
32
- BlackConfig,
33
- DocfmtConfig,
34
- DoitConfig,
35
- IsortConfig,
36
- NoxConfig,
37
- TomlSortConfig,
38
- )
39
-
40
- __all__ = [
41
- "create_kitchen_sink_tools",
42
- ]
43
-
44
-
45
- def create_kitchen_sink_tools() -> ToolsConfig:
46
- """
47
- Get a new instance of the configuration which enables every tool: uv as the workflow
48
- tool, formatting, testing, doit, nox, mkinit, static analysis, publishing and the
49
- agent skill. Sphinx is left disabled since it needs project-specific settings.
50
- """
51
- return ToolsConfig(
52
- workflow_tool="uv",
53
- formatting=FormattingConfig(
54
- formatters=(
55
- AutoflakeConfig(),
56
- IsortConfig(),
57
- DocfmtConfig(),
58
- BlackConfig(),
59
- TomlSortConfig(),
60
- )
61
- ),
62
- test=TestConfig(),
63
- doit=DoitConfig(),
64
- nox=NoxConfig(),
65
- doc=DocConfig(),
66
- analysis=AnalysisConfig(),
67
- publish=PublishConfig(),
68
- skill=SkillConfig(),
69
- )
@@ -1,175 +0,0 @@
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
- "DocfmtConfig",
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
- versions = project.pinned_versions
72
- if versions is None:
73
- return {}
74
- return {"target-version": [f"py{maj}{min}" for maj, min in versions.versions]}
75
-
76
- def command(self, py_paths: list[str], toml_paths: list[str]) -> list[str]:
77
- return ["black"] + py_paths
78
-
79
-
80
- @dataclass(kw_only=True)
81
- class DocfmtConfig(BaseFormatterConfig):
82
- table_path: ClassVar[str] = "tool.docfmt"
83
-
84
- # docfmt returns 0 when it writes changes in-place; 1 means "would change"
85
- # in check mode and 2 is an error, so only 0 is expected here
86
- expect_rc: ClassVar[frozenset[int]] = frozenset({0})
87
-
88
- # line length is left unset: docfmt picks it up from `[tool.black]`
89
- in_place: bool | None = field(default=True, metadata={"toml": "in-place"})
90
- recursive: bool | None = True
91
- summary_on_own_line: bool | None = field(
92
- default=True, metadata={"toml": "summary-on-own-line"}
93
- )
94
- force_reflow: bool | None = field(default=True, metadata={"toml": "force-reflow"})
95
-
96
- def command(self, py_paths: list[str], toml_paths: list[str]) -> list[str]:
97
- return ["docfmt"] + py_paths
98
-
99
-
100
- @dataclass(kw_only=True)
101
- class TomlSortConfig(BaseFormatterConfig):
102
- table_path: ClassVar[str] = "tool.tomlsort"
103
-
104
- sort_first: tuple[str, ...] | None = (
105
- "project",
106
- "dependency-groups",
107
- "build-system",
108
- )
109
- sort_table_keys: bool | None = True
110
-
111
- # explicit since the toml-sort CLI and library defaults disagree; must be
112
- # pinned so the sync engine's normalization and the format task agree
113
- spaces_before_inline_comment: int | None = 2
114
-
115
- def command(self, py_paths: list[str], toml_paths: list[str]) -> list[str]:
116
- return ["toml-sort", "-i"] + toml_paths
117
-
118
-
119
- # --- non-formatter tools ---
120
-
121
-
122
- @dataclass(kw_only=True)
123
- class PytestConfig(BaseToolConfig):
124
- table_path: ClassVar[str] = "tool.pytest.ini_options"
125
-
126
- addopts: str | None = "--import-mode=importlib -s -v -rA"
127
- cache_dir: str | None = str(_paths.PYTEST_CACHE_PATH)
128
- testpaths: str | None = "test"
129
-
130
-
131
- @dataclass(kw_only=True)
132
- class CoverageRunConfig(BaseToolConfig):
133
- table_path: ClassVar[str] = "tool.coverage.run"
134
-
135
- data_file: str | None = str(_paths.COVERAGE_DATA_PATH)
136
-
137
-
138
- @dataclass(kw_only=True)
139
- class CoverageReportConfig(BaseToolConfig):
140
- table_path: ClassVar[str] = "tool.coverage.report"
141
-
142
- exclude_lines: tuple[str, ...] | None = (
143
- "if TYPE_CHECKING:",
144
- "\\.\\.\\.$",
145
- )
146
-
147
-
148
- @dataclass(kw_only=True)
149
- class DoitConfig(BaseToolConfig):
150
- table_path: ClassVar[str] = "tool.doit"
151
-
152
- dep_file: str | None = str(_paths.DOIT_DB_PATH)
153
- verbosity: int | None = 2
154
-
155
-
156
- @dataclass(kw_only=True)
157
- class NoxConfig(BaseToolConfig):
158
- table_path: ClassVar[str] = "tool.nox"
159
-
160
- default_venv_backend: str | None = "uv"
161
- envdir: str | None = str(_paths.NOX_ENVDIR_PATH)
162
-
163
-
164
- @dataclass(kw_only=True)
165
- class MypyConfig(BaseToolConfig):
166
- table_path: ClassVar[str] = "tool.mypy"
167
-
168
- ignore_missing_imports: bool | None = True
169
-
170
- def extra_toml(self, project: ProjectConfig) -> dict[str, Any]:
171
- versions = project.pinned_versions
172
- if versions is None:
173
- return {}
174
- maj, min = versions.versions[0]
175
- return {"python_version": f"{maj}.{min}"}
File without changes
File without changes