ducktools-classbuilder 0.7.1__tar.gz → 0.7.3__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.1 → ducktools_classbuilder-0.7.3}/.github/workflows/auto_test.yml +2 -2
  2. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/PKG-INFO +3 -2
  3. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/docs/extension_examples.md +132 -0
  4. ducktools_classbuilder-0.7.3/docs_code/docs_ex10_frozen_attributes.py +125 -0
  5. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/pyproject.toml +6 -6
  6. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/src/ducktools/classbuilder/__init__.py +14 -10
  7. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/src/ducktools/classbuilder/__init__.pyi +7 -7
  8. ducktools_classbuilder-0.7.3/src/ducktools/classbuilder/_version.py +2 -0
  9. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/src/ducktools/classbuilder/annotations.py +39 -2
  10. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/src/ducktools_classbuilder.egg-info/PKG-INFO +3 -2
  11. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/src/ducktools_classbuilder.egg-info/SOURCES.txt +1 -0
  12. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/src/ducktools_classbuilder.egg-info/requires.txt +3 -1
  13. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/annotations/test_annotated.py +12 -7
  14. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/test_core.py +27 -0
  15. ducktools_classbuilder-0.7.1/src/ducktools/classbuilder/_version.py +0 -2
  16. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/.github/dependabot.yml +0 -0
  17. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/.github/workflows/publish_to_pypi.yml +0 -0
  18. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/.github/workflows/publish_to_testpypi.yml +0 -0
  19. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/.gitignore +0 -0
  20. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/.readthedocs.yaml +0 -0
  21. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/LICENSE.md +0 -0
  22. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/MANIFEST.in +0 -0
  23. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/README.md +0 -0
  24. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/docs/Makefile +0 -0
  25. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/docs/api.md +0 -0
  26. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/docs/approach_vs_tool.md +0 -0
  27. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/docs/conf.py +0 -0
  28. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/docs/generated_code.md +0 -0
  29. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/docs/index.md +0 -0
  30. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/docs/make.bat +0 -0
  31. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/docs/perf/performance_tests.md +0 -0
  32. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/docs/prefab/index.md +0 -0
  33. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/docs/tutorial.md +0 -0
  34. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/docs_code/docs_ex1_basic.py +0 -0
  35. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/docs_code/docs_ex2_register.py +0 -0
  36. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/docs_code/docs_ex3_iterable.py +0 -0
  37. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/docs_code/docs_ex5_frozen.py +0 -0
  38. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/docs_code/docs_ex7_posonly.py +0 -0
  39. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/docs_code/docs_ex8_converters.py +0 -0
  40. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/docs_code/docs_ex9_annotated.py +0 -0
  41. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/docs_code/tutorial_code.py +0 -0
  42. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/perf/cluegen.py +0 -0
  43. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/perf/dataklasses.py +0 -0
  44. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/perf/hyperfine_testmaker.py +0 -0
  45. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/perf/perf_profile.py +0 -0
  46. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/setup.cfg +0 -0
  47. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/src/ducktools/classbuilder/annotations.pyi +0 -0
  48. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/src/ducktools/classbuilder/prefab.py +0 -0
  49. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/src/ducktools/classbuilder/prefab.pyi +0 -0
  50. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/src/ducktools/classbuilder/py.typed +0 -0
  51. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/src/ducktools_classbuilder.egg-info/dependency_links.txt +0 -0
  52. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/src/ducktools_classbuilder.egg-info/top_level.txt +0 -0
  53. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/annotations/test_annotations_module.py +0 -0
  54. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/annotations/test_future_annotations.py +0 -0
  55. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/conftest.py +0 -0
  56. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/prefab/dynamic/test_compare_attrib.py +0 -0
  57. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/prefab/dynamic/test_construction.py +0 -0
  58. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/prefab/dynamic/test_frozen.py +0 -0
  59. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/prefab/dynamic/test_internals.py +0 -0
  60. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/prefab/dynamic/test_pre_post_init.py +0 -0
  61. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/prefab/dynamic/test_private.py +0 -0
  62. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/prefab/dynamic/test_slots_novalues.py +0 -0
  63. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/prefab/dynamic/test_slotted_class.py +0 -0
  64. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/prefab/dynamic/test_subclass_implementation.py +0 -0
  65. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/prefab/shared/conftest.py +0 -0
  66. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/prefab/shared/examples/creation.py +0 -0
  67. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/prefab/shared/examples/creation_empty.py +0 -0
  68. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/prefab/shared/examples/dunders.py +0 -0
  69. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/prefab/shared/examples/fails/creation_1.py +0 -0
  70. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/prefab/shared/examples/fails/creation_2.py +0 -0
  71. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/prefab/shared/examples/fails/creation_3.py +0 -0
  72. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/prefab/shared/examples/fails/creation_5.py +0 -0
  73. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/prefab/shared/examples/fails/inheritance_1.py +0 -0
  74. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/prefab/shared/examples/fails/inheritance_2.py +0 -0
  75. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/prefab/shared/examples/funcs_prefabs.py +0 -0
  76. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/prefab/shared/examples/hint_syntax.py +0 -0
  77. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/prefab/shared/examples/inheritance.py +0 -0
  78. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/prefab/shared/examples/init_ex.py +0 -0
  79. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/prefab/shared/examples/kw_only.py +0 -0
  80. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/prefab/shared/examples/repr_func.py +0 -0
  81. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/prefab/shared/test_creation.py +0 -0
  82. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/prefab/shared/test_dunders.py +0 -0
  83. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/prefab/shared/test_funcs.py +0 -0
  84. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/prefab/shared/test_hint_syntax.py +0 -0
  85. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/prefab/shared/test_inheritance.py +0 -0
  86. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/prefab/shared/test_init.py +0 -0
  87. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/prefab/shared/test_kw_only.py +0 -0
  88. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/prefab/shared/test_repr.py +0 -0
  89. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/py312_tests/test_generic_annotations.py +0 -0
  90. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/py314_tests/test_forwardref_annotations.py +0 -0
  91. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/test_field_flags.py +0 -0
  92. {ducktools_classbuilder-0.7.1 → ducktools_classbuilder-0.7.3}/tests/test_slotmakermeta.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.13-dev", "3.12", "3.11", "3.10", "pypy-3.10", "3.9", "3.8"]
