ducktools-classbuilder 0.6.0__tar.gz → 0.6.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 (70) hide show
  1. {ducktools_classbuilder-0.6.0/src/ducktools_classbuilder.egg-info → ducktools_classbuilder-0.6.2}/PKG-INFO +1 -1
  2. ducktools_classbuilder-0.6.2/docs/approach_vs_tool.md +30 -0
  3. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/docs/extension_examples.md +8 -8
  4. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/docs/tutorial.md +2 -2
  5. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/src/ducktools/classbuilder/__init__.py +70 -25
  6. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/src/ducktools/classbuilder/__init__.pyi +25 -11
  7. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/src/ducktools/classbuilder/annotations.pyi +1 -1
  8. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/src/ducktools/classbuilder/prefab.py +160 -167
  9. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/src/ducktools/classbuilder/prefab.pyi +7 -6
  10. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2/src/ducktools_classbuilder.egg-info}/PKG-INFO +1 -1
  11. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/test_core.py +33 -2
  12. ducktools_classbuilder-0.6.0/docs/approach_vs_tool.md +0 -16
  13. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/LICENSE.md +0 -0
  14. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/MANIFEST.in +0 -0
  15. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/README.md +0 -0
  16. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/docs/Makefile +0 -0
  17. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/docs/api.md +0 -0
  18. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/docs/conf.py +0 -0
  19. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/docs/generated_code.md +0 -0
  20. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/docs/index.md +0 -0
  21. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/docs/make.bat +0 -0
  22. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/docs/perf/performance_tests.md +0 -0
  23. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/docs/prefab/index.md +0 -0
  24. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/pyproject.toml +0 -0
  25. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/setup.cfg +0 -0
  26. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/src/ducktools/classbuilder/annotations.py +0 -0
  27. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/src/ducktools/classbuilder/py.typed +0 -0
  28. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/src/ducktools_classbuilder.egg-info/SOURCES.txt +0 -0
  29. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/src/ducktools_classbuilder.egg-info/dependency_links.txt +0 -0
  30. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/src/ducktools_classbuilder.egg-info/requires.txt +0 -0
  31. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/src/ducktools_classbuilder.egg-info/top_level.txt +0 -0
  32. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/annotations/test_annotated.py +0 -0
  33. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/annotations/test_annotations_module.py +0 -0
  34. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/conftest.py +0 -0
  35. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/dynamic/test_compare_attrib.py +0 -0
  36. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/dynamic/test_construction.py +0 -0
  37. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/dynamic/test_internals.py +0 -0
  38. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/dynamic/test_pre_post_init.py +0 -0
  39. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/dynamic/test_slots_novalues.py +0 -0
  40. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/dynamic/test_slotted_class.py +0 -0
  41. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/dynamic/test_subclass_implementation.py +0 -0
  42. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/conftest.py +0 -0
  43. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/examples/creation.py +0 -0
  44. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/examples/creation_empty.py +0 -0
  45. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/examples/dunders.py +0 -0
  46. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/examples/fails/creation_1.py +0 -0
  47. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/examples/fails/creation_2.py +0 -0
  48. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/examples/fails/creation_3.py +0 -0
  49. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/examples/fails/creation_5.py +0 -0
  50. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/examples/fails/inheritance_1.py +0 -0
  51. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/examples/fails/inheritance_2.py +0 -0
  52. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/examples/frozen_prefabs.py +0 -0
  53. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/examples/funcs_prefabs.py +0 -0
  54. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/examples/hint_syntax.py +0 -0
  55. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/examples/inheritance.py +0 -0
  56. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/examples/init_ex.py +0 -0
  57. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/examples/kw_only.py +0 -0
  58. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/examples/repr_func.py +0 -0
  59. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/test_creation.py +0 -0
  60. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/test_dunders.py +0 -0
  61. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/test_frozen.py +0 -0
  62. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/test_funcs.py +0 -0
  63. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/test_hint_syntax.py +0 -0
  64. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/test_inheritance.py +0 -0
  65. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/test_init.py +0 -0
  66. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/test_kw_only.py +0 -0
  67. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/test_repr.py +0 -0
  68. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/py312_tests/test_generic_annotations.py +0 -0
  69. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/test_field_flags.py +0 -0
  70. {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.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.6.0
3
+ Version: 0.6.2
4
4
  Summary: Toolkit for creating class boilerplate generators
5
5
  Author: David C Ellis
6
6
  License: MIT License
@@ -0,0 +1,30 @@
1
+ # Approaches and tools #
2
+
3
+ As this module's code generation is inspired by the workings of [David Beazley's Cluegen](https://github.com/dabeaz/cluegen)
4
+ I thought it was briefly worth discussing his note on learning an approach vs using a tool.
5
+
6
+ I think that learning an approach is valuable, this module would not exist without the
7
+ example given by `cluegen`.
8
+
9
+ However, what I found was that in essentially every case where I wanted to use
10
+ these generating tools, I needed to modify them - often significantly.
11
+ It quickly became easier to just create my own tool and upload it as a package.
12
+
13
+ For example, `cluegen` has a few subtle "exercises for the reader". It needs extending
14
+ and fixing for some use-cases.
15
+ * Default values that are not builtins need to be passed as part of the globals
16
+ dict to `exec`.
17
+ * No support for mutable defaults.
18
+ * Subclass methods will be overwritten if they call a cluegen method that has not been
19
+ generated via `super().methodname(...)`
20
+ * `inspect.signature(cls)` does not work if `cls.__init__` has not already been generated.
21
+ (I think this is actually a bug in inspect).
22
+ * Need an extra filter to support things like `ClassVar`.
23
+
24
+ In the general spirit though, this module intends to provide some basic tools to help
25
+ create your own customized boilerplate generators.
26
+ The generator included in the base module is intended to be used to help 'bootstrap' a
27
+ modified generator with features that work how **you** want them to work.
28
+
29
+ The `prefab` module is the more fully featured tool that handles the additional cases *I*
30
+ needed.
@@ -79,12 +79,12 @@ from ducktools.classbuilder import (
79
79
  )
80
80
 
81
81
 
82
- def iter_generator(cls):
82
+ def iter_generator(cls, funcname="__iter__"):
83
83
  field_names = get_fields(cls).keys()
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 __iter__(self):\n" f"{field_yield}"
87
+ code = f"def {funcname}(self):\n" f"{field_yield}"
88
88
  globs = {}
89
89
  return GeneratedCode(code, globs)
90
90
 
@@ -143,7 +143,7 @@ class PosOnlyField(Field):
143
143
  __slots__ = SlotFields(pos_only=True)
144
144
 
145
145
 
146
- def init_generator(cls):
146
+ def init_generator(cls, funcname="__init__"):
147
147
  fields = get_fields(cls)
148
148
 
149
149
  arglist = []
@@ -177,11 +177,11 @@ def init_generator(cls):
177
177
 
178
178
  args = ", ".join(arglist)
179
179
  assigns = "\n ".join(assignments)
180
- code = f"def __init__(self, {args}):\n" f" {assigns}\n"
180
+ code = f"def {funcname}(self, {args}):\n" f" {assigns}\n"
181
181
  return GeneratedCode(code, globs)
182
182
 
183
183
 
184
- def repr_generator(cls):
184
+ def repr_generator(cls, funcname="__repr__"):
185
185
  fields = get_fields(cls)
186
186
  content_list = []
187
187
  for name, field in fields.items():
@@ -193,7 +193,7 @@ def repr_generator(cls):
193
193
 
194
194
  content = ", ".join(content_list)
195
195
  code = (
196
- f"def __repr__(self):\n"
196
+ f"def {funcname}(self):\n"
197
197
  f" return f'{{type(self).__qualname__}}({content})'\n"
198
198
  )
199
199
  globs = {}
@@ -281,7 +281,7 @@ class ConverterField(Field):
281
281
  converter = Field(default=None)
282
282
 
283
283
 
284
- def setattr_generator(cls):
284
+ def setattr_generator(cls, funcname="__setattr__"):
285
285
  fields = get_fields(cls)
286
286
  converters = {}
287
287
  for k, v in fields.items():
@@ -294,7 +294,7 @@ def setattr_generator(cls):
294
294
  }
