code-is-magic-markers 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.
- code_is_magic_markers-0.1.0/.github/pull_request_template.md +14 -0
- code_is_magic_markers-0.1.0/.github/workflows/ci.yml +94 -0
- code_is_magic_markers-0.1.0/.github/workflows/release.yml +104 -0
- code_is_magic_markers-0.1.0/.gitignore +44 -0
- code_is_magic_markers-0.1.0/CHANGELOG.md +22 -0
- code_is_magic_markers-0.1.0/CONTRIBUTING.md +75 -0
- code_is_magic_markers-0.1.0/LICENSE +21 -0
- code_is_magic_markers-0.1.0/Makefile +64 -0
- code_is_magic_markers-0.1.0/PKG-INFO +241 -0
- code_is_magic_markers-0.1.0/README.md +210 -0
- code_is_magic_markers-0.1.0/examples/example.py +342 -0
- code_is_magic_markers-0.1.0/examples/real_usage.py +437 -0
- code_is_magic_markers-0.1.0/pyproject.toml +133 -0
- code_is_magic_markers-0.1.0/src/markers/__init__.py +29 -0
- code_is_magic_markers-0.1.0/src/markers/_types.py +141 -0
- code_is_magic_markers-0.1.0/src/markers/core.py +120 -0
- code_is_magic_markers-0.1.0/src/markers/descriptors.py +58 -0
- code_is_magic_markers-0.1.0/src/markers/groups.py +88 -0
- code_is_magic_markers-0.1.0/src/markers/marker.py +184 -0
- code_is_magic_markers-0.1.0/src/markers/py.typed +0 -0
- code_is_magic_markers-0.1.0/src/markers/registry.py +129 -0
- code_is_magic_markers-0.1.0/tests/__init__.py +0 -0
- code_is_magic_markers-0.1.0/tests/test_examples.py +34 -0
- code_is_magic_markers-0.1.0/tests/test_markers.py +806 -0
- code_is_magic_markers-0.1.0/tox.ini +42 -0
- code_is_magic_markers-0.1.0/uv.lock +749 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
## What
|
|
2
|
+
|
|
3
|
+
<!-- Brief description of the change -->
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
<!-- Motivation / context -->
|
|
8
|
+
|
|
9
|
+
## Testing
|
|
10
|
+
|
|
11
|
+
- [ ] Tests pass (`make test`)
|
|
12
|
+
- [ ] Lint passes (`make lint`)
|
|
13
|
+
- [ ] Type check passes (`make typecheck`)
|
|
14
|
+
- [ ] New tests added for new functionality
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
lint:
|
|
15
|
+
name: Lint
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: "3.12"
|
|
23
|
+
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: pip install ruff
|
|
26
|
+
|
|
27
|
+
- name: Ruff check
|
|
28
|
+
run: ruff check src/ tests/
|
|
29
|
+
|
|
30
|
+
- name: Ruff format check
|
|
31
|
+
run: ruff format --check src/ tests/
|
|
32
|
+
|
|
33
|
+
typecheck:
|
|
34
|
+
name: Type check
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/checkout@v4
|
|
38
|
+
|
|
39
|
+
- uses: actions/setup-python@v5
|
|
40
|
+
with:
|
|
41
|
+
python-version: "3.12"
|
|
42
|
+
|
|
43
|
+
- name: Install dependencies
|
|
44
|
+
run: |
|
|
45
|
+
pip install mypy
|
|
46
|
+
pip install -e .
|
|
47
|
+
|
|
48
|
+
- name: Mypy
|
|
49
|
+
run: mypy src/markers/ --ignore-missing-imports
|
|
50
|
+
|
|
51
|
+
test:
|
|
52
|
+
name: Test (Python ${{ matrix.python-version }})
|
|
53
|
+
runs-on: ubuntu-latest
|
|
54
|
+
strategy:
|
|
55
|
+
fail-fast: false
|
|
56
|
+
matrix:
|
|
57
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
58
|
+
steps:
|
|
59
|
+
- uses: actions/checkout@v4
|
|
60
|
+
|
|
61
|
+
- uses: actions/setup-python@v5
|
|
62
|
+
with:
|
|
63
|
+
python-version: ${{ matrix.python-version }}
|
|
64
|
+
|
|
65
|
+
- name: Install package
|
|
66
|
+
run: pip install -e ".[dev]"
|
|
67
|
+
|
|
68
|
+
- name: Run tests
|
|
69
|
+
run: pytest tests/ -v --tb=short
|
|
70
|
+
|
|
71
|
+
coverage:
|
|
72
|
+
name: Coverage
|
|
73
|
+
runs-on: ubuntu-latest
|
|
74
|
+
needs: [test]
|
|
75
|
+
steps:
|
|
76
|
+
- uses: actions/checkout@v4
|
|
77
|
+
|
|
78
|
+
- uses: actions/setup-python@v5
|
|
79
|
+
with:
|
|
80
|
+
python-version: "3.12"
|
|
81
|
+
|
|
82
|
+
- name: Install dependencies
|
|
83
|
+
run: pip install -e ".[dev]"
|
|
84
|
+
|
|
85
|
+
- name: Run tests with coverage
|
|
86
|
+
run: pytest tests/ --cov=markers --cov-report=xml --cov-report=term-missing
|
|
87
|
+
|
|
88
|
+
- name: Upload coverage
|
|
89
|
+
uses: codecov/codecov-action@v4
|
|
90
|
+
with:
|
|
91
|
+
file: coverage.xml
|
|
92
|
+
fail_ci_if_error: false
|
|
93
|
+
env:
|
|
94
|
+
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
id-token: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
check:
|
|
14
|
+
name: Pre-release checks
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.12"
|
|
22
|
+
|
|
23
|
+
- name: Install dependencies
|
|
24
|
+
run: pip install -e ".[dev]" ruff mypy
|
|
25
|
+
|
|
26
|
+
- name: Lint
|
|
27
|
+
run: ruff check src/ tests/
|
|
28
|
+
|
|
29
|
+
- name: Type check
|
|
30
|
+
run: mypy src/markers/ --ignore-missing-imports
|
|
31
|
+
|
|
32
|
+
- name: Test
|
|
33
|
+
run: pytest tests/ -v
|
|
34
|
+
|
|
35
|
+
build:
|
|
36
|
+
name: Build
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
needs: [check]
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@v4
|
|
41
|
+
|
|
42
|
+
- uses: actions/setup-python@v5
|
|
43
|
+
with:
|
|
44
|
+
python-version: "3.12"
|
|
45
|
+
|
|
46
|
+
- name: Install build tools
|
|
47
|
+
run: pip install build
|
|
48
|
+
|
|
49
|
+
- name: Build sdist and wheel
|
|
50
|
+
run: python -m build
|
|
51
|
+
|
|
52
|
+
- name: Upload build artifacts
|
|
53
|
+
uses: actions/upload-artifact@v4
|
|
54
|
+
with:
|
|
55
|
+
name: dist
|
|
56
|
+
path: dist/
|
|
57
|
+
|
|
58
|
+
publish-testpypi:
|
|
59
|
+
name: Publish to TestPyPI
|
|
60
|
+
runs-on: ubuntu-latest
|
|
61
|
+
needs: [build]
|
|
62
|
+
environment: testpypi
|
|
63
|
+
steps:
|
|
64
|
+
- uses: actions/download-artifact@v4
|
|
65
|
+
with:
|
|
66
|
+
name: dist
|
|
67
|
+
path: dist/
|
|
68
|
+
|
|
69
|
+
- name: Publish to TestPyPI
|
|
70
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
71
|
+
with:
|
|
72
|
+
repository-url: https://test.pypi.org/legacy/
|
|
73
|
+
|
|
74
|
+
publish-pypi:
|
|
75
|
+
name: Publish to PyPI
|
|
76
|
+
runs-on: ubuntu-latest
|
|
77
|
+
needs: [publish-testpypi]
|
|
78
|
+
environment: pypi
|
|
79
|
+
steps:
|
|
80
|
+
- uses: actions/download-artifact@v4
|
|
81
|
+
with:
|
|
82
|
+
name: dist
|
|
83
|
+
path: dist/
|
|
84
|
+
|
|
85
|
+
- name: Publish to PyPI
|
|
86
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
87
|
+
|
|
88
|
+
github-release:
|
|
89
|
+
name: GitHub Release
|
|
90
|
+
runs-on: ubuntu-latest
|
|
91
|
+
needs: [publish-pypi]
|
|
92
|
+
steps:
|
|
93
|
+
- uses: actions/checkout@v4
|
|
94
|
+
|
|
95
|
+
- uses: actions/download-artifact@v4
|
|
96
|
+
with:
|
|
97
|
+
name: dist
|
|
98
|
+
path: dist/
|
|
99
|
+
|
|
100
|
+
- name: Create GitHub Release
|
|
101
|
+
uses: softprops/action-gh-release@v2
|
|
102
|
+
with:
|
|
103
|
+
files: dist/*
|
|
104
|
+
generate_release_notes: true
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
*.egg-info/
|
|
7
|
+
*.egg
|
|
8
|
+
dist/
|
|
9
|
+
build/
|
|
10
|
+
.eggs/
|
|
11
|
+
|
|
12
|
+
# Virtual environments
|
|
13
|
+
.venv/
|
|
14
|
+
venv/
|
|
15
|
+
env/
|
|
16
|
+
|
|
17
|
+
# IDE
|
|
18
|
+
.idea/
|
|
19
|
+
.vscode/
|
|
20
|
+
*.swp
|
|
21
|
+
*.swo
|
|
22
|
+
*~
|
|
23
|
+
|
|
24
|
+
# Testing / coverage
|
|
25
|
+
.tox/
|
|
26
|
+
.pytest_cache/
|
|
27
|
+
htmlcov/
|
|
28
|
+
.coverage
|
|
29
|
+
.coverage.*
|
|
30
|
+
coverage.xml
|
|
31
|
+
*.cover
|
|
32
|
+
|
|
33
|
+
# mypy
|
|
34
|
+
.mypy_cache/
|
|
35
|
+
|
|
36
|
+
# ruff
|
|
37
|
+
.ruff_cache/
|
|
38
|
+
|
|
39
|
+
# claude
|
|
40
|
+
.claude/
|
|
41
|
+
|
|
42
|
+
# OS
|
|
43
|
+
.DS_Store
|
|
44
|
+
Thumbs.db
|
|
@@ -0,0 +1,22 @@
|
|
|
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.0] - 2025-04-12
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `Marker` base class — subclass to define markers with optional pydantic schema
|
|
13
|
+
- `MarkerGroup` — bundle related markers into a `.mixin` for clean inheritance
|
|
14
|
+
- `Registry` — track subclasses with `.subclasses()` and `.all` aggregation
|
|
15
|
+
- `MarkerInstance` — validated marker usage, works as both `Annotated[]` metadata and method decorator
|
|
16
|
+
- `MemberInfo` — unified metadata for fields and methods with `has`/`get`/`get_all` queries
|
|
17
|
+
- MRO-walking `Collector` with weakref-based per-class caching
|
|
18
|
+
- Intermediate marker base classes for shared schema fields
|
|
19
|
+
- `AllProxy` for cross-subclass collection via `Registry.all`
|
|
20
|
+
- PEP 561 `py.typed` marker
|
|
21
|
+
- 70 tests covering all features
|
|
22
|
+
- ORM-style real-world usage example with SQL generation
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025
|
|
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.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
.PHONY: help venv deps deps\:resolve deps\:install deps\:update test test\:cov lint format typecheck check build clean
|
|
2
|
+
|
|
3
|
+
VENV := .venv
|
|
4
|
+
UV := uv
|
|
5
|
+
|
|
6
|
+
help: ## Show this help
|
|
7
|
+
@grep -E '^[a-zA-Z_:-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
|
8
|
+
|
|
9
|
+
# --- Environment ---
|
|
10
|
+
|
|
11
|
+
venv: ## Create virtual environment
|
|
12
|
+
$(UV) venv $(VENV)
|
|
13
|
+
|
|
14
|
+
# --- Dependencies ---
|
|
15
|
+
|
|
16
|
+
deps\:resolve: ## Resolve and lock dependencies
|
|
17
|
+
$(UV) lock
|
|
18
|
+
|
|
19
|
+
deps\:install: ## Install project with dev extras into venv
|
|
20
|
+
$(UV) sync --extra dev
|
|
21
|
+
|
|
22
|
+
deps\:update: ## Update all dependencies and re-lock
|
|
23
|
+
$(UV) lock --upgrade
|
|
24
|
+
$(UV) sync --extra dev
|
|
25
|
+
|
|
26
|
+
deps: deps\:install ## Alias for deps:install
|
|
27
|
+
|
|
28
|
+
# --- Testing ---
|
|
29
|
+
|
|
30
|
+
test: ## Run tests
|
|
31
|
+
$(UV) run pytest tests/ -v
|
|
32
|
+
|
|
33
|
+
test\:cov: ## Run tests with coverage
|
|
34
|
+
$(UV) run pytest tests/ --cov=markers --cov-report=term-missing --cov-report=html
|
|
35
|
+
|
|
36
|
+
# --- Linting & Formatting ---
|
|
37
|
+
|
|
38
|
+
lint: ## Run linters (ruff check + format check)
|
|
39
|
+
$(UV) run ruff check src/ tests/
|
|
40
|
+
$(UV) run ruff format --check src/ tests/
|
|
41
|
+
|
|
42
|
+
format: ## Auto-format code
|
|
43
|
+
$(UV) run ruff format src/ tests/
|
|
44
|
+
$(UV) run ruff check --fix src/ tests/
|
|
45
|
+
|
|
46
|
+
# --- Type Checking ---
|
|
47
|
+
|
|
48
|
+
typecheck: ## Run type checker
|
|
49
|
+
$(UV) run mypy src/markers/ --ignore-missing-imports
|
|
50
|
+
|
|
51
|
+
# --- All Checks ---
|
|
52
|
+
|
|
53
|
+
check: lint typecheck test ## Run all checks (lint, typecheck, test)
|
|
54
|
+
|
|
55
|
+
# --- Build ---
|
|
56
|
+
|
|
57
|
+
build: ## Build sdist and wheel
|
|
58
|
+
$(UV) build
|
|
59
|
+
|
|
60
|
+
# --- Cleanup ---
|
|
61
|
+
|
|
62
|
+
clean: ## Clean build artifacts
|
|
63
|
+
rm -rf dist/ build/ *.egg-info src/*.egg-info .tox .pytest_cache .mypy_cache .ruff_cache htmlcov .coverage coverage.xml
|
|
64
|
+
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: code-is-magic-markers
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight class introspection toolkit — define typed markers, annotate fields and methods, collect metadata via MRO-walking descriptors.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Richard-Lynch/markers
|
|
6
|
+
Project-URL: Repository, https://github.com/Richard-Lynch/markers
|
|
7
|
+
Project-URL: Changelog, https://github.com/Richard-Lynch/markers/blob/main/CHANGELOG.md
|
|
8
|
+
Author: Richie
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: annotations,descriptors,introspection,markers,metadata,pydantic,registry,typing
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
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: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: pydantic>=2.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
29
|
+
Requires-Dist: tox>=4.0; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# markers
|
|
33
|
+
|
|
34
|
+
Lightweight class introspection toolkit for Python. Define typed markers, annotate fields and methods, collect metadata via MRO-walking descriptors.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install code-is-magic-markers
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick start
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from typing import Annotated
|
|
46
|
+
from markers import Marker, MarkerGroup, Registry
|
|
47
|
+
|
|
48
|
+
# 1. Define markers — the class body IS the schema
|
|
49
|
+
class Required(Marker): pass
|
|
50
|
+
|
|
51
|
+
class MaxLen(Marker):
|
|
52
|
+
mark = "max_length"
|
|
53
|
+
limit: int
|
|
54
|
+
|
|
55
|
+
class Searchable(Marker):
|
|
56
|
+
boost: float = 1.0
|
|
57
|
+
analyzer: str = "standard"
|
|
58
|
+
|
|
59
|
+
class OnSave(Marker):
|
|
60
|
+
mark = "on_save"
|
|
61
|
+
priority: int = 0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# 2. Bundle into groups
|
|
65
|
+
class Validation(MarkerGroup):
|
|
66
|
+
Required = Required
|
|
67
|
+
MaxLen = MaxLen
|
|
68
|
+
|
|
69
|
+
class Lifecycle(MarkerGroup):
|
|
70
|
+
OnSave = OnSave
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# 3. Annotate your classes
|
|
74
|
+
class User(Validation.mixin, Lifecycle.mixin):
|
|
75
|
+
name: Annotated[str, Validation.Required(), Validation.MaxLen(limit=100)]
|
|
76
|
+
email: Annotated[str, Validation.Required()]
|
|
77
|
+
bio: Annotated[str, Searchable()] = ""
|
|
78
|
+
|
|
79
|
+
@Lifecycle.OnSave(priority=10)
|
|
80
|
+
def validate(self) -> list[str]:
|
|
81
|
+
errors = []
|
|
82
|
+
for name, info in type(self).required.items():
|
|
83
|
+
if info.is_field and not getattr(self, name, None):
|
|
84
|
+
errors.append(f"{name} is required")
|
|
85
|
+
return errors
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# 4. Query metadata — same dict[str, MemberInfo] everywhere
|
|
89
|
+
User.fields # all fields
|
|
90
|
+
User.methods # all methods
|
|
91
|
+
User.members # both
|
|
92
|
+
User.required # only members marked 'required'
|
|
93
|
+
User.on_save # only members marked 'on_save'
|
|
94
|
+
|
|
95
|
+
# Introspect
|
|
96
|
+
User.fields["name"].get("max_length").limit # 100
|
|
97
|
+
User.methods["validate"].get("on_save").priority # 10
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Core concepts
|
|
101
|
+
|
|
102
|
+
### Marker
|
|
103
|
+
|
|
104
|
+
Subclass `Marker` to define a marker. The class body is the pydantic schema — typed fields become validated parameters:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
class ForeignKey(Marker):
|
|
108
|
+
mark = "foreign_key" # explicit name (default: lowercased class name)
|
|
109
|
+
table: str # required parameter
|
|
110
|
+
column: str = "id" # optional with default
|
|
111
|
+
on_delete: str = "CASCADE"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Markers work as both `Annotated[]` metadata and method decorators:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
# As annotation
|
|
118
|
+
author_id: Annotated[int, ForeignKey(table="users")]
|
|
119
|
+
|
|
120
|
+
# As decorator
|
|
121
|
+
@OnSave(priority=10)
|
|
122
|
+
def validate(self): ...
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Schema-less markers accept no parameters:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
class Required(Marker): pass
|
|
129
|
+
Required() # ok
|
|
130
|
+
Required(x=1) # TypeError
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Intermediate bases share schema fields:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
class LifecycleMarker(Marker):
|
|
137
|
+
priority: int = 0
|
|
138
|
+
|
|
139
|
+
class OnSave(LifecycleMarker):
|
|
140
|
+
mark = "on_save"
|
|
141
|
+
|
|
142
|
+
class OnDelete(LifecycleMarker):
|
|
143
|
+
mark = "on_delete"
|
|
144
|
+
|
|
145
|
+
# Both have 'priority'
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### MarkerGroup
|
|
149
|
+
|
|
150
|
+
Bundle related markers and produce a `.mixin`:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
class DB(MarkerGroup):
|
|
154
|
+
PrimaryKey = PrimaryKey
|
|
155
|
+
Indexed = Indexed
|
|
156
|
+
ForeignKey = ForeignKey
|
|
157
|
+
|
|
158
|
+
class User(DB.mixin):
|
|
159
|
+
id: Annotated[int, DB.PrimaryKey()]
|
|
160
|
+
email: Annotated[str, DB.Indexed(unique=True)]
|
|
161
|
+
|
|
162
|
+
User.primary_key # {'id': MemberInfo(...)}
|
|
163
|
+
User.indexed # {'email': MemberInfo(...)}
|
|
164
|
+
User.fields # all fields (from BaseMixin)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Groups compose via inheritance:
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
class ExtendedDB(DB):
|
|
171
|
+
Unique = Unique
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Registry
|
|
175
|
+
|
|
176
|
+
Track subclasses for cross-class queries:
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
class Entity(DB.mixin, Registry):
|
|
180
|
+
id: Annotated[int, DB.PrimaryKey()]
|
|
181
|
+
|
|
182
|
+
class User(Entity):
|
|
183
|
+
name: Annotated[str, Required()]
|
|
184
|
+
|
|
185
|
+
class Post(Entity):
|
|
186
|
+
title: Annotated[str, Required()]
|
|
187
|
+
|
|
188
|
+
# List all subclasses
|
|
189
|
+
Entity.subclasses() # [User, Post]
|
|
190
|
+
|
|
191
|
+
# Iterate with the same per-class API
|
|
192
|
+
for cls in Entity.subclasses():
|
|
193
|
+
print(cls.__name__, list(cls.required.keys()))
|
|
194
|
+
|
|
195
|
+
# Or gather across all subclasses
|
|
196
|
+
Entity.all.required # {'name': [MemberInfo(owner=User)], 'title': [MemberInfo(owner=Post)]}
|
|
197
|
+
Entity.all.fields # {'id': [MemberInfo(owner=User), MemberInfo(owner=Post)], ...}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### MemberInfo
|
|
201
|
+
|
|
202
|
+
Every collected member (field or method) is a `MemberInfo`:
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
info = User.fields["name"]
|
|
206
|
+
info.name # 'name'
|
|
207
|
+
info.kind # MemberKind.FIELD
|
|
208
|
+
info.type # <class 'str'>
|
|
209
|
+
info.owner # <class 'User'>
|
|
210
|
+
info.default # MISSING (no default)
|
|
211
|
+
info.has_default # False
|
|
212
|
+
info.is_field # True
|
|
213
|
+
info.is_method # False
|
|
214
|
+
info.markers # [MarkerInstance('required', ...), MarkerInstance('max_length', ...)]
|
|
215
|
+
info.has("required") # True
|
|
216
|
+
info.get("max_length").limit # 100
|
|
217
|
+
info.get_all("required") # [MarkerInstance(...)]
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## API reference
|
|
221
|
+
|
|
222
|
+
| Class | Purpose |
|
|
223
|
+
|-------|---------|
|
|
224
|
+
| `Marker` | Subclass to define markers with optional typed schema |
|
|
225
|
+
| `MarkerGroup` | Subclass to bundle markers into a `.mixin` |
|
|
226
|
+
| `Registry` | Subclass to track all subclasses, provides `.subclasses()` and `.all` |
|
|
227
|
+
| `MarkerInstance` | A specific usage of a marker with validated params |
|
|
228
|
+
| `MemberInfo` | Metadata about a field or method |
|
|
229
|
+
| `MemberKind` | Enum: `FIELD` or `METHOD` |
|
|
230
|
+
| `MISSING` | Sentinel for fields with no default |
|
|
231
|
+
|
|
232
|
+
### Marker class methods
|
|
233
|
+
|
|
234
|
+
| Method | Description |
|
|
235
|
+
|--------|-------------|
|
|
236
|
+
| `MyMarker.collect(cls)` | Collect members carrying this marker from `cls` |
|
|
237
|
+
| `Marker.invalidate(cls)` | Clear cached collection for `cls` |
|
|
238
|
+
|
|
239
|
+
## License
|
|
240
|
+
|
|
241
|
+
MIT
|