ducktools-classbuilder 0.5.1__py3-none-any.whl → 0.6.1__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.

@@ -25,33 +25,25 @@ A 'prebuilt' implementation of class generation.
25
25
 
26
26
  Includes pre and post init functions along with other methods.
27
27
  """
28
-
29
- import sys
30
-
31
28
  from . import (
32
29
  INTERNALS_DICT, NOTHING,
33
- Field, MethodMaker, SlotFields, GatheredFields,
34
- builder, fieldclass, get_flags, get_fields, make_slot_gatherer,
35
- frozen_setattr_maker, frozen_delattr_maker, is_classvar,
30
+ Field, MethodMaker, GatheredFields, GeneratedCode, SlotMakerMeta,
31
+ builder, get_flags, get_fields,
32
+ make_unified_gatherer,
33
+ frozen_setattr_maker, frozen_delattr_maker, eq_maker,
34
+ get_repr_generator,
36
35
  )
37
36
 
37
+ # These aren't used but are re-exported for ease of use
38
+ # noinspection PyUnresolvedReferences
39
+ from . import SlotFields, KW_ONLY
40
+
38
41
  PREFAB_FIELDS = "PREFAB_FIELDS"
39
42
  PREFAB_INIT_FUNC = "__prefab_init__"
40
43
  PRE_INIT_FUNC = "__prefab_pre_init__"
41
44
  POST_INIT_FUNC = "__prefab_post_init__"
42
45
 
43
46
 
44
- # KW_ONLY sentinel 'type' to use to indicate all subsequent attributes are
45
- # keyword only
46
- # noinspection PyPep8Naming
47
- class _KW_ONLY_TYPE:
48
- def __repr__(self):
49
- return "<KW_ONLY Sentinel Object>"
50
-
51
-
52
- KW_ONLY = _KW_ONLY_TYPE()
53
-
54
-
55
47
  class PrefabError(Exception):
56
48
  pass
57
49
 
@@ -68,310 +60,226 @@ def get_attributes(cls):
68
60
 
69
61
 
70
62
  # Method Generators
71
- def get_init_maker(*, init_name="__init__"):
72
- def __init__(cls: "type") -> "tuple[str, dict]":
73
- globs = {}
74
- # Get the internals dictionary and prepare attributes
75
- attributes = get_attributes(cls)
76
- flags = get_flags(cls)
77
-
78
- kw_only = flags.get("kw_only", False)
79
-
80
- # Handle pre/post init first - post_init can change types for __init__
81
- # Get pre and post init arguments
82
- pre_init_args = []
83
- post_init_args = []
84
- post_init_annotations = {}
85
-
86
- for func_name, func_arglist in [
87
- (PRE_INIT_FUNC, pre_init_args),
88
- (POST_INIT_FUNC, post_init_args),
89
- ]:
90
- try:
91
- func = getattr(cls, func_name)
92
- func_code = func.__code__
93
- except AttributeError:
94
- pass
95
- else:
96
- 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)
97
68
 
98
- # Identify if method is static, if so include first arg, otherwise skip
99
- is_static = type(cls.__dict__.get(func_name)) is staticmethod
69
+ kw_only = flags.get("kw_only", False)
100
70
 
101
- arglist = (
102
- func_code.co_varnames[:argcount]
103
- if is_static
104
- else func_code.co_varnames[1:argcount]
105
- )
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
106
88
 
107
- func_arglist.extend(arglist)
108
-
109
- if func_name == POST_INIT_FUNC:
110
- post_init_annotations.update(func.__annotations__)
111
-
112
- pos_arglist = []
113
- kw_only_arglist = []
114
- for name, attrib in attributes.items():
115
- # post_init annotations can be used to broaden types.
116
- if name in post_init_annotations:
117
- globs[f"_{name}_type"] = post_init_annotations[name]
118
- elif attrib.type is not NOTHING:
119
- globs[f"_{name}_type"] = attrib.type
120
-
121
- if attrib.init:
122
- if attrib.default is not NOTHING:
123
- if isinstance(attrib.default, (str, int, float, bool)):
124
- # Just use the literal in these cases
125
- if attrib.type is NOTHING:
126
- arg = f"{name}={attrib.default!r}"
127
- else:
128
- arg = f"{name}: _{name}_type = {attrib.default!r}"
129
- else:
130
- # No guarantee repr will work for other objects
131
- # so store the value in a variable and put it
132
- # in the globals dict for eval
133
- if attrib.type is NOTHING:
134
- arg = f"{name}=_{name}_default"
135
- else:
136
- arg = f"{name}: _{name}_type = _{name}_default"
137
- globs[f"_{name}_default"] = attrib.default
138
- elif attrib.default_factory is not NOTHING:
139
- # Use NONE here and call the factory later
140
- # 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
141
116
  if attrib.type is NOTHING:
142
- arg = f"{name}=None"
117
+ arg = f"{name}={attrib.default!r}"
143
118
  else:
144
- arg = f"{name}: _{name}_type = None"
145
- globs[f"_{name}_factory"] = attrib.default_factory
119
+ arg = f"{name}: _{name}_type = {attrib.default!r}"
146
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
147
124
  if attrib.type is NOTHING:
148
- arg = name
125
+ arg = f"{name}=_{name}_default"
149
126
  else:
150
- arg = f"{name}: _{name}_type"
151
- if attrib.kw_only or kw_only:
152
- kw_only_arglist.append(arg)
153
- else:
154
- pos_arglist.append(arg)
155
- # Not in init, but need to set defaults
156
- else:
157
- if attrib.default is not NOTHING:
127
+ arg = f"{name}: _{name}_type = _{name}_default"
158
128
  globs[f"_{name}_default"] = attrib.default
159
- elif attrib.default_factory is not NOTHING:
160
- globs[f"_{name}_factory"] = attrib.default_factory
161
-
162
- pos_args = ", ".join(pos_arglist)
163
- kw_args = ", ".join(kw_only_arglist)
164
- if pos_args and kw_args:
165
- args = f"{pos_args}, *, {kw_args}"
166
- elif kw_args:
167
- args = f"*, {kw_args}"
168
- else:
169
- args = pos_args
170
-
171
- assignments = []
172
- processes = [] # post_init values still need default factories to be called.
173
- for name, attrib in attributes.items():
174
- if attrib.init:
175
- if attrib.default_factory is not NOTHING:
176
- 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"
177
134
  else:
178
- value = name
135
+ arg = f"{name}: _{name}_type = None"
136
+ globs[f"_{name}_factory"] = attrib.default_factory
179
137
  else:
180
- if attrib.default_factory is not NOTHING:
181
- value = f"_{name}_factory()"
182
- elif attrib.default is not NOTHING:
183
- value = f"_{name}_default"
138
+ if attrib.type is NOTHING:
139
+ arg = name
184
140
  else:
185
- value = None
186
-
187
- if name in post_init_args:
188
- if attrib.default_factory is not NOTHING:
189
- processes.append((name, value))
190
- elif value is not None:
191
- assignments.append((name, value))
192
-
193
- if hasattr(cls, PRE_INIT_FUNC):
194
- pre_init_arg_call = ", ".join(f"{name}={name}" for name in pre_init_args)
195
- pre_init_call = f" self.{PRE_INIT_FUNC}({pre_init_arg_call})\n"
196
- else:
197
- pre_init_call = ""
198
-
199
- if assignments or processes:
200
- body = ""
201
- body += "\n".join(
202
- f" self.{name} = {value}" for name, value in assignments
203
- )
204
- body += "\n"
205
- body += "\n".join(f" {name} = {value}" for name, value in processes)
206
- else:
207
- body = " pass"
208
-
209
- if hasattr(cls, POST_INIT_FUNC):
210
- post_init_arg_call = ", ".join(f"{name}={name}" for name in post_init_args)
211
- post_init_call = f" self.{POST_INIT_FUNC}({post_init_arg_call})\n"
212
- else:
213
- post_init_call = ""
214
-
215
- code = (
216
- f"def {init_name}(self, {args}):\n"
217
- f"{pre_init_call}\n"
218
- f"{body}\n"
219
- f"{post_init_call}\n"
220
- )
221
- return code, globs
222
-
223
- return MethodMaker(init_name, __init__)
224
-
225
-
226
- def get_repr_maker(*, recursion_safe=False):
227
- def __repr__(cls: "type") -> "tuple[str, dict]":
228
- attributes = get_attributes(cls)
229
-
230
- globs = {}
231
-
232
- will_eval = True
233
- valid_names = []
234
- for name, attrib in attributes.items():
235
- if attrib.repr and not attrib.exclude_field:
236
- valid_names.append(name)
237
-
238
- # If the init fields don't match the repr, or some fields are excluded
239
- # generate a repr that clearly will not evaluate
240
- if will_eval and (attrib.exclude_field or (attrib.init ^ attrib.repr)):
241
- will_eval = False
242
-
243
- content = ", ".join(
244
- f"{name}={{self.{name}!r}}"
245
- for name in valid_names
246
- )
247
-
248
- if recursion_safe:
249
- import reprlib
250
- globs["_recursive_repr"] = reprlib.recursive_repr()
251
- recursion_func = "@_recursive_repr\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
252
147
  else:
253
- recursion_func = ""
254
-
255
- if will_eval:
256
- code = (
257
- f"{recursion_func}"
258
- f"def __repr__(self):\n"
259
- f" return f'{{type(self).__qualname__}}({content})'\n"
260
- )
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
261
170
  else:
262
- if content:
263
- code = (
264
- f"{recursion_func}"
265
- f"def __repr__(self):\n"
266
- f" return f'<prefab {{type(self).__qualname__}}; {content}>'\n"
267
- )
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"
268
175
  else:
269
- code = (
270
- f"{recursion_func}"
271
- f"def __repr__(self):\n"
272
- f" return f'<prefab {{type(self).__qualname__}}>'\n"
273
- )
176
+ value = None
274
177
 
275
- return code, globs
276
-
277
- return MethodMaker("__repr__", __repr__)
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))
278
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 = ""
279
189
 
280
- def get_eq_maker():
281
- def __eq__(cls: "type") -> "tuple[str, dict]":
282
- class_comparison = "self.__class__ is other.__class__"
283
- attribs = get_attributes(cls)
284
- field_names = [
285
- name
286
- for name, attrib in attribs.items()
287
- if attrib.compare and not attrib.exclude_field
288
- ]
190
+ if assignments or processes:
191
+ body = ""
192
+ body += "\n".join(
193
+ f" self.{name} = {value}" for name, value in assignments
194
+ )
195
+ body += "\n"
196
+ body += "\n".join(f" {name} = {value}" for name, value in processes)
197
+ else:
198
+ body = " pass"
289
199
 
290
- if field_names:
291
- selfvals = ",".join(f"self.{name}" for name in field_names)
292
- othervals = ",".join(f"other.{name}" for name in field_names)
293
- instance_comparison = f"({selfvals},) == ({othervals},)"
294
- else:
295
- instance_comparison = "True"
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 = ""
296
205
 
297
- code = (
298
- f"def __eq__(self, other):\n"
299
- f" return {instance_comparison} if {class_comparison} else NotImplemented\n"
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"
300
211
  )
301
- globs = {}
302
212
 
303
- return code, globs
213
+ return GeneratedCode(code, globs)
304
214
 
305
- return MethodMaker("__eq__", __eq__)
306
215
 
216
+ def iter_generator(cls, funcname="__iter__"):
217
+ fields = get_attributes(cls)
307
218
 
308
- def get_iter_maker():
309
- def __iter__(cls: "type") -> "tuple[str, dict]":
310
- fields = get_attributes(cls)
311
-
312
- valid_fields = (
313
- name for name, attrib in fields.items()
314
- if attrib.iter and not attrib.exclude_field
315
- )
316
-
317
- 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
+ )
318
223
 
319
- # if values is an empty string
320
- if not values:
321
- values = " yield from ()"
224
+ values = "\n".join(f" yield self.{name}" for name in valid_fields)
322
225
 
323
- code = f"def __iter__(self):\n{values}"
324
- globs = {}
325
- return code, globs
226
+ # if values is an empty string
227
+ if not values:
228
+ values = " yield from ()"
326
229
 
327
- return MethodMaker("__iter__", __iter__)
230
+ code = f"def {funcname}(self):\n{values}"
231
+ globs = {}
232
+ return GeneratedCode(code, globs)
328
233
 
329
234
 
330
- def get_asdict_maker():
331
- def as_dict_gen(cls: "type") -> "tuple[str, dict]":
332
- fields = get_attributes(cls)
235
+ def as_dict_generator(cls, funcname="as_dict"):
236
+ fields = get_attributes(cls)
333
237
 
334
- vals = ", ".join(
335
- f"'{name}': self.{name}"
336
- for name, attrib in fields.items()
337
- if attrib.serialize and not attrib.exclude_field
338
- )
339
- out_dict = f"{{{vals}}}"
340
- 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}"
341
245
 
342
- globs = {}
343
- return code, globs
344
- return MethodMaker("as_dict", as_dict_gen)
246
+ globs = {}
247
+ return GeneratedCode(code, globs)
345
248
 
346
249
 
347
- init_maker = get_init_maker()
348
- prefab_init_maker = get_init_maker(init_name=PREFAB_INIT_FUNC)
349
- repr_maker = get_repr_maker()
350
- recursive_repr_maker = get_repr_maker(recursion_safe=True)
351
- eq_maker = get_eq_maker()
352
- iter_maker = get_iter_maker()
353
- asdict_maker = get_asdict_maker()
250
+ init_maker = MethodMaker("__init__", init_generator)
251
+ prefab_init_maker = MethodMaker(PREFAB_INIT_FUNC, init_generator)
252
+ repr_maker = MethodMaker(
253
+ "__repr__",
254
+ get_repr_generator(recursion_safe=False, eval_safe=True)
255
+ )
256
+ recursive_repr_maker = MethodMaker(
257
+ "__repr__",
258
+ get_repr_generator(recursion_safe=True, eval_safe=True)
259
+ )
260
+ iter_maker = MethodMaker("__iter__", iter_generator)
261
+ asdict_maker = MethodMaker("as_dict", as_dict_generator)
354
262
 
355
263
 
356
264
  # Updated field with additional attributes
357
- @fieldclass
358
265
  class Attribute(Field):
359
- __slots__ = SlotFields(
360
- init=True,
361
- repr=True,
362
- compare=True,
363
- iter=True,
364
- kw_only=False,
365
- serialize=True,
366
- exclude_field=False,
367
- )
266
+ """
267
+ Get an object to define a prefab attribute
368
268
 
