ducktools-classbuilder 0.7.0__tar.gz → 0.7.2__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 (92) hide show
  1. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/PKG-INFO +2 -1
  2. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs/extension_examples.md +134 -2
  3. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs/tutorial.md +10 -8
  4. ducktools_classbuilder-0.7.2/docs_code/docs_ex10_frozen_attributes.py +125 -0
  5. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs_code/docs_ex3_iterable.py +1 -1
  6. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs_code/tutorial_code.py +4 -2
  7. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/pyproject.toml +1 -0
  8. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/src/ducktools/classbuilder/__init__.py +14 -17
  9. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/src/ducktools/classbuilder/__init__.pyi +7 -7
  10. ducktools_classbuilder-0.7.2/src/ducktools/classbuilder/_version.py +2 -0
  11. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/src/ducktools/classbuilder/annotations.py +106 -23
  12. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/src/ducktools/classbuilder/annotations.pyi +6 -0
  13. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/src/ducktools_classbuilder.egg-info/PKG-INFO +2 -1
  14. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/src/ducktools_classbuilder.egg-info/SOURCES.txt +4 -1
  15. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/annotations/test_annotated.py +12 -7
  16. ducktools_classbuilder-0.7.2/tests/annotations/test_future_annotations.py +45 -0
  17. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/conftest.py +2 -2
  18. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/dynamic/test_slotted_class.py +0 -1
  19. ducktools_classbuilder-0.7.2/tests/py314_tests/test_forwardref_annotations.py +45 -0
  20. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/test_core.py +27 -1
  21. ducktools_classbuilder-0.7.0/src/ducktools/classbuilder/_version.py +0 -2
  22. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/.github/dependabot.yml +0 -0
  23. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/.github/workflows/auto_test.yml +0 -0
  24. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/.github/workflows/publish_to_pypi.yml +0 -0
  25. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/.github/workflows/publish_to_testpypi.yml +0 -0
  26. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/.gitignore +0 -0
  27. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/.readthedocs.yaml +0 -0
  28. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/LICENSE.md +0 -0
  29. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/MANIFEST.in +0 -0
  30. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/README.md +0 -0
  31. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs/Makefile +0 -0
  32. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs/api.md +0 -0
  33. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs/approach_vs_tool.md +0 -0
  34. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs/conf.py +0 -0
  35. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs/generated_code.md +0 -0
  36. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs/index.md +0 -0
  37. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs/make.bat +0 -0
  38. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs/perf/performance_tests.md +0 -0
  39. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs/prefab/index.md +0 -0
  40. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs_code/docs_ex1_basic.py +0 -0
  41. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs_code/docs_ex2_register.py +0 -0
  42. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs_code/docs_ex5_frozen.py +0 -0
  43. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs_code/docs_ex7_posonly.py +0 -0
  44. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs_code/docs_ex8_converters.py +0 -0
  45. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs_code/docs_ex9_annotated.py +0 -0
  46. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/perf/cluegen.py +0 -0
  47. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/perf/dataklasses.py +0 -0
  48. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/perf/hyperfine_testmaker.py +0 -0
  49. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/perf/perf_profile.py +0 -0
  50. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/setup.cfg +0 -0
  51. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/src/ducktools/classbuilder/prefab.py +0 -0
  52. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/src/ducktools/classbuilder/prefab.pyi +0 -0
  53. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/src/ducktools/classbuilder/py.typed +0 -0
  54. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/src/ducktools_classbuilder.egg-info/dependency_links.txt +0 -0
  55. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/src/ducktools_classbuilder.egg-info/requires.txt +0 -0
  56. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/src/ducktools_classbuilder.egg-info/top_level.txt +0 -0
  57. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/annotations/test_annotations_module.py +0 -0
  58. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/dynamic/test_compare_attrib.py +0 -0
  59. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/dynamic/test_construction.py +0 -0
  60. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/dynamic/test_frozen.py +0 -0
  61. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/dynamic/test_internals.py +0 -0
  62. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/dynamic/test_pre_post_init.py +0 -0
  63. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/dynamic/test_private.py +0 -0
  64. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/dynamic/test_slots_novalues.py +0 -0
  65. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/dynamic/test_subclass_implementation.py +0 -0
  66. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/conftest.py +0 -0
  67. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/examples/creation.py +0 -0
  68. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/examples/creation_empty.py +0 -0
  69. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/examples/dunders.py +0 -0
  70. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/examples/fails/creation_1.py +0 -0
  71. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/examples/fails/creation_2.py +0 -0
  72. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/examples/fails/creation_3.py +0 -0
  73. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/examples/fails/creation_5.py +0 -0
  74. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/examples/fails/inheritance_1.py +0 -0
  75. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/examples/fails/inheritance_2.py +0 -0
  76. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/examples/funcs_prefabs.py +0 -0
  77. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/examples/hint_syntax.py +0 -0
  78. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/examples/inheritance.py +0 -0
  79. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/examples/init_ex.py +0 -0
  80. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/examples/kw_only.py +0 -0
  81. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/examples/repr_func.py +0 -0
  82. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/test_creation.py +0 -0
  83. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/test_dunders.py +0 -0
  84. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/test_funcs.py +0 -0
  85. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/test_hint_syntax.py +0 -0
  86. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/test_inheritance.py +0 -0
  87. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/test_init.py +0 -0
  88. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/test_kw_only.py +0 -0
  89. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/test_repr.py +0 -0
  90. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/py312_tests/test_generic_annotations.py +0 -0
  91. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/test_field_flags.py +0 -0
  92. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/test_slotmakermeta.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ducktools-classbuilder