295
295
 
296
296
  code = (
297
- f"def __setattr__(self, name, value):\n"
297
+ f"def {funcname}(self, name, value):\n"
298
298
  f" if conv := _converters.get(name):\n"
299
299
  f" _object_setattr(self, name, conv(value))\n"
300
300
  f" else:\n"
@@ -120,7 +120,7 @@ field_3: <HIDDEN>
120
120
  ```
121
121
 
122
122
  ```python
123
- def report_generator(cls):
123
+ def report_generator(cls, funcname="report"):
124
124
  fields = dtbuild.get_fields(cls)
125
125
 
126
126
  field_reports = []
@@ -135,7 +135,7 @@ def report_generator(cls):
135
135
 
136
136
  code = (
137
137
  "@property\n"
138
- "def report(self):\n"
138
+ f"def {funcname}(self):\n"
139
139
  f" return f\"{class_str}\\n{reports_str}\""
140
140
  )
141
141
  globs = {}
@@ -34,7 +34,7 @@ import sys
34
34
 
35
35
  from .annotations import get_ns_annotations, is_classvar
36
36
 
37
- __version__ = "v0.6.0"
37
+ __version__ = "v0.6.2"
38
38
 
39
39
  # Change this name if you make heavy modifications
40
40
  INTERNALS_DICT = "__classbuilder_internals__"
@@ -158,36 +158,73 @@ class MethodMaker:
158
158
  def __repr__(self):
159
159
  return f"<MethodMaker for {self.funcname!r} method>"
160
160
 
161
- def __get__(self, obj, objtype=None):
162
- if objtype is None or issubclass(objtype, type):
163
- # Called with get(ourclass, type(ourclass))
164
- cls = obj
161
+ def __get__(self, inst, cls):
162
+ local_vars = {}
163
+
164
+ # This can be called via super().funcname(...) in which case the class
165
+ # may not be the correct one. If this is the correct class
166
+ # it should have this descriptor in the class dict under
167
+ # the correct funcname.
168
+ # Otherwise is should be found in the MRO of the class.
169
+ if cls.__dict__.get(self.funcname) is self:
170
+ gen_cls = cls
165
171
  else:
166
- # Called with get(inst | None, ourclass)
167
- cls = objtype
172
+ for c in cls.__mro__[1:]: # skip 'cls' as special cased
173
+ if c.__dict__.get(self.funcname) is self:
174
+ gen_cls = c
175
+ break
176
+ else: # pragma: no cover
177
+ # This should only be reached if called with incorrect arguments
178
+ # manually
179
+ raise AttributeError(
180
+ f"Could not find {self!r} in class {cls.__name__!r} MRO."
181
+ )
168
182
 
169
- local_vars = {}
170
- gen = self.code_generator(cls)
183
+ gen = self.code_generator(gen_cls, self.funcname)
171
184
  exec(gen.source_code, gen.globs, local_vars)
172
185
  method = local_vars.get(self.funcname)
173
186
 
174
187
  try:
175
- method.__qualname__ = f"{cls.__qualname__}.{self.funcname}"
188
+ method.__qualname__ = f"{gen_cls.__qualname__}.{self.funcname}"
176
189
  except AttributeError:
177
190
  # This might be a property or some other special
178
191
  # descriptor. Don't try to rename.
179
192
  pass
180
193
 
181
194
  # Replace this descriptor on the class with the generated function
182
- setattr(cls, self.funcname, method)
195
+ setattr(gen_cls, self.funcname, method)
183
196
 
184
197
  # Use 'get' to return the generated function as a bound method
185
198
  # instead of as a regular function for first usage.
186
- return method.__get__(obj, objtype)
199
+ return method.__get__(inst, cls)
200
+
201
+
202
+ class _SignatureMaker:
203
+ # 'inspect.signature' calls the `__get__` method of the `__init__` methodmaker with
204
+ # the wrong arguments.
205
+ # Instead of __get__(None, cls) or __get__(inst, type(inst))
206
+ # it uses __get__(cls, type(cls)).
207
+ #
208
+ # If this is done before `__init__` has been generated then
209
+ # help(cls) will fail along with inspect.signature(cls)
210
+ # This signature maker descriptor is placed to override __signature__ and force
211
+ # the `__init__` signature to be generated first if the signature is requested.
212
+ def __get__(self, instance, cls):
213
+ import inspect # Deferred inspect import
214
+ _ = cls.__init__ # force generation of `__init__` function
215
+ # Remove this attribute from the class
216
+ # This prevents recursion back into this __get__ method.
217
+ delattr(cls, "__signature__")
218
+ sig = inspect.signature(cls)
219
+ setattr(cls, "__signature__", sig)
220
+ return sig
221
+
222
+
223
+ signature_maker = _SignatureMaker()
187
224
 
188
225
 
189
226
  def get_init_generator(null=NOTHING, extra_code=None):
190
- def cls_init_maker(cls):
227
+ def cls_init_maker(cls, funcname="__init__"):
191
228
  fields = get_fields(cls)
192
229
  flags = get_flags(cls)
193
230
 
@@ -239,7 +276,7 @@ def get_init_generator(null=NOTHING, extra_code=None):
239
276
 
240
277
  assigns = "\n ".join(assignments) if assignments else "pass\n"
241
278
  code = (
242
- f"def __init__(self, {args}):\n"
279
+ f"def {funcname}(self, {args}):\n"
243
280
  f" {assigns}\n"
244
281
  )
245
282
  # Handle additional function calls
@@ -265,7 +302,7 @@ def get_repr_generator(recursion_safe=False, eval_safe=False):
265
302
  not evaluate.
266
303
  :return:
267
304
  """
268
- def cls_repr_generator(cls):
305
+ def cls_repr_generator(cls, funcname="__repr__"):
269
306
  fields = get_fields(cls)
270
307
 
271
308
  globs = {}
@@ -295,19 +332,19 @@ def get_repr_generator(recursion_safe=False, eval_safe=False):
295
332
  if content:
296
333
  code = (
297
334
  f"{recursion_func}"
298
- f"def __repr__(self):\n"
335
+ f"def {funcname}(self):\n"
299
336
  f" return f'<generated class {{type(self).__qualname__}}; {content}>'\n"
300
337
  )
301
338
  else:
302
339
  code = (
303
340
  f"{recursion_func}"
304
- f"def __repr__(self):\n"
341
+ f"def {funcname}(self):\n"
305
342
  f" return f'<generated class {{type(self).__qualname__}}>'\n"
306
343
  )
307
344
  else:
308
345
  code = (
309
346
  f"{recursion_func}"
310
- f"def __repr__(self):\n"
347
+ f"def {funcname}(self):\n"
311
348
  f" return f'{{type(self).__qualname__}}({content})'\n"
312
349
  )
313
350
 
@@ -318,7 +355,7 @@ def get_repr_generator(recursion_safe=False, eval_safe=False):
318
355
  repr_generator = get_repr_generator()
319
356
 
320
357
 
321
- def eq_generator(cls):
358
+ def eq_generator(cls, funcname="__eq__"):
322
359
  class_comparison = "self.__class__ is other.__class__"
323
360
  field_names = [
324
361
  name
@@ -334,7 +371,7 @@ def eq_generator(cls):
334
371
  instance_comparison = "True"
335
372
 
336
373
  code = (
337
- f"def __eq__(self, other):\n"
374
+ f"def {funcname}(self, other):\n"
338
375
  f" return {instance_comparison} if {class_comparison} else NotImplemented\n"
339
376
  )
340
377
  globs = {}
@@ -342,7 +379,7 @@ def eq_generator(cls):
342
379
  return GeneratedCode(code, globs)
343
380
 
344
381
 
345
- def frozen_setattr_generator(cls):
382
+ def frozen_setattr_generator(cls, funcname="__setattr__"):
346
383
  globs = {}
347
384
  field_names = set(get_fields(cls))
348
385
  flags = get_flags(cls)
@@ -366,19 +403,19 @@ def frozen_setattr_generator(cls):
366
403
  f" else:\n"
367
404
  f" {setattr_method}\n"
368
405
  )
369
- code = f"def __setattr__(self, name, value):\n{body}"
406
+ code = f"def {funcname}(self, name, value):\n{body}"
370
407
 
371
408
  return GeneratedCode(code, globs)
372
409
 
373
410
 
374
- def frozen_delattr_generator(cls):
411
+ def frozen_delattr_generator(cls, funcname="__delattr__"):
375
412
  body = (
376
413
  ' raise TypeError(\n'
377
414
  ' f"{type(self).__name__!r} object "\n'
378
415
  ' f"does not support attribute deletion"\n'
379
416
  ' )\n'
380
417
  )
381
- code = f"def __delattr__(self, name):\n{body}"
418
+ code = f"def {funcname}(self, name):\n{body}"
382
419
  globs = {}
383
420
  return GeneratedCode(code, globs)
384
421
 
@@ -402,7 +439,7 @@ _field_init_maker = MethodMaker(
402
439
  )
403
440
 
404
441
 
405
- def builder(cls=None, /, *, gatherer, methods, flags=None):
442
+ def builder(cls=None, /, *, gatherer, methods, flags=None, fix_signature=True):
406
443
  """
407
444
  The main builder for class generation
408
445
 
@@ -413,6 +450,10 @@ def builder(cls=None, /, *, gatherer, methods, flags=None):
413
450
  :type methods: set[MethodMaker]
414
451
  :param flags: additional flags to store in the internals dictionary
415
452
  for use by method generators.
453
+ :type flags: None | dict[str, bool]
454
+ :param fix_signature: Add a __signature__ attribute to work-around an issue with
455
+ inspect.signature incorrectly handling __init__ descriptors.
456
+ :type fix_signature: bool
416
457
  :return: The modified class (the class itself is modified, but this is expected).
417
458
  """
418
459
  # Handle `None` to make wrapping with a decorator easier.
@@ -459,6 +500,10 @@ def builder(cls=None, /, *, gatherer, methods, flags=None):
459
500
 
460
501
  internals["methods"] = _MappingProxyType(internal_methods)
461
502
 
503
+ # Fix for inspect.signature(cls)
504
+ if fix_signature:
505
+ setattr(cls, "__signature__", signature_maker)
506
+
462
507
  return cls
463
508
 
464
509
 
@@ -1,6 +1,8 @@
1
1
  import types
2
2
  import typing
3
3
 
4
+ import inspect
5
+
4
6
  from collections.abc import Callable
5
7
  from types import MappingProxyType
6
8
  from typing_extensions import dataclass_transform
@@ -30,7 +32,10 @@ class _KW_ONLY_TYPE:
30
32
 
31
33
  KW_ONLY: _KW_ONLY_TYPE
32
34
  # Stub Only
33
- _codegen_type = Callable[[type], GeneratedCode]
35
+ @typing.type_check_only
36
+ class _CodegenType(typing.Protocol):
37
+ def __call__(self, cls: type, funcname: str = ...) -> GeneratedCode: ...
38
+
34
39
 
35
40
  class GeneratedCode:
36
41
  __slots__: tuple[str, str]
@@ -43,28 +48,33 @@ class GeneratedCode:
43
48
 
44
49
  class MethodMaker:
45
50
  funcname: str
46
- code_generator: _codegen_type
47
- def __init__(self, funcname: str, code_generator: _codegen_type) -> None: ...
51
+ code_generator: _CodegenType
52
+ def __init__(self, funcname: str, code_generator: _CodegenType) -> None: ...
48
53
  def __repr__(self) -> str: ...
49
- def __get__(self, instance, cls=None) -> Callable: ...
54
+ def __get__(self, instance, cls) -> Callable: ...
55
+
56
+ class _SignatureMaker:
57
+ def __get__(self, instance, cls) -> inspect.Signature: ...
58
+
59
+ signature_maker: _SignatureMaker
50
60
 
51
61
  def get_init_generator(
52
62
  null: _NothingType = NOTHING,
53
63
  extra_code: None | list[str] = None
54
- ) -> Callable[[type], GeneratedCode]: ...
64
+ ) -> _CodegenType: ...
55
65
 
56
- def init_generator(cls: type) -> GeneratedCode: ...
66
+ def init_generator(cls: type, funcname: str="__init__") -> GeneratedCode: ...
57
67
 
58
68
  def get_repr_generator(
59
69
  recursion_safe: bool = False,
60
70
  eval_safe: bool = False
61
- ) -> Callable[[type], GeneratedCode]: ...
62
- def repr_generator(cls: type) -> GeneratedCode: ...
63
- def eq_generator(cls: type) -> GeneratedCode: ...
71
+ ) -> _CodegenType: ...
72
+ def repr_generator(cls: type, funcname: str = "__repr__") -> GeneratedCode: ...
73
+ def eq_generator(cls: type, funcname: str = "__eq__") -> GeneratedCode: ...
64
74
 
65
- def frozen_setattr_generator(cls: type) -> GeneratedCode: ...
75
+ def frozen_setattr_generator(cls: type, funcname: str = "__setattr__") -> GeneratedCode: ...
66
76
 
67
- def frozen_delattr_generator(cls: type) -> GeneratedCode: ...
77
+ def frozen_delattr_generator(cls: type, funcname: str = "__delattr__") -> GeneratedCode: ...
68
78
 
69
79
  init_maker: MethodMaker
70
80
  repr_maker: MethodMaker
@@ -83,6 +93,7 @@ def builder(
83
93
  gatherer: Callable[[type], tuple[dict[str, Field], dict[str, typing.Any]]],
84
94
  methods: frozenset[MethodMaker] | set[MethodMaker],
85
95
  flags: dict[str, bool] | None = None,
96
+ fix_signature: bool = ...,
86
97
  ) -> type[_T]: ...
87
98
 
88
99
  @typing.overload
@@ -93,6 +104,7 @@ def builder(
93
104
  gatherer: Callable[[type], tuple[dict[str, Field], dict[str, typing.Any]]],
94
105
  methods: frozenset[MethodMaker] | set[MethodMaker],
95
106
  flags: dict[str, bool] | None = None,
107
+ fix_signature: bool = ...,
96
108
  ) -> Callable[[type[_T]], type[_T]]: ...
97
109
 
98
110
 
@@ -123,6 +135,7 @@ class Field(metaclass=SlotMakerMeta):
123
135
 
124
136
  __slots__: dict[str, str]
125
137
  __classbuilder_internals__: dict
138
+ __signature__: inspect.Signature
126
139
 
127
140
  def __init__(
128
141
  self,
@@ -246,6 +259,7 @@ class GatheredFields:
246
259
  modifications: dict[str, typing.Any]
247
260
 
248
261
  __classbuilder_internals__: dict
262
+ __signature__: inspect.Signature
249
263
 
250
264
  def __init__(
251
265
  self,
@@ -4,7 +4,7 @@ import types
4
4
  _T = typing.TypeVar("_T")
5
5
  _CopiableMappings = dict[str, typing.Any] | types.MappingProxyType[str, typing.Any]
6
6
 
7
- class _StringGlobs:
7
+ class _StringGlobs(dict):
8
8
  def __missing__(self, key: _T) -> _T: ...
9
9
 
10
10
 
@@ -60,202 +60,195 @@ def get_attributes(cls):
60
60
 
61
61
 
62
62
  # Method Generators
63
- def get_init_maker(*, init_name="__init__"):
64
- def __init__(cls: type) -> GeneratedCode:
65
- globs = {}
66
- # Get the internals dictionary and prepare attributes
67
- attributes = get_attributes(cls)
68
- flags = get_flags(cls)
69
-
70
- kw_only = flags.get("kw_only", False)
71
-
72
- # Handle pre/post init first - post_init can change types for __init__
73
- # Get pre and post init arguments
74
- pre_init_args = []
75
- post_init_args = []
76
- post_init_annotations = {}
77
-
78
- for func_name, func_arglist in [
79
- (PRE_INIT_FUNC, pre_init_args),
80
- (POST_INIT_FUNC, post_init_args),
81
- ]:
82
- try:
83
- func = getattr(cls, func_name)
84
- func_code = func.__code__
85
- except AttributeError:
86
- pass
87
- else:
88
- argcount = func_code.co_argcount + func_code.co_kwonlyargcount
63
+ def init_generator(cls, funcname="__init__"):
64
+ globs = {}
65
+ # Get the internals dictionary and prepare attributes
66
+ attributes = get_attributes(cls)
67
+ flags = get_flags(cls)
89
68
 
90
- # Identify if method is static, if so include first arg, otherwise skip
91
- is_static = type(cls.__dict__.get(func_name)) is staticmethod
69
+ kw_only = flags.get("kw_only", False)
92
70
 
93
- arglist = (
94
- func_code.co_varnames[:argcount]
95
- if is_static
96
- else func_code.co_varnames[1:argcount]
97
- )
71
+ # Handle pre/post init first - post_init can change types for __init__
72
+ # Get pre and post init arguments
73
+ pre_init_args = []
74
+ post_init_args = []
75
+ post_init_annotations = {}
76
+
77
+ for extra_funcname, func_arglist in [
78
+ (PRE_INIT_FUNC, pre_init_args),
79
+ (POST_INIT_FUNC, post_init_args),
80
+ ]:
81
+ try:
82
+ func = getattr(cls, extra_funcname)
83
+ func_code = func.__code__
84
+ except AttributeError:
85
+ pass
86
+ else:
87
+ argcount = func_code.co_argcount + func_code.co_kwonlyargcount
98
88
 
99
- func_arglist.extend(arglist)
100
-
101
- if func_name == POST_INIT_FUNC:
102
- post_init_annotations.update(func.__annotations__)
103
-
104
- pos_arglist = []
105
- kw_only_arglist = []
106
- for name, attrib in attributes.items():
107
- # post_init annotations can be used to broaden types.
108
- if name in post_init_annotations:
109
- globs[f"_{name}_type"] = post_init_annotations[name]
110
- elif attrib.type is not NOTHING:
111
- globs[f"_{name}_type"] = attrib.type
112
-
113
- if attrib.init:
114
- if attrib.default is not NOTHING:
115
- if isinstance(attrib.default, (str, int, float, bool)):
116
- # Just use the literal in these cases
117
- if attrib.type is NOTHING:
118
- arg = f"{name}={attrib.default!r}"
119
- else:
120
- arg = f"{name}: _{name}_type = {attrib.default!r}"
121
- else:
122
- # No guarantee repr will work for other objects
123
- # so store the value in a variable and put it
124
- # in the globals dict for eval
125
- if attrib.type is NOTHING:
126
- arg = f"{name}=_{name}_default"
127
- else:
128
- arg = f"{name}: _{name}_type = _{name}_default"
129
- globs[f"_{name}_default"] = attrib.default
130
- elif attrib.default_factory is not NOTHING:
131
- # Use NONE here and call the factory later
132
- # This matches the behaviour of compiled
89
+ # Identify if method is static, if so include first arg, otherwise skip
90
+ is_static = type(cls.__dict__.get(extra_funcname)) is staticmethod
91
+
92
+ arglist = (
93
+ func_code.co_varnames[:argcount]
94
+ if is_static
95
+ else func_code.co_varnames[1:argcount]
96
+ )
97
+
98
+ func_arglist.extend(arglist)
99
+
100
+ if extra_funcname == POST_INIT_FUNC:
101
+ post_init_annotations.update(func.__annotations__)
102
+
103
+ pos_arglist = []
104
+ kw_only_arglist = []
105
+ for name, attrib in attributes.items():
106
+ # post_init annotations can be used to broaden types.
107
+ if name in post_init_annotations:
108
+ globs[f"_{name}_type"] = post_init_annotations[name]
109
+ elif attrib.type is not NOTHING:
110
+ globs[f"_{name}_type"] = attrib.type
111
+
112
+ if attrib.init:
113
+ if attrib.default is not NOTHING:
114
+ if isinstance(attrib.default, (str, int, float, bool)):
115
+ # Just use the literal in these cases
133
116
  if attrib.type is NOTHING:
134
- arg = f"{name}=None"
117
+ arg = f"{name}={attrib.default!r}"
135
118
  else:
136
- arg = f"{name}: _{name}_type = None"
137
- globs[f"_{name}_factory"] = attrib.default_factory
119
+ arg = f"{name}: _{name}_type = {attrib.default!r}"
138
120
  else:
121
+ # No guarantee repr will work for other objects
122
+ # so store the value in a variable and put it
123
+ # in the globals dict for eval
139
124
  if attrib.type is NOTHING:
140
- arg = name
125
+ arg = f"{name}=_{name}_default"
141
126
  else:
142
- arg = f"{name}: _{name}_type"
143
- if attrib.kw_only or kw_only:
144
- kw_only_arglist.append(arg)
145
- else:
146
- pos_arglist.append(arg)
147
- # Not in init, but need to set defaults
148
- else:
149
- if attrib.default is not NOTHING:
127
+ arg = f"{name}: _{name}_type = _{name}_default"
150
128
  globs[f"_{name}_default"] = attrib.default
151
- elif attrib.default_factory is not NOTHING:
152
- globs[f"_{name}_factory"] = attrib.default_factory
153
-
154
- pos_args = ", ".join(pos_arglist)
155
- kw_args = ", ".join(kw_only_arglist)
156
- if pos_args and kw_args:
157
- args = f"{pos_args}, *, {kw_args}"
158
- elif kw_args:
159
- args = f"*, {kw_args}"
160
- else:
161
- args = pos_args
162
-
163
- assignments = []
164
- processes = [] # post_init values still need default factories to be called.
165
- for name, attrib in attributes.items():
166
- if attrib.init:
167
- if attrib.default_factory is not NOTHING:
168
- value = f"{name} if {name} is not None else _{name}_factory()"
129
+ elif attrib.default_factory is not NOTHING:
130
+ # Use NONE here and call the factory later
131
+ # This matches the behaviour of compiled
132
+ if attrib.type is NOTHING:
133
+ arg = f"{name}=None"
169
134
  else:
170
- value = name
135
+ arg = f"{name}: _{name}_type = None"
136
+ globs[f"_{name}_factory"] = attrib.default_factory
171
137
  else:
172
- if attrib.default_factory is not NOTHING:
173
- value = f"_{name}_factory()"
174
- elif attrib.default is not NOTHING:
175
- value = f"_{name}_default"
138
+ if attrib.type is NOTHING:
139
+ arg = name
176
140
  else:
177
- value = None
178
-
179
- if name in post_init_args:
180
- if attrib.default_factory is not NOTHING:
181
- processes.append((name, value))
182
- elif value is not None:
183
- assignments.append((name, value))
184
-
185
- if hasattr(cls, PRE_INIT_FUNC):
186
- pre_init_arg_call = ", ".join(f"{name}={name}" for name in pre_init_args)
187
- pre_init_call = f" self.{PRE_INIT_FUNC}({pre_init_arg_call})\n"
141
+ arg = f"{name}: _{name}_type"
142
+ if attrib.kw_only or kw_only:
143
+ kw_only_arglist.append(arg)
144
+ else:
145
+ pos_arglist.append(arg)
146
+ # Not in init, but need to set defaults
188
147
  else:
189
- pre_init_call = ""
190
-
191
- if assignments or processes:
192
- body = ""
193
- body += "\n".join(
194
- f" self.{name} = {value}" for name, value in assignments
195
- )
196
- body += "\n"
197
- body += "\n".join(f" {name} = {value}" for name, value in processes)
148
+ if attrib.default is not NOTHING:
149
+ globs[f"_{name}_default"] = attrib.default
150
+ elif attrib.default_factory is not NOTHING:
151
+ globs[f"_{name}_factory"] = attrib.default_factory
152
+
153
+ pos_args = ", ".join(pos_arglist)
154
+ kw_args = ", ".join(kw_only_arglist)
155
+ if pos_args and kw_args:
156
+ args = f"{pos_args}, *, {kw_args}"
157
+ elif kw_args:
158
+ args = f"*, {kw_args}"
159
+ else:
160
+ args = pos_args
161
+
162
+ assignments = []
163
+ processes = [] # post_init values still need default factories to be called.
164
+ for name, attrib in attributes.items():
165
+ if attrib.init:
166
+ if attrib.default_factory is not NOTHING:
167
+ value = f"{name} if {name} is not None else _{name}_factory()"
168
+ else:
169
+ value = name
198
170
  else:
199
- body = " pass"
171
+ if attrib.default_factory is not NOTHING:
172
+ value = f"_{name}_factory()"
173
+ elif attrib.default is not NOTHING:
174
+ value = f"_{name}_default"
175
+ else:
176
+ value = None
200
177
 
201
- if hasattr(cls, POST_INIT_FUNC):
202
- post_init_arg_call = ", ".join(f"{name}={name}" for name in post_init_args)
203
- post_init_call = f" self.{POST_INIT_FUNC}({post_init_arg_call})\n"
204
- else:
205
- post_init_call = ""
178
+ if name in post_init_args:
179
+ if attrib.default_factory is not NOTHING:
180
+ processes.append((name, value))
181
+ elif value is not None:
182
+ assignments.append((name, value))
183
+
184
+ if hasattr(cls, PRE_INIT_FUNC):
185
+ pre_init_arg_call = ", ".join(f"{name}={name}" for name in pre_init_args)
186
+ pre_init_call = f" self.{PRE_INIT_FUNC}({pre_init_arg_call})\n"
187
+ else:
188
+ pre_init_call = ""
206
189
 
207
- code = (
208
- f"def {init_name}(self, {args}):\n"
209
- f"{pre_init_call}\n"
210
- f"{body}\n"
211
- f"{post_init_call}\n"
190
+ if assignments or processes:
191
+ body = ""
192
+ body += "\n".join(
193
+ f" self.{name} = {value}" for name, value in assignments
212
194
  )
213
- return GeneratedCode(code, globs)
195
+ body += "\n"
196
+ body += "\n".join(f" {name} = {value}" for name, value in processes)
197
+ else:
198
+ body = " pass"
214
199
 
215
- return MethodMaker(init_name, __init__)
200
+ if hasattr(cls, POST_INIT_FUNC):
201
+ post_init_arg_call = ", ".join(f"{name}={name}" for name in post_init_args)
202
+ post_init_call = f" self.{POST_INIT_FUNC}({post_init_arg_call})\n"
203
+ else:
204
+ post_init_call = ""
205
+
206
+ code = (
207
+ f"def {funcname}(self, {args}):\n"
208
+ f"{pre_init_call}\n"
209
+ f"{body}\n"
210
+ f"{post_init_call}\n"
211
+ )
216
212
 
213
+ return GeneratedCode(code, globs)
217
214
 
218
- def get_iter_maker():
219
- def __iter__(cls: type) -> GeneratedCode:
220
- fields = get_attributes(cls)
221
215
 
222
- valid_fields = (
223
- name for name, attrib in fields.items()
224
- if attrib.iter
225
- )
216
+ def iter_generator(cls, funcname="__iter__"):
217
+ fields = get_attributes(cls)
226
218
 
227
- values = "\n".join(f" yield self.{name}" for name in valid_fields)
219
+ valid_fields = (
220
+ name for name, attrib in fields.items()
221
+ if attrib.iter
222
+ )
228
223
 
229
- # if values is an empty string
230
- if not values:
231
- values = " yield from ()"
224
+ values = "\n".join(f" yield self.{name}" for name in valid_fields)
232
225
 
233
- code = f"def __iter__(self):\n{values}"
234
- globs = {}
235
- return GeneratedCode(code, globs)
226
+ # if values is an empty string
227
+ if not values:
228
+ values = " yield from ()"
236
229
 
237
- return MethodMaker("__iter__", __iter__)
230
+ code = f"def {funcname}(self):\n{values}"
231
+ globs = {}
232
+ return GeneratedCode(code, globs)
238
233
 
239
234
 
240
- def get_asdict_maker():
241
- def as_dict_gen(cls: type) -> GeneratedCode:
242
- fields = get_attributes(cls)
235
+ def as_dict_generator(cls, funcname="as_dict"):
236
+ fields = get_attributes(cls)
243
237
 
244
- vals = ", ".join(
245
- f"'{name}': self.{name}"
246
- for name, attrib in fields.items()
247
- if attrib.serialize
248
- )
249
- out_dict = f"{{{vals}}}"
250
- code = f"def as_dict(self): return {out_dict}"
238
+ vals = ", ".join(
239
+ f"'{name}': self.{name}"
240
+ for name, attrib in fields.items()
241
+ if attrib.serialize
242
+ )
243
+ out_dict = f"{{{vals}}}"
244
+ code = f"def {funcname}(self): return {out_dict}"
251
245
 
252
- globs = {}
253
- return GeneratedCode(code, globs)
254
- return MethodMaker("as_dict", as_dict_gen)
246
+ globs = {}
247
+ return GeneratedCode(code, globs)
255
248
 
256
249
 
257
- init_maker = get_init_maker()
258
- prefab_init_maker = get_init_maker(init_name=PREFAB_INIT_FUNC)
250
+ init_maker = MethodMaker("__init__", init_generator)
251
+ prefab_init_maker = MethodMaker(PREFAB_INIT_FUNC, init_generator)
259
252
  repr_maker = MethodMaker(
260
253
  "__repr__",
261
254
  get_repr_generator(recursion_safe=False, eval_safe=True)
@@ -264,8 +257,8 @@ recursive_repr_maker = MethodMaker(
264
257
  "__repr__",
265
258
  get_repr_generator(recursion_safe=True, eval_safe=True)
266
259
  )
267
- iter_maker = get_iter_maker()
268
- asdict_maker = get_asdict_maker()
260
+ iter_maker = MethodMaker("__iter__", iter_generator)
261
+ asdict_maker = MethodMaker("as_dict", as_dict_generator)
269
262
 
270
263
 
271
264
  # Updated field with additional attributes
@@ -2,11 +2,14 @@ import typing
2
2
  from types import MappingProxyType
3
3
  from typing_extensions import dataclass_transform
4
4
 
5
+ import inspect
6
+
5
7
  from collections.abc import Callable
6
8
 
7
9
  from . import (
8
10
  NOTHING,
9
11
  Field,
12
+ GeneratedCode,
10
13
  MethodMaker,
11
14
  SlotMakerMeta,
12
15
  )
@@ -27,12 +30,9 @@ class PrefabError(Exception): ...
27
30
 
28
31
  def get_attributes(cls: type) -> dict[str, Attribute]: ...
29
32
 
30
- def get_init_maker(*, init_name: str="__init__") -> MethodMaker: ...
31
-
32
- def get_iter_maker() -> MethodMaker: ...
33
-
34
- def get_asdict_maker() -> MethodMaker: ...
35
-
33
+ def init_generator(cls: type, funcname: str = "__init__") -> GeneratedCode: ...
34
+ def iter_generator(cls: type, funcname: str = "__iter__") -> GeneratedCode: ...
35
+ def as_dict_generator(cls: type, funcname: str = "as_dict") -> GeneratedCode: ...
36
36
 
37
37
  init_maker: MethodMaker
38
38
  prefab_init_maker: MethodMaker
@@ -44,6 +44,7 @@ asdict_maker: MethodMaker
44
44
 
45
45
  class Attribute(Field):
46
46
  __slots__: dict
47
+ __signature__: inspect.Signature
47
48
 
48
49
  iter: bool
49
50
  serialize: bool
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ducktools-classbuilder
3
- Version: 0.6.0
3
+ Version: 0.6.2
4
4
  Summary: Toolkit for creating class boilerplate generators
5
5
  Author: David C Ellis
6
6
  License: MIT License
@@ -8,6 +8,7 @@ from ducktools.classbuilder import (
8
8
 
9
9
  builder,
10
10
  default_methods,
11
+ eq_maker,
11
12
  get_fields,
12
13
  get_flags,
13
14
  get_methods,
@@ -50,8 +51,8 @@ def test_get_fields_flags_methods():
50
51
 
51
52
 
52
53
  def test_method_maker():
53
- def generator(cls):
54
- code = "def demo(self): return self.x"
54
+ def generator(cls, funcname="demo"):
55
+ code = f"def {funcname}(self): return self.x"
55
56
  globs = {}
56
57
  return GeneratedCode(code, globs)
57
58
 
@@ -527,3 +528,33 @@ def test_signature():
527
528
  __slots__ = SlotFields(x=42)
528
529
 
529
530
  assert str(inspect.signature(SigClass)) == "(x=42)"
531
+
532
+
533
+ def test_subclass_method_not_overwritten():
534
+ @slotclass
535
+ class X:
536
+ __slots__ = SlotFields(x=Field())
537
+
538
+ class Y(X):
539
+ def __init__(self, x, y):
540
+ self.y = y
541
+ super().__init__(x=x)
542
+
543
+ y_init_func = Y.__init__
544
+
545
+ assert X.__dict__["__eq__"] is eq_maker
546
+
547
+ y_inst = Y(0, 1)
548
+
549
+ # super().__init__ method generated correctly
550
+ assert y_init_func is Y.__init__
551
+ assert X.__dict__["__init__"] is not init_maker
552
+ assert (y_inst.x, y_inst.y) == (0, 1)
553
+
554
+ # Would fail previously as __init__ would be overwritten
555
+ y_inst_2 = Y(0, 2)
556
+
557
+ assert y_inst == y_inst_2
558
+
559
+ assert X.__dict__["__eq__"] is not eq_maker
560
+ assert "__eq__" not in Y.__dict__
@@ -1,16 +0,0 @@
1
- # Approaches and tools #
2
-
3
- As this module's code generation is inspired by the workings of [David Beazley's Cluegen](https://github.com/dabeaz/cluegen)
4
- I thought it was briefly worth discussing his note on learning an approach vs using a tool.
5
-
6
- I think that learning an approach is valuable, this module would not exist without the
7
- example given by `cluegen`. It also wouldn't exist if I hadn't needed to extend `cluegen`
8
- for some basic features (try using `Path` default values with `cluegen`).
9
-
10
- In the general spirit though, this module intends to provide some basic tools to help
11
- build your own custom class generators.
12
- The generator included in the base module is intended to be used to help 'bootstrap' a
13
- modified generator with features that work how **you** want them to work.
14
-
15
- The `prefab` module is the more fully featured powertool *I* built with these tools.
16
- However, much like a prefabricated building, it may not be what you desire.