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.

Files changed (97) hide show
  1. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/.github/workflows/auto_test.yml +5 -5
  2. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/.github/workflows/publish_to_pypi.yml +2 -2
  3. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/.github/workflows/publish_to_testpypi.yml +2 -2
  4. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/PKG-INFO +7 -17
  5. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs/tutorial.md +5 -6
  6. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs_code/docs_ex9_annotated.py +14 -14
  7. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs_code/tutorial_code.py +5 -7
  8. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/pyproject.toml +23 -7
  9. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/src/ducktools/classbuilder/__init__.py +160 -67
  10. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/src/ducktools/classbuilder/__init__.pyi +16 -9
  11. ducktools_classbuilder-0.10.0/src/ducktools/classbuilder/_version.py +2 -0
  12. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/src/ducktools/classbuilder/annotations.py +2 -13
  13. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/src/ducktools/classbuilder/prefab.py +1 -2
  14. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/src/ducktools/classbuilder/prefab.pyi +1 -0
  15. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/src/ducktools_classbuilder.egg-info/PKG-INFO +7 -17
  16. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/src/ducktools_classbuilder.egg-info/SOURCES.txt +19 -32
  17. ducktools_classbuilder-0.10.0/src/ducktools_classbuilder.egg-info/requires.txt +5 -0
  18. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/tests/annotations/test_annotated.py +5 -3
  19. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/tests/annotations/test_annotations_module.py +7 -7
  20. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/tests/annotations/test_future_annotations.py +3 -0
  21. ducktools_classbuilder-0.10.0/tests/conftest.py +20 -0
  22. ducktools_classbuilder-0.10.0/tests/helpers/utils.py +7 -0
  23. {ducktools_classbuilder-0.9.1/tests/prefab/dynamic → ducktools_classbuilder-0.10.0/tests/prefab}/test_construction.py +4 -0
  24. ducktools_classbuilder-0.10.0/tests/prefab/test_creation.py +300 -0
  25. ducktools_classbuilder-0.10.0/tests/prefab/test_dunders.py +188 -0
  26. {ducktools_classbuilder-0.9.1/tests/prefab/shared → ducktools_classbuilder-0.10.0/tests/prefab}/test_funcs.py +22 -8
  27. {ducktools_classbuilder-0.9.1/tests/prefab/shared → ducktools_classbuilder-0.10.0/tests/prefab}/test_hint_syntax.py +23 -6
  28. ducktools_classbuilder-0.10.0/tests/prefab/test_inheritance.py +123 -0
  29. {ducktools_classbuilder-0.9.1/tests/prefab/shared → ducktools_classbuilder-0.10.0/tests/prefab}/test_init.py +121 -40
  30. ducktools_classbuilder-0.9.1/tests/prefab/dynamic/test_internals.py → ducktools_classbuilder-0.10.0/tests/prefab/test_internals_dict.py +1 -1
  31. {ducktools_classbuilder-0.9.1/tests/prefab/shared → ducktools_classbuilder-0.10.0/tests/prefab}/test_kw_only.py +51 -14
  32. {ducktools_classbuilder-0.9.1/tests/prefab/shared → ducktools_classbuilder-0.10.0/tests/prefab}/test_repr.py +48 -14
  33. {ducktools_classbuilder-0.9.1/tests/prefab/dynamic → ducktools_classbuilder-0.10.0/tests/prefab}/test_slotted_class.py +11 -2
  34. {ducktools_classbuilder-0.9.1/tests/prefab/dynamic → ducktools_classbuilder-0.10.0/tests/prefab}/test_subclass_implementation.py +7 -2
  35. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/tests/test_core.py +14 -0
  36. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/tests/test_field_flags.py +6 -0
  37. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/tests/test_slotmakermeta.py +31 -18
  38. ducktools_classbuilder-0.10.0/uv.lock +910 -0
  39. ducktools_classbuilder-0.9.1/src/ducktools/classbuilder/_version.py +0 -2
  40. ducktools_classbuilder-0.9.1/src/ducktools_classbuilder.egg-info/requires.txt +0 -17
  41. ducktools_classbuilder-0.9.1/tests/conftest.py +0 -10
  42. ducktools_classbuilder-0.9.1/tests/prefab/shared/conftest.py +0 -17
  43. ducktools_classbuilder-0.9.1/tests/prefab/shared/examples/creation.py +0 -144
  44. ducktools_classbuilder-0.9.1/tests/prefab/shared/examples/creation_empty.py +0 -17
  45. ducktools_classbuilder-0.9.1/tests/prefab/shared/examples/dunders.py +0 -66
  46. ducktools_classbuilder-0.9.1/tests/prefab/shared/examples/fails/creation_2.py +0 -7
  47. ducktools_classbuilder-0.9.1/tests/prefab/shared/examples/fails/creation_3.py +0 -7
  48. ducktools_classbuilder-0.9.1/tests/prefab/shared/examples/fails/creation_5.py +0 -6
  49. ducktools_classbuilder-0.9.1/tests/prefab/shared/examples/fails/inheritance_1.py +0 -11
  50. ducktools_classbuilder-0.9.1/tests/prefab/shared/examples/fails/inheritance_2.py +0 -12
  51. ducktools_classbuilder-0.9.1/tests/prefab/shared/examples/funcs_prefabs.py +0 -20
  52. ducktools_classbuilder-0.9.1/tests/prefab/shared/examples/hint_syntax.py +0 -19
  53. ducktools_classbuilder-0.9.1/tests/prefab/shared/examples/inheritance.py +0 -65
  54. ducktools_classbuilder-0.9.1/tests/prefab/shared/examples/init_ex.py +0 -117
  55. ducktools_classbuilder-0.9.1/tests/prefab/shared/examples/kw_only.py +0 -49
  56. ducktools_classbuilder-0.9.1/tests/prefab/shared/examples/repr_func.py +0 -45
  57. ducktools_classbuilder-0.9.1/tests/prefab/shared/test_creation.py +0 -237
  58. ducktools_classbuilder-0.9.1/tests/prefab/shared/test_dunders.py +0 -120
  59. ducktools_classbuilder-0.9.1/tests/prefab/shared/test_inheritance.py +0 -61
  60. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/.github/dependabot.yml +0 -0
  61. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/.gitignore +0 -0
  62. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/.readthedocs.yaml +0 -0
  63. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/LICENSE +0 -0
  64. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/MANIFEST.in +0 -0
  65. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/README.md +0 -0
  66. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs/Makefile +0 -0
  67. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs/api.md +0 -0
  68. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs/approach_vs_tool.md +0 -0
  69. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs/conf.py +0 -0
  70. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs/extension_examples.md +0 -0
  71. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs/generated_code.md +0 -0
  72. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs/index.md +0 -0
  73. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs/make.bat +0 -0
  74. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs/perf/performance_tests.md +0 -0
  75. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs/prefab/index.md +0 -0
  76. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs_code/docs_ex10_frozen_attributes.py +0 -0
  77. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs_code/docs_ex1_basic.py +0 -0
  78. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs_code/docs_ex2_register.py +0 -0
  79. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs_code/docs_ex3_iterable.py +0 -0
  80. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs_code/docs_ex5_frozen.py +0 -0
  81. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs_code/docs_ex7_posonly.py +0 -0
  82. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs_code/docs_ex8_converters.py +0 -0
  83. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/docs_code/index_example.py +0 -0
  84. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/setup.cfg +0 -0
  85. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/src/ducktools/classbuilder/annotations.pyi +0 -0
  86. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/src/ducktools/classbuilder/py.typed +0 -0
  87. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/src/ducktools_classbuilder.egg-info/dependency_links.txt +0 -0
  88. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/src/ducktools_classbuilder.egg-info/top_level.txt +0 -0
  89. {ducktools_classbuilder-0.9.1/tests/prefab/dynamic → ducktools_classbuilder-0.10.0/tests/prefab}/test_compare_attrib.py +0 -0
  90. {ducktools_classbuilder-0.9.1/tests/prefab/dynamic → ducktools_classbuilder-0.10.0/tests/prefab}/test_frozen.py +0 -0
  91. {ducktools_classbuilder-0.9.1/tests/prefab/dynamic → ducktools_classbuilder-0.10.0/tests/prefab}/test_pre_post_init.py +0 -0
  92. {ducktools_classbuilder-0.9.1/tests/prefab/dynamic → ducktools_classbuilder-0.10.0/tests/prefab}/test_private.py +0 -0
  93. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/tests/prefab/test_replace.py +0 -0
  94. {ducktools_classbuilder-0.9.1/tests/prefab/dynamic → ducktools_classbuilder-0.10.0/tests/prefab}/test_slots_novalues.py +0 -0
  95. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/tests/py312_tests/test_generic_annotations.py +0 -0
  96. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/tests/py314_tests/_test_support.py +0 -0
  97. {ducktools_classbuilder-0.9.1 → ducktools_classbuilder-0.10.0}/tests/py314_tests/test_forwardref_annotations.py +0 -0
