@@ -35,6 +35,32 @@ class Sentinel:
3535 pass
3636
3737
38+ @dataclasses .dataclass
39+ class _BaseInfraState :
40+ """Ephemeral/recomputable state, stored as a PrivateAttr in
41+ __pydantic_private__ and accessed via _fast_state() to bypass
42+ pydantic's __getattr__ on hot paths. Reset on pickle."""
43+
44+ checked_configs : bool = False
45+ factory : str | None = None
46+ uid : str | None = None
47+ infra_method : "InfraMethod | None" = None
48+ method_override : tp .Any = None
49+
50+
51+ def _fast_state (infra : "BaseInfra" ) -> _BaseInfraState :
52+ """Read _state directly from __pydantic_private__, bypassing __getattr__
53+ for fast access.
54+
55+ Falls back to normal access if pydantic's storage changes.
56+ See test_fast_state_no_fallback for upgrade detection.
57+ """
58+ try :
59+ return infra .__pydantic_private__ ["_state" ] # type: ignore[index]
60+ except (KeyError , TypeError ):
61+ return infra ._state
62+
63+
3864@pydantic .model_validator (mode = "after" )
3965def model_with_infra_validator_after (obj : pydantic .BaseModel ) -> pydantic .BaseModel :
4066 return _add_name (obj , propagate_defaults = True )
@@ -115,13 +141,20 @@ class BaseInfra(pydantic.BaseModel):
115141 # {factory} will be replaced by method name and version tag
116142 # {uid} by the owner class uid
117143 _uid_string : str = "{method},{version}/{uid}"
118- # information stored for fast access after apply is called
119- _uid : str | None = None # stored uid once computed (and once model is frozen)
144+ _state : _BaseInfraState = pydantic . PrivateAttr ( default_factory = _BaseInfraState )
145+ _uid : str | None = None # deprecated: now in _state, kept for backward compat
120146 _obj : tp .Any = pydantic .PrivateAttr () # pydantic model the infra is an attribute of
121- _checked_configs : bool = False # only do it once
122147 _infra_name : str = ""
123148 _infra_method : "InfraMethod | None" = pydantic .PrivateAttr (None ) # method container
124149
150+ def __getstate__ (self ) -> dict [str , tp .Any ]:
151+ out = super ().__getstate__ ()
152+ # Reset ephemeral state so pickled copies start fresh
153+ state = out ["__pydantic_private__" ].get ("_state" )
154+ if state is not None :
155+ out ["__pydantic_private__" ]["_state" ] = type (state )()
156+ return out
157+
125158 def __setstate__ (self , state : tp .Any ) -> None :
126159 if "__dict__" in state :
127160 d = state ["__dict__" ]
@@ -131,6 +164,8 @@ def __setstate__(self, state: tp.Any) -> None:
131164 if "__pydantic_private__" in state :
132165 d = state ["__pydantic_private__" ]
133166 d .setdefault ("_uid_string" , "{method},{version}/{uid}" )
167+ if "_state" not in d :
168+ d ["_state" ] = type (self )._state .default_factory () # type: ignore
134169 # infra method can lose the name, so let's recover it
135170 # (in private infra in particular)
136171 iname = d .get ("_infra_name" , None )
@@ -195,7 +230,8 @@ def config(self, uid: bool = True, exclude_defaults: bool = False) -> ConfDict:
195230 return cdict
196231
197232 def _check_configs (self , write : bool = True ) -> None :
198- if self ._checked_configs :
233+ state = _fast_state (self )
234+ if state .checked_configs :
199235 return # already done
200236 xpfolder = self .uid_folder ()
201237 if xpfolder is None :
@@ -207,7 +243,7 @@ def _check_configs(self, write: bool = True) -> None:
207243 full_uid = self .config (uid = True , exclude_defaults = False ),
208244 )
209245 dump .check_and_write (xpfolder , write = write )
210- self . _checked_configs = True
246+ state . checked_configs = True
211247 # Set permissions on written files
212248 if write :
213249 for name in ("uid" , "full-uid" , "config" ):
@@ -219,6 +255,9 @@ def _check_configs(self, write: bool = True) -> None:
219255 pass
220256
221257 def _factory (self ) -> str :
258+ state = _fast_state (self )
259+ if state .factory is not None :
260+ return state .factory
222261 cls = self ._obj .__class__
223262 if self ._infra_method is None :
224263 raise RuntimeError (f"Infra { self !r} was not applied to a method" )
@@ -238,52 +277,61 @@ def _factory(self) -> str:
238277 current_m = getattr (cls , m .__name__ )
239278 if isinstance (current_m , property ) and self ._infra_method is current_m .fget :
240279 factory = f"{ cls .__module__ } .{ cls .__qualname__ } .{ name } "
280+ state .factory = factory
241281 return factory
242282
243283 def uid (self ) -> str :
244284 """Returns the unique uid of the task"""
285+ state = _fast_state (self )
286+ if state .uid is not None :
287+ return state .uid
288+ # deprecated path: check legacy _uid attribute for backward compat
245289 if not hasattr (self , "_uid" ):
246290 self ._uid = None # backward-compatibility
247- if self ._uid is None :
248- cfg = self .config (uid = True , exclude_defaults = True )
249- uid = cfg .to_uid ()
250- uid = uid if uid else "default"
251- params = dict (method = self ._factory (), version = self .version , uid = uid )
252- parsed = string .Formatter ().parse (self ._uid_string )
253- names = {v [1 ] for v in parsed if v [1 ] is not None }
254- if names != set (params ):
255- msg = f"uid_string { self ._uid_string !r} should contain exactly { set (params )} "
256- msg += f"\n but got { names } for infra applied on { self ._obj !r} "
257- raise ValueError (msg )
258- self ._uid = self ._uid_string .format (** params )
259- utils .recursive_freeze (self ._obj )
260- msg = "Froze instance %s after computing its uid: %s"
261- logger .debug (msg , repr (self ._obj ), self ._uid )
262- # compat
263- if self .folder is not None and uid != "default" :
264- folder = Path (self .folder ) / self ._uid
265- if not folder .exists ():
266- params ["uid" ] = cfg .to_uid (version = 2 )
267- old = Path (self .folder ) / self ._uid_string .format (** params )
268- if old .exists ():
269- # rename all folders in cache at once if possible
270- from exca import helpers
271-
272- helpers .update_uids (self .folder , dryrun = False )
273- if old .exists ():
274- # if this very cache was not updated
275- # (eg: because of unexpected uid_string), then fix it manually
276- msg = "Automatic update fail, manual update to new uid: '%s' -> '%s'"
277- logger .warning (msg , old , folder )
278- shutil .move (old , folder )
279- latest = ConfDict .LATEST_UID_VERSION
280- # warn for mixture of versioning
281- if self .folder is not None and ConfDict .UID_VERSION != latest :
282- new = Path (self .folder ) / cfg .to_uid (version = latest )
283- if new .exists ():
284- msg = "Found folder with latest version %s but currently using %s"
285- logger .warning (msg , latest , ConfDict .UID_VERSION )
286- return self ._uid
291+ if self ._uid is not None :
292+ state .uid = self ._uid
293+ return self ._uid
294+ cfg = self .config (uid = True , exclude_defaults = True )
295+ uid = cfg .to_uid ()
296+ uid = uid if uid else "default"
297+ params = dict (method = self ._factory (), version = self .version , uid = uid )
298+ parsed = string .Formatter ().parse (self ._uid_string )
299+ names = {v [1 ] for v in parsed if v [1 ] is not None }
300+ if names != set (params ):
301+ msg = f"uid_string { self ._uid_string !r} should contain exactly { set (params )} "
302+ msg += f"\n but got { names } for infra applied on { self ._obj !r} "
303+ raise ValueError (msg )
304+ computed = self ._uid_string .format (** params )
305+ state .uid = computed
306+ self ._uid = computed # deprecated: kept for backward compat
307+ utils .recursive_freeze (self ._obj )
308+ msg = "Froze instance %s after computing its uid: %s"
309+ logger .debug (msg , repr (self ._obj ), computed )
310+ # compat
311+ if self .folder is not None and uid != "default" :
312+ folder = Path (self .folder ) / computed
313+ if not folder .exists ():
314+ params ["uid" ] = cfg .to_uid (version = 2 )
315+ old = Path (self .folder ) / self ._uid_string .format (** params )
316+ if old .exists ():
317+ # rename all folders in cache at once if possible
318+ from exca import helpers
319+
320+ helpers .update_uids (self .folder , dryrun = False )
321+ if old .exists ():
322+ # if this very cache was not updated
323+ # (eg: because of unexpected uid_string), then fix it manually
324+ msg = "Automatic update fail, manual update to new uid: '%s' -> '%s'"
325+ logger .warning (msg , old , folder )
326+ shutil .move (old , folder )
327+ latest = ConfDict .LATEST_UID_VERSION
328+ # warn for mixture of versioning
329+ if self .folder is not None and ConfDict .UID_VERSION != latest :
330+ new = Path (self .folder ) / cfg .to_uid (version = latest )
331+ if new .exists ():
332+ msg = "Found folder with latest version %s but currently using %s"
333+ logger .warning (msg , latest , ConfDict .UID_VERSION )
334+ return computed
287335
288336 def uid_folder (self , create : bool = False ) -> Path | None :
289337 """Folder where this task instance is stored"""
@@ -438,7 +486,16 @@ class InfraMethod(BaseInfraMethod):
438486 default_infra : BaseInfra | None = None # COMPAT
439487
440488 def __call__ (self , obj : pydantic .BaseModel ) -> tp .Any :
441- if self .infra_name is None or not self .infra_name :
489+ # fast path: return cached result if this InfraMethod already resolved
490+ infra_name = self .infra_name
491+ if infra_name :
492+ infra = getattr (obj , infra_name , None )
493+ if infra is not None :
494+ state = _fast_state (infra )
495+ if state .infra_method is self and state .method_override is not None :
496+ return state .method_override
497+ # full dispatch (first call or legacy/override paths)
498+ if not infra_name :
442499 default_infra = getattr (self , "default_infra" , None ) # LEGACY
443500 if default_infra is not None :
444501 self .infra_name = default_infra ._infra_name
@@ -455,9 +512,13 @@ def __call__(self, obj: pydantic.BaseModel) -> tp.Any:
455512 raise TypeError ("infra can only be added to pydantic.BaseModel" )
456513 # get default
457514 if infra_name in type (obj ).model_fields :
458- default_imethod = type (obj ).model_fields [infra_name ].default ._infra_method
515+ default_imethod = (
516+ type (obj )
517+ .model_fields [infra_name ]
518+ .default .__pydantic_private__ ["_infra_method" ]
519+ )
459520 elif infra_name .startswith ("_" ):
460- default_imethod = obj .__private_attributes__ [infra_name ].default ._infra_method # type: ignore
521+ default_imethod = obj .__private_attributes__ [infra_name ].default .__pydantic_private__ [ " _infra_method" ] # type: ignore
461522 else :
462523 raise RuntimeError (f"Could not find infra named { infra_name !r} on { obj !r} " )
463524 if default_imethod is None :
@@ -468,12 +529,16 @@ def __call__(self, obj: pydantic.BaseModel) -> tp.Any:
468529 msg = "This should only happen when unpickling config which was modified from legacy to decorator"
469530 logger .warning (msg )
470531 return None
471- if not hasattr (infra , "_obj" ):
532+ if "_obj" not in (infra . __pydantic_private__ or {} ):
472533 infra ._obj = obj # only for legacy to decorator change compatibility
473534 if default_imethod is not self :
474535 # bypassing infra as it was overriden
475536 return functools .partial (self .method , obj )
476- return infra ._method_override
537+ result = infra ._method_override
538+ state = _fast_state (infra )
539+ state .infra_method = self
540+ state .method_override = result
541+ return result
477542
478543 def check_method_signature (self ) -> None :
479544 sig = inspect .signature (self .method )
0 commit comments