3
- Version: 0.7.0
3
+ Version: 0.7.2
4
4
  Summary: Toolkit for creating class boilerplate generators
5
5
  Author: David C Ellis
6
6
  License: MIT License
@@ -32,6 +32,7 @@ Classifier: Programming Language :: Python :: 3.9
32
32
  Classifier: Programming Language :: Python :: 3.10
33
33
  Classifier: Programming Language :: Python :: 3.11
34
34
  Classifier: Programming Language :: Python :: 3.12
35
+ Classifier: Programming Language :: Python :: 3.13
35
36
  Classifier: Operating System :: OS Independent
36
37
  Classifier: License :: OSI Approved :: MIT License
37
38
  Requires-Python: >=3.8
@@ -84,7 +84,7 @@ def iter_generator(cls, funcname="__iter__"):
84
84
  field_yield = "\n".join(f" yield self.{f}" for f in field_names)
85
85
  if not field_yield:
86
86
  field_yield = " yield from ()"
87
- code = f"def {funcname}(self):\n" f"{field_yield}"
87
+ code = f"def {funcname}(self):\n{field_yield}"
88
88
  globs = {}
89
89
  return GeneratedCode(code, globs)
90
90
 
@@ -259,6 +259,138 @@ if __name__ == "__main__":
259
259
  print(e)
