Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:

jobs:
test:
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
strategy:
matrix:
python-version: [3.8, 3.9, '3.10', 3.11, 3.12-dev,3.13-dev]
Expand Down
46 changes: 1 addition & 45 deletions executing/_position_node_finder.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
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

Expand All @@ -25,51 +26,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))
Expand Down
139 changes: 139 additions & 0 deletions executing/_utils.py
Original file line number Diff line number Diff line change
@@ -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
64 changes: 6 additions & 58 deletions executing/executing.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@
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
Expand All @@ -52,48 +52,8 @@

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
Expand Down Expand Up @@ -581,11 +541,11 @@ def __init__(self, frame, stmts, tree, lasti, source):
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
Expand All @@ -595,10 +555,11 @@ def __init__(self, frame, stmts, tree, lasti, source):
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)
Expand Down Expand Up @@ -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]
Expand Down
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,7 @@ warn_redundant_casts=true

[[tool.mypy.overrides]]
module = "astroid"
ignore_missing_imports = true
ignore_missing_imports = true

[tool.pytest.ini_options]
python_functions = "test_"
14 changes: 5 additions & 9 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from collections import defaultdict, namedtuple
from random import shuffle
import pytest
from executing._utils import mangled_name

sys.path.append(os.path.dirname(os.path.dirname(__file__)))

Expand Down Expand Up @@ -715,11 +716,7 @@ def sample_files(samples):
@pytest.mark.skipif(sys.version_info<(3,),reason="no 2.7 support")
def test_small_samples(full_filename, result_filename):
skip_sentinel = [
"load_deref",
"4851dc1b626a95e97dbe0c53f96099d165b755dd1bd552c6ca771f7bca6d30f5",
"508ccd0dcac13ecee6f0cea939b73ba5319c780ddbb6c496be96fe5614871d4a",
"fc6eb521024986baa84af2634f638e40af090be4aa70ab3c22f3d022e8068228",
"42a37b8a823eb2e510b967332661afd679c82c60b7177b992a47c16d81117c8a",
"206e0609ff0589a0a32422ee902f09156af91746e27157c32c9595d12072f92a",
]

Expand Down Expand Up @@ -1077,6 +1074,7 @@ def p(*args):
p()

p("ast node:")
p(mangled_name(node))
p(ast_dump(node, indent=4))

parents = []
Expand Down Expand Up @@ -1429,11 +1427,9 @@ def check_code(self, code, nodes, decorators, check_names):

raise

# `argval` isn't set for all relevant instructions in python 2
# The relation between `ast.Name` and `argval` is already
# covered by the verifier and much more complex in python 3.11
if isinstance(node, ast.Name) and not py11:
assert inst.argval == node.id, (inst, ast.dump(node))
if isinstance(node, ast.Name) and inst.opname != "CALL_INTRINSIC_1" and inst.argval not in ("__classdict__","__classdictcell__","__static_attributes__"):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this worth explaining?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deleting it caused no problem and I can't remember why this was needed. Maybe it is just not needed any more.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like this reply was meant to be on the other comment?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, sorry. I wrote it on my phone.

# CALL_INTRINSIC_1 and some special names are excuded here because they are generated by cpython for some synthetic code
assert mangled_name(node) == inst.argval

if ex.decorator:
decorators[(node.lineno, node.name)].append(ex.decorator)
Expand Down
Loading