mirror of https://github.com/swig/swig.git
194 lines
7.0 KiB
Python
194 lines
7.0 KiB
Python
import ast
|
|
import contextlib
|
|
import inspect
|
|
import os
|
|
import random
|
|
import string
|
|
import sys
|
|
|
|
|
|
def swig_assert(condition, msg="Assertion failed"):
|
|
"""Variant of `assert` that is not disabled by the -O flag"""
|
|
if not condition:
|
|
raise AssertionError(msg)
|
|
|
|
def swig_check(lhs, rhs):
|
|
"""More user friendly assert function which shows values in an AssertionError if they are not equal"""
|
|
if not lhs == rhs:
|
|
raise AssertionError("'{lhs}' != '{rhs}', repr(lhs)={lr}, repr(rhs)={rr}".format(lhs=lhs, rhs=rhs, lr=repr(lhs), rr=repr(rhs)))
|
|
|
|
|
|
def swig_annotations_in_stub():
|
|
"""Return whether generated annotations are stored in a .pyi stub."""
|
|
return "-pyi" in os.environ.get("SWIG_FEATURES", "").split()
|
|
|
|
|
|
def _swig_stub_annotation_text(annotation):
|
|
"""Return the text represented by a generated stub annotation AST node."""
|
|
value = getattr(annotation, "value", None)
|
|
if isinstance(value, str):
|
|
return value
|
|
value = getattr(annotation, "s", None)
|
|
if isinstance(value, str):
|
|
return value
|
|
if isinstance(annotation, ast.Name):
|
|
return annotation.id
|
|
raise RuntimeError("unexpected annotation in generated stub: {}".format(ast.dump(annotation)))
|
|
|
|
|
|
def _swig_stub_lookup(tree, names):
|
|
"""Find the AST declaration for a list of nested names, or None if absent."""
|
|
node = tree
|
|
for name in names:
|
|
matches = [
|
|
child
|
|
for child in node.body
|
|
if isinstance(child, (ast.ClassDef, ast.FunctionDef)) and child.name == name
|
|
]
|
|
if not matches:
|
|
return None
|
|
node = matches[-1]
|
|
return node
|
|
|
|
|
|
def _swig_split_flat_name(tree, names):
|
|
"""Split a flat -fastproxy method name, Class_method, into its stub class and method names."""
|
|
for child in tree.body:
|
|
if isinstance(child, ast.ClassDef) and names[0].startswith(child.name + "_"):
|
|
return [child.name, names[0][len(child.name) + 1:]] + names[1:]
|
|
return names
|
|
|
|
|
|
def _swig_stub_node(tree, qualified_name, fastproxy):
|
|
"""Find the AST declaration for a qualified name in a generated stub."""
|
|
names = qualified_name.split(".")
|
|
if fastproxy:
|
|
# With -fastproxy a method is the flat C wrapper function, so its
|
|
# __qualname__ is Class_method rather than Class.method. Module level
|
|
# functions keep their plain name, so only split names not found as is.
|
|
if _swig_stub_lookup(tree, names) is None:
|
|
names = _swig_split_flat_name(tree, names)
|
|
node = _swig_stub_lookup(tree, names)
|
|
if node is None:
|
|
raise RuntimeError("{} is missing from generated stub".format(qualified_name))
|
|
return node
|
|
|
|
|
|
def swig_get_annotations(obj, module_name, fastproxy=False):
|
|
"""Return the annotations for a generated Python object.
|
|
|
|
When SWIG_FEATURES contains -pyi, read the annotations from the generated
|
|
``module_name + ".pyi"`` stub. Otherwise, read annotations attached to the
|
|
runtime object, using inspect.get_annotations() when available and falling
|
|
back to __annotations__ on older Python versions.
|
|
|
|
Args:
|
|
obj: Generated module, class, or function to inspect.
|
|
module_name: Generated module name used to locate its .pyi file.
|
|
fastproxy: True when the module was generated with -fastproxy, as the
|
|
proxy methods are then named after the flat C wrapper functions
|
|
rather than after the methods in the stub.
|
|
|
|
Returns:
|
|
A dictionary mapping annotated names to their annotation values.
|
|
"""
|
|
if not swig_annotations_in_stub():
|
|
if hasattr(inspect, "get_annotations"):
|
|
return inspect.get_annotations(obj) # python 3.10 added inspect.get_annotations
|
|
return getattr(obj, "__annotations__", {}) # python 3.14 removed __annotations__
|
|
|
|
with open(module_name + ".pyi") as stub_file:
|
|
tree = ast.parse(stub_file.read(), filename=stub_file.name)
|
|
|
|
node = tree if inspect.ismodule(obj) else _swig_stub_node(tree, obj.__qualname__, fastproxy)
|
|
annotations = {}
|
|
for child in node.body:
|
|
if isinstance(child, ast.AnnAssign) and isinstance(child.target, ast.Name):
|
|
value = getattr(child.annotation, "value", None)
|
|
is_string = isinstance(value, str)
|
|
if child.annotation.__class__.__name__ == "Str":
|
|
is_string = isinstance(getattr(child.annotation, "s", None), str)
|
|
if is_string:
|
|
annotations[child.target.id] = _swig_stub_annotation_text(child.annotation)
|
|
|
|
if isinstance(node, ast.FunctionDef):
|
|
arguments = getattr(node.args, "posonlyargs", []) + node.args.args + node.args.kwonlyargs
|
|
if node.args.vararg:
|
|
arguments.append(node.args.vararg)
|
|
if node.args.kwarg:
|
|
arguments.append(node.args.kwarg)
|
|
for argument in arguments:
|
|
if argument.annotation:
|
|
annotations[argument.arg] = _swig_stub_annotation_text(argument.annotation)
|
|
if node.returns:
|
|
annotations["return"] = _swig_stub_annotation_text(node.returns)
|
|
|
|
return annotations
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def swig_assert_raises(exc_cls):
|
|
"""Reimplementation of pytest.raises context manager."""
|
|
try:
|
|
yield
|
|
except exc_cls:
|
|
pass
|
|
else:
|
|
raise AssertionError("{} was not raised".format(exc_cls.__name__))
|
|
|
|
|
|
def swig_random_string(length=None):
|
|
if length is None:
|
|
length = random.randint(0, 16)
|
|
domain = string.ascii_letters + string.digits
|
|
if sys.version_info < (3, 6):
|
|
chars = [random.choice(domain) for _ in range(length)]
|
|
else:
|
|
chars = random.choices(domain, k=length)
|
|
return "".join(chars)
|
|
|
|
|
|
def swig_run_threaded(
|
|
func,
|
|
max_workers=8,
|
|
pass_count=False,
|
|
pass_barrier=False,
|
|
outer_iterations=1,
|
|
prepare_args=None,
|
|
):
|
|
"""Runs a function many times in parallel.
|
|
Copied from numpy/testing/_private/utils.py::run_threaded
|
|
"""
|
|
import concurrent.futures
|
|
import threading
|
|
|
|
for _ in range(outer_iterations):
|
|
executor = concurrent.futures.ThreadPoolExecutor(max_workers=max_workers)
|
|
with executor as tpe:
|
|
if prepare_args is None:
|
|
args = []
|
|
else:
|
|
args = prepare_args()
|
|
if pass_barrier:
|
|
barrier = threading.Barrier(max_workers)
|
|
args.append(barrier)
|
|
if pass_count:
|
|
all_args = [[func, i] + args for i in range(max_workers)]
|
|
else:
|
|
all_args = [[func] + args for _ in range(max_workers)]
|
|
try:
|
|
futures = []
|
|
for arg in all_args:
|
|
futures.append(tpe.submit(*arg))
|
|
except RuntimeError as e:
|
|
print(
|
|
"Spawning {} threads failed with "
|
|
"error {} (likely due to resource limits on the "
|
|
"system running the tests)".format(max_workers, e)
|
|
)
|
|
finally:
|
|
if len(futures) < max_workers and pass_barrier:
|
|
barrier.abort()
|
|
for f in futures:
|
|
f.result()
|