# Released under the MIT License. See LICENSE for details.#"""Functionality related to capturing nested dataclass paths."""from__future__importannotationsimportdataclassesfromtypingimportTYPE_CHECKING,TypeVar,Genericfromefro.dataclassio._baseimport_parse_annotated,_get_originfromefro.dataclassio._prepimportPrepSessionifTYPE_CHECKING:fromtypingimportAny,CallableT=TypeVar('T')class_PathCapture:"""Utility for obtaining dataclass storage paths in a type safe way."""def__init__(self,obj:Any,pathparts:list[str]|None=None):self._is_dataclass=dataclasses.is_dataclass(obj)ifpathpartsisNone:pathparts=[]self._cls=objifisinstance(obj,type)elsetype(obj)self._pathparts=pathpartsdef__getattr__(self,name:str)->_PathCapture:# We only allow diving into sub-objects if we are a dataclass.ifnotself._is_dataclass:raiseTypeError(f"Field path cannot include attribute '{name}' "f'under parent {self._cls}; parent types must be dataclasses.')prep=PrepSession(explicit=False).prep_dataclass(self._cls,recursion_level=0)assertprepisnotNonetry:anntype=prep.annotations[name]exceptKeyErrorasexc:raiseAttributeError(f'{type(self)} has no {name} field.')fromexcanntype,ioattrs=_parse_annotated(anntype)storagename=(nameif(ioattrsisNoneorioattrs.storagenameisNone)elseioattrs.storagename)origin=_get_origin(anntype)return_PathCapture(origin,pathparts=self._pathparts+[storagename])@propertydefpath(self)->str:"""The final output path."""return'.'.join(self._pathparts)classDataclassFieldLookup(Generic[T]):"""Get info about nested dataclass fields in type-safe way."""def__init__(self,cls:type[T])->None:self.cls=cls
[docs]defpath(self,callback:Callable[[T],Any])->str:"""Look up a path on child dataclass fields. example: DataclassFieldLookup(MyType).path(lambda obj: obj.foo.bar) The above example will return the string 'foo.bar' or something like 'f.b' if the dataclasses have custom storage names set. It will also be static-type-checked, triggering an error if MyType.foo.bar is not a valid path. Note, however, that the callback technically allows any return value but only nested dataclasses and their fields will succeed. """# We tell the type system that we are returning an instance# of our class, which allows it to perform type checking on# member lookups. In reality, however, we are providing a# special object which captures path lookups, so we can build# a string from them.ifnotTYPE_CHECKING:out=callback(_PathCapture(self.cls))ifnotisinstance(out,_PathCapture):raiseTypeError(f'Expected a valid path under'f' the provided object; got a {type(out)}.')returnout.pathreturn''
[docs]defpaths(self,callback:Callable[[T],list[Any]])->list[str]:"""Look up multiple paths on child dataclass fields. Functionality is identical to path() but for multiple paths at once. example: DataclassFieldLookup(MyType).paths(lambda obj: [obj.foo, obj.bar]) """outvals:list[str]=[]ifnotTYPE_CHECKING:outs=callback(_PathCapture(self.cls))assertisinstance(outs,list)foroutinouts:ifnotisinstance(out,_PathCapture):raiseTypeError(f'Expected a valid path under'f' the provided object; got a {type(out)}.')outvals.append(out.path)returnoutvals