pkg-ext 0.1.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.
Files changed (91) hide show
  1. pkg_ext-0.1.0/.gitignore +44 -0
  2. pkg_ext-0.1.0/LICENSE +21 -0
  3. pkg_ext-0.1.0/PKG-INFO +439 -0
  4. pkg_ext-0.1.0/README.md +421 -0
  5. pkg_ext-0.1.0/pkg_ext/__init__.py +20 -0
  6. pkg_ext-0.1.0/pkg_ext/_internal/__init__.py +1 -0
  7. pkg_ext-0.1.0/pkg_ext/_internal/__main__.py +4 -0
  8. pkg_ext-0.1.0/pkg_ext/_internal/api_diff.py +449 -0
  9. pkg_ext-0.1.0/pkg_ext/_internal/api_dumper.py +218 -0
  10. pkg_ext-0.1.0/pkg_ext/_internal/changelog/__init__.py +59 -0
  11. pkg_ext-0.1.0/pkg_ext/_internal/changelog/actions.py +432 -0
  12. pkg_ext-0.1.0/pkg_ext/_internal/changelog/committer.py +147 -0
  13. pkg_ext-0.1.0/pkg_ext/_internal/changelog/parser.py +27 -0
  14. pkg_ext-0.1.0/pkg_ext/_internal/changelog/write_changelog_md.py +195 -0
  15. pkg_ext-0.1.0/pkg_ext/_internal/cli/__init__.py +13 -0
  16. pkg_ext-0.1.0/pkg_ext/_internal/cli/api_cmds.py +65 -0
  17. pkg_ext-0.1.0/pkg_ext/_internal/cli/base_commands.py +123 -0
  18. pkg_ext-0.1.0/pkg_ext/_internal/cli/changelog_cmds.py +134 -0
  19. pkg_ext-0.1.0/pkg_ext/_internal/cli/gen_cmds.py +51 -0
  20. pkg_ext-0.1.0/pkg_ext/_internal/cli/options.py +52 -0
  21. pkg_ext-0.1.0/pkg_ext/_internal/cli/stability.py +127 -0
  22. pkg_ext-0.1.0/pkg_ext/_internal/cli/stability_cmds.py +120 -0
  23. pkg_ext-0.1.0/pkg_ext/_internal/cli/workflow_cmds.py +340 -0
  24. pkg_ext-0.1.0/pkg_ext/_internal/cli/workflows.py +288 -0
  25. pkg_ext-0.1.0/pkg_ext/_internal/cli.py +14 -0
  26. pkg_ext-0.1.0/pkg_ext/_internal/config.py +240 -0
  27. pkg_ext-0.1.0/pkg_ext/_internal/context.py +118 -0
  28. pkg_ext-0.1.0/pkg_ext/_internal/errors.py +49 -0
  29. pkg_ext-0.1.0/pkg_ext/_internal/file_parser.py +199 -0
  30. pkg_ext-0.1.0/pkg_ext/_internal/generation/__init__.py +16 -0
  31. pkg_ext-0.1.0/pkg_ext/_internal/generation/docs.py +226 -0
  32. pkg_ext-0.1.0/pkg_ext/_internal/generation/docs_constants.py +7 -0
  33. pkg_ext-0.1.0/pkg_ext/_internal/generation/docs_mkdocs.py +174 -0
  34. pkg_ext-0.1.0/pkg_ext/_internal/generation/docs_render.py +473 -0
  35. pkg_ext-0.1.0/pkg_ext/_internal/generation/docs_version.py +132 -0
  36. pkg_ext-0.1.0/pkg_ext/_internal/generation/example_gen.py +183 -0
  37. pkg_ext-0.1.0/pkg_ext/_internal/generation/groups.py +69 -0
  38. pkg_ext-0.1.0/pkg_ext/_internal/generation/init_file.py +55 -0
  39. pkg_ext-0.1.0/pkg_ext/_internal/generation/pyproject.py +15 -0
  40. pkg_ext-0.1.0/pkg_ext/_internal/generation/test_gen.py +103 -0
  41. pkg_ext-0.1.0/pkg_ext/_internal/git_usage/__init__.py +28 -0
  42. pkg_ext-0.1.0/pkg_ext/_internal/git_usage/actions.py +36 -0
  43. pkg_ext-0.1.0/pkg_ext/_internal/git_usage/state.py +280 -0
  44. pkg_ext-0.1.0/pkg_ext/_internal/git_usage/url.py +44 -0
  45. pkg_ext-0.1.0/pkg_ext/_internal/interactive.py +185 -0
  46. pkg_ext-0.1.0/pkg_ext/_internal/models/__init__.py +78 -0
  47. pkg_ext-0.1.0/pkg_ext/_internal/models/api_dump.py +138 -0
  48. pkg_ext-0.1.0/pkg_ext/_internal/models/code_state.py +96 -0
  49. pkg_ext-0.1.0/pkg_ext/_internal/models/groups.py +176 -0
  50. pkg_ext-0.1.0/pkg_ext/_internal/models/py_files.py +108 -0
  51. pkg_ext-0.1.0/pkg_ext/_internal/models/py_symbols.py +90 -0
  52. pkg_ext-0.1.0/pkg_ext/_internal/models/ref_state.py +47 -0
  53. pkg_ext-0.1.0/pkg_ext/_internal/models/types.py +57 -0
  54. pkg_ext-0.1.0/pkg_ext/_internal/pkg_state.py +229 -0
  55. pkg_ext-0.1.0/pkg_ext/_internal/py_format.py +66 -0
  56. pkg_ext-0.1.0/pkg_ext/_internal/reference_handling/__init__.py +9 -0
  57. pkg_ext-0.1.0/pkg_ext/_internal/reference_handling/added.py +219 -0
  58. pkg_ext-0.1.0/pkg_ext/_internal/reference_handling/promote.py +150 -0
  59. pkg_ext-0.1.0/pkg_ext/_internal/reference_handling/removed.py +64 -0
  60. pkg_ext-0.1.0/pkg_ext/_internal/settings.py +190 -0
  61. pkg_ext-0.1.0/pkg_ext/_internal/signature_parser.py +402 -0
  62. pkg_ext-0.1.0/pkg_ext/_internal/testdata/e2e/01_initial/my_pkg/__init__.py +10 -0
  63. pkg_ext-0.1.0/pkg_ext/_internal/testdata/e2e/01_initial/my_pkg/_internal.py +13 -0
  64. pkg_ext-0.1.0/pkg_ext/_internal/testdata/e2e/01_initial/my_pkg/_internal2.py +6 -0
  65. pkg_ext-0.1.0/pkg_ext/_internal/testdata/e2e/01_initial/my_pkg/my_dep.py +4 -0
  66. pkg_ext-0.1.0/pkg_ext/_internal/testdata/e2e/01_initial/my_pkg/my_group.py +6 -0
  67. pkg_ext-0.1.0/pkg_ext/_internal/testdata/e2e/02_dep_order/my_pkg/__init__.py +8 -0
  68. pkg_ext-0.1.0/pkg_ext/_internal/testdata/e2e/02_dep_order/my_pkg/a.py +5 -0
  69. pkg_ext-0.1.0/pkg_ext/_internal/testdata/e2e/02_dep_order/my_pkg/b.py +5 -0
  70. pkg_ext-0.1.0/pkg_ext/_internal/testdata/e2e/02_dep_order/my_pkg/c.py +2 -0
  71. pkg_ext-0.1.0/pkg_ext/_internal/testdata/e2e/02_dep_order/my_pkg/g1.py +8 -0
  72. pkg_ext-0.1.0/pkg_ext/_internal/testdata/e2e/03_nested/my_pkg/__init__.py +8 -0
  73. pkg_ext-0.1.0/pkg_ext/_internal/testdata/e2e/03_nested/my_pkg/_internal/__init__.py +0 -0
  74. pkg_ext-0.1.0/pkg_ext/_internal/testdata/e2e/03_nested/my_pkg/_internal/a.py +6 -0
  75. pkg_ext-0.1.0/pkg_ext/_internal/testdata/e2e/03_nested/my_pkg/n1.py +6 -0
  76. pkg_ext-0.1.0/pkg_ext/_internal/testdata/e2e/04_git_fix/my_pkg/__init__.py +8 -0
  77. pkg_ext-0.1.0/pkg_ext/_internal/testdata/e2e/04_git_fix/my_pkg/chosen.py +2 -0
  78. pkg_ext-0.1.0/pkg_ext/_internal/testdata/e2e/04_git_fix/my_pkg/git_inferred.py +4 -0
  79. pkg_ext-0.1.0/pkg_ext/_internal/testdata/e2e/04_git_fix/my_pkg/inferred.py +2 -0
  80. pkg_ext-0.1.0/pkg_ext/_internal/version_bump.py +173 -0
  81. pkg_ext-0.1.0/pkg_ext/_internal/warnings.py +153 -0
  82. pkg_ext-0.1.0/pkg_ext/_internal/warnings_gen.py +199 -0
  83. pkg_ext-0.1.0/pkg_ext/api_commands.py +6 -0
  84. pkg_ext-0.1.0/pkg_ext/api_dump.py +30 -0
  85. pkg_ext-0.1.0/pkg_ext/changelog.py +8 -0
  86. pkg_ext-0.1.0/pkg_ext/cli.py +5 -0
  87. pkg_ext-0.1.0/pkg_ext/generate.py +8 -0
  88. pkg_ext-0.1.0/pkg_ext/py.typed +0 -0
  89. pkg_ext-0.1.0/pkg_ext/stability.py +8 -0
  90. pkg_ext-0.1.0/pkg_ext/workflows.py +8 -0
  91. pkg_ext-0.1.0/pyproject.toml +96 -0
