@@ -180,6 +180,9 @@ def __init__(self, context: ExpressionsContext | None = None) -> None:
180180 "StopIteration" : StopIteration ,
181181 "TimeoutError" : TimeoutError ,
182182 "RuntimeError" : RuntimeError ,
183+ "SyntaxError" : SyntaxError ,
184+ "IndentationError" : IndentationError ,
185+ "AttributeError" : AttributeError ,
183186 "SecurityError" : SecurityError ,
184187 }
185188
@@ -802,7 +805,7 @@ def visit_Try(self, node: ast.Try) -> None:
802805 handled = False
803806 for handler in node .handlers :
804807 if handler .type is None :
805- exc_class = Exception
808+ exc_class : t . Type [ BaseException ] = Exception
806809 else :
807810 exc_class = self ._get_exception_class (handler .type )
808811
@@ -827,51 +830,79 @@ def visit_Try(self, node: ast.Try) -> None:
827830 for stmt in node .finalbody :
828831 self .visit (stmt )
829832
830- def _get_exception_class (self , node : ast .expr ) -> t .Type [Exception ]:
831- if isinstance (node , ast .Name ):
832- class_name = node .id
833- if class_name in globals ()["__builtins__" ]:
834- exc_class = globals ()["__builtins__" ][class_name ]
835- if isinstance (exc_class , type ) and issubclass (
836- exc_class , Exception
837- ):
838- return exc_class
839- raise NameError (
840- f"Name '{ class_name } ' is not defined or is not an exception "
841- "class"
842- )
843- elif isinstance (node , ast .Attribute ):
844- value = self .visit (node .value )
845- attr = node .attr
846- if hasattr (value , attr ):
847- exc_class = getattr (value , attr )
848- if isinstance (exc_class , type ) and issubclass (
849- exc_class , Exception
850- ):
851- return exc_class
852- raise NameError (f"'{ attr } ' is not a valid exception class" )
853- else :
854- raise TypeError (
855- f"Invalid exception class specification: { ast .dump (node )} "
856- )
833+ def _get_exception_class (self , node : ast .expr ) -> t .Type [BaseException ]:
834+ """Resolve the AST node to an actual exception class object."""
835+ # Use self.visit to evaluate the expression node (which might be a Name
836+ # like 'ValueError' or user-defined classes like 'ValidationError', or
837+ # even dotted attributes). This way, if the user code declared:
838+ #
839+ # class ValidationError(InputError): pass
840+ #
841+ # then we have that class in self.local_env, and self.visit(node)
842+ # will retrieve it.
843+ value = self .visit (node )
844+
845+ # Make sure it's a subclass of BaseException (i.e. an Exception).
846+ if isinstance (value , type ) and issubclass (value , BaseException ):
847+ return value
848+
849+ # Otherwise, it's not a valid exception class.
850+ # If "value" is, for instance, a string or a function, we raise.
851+ # (If "value" doesn't have a __name__, make a fallback name.)
852+ value_name = getattr (value , "__name__" , repr (value ))
853+ raise NameError (
854+ f"Name '{ value_name } ' is not defined or is not an exception class"
855+ )
857856
858857 def visit_Raise (self , node : ast .Raise ) -> None :
858+ """
859+ Handle raise statements that preserve exception chaining.
860+
861+ - If node.exc is a call (e.g. TypeError("msg")), we create an instance.
862+ - If node.exc is already an exception instance, we just raise it as-is.
863+ - If node.cause is specified, we set that as the __cause__.
864+ """
859865 if node .exc is None :
860866 raise RuntimeError ("No active exception to re-raise" )
861867
862- exc = self .visit (node .exc )
863- if isinstance (exc , type ) and issubclass (exc , Exception ):
864- if node .cause :
865- cause = self .visit (node .cause )
866- raise exc from cause
867- raise exc ()
868- else :
869- if isinstance (exc , BaseException ):
870- raise exc
868+ # Evaluate the main 'exc'
869+ # Could be a class like ValueError, or an instance like ValueError()
870+ exc_obj = self .visit (node .exc )
871+
872+ # Evaluate the 'from' cause if present
873+ cause_obj = self .visit (node .cause ) if node .cause else None
874+
875+ # If exc_obj is already an exception instance
876+ if isinstance (exc_obj , BaseException ):
877+ if cause_obj is not None :
878+ if not isinstance (cause_obj , BaseException ):
879+ raise TypeError (
880+ "Expected an exception instance for 'from' cause, "
881+ f"got { type (cause_obj ).__name__ } "
882+ )
883+ # Raise existing exception instance with cause
884+ raise exc_obj from cause_obj
871885 else :
872- raise TypeError (
873- f"Expected an exception instance, got { type (exc ).__name__ } "
874- )
886+ raise exc_obj
887+
888+ # If exc_obj is a class (like ValueError) rather than an instance
889+ if isinstance (exc_obj , type ) and issubclass (exc_obj , BaseException ):
890+ new_exc = exc_obj () # Instantiate
891+ if cause_obj is not None :
892+ if not isinstance (cause_obj , BaseException ):
893+ raise TypeError (
894+ "Expected an exception instance for 'from' cause, "
895+ f"got { type (cause_obj ).__name__ } "
896+ )
897+ raise new_exc from cause_obj
898+ else :
899+ raise new_exc
900+
901+ # Otherwise it's not a valid exception type or instance
902+ raise TypeError (
903+ "Expected an exception instance or class, got "
904+ f"{ type (exc_obj ).__name__ } "
905+ )
875906
876907 def visit_With (self , node : ast .With ) -> None :
877908 """
@@ -1595,72 +1626,127 @@ def visit_Call(self, node: ast.Call) -> t.Any:
15951626 # Call the function
15961627 return self ._call_function (func , pos_args , kwargs )
15971628
1598- def visit_GeneratorExp (
1599- self , node : ast .GeneratorExp
1600- ) -> t .Generator [t .Any , None , None ]:
1601- """Handle generator expressions."""
1602- # Create new scope for the generator expression
1603- gen_scope = Scope ()
1604- self .scope_stack .append (gen_scope )
1629+ def visit_GeneratorExp (self , node : ast .GeneratorExp ) -> t .Any :
1630+ """
1631+ Handle generator expressions with closure semantics.
16051632
1606- # Copy the outer environment
1607- outer_env = self . local_env
1608- self . local_env = outer_env . copy ()
1633+ Capture the current local_env when the generator expression is created
1634+ so that references to local variables (like 'y=2') remain valid even
1635+ after returning from the enclosing function.
16091636
1610- try :
1637+ Example user code that requires closure:
1638+ def gen():
1639+ y = 2
1640+ return (x + y + i for i in range(3))
1641+
1642+ Without closure capturing, "y" would appear undefined once 'gen'
1643+ returns.
1644+ """
1645+ # Copy (snapshot) the current local environment for closure
1646+ closure_env = dict (self .local_env )
1647+
1648+ def _generator_expression_runner ():
1649+ """
1650+ A helper that reinstalls the 'closure_env' as 'self.local_env'
1651+ whenever we iterate the generator, mimicking how Python closures
1652+ keep references to their defining scopes.
1653+ """
1654+ prev_env = self .local_env
1655+ self .local_env = closure_env
1656+ try :
1657+ # Actually produce the items by calling the comprehension logic
1658+ yield from self ._evaluate_generator_exp (node )
1659+ finally :
1660+ self .local_env = prev_env
16111661
1612- def generator () -> t .Generator [t .Any , None , None ]:
1613- def process_generator (
1614- generators : list , index : int = 0
1615- ) -> t .Generator [t .Any , None , None ]:
1616- if index >= len (generators ):
1617- # Base case: all generators processed, yield element
1618- value = self .visit (node .elt )
1619- yield value
1620- return
1662+ return _generator_expression_runner ()
16211663
1622- generator = generators [index ]
1623- iter_obj = self .visit (generator .iter )
1664+ def _evaluate_generator_exp (
1665+ self , node : ast .GeneratorExp
1666+ ) -> t .Iterator [t .Any ]:
1667+ """
1668+ Evaluate an already-constructed generator expression node under the
1669+ current (already snapshotted) self.local_env.
1670+ """
16241671
1625- # Save the current environment before processing this
1626- # generator
1627- current_env = self .local_env .copy ()
1672+ def inner ():
1673+ # The logic below closely matches typical generator expression
1674+ # evaluation: we handle node.generators (the "for" clauses, possibly
1675+ # with if-filters) and node.elt (the yield expression).
1676+ generators = node .generators
1677+
1678+ # We can reuse your existing "process_generator" or whatever method
1679+ # you have. The snippet below shows a direct approach:
1680+ def process (gens , index = 0 ):
1681+ """
1682+ Recursively handle each comprehension generator in 'gens'.
1683+ Once we exhaust them, evaluate node.elt and yield it.
1684+ """
1685+ if index >= len (gens ):
1686+ # If no more loops, the expression is node.elt
1687+ yield self .visit (node .elt )
1688+ return
16281689
1629- for item in iter_obj :
1630- # Restore environment from before this generator's loop
1631- self .local_env = current_env .copy ()
1690+ gen = gens [index ]
1691+ iter_obj = self .visit (gen .iter )
16321692
1633- try :
1634- self ._handle_unpacking_target (
1635- generator .target , item
1636- )
1637- except (TypeError , ValueError , SyntaxError ):
1638- if isinstance (generator .target , ast .Name ):
1639- self ._set_name_value (generator .target .id , item )
1640- else :
1641- raise
1693+ try :
1694+ iterator = iter (iter_obj )
1695+ except TypeError as e :
1696+ # If not iterable
1697+ raise TypeError (
1698+ f"object is not iterable: { iter_obj } "
1699+ ) from e
1700+
1701+ for item in iterator :
1702+ # Assign the loop target
1703+ self ._assign_comprehension_target (gen .target , item )
1704+ # Check any if-conditions
1705+ if gen .ifs :
1706+ skip = False
1707+ for if_test in gen .ifs :
1708+ condition = self .visit (if_test )
1709+ if not condition :
1710+ skip = True
1711+ break
1712+ if skip :
1713+ continue
16421714
1643- # Check if conditions
1644- if all (
1645- self .visit (if_clause ) for if_clause in generator .ifs
1646- ):
1647- # Process next generator or yield result
1648- yield from process_generator (generators , index + 1 )
1715+ # Recurse to handle the next generator
1716+ yield from process (gens , index + 1 )
16491717
1650- # Update outer environment with any named expression
1651- # bindings
1652- for name , value in self .local_env .items ():
1653- if name not in current_env :
1654- outer_env [name ] = value
1718+ yield from process (generators )
16551719
1656- # Start processing generators recursively
1657- yield from process_generator (node .generators )
1720+ yield from inner ()
16581721
1659- return generator ()
1660- finally :
1661- # Restore the outer environment and pop the scope
1662- self .local_env = outer_env
1663- self .scope_stack .pop ()
1722+ def _assign_comprehension_target (
1723+ self , target : ast .expr , value : t .Any
1724+ ) -> None :
1725+ """
1726+ Assign 'value' to the target node within a comprehension. This can be
1727+ something like 'for i in range(5)', or 'for k, v in items'.
1728+ If it's just a Name node, we store in self.local_env. If it's a tuple
1729+ or list, we destructure.
1730+ """
1731+ if isinstance (target , ast .Name ):
1732+ # This is a simple Name like: for i in ...
1733+ self ._set_name_value (target .id , value )
1734+ elif isinstance (target , (ast .Tuple , ast .List )):
1735+ # Destructure the value into the sub-targets
1736+ if not isinstance (value , (tuple , list )):
1737+ raise TypeError (f"cannot unpack non-iterable value { value } " )
1738+ if len (value ) != len (target .elts ):
1739+ raise ValueError (
1740+ f"cannot unpack { len (value )} values into "
1741+ f"{ len (target .elts )} targets"
1742+ )
1743+ for t_elt , v_elt in zip (target .elts , value ):
1744+ self ._assign_comprehension_target (t_elt , v_elt )
1745+ else :
1746+ # For something more complex, handle similarly or raise an error
1747+ raise NotImplementedError (
1748+ f"unsupported comprehension target type: { type (target )} "
1749+ )
16641750
16651751 def visit_List (self , node : ast .List ) -> list :
16661752 return [self .visit (elt ) for elt in node .elts ]
0 commit comments