@@ -334,15 +334,12 @@ def __get__(self, instance, cls=None):
334334def get_init_generator (null = NOTHING , extra_code = None ):
335335 def cls_init_maker (cls , funcname = "__init__" ):
336336 fields = get_fields (cls )
337- flags = get_flags (cls )
338337
339338 arglist = []
340339 kw_only_arglist = []
341340 assignments = []
342341 globs = {}
343342
344- kw_only_flag = flags .get ("kw_only" , False )
345-
346343 for k , v in fields .items ():
347344 if v .init :
348345 if v .default is not null :
@@ -357,7 +354,7 @@ def cls_init_maker(cls, funcname="__init__"):
357354 arg = f"{ k } "
358355 assignment = f"self.{ k } = { k } "
359356
360- if kw_only_flag or v .kw_only :
357+ if v .kw_only :
361358 kw_only_arglist .append (arg )
362359 else :
363360 arglist .append (arg )
@@ -548,31 +545,45 @@ def ge_generator(cls, funcname="__ge__"):
548545 return get_order_generator (cls , funcname , operator = ">=" )
549546
550547
551- def replace_generator (cls , funcname = "__replace__" ):
552- # Generate the replace method for built classes
553- # unlike the dataclasses implementation this is generated
554- attribs = get_fields (cls )
548+ def _get_replace_generator (private_type = False ):
549+ def cls_replace_generator (cls , funcname = "__replace__" ):
550+ # Generate the replace method for built classes
551+ # unlike the dataclasses implementation this is generated
552+ attribs = get_fields (cls )
553+
554+ # This is essentially the as_dict generator for prefabs
555+ # except based on attrib.init instead of .serialize
556+ if private_type :
557+ vals = ", " .join (
558+ f"'{ name } ': self.{ name } "
559+ if name != "type"
560+ else f"'{ name } ': self._{ name } "
561+ for name , attrib in attribs .items ()
562+ if attrib .init
563+ )
564+ else :
565+ vals = ", " .join (
566+ f"'{ name } ': self.{ name } "
567+ for name , attrib in attribs .items ()
568+ if attrib .init
569+ )
570+ init_dict = f"{{{ vals } }}"
555571
556- # This is essentially the as_dict generator for prefabs
557- # except based on attrib.init instead of .serialize
558- vals = ", " .join (
559- f"'{ name } ': self.{ name } "
560- for name , attrib in attribs .items ()
561- if attrib .init
562- )
563- init_dict = f"{{{ vals } }}"
572+ # fmt: off
573+ code = (
574+ f"def { funcname } (self, /, **changes):\n "
575+ f" new_kwargs = { init_dict } \n "
576+ f" new_kwargs |= changes\n "
577+ f" return self.__class__(**new_kwargs)\n "
578+ )
579+ # fmt: on
580+ globs = {}
581+ return GeneratedCode (code , globs )
564582
565- # fmt: off
566- code = (
567- f"def { funcname } (self, /, **changes):\n "
568- f" new_kwargs = { init_dict } \n "
569- f" new_kwargs |= changes\n "
570- f" return self.__class__(**new_kwargs)\n "
571- )
572- # fmt: on
573- globs = {}
574- return GeneratedCode (code , globs )
583+ return cls_replace_generator
575584
585+ replace_generator = _get_replace_generator ()
586+ _field_replace_generator = _get_replace_generator (private_type = True )
576587
577588def frozen_setattr_generator (cls , funcname = "__setattr__" ):
578589 globs = {}
@@ -657,6 +668,10 @@ def hash_generator(cls, funcname="__hash__"):
657668 )
658669)
659670
671+ # Special `__replace__` method for `Field` that will use the internal `_type`
672+ # value instead of the resolved `type` property
673+ _field_replace_maker = MethodMaker ("__replace__" , _field_replace_generator )
674+
660675
661676def add_methods (cls , methods , * , internals = None ):
662677 """
@@ -730,13 +745,21 @@ def builder(cls=None, /, *, gatherer, methods, flags=None, fix_signature=True, f
730745 flag_dict |= flags
731746 internals ["flags" ] = flag_dict
732747
748+ kw_only = flag_dict .get ("kw_only" , False )
733749 cls_gathered = cls .__dict__ .get (GATHERED_DATA )
734750
735751 if cls_gathered :
736752 cls_fields , modifications = cls_gathered
737753 else :
738754 cls_fields , modifications = gatherer (cls )
739755
756+ if kw_only :
757+ # Update the class fields to make all Fields kw_only
758+ cls_fields = {
759+ k : v if v .kw_only else v .__replace__ (kw_only = True )
760+ for k , v in cls_fields .items ()
761+ }
762+
740763 for name , value in modifications .items ():
741764 if value is NOTHING :
742765 delattr (cls , name )
@@ -1075,7 +1098,7 @@ def __init__(
10751098
10761099 def __init_subclass__ (cls , frozen = False , ignore_annotations = False ):
10771100 # Subclasses of Field can be created as if they are dataclasses
1078- field_methods = {_field_init_maker , repr_maker , eq_maker , replace_maker }
1101+ field_methods = {_field_init_maker , repr_maker , eq_maker , _field_replace_maker }
10791102 if frozen or _UNDER_TESTING :
10801103 field_methods |= {frozen_setattr_maker , frozen_delattr_maker }
10811104
@@ -1110,8 +1133,9 @@ def from_field(cls, fld, /, **kwargs):
11101133 :param kwargs: Additional keyword arguments for subclasses
11111134 :return: new field subclass instance
11121135 """
1136+ # type is special cased to get the internal value
11131137 inst_fields = {
1114- k : getattr (fld , k )
1138+ k : getattr (fld , k ) if k != "type" else getattr ( fld , "_type" )
11151139 for k in get_fields (type (fld ))
11161140 }
11171141 argument_dict = {** inst_fields , ** kwargs }
@@ -1152,19 +1176,21 @@ def _build_field():
11521176 "kw_only" : "Make this a keyword only parameter in __init__" ,
11531177 }
11541178
1179+ # Fields here must be marked as kw_only to prevent the builder from trying
1180+ # to call the __replace__ method which doesn't exist yet
11551181 fields = {
1156- "default" : Field (default = NOTHING , doc = field_docs ["default" ]),
1157- "default_factory" : Field (default = NOTHING , doc = field_docs ["default_factory" ]),
1158- "type" : Field (default = NOTHING , doc = field_docs ["type" ]),
1159- "doc" : Field (default = None , doc = field_docs ["doc" ]),
1160- "init" : Field (default = True , doc = field_docs ["init" ]),
1161- "repr" : Field (default = True , doc = field_docs ["repr" ]),
1162- "compare" : Field (default = True , doc = field_docs ["compare" ]),
1163- "kw_only" : Field (default = False , doc = field_docs ["kw_only" ]),
1182+ "default" : Field (default = NOTHING , doc = field_docs ["default" ], kw_only = True ),
1183+ "default_factory" : Field (default = NOTHING , doc = field_docs ["default_factory" ], kw_only = True ),
1184+ "type" : Field (default = NOTHING , doc = field_docs ["type" ], kw_only = True ),
1185+ "doc" : Field (default = None , doc = field_docs ["doc" ], kw_only = True ),
1186+ "init" : Field (default = True , doc = field_docs ["init" ], kw_only = True ),
1187+ "repr" : Field (default = True , doc = field_docs ["repr" ], kw_only = True ),
1188+ "compare" : Field (default = True , doc = field_docs ["compare" ], kw_only = True ),
1189+ "kw_only" : Field (default = False , doc = field_docs ["kw_only" ], kw_only = True ),
11641190 }
11651191 modifications = {"__slots__" : field_docs }
11661192
1167- field_methods = {repr_maker , eq_maker , replace_maker }
1193+ field_methods = {repr_maker , eq_maker , _field_replace_maker }
11681194 if _UNDER_TESTING :
11691195 field_methods |= {frozen_setattr_maker , frozen_delattr_maker }
11701196
@@ -1448,6 +1474,23 @@ def check_argument_order(cls):
14481474 used_default = True
14491475
14501476
1477+ def replace (obj , / , ** changes ):
1478+ """
1479+ Create a copy of a prefab instance with values provided to 'changes' replaced
1480+
1481+ :param obj: built class
1482+ :return: new built class instance with changes applied
1483+ """
1484+ if not build_completed (type (obj )):
1485+ raise TypeError ("replace() should be called on classbuilder class instances" )
1486+ try :
1487+ replace_func = obj .__replace__
1488+ except AttributeError :
1489+ raise TypeError (f"{ obj .__class__ .__name__ !r} does not support __replace__" )
1490+
1491+ return replace_func (** changes )
1492+
1493+
14511494# Class Decorators
14521495def slotclass (cls = None , / , * , methods = default_methods , syntax_check = True ):
14531496 """
0 commit comments