@@ -0,0 +1,44 @@
1
+ # path-sync copy -n python-template
2
+
3
+ # === DO_NOT_EDIT: path-sync gitignore ===
4
+ # Python bytecode
5
+ __pycache__/
6
+ *.pyc
7
+ *.pyo
8
+
9
+ # Virtual environments
10
+ .venv/
11
+ venv/
12
+
13
+ # Build artifacts
14
+ dist/
15
+ build/
16
+ *.egg-info/
17
+
18
+ # Coverage
19
+ .coverage
20
+ htmlcov/
21
+ coverage.xml
22
+
23
+ # Cache directories
24
+ .ruff_cache/
25
+ .pytest_cache/
26
+ .mypy_cache/
27
+
28
+ # IDE
29
+ .idea/
30
+ *.iml
31
+ .vscode/
32
+
33
+ # pkg-ext dev mode files
34
+ .groups-dev.yaml
35
+ CHANGELOG-dev.md
36
+ *.api-dev.yaml
37
+
38
+
39
+ # MkDocs build output
40
+ site/
41
+ # === OK_EDIT: path-sync gitignore ===
42
+ # Include dev mode files in testdata for e2e regression tests
43
+ !pkg_ext/_internal/testdata/e2e/**/.groups-dev.yaml
44
+ !pkg_ext/_internal/testdata/e2e/**/CHANGELOG-dev.md
pkg_ext-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Espen Albert
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
pkg_ext-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,439 @@
1
+ Metadata-Version: 2.4
2
+ Name: pkg-ext
3
+ Version: 0.1.0
4
+ Summary: Python package public API management, versioning, and changelog generation
5
+ Author-email: EspenAlbert <espen.albert1@gmail.com>
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Keywords: api,changelog,cli,package-management,versioning
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Requires-Python: >=3.13
12
+ Requires-Dist: ask-shell
13
+ Requires-Dist: gitpython>=3.1.0
14
+ Requires-Dist: model-lib[toml]
15
+ Requires-Dist: tomlkit>=0.13
16
+ Requires-Dist: zero-3rdparty
17
+ Description-Content-Type: text/markdown
18
+
19
+ # Pkg Ext
20
+
21
+ A CLI tool for managing Python package public API, versioning, and changelog generation.
22
+
23
+ ## Overview
24
+
25
+ `pkg-ext` tracks which symbols (functions, classes, exceptions) in your package are "exposed" (public) vs "hidden" (internal). It:
26
+ - Generates `__init__.py` with imports and `__all__` based on decisions stored in changelog entries
27
+ - Creates group modules (e.g., `my_group.py`) that re-export related symbols
28
+ - Maintains a structured changelog directory (`.changelog/`) per PR
29
+ - Bumps version based on changelog action types (make_public=minor, fix=patch, delete/rename=major)
30
+ - Writes a human-readable `CHANGELOG.md`
31
+ - Provides [stability decorators](docs/stability.md) (`@experimental`, `@deprecated`) with suppressible warnings
32
+ - Generates `_warnings.py` in target packages to avoid runtime pkg-ext dependency
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ uv pip install pkg-ext
38
+ # or
39
+ pip install pkg-ext
40
+ ```
41
+
42
+ ## Core Concepts
43
+
44
+ ### Symbol Reference IDs
45
+
46
+ Symbols are identified by `{module_path}.{symbol_name}`, e.g., `my_pkg.utils.parse_config`.
47
+
48
+ ### Changelog Actions
49
+
50
+ Stored in `.changelog/{pr_number}.yaml` files using Pydantic discriminated unions:
51
+
52
+ | Action Type | Description | Version Bump | Key Fields |
53
+ |-------------|-------------|--------------|------------|
54
+ | `make_public` | Make symbol public | Minor | `group`, `details` |
55
+ | `keep_private` | Keep symbol internal | None | `full_path` |
56
+ | `fix` | Bug fix from git commit | Patch | `short_sha`, `message`, `changelog_message`, `ignored` |
57
+ | `delete` | Remove from public API | Major | `group` |
58
+ | `rename` | Rename with old alias | Major | `group`, `old_name` |
59
+ | `breaking_change` | Breaking API change | Major | `group`, `details` |
60
+ | `additional_change` | Non-breaking change | Patch | `group`, `details` |
61
+ | `group_module` | Assign module to a group | None | `module_path` |
62
+ | `release` | Version release marker | None | `old_version` |
63
+ | `experimental` | Mark as experimental | Patch | `target`, `group`/`parent` |
64
+ | `ga` | Graduate to GA | Patch | `target`, `group`/`parent` |
65
+ | `deprecated` | Mark as deprecated | Patch | `target`, `group`/`parent`, `replacement` |
66
+ | `max_bump_type` | Cap version bump | None | `max_bump`, `reason` |
67
+ | `chore` | Internal changes | Patch | `description` |
68
+
69
+ All actions inherit common fields: `name`, `ts`, `author`, `pr`.
70
+
71
+ The `breaking_change` and `additional_change` actions support optional fields for API diff:
72
+ - `change_kind: str | None` - machine-readable change type (e.g., `param_removed`, `default_changed`)
73
+ - `auto_generated: bool` - `true` when created by API diff, `false` for interactive actions
74
+ - `field_name: str | None` - field name for field-level changes
75
+
76
+ ### Stability Targets
77
+
78
+ Stability actions (`experimental`, `ga`, `deprecated`) support three target levels:
79
+
80
+ | Target | Description | Required Field |
81
+ |--------|-------------|----------------|
82
+ | `group` | Entire group | `name` = group name |
83
+ | `symbol` | Single symbol | `group` + `name` = symbol name |
84
+ | `arg` | Function argument | `parent` = `{group}.{symbol}`, `name` = arg name |
85
+
86
+ ### Public Groups
87
+
88
+ Groups organize related symbols. Configured in `.groups.yaml`:
89
+
90
+ ```yaml
91
+ groups:
92
+ - name: __ROOT__ # Top-level exports in __init__.py
93
+ owned_refs: []
94
+ owned_modules: []
95
+ - name: my_group
96
+ owned_refs:
97
+ - my_pkg.utils.parse_config
98
+ owned_modules:
99
+ - my_pkg.utils
100
+ ```
101
+
102
+ When a new symbol is exposed, the tool prompts you to select which group it belongs to. All symbols from the same module go to the same group.
103
+
104
+ ## CLI Commands
105
+
106
+ ```bash
107
+ pkg-ext [OPTIONS] COMMAND
108
+ ```
109
+
110
+ ### Global Options
111
+
112
+ | Option | Description |
113
+ |--------|-------------|
114
+ | `-p, --path, --pkg-path` | Package directory path (auto-detected if not provided) |
115
+ | `--repo-root` | Repository root (auto-detected from `.git`) |
116
+ | `--is-bot` | CI mode: no prompts, fail on missing decisions |
117
+ | `--skip-open` | Skip opening files in editor |
118
+ | `--tag-prefix` | Git tag prefix (e.g., `v` for `v1.0.0`) |
119
+
120
+ ### Command Reference
121
+
122
+ | Category | Commands | Description | Docs |
123
+ |----------|----------|-------------|------|
124
+ | Workflow | `pre-change`, `pre-commit`, `post-merge` | Development lifecycle commands | [docs/workflows](docs/workflows/index.md) |
125
+ | Changelog | `chore`, `promote`, `release-notes` | Changelog management | [docs/changelog](docs/changelog/index.md) |
126
+ | Stability | `exp`, `ga`, `dep` | Stability level management | [docs/stability](docs/stability/index.md) |
127
+ | API | `diff-api`, `dump-api` | API comparison and export | [docs/api_commands](docs/api_commands/index.md) |
128
+ | Generation | `gen-docs`, `gen-examples`, `gen-tests` | Generate documentation and scaffolds | [docs/generate](docs/generate/index.md) |
129
+
130
+ ### When to Use Workflow Commands
131
+
132
+ | Scenario | Command |
133
+ |----------|---------|
134
+ | Added or removed symbols | `pre-change` |
135
+ | Final validation before commit | `pre-commit` |
136
+ | Single command for everything | `pre-change --full` |
137
+ | CI/CD pipeline | `pre-commit` (bot mode) |
138
+
139
+ - **`pre-change`** handles interactive decisions (expose/hide symbols, delete/rename). Fast because it only generates example and test scaffolds.
140
+ - **`pre-commit`** validates all decisions are made (fails in bot mode if prompts needed), syncs generated files, regenerates docs, and runs the dirty check.
141
+ - **`pre-change --full`** combines both: runs interactive prompts, generates examples/tests, then syncs files and regenerates docs. The dirty check is skipped since you're still developing.
142
+
143
+ ## Configuration
144
+
145
+ ### User Config (`~/.config/pkg-ext/config.toml`)
146
+
147
+ ```toml
148
+ [user]
149
+ editor = "cursor" # or "code", "vim", etc.
150
+ skip_open_in_editor = false
151
+ ```
152
+
153
+ ### Project Config (`pyproject.toml`)
154
+
155
+ ```toml
156
+ [tool.pkg-ext]
157
+ tag_prefix = "v"
158
+ file_header = "# Generated by pkg-ext"
159
+ commit_fix_prefixes = ["fix:", "bugfix:", "hotfix:"]
160
+ commit_diff_suffixes = [".py", ".pyi"]
161
+ changelog_cleanup_count = 30 # Archive when count exceeds this
162
+ changelog_keep_count = 10 # Keep this many after cleanup
163
+ format_command = ["ruff", "format"] # ruff check --fix always runs first
164
+ max_bump_type = "minor" # Cap version bumps (patch, minor, major)
165
+ # after_file_write_hooks = ["extra-cmd {pkg_path}"] # Custom post-write hooks
166
+ ```
167
+
168
+ ### Group Configuration
169
+
170
+ Define groups with explicit settings in `pyproject.toml`:
171
+
172
+ ```toml
173
+ [tool.pkg-ext.groups.my_group]
174
+ dependencies = ["__ROOT__"] # Groups this depends on
175
+ docs_exclude = ["internal_helper"]
176
+ docstring = "Utilities for common operations"
177
+ ```
178
+
179
+ **Note:** Stability is not configured here. Use `pkg-ext exp/ga/dep` CLI commands to manage stability via changelog actions.
180
+
181
+ ### Version Bump Limits
182
+
183
+ For pre-1.0.0 packages where breaking changes are expected, cap the version bump:
184
+
185
+ **Project-level** (applies to all PRs):
186
+
187
+ ```toml
188
+ # pyproject.toml
189
+ [tool.pkg-ext]
190
+ max_bump_type = "minor" # All PRs capped to minor
191
+ ```
192
+
193
+ **Per-PR override** (`MaxBumpTypeAction` in changelog overrides config):
194
+
195
+ ```yaml
196
+ # .changelog/{pr}.yaml
197
+ name: version_cap
198
+ type: max_bump_type
199
+ max_bump: patch
200
+ reason: Documentation-only release
201
+ ts: '2026-01-17T14:35:00+00:00'
202
+ ```
203
+
204
+ ### Dev Mode
205
+
206
+ The `pre-commit` command enables dev mode, which writes to `-dev` suffixed files:
207
+ - `.groups-dev.yaml` instead of `.groups.yaml`
208
+ - `CHANGELOG-dev.md` instead of `CHANGELOG.md`
209
+
210
+ This allows iterating on changelog entries during development without modifying the production files. The real files are only updated by `post-merge` after PR is merged.
211
+
212
+ ## Generated Files
213
+
214
+ ### Files Updated During PR
215
+
216
+ | File | Purpose | Editable |
217
+ |------|---------|----------|
218
+ | `.changelog/{pr}.yaml` | Changelog actions for this PR | Yes |
219
+ | `.groups-dev.yaml` | Group assignments (dev copy) | No |
220
+ | `CHANGELOG-dev.md` | Human-readable changelog (dev copy) | No |
221
+ | `{pkg}.api-dev.yaml` | API dump for dev comparison (gitignored) | No |
222
+ | `{pkg}/__init__.py` | Package exports (VERSION unchanged) | No |
223
+ | `{pkg}/{group}.py` | Group re-export modules | No |
224
+ | `{pkg}/_warnings.py` | Stability warning decorators | No |
225
+ | `docs/**/*.md` | API documentation | Yes (outside markers) |
226
+ | `{group}_examples.py` | Example scaffolds | Yes (outside markers) |
227
+ | `{group}_test.py` | Test scaffolds | Yes (outside markers) |
228
+
229
+ - `__init__.py` exports are updated but VERSION remains unchanged until release
230
+ - Symbol doc pages include a "Changes" table showing unreleased modifications
231
+ - Content outside `=== OK_EDIT: pkg-ext ... ===` markers can be customized and is preserved
232
+
233
+ ### Files Updated During Release
234
+
235
+ These files are updated by `post-merge` after PR is merged (main branch only):
236
+
237
+ | File | What Changes |
238
+ |------|--------------|
239
+ | `.groups.yaml` | Copied from `.groups-dev.yaml` |
240
+ | `CHANGELOG.md` | Copied from `CHANGELOG-dev.md` |
241
+ | `{pkg}/__init__.py` | VERSION updated to new version |
242
+ | `pyproject.toml` | Version field updated (if used) |
243
+ | `{pkg}.api.yaml` | Regenerated with new version |
244
+ | `docs/**/*.md` | Unreleased changes become versioned |
245
+
246
+ ### File Contents Examples
247
+
248
+ **`__init__.py`:**
249
+
250
+ ```python
251
+ # Generated by pkg-ext
252
+ # flake8: noqa
253
+ from my_pkg import my_group
254
+ from my_pkg.utils import parse_config
255
+
256
+ VERSION = "0.1.0"
257
+ __all__ = [
258
+ "my_group",
259
+ "parse_config",
260
+ ]
261
+ ```
262
+
263
+ **Group module (`my_group.py`):**
264
+
265
+ ```python
266
+ # Generated by pkg-ext
267
+ from my_pkg.helpers import helper_func as _helper_func
268
+ from my_pkg._warnings import _experimental
269
+
270
+ helper_func = _experimental(_helper_func) # With experimental stability
271
+ ```
272
+
273
+ The underscore alias pattern prevents re-export issues with `__all__`.
274
+
275
+ **`_warnings.py`:**
276
+
277
+ When any group has non-GA stability, pkg-ext generates a `_warnings.py` module in the target package (removes runtime dependency on pkg-ext):
278
+
279
+ ```python
280
+ class MyPkgWarning(UserWarning): ...
281
+ class MyPkgExperimentalWarning(MyPkgWarning): ...
282
+ class MyPkgDeprecationWarning(MyPkgWarning, DeprecationWarning): ...
283
+ ```
284
+
285
+ ## Symbol Detection
286
+
287
+ The tool parses Python files using AST to find:
288
+ - **Functions** - Public functions (not starting with `_`)
289
+ - **Classes** - Public classes
290
+ - **Exceptions** - Classes inheriting from `Exception` or `BaseException`
291
+ - **Type Aliases** - Names ending with `T`
292
+ - **Global Variables** - UPPERCASE names with 2+ characters
293
+
294
+ Files skipped:
295
+ - `__init__.py`, `__main__.py` (dunder files)
296
+ - `*_test.py`, `test_*.py`, `conftest.py` (test files)
297
+ - Files starting with the configured `file_header` (already generated)
298
+
299
+ ## Automatic Behaviors
300
+
301
+ ### Function Argument Exposure
302
+
303
+ When exposing a function, its type hint arguments are auto-exposed if they reference local package types.
304
+
305
+ ### Git Integration
306
+
307
+ - Uses [GitPython](https://gitpython.readthedocs.io/) for commit analysis
308
+ - Uses [gh CLI](https://cli.github.com/) to detect PR info
309
+ - Extracts PR number from merge commit message (`Merge pull request #123`)
310
+
311
+ ## API Diff and Breaking Change Detection
312
+
313
+ During `pre-commit`, pkg-ext compares `{pkg}.api.yaml` (baseline from last release) against `{pkg}.api-dev.yaml` (current code) to detect API changes.
314
+
315
+ ### Detected Change Types
316
+
317
+ | Change | Breaking? | `change_kind` |
318
+ |--------|-----------|---------------|
319
+ | Parameter removed | Yes | `param_removed` |
320
+ | Required parameter added | Yes | `required_param_added` |
321
+ | Parameter type changed | Yes | `param_type_changed` |
322
+ | Return type changed | Yes | `return_type_changed` |
323
+ | Default removed | Yes | `default_removed` |
324
+ | Required field added | Yes | `required_field_added` |
325
+ | Field removed | Yes | `field_removed` |
326
+ | Base class removed | Yes | `base_class_removed` |
327
+ | Optional parameter added | No | `optional_param_added` |
328
+ | Default added | No | `default_added` |
329
+ | Default changed | No | `default_changed` |
330
+ | Optional field added | No | `optional_field_added` |
331
+
332
+ ### Auto-Generated Actions
333
+
334
+ API diff creates `BreakingChangeAction` or `AdditionalChangeAction` entries with `auto_generated: true`. These are:
335
+ - Replaced on each `pre-commit` run
336
+ - Keyed by `(name, group, type, change_kind)` for deduplication
337
+ - Timestamps preserved for unchanged changes
338
+
339
+ Interactive actions (from `pre-change`) are never replaced.
340
+
341
+ ### First Release
342
+
343
+ When no baseline `{pkg}.api.yaml` exists, diff is skipped (nothing to compare against).
344
+
345
+ ## Limitations
346
+
347
+ ### Symbol Detection
348
+ - **Type aliases require `T` suffix** - e.g., `ConfigT` not `Config`
349
+ - **Global vars require UPPERCASE** - e.g., `DEFAULT_TIMEOUT` not `default_timeout`
350
+ - **Exceptions require `Error` suffix** - e.g., `ParseError` not `ParseException`
351
+ - **No relative import support** - Only `from pkg.module import ...` is tracked
352
+
353
+ ### Group Handling
354
+ - **One group per module** - All symbols from a module belong to the same group
355
+ - **Cannot move symbols between groups** - Once assigned, module-to-group mapping is fixed
356
+ - **Root group always exists** - Cannot be removed, used for top-level exports
357
+
358
+ ### Git Requirements
359
+ - **Requires `gh` CLI** for PR info detection
360
+ - **Merge commit format expected** - `Merge pull request #123 from ...`
361
+ - **Single remote assumed** - Uses first remote for URL
362
+
363
+ ### Changelog
364
+ - **PR-based storage** - Each PR gets one `.yaml` file
365
+ - **No conflict resolution** - Manual merge of `.changelog/` files needed
366
+ - **Archiving by PR number** - Old entries archived to `.changelog/000/*.yaml`
367
+
368
+ ### Version Bumping
369
+ - **SemVer only** - No calendar versioning support
370
+ - **`pyproject.toml` or `__init__.py`** - Version must exist in one of these
371
+ - **Pre-release suffixes** - Supports `rc`, `a` (alpha), `b` (beta)
372
+
373
+ ### Interactive Mode
374
+ - **Removed reference handling incomplete** - `select_ref` and `select_multiple_ref_state` raise `NotImplementedError`. This breaks rename workflows when symbols are removed.
375
+ - **Alias creation not implemented** - `confirm_create_alias` always returns `False`
376
+
377
+ ### Stability
378
+ - **Non-callable symbols** - Constants and type aliases in experimental/deprecated groups don't emit warnings. `@experimental` and `@deprecated` only work on functions and classes.
379
+ - **Arg-level only for GA groups** - Cannot track arg-level stability changes until group is GA.
380
+
381
+ ### API Diff
382
+ - **No rename detection** - Renames are treated as remove + add (two separate actions)
383
+ - **Return types always breaking** - No semantic analysis (e.g., returning subclass is flagged as breaking)
384
+ - **Factory defaults** - Defaults using `"..."` (factory pattern) may cause false positives
385
+
386
+ ## Dependencies
387
+
388
+ - **[ask-shell](https://github.com/EspenAlbert/ask-shell)** - Interactive prompts and shell execution
389
+ - **[model-lib](https://github.com/EspenAlbert/model-lib)** - YAML/TOML parsing and Pydantic models
390
+ - **[GitPython](https://gitpython.readthedocs.io/)** - Git repository access
391
+
392
+ ## Appendix
393
+
394
+ ### File Structure
395
+
396
+ ```
397
+ my-repo/
398
+ CHANGELOG.md # Human-readable changelog
399
+ .groups.yaml # Group definitions
400
+ .changelog/ # Per-PR changelog actions
401
+ 123.yaml # Actions from PR #123
402
+ 000/ # Archived old entries
403
+ 001.yaml
404
+ my_pkg/
405
+ __init__.py # Generated exports
406
+ my_group.py # Generated group module
407
+ _warnings.py # Generated stability module (if needed)
408
+ utils.py # Source file
409
+ _internal.py # Private module (ignored)
410
+ ```
411
+
412
+ ### CI Configuration
413
+
414
+ **GitHub Actions:**
415
+
416
+ ```yaml
417
+ jobs:
418
+ validate:
419
+ runs-on: ubuntu-latest
420
+ steps:
421
+ - uses: actions/checkout@v4
422
+ - run: pip install pkg-ext
423
+ - run: pkg-ext pre-commit
424
+
425
+ release:
426
+ if: github.ref == 'refs/heads/main'
427
+ needs: validate
428
+ runs-on: ubuntu-latest
429
+ steps:
430
+ - uses: actions/checkout@v4
431
+ with:
432
+ fetch-depth: 0
433
+ - run: pip install pkg-ext
434
+ - run: pkg-ext post-merge --push
435
+ ```
436
+
437
+ ## Contributing
438
+
439
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, workflow, and git hooks.