@@ -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", "3.9", "3.8"]
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 .[testing]
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.8
38
+ - name: Set up Python 3.10
39
39
  uses: actions/setup-python@v5
40
40
  with:
41
- python-version: "3.8"
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 .[testing,type_checking]
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
@@ -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.8"
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.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.8"
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
1
+ Metadata-Version: 2.4
2
2
  Name: ducktools-classbuilder
3
- Version: 0.9.1
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.8
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
- _meta_gatherer = fields_attribute_gatherer
197
-
196
+
198
197
  def __init_subclass__(cls):
199
- # Check if the metaclass has generated slots
200
- meta_slotted = '__slots__' in vars(cls) and isinstance(cls.__slots__, dtbuild.SlotFields)
201
- gatherer = slot_gatherer if meta_slotted else fields_attribute_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,
@@ -6,9 +6,10 @@ from ducktools.classbuilder import (
6
6
  default_methods,
7
7
  get_fields,
8
8
  get_methods,
9
- slot_gatherer,
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
- # This attribute tells the slotmaker to use this gatherer
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 = slot_gatherer if slots else annotated_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
- _meta_gatherer = fields_attribute_gatherer
121
-
120
+
122
121
  def __init_subclass__(cls):
123
- # Check if the metaclass has generated slots
124
- meta_slotted = '__slots__' in vars(cls) and isinstance(cls.__slots__, dtbuild.SlotFields)
125
- gatherer = slot_gatherer if meta_slotted else fields_attribute_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.8"
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
- testing = ["pytest>=8.2", "pytest-cov", "typing_extensions"]
32
- type_checking = ["mypy"]
33
- performance_tests = ["attrs", "pydantic"]
34
- docs = ["sphinx", "myst-parser", "sphinx_rtd_theme"]
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 _KW_ONLY_TYPE:
117
+ class _KW_ONLY_META(type):
118
118
  def __repr__(self):
119
- return "<KW_ONLY Sentinel Object>"
119
+ return "<KW_ONLY Sentinel>"
120
120
 
121
121
 
122
- KW_ONLY = _KW_ONLY_TYPE()
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 = ns.get(META_GATHERER_NAME, None)
581
- if not gatherer:
582
- for base in bases:
583
- if g := getattr(base, META_GATHERER_NAME, None):
584
- gatherer = g
585
- break
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
- if not gatherer:
588
- gatherer = unified_gatherer
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__"] = SlotFields(cls_fields)
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
- # If this base class did not define __slots__ the metaclass would break it.
632
- # This will be replaced by the builder.
633
- __slots__ = SlotFields(
634
- default=NOTHING,
635
- default_factory=NOTHING,
636
- type=NOTHING,
637
- doc=None,
638
- init=True,
639
- repr=True,
640
- compare=True,
641
- kw_only=False,
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
- # To choose between annotation and attribute gatherers
912
- # compare sets of names.
913
- # Don't bother evaluating string annotations, as we only need names
914
- cls_annotations = get_ns_annotations(cls_dict)
915
- cls_attributes = {
916
- k: v for k, v in cls_dict.items() if isinstance(v, field_type)
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
- cls_annotation_names = cls_annotations.keys()
920
- cls_attribute_names = cls_attributes.keys()
1038
+ cls_annotation_names = cls_annotations.keys()
1039
+ cls_attribute_names = cls_attributes.keys()
921
1040
 
922
- if set(cls_annotation_names).issuperset(set(cls_attribute_names)):
923
- # All `Field` values have annotations, so use annotation gatherer
924
- return anno_g(cls_dict)
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
- return attrib_g(cls_dict)
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