17
+ python-version: ["3.13", "3.12", "3.11", "3.10", "pypy-3.10", "3.9", "3.8"]
18
18
 
19
19
  steps:
20
20
  - uses: actions/checkout@v4
@@ -42,7 +42,7 @@ jobs:
42
42
  - name: Install dependencies
43
43
  run: |
44
44
  python -m pip install --upgrade pip
45
- python -m pip install -e .[testing]
45
+ python -m pip install -e .[testing,type_checking]
46
46
  - name: Check type stub files
47
47
  run: |
48
48
  python -m mypy.stubtest ducktools.classbuilder
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ducktools-classbuilder
3
- Version: 0.7.1
3
+ Version: 0.7.3
4
4
  Summary: Toolkit for creating class boilerplate generators
5
5
  Author: David C Ellis
6
6
  License: MIT License
@@ -41,8 +41,9 @@ License-File: LICENSE.md
41
41
  Provides-Extra: testing
42
42
  Requires-Dist: pytest>=8.2; extra == "testing"
43
43
  Requires-Dist: pytest-cov; extra == "testing"
44
- Requires-Dist: mypy; extra == "testing"
45
44
  Requires-Dist: typing_extensions; extra == "testing"
45
+ Provides-Extra: type-checking
46
+ Requires-Dist: mypy; extra == "type-checking"
46
47
  Provides-Extra: performance-tests
47
48
  Requires-Dist: attrs; extra == "performance-tests"
48
49
  Requires-Dist: pydantic; extra == "performance-tests"
