@@ -317,6 +317,41 @@ def infer_annotations(cls, sig: inspect.Signature) -> inspect.Signature:
317317 assert cls ._get_root_ordinal (inferred_sig ) == root_ordinal != set ()
318318 return inferred_sig
319319
320+ @classmethod
321+ def extract_operations (
322+ cls , value , _seen : set [int ] | None = None
323+ ) -> frozenset [Operation ]:
324+ """Computes the set of :class:`Operation` s appearing directly in ``value`` .
325+
326+ An :class:`Operation` counts when it appears as a value in a collection,
327+ including as the key of a mapping, which is why this cannot be written
328+ in terms of :func:`flatten` . It does not count when it is applied to
329+ arguments, since the resulting :class:`Term` is a use of the operation
330+ rather than a binding occurrence of it.
331+
332+ :param value: The value to traverse.
333+ :returns: The operations that could be bound by a parameter given ``value``.
334+ """
335+ _seen = set () if _seen is None else _seen
336+ if id (value ) in _seen :
337+ return frozenset ()
338+ _seen .add (id (value ))
339+
340+ if isinstance (value , Operation ):
341+ return frozenset ({value })
342+ elif isinstance (value , dict ):
343+ return frozenset ().union (
344+ frozenset (),
345+ * (cls .extract_operations (k , _seen ) for k in value .keys ()),
346+ * (cls .extract_operations (v , _seen ) for v in value .values ()),
347+ )
348+ elif isinstance (value , list | set | frozenset | tuple ):
349+ return frozenset ().union (
350+ frozenset (), * (cls .extract_operations (v , _seen ) for v in value )
351+ )
352+ else :
353+ return frozenset ()
354+
320355 def analyze (self , bound_sig : inspect .BoundArguments ) -> frozenset [Operation ]:
321356 """
322357 Computes a set of bound variables given a signature with bound arguments.
@@ -342,43 +377,19 @@ def analyze(self, bound_sig: inspect.BoundArguments) -> frozenset[Operation]:
342377 param_ordinal = self ._get_param_ordinal (param )
343378 if param_ordinal <= self .ordinal and not param_ordinal <= return_ordinal :
344379 param_value = bound_sig .arguments [name ]
345- param_bound_vars = set ()
346-
347- if self ._param_is_var (param ):
348- # Handle individual Operation parameters (existing behavior)
349- if param .kind is inspect .Parameter .VAR_POSITIONAL :
350- # pre-condition: all bound variables should be distinct
351- assert len (param_value ) == len (set (param_value ))
352- param_bound_vars = set (param_value )
353- elif param .kind is inspect .Parameter .VAR_KEYWORD :
354- # pre-condition: all bound variables should be distinct
355- assert len (param_value .values ()) == len (
356- set (param_value .values ())
357- )
358- param_bound_vars = set (param_value .values ())
359- else :
360- param_bound_vars = {param_value }
361- elif param_ordinal : # Only process if there's a Scoped annotation
362- # We can't use flatten here because we want to be able
363- # to see dict keys
364- def extract_operations (obj , _seen = None ):
365- if _seen is None :
366- _seen = set ()
367- obj_id = id (obj )
368- if obj_id in _seen :
369- return
370- _seen .add (obj_id )
371- if isinstance (obj , Operation ):
372- param_bound_vars .add (obj )
373- elif isinstance (obj , dict ):
374- for k , v in obj .items ():
375- extract_operations (k , _seen )
376- extract_operations (v , _seen )
377- elif isinstance (obj , list | set | tuple ):
378- for v in obj :
379- extract_operations (v , _seen )
380-
381- extract_operations (param_value )
380+ param_bound_vars : frozenset [Operation ] = (
381+ self .extract_operations (param_value )
382+ # only process if the parameter is an Operation or is Scoped
383+ if self ._param_is_var (param ) or param_ordinal
384+ else frozenset ()
385+ )
386+
387+ if self ._param_is_var (param ) and param .kind in (
388+ inspect .Parameter .VAR_POSITIONAL ,
389+ inspect .Parameter .VAR_KEYWORD ,
390+ ):
391+ # pre-condition: all bound variables should be distinct
392+ assert len (param_bound_vars ) == len (param_value )
382393
383394 # pre-condition: all bound variables should be distinct
384395 if param_bound_vars :
0 commit comments