crosshair-tool 0.0.99__cp312-cp312-macosx_10_13_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- _crosshair_tracers.cpython-312-darwin.so +0 -0
- crosshair/__init__.py +42 -0
- crosshair/__main__.py +8 -0
- crosshair/_mark_stacks.h +790 -0
- crosshair/_preliminaries_test.py +18 -0
- crosshair/_tracers.h +94 -0
- crosshair/_tracers_pycompat.h +522 -0
- crosshair/_tracers_test.py +138 -0
- crosshair/abcstring.py +245 -0
- crosshair/auditwall.py +190 -0
- crosshair/auditwall_test.py +77 -0
- crosshair/codeconfig.py +113 -0
- crosshair/codeconfig_test.py +117 -0
- crosshair/condition_parser.py +1237 -0
- crosshair/condition_parser_test.py +497 -0
- crosshair/conftest.py +30 -0
- crosshair/copyext.py +155 -0
- crosshair/copyext_test.py +84 -0
- crosshair/core.py +1763 -0
- crosshair/core_and_libs.py +149 -0
- crosshair/core_regestered_types_test.py +82 -0
- crosshair/core_test.py +1316 -0
- crosshair/diff_behavior.py +314 -0
- crosshair/diff_behavior_test.py +261 -0
- crosshair/dynamic_typing.py +346 -0
- crosshair/dynamic_typing_test.py +210 -0
- crosshair/enforce.py +282 -0
- crosshair/enforce_test.py +182 -0
- crosshair/examples/PEP316/__init__.py +1 -0
- crosshair/examples/PEP316/bugs_detected/__init__.py +0 -0
- crosshair/examples/PEP316/bugs_detected/getattr_magic.py +16 -0
- crosshair/examples/PEP316/bugs_detected/hash_consistent_with_equals.py +31 -0
- crosshair/examples/PEP316/bugs_detected/shopping_cart.py +24 -0
- crosshair/examples/PEP316/bugs_detected/showcase.py +39 -0
- crosshair/examples/PEP316/correct_code/__init__.py +0 -0
- crosshair/examples/PEP316/correct_code/arith.py +60 -0
- crosshair/examples/PEP316/correct_code/chess.py +77 -0
- crosshair/examples/PEP316/correct_code/nesting_inference.py +17 -0
- crosshair/examples/PEP316/correct_code/numpy_examples.py +132 -0
- crosshair/examples/PEP316/correct_code/rolling_average.py +35 -0
- crosshair/examples/PEP316/correct_code/showcase.py +104 -0
- crosshair/examples/__init__.py +0 -0
- crosshair/examples/check_examples_test.py +146 -0
- crosshair/examples/deal/__init__.py +1 -0
- crosshair/examples/icontract/__init__.py +1 -0
- crosshair/examples/icontract/bugs_detected/__init__.py +0 -0
- crosshair/examples/icontract/bugs_detected/showcase.py +41 -0
- crosshair/examples/icontract/bugs_detected/wrong_sign.py +8 -0
- crosshair/examples/icontract/correct_code/__init__.py +0 -0
- crosshair/examples/icontract/correct_code/arith.py +51 -0
- crosshair/examples/icontract/correct_code/showcase.py +94 -0
- crosshair/fnutil.py +391 -0
- crosshair/fnutil_test.py +75 -0
- crosshair/fuzz_core_test.py +516 -0
- crosshair/libimpl/__init__.py +0 -0
- crosshair/libimpl/arraylib.py +161 -0
- crosshair/libimpl/binascii_ch_test.py +30 -0
- crosshair/libimpl/binascii_test.py +67 -0
- crosshair/libimpl/binasciilib.py +150 -0
- crosshair/libimpl/bisectlib_test.py +23 -0
- crosshair/libimpl/builtinslib.py +5228 -0
- crosshair/libimpl/builtinslib_ch_test.py +1191 -0
- crosshair/libimpl/builtinslib_test.py +3735 -0
- crosshair/libimpl/codecslib.py +86 -0
- crosshair/libimpl/codecslib_test.py +86 -0
- crosshair/libimpl/collectionslib.py +264 -0
- crosshair/libimpl/collectionslib_ch_test.py +252 -0
- crosshair/libimpl/collectionslib_test.py +332 -0
- crosshair/libimpl/copylib.py +23 -0
- crosshair/libimpl/copylib_test.py +18 -0
- crosshair/libimpl/datetimelib.py +2559 -0
- crosshair/libimpl/datetimelib_ch_test.py +354 -0
- crosshair/libimpl/datetimelib_test.py +112 -0
- crosshair/libimpl/decimallib.py +5257 -0
- crosshair/libimpl/decimallib_ch_test.py +78 -0
- crosshair/libimpl/decimallib_test.py +76 -0
- crosshair/libimpl/encodings/__init__.py +23 -0
- crosshair/libimpl/encodings/_encutil.py +187 -0
- crosshair/libimpl/encodings/ascii.py +44 -0
- crosshair/libimpl/encodings/latin_1.py +40 -0
- crosshair/libimpl/encodings/utf_8.py +93 -0
- crosshair/libimpl/encodings_ch_test.py +83 -0
- crosshair/libimpl/fractionlib.py +16 -0
- crosshair/libimpl/fractionlib_test.py +80 -0
- crosshair/libimpl/functoolslib.py +34 -0
- crosshair/libimpl/functoolslib_test.py +56 -0
- crosshair/libimpl/hashliblib.py +30 -0
- crosshair/libimpl/hashliblib_test.py +18 -0
- crosshair/libimpl/heapqlib.py +47 -0
- crosshair/libimpl/heapqlib_test.py +21 -0
- crosshair/libimpl/importliblib.py +18 -0
- crosshair/libimpl/importliblib_test.py +38 -0
- crosshair/libimpl/iolib.py +216 -0
- crosshair/libimpl/iolib_ch_test.py +128 -0
- crosshair/libimpl/iolib_test.py +19 -0
- crosshair/libimpl/ipaddresslib.py +8 -0
- crosshair/libimpl/itertoolslib.py +44 -0
- crosshair/libimpl/itertoolslib_test.py +44 -0
- crosshair/libimpl/jsonlib.py +984 -0
- crosshair/libimpl/jsonlib_ch_test.py +42 -0
- crosshair/libimpl/jsonlib_test.py +51 -0
- crosshair/libimpl/mathlib.py +179 -0
- crosshair/libimpl/mathlib_ch_test.py +44 -0
- crosshair/libimpl/mathlib_test.py +67 -0
- crosshair/libimpl/oslib.py +7 -0
- crosshair/libimpl/pathliblib_test.py +10 -0
- crosshair/libimpl/randomlib.py +178 -0
- crosshair/libimpl/randomlib_test.py +120 -0
- crosshair/libimpl/relib.py +846 -0
- crosshair/libimpl/relib_ch_test.py +169 -0
- crosshair/libimpl/relib_test.py +493 -0
- crosshair/libimpl/timelib.py +72 -0
- crosshair/libimpl/timelib_test.py +82 -0
- crosshair/libimpl/typeslib.py +15 -0
- crosshair/libimpl/typeslib_test.py +36 -0
- crosshair/libimpl/unicodedatalib.py +75 -0
- crosshair/libimpl/unicodedatalib_test.py +42 -0
- crosshair/libimpl/urlliblib.py +23 -0
- crosshair/libimpl/urlliblib_test.py +19 -0
- crosshair/libimpl/weakreflib.py +13 -0
- crosshair/libimpl/weakreflib_test.py +69 -0
- crosshair/libimpl/zliblib.py +15 -0
- crosshair/libimpl/zliblib_test.py +13 -0
- crosshair/lsp_server.py +261 -0
- crosshair/lsp_server_test.py +30 -0
- crosshair/main.py +973 -0
- crosshair/main_test.py +543 -0
- crosshair/objectproxy.py +376 -0
- crosshair/objectproxy_test.py +41 -0
- crosshair/opcode_intercept.py +601 -0
- crosshair/opcode_intercept_test.py +304 -0
- crosshair/options.py +218 -0
- crosshair/options_test.py +10 -0
- crosshair/patch_equivalence_test.py +75 -0
- crosshair/path_cover.py +209 -0
- crosshair/path_cover_test.py +138 -0
- crosshair/path_search.py +161 -0
- crosshair/path_search_test.py +52 -0
- crosshair/pathing_oracle.py +271 -0
- crosshair/pathing_oracle_test.py +21 -0
- crosshair/pure_importer.py +27 -0
- crosshair/pure_importer_test.py +16 -0
- crosshair/py.typed +0 -0
- crosshair/register_contract.py +273 -0
- crosshair/register_contract_test.py +190 -0
- crosshair/simplestructs.py +1165 -0
- crosshair/simplestructs_test.py +283 -0
- crosshair/smtlib.py +24 -0
- crosshair/smtlib_test.py +14 -0
- crosshair/statespace.py +1199 -0
- crosshair/statespace_test.py +108 -0
- crosshair/stubs_parser.py +352 -0
- crosshair/stubs_parser_test.py +43 -0
- crosshair/test_util.py +329 -0
- crosshair/test_util_test.py +26 -0
- crosshair/tools/__init__.py +0 -0
- crosshair/tools/check_help_in_doc.py +264 -0
- crosshair/tools/check_init_and_setup_coincide.py +119 -0
- crosshair/tools/generate_demo_table.py +127 -0
- crosshair/tracers.py +544 -0
- crosshair/tracers_test.py +154 -0
- crosshair/type_repo.py +151 -0
- crosshair/unicode_categories.py +589 -0
- crosshair/unicode_categories_test.py +27 -0
- crosshair/util.py +741 -0
- crosshair/util_test.py +173 -0
- crosshair/watcher.py +307 -0
- crosshair/watcher_test.py +107 -0
- crosshair/z3util.py +76 -0
- crosshair/z3util_test.py +11 -0
- crosshair_tool-0.0.99.dist-info/METADATA +144 -0
- crosshair_tool-0.0.99.dist-info/RECORD +176 -0
- crosshair_tool-0.0.99.dist-info/WHEEL +6 -0
- crosshair_tool-0.0.99.dist-info/entry_points.txt +3 -0
- crosshair_tool-0.0.99.dist-info/licenses/LICENSE +93 -0
- crosshair_tool-0.0.99.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import z3 # type: ignore
|
|
5
|
+
|
|
6
|
+
from crosshair.core import Patched, proxy_for_type
|
|
7
|
+
from crosshair.statespace import (
|
|
8
|
+
HeapRef,
|
|
9
|
+
RootNode,
|
|
10
|
+
SimpleStateSpace,
|
|
11
|
+
SnapshotRef,
|
|
12
|
+
StateSpace,
|
|
13
|
+
StateSpaceContext,
|
|
14
|
+
model_value_to_python,
|
|
15
|
+
)
|
|
16
|
+
from crosshair.tracers import COMPOSITE_TRACER
|
|
17
|
+
from crosshair.util import UnknownSatisfiability
|
|
18
|
+
|
|
19
|
+
_HEAD_SNAPSHOT = SnapshotRef(-1)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_find_key_in_heap():
|
|
23
|
+
space = SimpleStateSpace()
|
|
24
|
+
listref = z3.Const("listref", HeapRef)
|
|
25
|
+
listval1 = space.find_key_in_heap(listref, list, lambda t: [], _HEAD_SNAPSHOT)
|
|
26
|
+
assert isinstance(listval1, list)
|
|
27
|
+
listval2 = space.find_key_in_heap(listref, list, lambda t: [], _HEAD_SNAPSHOT)
|
|
28
|
+
assert listval1 is listval2
|
|
29
|
+
dictref = z3.Const("dictref", HeapRef)
|
|
30
|
+
dictval = space.find_key_in_heap(dictref, dict, lambda t: {}, _HEAD_SNAPSHOT)
|
|
31
|
+
assert dictval is not listval1
|
|
32
|
+
assert isinstance(dictval, dict)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_timeout() -> None:
|
|
36
|
+
num_ints = 100
|
|
37
|
+
space = StateSpace(time.monotonic() + 60_000, 0.1, RootNode())
|
|
38
|
+
with pytest.raises(UnknownSatisfiability):
|
|
39
|
+
with Patched(), StateSpaceContext(space), COMPOSITE_TRACER:
|
|
40
|
+
ints = [proxy_for_type(int, f"i{i}") for i in range(num_ints)]
|
|
41
|
+
for i in range(num_ints - 2):
|
|
42
|
+
t0 = time.monotonic()
|
|
43
|
+
if ints[i] * ints[i + 1] == ints[i + 2]:
|
|
44
|
+
pass
|
|
45
|
+
ints[i + 1] += ints[i]
|
|
46
|
+
solve_time = time.monotonic() - t0
|
|
47
|
+
assert 0.05 < solve_time < 0.5
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_infinite_timeout() -> None:
|
|
51
|
+
space = StateSpace(time.monotonic() + 1000, float("+inf"), RootNode())
|
|
52
|
+
assert space.solver.check(True) == z3.sat
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_checkpoint() -> None:
|
|
56
|
+
space = SimpleStateSpace()
|
|
57
|
+
ref = z3.Const("ref", HeapRef)
|
|
58
|
+
|
|
59
|
+
def find_key(snapshot):
|
|
60
|
+
return space.find_key_in_heap(ref, list, lambda t: [], snapshot)
|
|
61
|
+
|
|
62
|
+
orig_snapshot = space.current_snapshot()
|
|
63
|
+
listval = find_key(_HEAD_SNAPSHOT)
|
|
64
|
+
space.checkpoint()
|
|
65
|
+
|
|
66
|
+
head_listval = find_key(_HEAD_SNAPSHOT)
|
|
67
|
+
head_listval.append(42)
|
|
68
|
+
assert len(head_listval) == 1
|
|
69
|
+
assert listval is not head_listval
|
|
70
|
+
assert len(listval) == 0
|
|
71
|
+
|
|
72
|
+
listval_again = find_key(orig_snapshot)
|
|
73
|
+
assert listval_again is listval
|
|
74
|
+
head_listval_again = find_key(_HEAD_SNAPSHOT)
|
|
75
|
+
assert head_listval_again is head_listval
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_model_value_to_python_AlgebraicNumRef():
|
|
79
|
+
# Tests that z3.AlgebraicNumRef is handled properly.
|
|
80
|
+
# See https://github.com/pschanely/CrossHair/issues/242
|
|
81
|
+
rt2 = z3.simplify(z3.Sqrt(2))
|
|
82
|
+
assert type(rt2) == z3.AlgebraicNumRef
|
|
83
|
+
model_value_to_python(rt2)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_model_value_to_python_ArithRef():
|
|
87
|
+
# Tests that a plain z3.ArithRef can be exported as Python
|
|
88
|
+
# See https://github.com/pschanely/CrossHair/issues/381
|
|
89
|
+
rt2 = z3.ToInt(2 ** z3.Int("x"))
|
|
90
|
+
print("type(rt2)", type(rt2))
|
|
91
|
+
assert type(rt2) == z3.ArithRef
|
|
92
|
+
model_value_to_python(rt2)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_smt_fanout(space: SimpleStateSpace):
|
|
96
|
+
option1 = z3.Bool("option1")
|
|
97
|
+
option2 = z3.Bool("option2")
|
|
98
|
+
space.add(z3.Xor(option1, option2)) # Ensure exactly one option can be set
|
|
99
|
+
exprs_and_results = [(option1, "result1"), (option2, "result2")]
|
|
100
|
+
|
|
101
|
+
result = space.smt_fanout(exprs_and_results, desc="choose_one")
|
|
102
|
+
assert result in ("result1", "result2")
|
|
103
|
+
if result == "result1":
|
|
104
|
+
assert space.is_possible(option1)
|
|
105
|
+
assert not space.is_possible(option2)
|
|
106
|
+
else:
|
|
107
|
+
assert not space.is_possible(option1)
|
|
108
|
+
assert space.is_possible(option2)
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
from collections.abc import __all__ as abc_all
|
|
7
|
+
from importlib import import_module
|
|
8
|
+
from inspect import Parameter, Signature, signature
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from types import ClassMethodDescriptorType, MethodDescriptorType, WrapperDescriptorType
|
|
11
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, TypeVar, Union
|
|
12
|
+
from typing import __all__ as typing_all # type: ignore
|
|
13
|
+
|
|
14
|
+
from typeshed_client import get_stub_ast # type: ignore
|
|
15
|
+
from typeshed_client import get_search_context, get_stub_file
|
|
16
|
+
|
|
17
|
+
from crosshair.fnutil import resolve_signature
|
|
18
|
+
from crosshair.util import debug
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def signature_from_stubs(fn: Callable) -> Tuple[List[Signature], bool]:
|
|
22
|
+
"""
|
|
23
|
+
Try to find signature(s) for the given function in the stubs.
|
|
24
|
+
|
|
25
|
+
For overloaded functions, all signatures found will be returned.
|
|
26
|
+
|
|
27
|
+
:param fn: The function to lookup a signature for.
|
|
28
|
+
:return: A list containing the signature(s) found, if any and a validity boolean.\
|
|
29
|
+
If the boolean is False, signatures returned might be incomplete (some error\
|
|
30
|
+
occured while parsing).
|
|
31
|
+
"""
|
|
32
|
+
# ast.get_source_segment requires Python 3.8
|
|
33
|
+
if sys.version_info < (3, 8):
|
|
34
|
+
return [], True
|
|
35
|
+
if getattr(fn, "__module__", None) and getattr(fn, "__qualname__", None):
|
|
36
|
+
module_name = fn.__module__
|
|
37
|
+
else:
|
|
38
|
+
# Some builtins and some C functions are wrapped into Descriptors.
|
|
39
|
+
if isinstance(
|
|
40
|
+
fn, (MethodDescriptorType, WrapperDescriptorType, ClassMethodDescriptorType)
|
|
41
|
+
) and getattr(fn, "__qualname__", None):
|
|
42
|
+
module_name = fn.__objclass__.__module__
|
|
43
|
+
else:
|
|
44
|
+
# Builtins classmethods have their module available only via __self__.
|
|
45
|
+
fn_self = getattr(fn, "__self__", None)
|
|
46
|
+
if isinstance(fn_self, type):
|
|
47
|
+
module_name = fn_self.__module__
|
|
48
|
+
else:
|
|
49
|
+
return [], True
|
|
50
|
+
|
|
51
|
+
# Use the `qualname` to find the function inside its module.
|
|
52
|
+
path_in_module: List[str] = fn.__qualname__.split(".")
|
|
53
|
+
# Find the stub_file and corresponding AST using `typeshed_client`.
|
|
54
|
+
search_path = [Path(path) for path in sys.path if path]
|
|
55
|
+
search_context = get_search_context(search_path=search_path)
|
|
56
|
+
stub_file = get_stub_file(module_name, search_context=search_context)
|
|
57
|
+
module = get_stub_ast(module_name, search_context=search_context)
|
|
58
|
+
if not stub_file or not module or not isinstance(module, ast.Module):
|
|
59
|
+
debug("No stub found for module", module_name)
|
|
60
|
+
return [], True
|
|
61
|
+
glo = globals().copy()
|
|
62
|
+
return _sig_from_ast(module.body, path_in_module, stub_file.read_text(), glo)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _get_source_segment(source: str, node: ast.AST) -> Optional[str]:
|
|
66
|
+
"""Get source code segment of the *source* that generated *node*."""
|
|
67
|
+
if sys.version_info >= (3, 8):
|
|
68
|
+
return ast.get_source_segment(source, node)
|
|
69
|
+
raise NotImplementedError("ast.get_source_segment not available for python < 3.8.")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _sig_from_ast(
|
|
73
|
+
stmts: List[ast.stmt],
|
|
74
|
+
next_steps: List[str],
|
|
75
|
+
stub_text: str,
|
|
76
|
+
glo: Dict[str, Any],
|
|
77
|
+
) -> Tuple[List[Signature], bool]:
|
|
78
|
+
"""Lookup in the given ast for a function signature, following `next_steps` path."""
|
|
79
|
+
if len(next_steps) == 0:
|
|
80
|
+
return [], True
|
|
81
|
+
|
|
82
|
+
# First walk through the nodes to execute imports and assignments
|
|
83
|
+
for node in stmts:
|
|
84
|
+
# If we encounter an import statement, add it to the namespace
|
|
85
|
+
if isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
86
|
+
_exec_import(node, glo)
|
|
87
|
+
|
|
88
|
+
# If we encounter the definition of a `TypeVar`, add it to the namespace
|
|
89
|
+
elif isinstance(node, ast.Assign):
|
|
90
|
+
value_text = _get_source_segment(stub_text, node.value)
|
|
91
|
+
if value_text and "TypeVar" in value_text:
|
|
92
|
+
assign_text = _get_source_segment(stub_text, node)
|
|
93
|
+
if assign_text:
|
|
94
|
+
try:
|
|
95
|
+
exec(assign_text, glo)
|
|
96
|
+
except Exception:
|
|
97
|
+
debug("Not able to evaluate TypeVar assignment:", assign_text)
|
|
98
|
+
|
|
99
|
+
# Walk through the nodes to find the next node
|
|
100
|
+
next_node_name = next_steps[0]
|
|
101
|
+
sigs = []
|
|
102
|
+
is_valid = True
|
|
103
|
+
for node in stmts:
|
|
104
|
+
# Only one step remaining => look for the function itself
|
|
105
|
+
if (
|
|
106
|
+
len(next_steps) == 1
|
|
107
|
+
and isinstance(node, ast.FunctionDef)
|
|
108
|
+
and node.name == next_node_name
|
|
109
|
+
):
|
|
110
|
+
sig, valid = _sig_from_functiondef(node, stub_text, glo)
|
|
111
|
+
if sig:
|
|
112
|
+
sigs.append(sig)
|
|
113
|
+
is_valid = is_valid and valid
|
|
114
|
+
|
|
115
|
+
# More than one step remaining => look for the next step
|
|
116
|
+
elif (
|
|
117
|
+
isinstance(node, (ast.Module, ast.ClassDef, ast.FunctionDef))
|
|
118
|
+
and node.name == next_node_name
|
|
119
|
+
):
|
|
120
|
+
new_sigs, valid = _sig_from_ast(node.body, next_steps[1:], stub_text, glo)
|
|
121
|
+
sigs.extend(new_sigs)
|
|
122
|
+
is_valid = is_valid and valid
|
|
123
|
+
|
|
124
|
+
# Additionally, we might need to look for the next node into if statements
|
|
125
|
+
for node in stmts:
|
|
126
|
+
if isinstance(node, (ast.If)):
|
|
127
|
+
assign_text = _get_source_segment(stub_text, node.test)
|
|
128
|
+
# Some function depends on the execution environment
|
|
129
|
+
if assign_text and "sys." in assign_text:
|
|
130
|
+
condition = None
|
|
131
|
+
try:
|
|
132
|
+
condition = eval(assign_text, glo)
|
|
133
|
+
except Exception:
|
|
134
|
+
debug("Not able to evaluate condition:", assign_text)
|
|
135
|
+
if condition is not None:
|
|
136
|
+
new_sigs, valid = _sig_from_ast(
|
|
137
|
+
node.body if condition else node.orelse,
|
|
138
|
+
next_steps,
|
|
139
|
+
stub_text,
|
|
140
|
+
glo,
|
|
141
|
+
)
|
|
142
|
+
sigs.extend(new_sigs)
|
|
143
|
+
is_valid = is_valid and valid
|
|
144
|
+
|
|
145
|
+
return sigs, is_valid
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _exec_import(imp: Union[ast.Import, ast.ImportFrom], glo: Dict[str, Any]):
|
|
149
|
+
"""Try to execute the import statement and add it to the `glo` namespace."""
|
|
150
|
+
if isinstance(imp, ast.Import):
|
|
151
|
+
for n in imp.names:
|
|
152
|
+
name = n.name
|
|
153
|
+
asname = n.asname or name
|
|
154
|
+
if name != "_typeshed":
|
|
155
|
+
try:
|
|
156
|
+
glo[asname] = import_module(name)
|
|
157
|
+
except Exception:
|
|
158
|
+
debug("Not able to import", name)
|
|
159
|
+
|
|
160
|
+
elif isinstance(imp, ast.ImportFrom):
|
|
161
|
+
# Replace imports from `_typeshed` by their equivalent
|
|
162
|
+
if imp.module == "_typeshed":
|
|
163
|
+
for n in imp.names:
|
|
164
|
+
name = n.name
|
|
165
|
+
asname = n.asname or name
|
|
166
|
+
if name in _REPLACE_TYPESHED:
|
|
167
|
+
new_module, replace = _REPLACE_TYPESHED[name]
|
|
168
|
+
glo[asname] = getattr(import_module(new_module), replace)
|
|
169
|
+
elif name == "Self":
|
|
170
|
+
Self = TypeVar("Self")
|
|
171
|
+
glo["Self"] = Self
|
|
172
|
+
elif imp.module:
|
|
173
|
+
try:
|
|
174
|
+
module = import_module(imp.module)
|
|
175
|
+
except Exception:
|
|
176
|
+
debug("Not able to import", imp.module)
|
|
177
|
+
return
|
|
178
|
+
for n in imp.names:
|
|
179
|
+
name = n.name
|
|
180
|
+
asname = n.asname or name
|
|
181
|
+
try:
|
|
182
|
+
glo[asname] = getattr(module, name)
|
|
183
|
+
except Exception:
|
|
184
|
+
debug("Not able to import", name, "from", imp.module)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# Replace _typeshed imports by their closest equivalent
|
|
188
|
+
_collection_module = "typing" if sys.version_info < (3, 9) else "collections.abc"
|
|
189
|
+
_REPLACE_TYPESHED: Dict[str, Tuple[str, str]] = {
|
|
190
|
+
"SupportsLenAndGetItem": (_collection_module, "Collection"),
|
|
191
|
+
"SupportsNext": (_collection_module, "Iterator"),
|
|
192
|
+
"SupportsAnext": (_collection_module, "AsyncIterator"),
|
|
193
|
+
# One might wish to add more if needed, but exact equivalents do not exist.
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _sig_from_functiondef(
|
|
198
|
+
fn_def: ast.FunctionDef, stub_text: str, glo: Dict[str, Any]
|
|
199
|
+
) -> Tuple[Optional[Signature], bool]:
|
|
200
|
+
"""Given an ast FunctionDef, return the corresponding signature."""
|
|
201
|
+
# Get the source text for the function stub and parse the signature from it.
|
|
202
|
+
function_text = _get_source_segment(stub_text, fn_def)
|
|
203
|
+
if function_text:
|
|
204
|
+
exec(function_text, glo)
|
|
205
|
+
sig_or_error = resolve_signature(glo[fn_def.name])
|
|
206
|
+
if isinstance(sig_or_error, str):
|
|
207
|
+
try:
|
|
208
|
+
sig_or_error = signature(glo[fn_def.name])
|
|
209
|
+
except Exception:
|
|
210
|
+
debug("Not able to perform function evaluation:", function_text)
|
|
211
|
+
return None, False
|
|
212
|
+
parsed_sig, valid = _parse_sig(sig_or_error, glo)
|
|
213
|
+
# If the function is @classmethod, remove cls from the signature.
|
|
214
|
+
for decorator in fn_def.decorator_list:
|
|
215
|
+
if isinstance(decorator, ast.Name) and decorator.id == "classmethod":
|
|
216
|
+
oldparams = list(parsed_sig.parameters.values())
|
|
217
|
+
newparams = oldparams[1:]
|
|
218
|
+
slf = "Self"
|
|
219
|
+
if (
|
|
220
|
+
slf in glo
|
|
221
|
+
and oldparams[0].annotation == Type[glo[slf]]
|
|
222
|
+
and parsed_sig.return_annotation == glo[slf]
|
|
223
|
+
):
|
|
224
|
+
# We don't support return type "Self" in classmethods.
|
|
225
|
+
return (
|
|
226
|
+
parsed_sig.replace(
|
|
227
|
+
parameters=newparams,
|
|
228
|
+
return_annotation=Parameter.empty,
|
|
229
|
+
),
|
|
230
|
+
False,
|
|
231
|
+
)
|
|
232
|
+
return parsed_sig.replace(parameters=newparams), valid
|
|
233
|
+
return parsed_sig, valid
|
|
234
|
+
return None, False
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _parse_sig(sig: Signature, glo: Dict[str, Any]) -> Tuple[Signature, bool]:
|
|
238
|
+
"""
|
|
239
|
+
Signature annotations are escaped into strings.
|
|
240
|
+
|
|
241
|
+
This is due to `from __future__ import annotations`.
|
|
242
|
+
"""
|
|
243
|
+
is_valid = True
|
|
244
|
+
ret_type, valid = _parse_annotation(sig.return_annotation, glo)
|
|
245
|
+
is_valid = is_valid and valid
|
|
246
|
+
params: List[Parameter] = []
|
|
247
|
+
for param in sig.parameters.values():
|
|
248
|
+
annot, valid = _parse_annotation(param.annotation, glo)
|
|
249
|
+
params.append(param.replace(annotation=annot))
|
|
250
|
+
is_valid = is_valid and valid
|
|
251
|
+
return sig.replace(parameters=params, return_annotation=ret_type), is_valid
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _parse_annotation(annotation: Any, glo: Dict[str, Any]) -> Tuple[Any, bool]:
|
|
255
|
+
if isinstance(annotation, str):
|
|
256
|
+
if sys.version_info < (3, 10):
|
|
257
|
+
annotation = _rewrite_with_union(annotation)
|
|
258
|
+
if sys.version_info < (3, 9):
|
|
259
|
+
annotation = _rewrite_with_typing_types(annotation, glo)
|
|
260
|
+
try:
|
|
261
|
+
return eval(annotation, glo), True
|
|
262
|
+
except Exception as e:
|
|
263
|
+
debug("Not able to parse annotation:", annotation, "Error:", e)
|
|
264
|
+
return Parameter.empty, False
|
|
265
|
+
return annotation, True
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _rewrite_with_union(s: str) -> str:
|
|
269
|
+
"""
|
|
270
|
+
Undo PEP 604 to be compliant with Python < 3.10.
|
|
271
|
+
|
|
272
|
+
For example `Dict[str | int]` will become `Dict[Union[str, int]]`
|
|
273
|
+
|
|
274
|
+
Main idea of the algorithm:
|
|
275
|
+
- Walk through the string and remember each opening parenthesis or bracket (push the
|
|
276
|
+
current state to the saved states).
|
|
277
|
+
- Uppon closing a parenthesis or bracket, if a `|` was found since the opening
|
|
278
|
+
parenthesis, surround with `Union[]` and replace `|` by `,`. Then pop the state
|
|
279
|
+
from the saved states.
|
|
280
|
+
Note: the given string is assumed to have a valid syntax.
|
|
281
|
+
"""
|
|
282
|
+
s_new = s # The new string being built
|
|
283
|
+
saved_states: List[Tuple[int, bool]] = [] # Stack of saved states
|
|
284
|
+
start: int = 0 # Index (in s_new) where Union would begin
|
|
285
|
+
found: bool = False # True if a `|` was found since `start`
|
|
286
|
+
idx: int = 0 # Current index in `s_new`
|
|
287
|
+
|
|
288
|
+
for char in s:
|
|
289
|
+
if char == "|":
|
|
290
|
+
found = True
|
|
291
|
+
|
|
292
|
+
# Closing the current scope. Surround with `Union[]` if a `|` was found.
|
|
293
|
+
if char == ")" or char == "]" or char == ",":
|
|
294
|
+
if found:
|
|
295
|
+
s_new = (
|
|
296
|
+
s_new[: start + 1]
|
|
297
|
+
+ "Union["
|
|
298
|
+
+ s_new[start + 1 : idx].replace("|", ",")
|
|
299
|
+
+ "]"
|
|
300
|
+
+ s_new[idx:]
|
|
301
|
+
)
|
|
302
|
+
idx += len("Union[]")
|
|
303
|
+
if char != ",":
|
|
304
|
+
start, found = saved_states.pop() # Restore previous scope.
|
|
305
|
+
|
|
306
|
+
# Opening a new scope.
|
|
307
|
+
if char == "(" or char == "[" or char == ",":
|
|
308
|
+
if char != ",":
|
|
309
|
+
saved_states.append((start, found)) # Save the current scope.
|
|
310
|
+
start = idx
|
|
311
|
+
found = False
|
|
312
|
+
idx += 1
|
|
313
|
+
|
|
314
|
+
if found:
|
|
315
|
+
s_new = "Union[" + s_new.replace("|", ",") + "]"
|
|
316
|
+
return s_new
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
_REPLACEMENTS_PEP_585: Dict[re.Pattern[str], str] = {}
|
|
320
|
+
"""Dictionnary of regexes and replacement strings to revert PEP 585."""
|
|
321
|
+
|
|
322
|
+
if sys.version_info < (3, 9):
|
|
323
|
+
# 1. Replace type subscription by types from typing
|
|
324
|
+
base = r"(?<![\.\w])"
|
|
325
|
+
for t in typing_all:
|
|
326
|
+
replacement = "typing." + t + "["
|
|
327
|
+
_REPLACEMENTS_PEP_585[re.compile(base + t.lower() + r"\[")] = replacement
|
|
328
|
+
|
|
329
|
+
# 2. Replace collections.abc by typing
|
|
330
|
+
# (?<![\.\w]) is to avoid match if the char before is alphanumerical or a dot
|
|
331
|
+
bases = [r"(?<![\.\w])collections\.abc\.", r"(?<![\.\w])"]
|
|
332
|
+
for t in set(typing_all).intersection(abc_all):
|
|
333
|
+
replacement = "typing." + t + "["
|
|
334
|
+
for base in bases:
|
|
335
|
+
_REPLACEMENTS_PEP_585[re.compile(base + t + r"\[")] = replacement
|
|
336
|
+
# Special case for `from collections.abc import Set as AbstractSet`
|
|
337
|
+
_REPLACEMENTS_PEP_585[re.compile(r"(?<![\.\w])AbstractSet\[")] = "typing.Set["
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _rewrite_with_typing_types(s: str, glo: Dict[str, Any]) -> str:
|
|
341
|
+
"""
|
|
342
|
+
Undo PEP 585 to be compliant with Python < 3.9.
|
|
343
|
+
|
|
344
|
+
For example `list[int]` will become `typing.List[int]` and types from
|
|
345
|
+
collections.abc will be replaced by those of typing.
|
|
346
|
+
"""
|
|
347
|
+
for regx, replace in _REPLACEMENTS_PEP_585.items():
|
|
348
|
+
s_new = regx.sub(replace, s)
|
|
349
|
+
if s != s_new and replace.startswith("typing.") and "typing" not in glo:
|
|
350
|
+
glo["typing"] = import_module("typing")
|
|
351
|
+
s = s_new
|
|
352
|
+
return s
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import sys
|
|
3
|
+
from random import Random
|
|
4
|
+
|
|
5
|
+
from crosshair.stubs_parser import (
|
|
6
|
+
_rewrite_with_typing_types,
|
|
7
|
+
_rewrite_with_union,
|
|
8
|
+
signature_from_stubs,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_rewrite_with_union():
|
|
13
|
+
test_str = "List[str | int] | Callable[int | str, int]"
|
|
14
|
+
expect = "Union[List[Union[str , int]] , Callable[Union[int , str], int]]"
|
|
15
|
+
assert expect == _rewrite_with_union(test_str)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
if sys.version_info < (3, 9):
|
|
19
|
+
|
|
20
|
+
def test_rewrite_with_typing_types():
|
|
21
|
+
test_str = "list[dict[int, list]]"
|
|
22
|
+
expect = "typing.List[typing.Dict[int, list]]"
|
|
23
|
+
glo = dict()
|
|
24
|
+
assert expect == _rewrite_with_typing_types(test_str, glo)
|
|
25
|
+
assert "typing" in glo
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_signature_from_stubs():
|
|
29
|
+
s, valid = signature_from_stubs(Random.randint)
|
|
30
|
+
if sys.version_info >= (3, 8):
|
|
31
|
+
assert valid and str(s[0]) == "(self, a: int, b: int) -> int"
|
|
32
|
+
s, valid = signature_from_stubs(Random.sample)
|
|
33
|
+
expect_re = re.compile(
|
|
34
|
+
r"""
|
|
35
|
+
\( self .*
|
|
36
|
+
population .* sequence .* _T .*
|
|
37
|
+
\) \s \- \> .* _T
|
|
38
|
+
""",
|
|
39
|
+
re.VERBOSE | re.IGNORECASE,
|
|
40
|
+
)
|
|
41
|
+
assert valid and expect_re.match(str(s[0]))
|
|
42
|
+
else:
|
|
43
|
+
assert not s
|