diff options
author | robot-piglet <[email protected]> | 2025-09-16 14:42:04 +0300 |
---|---|---|
committer | robot-piglet <[email protected]> | 2025-09-16 14:54:34 +0300 |
commit | 30351fcdf2948b8ea9afe9476a1b6aa2b56384d7 (patch) | |
tree | cd45037f6bcca203afed5d4759e3c034dcb40a90 /contrib | |
parent | 91172f9b69f179f6afba6485d257f3f026bffaa6 (diff) |
Intermediate changes
commit_hash:b6c37a893bfe6bb817a0e38d120edcc594c55985
Diffstat (limited to 'contrib')
-rw-r--r-- | contrib/python/executing/.dist-info/METADATA | 3 | ||||
-rw-r--r-- | contrib/python/executing/executing/_position_node_finder.py | 167 | ||||
-rw-r--r-- | contrib/python/executing/executing/_utils.py | 139 | ||||
-rw-r--r-- | contrib/python/executing/executing/executing.py | 64 | ||||
-rw-r--r-- | contrib/python/executing/executing/version.py | 2 | ||||
-rw-r--r-- | contrib/python/executing/ya.make | 3 |
6 files changed, 260 insertions, 118 deletions
diff --git a/contrib/python/executing/.dist-info/METADATA b/contrib/python/executing/.dist-info/METADATA index 6e4ac6516d3..65953e6c8f7 100644 --- a/contrib/python/executing/.dist-info/METADATA +++ b/contrib/python/executing/.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: executing -Version: 2.2.0 +Version: 2.2.1 Summary: Get the currently executing AST node of a frame, and other information Home-page: https://github.com/alexmojaki/executing Author: Alex Hall @@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 Requires-Python: >=3.8 Description-Content-Type: text/markdown License-File: LICENSE.txt diff --git a/contrib/python/executing/executing/_position_node_finder.py b/contrib/python/executing/executing/_position_node_finder.py index 0f8344106f2..27028d26ec9 100644 --- a/contrib/python/executing/executing/_position_node_finder.py +++ b/contrib/python/executing/executing/_position_node_finder.py @@ -5,8 +5,10 @@ from types import CodeType, FrameType from typing import Any, Callable, Iterator, Optional, Sequence, Set, Tuple, Type, Union, cast from .executing import EnhancedAST, NotOneValueFound, Source, only, function_node_types, assert_ from ._exceptions import KnownIssue, VerifierFailure +from ._utils import mangled_name from functools import lru_cache +import itertools # the code in this module can use all python>=3.11 features @@ -25,51 +27,6 @@ def node_and_parents(node: EnhancedAST) -> Iterator[EnhancedAST]: yield from parents(node) -def mangled_name(node: EnhancedAST) -> str: - """ - - Parameters: - node: the node which should be mangled - name: the name of the node - - Returns: - The mangled name of `node` - """ - if isinstance(node, ast.Attribute): - name = node.attr - elif isinstance(node, ast.Name): - name = node.id - elif isinstance(node, (ast.alias)): - name = node.asname or node.name.split(".")[0] - elif isinstance(node, (ast.FunctionDef, ast.ClassDef, ast.AsyncFunctionDef)): - name = node.name - elif isinstance(node, ast.ExceptHandler): - assert node.name - name = node.name - elif sys.version_info >= (3,12) and isinstance(node,ast.TypeVar): - name=node.name - else: - raise TypeError("no node to mangle for type "+repr(type(node))) - - if name.startswith("__") and not name.endswith("__"): - - parent,child=node.parent,node - - while not (isinstance(parent,ast.ClassDef) and child not in parent.bases): - if not hasattr(parent,"parent"): - break # pragma: no mutate - - parent,child=parent.parent,parent - else: - class_name=parent.name.lstrip("_") - if class_name!="": - return "_" + class_name + name - - - - return name - - @lru_cache(128) # pragma: no mutate def get_instructions(code: CodeType) -> list[dis.Instruction]: return list(dis.get_instructions(code)) @@ -115,6 +72,7 @@ class PositionNodeFinder(object): def __init__(self, frame: FrameType, stmts: Set[EnhancedAST], tree: ast.Module, lasti: int, source: Source): self.bc_dict={bc.offset:bc for bc in get_instructions(frame.f_code) } + self.frame=frame self.source = source self.decorator: Optional[EnhancedAST] = None @@ -302,6 +260,23 @@ class PositionNodeFinder(object): # handle positions changes for __enter__ return node.parent.parent + if sys.version_info >= (3, 14) and instruction.opname == "CALL": + before = self.instruction_before(instruction) + if ( + before is not None + and before.opname == "LOAD_SPECIAL" + and before.argrepr in ("__enter__","__aenter__") + and before.positions == instruction.positions + and isinstance(node.parent, ast.withitem) + and node is node.parent.context_expr + ): + return node.parent.parent + + if sys.version_info >= (3, 14) and isinstance(node, ast.UnaryOp) and isinstance(node.op,ast.Not) and instruction.opname !="UNARY_NOT": + # fix for https://github.com/python/cpython/issues/137843 + return node.operand + + return node def known_issues(self, node: EnhancedAST, instruction: dis.Instruction) -> None: @@ -383,6 +358,7 @@ class PositionNodeFinder(object): if (instruction.opname, instruction.argval) in [ ("LOAD_DEREF", "__class__"), ("LOAD_FAST", first_arg), + ("LOAD_FAST_BORROW", first_arg), ("LOAD_DEREF", first_arg), ]: raise KnownIssue("super optimization") @@ -423,7 +399,7 @@ class PositionNodeFinder(object): ): raise KnownIssue(f"can not map {instruction.opname} to two ast nodes") - if instruction.opname == "LOAD_FAST" and instruction.argval == "__class__": + if instruction.opname in ("LOAD_FAST","LOAD_FAST_BORROW") and instruction.argval == "__class__": # example: # class T: # def a(): @@ -444,6 +420,60 @@ class PositionNodeFinder(object): # https://github.com/python/cpython/issues/114671 self.result = node.operand + if sys.version_info >= (3,14): + + + if header_length := self.annotation_header_size(): + + last_offset=list(self.bc_dict.keys())[-1] + if ( + not (header_length*2 < instruction.offset <last_offset-4) + ): + # https://github.com/python/cpython/issues/135700 + raise KnownIssue("synthetic opcodes in annotations are just bound to the first node") + + if self.frame.f_code.co_name=="__annotate__" and instruction.opname=="STORE_SUBSCR": + raise KnownIssue("synthetic code to store annotation") + + if self.frame.f_code.co_name=="__annotate__" and isinstance(node,ast.AnnAssign): + raise KnownIssue("some opcodes in the annotation are just bound specific nodes") + + if isinstance(node,(ast.TypeAlias)) and self.frame.f_code.co_name==node.name.id : + raise KnownIssue("some opcodes in the annotation are just bound TypeAlias") + + if instruction.opname == "STORE_NAME" and instruction.argrepr == "__annotate__": + raise KnownIssue("just a store of the annotation") + + if instruction.opname == "IS_OP" and isinstance(node,ast.Name): + raise KnownIssue("part of a check that a name like `all` is a builtin") + + + + def annotation_header_size(self)->int: + if sys.version_info >=(3,14): + header=[inst.opname for inst in itertools.islice(self.bc_dict.values(),8)] + + if len(header)==8: + if header[0] in ("COPY_FREE_VARS","MAKE_CELL"): + del header[0] + header_size=8 + else: + del header[7] + header_size=7 + + if header==[ + "RESUME", + "LOAD_FAST_BORROW", + "LOAD_SMALL_INT", + "COMPARE_OP", + "POP_JUMP_IF_FALSE", + "NOT_TAKEN", + "LOAD_COMMON_CONSTANT", + ]: + return header_size + + return 0 + @staticmethod def is_except_cleanup(inst: dis.Instruction, node: EnhancedAST) -> bool: if inst.opname not in ( @@ -551,7 +581,7 @@ class PositionNodeFinder(object): # call to context.__exit__ return - if inst_match(("CALL", "LOAD_FAST")) and node_match( + if inst_match(("CALL", "LOAD_FAST","LOAD_FAST_BORROW")) and node_match( (ast.ListComp, ast.GeneratorExp, ast.SetComp, ast.DictComp) ): # call to the generator function @@ -657,11 +687,12 @@ class PositionNodeFinder(object): if inst_match("COMPARE_OP", argval="==") and node_match(ast.MatchValue): return - if inst_match("BINARY_OP") and node_match( - ast.AugAssign, op=op_type_map[instruction.argrepr.removesuffix("=")] - ): - # a+=5 - return + if inst_match("BINARY_OP"): + arg=instruction.argrepr.removesuffix("=") + + if arg!="[]" and node_match( ast.AugAssign, op=op_type_map[arg]): + # a+=5 + return if node_match(ast.Attribute, ctx=ast.Del) and inst_match( "DELETE_ATTR", argval=mangled_name(node) @@ -693,11 +724,15 @@ class PositionNodeFinder(object): "LOAD_NAME", "LOAD_FAST", "LOAD_FAST_CHECK", + "LOAD_FAST_BORROW", "LOAD_GLOBAL", "LOAD_DEREF", "LOAD_FROM_DICT_OR_DEREF", + "LOAD_FAST_BORROW_LOAD_FAST_BORROW", ), - argval=mangled_name(node), + ) and ( + mangled_name(node) in instruction.argval if isinstance(instruction.argval,tuple) + else instruction.argval == mangled_name(node) ): return @@ -707,7 +742,7 @@ class PositionNodeFinder(object): return if node_match(ast.Constant) and inst_match( - "LOAD_CONST", argval=cast(ast.Constant, node).value + ("LOAD_CONST","LOAD_SMALL_INT"), argval=cast(ast.Constant, node).value ): return @@ -767,7 +802,7 @@ class PositionNodeFinder(object): if( inst_match("CALL_INTRINSIC_1", argrepr="INTRINSIC_TYPEALIAS") or inst_match( - ("STORE_NAME", "STORE_FAST", "STORE_DEREF"), argrepr=node.name.id + ("STORE_NAME", "STORE_FAST", "STORE_DEREF","STORE_GLOBAL"), argrepr=node.name.id ) or inst_match("CALL") ): @@ -807,6 +842,10 @@ class PositionNodeFinder(object): if inst_match("LOAD_FAST",argval=".kwdefaults"): return + if sys.version_info >= (3, 14): + if inst_match("LOAD_FAST_BORROW_LOAD_FAST_BORROW",argval=(".defaults",".kwdefaults")): + return + if inst_match("STORE_NAME", argval="__classdictcell__"): # this is a general thing return @@ -840,7 +879,7 @@ class PositionNodeFinder(object): if inst_match("LOAD_FAST", argval="__classdict__"): return - if inst_match("LOAD_FAST") and node_match( + if inst_match(("LOAD_FAST","LOAD_FAST_BORROW")) and node_match( ( ast.FunctionDef, ast.ClassDef, @@ -866,7 +905,21 @@ class PositionNodeFinder(object): # the node is the first node in the body return - if inst_match("LOAD_FAST") and isinstance(node.parent,ast.TypeVar): + if inst_match(("LOAD_FAST","LOAD_FAST_BORROW")) and isinstance(node.parent,ast.TypeVar): + return + + if inst_match("CALL_INTRINSIC_2",argrepr="INTRINSIC_SET_TYPEPARAM_DEFAULT") and node_match((ast.TypeVar,ast.ParamSpec,ast.TypeVarTuple)): + return + + if sys.version_info >= (3, 14): + if inst_match("BINARY_OP",argrepr="[]") and node_match(ast.Subscript): + return + if inst_match("LOAD_FAST_BORROW", argval="__classdict__"): + return + if inst_match(("STORE_NAME","LOAD_NAME"), argval="__conditional_annotations__"): + return + + if inst_match("LOAD_FAST_BORROW_LOAD_FAST_BORROW") and node_match(ast.Name) and node.id in instruction.argval: return diff --git a/contrib/python/executing/executing/_utils.py b/contrib/python/executing/executing/_utils.py new file mode 100644 index 00000000000..42511844eb1 --- /dev/null +++ b/contrib/python/executing/executing/_utils.py @@ -0,0 +1,139 @@ + +import ast +import sys +import dis +from typing import cast, Any,Iterator +import types + + + +def assert_(condition, message=""): + # type: (Any, str) -> None + """ + Like an assert statement, but unaffected by -O + :param condition: value that is expected to be truthy + :type message: Any + """ + if not condition: + raise AssertionError(str(message)) + + +if sys.version_info >= (3, 4): + # noinspection PyUnresolvedReferences + _get_instructions = dis.get_instructions + from dis import Instruction as _Instruction + + class Instruction(_Instruction): + lineno = None # type: int +else: + from collections import namedtuple + + class Instruction(namedtuple('Instruction', 'offset argval opname starts_line')): + lineno = None # type: int + + from dis import HAVE_ARGUMENT, EXTENDED_ARG, hasconst, opname, findlinestarts, hasname + + # Based on dis.disassemble from 2.7 + # Left as similar as possible for easy diff + + def _get_instructions(co): + # type: (types.CodeType) -> Iterator[Instruction] + code = co.co_code + linestarts = dict(findlinestarts(co)) + n = len(code) + i = 0 + extended_arg = 0 + while i < n: + offset = i + c = code[i] + op = ord(c) + lineno = linestarts.get(i) + argval = None + i = i + 1 + if op >= HAVE_ARGUMENT: + oparg = ord(code[i]) + ord(code[i + 1]) * 256 + extended_arg + extended_arg = 0 + i = i + 2 + if op == EXTENDED_ARG: + extended_arg = oparg * 65536 + + if op in hasconst: + argval = co.co_consts[oparg] + elif op in hasname: + argval = co.co_names[oparg] + elif opname[op] == 'LOAD_FAST': + argval = co.co_varnames[oparg] + yield Instruction(offset, argval, opname[op], lineno) + +def get_instructions(co): + # type: (types.CodeType) -> Iterator[EnhancedInstruction] + lineno = co.co_firstlineno + for inst in _get_instructions(co): + inst = cast(EnhancedInstruction, inst) + lineno = inst.starts_line or lineno + assert_(lineno) + inst.lineno = lineno + yield inst + + +# Type class used to expand out the definition of AST to include fields added by this library +# It's not actually used for anything other than type checking though! +class EnhancedAST(ast.AST): + parent = None # type: EnhancedAST + +# Type class used to expand out the definition of AST to include fields added by this library +# It's not actually used for anything other than type checking though! +class EnhancedInstruction(Instruction): + _copied = None # type: bool + + + + + +def mangled_name(node): + # type: (EnhancedAST) -> str + """ + + Parameters: + node: the node which should be mangled + name: the name of the node + + Returns: + The mangled name of `node` + """ + + function_class_types=(ast.FunctionDef, ast.ClassDef, ast.AsyncFunctionDef) + + if isinstance(node, ast.Attribute): + name = node.attr + elif isinstance(node, ast.Name): + name = node.id + elif isinstance(node, (ast.alias)): + name = node.asname or node.name.split(".")[0] + elif isinstance(node, function_class_types): + name = node.name + elif isinstance(node, ast.ExceptHandler): + assert node.name + name = node.name + elif sys.version_info >= (3,12) and isinstance(node,ast.TypeVar): + name=node.name + else: + raise TypeError("no node to mangle") + + if name.startswith("__") and not name.endswith("__"): + + parent,child=node.parent,node + + while not (isinstance(parent,ast.ClassDef) and child not in parent.bases): + if not hasattr(parent,"parent"): + break # pragma: no mutate + + parent,child=parent.parent,parent + else: + class_name=parent.name.lstrip("_") + if class_name!="" and child not in parent.decorator_list: + return "_" + class_name + name + + + + return name diff --git a/contrib/python/executing/executing/executing.py b/contrib/python/executing/executing/executing.py index 5cf117e18c5..dd1e1d79855 100644 --- a/contrib/python/executing/executing/executing.py +++ b/contrib/python/executing/executing/executing.py @@ -40,8 +40,8 @@ from operator import attrgetter from pathlib import Path from threading import RLock from tokenize import detect_encoding -from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, Iterator, List, Optional, Sequence, Set, Sized, Tuple, \ - Type, TypeVar, Union, cast +from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, Iterator, List, Optional, Sequence, Set, Sized, Tuple, Type, TypeVar, Union, cast +from ._utils import mangled_name,assert_, EnhancedAST,EnhancedInstruction,Instruction,get_instructions if TYPE_CHECKING: # pragma: no cover from asttokens import ASTTokens, ASTText @@ -52,48 +52,8 @@ function_node_types = (ast.FunctionDef, ast.AsyncFunctionDef) # type: Tuple[Type cache = lru_cache(maxsize=None) -# Type class used to expand out the definition of AST to include fields added by this library -# It's not actually used for anything other than type checking though! -class EnhancedAST(ast.AST): - parent = None # type: EnhancedAST - - -class Instruction(dis.Instruction): - lineno = None # type: int - - -# Type class used to expand out the definition of AST to include fields added by this library -# It's not actually used for anything other than type checking though! -class EnhancedInstruction(Instruction): - _copied = None # type: bool - - - -def assert_(condition, message=""): - # type: (Any, str) -> None - """ - Like an assert statement, but unaffected by -O - :param condition: value that is expected to be truthy - :type message: Any - """ - if not condition: - raise AssertionError(str(message)) - - -def get_instructions(co): - # type: (types.CodeType) -> Iterator[EnhancedInstruction] - lineno = co.co_firstlineno - for inst in dis.get_instructions(co): - inst = cast(EnhancedInstruction, inst) - lineno = inst.starts_line or lineno - assert_(lineno) - inst.lineno = lineno - yield inst - - TESTING = 0 - class NotOneValueFound(Exception): def __init__(self,msg,values=[]): # type: (str, Sequence) -> None @@ -581,11 +541,11 @@ class SentinelNodeFinder(object): elif op_name in ('LOAD_ATTR', 'LOAD_METHOD', 'LOOKUP_METHOD'): typ = ast.Attribute ctx = ast.Load - extra_filter = lambda e: attr_names_match(e.attr, instruction.argval) + extra_filter = lambda e:mangled_name(e) == instruction.argval elif op_name in ('LOAD_NAME', 'LOAD_GLOBAL', 'LOAD_FAST', 'LOAD_DEREF', 'LOAD_CLASSDEREF'): typ = ast.Name ctx = ast.Load - extra_filter = lambda e: e.id == instruction.argval + extra_filter = lambda e:mangled_name(e) == instruction.argval elif op_name in ('COMPARE_OP', 'IS_OP', 'CONTAINS_OP'): typ = ast.Compare extra_filter = lambda e: len(e.ops) == 1 @@ -595,10 +555,11 @@ class SentinelNodeFinder(object): elif op_name.startswith('STORE_ATTR'): ctx = ast.Store typ = ast.Attribute - extra_filter = lambda e: attr_names_match(e.attr, instruction.argval) + extra_filter = lambda e:mangled_name(e) == instruction.argval else: raise RuntimeError(op_name) + with lock: exprs = { cast(EnhancedAST, node) @@ -1126,19 +1087,6 @@ def find_node_ipython(frame, lasti, stmts, source): return decorator, node -def attr_names_match(attr, argval): - # type: (str, str) -> bool - """ - Checks that the user-visible attr (from ast) can correspond to - the argval in the bytecode, i.e. the real attribute fetched internally, - which may be mangled for private attributes. - """ - if attr == argval: - return True - if not attr.startswith("__"): - return False - return bool(re.match(r"^_\w+%s$" % attr, argval)) - def node_linenos(node): # type: (ast.AST) -> Iterator[int] diff --git a/contrib/python/executing/executing/version.py b/contrib/python/executing/executing/version.py index e212710ff7b..1ef59ad4406 100644 --- a/contrib/python/executing/executing/version.py +++ b/contrib/python/executing/executing/version.py @@ -1 +1 @@ -__version__ = '2.2.0'
\ No newline at end of file +__version__ = '2.2.1'
\ No newline at end of file diff --git a/contrib/python/executing/ya.make b/contrib/python/executing/ya.make index 32d111cc478..4cdb448b48d 100644 --- a/contrib/python/executing/ya.make +++ b/contrib/python/executing/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(2.2.0) +VERSION(2.2.1) LICENSE(MIT) @@ -14,6 +14,7 @@ PY_SRCS( executing/_exceptions.py executing/_position_node_finder.py executing/_pytest_utils.py + executing/_utils.py executing/executing.py executing/version.py ) |