260
260
  ```
261
261
 
262
+ #### Frozen Attributes ####
263
+
264
+ Here's an implementation that allows freezing of individual attributes.
265
+
266
+ ```python
267
+ import ducktools.classbuilder as dtbuild
268
+
269
+
270
+ class FreezableField(dtbuild.Field):
271
+ frozen: bool = False
272
+
273
+
274
+ def setattr_generator(cls, funcname="__setattr__"):
275
+ globs = {}
276
+
277
+ flags = dtbuild.get_flags(cls)
278
+ fields = dtbuild.get_fields(cls)
279
+
280
+ frozen_fields = set(
281
+ name for name, field in fields.items()
282
+ if getattr(field, "frozen", False)
283
+ )
284
+
285
+ globs["__frozen_fields"] = frozen_fields
286
+
287
+ if flags.get("slotted", True):
288
+ globs["__setattr_func"] = object.__setattr__
289
+ setattr_method = "__setattr_func(self, name, value)"
290
+ attrib_check = "hasattr(self, name)"
291
+ else:
292
+ setattr_method = "self.__dict__[name] = value"
293
+ attrib_check = "name in self.__dict__"
294
+
295
+ code = (
296
+ f"def {funcname}(self, name, value):\n"
297
+ f" if name in __frozen_fields and {attrib_check}:\n"
298
+ f" raise AttributeError(\n"
299
+ f" f'Attribute {{name!r}} does not support assignment'\n"
300
+ f" )\n"
301
+ f" else:\n"
302
+ f" {setattr_method}\n"
303
+ )
304
+
305
+ return dtbuild.GeneratedCode(code, globs)
306
+
307
+
308
+ def delattr_generator(cls, funcname="__delattr__"):
309
+ globs = {}
310
+
311
+ flags = dtbuild.get_flags(cls)
312
+ fields = dtbuild.get_fields(cls)
313
+
314
+ frozen_fields = set(
315
+ name for name, field in fields.items()
316
+ if getattr(field, "frozen", False)
317
+ )
318
+
319
+ globs["__frozen_fields"] = frozen_fields
320
+
321
+ if flags.get("slotted", True):
322
+ globs["__delattr_func"] = object.__delattr__
323
+ delattr_method = "__delattr_func(self, name)"
324
+ else:
325
+ delattr_method = "del self.__dict__[name]"
326
+
327
+ code = (
328
+ f"def {funcname}(self, name):\n"
329
+ f" if name in __frozen_fields:"
330
+ f" raise AttributeError(\n"
331
+ f" f'Attribute {{name!r}} is frozen and can not be deleted'\n"
332
+ f" )\n"
333
+ f" else:\n"
334
+ f" {delattr_method}\n"
335
+ )
336
+
337
+ return dtbuild.GeneratedCode(code, globs)
338
+
339
+
340
+ frozen_setattr_field_maker = dtbuild.MethodMaker("__setattr__", setattr_generator)
341
+ frozen_delattr_field_maker = dtbuild.MethodMaker("__delattr__", delattr_generator)
342
+ gatherer = dtbuild.make_unified_gatherer(FreezableField)
343
+
344
+
345
+ def freezable(cls=None, /, *, frozen=False):
346
+ if cls is None:
347
+ return lambda cls_: freezable(cls_, frozen=frozen)
348
+
349
+ # To make a slotted class use a base class with metaclass
350
+ flags = {"frozen": frozen, "slotted": False}
351
+
352
+ cls = dtbuild.builder(
353
+ cls,
354
+ gatherer=gatherer,
355
+ methods=dtbuild.default_methods,
356
+ flags=flags,
357
+ )
358
+
359
+ # Frozen attributes need to be added afterwards
360
+ # Due to the need to know if frozen fields exist
361
+ if frozen:
362
+ setattr(cls, "__setattr__", dtbuild.frozen_setattr_maker)
363
+ setattr(cls, "__delattr__", dtbuild.frozen_delattr_maker)
364
+ else:
365
+ fields = dtbuild.get_fields(cls)
366
+ has_frozen_fields = False
367
+ for f in fields.values():
368
+ if getattr(f, "frozen", False):
369
+ has_frozen_fields = True
370
+ break
371
+
372
+ if has_frozen_fields:
373
+ setattr(cls, "__setattr__", frozen_setattr_field_maker)
374
+ setattr(cls, "__delattr__", frozen_delattr_field_maker)
375
+
376
+ return cls
377
+
378
+
379
+ @freezable
380
+ class X:
381
+ a: int = 2
382
+ b: int = FreezableField(default=12, frozen=True)
383
+
384
+
385
+ x = X()
386
+ x.a = 21
387
+
388
+ try:
389
+ x.b = 43
390
+ except AttributeError as e:
391
+ print(repr(e))
392
+ ```
393
+
262
394
  #### Converters ####
263
395
 
264
396
  Here's an implementation of basic converters that always convert when
@@ -330,7 +462,7 @@ if __name__ == "__main__":
330
462
 
331
463
  This seems to be a feature people keep requesting for `dataclasses`.
332
464
 
333
- To implement this you simply need to create a new annotated_gatherer function.
465
+ To implement this you need to create a new annotated_gatherer function.
334
466
 
335
467
  > Note: Field classes will be frozen when running under pytest.
336
468
  > They should not be mutated by gatherers.
@@ -1,6 +1,6 @@
1
1
  # Tutorial: Making a class boilerplate generator #
2
2
 
3
- The core idea is that there are 3 parts to the process of generating
3
+ The core idea is that there are 4 parts to the process of generating
4
4
  the class boilerplate that need to be handled:
5
5
 
6
6
  1. Create a new subclass of `Field` if you need to add any extra attributes to fields
@@ -162,7 +162,7 @@ print(report_generator(CodegenDemo).source_code)
162
162
  Here we will make both a simple decorator based builder and then a subclass
163
163
  based builder that can create `__slots__`.
164
164
 
165
- ### Decorator builder ###
165
+ ### 4a: Decorator builder ###
166
166
  ```python
