Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-24.04
strategy:
matrix:
python-version: [3.8, 3.9, '3.10', 3.11, 3.12-dev,3.13-dev]
python-version: [3.8, 3.9, '3.10', '3.11', '3.12' ,'3.13' ,3.14-dev]

steps:
- uses: actions/checkout@v2
Expand All @@ -31,7 +31,7 @@ jobs:
pip install mypy==0.910
python -m mypy executing --exclude=executing/_position_node_finder.py
# fromJson because https://github.community/t/passing-an-array-literal-to-contains-function-causes-syntax-error/17213/3
if: ${{ !contains(fromJson('["pypy-3.6", "3.11","3.12-dev","3.13-dev"]'), matrix.python-version) }}
if: ${{ !contains(fromJson('["pypy-3.6", "3.11","3.12","3.13","3.14-dev"]'), matrix.python-version) }}
# pypy < 3.8 very doesn't work
- name: Mypy testing (3.11)
run: |
Expand Down
121 changes: 109 additions & 12 deletions executing/_position_node_finder.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from ._utils import mangled_name

from functools import lru_cache
import itertools

# the code in this module can use all python>=3.11 features

Expand Down Expand Up @@ -71,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
Expand Down Expand Up @@ -258,6 +260,23 @@ def fix_result(
# 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:
Expand Down Expand Up @@ -339,6 +358,7 @@ def known_issues(self, node: EnhancedAST, instruction: dis.Instruction) -> None:
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")
Expand Down Expand Up @@ -379,7 +399,7 @@ def known_issues(self, node: EnhancedAST, instruction: dis.Instruction) -> None:
):
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():
Expand All @@ -400,6 +420,60 @@ def known_issues(self, node: EnhancedAST, instruction: dis.Instruction) -> None:
# 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 (
Expand Down Expand Up @@ -507,7 +581,7 @@ def node_match(node_type: Union[Type, Tuple[Type, ...]], **kwargs: Any) -> bool:
# 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
Expand Down Expand Up @@ -613,11 +687,12 @@ def node_match(node_type: Union[Type, Tuple[Type, ...]], **kwargs: Any) -> bool:
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)
Expand Down Expand Up @@ -649,11 +724,15 @@ def node_match(node_type: Union[Type, Tuple[Type, ...]], **kwargs: Any) -> bool:
"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

Expand All @@ -663,7 +742,7 @@ def node_match(node_type: Union[Type, Tuple[Type, ...]], **kwargs: Any) -> bool:
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

Expand Down Expand Up @@ -723,7 +802,7 @@ def node_match(node_type: Union[Type, Tuple[Type, ...]], **kwargs: Any) -> bool:
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")
):
Expand Down Expand Up @@ -763,6 +842,10 @@ def node_match(node_type: Union[Type, Tuple[Type, ...]], **kwargs: Any) -> bool:
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
Expand Down Expand Up @@ -796,7 +879,7 @@ def node_match(node_type: Union[Type, Tuple[Type, ...]], **kwargs: Any) -> bool:
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,
Expand All @@ -822,7 +905,21 @@ def node_match(node_type: Union[Type, Tuple[Type, ...]], **kwargs: Any) -> bool:
# 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


Expand Down
9 changes: 6 additions & 3 deletions tests/analyse.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ class Frame:

filename = pathlib.Path(sys.argv[1])

if ":" in sys.argv[2]:
if len(sys.argv)<3:
start=0
end=100
elif ":" in sys.argv[2]:
start, end = sys.argv[2].split(":")
start = int(start)
end = int(end)
Expand Down Expand Up @@ -110,8 +113,8 @@ def inspect(bc):
for i in dis.get_instructions(bc, show_caches=True):

if (
i.positions.lineno is not None
and i.positions.lineno <= end
i.positions.lineno is None
or i.positions.lineno <= end
and start <= i.positions.end_lineno
):
if first:
Expand Down
Loading