ducktools-classbuilder 0.6.0__py3-none-any.whl → 0.6.2__py3-none-any.whl

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.

@@ -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
@@ -0,0 +1,12 @@
1
+ ducktools/classbuilder/__init__.py,sha256=9IBwSOSXY9SFbbV6msfS9oUx2BneNrQvjZsb-iLSLZ4,32820
2
+ ducktools/classbuilder/__init__.pyi,sha256=JaAfc6y4Jj920y4lm9peduNXeb9XUJwKN-QqFpqG5pw,7924
3
+ ducktools/classbuilder/annotations.py,sha256=17g1WnWfyMWBHKQp2HD_hvV-n7CKXy_zv8NTODpOsuU,5437
4
+ ducktools/classbuilder/annotations.pyi,sha256=cTZQ-pp2IkfdvwHiU3pIySUzzBKxfOtO-e5PkA8TNwo,520
5
+ ducktools/classbuilder/prefab.py,sha256=LCrnS2zKADX5FS0SdzCYLF4n5yR43CIlv2QLL6PDjl4,23292
6
+ ducktools/classbuilder/prefab.pyi,sha256=kmqZcP2aGHEAb3-cLCBlYi6gclzsQAAmY7cnHvKT7jQ,4360
7
+ ducktools/classbuilder/py.typed,sha256=la67KBlbjXN-_-DfGNcdOcjYumVpKG_Tkw-8n5dnGB4,8
8
+ ducktools_classbuilder-0.6.2.dist-info/LICENSE.md,sha256=6Thz9Dbw8R4fWInl6sGl8Rj3UnKnRbDwrc6jZerpugQ,1070
9
+ ducktools_classbuilder-0.6.2.dist-info/METADATA,sha256=fGjzXJdePiVfTkzZozk9-HHFW-zZJakoPnBrTmMepAE,10911
10
+ ducktools_classbuilder-0.6.2.dist-info/WHEEL,sha256=y4mX-SOX4fYIkonsAGA5N0Oy-8_gI4FXw5HNI1xqvWg,91
11
+ ducktools_classbuilder-0.6.2.dist-info/top_level.txt,sha256=uSDLtio3ZFqdwcsMJ2O5yhjB4Q3ytbBWbA8rJREganc,10
12
+ ducktools_classbuilder-0.6.2.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.43.0)
2
+ Generator: setuptools (70.2.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,12 +0,0 @@
1
- ducktools/classbuilder/__init__.py,sha256=8gNWL8iLs39J2Rvh1cL7XDiJ4jFiUCZCu7HGY5qekFk,30686
2
- ducktools/classbuilder/__init__.pyi,sha256=kC78hkIfiQM4Vw9obcFu7TxFt1p7-oENDVsGXX0_rWA,7463
3
- ducktools/classbuilder/annotations.py,sha256=17g1WnWfyMWBHKQp2HD_hvV-n7CKXy_zv8NTODpOsuU,5437
4
- ducktools/classbuilder/annotations.pyi,sha256=K8n7TammaRLHs0BhnVhPt4HVDk4kIesvZXTONNeoGv8,514
5
- ducktools/classbuilder/prefab.py,sha256=h1MB-KJIkb1AsoWLJzmsx3U8CPOnZJY5nZz4TEhSfPo,24041
6
- ducktools/classbuilder/prefab.pyi,sha256=faU5kmjpLeP4Bmt2m08mSaVIjlDAZM2_NpdlUinpyBA,4202
7
- ducktools/classbuilder/py.typed,sha256=la67KBlbjXN-_-DfGNcdOcjYumVpKG_Tkw-8n5dnGB4,8
8
- ducktools_classbuilder-0.6.0.dist-info/LICENSE.md,sha256=6Thz9Dbw8R4fWInl6sGl8Rj3UnKnRbDwrc6jZerpugQ,1070
9
- ducktools_classbuilder-0.6.0.dist-info/METADATA,sha256=PLMUfDXnRdP1rCMnIz1H-r2q464FbfEROB46BSFRu-k,10911
10
- ducktools_classbuilder-0.6.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
11
- ducktools_classbuilder-0.6.0.dist-info/top_level.txt,sha256=uSDLtio3ZFqdwcsMJ2O5yhjB4Q3ytbBWbA8rJREganc,10
12
- ducktools_classbuilder-0.6.0.dist-info/RECORD,,