167
167
  def reportclass(cls):
168
168
  gatherer = fields_attribute_gatherer
@@ -177,21 +177,22 @@ def reportclass(cls):
177
177
  flags = {"slotted": slotted}
178
178
 
179
179
  return dtbuild.builder(cls, gatherer=gatherer, methods=methods, flags=flags)
180
+ ```
180
181
 
181
- # Step 4b: Define a base class builder
182
+ ### 4b: Base class Builder ###
183
+ ```python
182
184
  # Once slots have been made, slot_gatherer should be used.
183
185
  slot_gatherer = dtbuild.make_slot_gatherer(CustomField)
184
- ```
185
186
 
186
- ### Base class Builder ###
187
- ```python
187
+
188
188
  class ReportClass(metaclass=dtbuild.SlotMakerMeta):
189
189
  __slots__ = {}
190
190
  _meta_gatherer = fields_attribute_gatherer
191
191
 
192
192
  def __init_subclass__(cls):
193
- slotted = '__slots__' in vars(cls) and isinstance(cls.__slots__, dtbuild.SlotFields)
194
- gatherer = slot_gatherer if slotted else fields_attribute_gatherer
193
+ # Check if the metaclass has generated slots
194
+ meta_slotted = '__slots__' in vars(cls) and isinstance(cls.__slots__, dtbuild.SlotFields)
195
+ gatherer = slot_gatherer if meta_slotted else fields_attribute_gatherer
195
196
  methods = {
196
197
  dtbuild.eq_maker,
197
198
  dtbuild.repr_maker,
@@ -199,6 +200,7 @@ class ReportClass(metaclass=dtbuild.SlotMakerMeta):
199
200
  report_maker
200
201
  }
201
202
 
203
+ # The class may still have slots unrelated to code generation
202
204
  slotted = "__slots__" in vars(cls)
203
205
  flags = {"slotted": slotted}
204
206
 
