code-is-magic-markers 0.2.0__tar.gz → 0.3.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.
- {code_is_magic_markers-0.2.0 → code_is_magic_markers-0.3.0}/.github/workflows/ci.yml +9 -9
- {code_is_magic_markers-0.2.0 → code_is_magic_markers-0.3.0}/.github/workflows/release.yml +10 -10
- code_is_magic_markers-0.3.0/CHANGELOG.md +61 -0
- code_is_magic_markers-0.3.0/CONTRIBUTING.md +105 -0
- {code_is_magic_markers-0.2.0 → code_is_magic_markers-0.3.0}/PKG-INFO +76 -1
- {code_is_magic_markers-0.2.0 → code_is_magic_markers-0.3.0}/README.md +75 -0
- code_is_magic_markers-0.3.0/examples/type_check_example.py +181 -0
- {code_is_magic_markers-0.2.0 → code_is_magic_markers-0.3.0}/pyproject.toml +1 -1
- {code_is_magic_markers-0.2.0 → code_is_magic_markers-0.3.0}/src/markers/__init__.py +16 -0
- {code_is_magic_markers-0.2.0 → code_is_magic_markers-0.3.0}/src/markers/_types.py +9 -3
- {code_is_magic_markers-0.2.0 → code_is_magic_markers-0.3.0}/src/markers/descriptors.py +23 -1
- {code_is_magic_markers-0.2.0 → code_is_magic_markers-0.3.0}/src/markers/groups.py +26 -12
- code_is_magic_markers-0.3.0/src/markers/groups.pyi +19 -0
- {code_is_magic_markers-0.2.0 → code_is_magic_markers-0.3.0}/src/markers/marker.py +16 -3
- code_is_magic_markers-0.3.0/src/markers/marker.pyi +30 -0
- code_is_magic_markers-0.2.0/CHANGELOG.md +0 -28
- code_is_magic_markers-0.2.0/CONTRIBUTING.md +0 -75
- {code_is_magic_markers-0.2.0 → code_is_magic_markers-0.3.0}/.github/pull_request_template.md +0 -0
- {code_is_magic_markers-0.2.0 → code_is_magic_markers-0.3.0}/.gitignore +0 -0
- {code_is_magic_markers-0.2.0 → code_is_magic_markers-0.3.0}/LICENSE +0 -0
- {code_is_magic_markers-0.2.0 → code_is_magic_markers-0.3.0}/Makefile +0 -0
- {code_is_magic_markers-0.2.0 → code_is_magic_markers-0.3.0}/examples/example.py +0 -0
- {code_is_magic_markers-0.2.0 → code_is_magic_markers-0.3.0}/examples/real_usage.py +0 -0
- {code_is_magic_markers-0.2.0 → code_is_magic_markers-0.3.0}/src/markers/core.py +0 -0
- {code_is_magic_markers-0.2.0 → code_is_magic_markers-0.3.0}/src/markers/py.typed +0 -0
- {code_is_magic_markers-0.2.0 → code_is_magic_markers-0.3.0}/src/markers/registry.py +0 -0
- {code_is_magic_markers-0.2.0 → code_is_magic_markers-0.3.0}/tests/__init__.py +0 -0
- {code_is_magic_markers-0.2.0 → code_is_magic_markers-0.3.0}/tests/test_examples.py +0 -0
- {code_is_magic_markers-0.2.0 → code_is_magic_markers-0.3.0}/tests/test_markers.py +0 -0
- {code_is_magic_markers-0.2.0 → code_is_magic_markers-0.3.0}/tox.ini +0 -0
- {code_is_magic_markers-0.2.0 → code_is_magic_markers-0.3.0}/uv.lock +0 -0
|
@@ -17,9 +17,9 @@ jobs:
|
|
|
17
17
|
name: Lint
|
|
18
18
|
runs-on: ubuntu-latest
|
|
19
19
|
steps:
|
|
20
|
-
- uses: actions/checkout@v5
|
|
20
|
+
- uses: actions/checkout@v5.0.1
|
|
21
21
|
|
|
22
|
-
- uses: astral-sh/setup-uv@
|
|
22
|
+
- uses: astral-sh/setup-uv@v8.0.0
|
|
23
23
|
with:
|
|
24
24
|
python-version: "3.14"
|
|
25
25
|
|
|
@@ -36,9 +36,9 @@ jobs:
|
|
|
36
36
|
name: Type check
|
|
37
37
|
runs-on: ubuntu-latest
|
|
38
38
|
steps:
|
|
39
|
-
- uses: actions/checkout@v5
|
|
39
|
+
- uses: actions/checkout@v5.0.1
|
|
40
40
|
|
|
41
|
-
- uses: astral-sh/setup-uv@
|
|
41
|
+
- uses: astral-sh/setup-uv@v8.0.0
|
|
42
42
|
with:
|
|
43
43
|
python-version: "3.14"
|
|
44
44
|
|
|
@@ -56,9 +56,9 @@ jobs:
|
|
|
56
56
|
matrix:
|
|
57
57
|
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
58
58
|
steps:
|
|
59
|
-
- uses: actions/checkout@v5
|
|
59
|
+
- uses: actions/checkout@v5.0.1
|
|
60
60
|
|
|
61
|
-
- uses: astral-sh/setup-uv@
|
|
61
|
+
- uses: astral-sh/setup-uv@v8.0.0
|
|
62
62
|
with:
|
|
63
63
|
python-version: ${{ matrix.python-version }}
|
|
64
64
|
|
|
@@ -73,9 +73,9 @@ jobs:
|
|
|
73
73
|
runs-on: ubuntu-latest
|
|
74
74
|
needs: [test]
|
|
75
75
|
steps:
|
|
76
|
-
- uses: actions/checkout@v5
|
|
76
|
+
- uses: actions/checkout@v5.0.1
|
|
77
77
|
|
|
78
|
-
- uses: astral-sh/setup-uv@
|
|
78
|
+
- uses: astral-sh/setup-uv@v8.0.0
|
|
79
79
|
with:
|
|
80
80
|
python-version: "3.14"
|
|
81
81
|
|
|
@@ -86,7 +86,7 @@ jobs:
|
|
|
86
86
|
run: uv run pytest tests/ --cov=markers --cov-report=xml --cov-report=term-missing
|
|
87
87
|
|
|
88
88
|
- name: Upload coverage
|
|
89
|
-
uses: codecov/codecov-action@
|
|
89
|
+
uses: codecov/codecov-action@v6.0.0
|
|
90
90
|
with:
|
|
91
91
|
file: coverage.xml
|
|
92
92
|
fail_ci_if_error: false
|
|
@@ -14,9 +14,9 @@ jobs:
|
|
|
14
14
|
name: Pre-release checks
|
|
15
15
|
runs-on: ubuntu-latest
|
|
16
16
|
steps:
|
|
17
|
-
- uses: actions/checkout@v5
|
|
17
|
+
- uses: actions/checkout@v5.0.1
|
|
18
18
|
|
|
19
|
-
- uses: astral-sh/setup-uv@
|
|
19
|
+
- uses: astral-sh/setup-uv@v8.0.0
|
|
20
20
|
|
|
21
21
|
- name: Install dependencies
|
|
22
22
|
run: uv sync --extra dev
|
|
@@ -35,15 +35,15 @@ jobs:
|
|
|
35
35
|
runs-on: ubuntu-latest
|
|
36
36
|
needs: [check]
|
|
37
37
|
steps:
|
|
38
|
-
- uses: actions/checkout@v5
|
|
38
|
+
- uses: actions/checkout@v5.0.1
|
|
39
39
|
|
|
40
|
-
- uses: astral-sh/setup-uv@
|
|
40
|
+
- uses: astral-sh/setup-uv@v8.0.0
|
|
41
41
|
|
|
42
42
|
- name: Build sdist and wheel
|
|
43
43
|
run: uv build
|
|
44
44
|
|
|
45
45
|
- name: Upload build artifacts
|
|
46
|
-
uses: actions/upload-artifact@
|
|
46
|
+
uses: actions/upload-artifact@v7.0.1
|
|
47
47
|
with:
|
|
48
48
|
name: dist
|
|
49
49
|
path: dist/
|
|
@@ -54,7 +54,7 @@ jobs:
|
|
|
54
54
|
needs: [build]
|
|
55
55
|
environment: testpypi
|
|
56
56
|
steps:
|
|
57
|
-
- uses: actions/download-artifact@
|
|
57
|
+
- uses: actions/download-artifact@v8.0.1
|
|
58
58
|
with:
|
|
59
59
|
name: dist
|
|
60
60
|
path: dist/
|
|
@@ -70,7 +70,7 @@ jobs:
|
|
|
70
70
|
needs: [publish-testpypi]
|
|
71
71
|
environment: pypi
|
|
72
72
|
steps:
|
|
73
|
-
- uses: actions/download-artifact@
|
|
73
|
+
- uses: actions/download-artifact@v8.0.1
|
|
74
74
|
with:
|
|
75
75
|
name: dist
|
|
76
76
|
path: dist/
|
|
@@ -83,15 +83,15 @@ jobs:
|
|
|
83
83
|
runs-on: ubuntu-latest
|
|
84
84
|
needs: [publish-pypi]
|
|
85
85
|
steps:
|
|
86
|
-
- uses: actions/checkout@v5
|
|
86
|
+
- uses: actions/checkout@v5.0.1
|
|
87
87
|
|
|
88
|
-
- uses: actions/download-artifact@
|
|
88
|
+
- uses: actions/download-artifact@v8.0.1
|
|
89
89
|
with:
|
|
90
90
|
name: dist
|
|
91
91
|
path: dist/
|
|
92
92
|
|
|
93
93
|
- name: Create GitHub Release
|
|
94
|
-
uses: softprops/action-gh-release@
|
|
94
|
+
uses: softprops/action-gh-release@v3.0.0
|
|
95
95
|
with:
|
|
96
96
|
files: dist/*
|
|
97
97
|
generate_release_notes: true
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.3.0] - 2026-04-13
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- PEP 681 `dataclass_transform` support — marker constructor parameters (`MaxLen(limit=100)`) are fully validated by type checkers (Pylance, Pyright, mypy). Typos, wrong types, and missing required params are caught statically.
|
|
13
|
+
- Type stubs (`marker.pyi`, `groups.pyi`) for full IDE integration
|
|
14
|
+
- `BaseMixinMeta` metaclass for generated mixin classes
|
|
15
|
+
- `.fields`, `.methods`, `.members` are now fully typed as `dict[str, MemberInfo]` on any class using a group mixin
|
|
16
|
+
- Decorator usage (`@OnSave(priority=10)`) preserves the decorated function's type signature
|
|
17
|
+
- Type checking example (`examples/type_check_example.py`) demonstrating all typed patterns
|
|
18
|
+
- "Type checking" section in README documenting what's typed and escape hatches for dynamic descriptors
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- `MarkerInstance.__call__` signature uses `TypeVar` to preserve decorated function types
|
|
23
|
+
- `MarkerGroupMeta.__new__` return type narrowed to `MarkerGroupMeta` (was `type`)
|
|
24
|
+
- `MarkerGroup.mixin` typed as `type[BaseMixin]` (was `type`)
|
|
25
|
+
- Generated mixin classes now use `BaseMixinMeta` metaclass (was `type`)
|
|
26
|
+
- Removed `from __future__ import annotations` from `groups.py` for better type checker resolution
|
|
27
|
+
|
|
28
|
+
## [0.2.0] - 2026-04-13
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- Python 3.14 support (PEP 749 deferred annotations)
|
|
33
|
+
- Python 3.14 in CI test matrix, tox env list, and pyproject.toml classifiers
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- `MarkerMeta.__new__` restructured to read annotations from the created class rather than the namespace dict, for compatibility with Python 3.14.4+ where `__annotations__` is no longer populated in the namespace
|
|
38
|
+
- `Collector.collect` uses `annotationlib.get_annotations(format=STRING)` on Python 3.14+ instead of reading `__annotations__` directly
|
|
39
|
+
- CI lint, typecheck, and coverage jobs now default to Python 3.14
|
|
40
|
+
|
|
41
|
+
## [0.1.1] - 2026-04-13
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- Expanded README with motivation, detailed guides for all concepts, caching/invalidation docs, reusable mixins pattern, and complete API reference tables
|
|
46
|
+
|
|
47
|
+
## [0.1.0] - 2025-04-12
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- `Marker` base class — subclass to define markers with optional pydantic schema
|
|
52
|
+
- `MarkerGroup` — bundle related markers into a `.mixin` for clean inheritance
|
|
53
|
+
- `Registry` — track subclasses with `.subclasses()` and `.all` aggregation
|
|
54
|
+
- `MarkerInstance` — validated marker usage, works as both `Annotated[]` metadata and method decorator
|
|
55
|
+
- `MemberInfo` — unified metadata for fields and methods with `has`/`get`/`get_all` queries
|
|
56
|
+
- MRO-walking `Collector` with weakref-based per-class caching
|
|
57
|
+
- Intermediate marker base classes for shared schema fields
|
|
58
|
+
- `AllProxy` for cross-subclass collection via `Registry.all`
|
|
59
|
+
- PEP 561 `py.typed` marker
|
|
60
|
+
- 70 tests covering all features
|
|
61
|
+
- ORM-style real-world usage example with SQL generation
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Setup
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
git clone https://github.com/Richard-Lynch/markers.git
|
|
7
|
+
cd markers
|
|
8
|
+
make venv
|
|
9
|
+
make deps
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Development workflow
|
|
13
|
+
|
|
14
|
+
### Run tests
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# Quick run
|
|
18
|
+
make test
|
|
19
|
+
|
|
20
|
+
# With coverage
|
|
21
|
+
make test:cov
|
|
22
|
+
|
|
23
|
+
# Full matrix (Python 3.10–3.14)
|
|
24
|
+
tox
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Lint and format
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Check
|
|
31
|
+
make lint
|
|
32
|
+
|
|
33
|
+
# Auto-fix
|
|
34
|
+
make format
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Type check
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
make typecheck
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Run all checks
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
make check
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Versioning
|
|
50
|
+
|
|
51
|
+
This project follows [Semantic Versioning](https://semver.org/):
|
|
52
|
+
|
|
53
|
+
- **Patch** (0.1.x): Bug fixes, documentation, CI changes
|
|
54
|
+
- **Minor** (0.x.0): New features, new Python version support, backwards-compatible changes
|
|
55
|
+
- **Major** (x.0.0): Breaking API changes
|
|
56
|
+
|
|
57
|
+
## Release process
|
|
58
|
+
|
|
59
|
+
`main` is protected — all changes go through pull requests.
|
|
60
|
+
|
|
61
|
+
1. Create a release branch: `git checkout -b release/v0.x.y`
|
|
62
|
+
2. Update version in `pyproject.toml`
|
|
63
|
+
3. Update `CHANGELOG.md`
|
|
64
|
+
4. Open a PR, get CI green, merge
|
|
65
|
+
5. Tag main: `git checkout main && git pull && git tag v0.x.y && git push origin v0.x.y`
|
|
66
|
+
|
|
67
|
+
The GitHub Actions release workflow will automatically:
|
|
68
|
+
- Run all checks
|
|
69
|
+
- Build sdist + wheel
|
|
70
|
+
- Publish to TestPyPI
|
|
71
|
+
- Publish to PyPI
|
|
72
|
+
- Create a GitHub Release with auto-generated notes
|
|
73
|
+
|
|
74
|
+
## Project structure
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
src/markers/
|
|
78
|
+
├── __init__.py # Public API exports
|
|
79
|
+
├── _types.py # MarkerInstance, MemberInfo, MemberKind, MISSING
|
|
80
|
+
├── core.py # Collector — MRO walking + caching
|
|
81
|
+
├── descriptors.py # BaseMixin, BaseMixinMeta + descriptor classes
|
|
82
|
+
├── marker.py # Marker + MarkerMeta
|
|
83
|
+
├── marker.pyi # Type stub — dataclass_transform for marker kwargs
|
|
84
|
+
├── groups.py # MarkerGroup + MarkerGroupMeta
|
|
85
|
+
├── groups.pyi # Type stub — mixin base class typing
|
|
86
|
+
├── registry.py # Registry + AllProxy
|
|
87
|
+
└── py.typed # PEP 561 marker
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Type stubs
|
|
91
|
+
|
|
92
|
+
The library uses `.pyi` stubs for `marker.py` and `groups.py` to provide
|
|
93
|
+
type information that can't be expressed inline:
|
|
94
|
+
|
|
95
|
+
- **`marker.pyi`** — Uses PEP 681 `dataclass_transform` on `MarkerMeta` so
|
|
96
|
+
type checkers validate marker constructor kwargs against the schema
|
|
97
|
+
annotations on each `Marker` subclass. Also exposes `__call__` and
|
|
98
|
+
`__getattr__` on `Marker` instances for decorator and attribute access.
|
|
99
|
+
|
|
100
|
+
- **`groups.pyi`** — Declares `MarkerGroup.mixin = BaseMixin` so type checkers
|
|
101
|
+
accept `class User(DB.mixin):` as valid and know the class inherits
|
|
102
|
+
`BaseMixin`'s typed `.fields`, `.methods`, `.members` descriptors.
|
|
103
|
+
|
|
104
|
+
When modifying `marker.py` or `groups.py`, check that the corresponding
|
|
105
|
+
`.pyi` stub stays in sync.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-is-magic-markers
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Lightweight class introspection toolkit — define typed markers, annotate fields and methods, collect metadata via MRO-walking descriptors.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Richard-Lynch/markers
|
|
6
6
|
Project-URL: Repository, https://github.com/Richard-Lynch/markers
|
|
@@ -372,6 +372,81 @@ inst.analyzer # 'english'
|
|
|
372
372
|
| `inst.marker_name` | `str` | The marker type name |
|
|
373
373
|
| `inst.<field>` | `Any` | Access any schema field as an attribute |
|
|
374
374
|
|
|
375
|
+
## Type checking
|
|
376
|
+
|
|
377
|
+
The library ships with PEP 561 `py.typed` support and type stubs for full IDE integration (Pylance, Pyright, mypy).
|
|
378
|
+
|
|
379
|
+
### What's typed automatically
|
|
380
|
+
|
|
381
|
+
**Marker constructors** — parameters are fully validated by type checkers via [PEP 681 `dataclass_transform`](https://peps.python.org/pep-0681/). Typos, wrong types, and missing required params are all caught:
|
|
382
|
+
|
|
383
|
+
```python
|
|
384
|
+
class MaxLen(Marker):
|
|
385
|
+
mark = "max_length"
|
|
386
|
+
limit: int
|
|
387
|
+
|
|
388
|
+
MaxLen(limit=100) # ok
|
|
389
|
+
MaxLen(limit="oops") # type error: str is not int
|
|
390
|
+
MaxLen() # type error: missing 'limit'
|
|
391
|
+
MaxLen(limti=100) # type error: no param 'limti'
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
**BaseMixin descriptors** — `.fields`, `.methods`, `.members` are typed as `dict[str, MemberInfo]` on any class using a group mixin. Full dict operations (`.items()`, `.keys()`, `.values()`, indexing) work without casts:
|
|
395
|
+
|
|
396
|
+
```python
|
|
397
|
+
class User(DB.mixin):
|
|
398
|
+
id: Annotated[int, DB.PrimaryKey()]
|
|
399
|
+
|
|
400
|
+
for name, info in User.fields.items(): # (str, MemberInfo) — fully typed
|
|
401
|
+
print(info.is_field, info.has("primary_key"))
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**Decorator signatures** — `@OnSave(priority=10)` preserves the decorated function's type. Type checkers see the original return type, not `MarkerInstance`:
|
|
405
|
+
|
|
406
|
+
```python
|
|
407
|
+
@Lifecycle.OnSave(priority=10)
|
|
408
|
+
def validate(self) -> list[str]: ...
|
|
409
|
+
|
|
410
|
+
reveal_type(User().validate()) # list[str]
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
**Registry `.all` proxy** — `.all.fields`, `.all.methods`, `.all.members`, and `.all.<marker_name>` are typed as `dict[str, list[MemberInfo]]`.
|
|
414
|
+
|
|
415
|
+
**`Marker.collect()`** — always returns `dict[str, MemberInfo]`, fully typed:
|
|
416
|
+
|
|
417
|
+
```python
|
|
418
|
+
PrimaryKey.collect(User) # dict[str, MemberInfo] — no type: ignore needed
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### What's not typed automatically
|
|
422
|
+
|
|
423
|
+
**Marker-specific descriptors** like `.primary_key`, `.required`, `.indexed` are added dynamically by `MarkerGroupMeta` and are not visible to type checkers. Two options:
|
|
424
|
+
|
|
425
|
+
**Option A** — Use `Marker.collect()` (no annotations needed):
|
|
426
|
+
|
|
427
|
+
```python
|
|
428
|
+
# Instead of User.primary_key, use:
|
|
429
|
+
PrimaryKey.collect(User) # fully typed: dict[str, MemberInfo]
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
**Option B** — Add explicit `ClassVar` annotations (opt-in per class):
|
|
433
|
+
|
|
434
|
+
```python
|
|
435
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
436
|
+
|
|
437
|
+
class User(DB.mixin):
|
|
438
|
+
if TYPE_CHECKING:
|
|
439
|
+
primary_key: ClassVar[dict[str, MemberInfo]]
|
|
440
|
+
indexed: ClassVar[dict[str, MemberInfo]]
|
|
441
|
+
|
|
442
|
+
id: Annotated[int, DB.PrimaryKey()]
|
|
443
|
+
email: Annotated[str, DB.Indexed(unique=True)]
|
|
444
|
+
|
|
445
|
+
User.primary_key # now typed as dict[str, MemberInfo]
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
The `TYPE_CHECKING` guard ensures these annotations don't affect runtime behavior — the actual values come from the dynamically-installed `MarkerDescriptor` instances.
|
|
449
|
+
|
|
375
450
|
## How collection works
|
|
376
451
|
|
|
377
452
|
When you access a descriptor like `User.fields` or `User.required`, the library:
|
|
@@ -340,6 +340,81 @@ inst.analyzer # 'english'
|
|
|
340
340
|
| `inst.marker_name` | `str` | The marker type name |
|
|
341
341
|
| `inst.<field>` | `Any` | Access any schema field as an attribute |
|
|
342
342
|
|
|
343
|
+
## Type checking
|
|
344
|
+
|
|
345
|
+
The library ships with PEP 561 `py.typed` support and type stubs for full IDE integration (Pylance, Pyright, mypy).
|
|
346
|
+
|
|
347
|
+
### What's typed automatically
|
|
348
|
+
|
|
349
|
+
**Marker constructors** — parameters are fully validated by type checkers via [PEP 681 `dataclass_transform`](https://peps.python.org/pep-0681/). Typos, wrong types, and missing required params are all caught:
|
|
350
|
+
|
|
351
|
+
```python
|
|
352
|
+
class MaxLen(Marker):
|
|
353
|
+
mark = "max_length"
|
|
354
|
+
limit: int
|
|
355
|
+
|
|
356
|
+
MaxLen(limit=100) # ok
|
|
357
|
+
MaxLen(limit="oops") # type error: str is not int
|
|
358
|
+
MaxLen() # type error: missing 'limit'
|
|
359
|
+
MaxLen(limti=100) # type error: no param 'limti'
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
**BaseMixin descriptors** — `.fields`, `.methods`, `.members` are typed as `dict[str, MemberInfo]` on any class using a group mixin. Full dict operations (`.items()`, `.keys()`, `.values()`, indexing) work without casts:
|
|
363
|
+
|
|
364
|
+
```python
|
|
365
|
+
class User(DB.mixin):
|
|
366
|
+
id: Annotated[int, DB.PrimaryKey()]
|
|
367
|
+
|
|
368
|
+
for name, info in User.fields.items(): # (str, MemberInfo) — fully typed
|
|
369
|
+
print(info.is_field, info.has("primary_key"))
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
**Decorator signatures** — `@OnSave(priority=10)` preserves the decorated function's type. Type checkers see the original return type, not `MarkerInstance`:
|
|
373
|
+
|
|
374
|
+
```python
|
|
375
|
+
@Lifecycle.OnSave(priority=10)
|
|
376
|
+
def validate(self) -> list[str]: ...
|
|
377
|
+
|
|
378
|
+
reveal_type(User().validate()) # list[str]
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
**Registry `.all` proxy** — `.all.fields`, `.all.methods`, `.all.members`, and `.all.<marker_name>` are typed as `dict[str, list[MemberInfo]]`.
|
|
382
|
+
|
|
383
|
+
**`Marker.collect()`** — always returns `dict[str, MemberInfo]`, fully typed:
|
|
384
|
+
|
|
385
|
+
```python
|
|
386
|
+
PrimaryKey.collect(User) # dict[str, MemberInfo] — no type: ignore needed
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### What's not typed automatically
|
|
390
|
+
|
|
391
|
+
**Marker-specific descriptors** like `.primary_key`, `.required`, `.indexed` are added dynamically by `MarkerGroupMeta` and are not visible to type checkers. Two options:
|
|
392
|
+
|
|
393
|
+
**Option A** — Use `Marker.collect()` (no annotations needed):
|
|
394
|
+
|
|
395
|
+
```python
|
|
396
|
+
# Instead of User.primary_key, use:
|
|
397
|
+
PrimaryKey.collect(User) # fully typed: dict[str, MemberInfo]
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
**Option B** — Add explicit `ClassVar` annotations (opt-in per class):
|
|
401
|
+
|
|
402
|
+
```python
|
|
403
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
404
|
+
|
|
405
|
+
class User(DB.mixin):
|
|
406
|
+
if TYPE_CHECKING:
|
|
407
|
+
primary_key: ClassVar[dict[str, MemberInfo]]
|
|
408
|
+
indexed: ClassVar[dict[str, MemberInfo]]
|
|
409
|
+
|
|
410
|
+
id: Annotated[int, DB.PrimaryKey()]
|
|
411
|
+
email: Annotated[str, DB.Indexed(unique=True)]
|
|
412
|
+
|
|
413
|
+
User.primary_key # now typed as dict[str, MemberInfo]
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
The `TYPE_CHECKING` guard ensures these annotations don't affect runtime behavior — the actual values come from the dynamically-installed `MarkerDescriptor` instances.
|
|
417
|
+
|
|
343
418
|
## How collection works
|
|
344
419
|
|
|
345
420
|
When you access a descriptor like `User.fields` or `User.required`, the library:
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Example for validating type checker behavior with markers.
|
|
3
|
+
|
|
4
|
+
Open this file in your IDE (Pylance/Pyright) and verify:
|
|
5
|
+
- .fields, .members, .methods are fully typed as dict[str, MemberInfo]
|
|
6
|
+
- .items(), .keys(), .values() all work on descriptor results
|
|
7
|
+
- Decorated methods preserve their signatures
|
|
8
|
+
- Registry .all proxy returns dict[str, list[MemberInfo]]
|
|
9
|
+
|
|
10
|
+
Marker-specific descriptors (.primary_key, .required, etc.) are
|
|
11
|
+
dynamic — type checkers can't see them without help. Two options:
|
|
12
|
+
|
|
13
|
+
1. Use Marker.collect(cls) — fully typed, no annotations needed.
|
|
14
|
+
2. Add ClassVar annotations under TYPE_CHECKING on your class.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from typing import TYPE_CHECKING, Annotated, ClassVar
|
|
18
|
+
|
|
19
|
+
from markers import Marker, MarkerGroup, MemberInfo, Registry
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# -- Markers --
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Required(Marker):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MaxLen(Marker):
|
|
30
|
+
mark = "max_length"
|
|
31
|
+
limit: int
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PrimaryKey(Marker):
|
|
35
|
+
mark = "primary_key"
|
|
36
|
+
auto_increment: bool = True
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Indexed(Marker):
|
|
40
|
+
unique: bool = False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Transition(Marker):
|
|
44
|
+
source: list[str]
|
|
45
|
+
target: str
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# -- Groups --
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Validation(MarkerGroup):
|
|
52
|
+
Required = Required
|
|
53
|
+
MaxLen = MaxLen
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class DB(MarkerGroup):
|
|
57
|
+
PrimaryKey = PrimaryKey
|
|
58
|
+
Indexed = Indexed
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class SM(MarkerGroup):
|
|
62
|
+
Transition = Transition
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ============================================================
|
|
66
|
+
# 1. BaseMixin descriptors: always typed
|
|
67
|
+
# ============================================================
|
|
68
|
+
|
|
69
|
+
class User(DB.mixin):
|
|
70
|
+
id: Annotated[int, DB.PrimaryKey()]
|
|
71
|
+
email: Annotated[str, DB.Indexed(unique=True)]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# .fields, .methods, .members — all dict[str, MemberInfo]
|
|
75
|
+
fields: dict[str, MemberInfo] = User.fields
|
|
76
|
+
methods: dict[str, MemberInfo] = User.methods
|
|
77
|
+
members: dict[str, MemberInfo] = User.members
|
|
78
|
+
|
|
79
|
+
# .items() yields (str, MemberInfo)
|
|
80
|
+
for field_name, info in User.fields.items():
|
|
81
|
+
print(f"{field_name}: is_field={info.is_field}, has_default={info.has_default}")
|
|
82
|
+
|
|
83
|
+
for field_name in User.fields.keys():
|
|
84
|
+
print(field_name)
|
|
85
|
+
|
|
86
|
+
for info in User.fields.values():
|
|
87
|
+
print(info.name, info.kind)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ============================================================
|
|
91
|
+
# 2. MemberInfo methods
|
|
92
|
+
# ============================================================
|
|
93
|
+
|
|
94
|
+
id_info = User.fields["id"]
|
|
95
|
+
print(id_info.has("primary_key")) # bool
|
|
96
|
+
print(id_info.get("primary_key")) # MarkerInstance | None
|
|
97
|
+
print(id_info.get_all("primary_key")) # list[MarkerInstance]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ============================================================
|
|
101
|
+
# 3. Marker-specific descriptors: option A — Marker.collect()
|
|
102
|
+
# Fully typed, no annotations needed.
|
|
103
|
+
# ============================================================
|
|
104
|
+
|
|
105
|
+
pk: dict[str, MemberInfo] = PrimaryKey.collect(User)
|
|
106
|
+
idx: dict[str, MemberInfo] = Indexed.collect(User)
|
|
107
|
+
|
|
108
|
+
for field_name, info in PrimaryKey.collect(User).items():
|
|
109
|
+
marker = info.get("primary_key")
|
|
110
|
+
print(f"{field_name}: has pk = {marker is not None}")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ============================================================
|
|
114
|
+
# 4. Marker-specific descriptors: option B — ClassVar annotations
|
|
115
|
+
# Opt-in explicit typing for descriptor access on the class.
|
|
116
|
+
# ============================================================
|
|
117
|
+
|
|
118
|
+
class Product(DB.mixin):
|
|
119
|
+
if TYPE_CHECKING:
|
|
120
|
+
primary_key: ClassVar[dict[str, MemberInfo]]
|
|
121
|
+
indexed: ClassVar[dict[str, MemberInfo]]
|
|
122
|
+
|
|
123
|
+
id: Annotated[int, DB.PrimaryKey()]
|
|
124
|
+
sku: Annotated[str, DB.Indexed(unique=True)]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# Now type checkers know about these:
|
|
128
|
+
product_pk: dict[str, MemberInfo] = Product.primary_key
|
|
129
|
+
product_idx: dict[str, MemberInfo] = Product.indexed
|
|
130
|
+
|
|
131
|
+
for field_name, info in Product.primary_key.items():
|
|
132
|
+
print(f"{field_name}: {info.name}")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ============================================================
|
|
136
|
+
# 5. Decorator preserves function type
|
|
137
|
+
# ============================================================
|
|
138
|
+
|
|
139
|
+
class Task(SM.mixin):
|
|
140
|
+
title: Annotated[str, SM.Transition(source=["backlog"], target="todo")]
|
|
141
|
+
|
|
142
|
+
@SM.Transition(source=["backlog"], target="todo")
|
|
143
|
+
def move_to_todo(self) -> str:
|
|
144
|
+
return "moved"
|
|
145
|
+
|
|
146
|
+
@SM.Transition(source=["todo", "in_progress"], target="done")
|
|
147
|
+
def complete(self) -> bool:
|
|
148
|
+
return True
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
task = Task()
|
|
152
|
+
|
|
153
|
+
# These preserve the original return types
|
|
154
|
+
result_str: str = task.move_to_todo()
|
|
155
|
+
result_bool: bool = task.complete()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# ============================================================
|
|
159
|
+
# 6. Registry .all proxy: dict[str, list[MemberInfo]]
|
|
160
|
+
# ============================================================
|
|
161
|
+
|
|
162
|
+
class Entity(DB.mixin, Registry):
|
|
163
|
+
id: Annotated[int, DB.PrimaryKey()]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class Customer(Entity):
|
|
167
|
+
tier: Annotated[str, DB.Indexed()] = "free"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class Order(Entity):
|
|
171
|
+
total: Annotated[float, DB.Indexed(unique=True)]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
all_fields: dict[str, list[MemberInfo]] = Entity.all.fields
|
|
175
|
+
|
|
176
|
+
for member_name, infos in Entity.all.fields.items():
|
|
177
|
+
owners = [i.owner.__name__ for i in infos if i.owner]
|
|
178
|
+
print(f"{member_name}: {owners}")
|
|
179
|
+
|
|
180
|
+
for cls in Entity.subclasses():
|
|
181
|
+
print(cls.__name__)
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "code-is-magic-markers"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Lightweight class introspection toolkit — define typed markers, annotate fields and methods, collect metadata via MRO-walking descriptors."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -42,6 +42,22 @@ Supporting types:
|
|
|
42
42
|
MemberInfo Metadata about a field or method. Use .has()/.get()/.get_all().
|
|
43
43
|
MemberKind Enum: FIELD or METHOD.
|
|
44
44
|
MISSING Sentinel for fields with no default value.
|
|
45
|
+
|
|
46
|
+
Type checking:
|
|
47
|
+
The library ships with PEP 561 ``py.typed`` support and type stubs.
|
|
48
|
+
|
|
49
|
+
- Marker constructor kwargs are **fully validated** by type checkers
|
|
50
|
+
(e.g. ``MaxLen(limit="oops")`` is a type error). This is powered by
|
|
51
|
+
PEP 681 ``dataclass_transform`` on the ``Marker`` metaclass.
|
|
52
|
+
- ``.fields``, ``.methods``, ``.members`` are typed as
|
|
53
|
+
``dict[str, MemberInfo]`` on any class using a group mixin.
|
|
54
|
+
- ``Marker.collect(cls)`` is the fully-typed alternative for
|
|
55
|
+
marker-specific queries (e.g. ``Required.collect(User)``).
|
|
56
|
+
- Decorator usage preserves function signatures (``@OnSave()``
|
|
57
|
+
does not erase the decorated function's type).
|
|
58
|
+
- Marker-specific descriptors (``.primary_key``, ``.required``)
|
|
59
|
+
are dynamic and not visible to type checkers. See ``MarkerGroup``
|
|
60
|
+
docstring for typed alternatives.
|
|
45
61
|
"""
|
|
46
62
|
|
|
47
63
|
from markers._types import MISSING, MarkerInstance, MemberInfo, MemberKind
|
|
@@ -6,7 +6,9 @@ from __future__ import annotations
|
|
|
6
6
|
|
|
7
7
|
from collections.abc import Callable
|
|
8
8
|
from enum import Enum, auto
|
|
9
|
-
from typing import Any
|
|
9
|
+
from typing import Any, TypeVar
|
|
10
|
+
|
|
11
|
+
_F = TypeVar("_F", bound=Callable[..., Any])
|
|
10
12
|
|
|
11
13
|
__all__ = ["MISSING", "MarkerInstance", "MemberInfo", "MemberKind"]
|
|
12
14
|
|
|
@@ -69,8 +71,12 @@ class MarkerInstance:
|
|
|
69
71
|
"""The marker type name (e.g. 'required', 'on_save')."""
|
|
70
72
|
return self._marker_name
|
|
71
73
|
|
|
72
|
-
def __call__(self, fn:
|
|
73
|
-
"""Decorate a function, attaching this MarkerInstance.
|
|
74
|
+
def __call__(self, fn: _F) -> _F:
|
|
75
|
+
"""Decorate a function, attaching this MarkerInstance.
|
|
76
|
+
|
|
77
|
+
The decorated function's type signature is preserved — type checkers
|
|
78
|
+
will see the original return type, not ``MarkerInstance``.
|
|
79
|
+
"""
|
|
74
80
|
if not callable(fn):
|
|
75
81
|
raise TypeError(f"MarkerInstance '{self._marker_name}' expected a callable, got {type(fn).__name__}")
|
|
76
82
|
markers: list[MarkerInstance] = list(getattr(fn, "_markers", []))
|
|
@@ -46,11 +46,33 @@ class MarkerDescriptor:
|
|
|
46
46
|
return collector.filter(cls, self._marker_name)
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
class
|
|
49
|
+
class BaseMixinMeta(type):
|
|
50
|
+
"""Metaclass for generated mixin classes.
|
|
51
|
+
|
|
52
|
+
Used by ``MarkerGroupMeta`` to create mixin classes that inherit
|
|
53
|
+
from ``BaseMixin`` and carry ``MarkerDescriptor`` instances for
|
|
54
|
+
each marker in the group.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class BaseMixin(metaclass=BaseMixinMeta):
|
|
50
59
|
"""Mixin providing fields/methods/members descriptors.
|
|
51
60
|
|
|
52
61
|
Every Marker.mixin inherits from this, so any class using
|
|
53
62
|
at least one marker mixin automatically gets these.
|
|
63
|
+
|
|
64
|
+
Marker-specific descriptors (e.g. ``.primary_key``, ``.required``)
|
|
65
|
+
are added dynamically by ``MarkerGroupMeta`` and work at runtime.
|
|
66
|
+
For static type checking of these descriptors, either:
|
|
67
|
+
|
|
68
|
+
- Use ``Marker.collect()``::
|
|
69
|
+
|
|
70
|
+
PrimaryKey.collect(User) # fully typed: dict[str, MemberInfo]
|
|
71
|
+
|
|
72
|
+
- Add explicit type annotations under ``TYPE_CHECKING``::
|
|
73
|
+
|
|
74
|
+
if TYPE_CHECKING:
|
|
75
|
+
primary_key: ClassVar[dict[str, MemberInfo]]
|
|
54
76
|
"""
|
|
55
77
|
|
|
56
78
|
fields = FieldsDescriptor()
|
|
@@ -15,15 +15,10 @@ Markers themselves are pure schema + factory.
|
|
|
15
15
|
User.fields # → dict[str, MemberInfo]
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
-
from
|
|
18
|
+
from typing import Any
|
|
19
19
|
|
|
20
|
-
from
|
|
21
|
-
|
|
22
|
-
from markers.descriptors import BaseMixin, MarkerDescriptor
|
|
23
|
-
from markers.marker import Marker
|
|
24
|
-
|
|
25
|
-
if TYPE_CHECKING:
|
|
26
|
-
from markers.marker import MarkerMeta
|
|
20
|
+
from markers.descriptors import BaseMixin, BaseMixinMeta, MarkerDescriptor
|
|
21
|
+
from markers.marker import Marker, MarkerMeta
|
|
27
22
|
|
|
28
23
|
__all__ = ["MarkerGroup"]
|
|
29
24
|
|
|
@@ -31,10 +26,10 @@ __all__ = ["MarkerGroup"]
|
|
|
31
26
|
class MarkerGroupMeta(type):
|
|
32
27
|
"""Metaclass that auto-builds a .mixin from Marker class attributes."""
|
|
33
28
|
|
|
34
|
-
mixin: type
|
|
29
|
+
mixin: type[BaseMixin]
|
|
35
30
|
_markers: dict[str, MarkerMeta]
|
|
36
31
|
|
|
37
|
-
def __new__(mcs, name: str, bases: tuple, namespace: dict, **kwargs: Any) ->
|
|
32
|
+
def __new__(mcs, name: str, bases: tuple, namespace: dict, **kwargs: Any) -> "MarkerGroupMeta":
|
|
38
33
|
cls = super().__new__(mcs, name, bases, namespace)
|
|
39
34
|
|
|
40
35
|
if name == "MarkerGroup":
|
|
@@ -61,7 +56,7 @@ class MarkerGroupMeta(type):
|
|
|
61
56
|
mark_name = marker_cls._mark_name
|
|
62
57
|
mixin_attrs[mark_name] = MarkerDescriptor(mark_name)
|
|
63
58
|
|
|
64
|
-
cls.mixin =
|
|
59
|
+
cls.mixin = BaseMixinMeta(f"{name}Mixin", (BaseMixin,), mixin_attrs) # type: ignore[assignment]
|
|
65
60
|
cls._markers = found_markers
|
|
66
61
|
return cls
|
|
67
62
|
|
|
@@ -108,9 +103,28 @@ class MarkerGroup(metaclass=MarkerGroupMeta):
|
|
|
108
103
|
|
|
109
104
|
# FullDB.mixin has all of DB's descriptors plus 'unique' and 'check'
|
|
110
105
|
|
|
106
|
+
Type checking:
|
|
107
|
+
``.fields``, ``.methods``, and ``.members`` are fully typed as
|
|
108
|
+
``dict[str, MemberInfo]`` via ``BaseMixin``. Marker-specific
|
|
109
|
+
descriptors (e.g. ``.primary_key``) are dynamic and not visible
|
|
110
|
+
to type checkers. Two typed alternatives:
|
|
111
|
+
|
|
112
|
+
- Use ``Marker.collect()``::
|
|
113
|
+
|
|
114
|
+
PrimaryKey.collect(User) # fully typed: dict[str, MemberInfo]
|
|
115
|
+
|
|
116
|
+
- Add explicit ``ClassVar`` annotations::
|
|
117
|
+
|
|
118
|
+
if TYPE_CHECKING:
|
|
119
|
+
primary_key: ClassVar[dict[str, MemberInfo]]
|
|
120
|
+
|
|
121
|
+
Marker constructor parameters (e.g. ``MaxLen(limit=100)``) are
|
|
122
|
+
fully validated by type checkers via ``dataclass_transform``.
|
|
123
|
+
|
|
111
124
|
Attributes:
|
|
112
|
-
mixin (type): The generated mixin class. Inherit from this.
|
|
125
|
+
mixin (type[BaseMixin]): The generated mixin class. Inherit from this.
|
|
113
126
|
_markers (dict[str, MarkerMeta]): Mapping of attribute name to Marker class.
|
|
114
127
|
"""
|
|
115
128
|
|
|
129
|
+
mixin: type[BaseMixin]
|
|
116
130
|
_markers: dict[str, MarkerMeta] = {}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from markers.descriptors import BaseMixin
|
|
4
|
+
from markers.marker import MarkerMeta
|
|
5
|
+
|
|
6
|
+
class MarkerGroupMeta(type):
|
|
7
|
+
mixin: type[BaseMixin]
|
|
8
|
+
_markers: dict[str, MarkerMeta]
|
|
9
|
+
def __new__(
|
|
10
|
+
mcs,
|
|
11
|
+
name: str,
|
|
12
|
+
bases: tuple[type, ...],
|
|
13
|
+
namespace: dict[str, Any],
|
|
14
|
+
**kwargs: Any,
|
|
15
|
+
) -> MarkerGroupMeta: ...
|
|
16
|
+
|
|
17
|
+
class MarkerGroup(metaclass=MarkerGroupMeta):
|
|
18
|
+
mixin = BaseMixin
|
|
19
|
+
_markers: dict[str, MarkerMeta]
|
|
@@ -203,13 +203,26 @@ class Marker(metaclass=MarkerMeta):
|
|
|
203
203
|
|
|
204
204
|
# Both have 'priority' with default 0, different mark names.
|
|
205
205
|
|
|
206
|
+
Type checking:
|
|
207
|
+
Marker constructor parameters are fully validated by type checkers
|
|
208
|
+
via PEP 681 ``dataclass_transform`` (declared in ``marker.pyi``).
|
|
209
|
+
This means ``MaxLen(limit="oops")`` is a static type error, and
|
|
210
|
+
``MaxLen()`` flags the missing required ``limit`` parameter.
|
|
211
|
+
|
|
212
|
+
Decorator usage preserves the decorated function's type signature::
|
|
213
|
+
|
|
214
|
+
@OnSave(priority=10)
|
|
215
|
+
def validate(self) -> list[str]: ...
|
|
216
|
+
# type checkers see validate as (self) -> list[str]
|
|
217
|
+
|
|
206
218
|
Class methods:
|
|
207
219
|
collect(cls, target):
|
|
208
220
|
Collect all members carrying this marker from ``target``.
|
|
209
|
-
Returns ``dict[str, MemberInfo]``.
|
|
210
|
-
|
|
221
|
+
Returns ``dict[str, MemberInfo]``. Fully typed — this is
|
|
222
|
+
the recommended way to query markers when you need static
|
|
223
|
+
type safety::
|
|
211
224
|
|
|
212
|
-
Required.collect(User) #
|
|
225
|
+
Required.collect(User) # dict[str, MemberInfo]
|
|
213
226
|
|
|
214
227
|
invalidate(cls, target):
|
|
215
228
|
Clear cached collection for ``target``. Useful after dynamic
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import Any, ClassVar, TypeVar
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
from typing_extensions import dataclass_transform
|
|
6
|
+
|
|
7
|
+
from markers._types import MemberInfo
|
|
8
|
+
|
|
9
|
+
_F = TypeVar("_F", bound=Callable[..., Any])
|
|
10
|
+
|
|
11
|
+
@dataclass_transform()
|
|
12
|
+
class MarkerMeta(type):
|
|
13
|
+
_mark_name: str
|
|
14
|
+
_schema_model: type[BaseModel] | None
|
|
15
|
+
_schema_annotations: dict[str, Any]
|
|
16
|
+
_schema_defaults: dict[str, Any]
|
|
17
|
+
|
|
18
|
+
class Marker(metaclass=MarkerMeta):
|
|
19
|
+
_mark_name: ClassVar[str]
|
|
20
|
+
_schema_model: ClassVar[type[BaseModel] | None]
|
|
21
|
+
_schema_annotations: ClassVar[dict[str, Any]]
|
|
22
|
+
_schema_defaults: ClassVar[dict[str, Any]]
|
|
23
|
+
mark: ClassVar[str]
|
|
24
|
+
marker_name: ClassVar[str]
|
|
25
|
+
def __call__(self, fn: _F) -> _F: ...
|
|
26
|
+
def __getattr__(self, key: str) -> Any: ...
|
|
27
|
+
@classmethod
|
|
28
|
+
def collect(cls, target: type) -> dict[str, MemberInfo]: ...
|
|
29
|
+
@classmethod
|
|
30
|
+
def invalidate(cls, target: type) -> None: ...
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to this project will be documented in this file.
|
|
4
|
-
|
|
5
|
-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
-
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
-
|
|
8
|
-
## [0.1.1] - 2026-04-13
|
|
9
|
-
|
|
10
|
-
### Changed
|
|
11
|
-
|
|
12
|
-
- Expanded README with motivation, detailed guides for all concepts, caching/invalidation docs, reusable mixins pattern, and complete API reference tables
|
|
13
|
-
|
|
14
|
-
## [0.1.0] - 2025-04-12
|
|
15
|
-
|
|
16
|
-
### Added
|
|
17
|
-
|
|
18
|
-
- `Marker` base class — subclass to define markers with optional pydantic schema
|
|
19
|
-
- `MarkerGroup` — bundle related markers into a `.mixin` for clean inheritance
|
|
20
|
-
- `Registry` — track subclasses with `.subclasses()` and `.all` aggregation
|
|
21
|
-
- `MarkerInstance` — validated marker usage, works as both `Annotated[]` metadata and method decorator
|
|
22
|
-
- `MemberInfo` — unified metadata for fields and methods with `has`/`get`/`get_all` queries
|
|
23
|
-
- MRO-walking `Collector` with weakref-based per-class caching
|
|
24
|
-
- Intermediate marker base classes for shared schema fields
|
|
25
|
-
- `AllProxy` for cross-subclass collection via `Registry.all`
|
|
26
|
-
- PEP 561 `py.typed` marker
|
|
27
|
-
- 70 tests covering all features
|
|
28
|
-
- ORM-style real-world usage example with SQL generation
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
# Contributing
|
|
2
|
-
|
|
3
|
-
## Setup
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
git clone https://github.com/Richard-Lynch/markers.git
|
|
7
|
-
cd markers
|
|
8
|
-
make venv
|
|
9
|
-
make deps
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
## Development workflow
|
|
13
|
-
|
|
14
|
-
### Run tests
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
# Quick run
|
|
18
|
-
make test
|
|
19
|
-
|
|
20
|
-
# With coverage
|
|
21
|
-
make test:cov
|
|
22
|
-
|
|
23
|
-
# Full matrix (Python 3.10–3.13)
|
|
24
|
-
tox
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
### Lint and format
|
|
28
|
-
|
|
29
|
-
```bash
|
|
30
|
-
# Check
|
|
31
|
-
make lint
|
|
32
|
-
|
|
33
|
-
# Auto-fix
|
|
34
|
-
make format
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
### Type check
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
make typecheck
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
### Run all checks
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
make check
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## Release process
|
|
50
|
-
|
|
51
|
-
1. Update version in `pyproject.toml`
|
|
52
|
-
2. Update `CHANGELOG.md`
|
|
53
|
-
3. Commit: `git commit -am "Release v0.x.y"`
|
|
54
|
-
4. Tag: `git tag v0.x.y`
|
|
55
|
-
5. Push: `git push origin main --tags`
|
|
56
|
-
|
|
57
|
-
The GitHub Actions release workflow will automatically:
|
|
58
|
-
- Run all checks
|
|
59
|
-
- Build sdist + wheel
|
|
60
|
-
- Publish to TestPyPI
|
|
61
|
-
- Publish to PyPI
|
|
62
|
-
- Create a GitHub Release with auto-generated notes
|
|
63
|
-
|
|
64
|
-
## Project structure
|
|
65
|
-
|
|
66
|
-
```
|
|
67
|
-
src/markers/
|
|
68
|
-
├── __init__.py # Public API exports
|
|
69
|
-
├── _types.py # MarkerInstance, MemberInfo, MemberKind, MISSING
|
|
70
|
-
├── core.py # Collector — MRO walking + caching
|
|
71
|
-
├── descriptors.py # BaseMixin + descriptor classes
|
|
72
|
-
├── marker.py # Marker + MarkerMeta
|
|
73
|
-
├── groups.py # MarkerGroup + MarkerGroupMeta
|
|
74
|
-
└── registry.py # Registry + AllProxy
|
|
75
|
-
```
|
{code_is_magic_markers-0.2.0 → code_is_magic_markers-0.3.0}/.github/pull_request_template.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|