369
- def validate_field(self):
370
- super().validate_field()
371
- if self.kw_only and not self.init:
372
- raise PrefabError(
373
- "Attribute cannot be keyword only if it is not in init."
374
- )
269
+ :param default: Default value for this attribute
270
+ :param default_factory: 0 argument callable to give a default value
271
+ (for otherwise mutable defaults, eg: list)
272
+ :param init: Include this attribute in the __init__ parameters
273
+ :param repr: Include this attribute in the class __repr__
274
+ :param compare: Include this attribute in the class __eq__
275
+ :param iter: Include this attribute in the class __iter__ if generated
276
+ :param kw_only: Make this argument keyword only in init
277
+ :param serialize: Include this attribute in methods that serialize to dict
278
+ :param doc: Parameter documentation for slotted classes
279
+ :param type: Type of this attribute (for slotted classes)
280
+ """
281
+ iter: bool = True
282
+ serialize: bool = True
375
283
 
376
284
 
377
285
  # noinspection PyShadowingBuiltins
@@ -390,7 +298,7 @@ def attribute(
390
298
  type=NOTHING,
391
299
  ):
392
300
  """
393
- Get an object to define a prefab Attribute
301
+ Helper function to get an object to define a prefab Attribute
394
302
 
395
303
  :param default: Default value for this attribute