@@ -0,0 +1,125 @@
1
+ import ducktools.classbuilder as dtbuild
2
+
3
+
4
+ class FreezableField(dtbuild.Field):
5
+ frozen: bool = False
6
+
7
+
8
+ def setattr_generator(cls, funcname="__setattr__"):
9
+ globs = {}
10
+
11
+ flags = dtbuild.get_flags(cls)
12
+ fields = dtbuild.get_fields(cls)
13
+
14
+ frozen_fields = set(
15
+ name for name, field in fields.items()
16
+ if getattr(field, "frozen", False)
17
+ )
18
+
19
+ globs["__frozen_fields"] = frozen_fields
20
+
21
+ if flags.get("slotted", True):
22
+ globs["__setattr_func"] = object.__setattr__
23
+ setattr_method = "__setattr_func(self, name, value)"
24
+ attrib_check = "hasattr(self, name)"
25
+ else:
26
+ setattr_method = "self.__dict__[name] = value"
27
+ attrib_check = "name in self.__dict__"
28
+
29
+ code = (
30
+ f"def {funcname}(self, name, value):\n"
31
+ f" if name in __frozen_fields and {attrib_check}:\n"
32
+ f" raise AttributeError(\n"
33
+ f" f'Attribute {{name!r}} does not support assignment'\n"
34
+ f" )\n"
35
+ f" else:\n"
36
+ f" {setattr_method}\n"
37
+ )
38
+
39
+ return dtbuild.GeneratedCode(code, globs)
40
+
41
+
42
+ def delattr_generator(cls, funcname="__delattr__"):
43
+ globs = {}
44
+
45
+ flags = dtbuild.get_flags(cls)
46
+ fields = dtbuild.get_fields(cls)
47
+
48
+ frozen_fields = set(
49
+ name for name, field in fields.items()
50
+ if getattr(field, "frozen", False)
51
+ )
52
+
53
+ globs["__frozen_fields"] = frozen_fields
54
+
55
+ if flags.get("slotted", True):
56
+ globs["__delattr_func"] = object.__delattr__
57
+ delattr_method = "__delattr_func(self, name)"
58
+ else:
59
+ delattr_method = "del self.__dict__[name]"
60
+
61
+ code = (
62
+ f"def {funcname}(self, name):\n"
63
+ f" if name in __frozen_fields:"
64
+ f" raise AttributeError(\n"
65
+ f" f'Attribute {{name!r}} is frozen and can not be deleted'\n"
66
+ f" )\n"
67
+ f" else:\n"
68
+ f" {delattr_method}\n"
69
+ )
70
+
71
+ return dtbuild.GeneratedCode(code, globs)
72
+
73
+
74
+ frozen_setattr_field_maker = dtbuild.MethodMaker("__setattr__", setattr_generator)
75
+ frozen_delattr_field_maker = dtbuild.MethodMaker("__delattr__", delattr_generator)
76
+ gatherer = dtbuild.make_unified_gatherer(FreezableField)
77
+
78
+
79
+ def freezable(cls=None, /, *, frozen=False):
80
+ if cls is None:
81
+ return lambda cls_: freezable(cls_, frozen=frozen)
82
+
83
+ # To make a slotted class use a base class with metaclass
84
+ flags = {"frozen": frozen, "slotted": False}
85
+
86
+ cls = dtbuild.builder(
87
+ cls,
88
+ gatherer=gatherer,
89
+ methods=dtbuild.default_methods,
90
+ flags=flags,
91
+ )
92
+
93
+ # Frozen attributes need to be added afterwards
94
+ # Due to the need to know if frozen fields exist
95
+ if frozen:
96
+ setattr(cls, "__setattr__", dtbuild.frozen_setattr_maker)
97
+ setattr(cls, "__delattr__", dtbuild.frozen_delattr_maker)
98
+ else:
99
+ fields = dtbuild.get_fields(cls)
100
+ has_frozen_fields = False
101
+ for f in fields.values():
102
+ if getattr(f, "frozen", False):
103
+ has_frozen_fields = True
104
+ break
105
+
106
+ if has_frozen_fields:
107
+ setattr(cls, "__setattr__", frozen_setattr_field_maker)
108
+ setattr(cls, "__delattr__", frozen_delattr_field_maker)
109
+
110
+ return cls
111
+
112
+
113
+ @freezable
114
+ class X:
115
+ a: int = 2
116
+ b: int = FreezableField(default=12, frozen=True)
117
+
118
+
119
+ x = X()
120
+ x.a = 21
121
+
122
+ try:
123
+ x.b = 43
124
+ except AttributeError as e:
125
+ print(repr(e))
@@ -13,7 +13,7 @@ def iter_generator(cls, funcname="__iter__"):
13
13
  field_yield = "\n".join(f" yield self.{f}" for f in field_names)
14
14
  if not field_yield:
15
15
  field_yield = " yield from ()"
16
- code = f"def {funcname}(self):\n" f"{field_yield}"
16
+ code = f"def {funcname}(self):\n{field_yield}"
17
17
  globs = {}
18
18
  return GeneratedCode(code, globs)
19
19
 
@@ -117,8 +117,9 @@ class ReportClass(metaclass=dtbuild.SlotMakerMeta):
117
117
  _meta_gatherer = fields_attribute_gatherer
118
118
 
119
119
  def __init_subclass__(cls):
120
- slotted = '__slots__' in vars(cls) and isinstance(cls.__slots__, dtbuild.SlotFields)
121
- gatherer = slot_gatherer if slotted else fields_attribute_gatherer
120
+ # Check if the metaclass has generated slots
121
+ meta_slotted = '__slots__' in vars(cls) and isinstance(cls.__slots__, dtbuild.SlotFields)
122
+ gatherer = slot_gatherer if meta_slotted else fields_attribute_gatherer
122
123
  methods = {
123
124
  dtbuild.eq_maker,
124
125
  dtbuild.repr_maker,
@@ -126,6 +127,7 @@ class ReportClass(metaclass=dtbuild.SlotMakerMeta):
126
127
  report_maker
127
128
  }
