Browse Source

Intermediate changes
commit_hash:4fd110744a809cc923058a0e9949e65d8fa64e4d

robot-piglet 5 months ago
parent
commit
7b52d3a3ff

+ 10 - 12
contrib/python/executing/.dist-info/METADATA

@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: executing
-Version: 2.0.1
+Version: 2.1.0
 Summary: Get the currently executing AST node of a frame, and other information
 Home-page: https://github.com/alexmojaki/executing
 Author: Alex Hall
@@ -9,25 +9,23 @@ License: MIT
 Classifier: License :: OSI Approved :: MIT License
 Classifier: Programming Language :: Python
 Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.5
-Classifier: Programming Language :: Python :: 3.6
-Classifier: Programming Language :: Python :: 3.7
 Classifier: Programming Language :: Python :: 3.8
 Classifier: Programming Language :: Python :: 3.9
 Classifier: Programming Language :: Python :: 3.10
 Classifier: Programming Language :: Python :: 3.11
 Classifier: Programming Language :: Python :: 3.12
-Requires-Python: >=3.5
+Classifier: Programming Language :: Python :: 3.13
+Requires-Python: >=3.8
 Description-Content-Type: text/markdown
 License-File: LICENSE.txt
 Provides-Extra: tests
-Requires-Dist: asttokens >=2.1.0 ; extra == 'tests'
-Requires-Dist: ipython ; extra == 'tests'
-Requires-Dist: pytest ; extra == 'tests'
-Requires-Dist: coverage ; extra == 'tests'
-Requires-Dist: coverage-enable-subprocess ; extra == 'tests'
-Requires-Dist: littleutils ; extra == 'tests'
-Requires-Dist: rich ; (python_version >= "3.11") and extra == 'tests'
+Requires-Dist: asttokens>=2.1.0; extra == "tests"
+Requires-Dist: ipython; extra == "tests"
+Requires-Dist: pytest; extra == "tests"
+Requires-Dist: coverage; extra == "tests"
+Requires-Dist: coverage-enable-subprocess; extra == "tests"
+Requires-Dist: littleutils; extra == "tests"
+Requires-Dist: rich; python_version >= "3.11" and extra == "tests"
 
 # executing
 

+ 121 - 9
contrib/python/executing/executing/_position_node_finder.py

@@ -72,7 +72,7 @@ def mangled_name(node: EnhancedAST) -> str:
 
 @lru_cache(128) # pragma: no mutate
 def get_instructions(code: CodeType) -> list[dis.Instruction]:
-    return list(dis.get_instructions(code, show_caches=True))
+    return list(dis.get_instructions(code))
 
 
 types_cmp_issue_fix = (
@@ -114,7 +114,7 @@ class PositionNodeFinder(object):
     """
 
     def __init__(self, frame: FrameType, stmts: Set[EnhancedAST], tree: ast.Module, lasti: int, source: Source):
-        self.bc_list = get_instructions(frame.f_code)
+        self.bc_dict={bc.offset:bc for bc in get_instructions(frame.f_code) }
 
         self.source = source
         self.decorator: Optional[EnhancedAST] = None
@@ -141,7 +141,7 @@ class PositionNodeFinder(object):
                 # we ignore here the start position and try to find the ast-node just by end position and expected node type
                 # This is save, because there can only be one attribute ending at a specific point in the source code.
                 typ = (ast.Attribute,)
-            elif self.opname(lasti) == "CALL":
+            elif self.opname(lasti) in ("CALL", "CALL_KW"):
                 # A CALL instruction can be a method call, in which case the lineno and col_offset gets changed by the compiler.
                 # Therefore we ignoring here this attributes and searchnig for a Call-node only by end_col_offset and end_lineno.
                 # This is save, because there can only be one method ending at a specific point in the source code.
@@ -156,13 +156,18 @@ class PositionNodeFinder(object):
                 typ=typ,
             )
 
-        self.known_issues(self.result, self.instruction(lasti))
+        instruction = self.instruction(lasti)
+        assert instruction is not None
+
+        self.result = self.fix_result(self.result, instruction)
+
+        self.known_issues(self.result, instruction)
 
         self.test_for_decorator(self.result, lasti)
 
         # verify
         if self.decorator is None:
-            self.verify(self.result, self.instruction(lasti))
+            self.verify(self.result, instruction)
         else: 
             assert_(self.decorator in self.result.decorator_list)
 
@@ -213,6 +218,32 @@ class PositionNodeFinder(object):
                 if sys.version_info < (3, 12):
                     index += 4
 
+    def fix_result(
+        self, node: EnhancedAST, instruction: dis.Instruction
+    ) -> EnhancedAST:
+        if (
+            sys.version_info >= (3, 12, 5)
+            and instruction.opname in ("GET_ITER", "FOR_ITER")
+            and isinstance(node.parent, ast.For)
+            and node is node.parent.iter
+        ):
+            # node positions have changed in 3.12.5
+            # https://github.com/python/cpython/issues/93691
+            # `for` calls __iter__ and __next__ during execution, the calling
+            # expression of these calls was the ast.For node since cpython 3.11 (see test_iter).
+            # cpython 3.12.5 changed this to the `iter` node of the loop, to make tracebacks easier to read.
+            # This keeps backward compatibility with older executing versions.
+
+            # there are also cases like:
+            #
+            # for a in iter(l): pass
+            #
+            # where `iter(l)` would be otherwise the resulting node for the `iter()` call and the __iter__ call of the for implementation.
+            # keeping the old behaviour makes it possible to distinguish both cases.
+
+            return node.parent
+        return node
+
     def known_issues(self, node: EnhancedAST, instruction: dis.Instruction) -> None:
         if instruction.opname in ("COMPARE_OP", "IS_OP", "CONTAINS_OP") and isinstance(
             node, types_cmp_issue
@@ -324,6 +355,35 @@ class PositionNodeFinder(object):
         ):
             raise KnownIssue("exception generation maps to condition")
 
+        if sys.version_info >= (3, 13):
+            if instruction.opname in (
+                "STORE_FAST_STORE_FAST",
+                "STORE_FAST_LOAD_FAST",
+                "LOAD_FAST_LOAD_FAST",
+            ):
+                raise KnownIssue(f"can not map {instruction.opname} to two ast nodes")
+
+            if instruction.opname == "LOAD_FAST" and instruction.argval == "__class__":
+                # example:
+                #   class T:
+                #       def a():
+                #           super()
+                #       some_node  # <- there is a LOAD_FAST for this node because we use super()
+
+                raise KnownIssue(
+                    f"loading of __class__ is accociated with a random node at the end of a class if you use super()"
+                )
+
+            if (
+                instruction.opname == "COMPARE_OP"
+                and isinstance(node, ast.UnaryOp)
+                and isinstance(node.operand,ast.Compare)
+                and isinstance(node.op, ast.Not)
+            ):
+                # work around for 
+                # https://github.com/python/cpython/issues/114671
+                self.result = node.operand
+
     @staticmethod
     def is_except_cleanup(inst: dis.Instruction, node: EnhancedAST) -> bool:
         if inst.opname not in (
@@ -703,6 +763,52 @@ class PositionNodeFinder(object):
             if node_match(ast.FormattedValue) and inst_match("FORMAT_VALUE"):
                 return
 
+        if sys.version_info >= (3, 13):
+
+            if inst_match("NOP"):
+                return
+
+            if inst_match("TO_BOOL") and node_match(ast.BoolOp):
+                return
+
+            if inst_match("CALL_KW") and node_match((ast.Call, ast.ClassDef)):
+                return
+
+            if inst_match("LOAD_FAST", argval=".type_params"):
+                return
+
+            if inst_match("LOAD_FAST", argval="__classdict__"):
+                return
+
+            if inst_match("LOAD_FAST") and node_match(
+                (
+                    ast.FunctionDef,
+                    ast.ClassDef,
+                    ast.TypeAlias,
+                    ast.TypeVar,
+                    ast.Lambda,
+                    ast.AsyncFunctionDef,
+                )
+            ):
+                # These are loads for closure variables.
+                # It is difficult to check that this is actually closure variable, see:
+                # https://github.com/alexmojaki/executing/pull/80#discussion_r1716027317
+                return
+
+            if (
+                inst_match("LOAD_FAST")
+                and node_match(ast.TypeAlias)
+                and node.name.id == instruction.argval
+            ):
+                return
+
+            if inst_match("STORE_NAME",argval="__static_attributes__"):
+                # the node is the first node in the body
+                return
+
+            if inst_match("LOAD_FAST") and isinstance(node.parent,ast.TypeVar):
+                return
+
 
         # old verifier
 
@@ -771,11 +877,14 @@ class PositionNodeFinder(object):
 
         raise VerifierFailure(title, node, instruction)
 
-    def instruction(self, index: int) -> dis.Instruction:
-        return self.bc_list[index // 2]
+    def instruction(self, index: int) -> Optional[dis.Instruction]:
+        return self.bc_dict.get(index,None)
 
     def opname(self, index: int) -> str:
-        return self.instruction(index).opname
+        i=self.instruction(index)
+        if i is None:
+            return "CACHE"
+        return i.opname
 
     extra_node_types=()
     if sys.version_info >= (3,12):
@@ -798,7 +907,10 @@ class PositionNodeFinder(object):
             *extra_node_types,
         ),
     ) -> EnhancedAST:
-        position = self.instruction(index).positions
+        instruction = self.instruction(index)
+        assert instruction is not None
+
+        position = instruction.positions
         assert position is not None and position.lineno is not None
 
         return only(

+ 5 - 6
contrib/python/executing/executing/executing.py

@@ -273,16 +273,15 @@ class Source(object):
                             node_finder = NodeFinder(frame, stmts, tree, lasti, source)
                             node = node_finder.result
                             decorator = node_finder.decorator
+
+                    if node:
+                        new_stmts = {statement_containing_node(node)}
+                        assert_(new_stmts <= stmts)
+                        stmts = new_stmts
                 except Exception:
                     if TESTING:
                         raise
 
-                assert stmts is not None
-                if node:
-                    new_stmts = {statement_containing_node(node)}
-                    assert_(new_stmts <= stmts)
-                    stmts = new_stmts
-
             executing_cache[key] = args = source, node, stmts, decorator
 
         return Executing(frame, *args)

+ 1 - 1
contrib/python/executing/executing/version.py

@@ -1 +1 @@
-__version__ = '2.0.1'
+__version__ = '2.1.0'

+ 1 - 1
contrib/python/executing/ya.make

@@ -2,7 +2,7 @@
 
 PY3_LIBRARY()
 
-VERSION(2.0.1)
+VERSION(2.1.0)
 
 LICENSE(MIT)