pyprojkit 0.3.0__tar.gz → 0.4.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.3.0
3
+ Version: 0.4.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>
@@ -32,7 +32,7 @@ Requires-Dist: isort>=6.0.1,<7 ; extra == 'format'
32
32
  Requires-Dist: genbadge[tests,coverage]>=1.1.0,<2 ; extra == 'test'
33
33
  Requires-Dist: pytest>=8.4.2,<9 ; extra == 'test'
34
34
  Requires-Dist: pytest-cov>=7.1.0 ; extra == 'test'
35
- Requires-Python: >=3.12, <3.15
35
+ Requires-Python: >=3.12, <4.0
36
36
  Project-URL: homepage, https://github.com/mm21/pyprojkit
37
37
  Provides-Extra: all
38
38
  Provides-Extra: analysis
@@ -56,14 +56,15 @@ Development workflow toolkit for Python projects
56
56
  - [Getting started](#getting-started)
57
57
  - [Declaring configuration: `pyprojconf.py`](#declaring-configuration-pyprojconfpy)
58
58
  - [Python versions](#python-versions)
59
- - [Tools and profiles](#tools-and-profiles)
59
+ - [Tools](#tools)
60
60
  - [Syncing `pyproject.toml`](#syncing-pyprojecttoml)
61
61
  - [Managed content](#managed-content)
62
+ - [Agent skill](#agent-skill)
62
63
  - [Check mode](#check-mode)
63
64
  - [doit tasks](#doit-tasks)
64
65
  - [nox sessions](#nox-sessions)
65
66
  - [Path conventions](#path-conventions)
66
- - [Custom profiles](#custom-profiles)
67
+ - [Sharing configuration](#sharing-configuration)
67
68
 
68
69
  ## Motivation
69
70
 
@@ -71,7 +72,7 @@ Every Python project accumulates the same development workflow boilerplate: form
71
72
 
72
73
  PyProjKit centralizes all of this. Each project declares its configuration exactly once in a `pyprojconf.py` at the project root; PyProjKit then:
73
74
 
74
- 1. **Syncs `pyproject.toml`**: writes the managed parts (version classifiers, `requires-python`, `[tool.*]` tables) while preserving everything else, with a `--check` mode for CI.
75
+ 1. **Syncs `pyproject.toml`**: writes the managed parts (version classifiers, `requires-python`, individual `[tool.*]` fields marked with `# pyprojkit-managed` comments) while preserving everything else, with a `--check` mode for CI.
75
76
  2. **Provides `doit` task factories**: format, test, badges, publish, and more, pre-wired to sensible conventions.
76
77
  3. **Provides `nox` session factories**: a test session across all supported Python versions, pinned to exact patch releases.
77
78
 
@@ -99,9 +100,13 @@ Then drop three small files into your project root.
99
100
  `pyprojconf.py`:
100
101
 
101
102
  ```python
102
- from pyprojkit import ProjectConfig, PythonVersions
103
+ from pyprojkit import ProjectConfig, PythonVersions, RequiresPython
103
104
 
104
- config = ProjectConfig(package="my_package", python=PythonVersions(3, (12, 14)))
105
+ config = ProjectConfig(
106
+ package="my_package",
107
+ requires_python=RequiresPython((3, 12)),
108
+ pinned_python=PythonVersions(3, (12, 14)),
109
+ )
105
110
  ```
106
111
 
107
112
  `dodo.py`:
@@ -130,44 +135,55 @@ doit badges
130
135
  nox
131
136
  ```
132
137
 
138
+ or run everything at once with `doit preflight`.
139
+
133
140
  ## Declaring configuration: `pyprojconf.py`
134
141
 
135
142
  `pyprojconf.py` must define a module-level `config: ProjectConfig`. `TaskFactory`, `NoxFactory`, and the `pyprojkit` CLI load it automatically from the current directory.
136
143
 
137
144
  ### Python versions
138
145
 
139
- `PythonVersions` takes the major version and an inclusive range of minor versions — only versions you explicitly support (i.e. test against):
146
+ Two separate concepts are configured:
147
+
148
+ - `requires_python` (required) is the minimum and optional exclusive maximum of interpreters the package can be installed on. It's independent of what you test: if the maximum is omitted it's derived from the minimum as the next major version, so downstream dependency resolution stays future-proof.
149
+ - `pinned_python` (optional) is the specific versions you test and target. `PythonVersions` takes the major version and an inclusive range of minor versions; pass a sequence for discontiguous ranges, e.g. `[PythonVersions(3, (8, 9)), PythonVersions(3, (11, 12))]`. Every pinned version must satisfy `requires_python`.
150
+
151
+ To exclude known-broken minor versions, use `RequiresPython((3, 12), exclude=[(3, 13)])`, which adds `!=3.13.*`.
140
152
 
141
153
  ```python
142
- python = PythonVersions(3, (12, 14))
154
+ requires_python = RequiresPython((3, 12)) # requires-python = ">=3.12,<4.0"
155
+ pinned_python = PythonVersions(3, (12, 14))
143
156
  ```
144
157
 
145
- This single declaration drives:
158
+ `requires_python` drives `requires-python`. `pinned_python` drives:
146
159
 
147
- - `requires-python` (`>=3.12,<3.15`)
148
160
  - Trove classifiers (`Programming Language :: Python :: 3.12`, ...)
149
161
  - black's `target-version` (`["py312", "py313", "py314"]`)
150
- - mypy's `python_version` (`3.12`)
162
+ - mypy's `python_version` (`3.12`, the lowest pinned version)
151
163
  - nox interpreter pins (`3.12.13`, `3.13.13`, `3.14.6`)
152
164
 
165
+ If `pinned_python` is omitted, classifiers, black's `target-version` and mypy's `python_version` are left unmanaged, and creating the nox test session raises a `ConfigError`.
166
+
153
167
  The exact patch releases used for nox come from a mapping bundled with PyProjKit, updated with each release — so updating PyProjKit refreshes the pins across all your projects. Individual projects can override:
154
168
 
155
169
  ```python
156
- python = PythonVersions(3, (12, 14), patch_overrides={(3, 14): "3.14.7"})
170
+ pinned_python = PythonVersions(3, (12, 14), patch_overrides={(3, 14): "3.14.7"})
157
171
  ```
158
172
 
159
- ### Tools and profiles
173
+ ### Tools
160
174
 
161
- Tool configurations are grouped by category under `ToolsConfig`, whose defaults constitute the default *profile*:
175
+ Tool configurations are grouped by category under `ToolsConfig`. Every category defaults to `None`, so the default configuration is minimal: no tools are managed and only the `sync` task exists. Set a category to enable it:
162
176
 
163
- - `formatting`: ordered chain of formatters; by default autoflake &rarr; isort &rarr; docfmt &rarr; black &rarr; toml-sort
177
+ - `workflow_tool`: `"uv"` or `"poetry"`, the tool managing the project environment; commands in the agent skill are run through it, e.g. `uv run doit test`
178
+ - `formatting`: ordered chain of formatters, e.g. autoflake &rarr; isort &rarr; docfmt &rarr; black &rarr; toml-sort
164
179
  - `test`: pytest + coverage settings
165
180
  - `doit`, `nox`: cache locations and backends
166
- - `doc`: documentation tools — mkinit (enabled by default) and sphinx (opt-in)
167
- - `analysis`: mypy + pyright (opt-in)
181
+ - `doc`: documentation tools — mkinit and sphinx (each opt-in within)
182
+ - `analysis`: mypy + pyright
168
183
  - `publish`: build/publish output directory
184
+ - `skill`: a `pyprojkit-python` [agent skill](https://agentskills.io) generated from the configuration
169
185
 
170
- Customize with `dataclasses.replace`:
186
+ `create_kitchen_sink_tools()` returns a `ToolsConfig` with everything enabled (except sphinx, which needs project-specific settings). Customize it with `dataclasses.replace`:
171
187
 
172
188
  ```python
173
189
  from dataclasses import replace
@@ -177,30 +193,34 @@ from pyprojkit import (
177
193
  DocConfig,
178
194
  ProjectConfig,
179
195
  PythonVersions,
196
+ RequiresPython,
180
197
  SphinxConfig,
181
- ToolsConfig,
198
+ create_kitchen_sink_tools,
182
199
  )
183
200
 
184
201
  config = ProjectConfig(
185
202
  package="my_package",
186
- python=PythonVersions(3, (12, 14)),
203
+ requires_python=RequiresPython((3, 12)),
204
+ pinned_python=PythonVersions(3, (12, 14)),
187
205
  tools=replace(
188
- ToolsConfig.default(),
206
+ create_kitchen_sink_tools(),
189
207
  doc=DocConfig(sphinx=SphinxConfig(copy_env_var="MY_PACKAGE_DOCS_DIR")),
190
- analysis=AnalysisConfig(),
208
+ analysis=None,
191
209
  ),
192
210
  )
193
211
  ```
194
212
 
195
- For one-off tweaks to managed tables there's an escape hatch, merged last into the synced output:
213
+ To tweak a managed value, replace it in the tool's config:
196
214
 
197
215
  ```python
198
216
  tools=replace(
199
- ToolsConfig.default(),
200
- tool_overrides={"tool.pytest.ini_options": {"addopts": "-x"}},
217
+ create_kitchen_sink_tools(),
218
+ test=TestConfig(pytest=PytestConfig(addopts="-x")),
201
219
  )
202
220
  ```
203
221
 
222
+ Anything PyProjKit doesn't manage — extra keys in managed tables, whole unmanaged tables — is simply edited directly in `pyproject.toml` and left untouched by the sync.
223
+
204
224
  ## Syncing `pyproject.toml`
205
225
 
206
226
  Formatters and other tools read their settings from `pyproject.toml`, so PyProjKit writes them there — `pyproject.toml` remains the single source of truth for the tools themselves, while `pyprojconf.py` is the single source of truth for you.
@@ -209,12 +229,28 @@ Formatters and other tools read their settings from `pyproject.toml`, so PyProjK
209
229
 
210
230
  The sync engine owns:
211
231
 
232
+ - A header comment at the top of `pyproject.toml` noting the file is managed (in part) by PyProjKit
212
233
  - `project.requires-python`
213
234
  - Python version classifiers (other classifiers are untouched)
214
- - One `[tool.X]` table per enabled tool (fully owned; hand edits inside are overwritten)
215
- - `[tool.pyprojkit].managed`: bookkeeping list of owned tables, so a tool dropped from your configuration gets its table cleanly removed on the next sync
235
+ - Individual key/value pairs within tool tables, each marked with an inline `# pyprojkit-managed` comment
236
+
237
+ The marker comment doubles as bookkeeping: a marked field dropped from your configuration is deleted on the next sync, and a table emptied that way is pruned. (The marker is reserved — don't put it on your own fields.)
238
+
239
+ Everything else — dependencies, `[tool.uv.sources]`, build system, unmanaged fields inside managed tables, whole unmanaged tables, comments — is preserved. Output is normalized with toml-sort using the same settings as the managed `[tool.tomlsort]` table, so syncing and formatting never fight.
240
+
241
+ Files synced by PyProjKit < 0.4 (whole-table ownership with a `[tool.pyprojkit]` bookkeeping table) are migrated automatically in a single sync.
242
+
243
+ ### Agent skill
216
244
 
217
- Everything else — dependencies, `[tool.uv.sources]`, build system, unmanaged tool tables — is preserved. Output is normalized with toml-sort using the same settings as the managed `[tool.tomlsort]` table, so syncing and formatting never fight.
245
+ PyProjKit generates a `pyprojkit-python` skill in the [Agent Skills](https://agentskills.io) format, usable by any agent which supports it (Claude Code, Codex, Cursor, etc). Its content is derived from your configuration: the Python versions to target, conventions such as placing internal (`_`-prefixed) symbols after public ones of the same kind, and, when `doit` is enabled, a checklist of the workflow tasks you've enabled to run before finishing a task — `doit init` with mkinit, `doit format` with formatting, `doit analysis` with analysis, `doit test` with testing Commands are prefixed with `<workflow_tool> run` when a workflow tool is configured.
246
+
247
+ ```python
248
+ tools=replace(ToolsConfig(), skill=SkillConfig())
249
+ ```
250
+
251
+ The skill is written to `.claude/skills/pyprojkit-python/SKILL.md` by default; set `SkillConfig.skills_dir` to write it elsewhere, e.g. `skills_dir=".agents/skills"` for agents reading the cross-agent location.
252
+
253
+ The generated skill file is overwritten on every sync (so it tracks both your configuration and PyProjKit updates) and removed when dropped from the configuration; disable entirely with `skill=None`. The `pyprojkit-` prefix namespaces the skill, so it never collides with your own skills.
218
254
 
219
255
  ### Check mode
220
256
 
@@ -224,7 +260,7 @@ Verify a project is in sync without writing (e.g. in CI):
224
260
  pyprojkit sync --check
225
261
  ```
226
262
 
227
- Prints a diff and exits nonzero if `pyproject.toml` is out of date.
263
+ Prints a diff and exits nonzero if `pyproject.toml` or the generated skill is out of date.
228
264
 
229
265
  ## doit tasks
230
266
 
@@ -251,11 +287,22 @@ or all at once: `globals().update(factory.create_all_tasks())`, which creates ev
251
287
  | `test` | Run pytest with coverage (HTML + XML) and JUnit output |
252
288
  | `badges` | Generate test/coverage badges via genbadge |
253
289
  | `publish` | Build and publish via uv (output dir is cleaned first, so stale artifacts are never published) |
254
- | `init` | Generate `__init__.py` files via mkinit (enabled by default) |
290
+ | `init` | Generate `__init__.py` files via mkinit |
255
291
  | `doc` | Build documentation via sphinx; `--copy` deploys to a directory given by an env var (opt-in) |
256
- | `analysis` | Run mypy (with HTML/cobertura reports) and pyright (opt-in) |
292
+ | `analysis` | Run mypy (with HTML/cobertura reports) and pyright (opt-in) on the configured `code_paths` |
293
+ | `preflight` | Run every enabled step (see below) |
294
+
295
+ The `preflight` task runs every enabled step in order — sync, init, format, analysis, test, badges, nox — continuing past failures, then fails once listing the failed steps. It's meant to be used by developers and CI alike:
257
296
 
258
- The `format` task passes an explicit list of paths to each formatter — root `*.py`/`*.toml` files plus the configured `format_paths` directories that exist (`src`, `test`, `doc`, `examples` by default) — so formatters never wander into `.venv` or other unrelated trees.
297
+ ```bash
298
+ doit preflight # edit files in place (sync, init, format, badges)
299
+ doit preflight --no-test # skip test, badges and nox
300
+ doit preflight --check # fail if any file would be edited, printing diffs
301
+ ```
302
+
303
+ In check mode, the editing steps run on a temporary copy of the project, which is then compared with the original, so the check is exactly "would an edit run change anything" — including interactions between tools, like formatters rewriting files generated by mkinit. Badges are generated from the test results and compared too, so committed badges must be up to date. Check mode is the default when the `CI` environment variable is set, as it is by most CI providers.
304
+
305
+ The `format` task passes an explicit list of paths to each formatter — root `*.py`/`*.toml` files plus the configured `code_paths` directories that exist (`src`, `test`, `doc`, `examples` by default) — so formatters never wander into `.venv` or other unrelated trees.
259
306
 
260
307
  ## nox sessions
261
308
 
@@ -277,34 +324,34 @@ All tasks share the same layout:
277
324
  - `__cache__/`: caches (doit db, pytest cache, coverage data, nox envs)
278
325
  - `__out__/`: generated artifacts (`test/` coverage + JUnit results, `doc/html`, `analysis/`, `uv/` build artifacts)
279
326
  - `badges/`: generated badge SVGs
280
- - `src/<package>`: package sources consumed by the `init` and `analysis` tasks; the containing directory is configurable via `ProjectConfig.packages_dir` (default `"src"`)
327
+ - `src/<package>`: package sources consumed by the `init` task; the containing directory is configurable via `ProjectConfig.packages_dir` (default `"src"`)
281
328
 
282
- ## Custom profiles
329
+ ## Sharing configuration
283
330
 
284
- A profile is a named factory for a pre-canned configuration, at two levels: the overall tools profile and the formatting profile it contains. The `"default"` profiles ship with PyProjKit; register your own once and reuse it across projects:
331
+ There is no profile registry: a shared configuration is just a function or global in a module you control, which each project imports.
285
332
 
286
333
  ```python
287
- # in your shared module, e.g. mycompany_profiles.py
334
+ # in your shared module, e.g. mycompany_config.py
288
335
  from dataclasses import replace
289
336
 
290
- from pyprojkit import AnalysisConfig, ToolsConfig, register_tools_profile
337
+ from pyprojkit import AnalysisConfig, ToolsConfig, create_kitchen_sink_tools
291
338
 
292
- register_tools_profile(
293
- "mycompany",
294
- lambda: replace(ToolsConfig.default(), analysis=AnalysisConfig()),
295
- )
339
+
340
+ def create_mycompany_tools() -> ToolsConfig:
341
+ return replace(create_kitchen_sink_tools(), analysis=AnalysisConfig())
296
342
  ```
297
343
 
298
344
  ```python
299
345
  # in each project's pyprojconf.py
300
- import mycompany_profiles # noqa: F401 -- registers the profile
346
+ from mycompany_config import create_mycompany_tools
301
347
 
302
- from pyprojkit import ProjectConfig, PythonVersions, get_tools_profile
348
+ from pyprojkit import ProjectConfig, PythonVersions, RequiresPython
303
349
 
304
350
  config = ProjectConfig(
305
351
  package="my_package",
306
- python=PythonVersions(3, (12, 14)),
307
- tools=get_tools_profile("mycompany"),
352
+ requires_python=RequiresPython((3, 12)),
353
+ pinned_python=PythonVersions(3, (12, 14)),
354
+ tools=create_mycompany_tools(),
308
355
  )
309
356
  ```
310
357
 
@@ -0,0 +1,315 @@
1
+ # PyProjKit
2
+
3
+ Development workflow toolkit for Python projects
4
+
5
+ [![Python versions](https://img.shields.io/pypi/pyversions/pyprojkit.svg)](https://pypi.org/project/pyprojkit)
6
+ [![PyPI](https://img.shields.io/pypi/v/pyprojkit?color=%2334D058&label=pypi%20package)](https://pypi.org/project/pyprojkit)
7
+ [![Tests](./badges/tests.svg?dummy=8484744)]()
8
+ [![Coverage](./badges/cov.svg?dummy=8484744)]()
9
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
10
+
11
+ - [PyProjKit](#pyprojkit)
12
+ - [Motivation](#motivation)
13
+ - [Getting started](#getting-started)
14
+ - [Declaring configuration: `pyprojconf.py`](#declaring-configuration-pyprojconfpy)
15
+ - [Python versions](#python-versions)
16
+ - [Tools](#tools)
17
+ - [Syncing `pyproject.toml`](#syncing-pyprojecttoml)
18
+ - [Managed content](#managed-content)
19
+ - [Agent skill](#agent-skill)
20
+ - [Check mode](#check-mode)
21
+ - [doit tasks](#doit-tasks)
22
+ - [nox sessions](#nox-sessions)
23
+ - [Path conventions](#path-conventions)
24
+ - [Sharing configuration](#sharing-configuration)
25
+
26
+ ## Motivation
27
+
28
+ Every Python project accumulates the same development workflow boilerplate: formatter settings in `pyproject.toml`, supported Python versions repeated across classifiers, `requires-python`, black's `target-version`, and the testing matrix, plus a `dodo.py` and `noxfile.py` that look nearly identical from project to project. Keeping all of it consistent — and keeping it up to date as tools and Python versions evolve — is tedious and error-prone.
29
+
30
+ PyProjKit centralizes all of this. Each project declares its configuration exactly once in a `pyprojconf.py` at the project root; PyProjKit then:
31
+
32
+ 1. **Syncs `pyproject.toml`**: writes the managed parts (version classifiers, `requires-python`, individual `[tool.*]` fields marked with `# pyprojkit-managed` comments) while preserving everything else, with a `--check` mode for CI.
33
+ 2. **Provides `doit` task factories**: format, test, badges, publish, and more, pre-wired to sensible conventions.
34
+ 3. **Provides `nox` session factories**: a test session across all supported Python versions, pinned to exact patch releases.
35
+
36
+ Supported Python versions are declared once and drive everything: classifiers, `requires-python`, black's `target-version`, mypy's `python_version`, and the nox interpreter matrix.
37
+
38
+ ## Getting started
39
+
40
+ Install from PyPI:
41
+
42
+ ```bash
43
+ pip install pyprojkit
44
+ ```
45
+
46
+ Or add it to your project's dev dependencies (formatter, test, and analysis tools are available as extras):
47
+
48
+ ```toml
49
+ [dependency-groups]
50
+ dev = [
51
+ "pyprojkit[all]"
52
+ ]
53
+ ```
54
+
55
+ Then drop three small files into your project root.
56
+
57
+ `pyprojconf.py`:
58
+
59
+ ```python
60
+ from pyprojkit import ProjectConfig, PythonVersions, RequiresPython
61
+
62
+ config = ProjectConfig(
63
+ package="my_package",
64
+ requires_python=RequiresPython((3, 12)),
65
+ pinned_python=PythonVersions(3, (12, 14)),
66
+ )
67
+ ```
68
+
69
+ `dodo.py`:
70
+
71
+ ```python
72
+ from pyprojkit import TaskFactory
73
+
74
+ globals().update(TaskFactory().create_all_tasks())
75
+ ```
76
+
77
+ `noxfile.py`:
78
+
79
+ ```python
80
+ from pyprojkit import NoxFactory
81
+
82
+ test = NoxFactory().create_test_session()
83
+ ```
84
+
85
+ Then sync your `pyproject.toml` and run your workflow:
86
+
87
+ ```bash
88
+ doit sync # or: pyprojkit sync
89
+ doit format
90
+ doit test
91
+ doit badges
92
+ nox
93
+ ```
94
+
95
+ or run everything at once with `doit preflight`.
96
+
97
+ ## Declaring configuration: `pyprojconf.py`
98
+
99
+ `pyprojconf.py` must define a module-level `config: ProjectConfig`. `TaskFactory`, `NoxFactory`, and the `pyprojkit` CLI load it automatically from the current directory.
100
+
101
+ ### Python versions
102
+
103
+ Two separate concepts are configured:
104
+
105
+ - `requires_python` (required) is the minimum and optional exclusive maximum of interpreters the package can be installed on. It's independent of what you test: if the maximum is omitted it's derived from the minimum as the next major version, so downstream dependency resolution stays future-proof.
106
+ - `pinned_python` (optional) is the specific versions you test and target. `PythonVersions` takes the major version and an inclusive range of minor versions; pass a sequence for discontiguous ranges, e.g. `[PythonVersions(3, (8, 9)), PythonVersions(3, (11, 12))]`. Every pinned version must satisfy `requires_python`.
107
+
108
+ To exclude known-broken minor versions, use `RequiresPython((3, 12), exclude=[(3, 13)])`, which adds `!=3.13.*`.
109
+
110
+ ```python
111
+ requires_python = RequiresPython((3, 12)) # requires-python = ">=3.12,<4.0"
112
+ pinned_python = PythonVersions(3, (12, 14))
113
+ ```
114
+
115
+ `requires_python` drives `requires-python`. `pinned_python` drives:
116
+
117
+ - Trove classifiers (`Programming Language :: Python :: 3.12`, ...)
118
+ - black's `target-version` (`["py312", "py313", "py314"]`)
119
+ - mypy's `python_version` (`3.12`, the lowest pinned version)
120
+ - nox interpreter pins (`3.12.13`, `3.13.13`, `3.14.6`)
121
+
122
+ If `pinned_python` is omitted, classifiers, black's `target-version` and mypy's `python_version` are left unmanaged, and creating the nox test session raises a `ConfigError`.
123
+
124
+ The exact patch releases used for nox come from a mapping bundled with PyProjKit, updated with each release — so updating PyProjKit refreshes the pins across all your projects. Individual projects can override:
125
+
126
+ ```python
127
+ pinned_python = PythonVersions(3, (12, 14), patch_overrides={(3, 14): "3.14.7"})
128
+ ```
129
+
130
+ ### Tools
131
+
132
+ Tool configurations are grouped by category under `ToolsConfig`. Every category defaults to `None`, so the default configuration is minimal: no tools are managed and only the `sync` task exists. Set a category to enable it:
133
+
134
+ - `workflow_tool`: `"uv"` or `"poetry"`, the tool managing the project environment; commands in the agent skill are run through it, e.g. `uv run doit test`
135
+ - `formatting`: ordered chain of formatters, e.g. autoflake &rarr; isort &rarr; docfmt &rarr; black &rarr; toml-sort
136
+ - `test`: pytest + coverage settings
137
+ - `doit`, `nox`: cache locations and backends
138
+ - `doc`: documentation tools — mkinit and sphinx (each opt-in within)
139
+ - `analysis`: mypy + pyright
140
+ - `publish`: build/publish output directory
141
+ - `skill`: a `pyprojkit-python` [agent skill](https://agentskills.io) generated from the configuration
142
+
143
+ `create_kitchen_sink_tools()` returns a `ToolsConfig` with everything enabled (except sphinx, which needs project-specific settings). Customize it with `dataclasses.replace`:
144
+
145
+ ```python
146
+ from dataclasses import replace
147
+
148
+ from pyprojkit import (
149
+ AnalysisConfig,
150
+ DocConfig,
151
+ ProjectConfig,
152
+ PythonVersions,
153
+ RequiresPython,
154
+ SphinxConfig,
155
+ create_kitchen_sink_tools,
156
+ )
157
+
158
+ config = ProjectConfig(
159
+ package="my_package",
160
+ requires_python=RequiresPython((3, 12)),
161
+ pinned_python=PythonVersions(3, (12, 14)),
162
+ tools=replace(
163
+ create_kitchen_sink_tools(),
164
+ doc=DocConfig(sphinx=SphinxConfig(copy_env_var="MY_PACKAGE_DOCS_DIR")),
165
+ analysis=None,
166
+ ),
167
+ )
168
+ ```
169
+
170
+ To tweak a managed value, replace it in the tool's config:
171
+
172
+ ```python
173
+ tools=replace(
174
+ create_kitchen_sink_tools(),
175
+ test=TestConfig(pytest=PytestConfig(addopts="-x")),
176
+ )
177
+ ```
178
+
179
+ Anything PyProjKit doesn't manage — extra keys in managed tables, whole unmanaged tables — is simply edited directly in `pyproject.toml` and left untouched by the sync.
180
+
181
+ ## Syncing `pyproject.toml`
182
+
183
+ Formatters and other tools read their settings from `pyproject.toml`, so PyProjKit writes them there — `pyproject.toml` remains the single source of truth for the tools themselves, while `pyprojconf.py` is the single source of truth for you.
184
+
185
+ ### Managed content
186
+
187
+ The sync engine owns:
188
+
189
+ - A header comment at the top of `pyproject.toml` noting the file is managed (in part) by PyProjKit
190
+ - `project.requires-python`
191
+ - Python version classifiers (other classifiers are untouched)
192
+ - Individual key/value pairs within tool tables, each marked with an inline `# pyprojkit-managed` comment
193
+
194
+ The marker comment doubles as bookkeeping: a marked field dropped from your configuration is deleted on the next sync, and a table emptied that way is pruned. (The marker is reserved — don't put it on your own fields.)
195
+
196
+ Everything else — dependencies, `[tool.uv.sources]`, build system, unmanaged fields inside managed tables, whole unmanaged tables, comments — is preserved. Output is normalized with toml-sort using the same settings as the managed `[tool.tomlsort]` table, so syncing and formatting never fight.
197
+
198
+ Files synced by PyProjKit < 0.4 (whole-table ownership with a `[tool.pyprojkit]` bookkeeping table) are migrated automatically in a single sync.
199
+
200
+ ### Agent skill
201
+
202
+ PyProjKit generates a `pyprojkit-python` skill in the [Agent Skills](https://agentskills.io) format, usable by any agent which supports it (Claude Code, Codex, Cursor, etc). Its content is derived from your configuration: the Python versions to target, conventions such as placing internal (`_`-prefixed) symbols after public ones of the same kind, and, when `doit` is enabled, a checklist of the workflow tasks you've enabled to run before finishing a task — `doit init` with mkinit, `doit format` with formatting, `doit analysis` with analysis, `doit test` with testing Commands are prefixed with `<workflow_tool> run` when a workflow tool is configured.
203
+
204
+ ```python
205
+ tools=replace(ToolsConfig(), skill=SkillConfig())
206
+ ```
207
+
208
+ The skill is written to `.claude/skills/pyprojkit-python/SKILL.md` by default; set `SkillConfig.skills_dir` to write it elsewhere, e.g. `skills_dir=".agents/skills"` for agents reading the cross-agent location.
209
+
210
+ The generated skill file is overwritten on every sync (so it tracks both your configuration and PyProjKit updates) and removed when dropped from the configuration; disable entirely with `skill=None`. The `pyprojkit-` prefix namespaces the skill, so it never collides with your own skills.
211
+
212
+ ### Check mode
213
+
214
+ Verify a project is in sync without writing (e.g. in CI):
215
+
216
+ ```bash
217
+ pyprojkit sync --check
218
+ ```
219
+
220
+ Prints a diff and exits nonzero if `pyproject.toml` or the generated skill is out of date.
221
+
222
+ ## doit tasks
223
+
224
+ `TaskFactory` creates `doit` tasks pre-wired to the configuration. Create them selectively:
225
+
226
+ ```python
227
+ from pyprojkit import TaskFactory
228
+
229
+ factory = TaskFactory()
230
+
231
+ task_sync = factory.create_sync_task()
232
+ task_format = factory.create_format_task()
233
+ task_test = factory.create_test_task()
234
+ task_badges = factory.create_badges_task()
235
+ task_publish = factory.create_publish_task()
236
+ ```
237
+
238
+ or all at once: `globals().update(factory.create_all_tasks())`, which creates every task enabled by the configuration.
239
+
240
+ | Task | Description |
241
+ | --- | --- |
242
+ | `sync` | Update `pyproject.toml` from `pyprojconf.py` |
243
+ | `format` | Run the configured formatter chain over the project |
244
+ | `test` | Run pytest with coverage (HTML + XML) and JUnit output |
245
+ | `badges` | Generate test/coverage badges via genbadge |
246
+ | `publish` | Build and publish via uv (output dir is cleaned first, so stale artifacts are never published) |
247
+ | `init` | Generate `__init__.py` files via mkinit |
248
+ | `doc` | Build documentation via sphinx; `--copy` deploys to a directory given by an env var (opt-in) |
249
+ | `analysis` | Run mypy (with HTML/cobertura reports) and pyright (opt-in) on the configured `code_paths` |
250
+ | `preflight` | Run every enabled step (see below) |
251
+
252
+ The `preflight` task runs every enabled step in order — sync, init, format, analysis, test, badges, nox — continuing past failures, then fails once listing the failed steps. It's meant to be used by developers and CI alike:
253
+
254
+ ```bash
255
+ doit preflight # edit files in place (sync, init, format, badges)
256
+ doit preflight --no-test # skip test, badges and nox
257
+ doit preflight --check # fail if any file would be edited, printing diffs
258
+ ```
259
+
260
+ In check mode, the editing steps run on a temporary copy of the project, which is then compared with the original, so the check is exactly "would an edit run change anything" — including interactions between tools, like formatters rewriting files generated by mkinit. Badges are generated from the test results and compared too, so committed badges must be up to date. Check mode is the default when the `CI` environment variable is set, as it is by most CI providers.
261
+
262
+ The `format` task passes an explicit list of paths to each formatter — root `*.py`/`*.toml` files plus the configured `code_paths` directories that exist (`src`, `test`, `doc`, `examples` by default) — so formatters never wander into `.venv` or other unrelated trees.
263
+
264
+ ## nox sessions
265
+
266
+ `NoxFactory.create_test_session()` registers a `test` session which, for each pinned interpreter, syncs the project's dev dependency group via uv into the session environment and runs pytest:
267
+
268
+ ```bash
269
+ $ nox -l
270
+ * test-3.12.13
271
+ * test-3.13.13
272
+ * test-3.14.6
273
+ ```
274
+
275
+ Extra arguments pass through to pytest: `nox -s test-3.12.13 -- -k my_test`.
276
+
277
+ ## Path conventions
278
+
279
+ All tasks share the same layout:
280
+
281
+ - `__cache__/`: caches (doit db, pytest cache, coverage data, nox envs)
282
+ - `__out__/`: generated artifacts (`test/` coverage + JUnit results, `doc/html`, `analysis/`, `uv/` build artifacts)
283
+ - `badges/`: generated badge SVGs
284
+ - `src/<package>`: package sources consumed by the `init` task; the containing directory is configurable via `ProjectConfig.packages_dir` (default `"src"`)
285
+
286
+ ## Sharing configuration
287
+
288
+ There is no profile registry: a shared configuration is just a function or global in a module you control, which each project imports.
289
+
290
+ ```python
291
+ # in your shared module, e.g. mycompany_config.py
292
+ from dataclasses import replace
293
+
294
+ from pyprojkit import AnalysisConfig, ToolsConfig, create_kitchen_sink_tools
295
+
296
+
297
+ def create_mycompany_tools() -> ToolsConfig:
298
+ return replace(create_kitchen_sink_tools(), analysis=AnalysisConfig())
299
+ ```
300
+
301
+ ```python
302
+ # in each project's pyprojconf.py
303
+ from mycompany_config import create_mycompany_tools
304
+
305
+ from pyprojkit import ProjectConfig, PythonVersions, RequiresPython
306
+
307
+ config = ProjectConfig(
308
+ package="my_package",
309
+ requires_python=RequiresPython((3, 12)),
310
+ pinned_python=PythonVersions(3, (12, 14)),
311
+ tools=create_mycompany_tools(),
312
+ )
313
+ ```
314
+
315
+ New formatters are equally pluggable: subclass `BaseFormatterConfig` (declaring the tool's `[tool.X]` table and command line) and include it in a `FormattingConfig` — the sync engine and format task pick it up automatically.