128
129
 
130
+ # The class may still have slots unrelated to code generation
129
131
  slotted = "__slots__" in vars(cls)
130
132
  flags = {"slotted": slotted}
131
133
 
@@ -20,6 +20,7 @@ classifiers = [
20
20
  "Programming Language :: Python :: 3.10",
21
21
  "Programming Language :: Python :: 3.11",
22
22
  "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
23
24
  "Operating System :: OS Independent",
24
25
  "License :: OSI Approved :: MIT License",
25
26
  ]
@@ -363,15 +363,17 @@ def eq_generator(cls, funcname="__eq__"):
363
363
  ]
364
364
 
365
365
  if field_names:
366
- selfvals = ",".join(f"self.{name}" for name in field_names)
367
- othervals = ",".join(f"other.{name}" for name in field_names)
368
- instance_comparison = f"({selfvals},) == ({othervals},)"
366
+ instance_comparison = "\n and ".join(
367
+ f"self.{name} == other.{name}" for name in field_names
368
+ )
369
369
  else:
370
370
  instance_comparison = "True"
371
371
 
372
372
  code = (
373
373
  f"def {funcname}(self, other):\n"
374
- f" return {instance_comparison} if {class_comparison} else NotImplemented\n"
374
+ f" return (\n"
375
+ f" {instance_comparison}\n"
376
+ f" ) if {class_comparison} else NotImplemented\n"
375
377
  )
376
378
  globs = {}
377
379
 
@@ -390,11 +392,13 @@ def frozen_setattr_generator(cls, funcname="__setattr__"):
390
392
  if flags.get("slotted", True):
391
393
  globs["__setattr_func"] = object.__setattr__
392
394
  setattr_method = "__setattr_func(self, name, value)"
395
+ hasattr_check = "hasattr(self, name)"
393
396
  else:
394
397
  setattr_method = "self.__dict__[name] = value"
398
+ hasattr_check = "name in self.__dict__"
395
399
 
396
400
  body = (
397
- f" if hasattr(self, name) or name not in __field_names:\n"
401
+ f" if {hasattr_check} or name not in __field_names:\n"
398
402
  f' raise TypeError(\n'
399
403
  f' f"{{type(self).__name__!r}} object does not support "'
400
404
  f' f"attribute assignment"\n'
@@ -707,10 +711,6 @@ def make_slot_gatherer(field_type=Field):
707
711
  "in order to generate a slotclass"
708
712
  )
709
713
 
710
- # Don't want to mutate original annotations so make a copy if it exists
711
- # Looking at the dict is a Python3.9 or earlier requirement
712
- cls_annotations = get_ns_annotations(cls_dict)
713
-
714
714
  cls_fields = {}
715
715
  slot_replacement = {}
716
716
 
@@ -724,8 +724,6 @@ def make_slot_gatherer(field_type=Field):
724
724
 
725
725
  if isinstance(v, field_type):
726
726
  attrib = v
727
- if attrib.type is not NOTHING:
728
- cls_annotations[k] = attrib.type
729
727
  else:
730
728
  # Plain values treated as defaults
731
729
  attrib = field_type(default=v)
@@ -738,7 +736,6 @@ def make_slot_gatherer(field_type=Field):
738
736
  # In this case, slots with documentation and new annotations.
739
737
  modifications = {
740
738
  "__slots__": slot_replacement,
741
- "__annotations__": cls_annotations,
742
739
  }
743
740
 
744
741
  return cls_fields, modifications
@@ -748,7 +745,7 @@ def make_slot_gatherer(field_type=Field):
748
745
 
749
746
  def make_annotation_gatherer(
750
747
  field_type=Field,
751
- leave_default_values=True,
748
+ leave_default_values=False,
752
749
  ):
753
750
  """
754
751
  Create a new annotation gatherer that will work with `Field` instances
@@ -814,7 +811,7 @@ def make_annotation_gatherer(
814
811
 
815
812
  def make_field_gatherer(
816
813
  field_type=Field,
817
- leave_default_values=True,
814
+ leave_default_values=False,
818
815
  ):
819
816
  def field_attribute_gatherer(cls_or_ns):
820
817
  if isinstance(cls_or_ns, (_MappingProxyType, dict)):
@@ -847,7 +844,7 @@ def make_field_gatherer(
847
844
 
848
845
  def make_unified_gatherer(
849
846
  field_type=Field,
850
- leave_default_values=True,
847
+ leave_default_values=False,
851
848
  ):
852
849
  """
