@@ -278,68 +278,74 @@ def build_env(
278278 name: Name of the object in the env.
279279 path: The module path to serialize. Other modules will not be walked and treated as imports.
280280 """
281+ # We don't rely on `env` to keep track of visited objects, because it's populated in post-order
282+ visited : t .Set [str ] = set ()
283+
284+ def walk (obj : t .Any , name : str ) -> None :
285+ obj_module = inspect .getmodule (obj )
286+ if name in visited or (obj_module and obj_module .__name__ == "builtins" ):
287+ return
288+
289+ visited .add (name )
290+ if name not in env :
291+ if hasattr (obj , c .SQLMESH_MACRO ):
292+ # We only need to add the undecorated code of @macro() functions in env, which
293+ # is accessible through the `__wrapped__` attribute added by functools.wraps
294+ obj = obj .__wrapped__
295+ elif callable (obj ) and not isinstance (obj , SERIALIZABLE_CALLABLES ):
296+ obj = getattr (obj , "__wrapped__" , None )
297+ name = getattr (obj , "__name__" , "" )
298+
299+ # Callable class instances shouldn't be serialized (e.g. tenacity.Retrying).
300+ # We still want to walk the callables they decorate, though
301+ if not isinstance (obj , SERIALIZABLE_CALLABLES ) or name in env :
302+ return
303+
304+ if (
305+ not obj_module
306+ or not hasattr (obj_module , "__file__" )
307+ or not _is_relative_to (obj_module .__file__ , path )
308+ ):
309+ env [name ] = obj
310+ return
311+ elif env [name ] != obj :
312+ raise SQLMeshError (
313+ f"Cannot store { obj } in environment, duplicate definitions found for '{ name } '"
314+ )
281315
282- obj_module = inspect .getmodule (obj )
283-
284- if obj_module and obj_module .__name__ == "builtins" :
285- return
286-
287- def walk (obj : t .Any ) -> None :
288316 if inspect .isclass (obj ):
289317 for var in decorator_vars (obj ):
290318 if obj_module and var in obj_module .__dict__ :
291- build_env (
292- obj_module .__dict__ [var ],
293- env = env ,
294- name = var ,
295- path = path ,
296- )
319+ walk (obj_module .__dict__ [var ], var )
297320
298321 for base in obj .__bases__ :
299- build_env (base , env = env , name = base .__qualname__ , path = path )
322+ walk (base , base .__qualname__ )
300323
301324 for k , v in obj .__dict__ .items ():
302325 if k .startswith ("__" ):
303326 continue
304- # traverse methods in a class to find global references
327+
328+ # Traverse methods in a class to find global references
305329 if isinstance (v , (classmethod , staticmethod )):
306330 v = v .__func__
331+
307332 if callable (v ):
308- # if the method is a part of the object, walk it
309- # else it is a global function and we just store it
333+ # Walk the method if it's part of the object, else it's a global function and we just store it
310334 if v .__qualname__ .startswith (obj .__qualname__ ):
311- walk (v )
335+ for k , v in func_globals (v ).items ():
336+ walk (v , k )
312337 else :
313- build_env (v , env = env , name = v .__name__ , path = path )
338+ walk (v , v .__name__ )
314339 elif callable (obj ):
315340 for k , v in func_globals (obj ).items ():
316- build_env (v , env = env , name = k , path = path )
317-
318- if name not in env :
319- if hasattr (obj , c .SQLMESH_MACRO ):
320- # We only need to add the undecorated code of @macro() functions in env, which
321- # is accessible through the `__wrapped__` attribute added by functools.wraps
322- obj = obj .__wrapped__
323- elif callable (obj ) and not isinstance (obj , SERIALIZABLE_CALLABLES ):
324- obj = getattr (obj , "__wrapped__" , None )
325- name = getattr (obj , "__name__" , "" )
326-
327- # Callable class instances shouldn't be serialized (e.g. tenacity.Retrying).
328- # We still want to walk the callables they decorate, though
329- if not isinstance (obj , SERIALIZABLE_CALLABLES ) or name in env :
330- return
341+ walk (v , k )
331342
343+ # We store the object in the environment after its dependencies, because otherwise we
344+ # could crash at environment hydration time, since dicts are ordered and the top-level
345+ # objects would be loaded before their dependencies.
332346 env [name ] = obj
333- if (
334- obj_module
335- and hasattr (obj_module , "__file__" )
336- and _is_relative_to (obj_module .__file__ , path )
337- ):
338- walk (env [name ])
339- elif env [name ] != obj :
340- raise SQLMeshError (
341- f"Cannot store { obj } in environment, duplicate definitions found for '{ name } '"
342- )
347+
348+ walk (obj , name )
343349
344350
345351class ExecutableKind (str , Enum ):
0 commit comments