ducktools-classbuilder 0.9.1__tar.gz → 0.10.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.
Potentially problematic release.
This version of ducktools-classbuilder might be problematic. Click here for more details.
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/.github/workflows/auto_test.yml +5 -5
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/.github/workflows/publish_to_pypi.yml +2 -2
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/.github/workflows/publish_to_testpypi.yml +2 -2
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/PKG-INFO +7 -17
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs/tutorial.md +5 -6
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs_code/docs_ex9_annotated.py +14 -14
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs_code/tutorial_code.py +5 -7
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/pyproject.toml +23 -7
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/src/ducktools/classbuilder/__init__.py +160 -67
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/src/ducktools/classbuilder/__init__.pyi +16 -9
- ducktools_classbuilder-0.10.0/src/ducktools/classbuilder/_version.py +2 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/src/ducktools/classbuilder/annotations.py +2 -13
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/src/ducktools/classbuilder/prefab.py +1 -2
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/src/ducktools/classbuilder/prefab.pyi +1 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/src/ducktools_classbuilder.egg-info/PKG-INFO +7 -17
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/src/ducktools_classbuilder.egg-info/SOURCES.txt +19 -32
- ducktools_classbuilder-0.10.0/src/ducktools_classbuilder.egg-info/requires.txt +5 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/tests/annotations/test_annotated.py +5 -3
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/tests/annotations/test_annotations_module.py +7 -7
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/tests/annotations/test_future_annotations.py +3 -0
- ducktools_classbuilder-0.10.0/tests/conftest.py +20 -0
- ducktools_classbuilder-0.10.0/tests/helpers/utils.py +7 -0
- {ducktools_classbuilder-0.9.1/tests/prefab/dynamic → ducktools_classbuilder-0.10.0/tests/prefab}/test_construction.py +4 -0
- ducktools_classbuilder-0.10.0/tests/prefab/test_creation.py +300 -0
- ducktools_classbuilder-0.10.0/tests/prefab/test_dunders.py +188 -0
- {ducktools_classbuilder-0.9.1/tests/prefab/shared → ducktools_classbuilder-0.10.0/tests/prefab}/test_funcs.py +22 -8
- {ducktools_classbuilder-0.9.1/tests/prefab/shared → ducktools_classbuilder-0.10.0/tests/prefab}/test_hint_syntax.py +23 -6
- ducktools_classbuilder-0.10.0/tests/prefab/test_inheritance.py +123 -0
- {ducktools_classbuilder-0.9.1/tests/prefab/shared → ducktools_classbuilder-0.10.0/tests/prefab}/test_init.py +121 -40
- ducktools_classbuilder-0.9.1/tests/prefab/dynamic/test_internals.py → ducktools_classbuilder-0.10.0/tests/prefab/test_internals_dict.py +1 -1
- {ducktools_classbuilder-0.9.1/tests/prefab/shared → ducktools_classbuilder-0.10.0/tests/prefab}/test_kw_only.py +51 -14
- {ducktools_classbuilder-0.9.1/tests/prefab/shared → ducktools_classbuilder-0.10.0/tests/prefab}/test_repr.py +48 -14
- {ducktools_classbuilder-0.9.1/tests/prefab/dynamic → ducktools_classbuilder-0.10.0/tests/prefab}/test_slotted_class.py +11 -2
- {ducktools_classbuilder-0.9.1/tests/prefab/dynamic → ducktools_classbuilder-0.10.0/tests/prefab}/test_subclass_implementation.py +7 -2
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/tests/test_core.py +14 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/tests/test_field_flags.py +6 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/tests/test_slotmakermeta.py +31 -18
- ducktools_classbuilder-0.10.0/uv.lock +910 -0
- ducktools_classbuilder-0.9.1/src/ducktools/classbuilder/_version.py +0 -2
- ducktools_classbuilder-0.9.1/src/ducktools_classbuilder.egg-info/requires.txt +0 -17
- ducktools_classbuilder-0.9.1/tests/conftest.py +0 -10
- ducktools_classbuilder-0.9.1/tests/prefab/shared/conftest.py +0 -17
- ducktools_classbuilder-0.9.1/tests/prefab/shared/examples/creation.py +0 -144
- ducktools_classbuilder-0.9.1/tests/prefab/shared/examples/creation_empty.py +0 -17
- ducktools_classbuilder-0.9.1/tests/prefab/shared/examples/dunders.py +0 -66
- ducktools_classbuilder-0.9.1/tests/prefab/shared/examples/fails/creation_2.py +0 -7
- ducktools_classbuilder-0.9.1/tests/prefab/shared/examples/fails/creation_3.py +0 -7
- ducktools_classbuilder-0.9.1/tests/prefab/shared/examples/fails/creation_5.py +0 -6
- ducktools_classbuilder-0.9.1/tests/prefab/shared/examples/fails/inheritance_1.py +0 -11
- ducktools_classbuilder-0.9.1/tests/prefab/shared/examples/fails/inheritance_2.py +0 -12
- ducktools_classbuilder-0.9.1/tests/prefab/shared/examples/funcs_prefabs.py +0 -20
- ducktools_classbuilder-0.9.1/tests/prefab/shared/examples/hint_syntax.py +0 -19
- ducktools_classbuilder-0.9.1/tests/prefab/shared/examples/inheritance.py +0 -65
- ducktools_classbuilder-0.9.1/tests/prefab/shared/examples/init_ex.py +0 -117
- ducktools_classbuilder-0.9.1/tests/prefab/shared/examples/kw_only.py +0 -49
- ducktools_classbuilder-0.9.1/tests/prefab/shared/examples/repr_func.py +0 -45
- ducktools_classbuilder-0.9.1/tests/prefab/shared/test_creation.py +0 -237
- ducktools_classbuilder-0.9.1/tests/prefab/shared/test_dunders.py +0 -120
- ducktools_classbuilder-0.9.1/tests/prefab/shared/test_inheritance.py +0 -61
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/.github/dependabot.yml +0 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/.gitignore +0 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/.readthedocs.yaml +0 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/LICENSE +0 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/MANIFEST.in +0 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/README.md +0 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs/Makefile +0 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs/api.md +0 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs/approach_vs_tool.md +0 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs/conf.py +0 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs/extension_examples.md +0 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs/generated_code.md +0 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs/index.md +0 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs/make.bat +0 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs/perf/performance_tests.md +0 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs/prefab/index.md +0 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs_code/docs_ex10_frozen_attributes.py +0 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs_code/docs_ex1_basic.py +0 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs_code/docs_ex2_register.py +0 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs_code/docs_ex3_iterable.py +0 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs_code/docs_ex5_frozen.py +0 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs_code/docs_ex7_posonly.py +0 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs_code/docs_ex8_converters.py +0 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs_code/index_example.py +0 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/setup.cfg +0 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/src/ducktools/classbuilder/annotations.pyi +0 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/src/ducktools/classbuilder/py.typed +0 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/src/ducktools_classbuilder.egg-info/dependency_links.txt +0 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/src/ducktools_classbuilder.egg-info/top_level.txt +0 -0
- {ducktools_classbuilder-0.9.1/tests/prefab/dynamic → ducktools_classbuilder-0.10.0/tests/prefab}/test_compare_attrib.py +0 -0
- {ducktools_classbuilder-0.9.1/tests/prefab/dynamic → ducktools_classbuilder-0.10.0/tests/prefab}/test_frozen.py +0 -0
- {ducktools_classbuilder-0.9.1/tests/prefab/dynamic → ducktools_classbuilder-0.10.0/tests/prefab}/test_pre_post_init.py +0 -0
- {ducktools_classbuilder-0.9.1/tests/prefab/dynamic → ducktools_classbuilder-0.10.0/tests/prefab}/test_private.py +0 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/tests/prefab/test_replace.py +0 -0
- {ducktools_classbuilder-0.9.1/tests/prefab/dynamic → ducktools_classbuilder-0.10.0/tests/prefab}/test_slots_novalues.py +0 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/tests/py312_tests/test_generic_annotations.py +0 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/tests/py314_tests/_test_support.py +0 -0
- {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/tests/py314_tests/test_forwardref_annotations.py +0 -0
{ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/.github/workflows/auto_test.yml
RENAMED
|
@@ -14,7 +14,7 @@ jobs:
|
|
|
14
14
|
fail-fast: false
|
|
15
15
|
matrix:
|
|
16
16
|
os: [ubuntu-latest]
|
|
17
|
-
python-version: ["3.14-dev", "3.13", "3.12", "3.11", "pypy-3.11", "3.10"
|
|
17
|
+
python-version: ["3.14-dev", "3.13", "3.12", "3.11", "pypy-3.11", "3.10"]
|
|
18
18
|
|
|
19
19
|
steps:
|
|
20
20
|
- uses: actions/checkout@v4
|
|
@@ -25,7 +25,7 @@ jobs:
|
|
|
25
25
|
- name: Install dependencies
|
|
26
26
|
run: |
|
|
27
27
|
python -m pip install --upgrade pip
|
|
28
|
-
python -m pip install -e .
|
|
28
|
+
python -m pip install -e . --group dev
|
|
29
29
|
- name: Test with pytest
|
|
30
30
|
run: |
|
|
31
31
|
pytest tests/ --cov=src/ --cov-report=term-missing
|
|
@@ -35,14 +35,14 @@ jobs:
|
|
|
35
35
|
|
|
36
36
|
steps:
|
|
37
37
|
- uses: actions/checkout@v4
|
|
38
|
-
- name: Set up Python 3.
|
|
38
|
+
- name: Set up Python 3.10
|
|
39
39
|
uses: actions/setup-python@v5
|
|
40
40
|
with:
|
|
41
|
-
python-version: "3.
|
|
41
|
+
python-version: "3.10"
|
|
42
42
|
- name: Install dependencies
|
|
43
43
|
run: |
|
|
44
44
|
python -m pip install --upgrade pip
|
|
45
|
-
python -m pip install -e .
|
|
45
|
+
python -m pip install -e . --group dev
|
|
46
46
|
- name: Check type stub files
|
|
47
47
|
run: |
|
|
48
48
|
python -m mypy.stubtest ducktools.classbuilder
|
{ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/.github/workflows/publish_to_pypi.yml
RENAMED
|
@@ -14,7 +14,7 @@ jobs:
|
|
|
14
14
|
- name: Set up Python
|
|
15
15
|
uses: actions/setup-python@v5
|
|
16
16
|
with:
|
|
17
|
-
python-version: "3.
|
|
17
|
+
python-version: "3.10"
|
|
18
18
|
- name: Install pypa/build
|
|
19
19
|
run: >-
|
|
20
20
|
python3 -m
|
|
@@ -69,7 +69,7 @@ jobs:
|
|
|
69
69
|
name: python-package-distributions
|
|
70
70
|
path: dist/
|
|
71
71
|
- name: Sign the dists with Sigstore
|
|
72
|
-
uses: sigstore/gh-action-sigstore-python@v3.0.
|
|
72
|
+
uses: sigstore/gh-action-sigstore-python@v3.0.1
|
|
73
73
|
with:
|
|
74
74
|
release-signing-artifacts: false
|
|
75
75
|
inputs: >-
|
|
@@ -15,7 +15,7 @@ jobs:
|
|
|
15
15
|
- name: Set up Python
|
|
16
16
|
uses: actions/setup-python@v5
|
|
17
17
|
with:
|
|
18
|
-
python-version: "3.
|
|
18
|
+
python-version: "3.10"
|
|
19
19
|
- name: Install pypa/build
|
|
20
20
|
run: >-
|
|
21
21
|
python3 -m
|
|
@@ -52,4 +52,4 @@ jobs:
|
|
|
52
52
|
- name: Publish distribution 📦 to TestPyPI
|
|
53
53
|
uses: pypa/gh-action-pypi-publish@release/v1
|
|
54
54
|
with:
|
|
55
|
-
repository-url: https://test.pypi.org/legacy/
|
|
55
|
+
repository-url: https://test.pypi.org/legacy/
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: ducktools-classbuilder
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: Toolkit for creating class boilerplate generators
|
|
5
5
|
Author: David C Ellis
|
|
6
6
|
Project-URL: Homepage, https://github.com/davidcellis/ducktools-classbuilder
|
|
7
7
|
Classifier: Development Status :: 4 - Beta
|
|
8
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
9
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
10
8
|
Classifier: Programming Language :: Python :: 3.10
|
|
11
9
|
Classifier: Programming Language :: Python :: 3.11
|
|
12
10
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -14,22 +12,14 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
14
12
|
Classifier: Programming Language :: Python :: 3.14
|
|
15
13
|
Classifier: Operating System :: OS Independent
|
|
16
14
|
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
-
Requires-Python: >=3.
|
|
15
|
+
Requires-Python: >=3.10
|
|
18
16
|
Description-Content-Type: text/markdown
|
|
19
17
|
License-File: LICENSE
|
|
20
|
-
Provides-Extra: testing
|
|
21
|
-
Requires-Dist: pytest>=8.2; extra == "testing"
|
|
22
|
-
Requires-Dist: pytest-cov; extra == "testing"
|
|
23
|
-
Requires-Dist: typing_extensions; extra == "testing"
|
|
24
|
-
Provides-Extra: type-checking
|
|
25
|
-
Requires-Dist: mypy; extra == "type-checking"
|
|
26
|
-
Provides-Extra: performance-tests
|
|
27
|
-
Requires-Dist: attrs; extra == "performance-tests"
|
|
28
|
-
Requires-Dist: pydantic; extra == "performance-tests"
|
|
29
18
|
Provides-Extra: docs
|
|
30
|
-
Requires-Dist: sphinx; extra == "docs"
|
|
31
|
-
Requires-Dist: myst-parser; extra == "docs"
|
|
32
|
-
Requires-Dist: sphinx_rtd_theme; extra == "docs"
|
|
19
|
+
Requires-Dist: sphinx>=8.1; extra == "docs"
|
|
20
|
+
Requires-Dist: myst-parser>=4.0; extra == "docs"
|
|
21
|
+
Requires-Dist: sphinx_rtd_theme>=3.0; extra == "docs"
|
|
22
|
+
Dynamic: license-file
|
|
33
23
|
|
|
34
24
|
# Ducktools: Class Builder #
|
|
35
25
|
|
|
@@ -191,14 +191,13 @@ def reportclass(cls):
|
|
|
191
191
|
slot_gatherer = dtbuild.make_slot_gatherer(CustomField)
|
|
192
192
|
|
|
193
193
|
|
|
194
|
-
class ReportClass(metaclass=dtbuild.SlotMakerMeta):
|
|
194
|
+
class ReportClass(metaclass=dtbuild.SlotMakerMeta, gatherer=fields_attribute_gatherer):
|
|
195
195
|
__slots__ = {}
|
|
196
|
-
|
|
197
|
-
|
|
196
|
+
|
|
198
197
|
def __init_subclass__(cls):
|
|
199
|
-
# Check if the metaclass has
|
|
200
|
-
|
|
201
|
-
gatherer =
|
|
198
|
+
# Check if the metaclass has pre-gathered data
|
|
199
|
+
pre_gathered = dtbuild.GATHERED_DATA in vars(cls)
|
|
200
|
+
gatherer = dtbuild.pre_gathered_gatherer if pre_gathered else fields_attribute_gatherer
|
|
202
201
|
methods = {
|
|
203
202
|
dtbuild.eq_maker,
|
|
204
203
|
dtbuild.repr_maker,
|
{ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs_code/docs_ex9_annotated.py
RENAMED
|
@@ -6,9 +6,10 @@ from ducktools.classbuilder import (
|
|
|
6
6
|
default_methods,
|
|
7
7
|
get_fields,
|
|
8
8
|
get_methods,
|
|
9
|
-
|
|
9
|
+
pre_gathered_gatherer,
|
|
10
10
|
Field,
|
|
11
11
|
SlotMakerMeta,
|
|
12
|
+
GATHERED_DATA,
|
|
12
13
|
NOTHING,
|
|
13
14
|
)
|
|
14
15
|
|
|
@@ -110,16 +111,15 @@ def annotatedclass(cls=None, *, kw_only=False):
|
|
|
110
111
|
|
|
111
112
|
|
|
112
113
|
# As a base class with slots
|
|
113
|
-
class AnnotatedClass(metaclass=SlotMakerMeta):
|
|
114
|
-
|
|
115
|
-
_meta_gatherer = annotated_gatherer
|
|
116
|
-
|
|
114
|
+
class AnnotatedClass(metaclass=SlotMakerMeta, gatherer=annotated_gatherer):
|
|
115
|
+
|
|
117
116
|
def __init_subclass__(cls, kw_only=False, **kwargs):
|
|
117
|
+
pre_gathered = GATHERED_DATA in cls.__dict__
|
|
118
118
|
slots = "__slots__" in cls.__dict__
|
|
119
119
|
|
|
120
120
|
# if slots is True then fields will already be present in __slots__
|
|
121
121
|
# Use the slot_gatherer for this case
|
|
122
|
-
gatherer =
|
|
122
|
+
gatherer = pre_gathered_gatherer if pre_gathered else annotated_gatherer
|
|
123
123
|
|
|
124
124
|
builder(
|
|
125
125
|
cls,
|
|
@@ -139,10 +139,10 @@ if __name__ == "__main__":
|
|
|
139
139
|
class X:
|
|
140
140
|
x: str
|
|
141
141
|
y: ClassVar[str] = "This should be ignored"
|
|
142
|
-
z: Annotated[ClassVar[str], "Should be ignored"] = "This should also be ignored"
|
|
143
|
-
a: Annotated[int, NO_INIT] = "Not In __init__ signature"
|
|
142
|
+
z: Annotated[ClassVar[str], "Should be ignored"] = "This should also be ignored" # type: ignore
|
|
143
|
+
a: Annotated[int, NO_INIT] = "Not In __init__ signature" # type: ignore
|
|
144
144
|
b: Annotated[str, NO_REPR] = "Not In Repr"
|
|
145
|
-
c: Annotated[list[str], NO_COMPARE] = Field(default_factory=list)
|
|
145
|
+
c: Annotated[list[str], NO_COMPARE] = Field(default_factory=list) # type: ignore
|
|
146
146
|
d: Annotated[str, IGNORE_ALL] = "Not Anywhere"
|
|
147
147
|
e: Annotated[str, KW_ONLY, NO_COMPARE]
|
|
148
148
|
|
|
@@ -150,23 +150,23 @@ if __name__ == "__main__":
|
|
|
150
150
|
class Y(AnnotatedClass):
|
|
151
151
|
x: str
|
|
152
152
|
y: ClassVar[str] = "This should be ignored"
|
|
153
|
-
z: Annotated[ClassVar[str], "Should be ignored"] = "This should also be ignored"
|
|
154
|
-
a: Annotated[int, NO_INIT] = "Not In __init__ signature"
|
|
153
|
+
z: Annotated[ClassVar[str], "Should be ignored"] = "This should also be ignored" # type: ignore
|
|
154
|
+
a: Annotated[int, NO_INIT] = "Not In __init__ signature" # type: ignore
|
|
155
155
|
b: Annotated[str, NO_REPR] = "Not In Repr"
|
|
156
|
-
c: Annotated[list[str], NO_COMPARE] = Field(default_factory=list)
|
|
156
|
+
c: Annotated[list[str], NO_COMPARE] = Field(default_factory=list) # type: ignore
|
|
157
157
|
d: Annotated[str, IGNORE_ALL] = "Not Anywhere"
|
|
158
158
|
e: Annotated[str, KW_ONLY, NO_COMPARE]
|
|
159
159
|
|
|
160
160
|
|
|
161
161
|
# Unslotted Demo
|
|
162
|
-
ex = X("Value of x", e="Value of e")
|
|
162
|
+
ex = X("Value of x", e="Value of e") # type: ignore
|
|
163
163
|
print(ex, "\n")
|
|
164
164
|
|
|
165
165
|
pp(get_fields(X))
|
|
166
166
|
print("\n")
|
|
167
167
|
|
|
168
168
|
# Slotted Demo
|
|
169
|
-
ex = Y("Value of x", e="Value of e")
|
|
169
|
+
ex = Y("Value of x", e="Value of e") # type: ignore
|
|
170
170
|
print(ex, "\n")
|
|
171
171
|
|
|
172
172
|
print(f"Slots: {Y.__dict__.get('__slots__')}")
|
|
@@ -115,14 +115,13 @@ def reportclass(cls):
|
|
|
115
115
|
slot_gatherer = dtbuild.make_slot_gatherer(CustomField)
|
|
116
116
|
|
|
117
117
|
|
|
118
|
-
class ReportClass(metaclass=dtbuild.SlotMakerMeta):
|
|
118
|
+
class ReportClass(metaclass=dtbuild.SlotMakerMeta, gatherer=fields_attribute_gatherer):
|
|
119
119
|
__slots__ = {}
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
|
|
122
121
|
def __init_subclass__(cls):
|
|
123
|
-
# Check if the metaclass has
|
|
124
|
-
|
|
125
|
-
gatherer =
|
|
122
|
+
# Check if the metaclass has pre-gathered data
|
|
123
|
+
pre_gathered = dtbuild.GATHERED_DATA in vars(cls)
|
|
124
|
+
gatherer = dtbuild.pre_gathered_gatherer if pre_gathered else fields_attribute_gatherer
|
|
126
125
|
methods = {
|
|
127
126
|
dtbuild.eq_maker,
|
|
128
127
|
dtbuild.repr_maker,
|
|
@@ -130,7 +129,6 @@ class ReportClass(metaclass=dtbuild.SlotMakerMeta):
|
|
|
130
129
|
report_maker
|
|
131
130
|
}
|
|
132
131
|
|
|
133
|
-
# The class may still have slots unrelated to code generation
|
|
134
132
|
slotted = "__slots__" in vars(cls)
|
|
135
133
|
flags = {"slotted": slotted}
|
|
136
134
|
|
|
@@ -12,11 +12,9 @@ authors = [
|
|
|
12
12
|
{ name="David C Ellis" },
|
|
13
13
|
]
|
|
14
14
|
readme="README.md"
|
|
15
|
-
requires-python = ">=3.
|
|
15
|
+
requires-python = ">=3.10"
|
|
16
16
|
classifiers = [
|
|
17
17
|
"Development Status :: 4 - Beta",
|
|
18
|
-
"Programming Language :: Python :: 3.8",
|
|
19
|
-
"Programming Language :: Python :: 3.9",
|
|
20
18
|
"Programming Language :: Python :: 3.10",
|
|
21
19
|
"Programming Language :: Python :: 3.11",
|
|
22
20
|
"Programming Language :: Python :: 3.12",
|
|
@@ -28,10 +26,23 @@ classifiers = [
|
|
|
28
26
|
dynamic = ['version']
|
|
29
27
|
|
|
30
28
|
[project.optional-dependencies]
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
# Needed for the current readthedocs.yaml
|
|
30
|
+
docs = [
|
|
31
|
+
"sphinx>=8.1",
|
|
32
|
+
"myst-parser>=4.0",
|
|
33
|
+
"sphinx_rtd_theme>=3.0",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[dependency-groups]
|
|
37
|
+
dev = [
|
|
38
|
+
"pytest>=8.4",
|
|
39
|
+
"pytest-cov>=6.1",
|
|
40
|
+
"mypy>=1.16",
|
|
41
|
+
]
|
|
42
|
+
performance = [
|
|
43
|
+
"attrs>=25.0",
|
|
44
|
+
"pydantic>=2.11",
|
|
45
|
+
]
|
|
35
46
|
|
|
36
47
|
[tool.setuptools.packages.find]
|
|
37
48
|
where = ["src"]
|
|
@@ -51,3 +62,8 @@ addopts= "--cov=src/ --cov-report=term-missing"
|
|
|
51
62
|
testpaths = [
|
|
52
63
|
"tests",
|
|
53
64
|
]
|
|
65
|
+
|
|
66
|
+
[tool.mypy]
|
|
67
|
+
# A combination of types in stubs and tests using dataclass syntax
|
|
68
|
+
# means that there are a number of annotations in otherwise unannotated areas
|
|
69
|
+
disable_error_code = ["annotation-unchecked"]
|
|
@@ -38,7 +38,7 @@ from ._version import __version__, __version_tuple__ # noqa: F401
|
|
|
38
38
|
# Change this name if you make heavy modifications
|
|
39
39
|
INTERNALS_DICT = "__classbuilder_internals__"
|
|
40
40
|
META_GATHERER_NAME = "_meta_gatherer"
|
|
41
|
-
|
|
41
|
+
GATHERED_DATA = "__classbuilder_gathered_fields__"
|
|
42
42
|
|
|
43
43
|
# If testing, make Field classes frozen to make sure attributes are not
|
|
44
44
|
# overwritten. When running this is a performance penalty so it is not required.
|
|
@@ -114,12 +114,16 @@ FIELD_NOTHING = _NothingType("FIELD")
|
|
|
114
114
|
# KW_ONLY sentinel 'type' to use to indicate all subsequent attributes are
|
|
115
115
|
# keyword only
|
|
116
116
|
# noinspection PyPep8Naming
|
|
117
|
-
class
|
|
117
|
+
class _KW_ONLY_META(type):
|
|
118
118
|
def __repr__(self):
|
|
119
|
-
return "<KW_ONLY Sentinel
|
|
119
|
+
return "<KW_ONLY Sentinel>"
|
|
120
120
|
|
|
121
121
|
|
|
122
|
-
KW_ONLY
|
|
122
|
+
class KW_ONLY(metaclass=_KW_ONLY_META):
|
|
123
|
+
"""
|
|
124
|
+
Sentinel Class to indicate that variables declared after
|
|
125
|
+
this sentinel are to be converted to KW_ONLY arguments.
|
|
126
|
+
"""
|
|
123
127
|
|
|
124
128
|
|
|
125
129
|
class GeneratedCode:
|
|
@@ -571,21 +575,25 @@ class SlotMakerMeta(type):
|
|
|
571
575
|
|
|
572
576
|
Will not convert `ClassVar` hinted values.
|
|
573
577
|
"""
|
|
574
|
-
def __new__(cls, name, bases, ns, slots=True, **kwargs):
|
|
578
|
+
def __new__(cls, name, bases, ns, slots=True, gatherer=None, **kwargs):
|
|
575
579
|
# This should only run if slots=True is declared
|
|
576
580
|
# and __slots__ have not already been defined
|
|
577
|
-
if slots and "__slots__" not in ns:
|
|
581
|
+
if slots and "__slots__" not in ns:
|
|
578
582
|
# Check if a different gatherer has been set in any base classes
|
|
579
583
|
# Default to unified gatherer
|
|
580
|
-
gatherer
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
584
|
+
if gatherer is None:
|
|
585
|
+
gatherer = ns.get(META_GATHERER_NAME, None)
|
|
586
|
+
if not gatherer:
|
|
587
|
+
for base in bases:
|
|
588
|
+
if g := getattr(base, META_GATHERER_NAME, None):
|
|
589
|
+
gatherer = g
|
|
590
|
+
break
|
|
591
|
+
|
|
592
|
+
if not gatherer:
|
|
593
|
+
gatherer = unified_gatherer
|
|
586
594
|
|
|
587
|
-
|
|
588
|
-
|
|
595
|
+
# Set the gatherer in the namespace
|
|
596
|
+
ns[META_GATHERER_NAME] = gatherer
|
|
589
597
|
|
|
590
598
|
# Obtain slots from annotations or attributes
|
|
591
599
|
cls_fields, cls_modifications = gatherer(ns)
|
|
@@ -595,14 +603,54 @@ class SlotMakerMeta(type):
|
|
|
595
603
|
else:
|
|
596
604
|
ns[k] = v
|
|
597
605
|
|
|
606
|
+
slots = {}
|
|
607
|
+
fields = {}
|
|
608
|
+
|
|
609
|
+
for k, v in cls_fields.items():
|
|
610
|
+
slots[k] = v.doc
|
|
611
|
+
if k not in {"__weakref__", "__dict__"}:
|
|
612
|
+
fields[k] = v
|
|
613
|
+
|
|
598
614
|
# Place slots *after* everything else to be safe
|
|
599
|
-
ns["__slots__"] =
|
|
615
|
+
ns["__slots__"] = slots
|
|
616
|
+
|
|
617
|
+
# Place pre-gathered field data - modifications are already applied
|
|
618
|
+
modifications = {}
|
|
619
|
+
ns[GATHERED_DATA] = fields, modifications
|
|
620
|
+
|
|
621
|
+
else:
|
|
622
|
+
if gatherer is not None:
|
|
623
|
+
ns[META_GATHERER_NAME] = gatherer
|
|
600
624
|
|
|
601
625
|
new_cls = super().__new__(cls, name, bases, ns, **kwargs)
|
|
602
626
|
|
|
603
627
|
return new_cls
|
|
604
628
|
|
|
605
629
|
|
|
630
|
+
# This class is set up before fields as it will be used to generate the Fields
|
|
631
|
+
# for Field itself so Field can have generated __eq__, __repr__ and other methods
|
|
632
|
+
class GatheredFields:
|
|
633
|
+
"""
|
|
634
|
+
Helper class to store gathered field data
|
|
635
|
+
"""
|
|
636
|
+
__slots__ = ("fields", "modifications")
|
|
637
|
+
|
|
638
|
+
def __init__(self, fields, modifications):
|
|
639
|
+
self.fields = fields
|
|
640
|
+
self.modifications = modifications
|
|
641
|
+
|
|
642
|
+
def __eq__(self, other):
|
|
643
|
+
if type(self) is type(other):
|
|
644
|
+
return self.fields == other.fields and self.modifications == other.modifications
|
|
645
|
+
|
|
646
|
+
def __repr__(self):
|
|
647
|
+
return f"{type(self).__name__}(fields={self.fields!r}, modifications={self.modifications!r})"
|
|
648
|
+
|
|
649
|
+
def __call__(self, cls_dict):
|
|
650
|
+
# cls_dict will be provided, but isn't needed
|
|
651
|
+
return self.fields, self.modifications
|
|
652
|
+
|
|
653
|
+
|
|
606
654
|
# The Field class can finally be defined.
|
|
607
655
|
# The __init__ method has to be written manually so Fields can be created
|
|
608
656
|
# However after this, the other methods can be generated.
|
|
@@ -628,17 +676,18 @@ class Field(metaclass=SlotMakerMeta):
|
|
|
628
676
|
:param compare: Include in the class __eq__.
|
|
629
677
|
:param kw_only: Make this a keyword only parameter in __init__.
|
|
630
678
|
"""
|
|
631
|
-
|
|
632
|
-
#
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
679
|
+
|
|
680
|
+
# Plain slots are required as part of bootstrapping
|
|
681
|
+
# This prevents SlotMakerMeta from trying to generate 'Field's
|
|
682
|
+
__slots__ = (
|
|
683
|
+
"default",
|
|
684
|
+
"default_factory",
|
|
685
|
+
"type",
|
|
686
|
+
"doc",
|
|
687
|
+
"init",
|
|
688
|
+
"repr",
|
|
689
|
+
"compare",
|
|
690
|
+
"kw_only",
|
|
642
691
|
)
|
|
643
692
|
|
|
644
693
|
# noinspection PyShadowingBuiltins
|
|
@@ -672,6 +721,7 @@ class Field(metaclass=SlotMakerMeta):
|
|
|
672
721
|
self.validate_field()
|
|
673
722
|
|
|
674
723
|
def __init_subclass__(cls, frozen=False):
|
|
724
|
+
# Subclasses of Field can be created as if they are dataclasses
|
|
675
725
|
field_methods = {_field_init_maker, repr_maker, eq_maker}
|
|
676
726
|
if frozen or _UNDER_TESTING:
|
|
677
727
|
field_methods.update({frozen_setattr_maker, frozen_delattr_maker})
|
|
@@ -707,6 +757,66 @@ class Field(metaclass=SlotMakerMeta):
|
|
|
707
757
|
return cls(**argument_dict)
|
|
708
758
|
|
|
709
759
|
|
|
760
|
+
def _build_field():
|
|
761
|
+
# Complete the construction of the Field class
|
|
762
|
+
field_docs = {
|
|
763
|
+
"default": "Standard default value to be used for attributes with this field.",
|
|
764
|
+
"default_factory":
|
|
765
|
+
"A zero-argument function to be called to generate a default value, "
|
|
766
|
+
"useful for mutable obects like lists.",
|
|
767
|
+
"type": "The type of the attribute to be assigned by this field.",
|
|
768
|
+
"doc":
|
|
769
|
+
"The documentation for the attribute that appears when calling "
|
|
770
|
+
"help(...) on the class. (Only in slotted classes).",
|
|
771
|
+
"init": "Include this attribute in the class __init__ parameters.",
|
|
772
|
+
"repr": "Include this attribute in the class __repr__",
|
|
773
|
+
"compare": "Include this attribute in the class __eq__ method",
|
|
774
|
+
"kw_only": "Make this a keyword only parameter in __init__",
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
fields = {
|
|
778
|
+
"default": Field(default=NOTHING, doc=field_docs["default"]),
|
|
779
|
+
"default_factory": Field(default=NOTHING, doc=field_docs["default_factory"]),
|
|
780
|
+
"type": Field(default=NOTHING, doc=field_docs["type"]),
|
|
781
|
+
"doc": Field(default=None, doc=field_docs["doc"]),
|
|
782
|
+
"init": Field(default=True, doc=field_docs["init"]),
|
|
783
|
+
"repr": Field(default=True, doc=field_docs["repr"]),
|
|
784
|
+
"compare": Field(default=True, doc=field_docs["compare"]),
|
|
785
|
+
"kw_only": Field(default=False, doc=field_docs["kw_only"])
|
|
786
|
+
}
|
|
787
|
+
modifications = {"__slots__": field_docs}
|
|
788
|
+
|
|
789
|
+
field_methods = {repr_maker, eq_maker}
|
|
790
|
+
if _UNDER_TESTING:
|
|
791
|
+
field_methods.update({frozen_setattr_maker, frozen_delattr_maker})
|
|
792
|
+
|
|
793
|
+
builder(
|
|
794
|
+
Field,
|
|
795
|
+
gatherer=GatheredFields(fields, modifications),
|
|
796
|
+
methods=field_methods,
|
|
797
|
+
flags={"slotted": True, "kw_only": True},
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
_build_field()
|
|
802
|
+
del _build_field
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def pre_gathered_gatherer(cls_or_ns):
|
|
806
|
+
"""
|
|
807
|
+
Retrieve fields previously gathered by SlotMakerMeta
|
|
808
|
+
|
|
809
|
+
:param cls_or_ns: Class to gather field information from (or class namespace)
|
|
810
|
+
:return: dict of field_name: Field(...) and modifications to be performed by the builder
|
|
811
|
+
"""
|
|
812
|
+
if isinstance(cls_or_ns, (_MappingProxyType, dict)):
|
|
813
|
+
cls_dict = cls_or_ns
|
|
814
|
+
else:
|
|
815
|
+
cls_dict = cls_or_ns.__dict__
|
|
816
|
+
|
|
817
|
+
return cls_dict[GATHERED_DATA]
|
|
818
|
+
|
|
819
|
+
|
|
710
820
|
def make_slot_gatherer(field_type=Field):
|
|
711
821
|
"""
|
|
712
822
|
Create a new annotation gatherer that will work with `Field` instances
|
|
@@ -721,7 +831,7 @@ def make_slot_gatherer(field_type=Field):
|
|
|
721
831
|
Gather field information for class generation based on __slots__
|
|
722
832
|
|
|
723
833
|
:param cls_or_ns: Class to gather field information from (or class namespace)
|
|
724
|
-
:return: dict of field_name: Field(...)
|
|
834
|
+
:return: dict of field_name: Field(...) and modifications to be performed by the builder
|
|
725
835
|
"""
|
|
726
836
|
if isinstance(cls_or_ns, (_MappingProxyType, dict)):
|
|
727
837
|
cls_dict = cls_or_ns
|
|
@@ -883,6 +993,7 @@ def make_field_gatherer(
|
|
|
883
993
|
def make_unified_gatherer(
|
|
884
994
|
field_type=Field,
|
|
885
995
|
leave_default_values=False,
|
|
996
|
+
ignore_annotations=False,
|
|
886
997
|
):
|
|
887
998
|
"""
|
|
888
999
|
Create a gatherer that will work via first slots, then
|
|
@@ -891,6 +1002,7 @@ def make_unified_gatherer(
|
|
|
891
1002
|
|
|
892
1003
|
:param field_type: The field class to use for gathering
|
|
893
1004
|
:param leave_default_values: leave default values in place
|
|
1005
|
+
:param ignore_annotations: don't attempt to read annotations
|
|
894
1006
|
:return: gatherer function
|
|
895
1007
|
"""
|
|
896
1008
|
slot_g = make_slot_gatherer(field_type)
|
|
@@ -903,27 +1015,35 @@ def make_unified_gatherer(
|
|
|
903
1015
|
else:
|
|
904
1016
|
cls_dict = cls_or_ns.__dict__
|
|
905
1017
|
|
|
1018
|
+
cls_gathered = cls_dict.get(GATHERED_DATA)
|
|
1019
|
+
if cls_gathered:
|
|
1020
|
+
return pre_gathered_gatherer(cls_dict)
|
|
1021
|
+
|
|
906
1022
|
cls_slots = cls_dict.get("__slots__")
|
|
907
1023
|
|
|
908
1024
|
if isinstance(cls_slots, SlotFields):
|
|
909
1025
|
return slot_g(cls_dict)
|
|
910
1026
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1027
|
+
if ignore_annotations:
|
|
1028
|
+
return attrib_g(cls_dict)
|
|
1029
|
+
else:
|
|
1030
|
+
# To choose between annotation and attribute gatherers
|
|
1031
|
+
# compare sets of names.
|
|
1032
|
+
# Don't bother evaluating string annotations, as we only need names
|
|
1033
|
+
cls_annotations = get_ns_annotations(cls_dict)
|
|
1034
|
+
cls_attributes = {
|
|
1035
|
+
k: v for k, v in cls_dict.items() if isinstance(v, field_type)
|
|
1036
|
+
}
|
|
918
1037
|
|
|
919
|
-
|
|
920
|
-
|
|
1038
|
+
cls_annotation_names = cls_annotations.keys()
|
|
1039
|
+
cls_attribute_names = cls_attributes.keys()
|
|
921
1040
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
1041
|
+
if set(cls_annotation_names).issuperset(set(cls_attribute_names)):
|
|
1042
|
+
# All `Field` values have annotations, so use annotation gatherer
|
|
1043
|
+
return anno_g(cls_dict)
|
|
925
1044
|
|
|
926
|
-
|
|
1045
|
+
return attrib_g(cls_dict)
|
|
1046
|
+
|
|
927
1047
|
return field_unified_gatherer
|
|
928
1048
|
|
|
929
1049
|
|
|
@@ -935,19 +1055,6 @@ annotation_gatherer = make_annotation_gatherer()
|
|
|
935
1055
|
unified_gatherer = make_unified_gatherer()
|
|
936
1056
|
|
|
937
1057
|
|
|
938
|
-
# Now the gatherers have been defined, add __repr__ and __eq__ to Field.
|
|
939
|
-
_field_methods = {repr_maker, eq_maker}
|
|
940
|
-
if _UNDER_TESTING:
|
|
941
|
-
_field_methods.update({frozen_setattr_maker, frozen_delattr_maker})
|
|
942
|
-
|
|
943
|
-
builder(
|
|
944
|
-
Field,
|
|
945
|
-
gatherer=slot_gatherer,
|
|
946
|
-
methods=_field_methods,
|
|
947
|
-
flags={"slotted": True, "kw_only": True},
|
|
948
|
-
)
|
|
949
|
-
|
|
950
|
-
|
|
951
1058
|
def check_argument_order(cls):
|
|
952
1059
|
"""
|
|
953
1060
|
Raise a SyntaxError if the argument order will be invalid for a generated
|
|
@@ -990,17 +1097,3 @@ def slotclass(cls=None, /, *, methods=default_methods, syntax_check=True):
|
|
|
990
1097
|
check_argument_order(cls)
|
|
991
1098
|
|
|
992
1099
|
return cls
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
@slotclass
|
|
996
|
-
class GatheredFields:
|
|
997
|
-
"""
|
|
998
|
-
A helper gatherer for fields that have been gathered externally.
|
|
999
|
-
"""
|
|
1000
|
-
__slots__ = SlotFields(
|
|
1001
|
-
fields=Field(),
|
|
1002
|
-
modifications=Field(),
|
|
1003
|
-
)
|
|
1004
|
-
|
|
1005
|
-
def __call__(self, cls):
|
|
1006
|
-
return self.fields, self.modifications
|