@@ -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
@@ -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))
@@ -27,6 +27,12 @@ classifiers = [
27
27
  dynamic = ['version']
28
28
  license = {file = "LICENSE.md"}
29
29
 
30
+ [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"]
35
+
30
36
  [tool.setuptools.packages.find]
31
37
  where = ["src"]
32
38
 
@@ -34,12 +40,6 @@ where = ["src"]
34
40
  version_file = "src/ducktools/classbuilder/_version.py"
35
41
  version_file_template = "__version__ = \"{version}\"\n__version_tuple__ = {version_tuple}\n"
36
42
 
37
-
38
- [project.optional-dependencies]
39
- testing = ["pytest>=8.2", "pytest-cov", "mypy", "typing_extensions"]
40
- performance_tests = ["attrs", "pydantic"]
41
- docs = ["sphinx", "myst-parser", "sphinx_rtd_theme"]
42
-
43
43
  [project.urls]
44
44
  "Homepage" = "https://github.com/davidcellis/ducktools-classbuilder"
45
45
 
@@ -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'
@@ -741,7 +745,7 @@ def make_slot_gatherer(field_type=Field):
741
745
 
742
746
  def make_annotation_gatherer(
743
747
  field_type=Field,
744
- leave_default_values=True,
748
+ leave_default_values=False,
745
749
  ):
746
750
  """
747
751
  Create a new annotation gatherer that will work with `Field` instances
@@ -807,7 +811,7 @@ def make_annotation_gatherer(
807
811
 
808
812
  def make_field_gatherer(
809
813
  field_type=Field,
810
- leave_default_values=True,
814
+ leave_default_values=False,
811
815
  ):
812
816
  def field_attribute_gatherer(cls_or_ns):
813
817
  if isinstance(cls_or_ns, (_MappingProxyType, dict)):
@@ -840,7 +844,7 @@ def make_field_gatherer(
840
844
 
841
845
  def make_unified_gatherer(
842
846
  field_type=Field,
843
- leave_default_values=True,
847
+ leave_default_values=False,
844
848
  ):
845
849
  """
846
850
  Create a gatherer that will work via first slots, then
@@ -890,7 +894,7 @@ annotation_gatherer = make_annotation_gatherer()
890
894
 
891
895
  # The unified gatherer used for slot classes must remove default
892
896
  # values for slots to work correctly.
893
- unified_gatherer = make_unified_gatherer(leave_default_values=False)
897
+ unified_gatherer = make_unified_gatherer()
894
898
 
895
899
 
896
900
  # Now the gatherers have been defined, add __repr__ and __eq__ to Field.
@@ -956,7 +960,7 @@ class AnnotationClass(metaclass=SlotMakerMeta):
956
960
  def __init_subclass__(
957
961
  cls,
958
962
  methods=default_methods,
959
- gatherer=make_unified_gatherer(leave_default_values=True),
963
+ gatherer=unified_gatherer,
960
964
  **kwargs
961
965
  ):
962
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.3"
2
+ __version_tuple__ = (0, 7, 3)
@@ -37,6 +37,36 @@ del _cell_factory
37
37
  # End evil stuff from types.py
38
38
 
39
39
 
40
+ class _Stringlike(str):
41
+ # There are typing operators that are not supported by strings
42
+ # This adds the 'or' operator '|'
43
+
44
+ def __or__(self, other):
45
+ if isinstance(other, str):
46
+ other_r = other
47
+ elif name := getattr(other, "__name__", None):
48
+ other_r = name
49
+ else:
50
+ other_r = str(other)
51
+
52
+ return type(self)(f"{self} | {other_r}")
53
+
54
+ def __ror__(self, other):
55
+ if isinstance(other, str):
56
+ other_r = other
57
+ elif name := getattr(other, "__name__", None):
58
+ other_r = name
59
+ else:
60
+ other_r = str(other)
61
+
62
+ return type(self)(f"{other_r} | {self}")
63
+
64
+ def __repr__(self):
65
+ base = super().__repr__()
66
+ clsname = type(self).__name__
67
+ return f"{clsname}({base})"
68
+
69
+
40
70
  class _StringGlobs(dict):
41
71
  """
42
72
  Based on the fake globals dictionary used for annotations
@@ -47,7 +77,7 @@ class _StringGlobs(dict):
47
77
  is not found.
48
78
  """
49
79
  def __missing__(self, key):
50
- return key
80
+ return _Stringlike(key)
51
81
 
52
82
  def __repr__(self):
53
83
  cls_name = self.__class__.__name__
@@ -165,7 +195,14 @@ def call_annotate_func(annotate):
165
195
  closure = None
166
196
 
167
197
  new_annotate = _FunctionType(annotate.__code__, globs, closure=closure)
168
- return new_annotate(1)
198
+
199
+ # Convert _Stringlike back to str
200
+ annos = {
201
+ k: str(v) if isinstance(v, _Stringlike) else v
202
+ for k, v in new_annotate(1).items()
203
+ }
204
+
205
+ return annos
169
206
 
170
207
 
171
208
  def get_ns_annotations(ns, eval_str=True):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ducktools-classbuilder
3
- Version: 0.7.1
3
+ Version: 0.7.3
4
4
  Summary: Toolkit for creating class boilerplate generators
5
5
  Author: David C Ellis
6
6
  License: MIT License
@@ -41,8 +41,9 @@ License-File: LICENSE.md
41
41
  Provides-Extra: testing
42
42
  Requires-Dist: pytest>=8.2; extra == "testing"
43
43
  Requires-Dist: pytest-cov; extra == "testing"
44
- Requires-Dist: mypy; extra == "testing"
45
44
  Requires-Dist: typing_extensions; extra == "testing"
45
+ Provides-Extra: type-checking
46
+ Requires-Dist: mypy; extra == "type-checking"
46
47
  Provides-Extra: performance-tests
47
48
  Requires-Dist: attrs; extra == "performance-tests"
48
49
  Requires-Dist: pydantic; extra == "performance-tests"
@@ -19,6 +19,7 @@ docs/make.bat
19
19
  docs/tutorial.md
20
20
  docs/perf/performance_tests.md
21
21
  docs/prefab/index.md
22
+ docs_code/docs_ex10_frozen_attributes.py
22
23
  docs_code/docs_ex1_basic.py
23
24
  docs_code/docs_ex2_register.py
24
25
  docs_code/docs_ex3_iterable.py
@@ -11,5 +11,7 @@ pydantic
11
11
  [testing]
12
12
  pytest>=8.2
13
13
  pytest-cov
14
- mypy
15
14
  typing_extensions
15
+
16
+ [type_checking]
17
+ mypy
@@ -60,9 +60,10 @@ def test_annotation_gatherer():
60
60
  for key in "defgh":
61
61
  assert key not in annos
62
62
 
63
- # Instance variables not removed from class
64
- # Field replaced with default value on class
65
- assert modifications["c"] == "c"
63
+ # Instance variables to be removed from class
64
+ assert modifications["a"] is NOTHING
65
+ assert modifications["b"] is NOTHING
66
+ assert modifications["c"] is NOTHING
66
67
 
67
68
 
68
69
  def test_make_annotation_gatherer():
@@ -71,7 +72,7 @@ def test_make_annotation_gatherer():
71
72
 
72
73
  gatherer = make_annotation_gatherer(
73
74
  field_type=NewField,
74
- leave_default_values=False,
75
+ leave_default_values=True,
75
76
  )
76
77
 
77
78
  class ExampleAnnotated:
@@ -91,10 +92,11 @@ def test_make_annotation_gatherer():
91
92
 
92
93
  assert annos["blank_field"] == NewField(type=str)
93
94
 
94
- # ABC should be present in annos but removed from the class
95
+ # ABC should be present in annos and in the class
95
96
  for key in "abc":
96
97
  assert annos[key] == NewField(default=key, type=annotations[key])
97
- assert modifications[key] is NOTHING
98
+
99
+ assert modifications["c"] == "c"
98
100
 
99
101
  # Opposite for classvar
100
102
  for key in "defgh":
@@ -114,7 +116,10 @@ def test_annotationclass():
114
116
  g: Annotated[Annotated[ClassVar[str], ""], ""] = "g"
115
117
  h: Annotated[CV[str], ''] = "h"
116
118
 
117
- for key in "abcdefgh":
119
+ for key in "abc":
120
+ assert key not in ExampleAnnotated.__dict__
121
+
122
+ for key in "defgh":
118
123
  assert key in ExampleAnnotated.__dict__
119
124
 
120
125
  ex = ExampleAnnotated()
@@ -9,10 +9,13 @@ from ducktools.classbuilder import (
9
9
  builder,
10
10
  default_methods,
11
11
  eq_maker,
12
+ frozen_delattr_maker,
13
+ frozen_setattr_maker,
12
14
  get_fields,
13
15
  get_flags,
14
16
  get_methods,
15
17
  init_maker,
18
+ make_unified_gatherer,
16
19
  slot_gatherer,
17
20
  slotclass,
18
21
 
@@ -165,6 +168,30 @@ def test_frozen_field():
165
168
  delattr(f, k)
166
169
 
167
170
 
171
+ def test_frozen_unslotted():
172
+ # Test a frozen class with defaults left in place
173
+
174
+ methods = default_methods | {frozen_setattr_maker, frozen_delattr_maker}
175
+ gatherer = make_unified_gatherer(Field, leave_default_values=True)
176
+
177
+ def b(cls):
178
+ return builder(cls, methods=methods, gatherer=gatherer,
179
+ flags={"frozen": True, "slotted": False})
180
+
181
+ @b
182
+ class Ex:
183
+ a: int = 41
184
+ b: str = "Hello"
185
+
186
+ ex = Ex()
187
+
188
+ with pytest.raises(TypeError):
189
+ ex.a = 42
190
+
191
+ with pytest.raises(TypeError):
192
+ ex.b = "goodbye"
193
+
194
+
168
195
  def test_slot_gatherer_success():
169
196
 
170
197
  fields = {
@@ -1,2 +0,0 @@
1
- __version__ = "0.7.1"
2
- __version_tuple__ = (0, 7, 1)