# SPDX-License-Identifier: MITfrom__future__importannotationsimportabcimportcontextlibimportcopyimportenumimportinspectimportitertoolsimportlinecacheimportsysimporttypesimportunicodedatafromcollections.abcimportCallable,Mappingfromfunctoolsimportcached_propertyfromtypingimportAny,NamedTuple,TypeVar# We need to import _compat itself in addition to the _compat members to avoid# having the thread-local in the globals here.from.import_compat,_config,settersfrom._compatimport(PY_3_10_PLUS,PY_3_11_PLUS,PY_3_13_PLUS,_AnnotationExtractor,_get_annotations,get_generic_base,)from.exceptionsimport(DefaultAlreadySetError,FrozenInstanceError,NotAnAttrsClassError,UnannotatedAttributeError,)# This is used at least twice, so cache it here._OBJ_SETATTR=object.__setattr___INIT_FACTORY_PAT="__attr_factory_%s"_CLASSVAR_PREFIXES=("typing.ClassVar","t.ClassVar","ClassVar","typing_extensions.ClassVar",)# we don't use a double-underscore prefix because that triggers# name mangling when trying to create a slot for the field# (when slots=True)_HASH_CACHE_FIELD="_attrs_cached_hash"_EMPTY_METADATA_SINGLETON=types.MappingProxyType({})# Unique object for unequivocal getattr() defaults._SENTINEL=object()_DEFAULT_ON_SETATTR=setters.pipe(setters.convert,setters.validate)class_Nothing(enum.Enum):""" Sentinel to indicate the lack of a value when `None` is ambiguous. If extending attrs, you can use ``typing.Literal[NOTHING]`` to show that a value may be ``NOTHING``. .. versionchanged:: 21.1.0 ``bool(NOTHING)`` is now False. .. versionchanged:: 22.2.0 ``NOTHING`` is now an ``enum.Enum`` variant. """NOTHING=enum.auto()def__repr__(self):return"NOTHING"def__bool__(self):returnFalseNOTHING=_Nothing.NOTHING"""Sentinel to indicate the lack of a value when `None` is ambiguous.When using in 3rd party code, use `attrs.NothingType` for type annotations."""class_CacheHashWrapper(int):""" An integer subclass that pickles / copies as None This is used for non-slots classes with ``cache_hash=True``, to avoid serializing a potentially (even likely) invalid hash value. Since `None` is the default value for uncalculated hashes, whenever this is copied, the copy's value for the hash should automatically reset. See GH #613 for more details. """def__reduce__(self,_none_constructor=type(None),_args=()):# noqa: B008return_none_constructor,_argsdefattrib(default=NOTHING,validator=None,repr=True,cmp=None,hash=None,init=True,metadata=None,type=None,converter=None,factory=None,kw_only=False,eq=None,order=None,on_setattr=None,alias=None,):""" Create a new field / attribute on a class. Identical to `attrs.field`, except it's not keyword-only. Consider using `attrs.field` in new code (``attr.ib`` will *never* go away, though). .. warning:: Does **nothing** unless the class is also decorated with `attr.s` (or similar)! .. versionadded:: 15.2.0 *convert* .. versionadded:: 16.3.0 *metadata* .. versionchanged:: 17.1.0 *validator* can be a ``list`` now. .. versionchanged:: 17.1.0 *hash* is `None` and therefore mirrors *eq* by default. .. versionadded:: 17.3.0 *type* .. deprecated:: 17.4.0 *convert* .. versionadded:: 17.4.0 *converter* as a replacement for the deprecated *convert* to achieve consistency with other noun-based arguments. .. versionadded:: 18.1.0 ``factory=f`` is syntactic sugar for ``default=attr.Factory(f)``. .. versionadded:: 18.2.0 *kw_only* .. versionchanged:: 19.2.0 *convert* keyword argument removed. .. versionchanged:: 19.2.0 *repr* also accepts a custom callable. .. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01. .. versionadded:: 19.2.0 *eq* and *order* .. versionadded:: 20.1.0 *on_setattr* .. versionchanged:: 20.3.0 *kw_only* backported to Python 2 .. versionchanged:: 21.1.0 *eq*, *order*, and *cmp* also accept a custom callable .. versionchanged:: 21.1.0 *cmp* undeprecated .. versionadded:: 22.2.0 *alias* """eq,eq_key,order,order_key=_determine_attrib_eq_order(cmp,eq,order,True)ifhashisnotNoneandhashisnotTrueandhashisnotFalse:msg="Invalid value for hash. Must be True, False, or None."raiseTypeError(msg)iffactoryisnotNone:ifdefaultisnotNOTHING:msg=("The `default` and `factory` arguments are mutually exclusive.")raiseValueError(msg)ifnotcallable(factory):msg="The `factory` argument must be a callable."raiseValueError(msg)default=Factory(factory)ifmetadataisNone:metadata={}# Apply syntactic sugar by auto-wrapping.ifisinstance(on_setattr,(list,tuple)):on_setattr=setters.pipe(*on_setattr)ifvalidatorandisinstance(validator,(list,tuple)):validator=and_(*validator)ifconverterandisinstance(converter,(list,tuple)):converter=pipe(*converter)return_CountingAttr(default=default,validator=validator,repr=repr,cmp=None,hash=hash,init=init,converter=converter,metadata=metadata,type=type,kw_only=kw_only,eq=eq,eq_key=eq_key,order=order,order_key=order_key,on_setattr=on_setattr,alias=alias,)def_compile_and_eval(script:str,globs:dict[str,Any]|None,locs:Mapping[str,object]|None=None,filename:str="",)->None:""" Evaluate the script with the given global (globs) and local (locs) variables. """bytecode=compile(script,filename,"exec")eval(bytecode,globs,locs)def_linecache_and_compile(script:str,filename:str,globs:dict[str,Any]|None,locals:Mapping[str,object]|None=None,)->dict[str,Any]:""" Cache the script with _linecache_, compile it and return the _locals_. """locs={}iflocalsisNoneelselocals# In order of debuggers like PDB being able to step through the code,# we add a fake linecache entry.count=1base_filename=filenamewhileTrue:linecache_tuple=(len(script),None,script.splitlines(True),filename,)old_val=linecache.cache.setdefault(filename,linecache_tuple)ifold_val==linecache_tuple:breakfilename=f"{base_filename[:-1]}-{count}>"count+=1_compile_and_eval(script,globs,locs,filename)returnlocsdef_make_attr_tuple_class(cls_name:str,attr_names:list[str])->type:""" Create a tuple subclass to hold `Attribute`s for an `attrs` class. The subclass is a bare tuple with properties for names. class MyClassAttributes(tuple): __slots__ = () x = property(itemgetter(0)) """attr_class_name=f"{cls_name}Attributes"body={}fori,attr_nameinenumerate(attr_names):defgetter(self,i=i):returnself[i]body[attr_name]=property(getter)returntype(attr_class_name,(tuple,),body)# Tuple class for extracted attributes from a class definition.# `base_attrs` is a subset of `attrs`.class_Attributes(NamedTuple):attrs:typebase_attrs:list[Attribute]base_attrs_map:dict[str,type]def_is_class_var(annot):""" Check whether *annot* is a typing.ClassVar. The string comparison hack is used to avoid evaluating all string annotations which would put attrs-based classes at a performance disadvantage compared to plain old classes. """annot=str(annot)# Annotation can be quoted.ifannot.startswith(("'",'"'))andannot.endswith(("'",'"')):annot=annot[1:-1]returnannot.startswith(_CLASSVAR_PREFIXES)def_has_own_attribute(cls,attrib_name):""" Check whether *cls* defines *attrib_name* (and doesn't just inherit it). """returnattrib_nameincls.__dict__def_collect_base_attrs(cls,taken_attr_names)->tuple[list[Attribute],dict[str,type]]:""" Collect attr.ibs from base classes of *cls*, except *taken_attr_names*. """base_attrs=[]base_attr_map={}# A dictionary of base attrs to their classes.# Traverse the MRO and collect attributes.forbase_clsinreversed(cls.__mro__[1:-1]):foraingetattr(base_cls,"__attrs_attrs__",[]):ifa.inheritedora.nameintaken_attr_names:continuea=a.evolve(inherited=True)# noqa: PLW2901base_attrs.append(a)base_attr_map[a.name]=base_cls# For each name, only keep the freshest definition i.e. the furthest at the# back. base_attr_map is fine because it gets overwritten with every new# instance.filtered=[]seen=set()forainreversed(base_attrs):ifa.nameinseen:continuefiltered.insert(0,a)seen.add(a.name)returnfiltered,base_attr_mapdef_collect_base_attrs_broken(cls,taken_attr_names):""" Collect attr.ibs from base classes of *cls*, except *taken_attr_names*. N.B. *taken_attr_names* will be mutated. Adhere to the old incorrect behavior. Notably it collects from the front and considers inherited attributes which leads to the buggy behavior reported in #428. """base_attrs=[]base_attr_map={}# A dictionary of base attrs to their classes.# Traverse the MRO and collect attributes.forbase_clsincls.__mro__[1:-1]:foraingetattr(base_cls,"__attrs_attrs__",[]):ifa.nameintaken_attr_names:continuea=a.evolve(inherited=True)# noqa: PLW2901taken_attr_names.add(a.name)base_attrs.append(a)base_attr_map[a.name]=base_clsreturnbase_attrs,base_attr_mapdef_transform_attrs(cls,these,auto_attribs,kw_only,collect_by_mro,field_transformer)->_Attributes:""" Transform all `_CountingAttr`s on a class into `Attribute`s. If *these* is passed, use that and don't look for them on the class. If *collect_by_mro* is True, collect them in the correct MRO order, otherwise use the old -- incorrect -- order. See #428. Return an `_Attributes`. """cd=cls.__dict__anns=_get_annotations(cls)iftheseisnotNone:ca_list=list(these.items())elifauto_attribsisTrue:ca_names={nameforname,attrincd.items()ifattr.__class__is_CountingAttr}ca_list=[]annot_names=set()forattr_name,typeinanns.items():if_is_class_var(type):continueannot_names.add(attr_name)a=cd.get(attr_name,NOTHING)ifa.__class__isnot_CountingAttr:a=attrib(a)ca_list.append((attr_name,a))unannotated=ca_names-annot_namesifunannotated:raiseUnannotatedAttributeError("The following `attr.ib`s lack a type annotation: "+", ".join(sorted(unannotated,key=lambdan:cd.get(n).counter))+".")else:ca_list=sorted(((name,attr)forname,attrincd.items()ifattr.__class__is_CountingAttr),key=lambdae:e[1].counter,)fca=Attribute.from_counting_attrown_attrs=[fca(attr_name,ca,anns.get(attr_name))forattr_name,cainca_list]ifcollect_by_mro:base_attrs,base_attr_map=_collect_base_attrs(cls,{a.nameforainown_attrs})else:base_attrs,base_attr_map=_collect_base_attrs_broken(cls,{a.nameforainown_attrs})ifkw_only:own_attrs=[a.evolve(kw_only=True)forainown_attrs]base_attrs=[a.evolve(kw_only=True)forainbase_attrs]attrs=base_attrs+own_attrsiffield_transformerisnotNone:attrs=tuple(field_transformer(cls,attrs))# Check attr order after executing the field_transformer.# Mandatory vs non-mandatory attr order only matters when they are part of# the __init__ signature and when they aren't kw_only (which are moved to# the end and can be mandatory or non-mandatory in any order, as they will# be specified as keyword args anyway). Check the order of those attrs:had_default=Falseforain(aforainattrsifa.initisnotFalseanda.kw_onlyisFalse):ifhad_defaultisTrueanda.defaultisNOTHING:msg=f"No mandatory attributes allowed after an attribute with a default value or factory. Attribute in question: {a!r}"raiseValueError(msg)ifhad_defaultisFalseanda.defaultisnotNOTHING:had_default=True# Resolve default field alias after executing field_transformer.# This allows field_transformer to differentiate between explicit vs# default aliases and supply their own defaults.forainattrs:ifnota.alias:# Evolve is very slow, so we hold our nose and do it dirty._OBJ_SETATTR.__get__(a)("alias",_default_init_alias_for(a.name))# Create AttrsClass *after* applying the field_transformer since it may# add or remove attributes!attr_names=[a.nameforainattrs]AttrsClass=_make_attr_tuple_class(cls.__name__,attr_names)return_Attributes(AttrsClass(attrs),base_attrs,base_attr_map)def_make_cached_property_getattr(cached_properties,original_getattr,cls):lines=[# Wrapped to get `__class__` into closure cell for super()# (It will be replaced with the newly constructed class after construction)."def wrapper(_cls):"," __class__ = _cls"," def __getattr__(self, item, cached_properties=cached_properties, original_getattr=original_getattr, _cached_setattr_get=_cached_setattr_get):"," func = cached_properties.get(item)"," if func is not None:"," result = func(self)"," _setter = _cached_setattr_get(self)"," _setter(item, result)"," return result",]iforiginal_getattrisnotNone:lines.append(" return original_getattr(self, item)",)else:lines.extend([" try:"," return super().__getattribute__(item)"," except AttributeError:"," if not hasattr(super(), '__getattr__'):"," raise"," return super().__getattr__(item)"," original_error = f\"'{self.__class__.__name__}' object has no attribute '{item}'\""," raise AttributeError(original_error)",])lines.extend([" return __getattr__","__getattr__ = wrapper(_cls)",])unique_filename=_generate_unique_filename(cls,"getattr")glob={"cached_properties":cached_properties,"_cached_setattr_get":_OBJ_SETATTR.__get__,"original_getattr":original_getattr,}return_linecache_and_compile("\n".join(lines),unique_filename,glob,locals={"_cls":cls})["__getattr__"]def_frozen_setattrs(self,name,value):""" Attached to frozen classes as __setattr__. """ifisinstance(self,BaseException)andnamein("__cause__","__context__","__traceback__","__suppress_context__","__notes__",):BaseException.__setattr__(self,name,value)returnraiseFrozenInstanceErrordef_frozen_delattrs(self,name):""" Attached to frozen classes as __delattr__. """ifisinstance(self,BaseException)andnamein("__notes__",):BaseException.__delattr__(self,name)returnraiseFrozenInstanceError
[docs]defevolve(*args,**changes):""" Create a new instance, based on the first positional argument with *changes* applied. .. tip:: On Python 3.13 and later, you can also use `copy.replace` instead. Args: inst: Instance of a class with *attrs* attributes. *inst* must be passed as a positional argument. changes: Keyword changes in the new copy. Returns: A copy of inst with *changes* incorporated. Raises: TypeError: If *attr_name* couldn't be found in the class ``__init__``. attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class. .. versionadded:: 17.1.0 .. deprecated:: 23.1.0 It is now deprecated to pass the instance using the keyword argument *inst*. It will raise a warning until at least April 2024, after which it will become an error. Always pass the instance as a positional argument. .. versionchanged:: 24.1.0 *inst* can't be passed as a keyword argument anymore. """try:(inst,)=argsexceptValueError:msg=(f"evolve() takes 1 positional argument, but {len(args)} were given")raiseTypeError(msg)fromNonecls=inst.__class__attrs=fields(cls)forainattrs:ifnota.init:continueattr_name=a.name# To deal with private attributes.init_name=a.aliasifinit_namenotinchanges:changes[init_name]=getattr(inst,attr_name)returncls(**changes)
class_ClassBuilder:""" Iteratively build *one* class. """__slots__=("_add_method_dunders","_attr_names","_attrs","_base_attr_map","_base_names","_cache_hash","_cls","_cls_dict","_delete_attribs","_frozen","_has_custom_setattr","_has_post_init","_has_pre_init","_is_exc","_on_setattr","_pre_init_has_args","_repr_added","_script_snippets","_slots","_weakref_slot","_wrote_own_setattr",)def__init__(self,cls:type,these,slots,frozen,weakref_slot,getstate_setstate,auto_attribs,kw_only,cache_hash,is_exc,collect_by_mro,on_setattr,has_custom_setattr,field_transformer,):attrs,base_attrs,base_map=_transform_attrs(cls,these,auto_attribs,kw_only,collect_by_mro,field_transformer,)self._cls=clsself._cls_dict=dict(cls.__dict__)ifslotselse{}self._attrs=attrsself._base_names={a.nameforainbase_attrs}self._base_attr_map=base_mapself._attr_names=tuple(a.nameforainattrs)self._slots=slotsself._frozen=frozenself._weakref_slot=weakref_slotself._cache_hash=cache_hashself._has_pre_init=bool(getattr(cls,"__attrs_pre_init__",False))self._pre_init_has_args=Falseifself._has_pre_init:# Check if the pre init method has more arguments than just `self`# We want to pass arguments if pre init expects argumentspre_init_func=cls.__attrs_pre_init__pre_init_signature=inspect.signature(pre_init_func)self._pre_init_has_args=len(pre_init_signature.parameters)>1self._has_post_init=bool(getattr(cls,"__attrs_post_init__",False))self._delete_attribs=notbool(these)self._is_exc=is_excself._on_setattr=on_setattrself._has_custom_setattr=has_custom_setattrself._wrote_own_setattr=Falseself._cls_dict["__attrs_attrs__"]=self._attrsiffrozen:self._cls_dict["__setattr__"]=_frozen_setattrsself._cls_dict["__delattr__"]=_frozen_delattrsself._wrote_own_setattr=Trueelifon_setattrin(_DEFAULT_ON_SETATTR,setters.validate,setters.convert,):has_validator=has_converter=Falseforainattrs:ifa.validatorisnotNone:has_validator=Trueifa.converterisnotNone:has_converter=Trueifhas_validatorandhas_converter:breakif((on_setattr==_DEFAULT_ON_SETATTRandnot(has_validatororhas_converter))or(on_setattr==setters.validateandnothas_validator)or(on_setattr==setters.convertandnothas_converter)):# If class-level on_setattr is set to convert + validate, but# there's no field to convert or validate, pretend like there's# no on_setattr.self._on_setattr=Noneifgetstate_setstate:(self._cls_dict["__getstate__"],self._cls_dict["__setstate__"],)=self._make_getstate_setstate()# tuples of script, globs, hookself._script_snippets:list[tuple[str,dict,Callable[[dict,dict],Any]]]=[]self._repr_added=False# We want to only do this check once; in 99.9% of cases these# exist.ifnothasattr(self._cls,"__module__")ornothasattr(self._cls,"__qualname__"):self._add_method_dunders=self._add_method_dunders_safeelse:self._add_method_dunders=self._add_method_dunders_unsafedef__repr__(self):returnf"<_ClassBuilder(cls={self._cls.__name__})>"def_eval_snippets(self)->None:""" Evaluate any registered snippets in one go. """script="\n".join([snippet[0]forsnippetinself._script_snippets])globs={}for_,snippet_globs,_inself._script_snippets:globs.update(snippet_globs)locs=_linecache_and_compile(script,_generate_unique_filename(self._cls,"methods"),globs,)for_,_,hookinself._script_snippets:hook(self._cls_dict,locs)defbuild_class(self):""" Finalize class based on the accumulated configuration. Builder cannot be used after calling this method. """self._eval_snippets()ifself._slotsisTrue:cls=self._create_slots_class()else:cls=self._patch_original_class()ifPY_3_10_PLUS:cls=abc.update_abstractmethods(cls)# The method gets only called if it's not inherited from a base class.# _has_own_attribute does NOT work properly for classmethods.if(getattr(cls,"__attrs_init_subclass__",None)and"__attrs_init_subclass__"notincls.__dict__):cls.__attrs_init_subclass__()returnclsdef_patch_original_class(self):""" Apply accumulated methods and return the class. """cls=self._clsbase_names=self._base_names# Clean class of attribute definitions (`attr.ib()`s).ifself._delete_attribs:fornameinself._attr_names:if(namenotinbase_namesandgetattr(cls,name,_SENTINEL)isnot_SENTINEL):# An AttributeError can happen if a base class defines a# class variable and we want to set an attribute with the# same name by using only a type annotation.withcontextlib.suppress(AttributeError):delattr(cls,name)# Attach our dunder methods.forname,valueinself._cls_dict.items():setattr(cls,name,value)# If we've inherited an attrs __setattr__ and don't write our own,# reset it to object's.ifnotself._wrote_own_setattrandgetattr(cls,"__attrs_own_setattr__",False):cls.__attrs_own_setattr__=Falseifnotself._has_custom_setattr:cls.__setattr__=_OBJ_SETATTRreturnclsdef_create_slots_class(self):""" Build and return a new class with a `__slots__` attribute. """cd={k:vfork,vinself._cls_dict.items()ifknotin(*tuple(self._attr_names),"__dict__","__weakref__")}# If our class doesn't have its own implementation of __setattr__# (either from the user or by us), check the bases, if one of them has# an attrs-made __setattr__, that needs to be reset. We don't walk the# MRO because we only care about our immediate base classes.# XXX: This can be confused by subclassing a slotted attrs class with# XXX: a non-attrs class and subclass the resulting class with an attrs# XXX: class. See `test_slotted_confused` for details. For now that's# XXX: OK with us.ifnotself._wrote_own_setattr:cd["__attrs_own_setattr__"]=Falseifnotself._has_custom_setattr:forbase_clsinself._cls.__bases__:ifbase_cls.__dict__.get("__attrs_own_setattr__",False):cd["__setattr__"]=_OBJ_SETATTRbreak# Traverse the MRO to collect existing slots# and check for an existing __weakref__.existing_slots={}weakref_inherited=Falseforbase_clsinself._cls.__mro__[1:-1]:ifbase_cls.__dict__.get("__weakref__",None)isnotNone:weakref_inherited=Trueexisting_slots.update({name:getattr(base_cls,name)fornameingetattr(base_cls,"__slots__",[])})base_names=set(self._base_names)names=self._attr_namesif(self._weakref_slotand"__weakref__"notingetattr(self._cls,"__slots__",())and"__weakref__"notinnamesandnotweakref_inherited):names+=("__weakref__",)cached_properties={name:cached_prop.funcforname,cached_propincd.items()ifisinstance(cached_prop,cached_property)}# Collect methods with a `__class__` reference that are shadowed in the new class.# To know to update them.additional_closure_functions_to_update=[]ifcached_properties:class_annotations=_get_annotations(self._cls)forname,funcincached_properties.items():# Add cached properties to names for slotting.names+=(name,)# Clear out function from class to avoid clashing.delcd[name]additional_closure_functions_to_update.append(func)annotation=inspect.signature(func).return_annotationifannotationisnotinspect.Parameter.empty:class_annotations[name]=annotationoriginal_getattr=cd.get("__getattr__")iforiginal_getattrisnotNone:additional_closure_functions_to_update.append(original_getattr)cd["__getattr__"]=_make_cached_property_getattr(cached_properties,original_getattr,self._cls)# We only add the names of attributes that aren't inherited.# Setting __slots__ to inherited attributes wastes memory.slot_names=[namefornameinnamesifnamenotinbase_names]# There are slots for attributes from current class# that are defined in parent classes.# As their descriptors may be overridden by a child class,# we collect them here and update the class dictreused_slots={slot:slot_descriptorforslot,slot_descriptorinexisting_slots.items()ifslotinslot_names}slot_names=[namefornameinslot_namesifnamenotinreused_slots]cd.update(reused_slots)ifself._cache_hash:slot_names.append(_HASH_CACHE_FIELD)cd["__slots__"]=tuple(slot_names)cd["__qualname__"]=self._cls.__qualname__# Create new class based on old class and our methods.cls=type(self._cls)(self._cls.__name__,self._cls.__bases__,cd)# The following is a fix for# <https://github.com/python-attrs/attrs/issues/102>.# If a method mentions `__class__` or uses the no-arg super(), the# compiler will bake a reference to the class in the method itself# as `method.__closure__`. Since we replace the class with a# clone, we rewrite these references so it keeps working.foriteminitertools.chain(cls.__dict__.values(),additional_closure_functions_to_update):ifisinstance(item,(classmethod,staticmethod)):# Class- and staticmethods hide their functions inside.# These might need to be rewritten as well.closure_cells=getattr(item.__func__,"__closure__",None)elifisinstance(item,property):# Workaround for property `super()` shortcut (PY3-only).# There is no universal way for other descriptors.closure_cells=getattr(item.fget,"__closure__",None)else:closure_cells=getattr(item,"__closure__",None)ifnotclosure_cells:# Catch None or the empty list.continueforcellinclosure_cells:try:match=cell.cell_contentsisself._clsexceptValueError:# noqa: PERF203# ValueError: Cell is emptypasselse:ifmatch:cell.cell_contents=clsreturnclsdefadd_repr(self,ns):script,globs=_make_repr_script(self._attrs,ns)def_attach_repr(cls_dict,globs):cls_dict["__repr__"]=self._add_method_dunders(globs["__repr__"])self._script_snippets.append((script,globs,_attach_repr))self._repr_added=Truereturnselfdefadd_str(self):ifnotself._repr_added:msg="__str__ can only be generated if a __repr__ exists."raiseValueError(msg)def__str__(self):returnself.__repr__()self._cls_dict["__str__"]=self._add_method_dunders(__str__)returnselfdef_make_getstate_setstate(self):""" Create custom __setstate__ and __getstate__ methods. """# __weakref__ is not writable.state_attr_names=tuple(anforaninself._attr_namesifan!="__weakref__")defslots_getstate(self):""" Automatically created by attrs. """return{name:getattr(self,name)fornameinstate_attr_names}hash_caching_enabled=self._cache_hashdefslots_setstate(self,state):""" Automatically created by attrs. """__bound_setattr=_OBJ_SETATTR.__get__(self)ifisinstance(state,tuple):# Backward compatibility with attrs instances pickled with# attrs versions before v22.2.0 which stored tuples.forname,valueinzip(state_attr_names,state):__bound_setattr(name,value)else:fornameinstate_attr_names:ifnameinstate:__bound_setattr(name,state[name])# The hash code cache is not included when the object is# serialized, but it still needs to be initialized to None to# indicate that the first call to __hash__ should be a cache# miss.ifhash_caching_enabled:__bound_setattr(_HASH_CACHE_FIELD,None)returnslots_getstate,slots_setstatedefmake_unhashable(self):self._cls_dict["__hash__"]=Nonereturnselfdefadd_hash(self):script,globs=_make_hash_script(self._cls,self._attrs,frozen=self._frozen,cache_hash=self._cache_hash,)defattach_hash(cls_dict:dict,locs:dict)->None:cls_dict["__hash__"]=self._add_method_dunders(locs["__hash__"])self._script_snippets.append((script,globs,attach_hash))returnselfdefadd_init(self):script,globs,annotations=_make_init_script(self._cls,self._attrs,self._has_pre_init,self._pre_init_has_args,self._has_post_init,self._frozen,self._slots,self._cache_hash,self._base_attr_map,self._is_exc,self._on_setattr,attrs_init=False,)def_attach_init(cls_dict,globs):init=globs["__init__"]init.__annotations__=annotationscls_dict["__init__"]=self._add_method_dunders(init)self._script_snippets.append((script,globs,_attach_init))returnselfdefadd_replace(self):self._cls_dict["__replace__"]=self._add_method_dunders(lambdaself,**changes:evolve(self,**changes))returnselfdefadd_match_args(self):self._cls_dict["__match_args__"]=tuple(field.nameforfieldinself._attrsiffield.initandnotfield.kw_only)defadd_attrs_init(self):script,globs,annotations=_make_init_script(self._cls,self._attrs,self._has_pre_init,self._pre_init_has_args,self._has_post_init,self._frozen,self._slots,self._cache_hash,self._base_attr_map,self._is_exc,self._on_setattr,attrs_init=True,)def_attach_attrs_init(cls_dict,globs):init=globs["__attrs_init__"]init.__annotations__=annotationscls_dict["__attrs_init__"]=self._add_method_dunders(init)self._script_snippets.append((script,globs,_attach_attrs_init))returnselfdefadd_eq(self):cd=self._cls_dictscript,globs=_make_eq_script(self._attrs)def_attach_eq(cls_dict,globs):cls_dict["__eq__"]=self._add_method_dunders(globs["__eq__"])self._script_snippets.append((script,globs,_attach_eq))cd["__ne__"]=__ne__returnselfdefadd_order(self):cd=self._cls_dictcd["__lt__"],cd["__le__"],cd["__gt__"],cd["__ge__"]=(self._add_method_dunders(meth)formethin_make_order(self._cls,self._attrs))returnselfdefadd_setattr(self):sa_attrs={}forainself._attrs:on_setattr=a.on_setattrorself._on_setattrifon_setattrandon_setattrisnotsetters.NO_OP:sa_attrs[a.name]=a,on_setattrifnotsa_attrs:returnselfifself._has_custom_setattr:# We need to write a __setattr__ but there already is one!msg="Can't combine custom __setattr__ with on_setattr hooks."raiseValueError(msg)# docstring comes from _add_method_dundersdef__setattr__(self,name,val):try:a,hook=sa_attrs[name]exceptKeyError:nval=valelse:nval=hook(self,a,val)_OBJ_SETATTR(self,name,nval)self._cls_dict["__attrs_own_setattr__"]=Trueself._cls_dict["__setattr__"]=self._add_method_dunders(__setattr__)self._wrote_own_setattr=Truereturnselfdef_add_method_dunders_unsafe(self,method:Callable)->Callable:""" Add __module__ and __qualname__ to a *method*. """method.__module__=self._cls.__module__method.__qualname__=f"{self._cls.__qualname__}.{method.__name__}"method.__doc__=(f"Method generated by attrs for class {self._cls.__qualname__}.")returnmethoddef_add_method_dunders_safe(self,method:Callable)->Callable:""" Add __module__ and __qualname__ to a *method* if possible. """withcontextlib.suppress(AttributeError):method.__module__=self._cls.__module__withcontextlib.suppress(AttributeError):method.__qualname__=f"{self._cls.__qualname__}.{method.__name__}"withcontextlib.suppress(AttributeError):method.__doc__=f"Method generated by attrs for class {self._cls.__qualname__}."returnmethoddef_determine_attrs_eq_order(cmp,eq,order,default_eq):""" Validate the combination of *cmp*, *eq*, and *order*. Derive the effective values of eq and order. If *eq* is None, set it to *default_eq*. """ifcmpisnotNoneandany((eqisnotNone,orderisnotNone)):msg="Don't mix `cmp` with `eq' and `order`."raiseValueError(msg)# cmp takes precedence due to bw-compatibility.ifcmpisnotNone:returncmp,cmp# If left None, equality is set to the specified default and ordering# mirrors equality.ifeqisNone:eq=default_eqiforderisNone:order=eqifeqisFalseandorderisTrue:msg="`order` can only be True if `eq` is True too."raiseValueError(msg)returneq,orderdef_determine_attrib_eq_order(cmp,eq,order,default_eq):""" Validate the combination of *cmp*, *eq*, and *order*. Derive the effective values of eq and order. If *eq* is None, set it to *default_eq*. """ifcmpisnotNoneandany((eqisnotNone,orderisnotNone)):msg="Don't mix `cmp` with `eq' and `order`."raiseValueError(msg)defdecide_callable_or_boolean(value):""" Decide whether a key function is used. """ifcallable(value):value,key=True,valueelse:key=Nonereturnvalue,key# cmp takes precedence due to bw-compatibility.ifcmpisnotNone:cmp,cmp_key=decide_callable_or_boolean(cmp)returncmp,cmp_key,cmp,cmp_key# If left None, equality is set to the specified default and ordering# mirrors equality.ifeqisNone:eq,eq_key=default_eq,Noneelse:eq,eq_key=decide_callable_or_boolean(eq)iforderisNone:order,order_key=eq,eq_keyelse:order,order_key=decide_callable_or_boolean(order)ifeqisFalseandorderisTrue:msg="`order` can only be True if `eq` is True too."raiseValueError(msg)returneq,eq_key,order,order_keydef_determine_whether_to_implement(cls,flag,auto_detect,dunders,default=True):""" Check whether we should implement a set of methods for *cls*. *flag* is the argument passed into @attr.s like 'init', *auto_detect* the same as passed into @attr.s and *dunders* is a tuple of attribute names whose presence signal that the user has implemented it themselves. Return *default* if no reason for either for or against is found. """ifflagisTrueorflagisFalse:returnflagifflagisNoneandauto_detectisFalse:returndefault# Logically, flag is None and auto_detect is True here.fordunderindunders:if_has_own_attribute(cls,dunder):returnFalsereturndefaultdefattrs(maybe_cls=None,these=None,repr_ns=None,repr=None,cmp=None,hash=None,init=None,slots=False,frozen=False,weakref_slot=True,str=False,auto_attribs=False,kw_only=False,cache_hash=False,auto_exc=False,eq=None,order=None,auto_detect=False,collect_by_mro=False,getstate_setstate=None,on_setattr=None,field_transformer=None,match_args=True,unsafe_hash=None,):r""" A class decorator that adds :term:`dunder methods` according to the specified attributes using `attr.ib` or the *these* argument. Consider using `attrs.define` / `attrs.frozen` in new code (``attr.s`` will *never* go away, though). Args: repr_ns (str): When using nested classes, there was no way in Python 2 to automatically detect that. This argument allows to set a custom name for a more meaningful ``repr`` output. This argument is pointless in Python 3 and is therefore deprecated. .. caution:: Refer to `attrs.define` for the rest of the parameters, but note that they can have different defaults. Notably, leaving *on_setattr* as `None` will **not** add any hooks. .. versionadded:: 16.0.0 *slots* .. versionadded:: 16.1.0 *frozen* .. versionadded:: 16.3.0 *str* .. versionadded:: 16.3.0 Support for ``__attrs_post_init__``. .. versionchanged:: 17.1.0 *hash* supports `None` as value which is also the default now. .. versionadded:: 17.3.0 *auto_attribs* .. versionchanged:: 18.1.0 If *these* is passed, no attributes are deleted from the class body. .. versionchanged:: 18.1.0 If *these* is ordered, the order is retained. .. versionadded:: 18.2.0 *weakref_slot* .. deprecated:: 18.2.0 ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` now raise a `DeprecationWarning` if the classes compared are subclasses of each other. ``__eq`` and ``__ne__`` never tried to compared subclasses to each other. .. versionchanged:: 19.2.0 ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` now do not consider subclasses comparable anymore. .. versionadded:: 18.2.0 *kw_only* .. versionadded:: 18.2.0 *cache_hash* .. versionadded:: 19.1.0 *auto_exc* .. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01. .. versionadded:: 19.2.0 *eq* and *order* .. versionadded:: 20.1.0 *auto_detect* .. versionadded:: 20.1.0 *collect_by_mro* .. versionadded:: 20.1.0 *getstate_setstate* .. versionadded:: 20.1.0 *on_setattr* .. versionadded:: 20.3.0 *field_transformer* .. versionchanged:: 21.1.0 ``init=False`` injects ``__attrs_init__`` .. versionchanged:: 21.1.0 Support for ``__attrs_pre_init__`` .. versionchanged:: 21.1.0 *cmp* undeprecated .. versionadded:: 21.3.0 *match_args* .. versionadded:: 22.2.0 *unsafe_hash* as an alias for *hash* (for :pep:`681` compliance). .. deprecated:: 24.1.0 *repr_ns* .. versionchanged:: 24.1.0 Instances are not compared as tuples of attributes anymore, but using a big ``and`` condition. This is faster and has more correct behavior for uncomparable values like `math.nan`. .. versionadded:: 24.1.0 If a class has an *inherited* classmethod called ``__attrs_init_subclass__``, it is executed after the class is created. .. deprecated:: 24.1.0 *hash* is deprecated in favor of *unsafe_hash*. """ifrepr_nsisnotNone:importwarningswarnings.warn(DeprecationWarning("The `repr_ns` argument is deprecated and will be removed in or after August 2025."),stacklevel=2,)eq_,order_=_determine_attrs_eq_order(cmp,eq,order,None)# unsafe_hash takes precedence due to PEP 681.ifunsafe_hashisnotNone:hash=unsafe_hashifisinstance(on_setattr,(list,tuple)):on_setattr=setters.pipe(*on_setattr)defwrap(cls):is_frozen=frozenor_has_frozen_base_class(cls)is_exc=auto_excisTrueandissubclass(cls,BaseException)has_own_setattr=auto_detectand_has_own_attribute(cls,"__setattr__")ifhas_own_setattrandis_frozen:msg="Can't freeze a class with a custom __setattr__."raiseValueError(msg)builder=_ClassBuilder(cls,these,slots,is_frozen,weakref_slot,_determine_whether_to_implement(cls,getstate_setstate,auto_detect,("__getstate__","__setstate__"),default=slots,),auto_attribs,kw_only,cache_hash,is_exc,collect_by_mro,on_setattr,has_own_setattr,field_transformer,)if_determine_whether_to_implement(cls,repr,auto_detect,("__repr__",)):builder.add_repr(repr_ns)ifstrisTrue:builder.add_str()eq=_determine_whether_to_implement(cls,eq_,auto_detect,("__eq__","__ne__"))ifnotis_excandeqisTrue:builder.add_eq()ifnotis_excand_determine_whether_to_implement(cls,order_,auto_detect,("__lt__","__le__","__gt__","__ge__")):builder.add_order()ifnotfrozen:builder.add_setattr()nonlocalhashif(hashisNoneandauto_detectisTrueand_has_own_attribute(cls,"__hash__")):hash=FalseifhashisnotTrueandhashisnotFalseandhashisnotNone:# Can't use `hash in` because 1 == True for example.msg="Invalid value for hash. Must be True, False, or None."raiseTypeError(msg)ifhashisFalseor(hashisNoneandeqisFalse)oris_exc:# Don't do anything. Should fall back to __object__'s __hash__# which is by id.ifcache_hash:msg="Invalid value for cache_hash. To use hash caching, hashing must be either explicitly or implicitly enabled."raiseTypeError(msg)elifhashisTrueor(hashisNoneandeqisTrueandis_frozenisTrue):# Build a __hash__ if told so, or if it's safe.builder.add_hash()else:# Raise TypeError on attempts to hash.ifcache_hash:msg="Invalid value for cache_hash. To use hash caching, hashing must be either explicitly or implicitly enabled."raiseTypeError(msg)builder.make_unhashable()if_determine_whether_to_implement(cls,init,auto_detect,("__init__",)):builder.add_init()else:builder.add_attrs_init()ifcache_hash:msg="Invalid value for cache_hash. To use hash caching, init must be True."raiseTypeError(msg)ifPY_3_13_PLUSandnot_has_own_attribute(cls,"__replace__"):builder.add_replace()if(PY_3_10_PLUSandmatch_argsandnot_has_own_attribute(cls,"__match_args__")):builder.add_match_args()returnbuilder.build_class()# maybe_cls's type depends on the usage of the decorator. It's a class# if it's used as `@attrs` but `None` if used as `@attrs()`.ifmaybe_clsisNone:returnwrapreturnwrap(maybe_cls)_attrs=attrs"""Internal alias so we can use it in functions that take an argument called*attrs*."""def_has_frozen_base_class(cls):""" Check whether *cls* has a frozen ancestor by looking at its __setattr__. """returncls.__setattr__is_frozen_setattrsdef_generate_unique_filename(cls:type,func_name:str)->str:""" Create a "filename" suitable for a function being generated. """return(f"<attrs generated {func_name}{cls.__module__}."f"{getattr(cls,'__qualname__',cls.__name__)}>")def_make_hash_script(cls:type,attrs:list[Attribute],frozen:bool,cache_hash:bool)->tuple[str,dict]:attrs=tuple(aforainattrsifa.hashisTrueor(a.hashisNoneanda.eqisTrue))tab=" "type_hash=hash(_generate_unique_filename(cls,"hash"))# If eq is custom generated, we need to include the functions in globsglobs={}hash_def="def __hash__(self"hash_func="hash(("closing_braces="))"ifnotcache_hash:hash_def+="):"else:hash_def+=", *"hash_def+=", _cache_wrapper=__import__('attr._make')._make._CacheHashWrapper):"hash_func="_cache_wrapper("+hash_funcclosing_braces+=")"method_lines=[hash_def]defappend_hash_computation_lines(prefix,indent):""" Generate the code for actually computing the hash code. Below this will either be returned directly or used to compute a value which is then cached, depending on the value of cache_hash """method_lines.extend([indent+prefix+hash_func,indent+f" {type_hash},",])forainattrs:ifa.eq_key:cmp_name=f"_{a.name}_key"globs[cmp_name]=a.eq_keymethod_lines.append(indent+f" {cmp_name}(self.{a.name}),")else:method_lines.append(indent+f" self.{a.name},")method_lines.append(indent+" "+closing_braces)ifcache_hash:method_lines.append(tab+f"if self.{_HASH_CACHE_FIELD} is None:")iffrozen:append_hash_computation_lines(f"object.__setattr__(self, '{_HASH_CACHE_FIELD}', ",tab*2)method_lines.append(tab*2+")")# close __setattr__else:append_hash_computation_lines(f"self.{_HASH_CACHE_FIELD} = ",tab*2)method_lines.append(tab+f"return self.{_HASH_CACHE_FIELD}")else:append_hash_computation_lines("return ",tab)script="\n".join(method_lines)returnscript,globsdef_add_hash(cls:type,attrs:list[Attribute]):""" Add a hash method to *cls*. """script,globs=_make_hash_script(cls,attrs,frozen=False,cache_hash=False)_compile_and_eval(script,globs,filename=_generate_unique_filename(cls,"__hash__"))cls.__hash__=globs["__hash__"]returnclsdef__ne__(self,other):""" Check equality and either forward a NotImplemented or return the result negated. """result=self.__eq__(other)ifresultisNotImplemented:returnNotImplementedreturnnotresultdef_make_eq_script(attrs:list)->tuple[str,dict]:""" Create __eq__ method for *cls* with *attrs*. """attrs=[aforainattrsifa.eq]lines=["def __eq__(self, other):"," if other.__class__ is not self.__class__:"," return NotImplemented",]globs={}ifattrs:lines.append(" return (")forainattrs:ifa.eq_key:cmp_name=f"_{a.name}_key"# Add the key function to the global namespace# of the evaluated function.globs[cmp_name]=a.eq_keylines.append(f" {cmp_name}(self.{a.name}) == {cmp_name}(other.{a.name})")else:lines.append(f" self.{a.name} == other.{a.name}")ifaisnotattrs[-1]:lines[-1]=f"{lines[-1]} and"lines.append(" )")else:lines.append(" return True")script="\n".join(lines)returnscript,globsdef_make_order(cls,attrs):""" Create ordering methods for *cls* with *attrs*. """attrs=[aforainattrsifa.order]defattrs_to_tuple(obj):""" Save us some typing. """returntuple(key(value)ifkeyelsevalueforvalue,keyin((getattr(obj,a.name),a.order_key)forainattrs))def__lt__(self,other):""" Automatically created by attrs. """ifother.__class__isself.__class__:returnattrs_to_tuple(self)<attrs_to_tuple(other)returnNotImplementeddef__le__(self,other):""" Automatically created by attrs. """ifother.__class__isself.__class__:returnattrs_to_tuple(self)<=attrs_to_tuple(other)returnNotImplementeddef__gt__(self,other):""" Automatically created by attrs. """ifother.__class__isself.__class__:returnattrs_to_tuple(self)>attrs_to_tuple(other)returnNotImplementeddef__ge__(self,other):""" Automatically created by attrs. """ifother.__class__isself.__class__:returnattrs_to_tuple(self)>=attrs_to_tuple(other)returnNotImplementedreturn__lt__,__le__,__gt__,__ge__def_add_eq(cls,attrs=None):""" Add equality methods to *cls* with *attrs*. """ifattrsisNone:attrs=cls.__attrs_attrs__script,globs=_make_eq_script(attrs)_compile_and_eval(script,globs,filename=_generate_unique_filename(cls,"__eq__"))cls.__eq__=globs["__eq__"]cls.__ne__=__ne__returnclsdef_make_repr_script(attrs,ns)->tuple[str,dict]:""" Create the source and globs for a __repr__ and return it. """# Figure out which attributes to include, and which function to use to# format them. The a.repr value can be either bool or a custom# callable.attr_names_with_reprs=tuple((a.name,(reprifa.reprisTrueelsea.repr),a.init)forainattrsifa.reprisnotFalse)globs={name+"_repr":rforname,r,_inattr_names_with_reprsifr!=repr}globs["_compat"]=_compatglobs["AttributeError"]=AttributeErrorglobs["NOTHING"]=NOTHINGattribute_fragments=[]forname,r,iinattr_names_with_reprs:accessor=("self."+nameifielse'getattr(self, "'+name+'", NOTHING)')fragment=("%s={%s!r}"%(name,accessor)ifr==reprelse"%s={%s_repr(%s)}"%(name,name,accessor))attribute_fragments.append(fragment)repr_fragment=", ".join(attribute_fragments)ifnsisNone:cls_name_fragment='{self.__class__.__qualname__.rsplit(">.", 1)[-1]}'else:cls_name_fragment=ns+".{self.__class__.__name__}"lines=["def __repr__(self):"," try:"," already_repring = _compat.repr_context.already_repring"," except AttributeError:"," already_repring = {id(self),}"," _compat.repr_context.already_repring = already_repring"," else:"," if id(self) in already_repring:"," return '...'"," else:"," already_repring.add(id(self))"," try:",f" return f'{cls_name_fragment}({repr_fragment})'"," finally:"," already_repring.remove(id(self))",]return"\n".join(lines),globsdef_add_repr(cls,ns=None,attrs=None):""" Add a repr method to *cls*. """ifattrsisNone:attrs=cls.__attrs_attrs__script,globs=_make_repr_script(attrs,ns)_compile_and_eval(script,globs,filename=_generate_unique_filename(cls,"__repr__"))cls.__repr__=globs["__repr__"]returncls
[docs]deffields(cls):""" Return the tuple of *attrs* attributes for a class. The tuple also allows accessing the fields by their names (see below for examples). Args: cls (type): Class to introspect. Raises: TypeError: If *cls* is not a class. attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class. Returns: tuple (with name accessors) of `attrs.Attribute` .. versionchanged:: 16.2.0 Returned tuple allows accessing the fields by name. .. versionchanged:: 23.1.0 Add support for generic classes. """generic_base=get_generic_base(cls)ifgeneric_baseisNoneandnotisinstance(cls,type):msg="Passed object must be a class."raiseTypeError(msg)attrs=getattr(cls,"__attrs_attrs__",None)ifattrsisNone:ifgeneric_baseisnotNone:attrs=getattr(generic_base,"__attrs_attrs__",None)ifattrsisnotNone:# Even though this is global state, stick it on here to speed# it up. We rely on `cls` being cached for this to be# efficient.cls.__attrs_attrs__=attrsreturnattrsmsg=f"{cls!r} is not an attrs-decorated class."raiseNotAnAttrsClassError(msg)returnattrs
[docs]deffields_dict(cls):""" Return an ordered dictionary of *attrs* attributes for a class, whose keys are the attribute names. Args: cls (type): Class to introspect. Raises: TypeError: If *cls* is not a class. attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class. Returns: dict[str, attrs.Attribute]: Dict of attribute name to definition .. versionadded:: 18.1.0 """ifnotisinstance(cls,type):msg="Passed object must be a class."raiseTypeError(msg)attrs=getattr(cls,"__attrs_attrs__",None)ifattrsisNone:msg=f"{cls!r} is not an attrs-decorated class."raiseNotAnAttrsClassError(msg)return{a.name:aforainattrs}
[docs]defvalidate(inst):""" Validate all attributes on *inst* that have a validator. Leaves all exceptions through. Args: inst: Instance of a class with *attrs* attributes. """if_config._run_validatorsisFalse:returnforainfields(inst.__class__):v=a.validatorifvisnotNone:v(inst,a,getattr(inst,a.name))
def_is_slot_attr(a_name,base_attr_map):""" Check if the attribute name comes from a slot class. """cls=base_attr_map.get(a_name)returnclsand"__slots__"incls.__dict__def_make_init_script(cls,attrs,pre_init,pre_init_has_args,post_init,frozen,slots,cache_hash,base_attr_map,is_exc,cls_on_setattr,attrs_init,)->tuple[str,dict,dict]:has_cls_on_setattr=(cls_on_setattrisnotNoneandcls_on_setattrisnotsetters.NO_OP)iffrozenandhas_cls_on_setattr:msg="Frozen classes can't use on_setattr."raiseValueError(msg)needs_cached_setattr=cache_hashorfrozenfiltered_attrs=[]attr_dict={}forainattrs:ifnota.initanda.defaultisNOTHING:continuefiltered_attrs.append(a)attr_dict[a.name]=aifa.on_setattrisnotNone:iffrozenisTrue:msg="Frozen classes can't use on_setattr."raiseValueError(msg)needs_cached_setattr=Trueelifhas_cls_on_setattranda.on_setattrisnotsetters.NO_OP:needs_cached_setattr=Truescript,globs,annotations=_attrs_to_init_script(filtered_attrs,frozen,slots,pre_init,pre_init_has_args,post_init,cache_hash,base_attr_map,is_exc,needs_cached_setattr,has_cls_on_setattr,"__attrs_init__"ifattrs_initelse"__init__",)ifcls.__module__insys.modules:# This makes typing.get_type_hints(CLS.__init__) resolve string types.globs.update(sys.modules[cls.__module__].__dict__)globs.update({"NOTHING":NOTHING,"attr_dict":attr_dict})ifneeds_cached_setattr:# Save the lookup overhead in __init__ if we need to circumvent# setattr hooks.globs["_cached_setattr_get"]=_OBJ_SETATTR.__get__returnscript,globs,annotationsdef_setattr(attr_name:str,value_var:str,has_on_setattr:bool)->str:""" Use the cached object.setattr to set *attr_name* to *value_var*. """returnf"_setattr('{attr_name}', {value_var})"def_setattr_with_converter(attr_name:str,value_var:str,has_on_setattr:bool,converter:Converter)->str:""" Use the cached object.setattr to set *attr_name* to *value_var*, but run its converter first. """returnf"_setattr('{attr_name}', {converter._fmt_converter_call(attr_name,value_var)})"def_assign(attr_name:str,value:str,has_on_setattr:bool)->str:""" Unless *attr_name* has an on_setattr hook, use normal assignment. Otherwise relegate to _setattr. """ifhas_on_setattr:return_setattr(attr_name,value,True)returnf"self.{attr_name} = {value}"def_assign_with_converter(attr_name:str,value_var:str,has_on_setattr:bool,converter:Converter)->str:""" Unless *attr_name* has an on_setattr hook, use normal assignment after conversion. Otherwise relegate to _setattr_with_converter. """ifhas_on_setattr:return_setattr_with_converter(attr_name,value_var,True,converter)returnf"self.{attr_name} = {converter._fmt_converter_call(attr_name,value_var)}"def_determine_setters(frozen:bool,slots:bool,base_attr_map:dict[str,type]):""" Determine the correct setter functions based on whether a class is frozen and/or slotted. """iffrozenisTrue:ifslotsisTrue:return(),_setattr,_setattr_with_converter# Dict frozen classes assign directly to __dict__.# But only if the attribute doesn't come from an ancestor slot# class.# Note _inst_dict will be used again below if cache_hash is Truedeffmt_setter(attr_name:str,value_var:str,has_on_setattr:bool)->str:if_is_slot_attr(attr_name,base_attr_map):return_setattr(attr_name,value_var,has_on_setattr)returnf"_inst_dict['{attr_name}'] = {value_var}"deffmt_setter_with_converter(attr_name:str,value_var:str,has_on_setattr:bool,converter:Converter,)->str:ifhas_on_setattror_is_slot_attr(attr_name,base_attr_map):return_setattr_with_converter(attr_name,value_var,has_on_setattr,converter)returnf"_inst_dict['{attr_name}'] = {converter._fmt_converter_call(attr_name,value_var)}"return(("_inst_dict = self.__dict__",),fmt_setter,fmt_setter_with_converter,)# Not frozen -- we can just assign directly.return(),_assign,_assign_with_converterdef_attrs_to_init_script(attrs:list[Attribute],is_frozen:bool,is_slotted:bool,call_pre_init:bool,pre_init_has_args:bool,call_post_init:bool,does_cache_hash:bool,base_attr_map:dict[str,type],is_exc:bool,needs_cached_setattr:bool,has_cls_on_setattr:bool,method_name:str,)->tuple[str,dict,dict]:""" Return a script of an initializer for *attrs*, a dict of globals, and annotations for the initializer. The globals are required by the generated script. """lines=["self.__attrs_pre_init__()"]ifcall_pre_initelse[]ifneeds_cached_setattr:lines.append(# Circumvent the __setattr__ descriptor to save one lookup per# assignment. Note _setattr will be used again below if# does_cache_hash is True."_setattr = _cached_setattr_get(self)")extra_lines,fmt_setter,fmt_setter_with_converter=_determine_setters(is_frozen,is_slotted,base_attr_map)lines.extend(extra_lines)args=[]kw_only_args=[]attrs_to_validate=[]# This is a dictionary of names to validator and converter callables.# Injecting this into __init__ globals lets us avoid lookups.names_for_globals={}annotations={"return":None}forainattrs:ifa.validator:attrs_to_validate.append(a)attr_name=a.namehas_on_setattr=a.on_setattrisnotNoneor(a.on_setattrisnotsetters.NO_OPandhas_cls_on_setattr)# a.alias is set to maybe-mangled attr_name in _ClassBuilder if not# explicitly providedarg_name=a.aliashas_factory=isinstance(a.default,Factory)maybe_self="self"ifhas_factoryanda.default.takes_selfelse""ifa.converterisnotNoneandnotisinstance(a.converter,Converter):converter=Converter(a.converter)else:converter=a.converterifa.initisFalse:ifhas_factory:init_factory_name=_INIT_FACTORY_PAT%(a.name,)ifconverterisnotNone:lines.append(fmt_setter_with_converter(attr_name,init_factory_name+f"({maybe_self})",has_on_setattr,converter,))names_for_globals[converter._get_global_name(a.name)]=(converter.converter)else:lines.append(fmt_setter(attr_name,init_factory_name+f"({maybe_self})",has_on_setattr,))names_for_globals[init_factory_name]=a.default.factoryelifconverterisnotNone:lines.append(fmt_setter_with_converter(attr_name,f"attr_dict['{attr_name}'].default",has_on_setattr,converter,))names_for_globals[converter._get_global_name(a.name)]=(converter.converter)else:lines.append(fmt_setter(attr_name,f"attr_dict['{attr_name}'].default",has_on_setattr,))elifa.defaultisnotNOTHINGandnothas_factory:arg=f"{arg_name}=attr_dict['{attr_name}'].default"ifa.kw_only:kw_only_args.append(arg)else:args.append(arg)ifconverterisnotNone:lines.append(fmt_setter_with_converter(attr_name,arg_name,has_on_setattr,converter))names_for_globals[converter._get_global_name(a.name)]=(converter.converter)else:lines.append(fmt_setter(attr_name,arg_name,has_on_setattr))elifhas_factory:arg=f"{arg_name}=NOTHING"ifa.kw_only:kw_only_args.append(arg)else:args.append(arg)lines.append(f"if {arg_name} is not NOTHING:")init_factory_name=_INIT_FACTORY_PAT%(a.name,)ifconverterisnotNone:lines.append(" "+fmt_setter_with_converter(attr_name,arg_name,has_on_setattr,converter))lines.append("else:")lines.append(" "+fmt_setter_with_converter(attr_name,init_factory_name+"("+maybe_self+")",has_on_setattr,converter,))names_for_globals[converter._get_global_name(a.name)]=(converter.converter)else:lines.append(" "+fmt_setter(attr_name,arg_name,has_on_setattr))lines.append("else:")lines.append(" "+fmt_setter(attr_name,init_factory_name+"("+maybe_self+")",has_on_setattr,))names_for_globals[init_factory_name]=a.default.factoryelse:ifa.kw_only:kw_only_args.append(arg_name)else:args.append(arg_name)ifconverterisnotNone:lines.append(fmt_setter_with_converter(attr_name,arg_name,has_on_setattr,converter))names_for_globals[converter._get_global_name(a.name)]=(converter.converter)else:lines.append(fmt_setter(attr_name,arg_name,has_on_setattr))ifa.initisTrue:ifa.typeisnotNoneandconverterisNone:annotations[arg_name]=a.typeelifconverterisnotNoneandconverter._first_param_type:# Use the type from the converter if present.annotations[arg_name]=converter._first_param_typeifattrs_to_validate:# we can skip this if there are no validators.names_for_globals["_config"]=_configlines.append("if _config._run_validators is True:")forainattrs_to_validate:val_name="__attr_validator_"+a.nameattr_name="__attr_"+a.namelines.append(f" {val_name}(self, {attr_name}, self.{a.name})")names_for_globals[val_name]=a.validatornames_for_globals[attr_name]=aifcall_post_init:lines.append("self.__attrs_post_init__()")# Because this is set only after __attrs_post_init__ is called, a crash# will result if post-init tries to access the hash code. This seemed# preferable to setting this beforehand, in which case alteration to field# values during post-init combined with post-init accessing the hash code# would result in silent bugs.ifdoes_cache_hash:ifis_frozen:ifis_slotted:init_hash_cache=f"_setattr('{_HASH_CACHE_FIELD}', None)"else:init_hash_cache=f"_inst_dict['{_HASH_CACHE_FIELD}'] = None"else:init_hash_cache=f"self.{_HASH_CACHE_FIELD} = None"lines.append(init_hash_cache)# For exceptions we rely on BaseException.__init__ for proper# initialization.ifis_exc:vals=",".join(f"self.{a.name}"forainattrsifa.init)lines.append(f"BaseException.__init__(self, {vals})")args=", ".join(args)pre_init_args=argsifkw_only_args:# leading comma & kw_only argsargs+=f"{', 'ifargselse''}*, {', '.join(kw_only_args)}"pre_init_kw_only_args=", ".join([f"{kw_arg_name}={kw_arg_name}"# We need to remove the defaults from the kw_only_args.forkw_arg_namein(kwa.split("=")[0]forkwainkw_only_args)])pre_init_args+=", "ifpre_init_argselse""pre_init_args+=pre_init_kw_only_argsifcall_pre_initandpre_init_has_args:# If pre init method has arguments, pass same arguments as `__init__`.lines[0]=f"self.__attrs_pre_init__({pre_init_args})"# Python <3.12 doesn't allow backslashes in f-strings.NL="\n "return(f"""def {method_name}(self, {args}):{NL.join(lines)iflineselse"pass"}""",names_for_globals,annotations,)def_default_init_alias_for(name:str)->str:""" The default __init__ parameter name for a field. This performs private-name adjustment via leading-unscore stripping, and is the default value of Attribute.alias if not provided. """returnname.lstrip("_")
[docs]classAttribute:""" *Read-only* representation of an attribute. .. warning:: You should never instantiate this class yourself. The class has *all* arguments of `attr.ib` (except for ``factory`` which is only syntactic sugar for ``default=Factory(...)`` plus the following: - ``name`` (`str`): The name of the attribute. - ``alias`` (`str`): The __init__ parameter name of the attribute, after any explicit overrides and default private-attribute-name handling. - ``inherited`` (`bool`): Whether or not that attribute has been inherited from a base class. - ``eq_key`` and ``order_key`` (`typing.Callable` or `None`): The callables that are used for comparing and ordering objects by this attribute, respectively. These are set by passing a callable to `attr.ib`'s ``eq``, ``order``, or ``cmp`` arguments. See also :ref:`comparison customization <custom-comparison>`. Instances of this class are frequently used for introspection purposes like: - `fields` returns a tuple of them. - Validators get them passed as the first argument. - The :ref:`field transformer <transform-fields>` hook receives a list of them. - The ``alias`` property exposes the __init__ parameter name of the field, with any overrides and default private-attribute handling applied. .. versionadded:: 20.1.0 *inherited* .. versionadded:: 20.1.0 *on_setattr* .. versionchanged:: 20.2.0 *inherited* is not taken into account for equality checks and hashing anymore. .. versionadded:: 21.1.0 *eq_key* and *order_key* .. versionadded:: 22.2.0 *alias* For the full version history of the fields, see `attr.ib`. """# These slots must NOT be reordered because we use them later for# instantiation.__slots__=(# noqa: RUF023"name","default","validator","repr","eq","eq_key","order","order_key","hash","init","metadata","type","converter","kw_only","inherited","on_setattr","alias",)
[docs]def__init__(self,name,default,validator,repr,cmp,# XXX: unused, remove along with other cmp code.hash,init,inherited,metadata=None,type=None,converter=None,kw_only=False,eq=None,eq_key=None,order=None,order_key=None,on_setattr=None,alias=None,):eq,eq_key,order,order_key=_determine_attrib_eq_order(cmp,eq_keyoreq,order_keyororder,True)# Cache this descriptor here to speed things up later.bound_setattr=_OBJ_SETATTR.__get__(self)# Despite the big red warning, people *do* instantiate `Attribute`# themselves.bound_setattr("name",name)bound_setattr("default",default)bound_setattr("validator",validator)bound_setattr("repr",repr)bound_setattr("eq",eq)bound_setattr("eq_key",eq_key)bound_setattr("order",order)bound_setattr("order_key",order_key)bound_setattr("hash",hash)bound_setattr("init",init)bound_setattr("converter",converter)bound_setattr("metadata",(types.MappingProxyType(dict(metadata))# Shallow copyifmetadataelse_EMPTY_METADATA_SINGLETON),)bound_setattr("type",type)bound_setattr("kw_only",kw_only)bound_setattr("inherited",inherited)bound_setattr("on_setattr",on_setattr)bound_setattr("alias",alias)
[docs]@classmethoddeffrom_counting_attr(cls,name:str,ca:_CountingAttr,type=None):# type holds the annotated value. deal with conflicts:iftypeisNone:type=ca.typeelifca.typeisnotNone:msg=f"Type annotation and type argument cannot both be present for '{name}'."raiseValueError(msg)returncls(name,ca._default,ca._validator,ca.repr,None,ca.hash,ca.init,False,ca.metadata,type,ca.converter,ca.kw_only,ca.eq,ca.eq_key,ca.order,ca.order_key,ca.on_setattr,ca.alias,)
# Don't use attrs.evolve since fields(Attribute) doesn't work
[docs]defevolve(self,**changes):""" Copy *self* and apply *changes*. This works similarly to `attrs.evolve` but that function does not work with :class:`attrs.Attribute`. It is mainly meant to be used for `transform-fields`. .. versionadded:: 20.3.0 """new=copy.copy(self)new._setattrs(changes.items())returnnew
# Don't use _add_pickle since fields(Attribute) doesn't work
[docs]def__getstate__(self):""" Play nice with pickle. """returntuple(getattr(self,name)ifname!="metadata"elsedict(self.metadata)fornameinself.__slots__)
[docs]def__setstate__(self,state):""" Play nice with pickle. """self._setattrs(zip(self.__slots__,state))
_a=[Attribute(name=name,default=NOTHING,validator=None,repr=True,cmp=None,eq=True,order=False,hash=(name!="metadata"),init=True,inherited=False,alias=_default_init_alias_for(name),)fornameinAttribute.__slots__]Attribute=_add_hash(_add_eq(_add_repr(Attribute,attrs=_a),attrs=[aforain_aifa.name!="inherited"],),attrs=[aforain_aifa.hashanda.name!="inherited"],)class_CountingAttr:""" Intermediate representation of attributes that uses a counter to preserve the order in which the attributes have been defined. *Internal* data structure of the attrs library. Running into is most likely the result of a bug like a forgotten `@attr.s` decorator. """__slots__=("_default","_validator","alias","converter","counter","eq","eq_key","hash","init","kw_only","metadata","on_setattr","order","order_key","repr","type",)__attrs_attrs__=(*tuple(Attribute(name=name,alias=_default_init_alias_for(name),default=NOTHING,validator=None,repr=True,cmp=None,hash=True,init=True,kw_only=False,eq=True,eq_key=None,order=False,order_key=None,inherited=False,on_setattr=None,)fornamein("counter","_default","repr","eq","order","hash","init","on_setattr","alias",)),Attribute(name="metadata",alias="metadata",default=None,validator=None,repr=True,cmp=None,hash=False,init=True,kw_only=False,eq=True,eq_key=None,order=False,order_key=None,inherited=False,on_setattr=None,),)cls_counter=0def__init__(self,default,validator,repr,cmp,hash,init,converter,metadata,type,kw_only,eq,eq_key,order,order_key,on_setattr,alias,):_CountingAttr.cls_counter+=1self.counter=_CountingAttr.cls_counterself._default=defaultself._validator=validatorself.converter=converterself.repr=reprself.eq=eqself.eq_key=eq_keyself.order=orderself.order_key=order_keyself.hash=hashself.init=initself.metadata=metadataself.type=typeself.kw_only=kw_onlyself.on_setattr=on_setattrself.alias=aliasdefvalidator(self,meth):""" Decorator that adds *meth* to the list of validators. Returns *meth* unchanged. .. versionadded:: 17.1.0 """ifself._validatorisNone:self._validator=methelse:self._validator=and_(self._validator,meth)returnmethdefdefault(self,meth):""" Decorator that allows to set the default for an attribute. Returns *meth* unchanged. Raises: DefaultAlreadySetError: If default has been set before. .. versionadded:: 17.1.0 """ifself._defaultisnotNOTHING:raiseDefaultAlreadySetErrorself._default=Factory(meth,takes_self=True)returnmeth_CountingAttr=_add_eq(_add_repr(_CountingAttr))
[docs]classFactory:""" Stores a factory callable. If passed as the default value to `attrs.field`, the factory is used to generate a new value. Args: factory (typing.Callable): A callable that takes either none or exactly one mandatory positional argument depending on *takes_self*. takes_self (bool): Pass the partially initialized instance that is being initialized as a positional argument. .. versionadded:: 17.1.0 *takes_self* """__slots__=("factory","takes_self")
[docs]classConverter:""" Stores a converter callable. Allows for the wrapped converter to take additional arguments. The arguments are passed in the order they are documented. Args: converter (Callable): A callable that converts the passed value. takes_self (bool): Pass the partially initialized instance that is being initialized as a positional argument. (default: `False`) takes_field (bool): Pass the field definition (an :class:`Attribute`) into the converter as a positional argument. (default: `False`) .. versionadded:: 24.1.0 """__slots__=("__call__","_first_param_type","_global_name","converter","takes_field","takes_self",)
@staticmethoddef_get_global_name(attr_name:str)->str:""" Return the name that a converter for an attribute name *attr_name* would have. """returnf"__attr_converter_{attr_name}"def_fmt_converter_call(self,attr_name:str,value_var:str)->str:""" Return a string that calls the converter for an attribute name *attr_name* and the value in variable named *value_var* according to `self.takes_self` and `self.takes_field`. """ifnot(self.takes_selforself.takes_field):returnf"{self._get_global_name(attr_name)}({value_var})"ifself.takes_selfandself.takes_field:returnf"{self._get_global_name(attr_name)}({value_var}, self, attr_dict['{attr_name}'])"ifself.takes_self:returnf"{self._get_global_name(attr_name)}({value_var}, self)"returnf"{self._get_global_name(attr_name)}({value_var}, attr_dict['{attr_name}'])"
[docs]def__getstate__(self):""" Return a dict containing only converter and takes_self -- the rest gets computed when loading. """return{"converter":self.converter,"takes_self":self.takes_self,"takes_field":self.takes_field,}
[docs]def__setstate__(self,state):""" Load instance from state. """self.__init__(**state)
[docs]defmake_class(name,attrs,bases=(object,),class_body=None,**attributes_arguments):r""" A quick way to create a new class called *name* with *attrs*. .. note:: ``make_class()`` is a thin wrapper around `attr.s`, not `attrs.define` which means that it doesn't come with some of the improved defaults. For example, if you want the same ``on_setattr`` behavior as in `attrs.define`, you have to pass the hooks yourself: ``make_class(..., on_setattr=setters.pipe(setters.convert, setters.validate)`` .. warning:: It is *your* duty to ensure that the class name and the attribute names are valid identifiers. ``make_class()`` will *not* validate them for you. Args: name (str): The name for the new class. attrs (list | dict): A list of names or a dictionary of mappings of names to `attr.ib`\ s / `attrs.field`\ s. The order is deduced from the order of the names or attributes inside *attrs*. Otherwise the order of the definition of the attributes is used. bases (tuple[type, ...]): Classes that the new class will subclass. class_body (dict): An optional dictionary of class attributes for the new class. attributes_arguments: Passed unmodified to `attr.s`. Returns: type: A new class with *attrs*. .. versionadded:: 17.1.0 *bases* .. versionchanged:: 18.1.0 If *attrs* is ordered, the order is retained. .. versionchanged:: 23.2.0 *class_body* .. versionchanged:: 25.2.0 Class names can now be unicode. """# Class identifiers are converted into the normal form NFKC while parsingname=unicodedata.normalize("NFKC",name)ifisinstance(attrs,dict):cls_dict=attrselifisinstance(attrs,(list,tuple)):cls_dict={a:attrib()forainattrs}else:msg="attrs argument must be a dict or a list."raiseTypeError(msg)pre_init=cls_dict.pop("__attrs_pre_init__",None)post_init=cls_dict.pop("__attrs_post_init__",None)user_init=cls_dict.pop("__init__",None)body={}ifclass_bodyisnotNone:body.update(class_body)ifpre_initisnotNone:body["__attrs_pre_init__"]=pre_initifpost_initisnotNone:body["__attrs_post_init__"]=post_initifuser_initisnotNone:body["__init__"]=user_inittype_=types.new_class(name,bases,{},lambdans:ns.update(body))# For pickling to work, the __module__ variable needs to be set to the# frame where the class is created. Bypass this step in environments where# sys._getframe is not defined (Jython for example) or sys._getframe is not# defined for arguments greater than 0 (IronPython).withcontextlib.suppress(AttributeError,ValueError):type_.__module__=sys._getframe(1).f_globals.get("__name__","__main__")# We do it here for proper warnings with meaningful stacklevel.cmp=attributes_arguments.pop("cmp",None)(attributes_arguments["eq"],attributes_arguments["order"],)=_determine_attrs_eq_order(cmp,attributes_arguments.get("eq"),attributes_arguments.get("order"),True,)cls=_attrs(these=cls_dict,**attributes_arguments)(type_)# Only add type annotations now or "_attrs()" will complain:cls.__annotations__={k:v.typefork,vincls_dict.items()ifv.typeisnotNone}returncls
# These are required by within this module so we define them here and merely# import into .validators / .converters.@attrs(slots=True,unsafe_hash=True)class_AndValidator:""" Compose many validators to a single one. """_validators=attrib()def__call__(self,inst,attr,value):forvinself._validators:v(inst,attr,value)defand_(*validators):""" A validator that composes multiple validators into one. When called on a value, it runs all wrapped validators. Args: validators (~collections.abc.Iterable[typing.Callable]): Arbitrary number of validators. .. versionadded:: 17.1.0 """vals=[]forvalidatorinvalidators:vals.extend(validator._validatorsifisinstance(validator,_AndValidator)else[validator])return_AndValidator(tuple(vals))defpipe(*converters):""" A converter that composes multiple converters into one. When called on a value, it runs all wrapped converters, returning the *last* value. Type annotations will be inferred from the wrapped converters', if they have any. converters (~collections.abc.Iterable[typing.Callable]): Arbitrary number of converters. .. versionadded:: 20.1.0 """return_instance=any(isinstance(c,Converter)forcinconverters)ifreturn_instance:defpipe_converter(val,inst,field):forcinconverters:val=(c(val,inst,field)ifisinstance(c,Converter)elsec(val))returnvalelse:defpipe_converter(val):forcinconverters:val=c(val)returnvalifnotconverters:# If the converter list is empty, pipe_converter is the identity.A=TypeVar("A")pipe_converter.__annotations__.update({"val":A,"return":A})else:# Get parameter type from first converter.t=_AnnotationExtractor(converters[0]).get_first_param_type()ift:pipe_converter.__annotations__["val"]=tlast=converters[-1]ifnotPY_3_11_PLUSandisinstance(last,Converter):last=last.__call__# Get return type from last converter.rt=_AnnotationExtractor(last).get_return_type()ifrt:pipe_converter.__annotations__["return"]=rtifreturn_instance:returnConverter(pipe_converter,takes_self=True,takes_field=True)returnpipe_converter