constutil 1.0.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.
- constutil-1.0.0/.github/workflows/ci.yml +41 -0
- constutil-1.0.0/.github/workflows/pages.yml +42 -0
- constutil-1.0.0/.github/workflows/pypi_publish.yml +71 -0
- constutil-1.0.0/.gitignore +13 -0
- constutil-1.0.0/LICENSE +21 -0
- constutil-1.0.0/PKG-INFO +388 -0
- constutil-1.0.0/README.md +364 -0
- constutil-1.0.0/examples/README.md +29 -0
- constutil-1.0.0/examples/days.py +35 -0
- constutil-1.0.0/examples/saturn_moons.py +35 -0
- constutil-1.0.0/pyproject.toml +53 -0
- constutil-1.0.0/scripts/build_docs.py +100 -0
- constutil-1.0.0/skills/constutil/SKILL.md +104 -0
- constutil-1.0.0/skills/constutil/agents/openai.yaml +6 -0
- constutil-1.0.0/src/constutil/__init__.py +14 -0
- constutil-1.0.0/src/constutil/constdef.py +28 -0
- constutil-1.0.0/src/constutil/constgroup.py +131 -0
- constutil-1.0.0/src/constutil/py.typed +0 -0
- constutil-1.0.0/tests/test_constutil.py +193 -0
- constutil-1.0.0/uv.lock +1239 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_call:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
tests:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
strategy:
|
|
16
|
+
fail-fast: false
|
|
17
|
+
matrix:
|
|
18
|
+
python: ['3.10', '3.11', '3.12', '3.13', '3.14']
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- uses: astral-sh/setup-uv@v6
|
|
22
|
+
with:
|
|
23
|
+
enable-cache: true
|
|
24
|
+
python-version: ${{ matrix.python }}
|
|
25
|
+
- run: uv sync --locked
|
|
26
|
+
- run: uv run pytest --cov=constutil --cov-report=term-missing --cov-fail-under=95
|
|
27
|
+
quality:
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/checkout@v4
|
|
31
|
+
- uses: astral-sh/setup-uv@v6
|
|
32
|
+
with:
|
|
33
|
+
enable-cache: true
|
|
34
|
+
python-version: '3.10'
|
|
35
|
+
- run: uv sync --locked
|
|
36
|
+
- run: uv run ruff check .
|
|
37
|
+
- run: uv run ruff format --check .
|
|
38
|
+
- run: uv run mypy
|
|
39
|
+
- run: uv run python scripts/build_docs.py
|
|
40
|
+
- run: uv build
|
|
41
|
+
- run: uv run twine check dist/*
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: GitHub Pages
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
concurrency:
|
|
12
|
+
group: pages
|
|
13
|
+
cancel-in-progress: false
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
build:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
- uses: astral-sh/setup-uv@v6
|
|
21
|
+
with:
|
|
22
|
+
enable-cache: true
|
|
23
|
+
python-version: '3.14'
|
|
24
|
+
- run: uv sync --locked
|
|
25
|
+
- run: uv run python scripts/build_docs.py
|
|
26
|
+
- uses: actions/configure-pages@v5
|
|
27
|
+
- uses: actions/upload-pages-artifact@v3
|
|
28
|
+
with:
|
|
29
|
+
path: site/
|
|
30
|
+
deploy:
|
|
31
|
+
needs: build
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
permissions:
|
|
34
|
+
pages: write
|
|
35
|
+
id-token: write
|
|
36
|
+
environment:
|
|
37
|
+
name: github-pages
|
|
38
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
39
|
+
steps:
|
|
40
|
+
- name: Deploy
|
|
41
|
+
id: deployment
|
|
42
|
+
uses: actions/deploy-pages@v4
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
test:
|
|
12
|
+
uses: ./.github/workflows/ci.yml
|
|
13
|
+
build:
|
|
14
|
+
needs: test
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: astral-sh/setup-uv@v6
|
|
19
|
+
with:
|
|
20
|
+
python-version: '3.14'
|
|
21
|
+
- run: uv sync --locked
|
|
22
|
+
- name: Verify release tag matches package version
|
|
23
|
+
env:
|
|
24
|
+
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
|
25
|
+
run: |
|
|
26
|
+
uv run python - <<'PY'
|
|
27
|
+
import os
|
|
28
|
+
import tomllib
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
version = tomllib.loads(Path('pyproject.toml').read_text())['project']['version']
|
|
31
|
+
if os.environ['RELEASE_TAG'] != f'v{version}':
|
|
32
|
+
raise SystemExit(f'Release tag must be v{version}')
|
|
33
|
+
PY
|
|
34
|
+
- run: uv build
|
|
35
|
+
- run: uv run twine check dist/*
|
|
36
|
+
- uses: actions/upload-artifact@v4
|
|
37
|
+
with:
|
|
38
|
+
name: distributions
|
|
39
|
+
path: dist/
|
|
40
|
+
if-no-files-found: error
|
|
41
|
+
publish:
|
|
42
|
+
needs: build
|
|
43
|
+
runs-on: ubuntu-latest
|
|
44
|
+
environment:
|
|
45
|
+
name: pypi
|
|
46
|
+
url: https://pypi.org/project/constutil/
|
|
47
|
+
permissions:
|
|
48
|
+
id-token: write
|
|
49
|
+
steps:
|
|
50
|
+
- uses: actions/download-artifact@v4
|
|
51
|
+
with:
|
|
52
|
+
name: distributions
|
|
53
|
+
path: dist/
|
|
54
|
+
- name: Publish with Trusted Publishing
|
|
55
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
56
|
+
release-assets:
|
|
57
|
+
needs: build
|
|
58
|
+
runs-on: ubuntu-latest
|
|
59
|
+
permissions:
|
|
60
|
+
contents: write
|
|
61
|
+
steps:
|
|
62
|
+
- uses: actions/download-artifact@v4
|
|
63
|
+
with:
|
|
64
|
+
name: distributions
|
|
65
|
+
path: dist/
|
|
66
|
+
- name: Attach distributions to the GitHub release
|
|
67
|
+
env:
|
|
68
|
+
GH_TOKEN: ${{ github.token }}
|
|
69
|
+
GH_REPO: ${{ github.repository }}
|
|
70
|
+
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
|
71
|
+
run: gh release upload "$RELEASE_TAG" dist/* --clobber
|
constutil-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Patchfork
|
|
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.
|
constutil-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: constutil
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Typed constant definitions and groups for Python
|
|
5
|
+
Project-URL: Homepage, https://github.com/patchfork/constutil
|
|
6
|
+
Project-URL: Documentation, https://constutil.patchfork.dev/
|
|
7
|
+
Project-URL: Repository, https://github.com/patchfork/constutil
|
|
8
|
+
Project-URL: Issues, https://github.com/patchfork/constutil/issues
|
|
9
|
+
Author-email: Patchfork <pypi@patchfork.dev>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: constants,dataclasses,enum,typing
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# constutil
|
|
26
|
+
|
|
27
|
+
Typed constant definitions and groups for Python, with no runtime dependencies.
|
|
28
|
+
|
|
29
|
+
`ConstDef` stores a value and a display name. `ConstGroup` collects named definitions
|
|
30
|
+
and provides ordered enumeration, lookup, and validation. They can describe simple
|
|
31
|
+
choices or richer records without requiring Python's `enum.Enum`.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
Requires **Python 3.10 or newer**.
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
pip install constutil
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Until the first PyPI release, install directly from GitHub:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
pip install git+https://github.com/patchfork/constutil.git
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Days of the week
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from constutil import IntConstDef, IntConstGroup
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Day(IntConstGroup):
|
|
54
|
+
MONDAY = IntConstDef(1, "Monday")
|
|
55
|
+
TUESDAY = IntConstDef(2, "Tuesday")
|
|
56
|
+
WEDNESDAY = IntConstDef(3, "Wednesday")
|
|
57
|
+
THURSDAY = IntConstDef(4, "Thursday")
|
|
58
|
+
FRIDAY = IntConstDef(5, "Friday")
|
|
59
|
+
SATURDAY = IntConstDef(6, "Saturday")
|
|
60
|
+
SUNDAY = IntConstDef(7, "Sunday")
|
|
61
|
+
|
|
62
|
+
_default_constant = MONDAY
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
assert Day.MONDAY.value == 1
|
|
66
|
+
assert Day.MONDAY.name == "Monday"
|
|
67
|
+
assert Day.get_default() is Day.MONDAY
|
|
68
|
+
assert Day.get_by_value(1) is Day.MONDAY
|
|
69
|
+
assert Day.get_constant(name="Monday") is Day.MONDAY
|
|
70
|
+
assert Day.get_by_constant_name("MONDAY") is Day.MONDAY
|
|
71
|
+
assert Day.get_all_values() == (1, 2, 3, 4, 5, 6, 7)
|
|
72
|
+
assert Day.get_as_pairs()[0] == (1, "Monday")
|
|
73
|
+
assert Day.get_by_value("1") is None
|
|
74
|
+
assert Day.get_by_name("monday") is None
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The attribute name (`MONDAY`), display name (`Monday`), and stored value (`1`)
|
|
78
|
+
are distinct. `.name` means display text, not the Python attribute name.
|
|
79
|
+
|
|
80
|
+
## Moons of Saturn: additional metadata
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from dataclasses import dataclass
|
|
84
|
+
from constutil import ConstDef, ConstGroup
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True, slots=True)
|
|
88
|
+
class MoonDef(ConstDef[str]):
|
|
89
|
+
discovered_by: str
|
|
90
|
+
discovery_year: int
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class SaturnMoon(ConstGroup[MoonDef]):
|
|
94
|
+
TITAN = MoonDef("titan", "Titan", "Christiaan Huygens", 1655)
|
|
95
|
+
IAPETUS = MoonDef("iapetus", "Iapetus", "Giovanni Domenico Cassini", 1671)
|
|
96
|
+
RHEA = MoonDef("rhea", "Rhea", "Giovanni Domenico Cassini", 1672)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
moon = SaturnMoon.get_by_value("titan")
|
|
100
|
+
assert moon is not None
|
|
101
|
+
assert moon.discovery_year == 1655 # The result retains the MoonDef type.
|
|
102
|
+
assert SaturnMoon.get_all_constant_names() == ("TITAN", "IAPETUS", "RHEA")
|
|
103
|
+
assert SaturnMoon.get_name("TITAN") is None
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## String constants and custom records
|
|
107
|
+
|
|
108
|
+
`IntConstDef` and `StrConstDef` are aliases for `ConstDef[int]` and `ConstDef[str]`.
|
|
109
|
+
`IntConstGroup` and `StrConstGroup` are aliases for their corresponding specialized
|
|
110
|
+
groups. The aliases do not introduce new runtime classes.
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from constutil import StrConstDef, StrConstGroup
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class Season(StrConstGroup):
|
|
117
|
+
SPRING = StrConstDef("spring", "Spring")
|
|
118
|
+
SUMMER = StrConstDef("summer", "Summer")
|
|
119
|
+
AUTUMN = StrConstDef("autumn", "Autumn")
|
|
120
|
+
WINTER = StrConstDef("winter", "Winter")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
assert Season.get_value("Spring") == "spring"
|
|
124
|
+
assert Season.is_valid_value("SPRING") is False
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
A custom member class does not have to inherit `ConstDef`. It must expose a
|
|
128
|
+
`.value` of type `int | str` and a `.name` of type `str`, as expressed by the
|
|
129
|
+
exported `ConstantMember` protocol. Pass the concrete member class to
|
|
130
|
+
`ConstGroup[YourMember]`; the protocol is a static typing contract, not a runtime
|
|
131
|
+
member-discovery type. Mutable dataclasses may also implement that contract.
|
|
132
|
+
|
|
133
|
+
## Runnable examples
|
|
134
|
+
|
|
135
|
+
Clone the repository and run from its root:
|
|
136
|
+
|
|
137
|
+
```sh
|
|
138
|
+
uv run examples/days.py 6
|
|
139
|
+
uv run examples/saturn_moons.py rhea
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`uv` installs this checkout automatically; no published release is needed. See
|
|
143
|
+
[examples/README.md](https://github.com/patchfork/constutil/tree/main/examples)
|
|
144
|
+
for default runs and invalid-input demonstrations.
|
|
145
|
+
|
|
146
|
+
## Behavior
|
|
147
|
+
|
|
148
|
+
### Comparison and lookup
|
|
149
|
+
|
|
150
|
+
All value, display-name, and attribute-name comparisons use **ordinary Python
|
|
151
|
+
`==`**, with no string conversion, whitespace trimming, or case normalization.
|
|
152
|
+
`1` does not match `"1"`, and `"titan"` does not match `"TITAN"`. Python's own
|
|
153
|
+
numeric equality still applies: `1 == True == 1.0`. Exact comparison does not
|
|
154
|
+
mean strict type identity.
|
|
155
|
+
|
|
156
|
+
`get_constant(value=None, name=None)` retains a combined lookup interface:
|
|
157
|
+
|
|
158
|
+
- A positional argument is a value: `Day.get_constant(1)`.
|
|
159
|
+
- Use `name="Monday"` to search by display name.
|
|
160
|
+
- A non-`None` value takes precedence when both arguments are supplied.
|
|
161
|
+
- No value and no name raises `ValueError`; a missing match returns `None`.
|
|
162
|
+
- All single-result lookups return the first match in declaration order.
|
|
163
|
+
|
|
164
|
+
### Enumeration, membership, and inheritance
|
|
165
|
+
|
|
166
|
+
Public attributes matching the declared member type are discovered in declaration
|
|
167
|
+
order. For a parameterized member such as `ConstDef[int]`, discovery also checks
|
|
168
|
+
its value with `isinstance(value, int)`. Unrelated attributes and wrong value types
|
|
169
|
+
are skipped. Prefix auxiliary attributes with `_` to exclude them explicitly.
|
|
170
|
+
|
|
171
|
+
**Inheritance is not meant to be derived beyond the generic derivations**:
|
|
172
|
+
use `class Day(ConstGroup[IntConstDef])`, its `IntConstGroup` alias, or a custom
|
|
173
|
+
record derived from `ConstDef[str]` as above. Do not extend a populated group or
|
|
174
|
+
build extra generic inheritance hierarchies. Enumeration inspects only the
|
|
175
|
+
concrete group's own class dictionary: inherited attributes may be accessible
|
|
176
|
+
through Python but are not included in that child's enumeration or lookup.
|
|
177
|
+
|
|
178
|
+
Members can be freely constructed. There is no singleton or uniqueness guarantee;
|
|
179
|
+
duplicate values, duplicate names, and multiple attributes referencing the same
|
|
180
|
+
member are allowed. `is_valid()` uses member equality, not identity. With
|
|
181
|
+
`ConstDef`, dataclass equality compares value and name and requires the same
|
|
182
|
+
runtime definition class.
|
|
183
|
+
|
|
184
|
+
`ConstDef` is a frozen dataclass. Groups are ordinary Python classes:
|
|
185
|
+
their attributes can be reassigned, and enumeration reflects changes immediately.
|
|
186
|
+
Each enumeration returns a fresh tuple or dictionary. A configured default is
|
|
187
|
+
returned as-is and is not required to belong to the group; absent defaults are
|
|
188
|
+
`None`. Custom mutable records remain mutable.
|
|
189
|
+
|
|
190
|
+
### API reference
|
|
191
|
+
|
|
192
|
+
| Method | Result |
|
|
193
|
+
| --- | --- |
|
|
194
|
+
| `get_default()` | Configured member or `None` |
|
|
195
|
+
| `get_all()` | Tuple of members |
|
|
196
|
+
| `get_all_map()` | Fresh attribute-name → member dictionary |
|
|
197
|
+
| `get_all_values()` | Tuple of stored values |
|
|
198
|
+
| `get_all_names()` | Tuple of display names |
|
|
199
|
+
| `get_all_constant_names()` | Tuple of Python attribute names |
|
|
200
|
+
| `get_as_pairs()` | Tuple of `(value, display_name)` pairs; value types preserved |
|
|
201
|
+
| `get_constant(value=None, name=None)` | Matching member or `None` |
|
|
202
|
+
| `get_by_value(value)` | Member matching the stored value or `None` |
|
|
203
|
+
| `get_by_name(name)` | Member matching the display name or `None` |
|
|
204
|
+
| `get_by_constant_name(name)` | Member matching the attribute name or `None` |
|
|
205
|
+
| `get_name(value)` | Display name or `None` |
|
|
206
|
+
| `get_value(name)` | Stored value or `None` |
|
|
207
|
+
| `is_valid(member)` | Whether an equal member exists in the group |
|
|
208
|
+
| `is_valid_value(value)` | Whether a member has an equal stored value |
|
|
209
|
+
| `is_value(member, value)` | Whether the supplied member's value equals `value` |
|
|
210
|
+
| `get_filtered(members)` | List copy of the input; **does not filter or validate membership** |
|
|
211
|
+
| `get_filtered_as_pairs(members)` | List of `(str(value), display_name)` pairs from the input |
|
|
212
|
+
| `has_required(values)` | **Exact set equality** with all group values |
|
|
213
|
+
|
|
214
|
+
The last three helpers intentionally retain their original behavior. Filter helpers
|
|
215
|
+
preserve input order and duplicates and accept foreign members. `has_required()`
|
|
216
|
+
ignores order and duplicates, but rejects missing or extra values. `is_value()`
|
|
217
|
+
does not check membership. Pair serialization in `get_filtered_as_pairs()` is an
|
|
218
|
+
output conversion, not a lookup comparison.
|
|
219
|
+
|
|
220
|
+
Lookup results preserve the declared member type. Value-only convenience methods
|
|
221
|
+
return `int | str` (and `None` for a missing lookup); use the typed member's `.value`
|
|
222
|
+
when the narrower scalar type matters. Annotations do not validate constructor
|
|
223
|
+
arguments at runtime.
|
|
224
|
+
|
|
225
|
+
## Python compatibility
|
|
226
|
+
|
|
227
|
+
The minimum is **Python 3.10**, determined by the features actually used:
|
|
228
|
+
|
|
229
|
+
| Feature | Introduced |
|
|
230
|
+
| --- | --- |
|
|
231
|
+
| `typing.Generic`, `TypeVar` | Python 3.5 |
|
|
232
|
+
| Dataclasses | Python 3.7 |
|
|
233
|
+
| `typing.Protocol`, `get_args`, `get_origin` | Python 3.8 |
|
|
234
|
+
| Built-in collection annotations such as `tuple[str, ...]` | Python 3.9 |
|
|
235
|
+
| Union annotations such as `MemberT | None` | Python 3.10 |
|
|
236
|
+
| `@dataclass(slots=True)` in optional metadata subclasses | Python 3.10 |
|
|
237
|
+
|
|
238
|
+
The generic base deliberately omits `slots=True`: older Python versions raise a
|
|
239
|
+
`TypeError` when instantiating a frozen, slotted generic alias because `typing`
|
|
240
|
+
tries to assign `__orig_class__`. Frozen definitions without slots work across the
|
|
241
|
+
supported versions. A subclass may use slots, but still inherits the base instance
|
|
242
|
+
dictionary.
|
|
243
|
+
|
|
244
|
+
Generic discovery uses `__orig_bases__`; it does not need Python 3.12's
|
|
245
|
+
`types.get_original_bases` or PEP 695 type-parameter syntax. The package includes
|
|
246
|
+
`py.typed`. CI tests Python 3.10–3.14. See the official
|
|
247
|
+
[dataclass documentation](https://docs.python.org/3.10/library/dataclasses.html),
|
|
248
|
+
[typing documentation](https://docs.python.org/3.10/library/typing.html), and
|
|
249
|
+
[Python 3.10 changes](https://docs.python.org/3.10/whatsnew/3.10.html).
|
|
250
|
+
|
|
251
|
+
## Adopt the coding skill (Codex and Claude Code)
|
|
252
|
+
|
|
253
|
+
The repository includes an opinionated, reusable
|
|
254
|
+
[constutil skill](https://github.com/patchfork/constutil/tree/main/skills/constutil).
|
|
255
|
+
It directs an agent to use `constutil` for related constant values, usually in a
|
|
256
|
+
`constants/` package, and explains naming, access, lookup, and existence checks.
|
|
257
|
+
Installing the Python dependency alone does **not** install the skill.
|
|
258
|
+
|
|
259
|
+
For one project, copy the complete `skills/constutil/` directory from this
|
|
260
|
+
repository to `<your-project>/.agents/skills/constutil/` and commit it. From that
|
|
261
|
+
project's root, with this repository cloned alongside it:
|
|
262
|
+
|
|
263
|
+
```sh
|
|
264
|
+
mkdir -p .agents/skills
|
|
265
|
+
cp -R ../constutil/skills/constutil .agents/skills/constutil
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
For personal use across projects, copy the directory to
|
|
269
|
+
`~/.agents/skills/constutil/` instead. Other agents supporting `SKILL.md` can use
|
|
270
|
+
the same skill folder in their own skill-discovery location. In Codex, invoke it
|
|
271
|
+
as `$constutil`, or let Codex select it for matching tasks. See the official
|
|
272
|
+
[skill installation and discovery documentation](https://learn.chatgpt.com/docs/build-skills).
|
|
273
|
+
|
|
274
|
+
To make this an always-applicable project convention, also add this instruction
|
|
275
|
+
to the consuming project's `AGENTS.md` (skill selection alone is task-dependent):
|
|
276
|
+
|
|
277
|
+
```text
|
|
278
|
+
Always use constutil when grouping related Python constant values. Keep groups
|
|
279
|
+
in the relevant constants package unless the existing package structure calls
|
|
280
|
+
for another location. Follow .agents/skills/constutil/SKILL.md for definition
|
|
281
|
+
and member names, access, exact lookup, and existence checks.
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Download from the documentation site
|
|
285
|
+
|
|
286
|
+
The site serves the same maintained skill files as this repository:
|
|
287
|
+
|
|
288
|
+
- [Skill instructions](https://constutil.patchfork.dev/skills/constutil/SKILL.md)
|
|
289
|
+
- [Codex metadata](https://constutil.patchfork.dev/skills/constutil/agents/openai.yaml)
|
|
290
|
+
- [Complete skill ZIP](https://constutil.patchfork.dev/skills/constutil.zip)
|
|
291
|
+
|
|
292
|
+
Extract the ZIP into `.agents/skills/` for Codex or `.claude/skills/` for Claude
|
|
293
|
+
Code. It contains a `constutil/` directory. Review the instructions and commit the
|
|
294
|
+
installed skill into your project. Installing the Python package does not install
|
|
295
|
+
or activate the skill automatically.
|
|
296
|
+
|
|
297
|
+
### Claude Code
|
|
298
|
+
|
|
299
|
+
Use the **same** `skills/constutil/` folder; the `SKILL.md` instructions are shared.
|
|
300
|
+
From the consuming project's root:
|
|
301
|
+
|
|
302
|
+
```sh
|
|
303
|
+
mkdir -p .claude/skills
|
|
304
|
+
cp -R ../constutil/skills/constutil .claude/skills/constutil
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Commit that folder for your team. For personal use across projects, copy it to
|
|
308
|
+
`~/.claude/skills/constutil/`. Invoke it as `/constutil`, or let Claude select it
|
|
309
|
+
when the task matches. The optional `agents/openai.yaml` file supplies Codex UI
|
|
310
|
+
metadata; Claude uses `SKILL.md`.
|
|
311
|
+
|
|
312
|
+
Add the same always-use instruction shown above to `CLAUDE.md`, changing the
|
|
313
|
+
reference to `.claude/skills/constutil/SKILL.md`. This makes the project convention
|
|
314
|
+
available each session while the skill supplies the detailed usage guidance.
|
|
315
|
+
See [Claude Code's skill documentation](https://code.claude.com/docs/en/skills).
|
|
316
|
+
|
|
317
|
+
The skill has no dependency on a framework or another skill. Keep its version
|
|
318
|
+
aligned with the library version when updating it.
|
|
319
|
+
|
|
320
|
+
## Documentation for agents
|
|
321
|
+
|
|
322
|
+
- [llms.txt](https://constutil.patchfork.dev/llms.txt): concise index of documentation,
|
|
323
|
+
examples, and skill instructions.
|
|
324
|
+
- [index.md](https://constutil.patchfork.dev/index.md): this README as plain Markdown.
|
|
325
|
+
- [llms-full.txt](https://constutil.patchfork.dev/llms-full.txt): the README and shared
|
|
326
|
+
skill instructions in one text file.
|
|
327
|
+
|
|
328
|
+
These files and the downloadable skill are generated from the repository on every
|
|
329
|
+
Pages deployment. `llms.txt` is a discovery aid; it does not install skills or make
|
|
330
|
+
an agent follow them automatically.
|
|
331
|
+
|
|
332
|
+
## Development
|
|
333
|
+
|
|
334
|
+
```sh
|
|
335
|
+
uv sync --locked
|
|
336
|
+
uv run pytest --cov=constutil --cov-report=term-missing
|
|
337
|
+
uv run ruff check .
|
|
338
|
+
uv run ruff format --check .
|
|
339
|
+
uv run mypy
|
|
340
|
+
uv build
|
|
341
|
+
uv run twine check dist/*
|
|
342
|
+
uv run python scripts/build_docs.py
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Tests execute the Python examples in this README as well as checking discovery,
|
|
346
|
+
comparison, defaults, metadata, aliases, and helper semantics. Documentation is
|
|
347
|
+
generated directly from this file into `site/index.html`, so the package README
|
|
348
|
+
and website share one source.
|
|
349
|
+
|
|
350
|
+
## Publishing
|
|
351
|
+
|
|
352
|
+
### PyPI
|
|
353
|
+
|
|
354
|
+
The `pypi_publish.yml` workflow runs on a published GitHub release, tests the package
|
|
355
|
+
on Python 3.10–3.14, checks types and formatting, verifies that the release tag
|
|
356
|
+
matches the package version, builds a wheel and source distribution, checks their
|
|
357
|
+
metadata, attaches the same wheel and source distribution to the GitHub release,
|
|
358
|
+
and publishes via PyPI Trusted Publishing. It does not require an API token.
|
|
359
|
+
|
|
360
|
+
One-time setup:
|
|
361
|
+
|
|
362
|
+
1. Create a GitHub environment named `pypi` in `patchfork/constutil`.
|
|
363
|
+
2. On PyPI, configure a pending publisher for `constutil` (or a trusted publisher
|
|
364
|
+
if you already own the project): owner `patchfork`, repository `constutil`,
|
|
365
|
+
workflow filename `pypi_publish.yml`, environment `pypi`.
|
|
366
|
+
3. Update `project.version` in `pyproject.toml`, run `uv lock`, and commit the changes.
|
|
367
|
+
4. Publish a GitHub release with a matching tag, for example `v1.0.0`.
|
|
368
|
+
|
|
369
|
+
PyPI project-name availability is decided by PyPI when registering or publishing.
|
|
370
|
+
See [PyPI's Trusted Publishing guide](https://docs.pypi.org/trusted-publishers/).
|
|
371
|
+
|
|
372
|
+
### GitHub Pages
|
|
373
|
+
|
|
374
|
+
Select **GitHub Actions** under the repository's **Settings → Pages → Build and
|
|
375
|
+
deployment → Source**. The `pages.yml` workflow builds this README and deploys it
|
|
376
|
+
on pushes to `main`, or through a manual workflow run.
|
|
377
|
+
|
|
378
|
+
The published site is [constutil.patchfork.dev](https://constutil.patchfork.dev/).
|
|
379
|
+
Its DNS record is `CNAME constutil → patchfork.github.io` (without a repository
|
|
380
|
+
path). The repository's Pages custom-domain setting must also be
|
|
381
|
+
`constutil.patchfork.dev`; this Actions deployment does not use a `CNAME` file.
|
|
382
|
+
HTTPS is managed by GitHub Pages.
|
|
383
|
+
|
|
384
|
+
See [GitHub's Pages workflow documentation](https://docs.github.com/en/pages/getting-started-with-github-pages/using-custom-workflows-with-github-pages).
|
|
385
|
+
|
|
386
|
+
## License
|
|
387
|
+
|
|
388
|
+
MIT. See [LICENSE](https://github.com/patchfork/constutil/blob/main/LICENSE).
|