853
850
  Create a gatherer that will work via first slots, then
@@ -897,7 +894,7 @@ annotation_gatherer = make_annotation_gatherer()
897
894
 
898
895
  # The unified gatherer used for slot classes must remove default
899
896
  # values for slots to work correctly.
900
- unified_gatherer = make_unified_gatherer(leave_default_values=False)
897
+ unified_gatherer = make_unified_gatherer()
901
898
 
902
899
 
903
900
  # Now the gatherers have been defined, add __repr__ and __eq__ to Field.
@@ -963,7 +960,7 @@ class AnnotationClass(metaclass=SlotMakerMeta):
963
960
  def __init_subclass__(
964
961
  cls,
965
962
  methods=default_methods,
966
- gatherer=make_unified_gatherer(leave_default_values=True),
963
+ gatherer=unified_gatherer,
967
964
  **kwargs
968
965
  ):
969
966
  # Check class dict otherwise this will always be True as this base
@@ -178,37 +178,37 @@ def make_slot_gatherer(
178
178
  @typing.overload
179
179
  def make_annotation_gatherer(
180
180
  field_type: type[_FieldType],
181
- leave_default_values: bool = True,
181
+ leave_default_values: bool = False,
182
182
  ) -> Callable[[type | _CopiableMappings], tuple[dict[str, _FieldType], dict[str, typing.Any]]]: ...
183
183
 
184
184
  @typing.overload
185
185
  def make_annotation_gatherer(
186
186
  field_type: _ReturnsField = Field,
187
- leave_default_values: bool = True,
187
+ leave_default_values: bool = False,
188
188
  ) -> Callable[[type | _CopiableMappings], tuple[dict[str, Field], dict[str, typing.Any]]]: ...
189
189
 
190
190
  @typing.overload
191
191
  def make_field_gatherer(
192
192
  field_type: type[_FieldType],
193
- leave_default_values: bool = True,
193
+ leave_default_values: bool = False,
194
194
  ) -> Callable[[type | _CopiableMappings], tuple[dict[str, _FieldType], dict[str, typing.Any]]]: ...
195
195
 
196
196
  @typing.overload
197
197
  def make_field_gatherer(
198
198
  field_type: _ReturnsField = Field,
199
- leave_default_values: bool = True,
199
+ leave_default_values: bool = False,
200
200
  ) -> Callable[[type | _CopiableMappings], tuple[dict[str, Field], dict[str, typing.Any]]]: ...
201
201
 
202
202
  @typing.overload
203
203
  def make_unified_gatherer(
204
204
  field_type: type[_FieldType],
205
- leave_default_values: bool = True,
205
+ leave_default_values: bool = False,
206
206
  ) -> Callable[[type | _CopiableMappings], tuple[dict[str, _FieldType], dict[str, typing.Any]]]: ...
207
207
 
208
208
  @typing.overload
209
209
  def make_unified_gatherer(
210
210
  field_type: _ReturnsField = Field,
211
- leave_default_values: bool = True,
211
+ leave_default_values: bool = False,
212
212
  ) -> Callable[[type | _CopiableMappings], tuple[dict[str, Field], dict[str, typing.Any]]]: ...
213
213
 
214
214
 
@@ -249,7 +249,7 @@ class AnnotationClass(metaclass=SlotMakerMeta):
249
249
  def __init_subclass__(
250
250
  cls,
251
251
  methods: frozenset[MethodMaker] | set[MethodMaker] = default_methods,
252
- gatherer: _gatherer_type = make_unified_gatherer(leave_default_values=True),
252
+ gatherer: _gatherer_type = unified_gatherer,
253
253
  **kwargs,
254
254
  ) -> None: ...
255
255
 
@@ -0,0 +1,2 @@
1
+ __version__ = "0.7.2"
2
+ __version_tuple__ = (0, 7, 2)