396
304
  :param default_factory: 0 argument callable to give a default value
@@ -401,15 +309,18 @@ def attribute(
401
309
  :param iter: Include this attribute in the class __iter__ if generated
402
310
  :param kw_only: Make this argument keyword only in init
403
311
  :param serialize: Include this attribute in methods that serialize to dict
404
- :param exclude_field: Exclude this field from all magic method generation
405
- apart from __init__ signature
406
- and do not include it in PREFAB_FIELDS
407
- Must be assigned in __prefab_post_init__
312
+ :param exclude_field: Shorthand for setting repr, compare, iter and serialize to False
408
313
  :param doc: Parameter documentation for slotted classes
409
314
  :param type: Type of this attribute (for slotted classes)
410
315
 
411
316
  :return: Attribute generated with these parameters.
412
317
  """
318
+ if exclude_field:
319
+ repr = False
320
+ compare = False
321
+ iter = False
322
+ serialize = False
323
+
413
324
  return Attribute(
414
325
  default=default,
415
326
  default_factory=default_factory,
@@ -419,90 +330,12 @@ def attribute(
419
330
  iter=iter,
420
331
  kw_only=kw_only,
421
332
  serialize=serialize,
422
- exclude_field=exclude_field,
423
333
  doc=doc,
424
334
  type=type,
425
335
  )
426
336
 
427
337
 
428
- slot_prefab_gatherer = make_slot_gatherer(Attribute)
429
-
430
-
431
- # Gatherer for classes built on attributes or annotations
432
- def attribute_gatherer(cls):
433
- cls_annotations = cls.__dict__.get("__annotations__", {})
434
- cls_annotation_names = cls_annotations.keys()
435
-
436
- cls_slots = cls.__dict__.get("__slots__", {})
437
-
438
- cls_attributes = {
439
- k: v for k, v in vars(cls).items() if isinstance(v, Attribute)
440
- }
441
-
442
- cls_attribute_names = cls_attributes.keys()
443
-
444
- cls_modifications = {}
445
-
446
- if set(cls_annotation_names).issuperset(set(cls_attribute_names)):
447
- # replace the classes' attributes dict with one with the correct
448
- # order from the annotations.
449
- kw_flag = False
450
- new_attributes = {}
451
- for name, value in cls_annotations.items():
452
- # Ignore ClassVar hints
453
- if is_classvar(value):
454
- continue
455
-
456
- # Look for the KW_ONLY annotation
457
- if value is KW_ONLY or value == "KW_ONLY":
458
- if kw_flag:
459
- raise PrefabError(
460
- "Class can not be defined as keyword only twice"
461
- )
462
- kw_flag = True
463
- else:
464
- # Copy attributes that are already defined to the new dict
465
- # generate Attribute() values for those that are not defined.
466
-
467
- # Extra parameters to pass to each Attribute
468
- extras = {
469
- "type": cls_annotations[name]
470
- }
471
- if kw_flag:
472
- extras["kw_only"] = True
473
-
474
- # If a field name is also declared in slots it can't have a real
475
- # default value and the attr will be the slot descriptor.
476
- if hasattr(cls, name) and name not in cls_slots:
477
- if name in cls_attribute_names:
478
- attrib = Attribute.from_field(
479
- cls_attributes[name],
480
- **extras,
481
- )
482
- else:
483
- attribute_default = getattr(cls, name)
484
- attrib = attribute(default=attribute_default, **extras)
485
-
486
- # Clear the attribute from the class after it has been used
487
- # in the definition.
488
- cls_modifications[name] = NOTHING
489
- else:
490
- attrib = attribute(**extras)
491
-
492
- new_attributes[name] = attrib
493
-
494
- cls_attributes = new_attributes
495
- else:
496
- for name in cls_attributes.keys():
497
- attrib = cls_attributes[name]
498
- cls_modifications[name] = NOTHING
499
-
500
- # Some items can still be annotated.
501
- if name in cls_annotations:
502
- new_attrib = Attribute.from_field(attrib, type=cls_annotations[name])
503
- cls_attributes[name] = new_attrib
504
-
505
- return cls_attributes, cls_modifications
338
+ prefab_gatherer = make_unified_gatherer(Attribute, False)
506
339
 
507
340
 
508
341
  # Class Builders
@@ -548,16 +381,13 @@ def _make_prefab(
548
381
  )
549
382
 
550
383
  slots = cls_dict.get("__slots__")
384
+
385
+ slotted = False if slots is None else True
386
+
551
387
  if gathered_fields is None:
552
- if isinstance(slots, SlotFields):
553
- gatherer = slot_prefab_gatherer
554
- slotted = True
555
- else:
556
- gatherer = attribute_gatherer
557
- slotted = False
388
+ gatherer = prefab_gatherer
558
389
  else:
559
390
  gatherer = gathered_fields
560
- slotted = False if slots is None else True
561
391
 
562
392
  methods = set()
563
393
 
@@ -657,9 +487,9 @@ def _make_prefab(
657
487
  post_init_args.extend(arglist)
658
488
 
659
489
  # Gather values for match_args and do some syntax checking
660
-
661
490
  default_defined = []
662
- valid_args = []
491
+ valid_args = list(fields.keys())
492
+
663
493
  for name, attrib in fields.items():
664
494
  # slot_gather and parent classes may use Fields
665
495
  # prefabs require Attributes, so convert.
@@ -667,15 +497,6 @@ def _make_prefab(
667
497
  attrib = Attribute.from_field(attrib)
668
498
  fields[name] = attrib
669
499
 
670
- # Excluded fields *MUST* be forwarded to post_init
671
- if attrib.exclude_field:
672
- if name not in post_init_args:
673
- raise PrefabError(
674
- f"{name!r} is an excluded attribute but is not passed to post_init"
675
- )
676
- else:
677
- valid_args.append(name)
678
-
679
500
  if not kw_only:
680
501
  # Syntax check arguments for __init__ don't have non-default after default
681
502
  if attrib.init and not attrib.kw_only:
@@ -698,6 +519,37 @@ def _make_prefab(
698
519
  return cls
699
520
 
700
521
 
522
+ class Prefab(metaclass=SlotMakerMeta):
523
+ _meta_gatherer = prefab_gatherer
524
+ __slots__ = {}
525
+
526
+ # noinspection PyShadowingBuiltins
527
+ def __init_subclass__(
528
+ cls,
529
+ init=True,
530
+ repr=True,
531
+ eq=True,
532
+ iter=False,
533
+ match_args=True,
534
+ kw_only=False,
535
+ frozen=False,
536
+ dict_method=False,
537
+ recursive_repr=False,
538
+ ):
539
+ _make_prefab(
540
+ cls,
541
+ init=init,
542
+ repr=repr,
543
+ eq=eq,
544
+ iter=iter,
545
+ match_args=match_args,
546
+ kw_only=kw_only,
547
+ frozen=frozen,
548
+ dict_method=dict_method,
549
+ recursive_repr=recursive_repr,
550
+ )
551
+
552
+
701
553
  # noinspection PyShadowingBuiltins
702
554
  def prefab(
703
555
  cls=None,
@@ -895,5 +747,5 @@ def as_dict(o):
895
747
  return {
896
748
  name: getattr(o, name)
897
749
  for name, attrib in flds.items()
898
- if attrib.serialize and not attrib.exclude_field
750
+ if attrib.serialize
899
751
  }