Skip to content
Draft
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
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-20.04
strategy:
matrix:
python-version: [3.5, 3.6, 3.7, 3.8, 3.9, '3.10', 3.11, 3.12-dev, pypy-3.6]
python-version: [3.8, 3.9, '3.10', 3.11, 3.12]

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"]'), matrix.python-version) }}
if: ${{ !contains(fromJson('["pypy-3.6", "3.11","3.12"]'), matrix.python-version) }}
# pypy < 3.8 very doesn't work
- name: Mypy testing (3.11)
run: |
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, show_caches=True))
Expand Down
Empty file added executing/_types.py
Empty file.
98 changes: 98 additions & 0 deletions executing/_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@

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))


# noinspection PyUnresolvedReferences
_get_instructions = dis.get_instructions
from dis import Instruction as _Instruction

class Instruction(_Instruction):
lineno = None # type: int

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
89 changes: 21 additions & 68 deletions executing/executing.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@
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
from ._exceptions import KnownIssue

if TYPE_CHECKING: # pragma: no cover
from asttokens import ASTTokens, ASTText
Expand All @@ -52,51 +53,11 @@

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
# type: (str, Sized) -> None
self.values=values
super(NotOneValueFound,self).__init__(msg)

Expand All @@ -107,11 +68,15 @@ def only(it):
# type: (Iterable[T]) -> T
if isinstance(it, Sized):
if len(it) != 1:
raise NotOneValueFound('Expected one value, found %s' % len(it))
raise NotOneValueFound('Expected one value, found %s' % len(it),it)
# noinspection PyTypeChecker
return list(it)[0]

lst = tuple(islice(it, 2))
if TESTING:
lst=tuple(it)
else:
lst = tuple(islice(it, 2))

if len(lst) == 0:
raise NotOneValueFound('Expected one value, found 0')
if len(lst) > 1:
Expand Down Expand Up @@ -582,11 +547,12 @@ 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
if sys.version_info[0] == 3 or 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 @@ -596,9 +562,10 @@ 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)
raise KnownIssue("can not map "+op_name)


with lock:
exprs = {
Expand All @@ -611,6 +578,7 @@ def __init__(self, frame, stmts, tree, lasti, source):
if statement_containing_node(node) == stmt
}


if ctx == ast.Store:
# No special bytecode tricks here.
# We can handle multiple assigned attributes with different names,
Expand Down Expand Up @@ -674,14 +642,12 @@ def get_original_clean_instructions(self):
# inserts JUMP_IF_NOT_DEBUG instructions in bytecode
# If they're not present in our compiled instructions,
# ignore them in the original bytecode
if not any(
if any(inst.opname == "JUMP_IF_NOT_DEBUG" for inst in result):
if not any(
inst.opname == "JUMP_IF_NOT_DEBUG"
for inst in self.compile_instructions()
):
result = [
inst for inst in result
if inst.opname != "JUMP_IF_NOT_DEBUG"
]
):
result = [inst for inst in result if inst.opname != "JUMP_IF_NOT_DEBUG"]

return result

Expand Down Expand Up @@ -1127,19 +1093,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: 1 addition & 4 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ classifiers =
License :: OSI Approved :: MIT License
Programming Language :: Python
Programming Language :: Python :: 3
Programming Language :: Python :: 3.5
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Expand All @@ -25,7 +22,7 @@ packages = executing
zip_safe = False
include_package_data = True
setup_requires = setuptools; setuptools_scm[toml]
python_requires = >=3.5
python_requires = >=3.8

[options.extras_require]
tests=
Expand Down
Loading