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
crosshair/util.py
ADDED
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
import builtins
|
|
2
|
+
import collections
|
|
3
|
+
import collections.abc
|
|
4
|
+
import contextlib
|
|
5
|
+
import functools
|
|
6
|
+
import importlib.util
|
|
7
|
+
import math
|
|
8
|
+
import os
|
|
9
|
+
import pathlib
|
|
10
|
+
import re
|
|
11
|
+
import sys
|
|
12
|
+
import threading
|
|
13
|
+
import traceback
|
|
14
|
+
import types
|
|
15
|
+
from array import array
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from enum import Enum
|
|
18
|
+
from inspect import (
|
|
19
|
+
BoundArguments,
|
|
20
|
+
Parameter,
|
|
21
|
+
getmodulename,
|
|
22
|
+
getsourcefile,
|
|
23
|
+
getsourcelines,
|
|
24
|
+
isdatadescriptor,
|
|
25
|
+
isfunction,
|
|
26
|
+
)
|
|
27
|
+
from time import monotonic
|
|
28
|
+
from types import BuiltinFunctionType, FunctionType, MethodDescriptorType, TracebackType
|
|
29
|
+
from typing import (
|
|
30
|
+
Any,
|
|
31
|
+
Callable,
|
|
32
|
+
Dict,
|
|
33
|
+
Generator,
|
|
34
|
+
Generic,
|
|
35
|
+
List,
|
|
36
|
+
Mapping,
|
|
37
|
+
MutableMapping,
|
|
38
|
+
Optional,
|
|
39
|
+
Sequence,
|
|
40
|
+
Set,
|
|
41
|
+
TextIO,
|
|
42
|
+
Tuple,
|
|
43
|
+
Type,
|
|
44
|
+
TypeVar,
|
|
45
|
+
Union,
|
|
46
|
+
cast,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
import typing_inspect # type: ignore
|
|
50
|
+
|
|
51
|
+
from crosshair.auditwall import opened_auditwall
|
|
52
|
+
from crosshair.tracers import COMPOSITE_TRACER, NoTracing, ResumedTracing, is_tracing
|
|
53
|
+
|
|
54
|
+
_DEBUG_STREAM: Optional[TextIO] = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# NOTE: many of these is_* functions should use a TypeGuard in 3.10 (or even TypeIs in 3.13)
|
|
58
|
+
|
|
59
|
+
if sys.version_info >= (3, 12):
|
|
60
|
+
from collections.abc import Buffer
|
|
61
|
+
|
|
62
|
+
def is_bytes_like(obj: object) -> bool:
|
|
63
|
+
return isinstance(obj, Buffer)
|
|
64
|
+
|
|
65
|
+
else:
|
|
66
|
+
from collections.abc import ByteString
|
|
67
|
+
|
|
68
|
+
def is_bytes_like(obj: object) -> bool:
|
|
69
|
+
return isinstance(obj, (ByteString, array))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def is_iterable(o: object) -> bool:
|
|
73
|
+
try:
|
|
74
|
+
iter(o) # type: ignore
|
|
75
|
+
return True
|
|
76
|
+
except TypeError:
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def is_hashable(o: object) -> bool:
|
|
81
|
+
return getattr(type(o), "__hash__", None) is not None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def is_pure_python(obj: object) -> bool:
|
|
85
|
+
if isinstance(obj, type):
|
|
86
|
+
return True if "__dict__" in dir(obj) else hasattr(obj, "__slots__")
|
|
87
|
+
elif callable(obj):
|
|
88
|
+
return isfunction(obj) # isfunction selects "user-defined" functions only
|
|
89
|
+
else:
|
|
90
|
+
return True
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def memo(f):
|
|
94
|
+
"""Decorate a function taking a single argument with a memoization decorator."""
|
|
95
|
+
saved = {}
|
|
96
|
+
|
|
97
|
+
@functools.wraps(f)
|
|
98
|
+
def memo_wrapper(a):
|
|
99
|
+
if a not in saved:
|
|
100
|
+
saved[a] = f(a)
|
|
101
|
+
return saved[a]
|
|
102
|
+
|
|
103
|
+
return memo_wrapper
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# Valid smtlib identifier chars: ~ ! @ $ % ^ & * _ - + = < > . ? /
|
|
107
|
+
# See the section on "symbols" here:
|
|
108
|
+
# https://smtlib.cs.uiowa.edu/papers/smt-lib-reference-v2.6-r2017-07-18.pdf
|
|
109
|
+
_SMTLIB_TRANSLATION = str.maketrans("[],", "<>.", " ")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def smtlib_typename(typ: Type) -> str:
|
|
113
|
+
return name_of_type(typ).translate(_SMTLIB_TRANSLATION)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def name_of_type(typ: Type) -> str:
|
|
117
|
+
return typ.__name__ if hasattr(typ, "__name__") else str(typ).split(".")[-1]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def samefile(f1: Optional[str], f2: Optional[str]) -> bool:
|
|
121
|
+
try:
|
|
122
|
+
return f1 is not None and f2 is not None and os.path.samefile(f1, f2)
|
|
123
|
+
except FileNotFoundError:
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def true_type(obj: object) -> Type:
|
|
128
|
+
with NoTracing():
|
|
129
|
+
return type(obj)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
CROSSHAIR_EXTRA_ASSERTS = os.environ.get("CROSSHAIR_EXTRA_ASSERTS", "0") == "1"
|
|
133
|
+
|
|
134
|
+
if CROSSHAIR_EXTRA_ASSERTS:
|
|
135
|
+
|
|
136
|
+
def assert_tracing(should_be_tracing):
|
|
137
|
+
def decorator(fn):
|
|
138
|
+
fn_name = fn.__qualname__
|
|
139
|
+
|
|
140
|
+
@functools.wraps(fn)
|
|
141
|
+
def check_tracing(*a, **kw):
|
|
142
|
+
if is_tracing() != should_be_tracing:
|
|
143
|
+
with NoTracing():
|
|
144
|
+
if should_be_tracing:
|
|
145
|
+
raise CrossHairInternal(
|
|
146
|
+
f"should be tracing when calling {fn_name}, but isn't"
|
|
147
|
+
)
|
|
148
|
+
else:
|
|
149
|
+
raise CrossHairInternal(
|
|
150
|
+
f"should not be tracing when calling {fn_name}, but is"
|
|
151
|
+
)
|
|
152
|
+
return fn(*a, **kw)
|
|
153
|
+
|
|
154
|
+
return check_tracing
|
|
155
|
+
|
|
156
|
+
return decorator
|
|
157
|
+
|
|
158
|
+
else:
|
|
159
|
+
|
|
160
|
+
def assert_tracing(should_be_tracing):
|
|
161
|
+
def decorator(fn):
|
|
162
|
+
return fn
|
|
163
|
+
|
|
164
|
+
return decorator
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class IdKeyedDict(collections.abc.MutableMapping):
|
|
168
|
+
def __init__(self) -> None:
|
|
169
|
+
# Confusingly, we hold both the key object and value object in
|
|
170
|
+
# our inner dict. Holding the key object ensures that we don't
|
|
171
|
+
# GC the key object, which could lead to reusing the same id()
|
|
172
|
+
# for a different object.
|
|
173
|
+
self.inner: Dict[int, Tuple[object, object]] = {}
|
|
174
|
+
|
|
175
|
+
def __getitem__(self, k):
|
|
176
|
+
return self.inner.__getitem__(id(k))[1]
|
|
177
|
+
|
|
178
|
+
def __setitem__(self, k, v):
|
|
179
|
+
return self.inner.__setitem__(id(k), (k, v))
|
|
180
|
+
|
|
181
|
+
def __delitem__(self, k):
|
|
182
|
+
return self.inner.__delitem__(id(k))
|
|
183
|
+
|
|
184
|
+
def __iter__(self):
|
|
185
|
+
raise NotImplementedError
|
|
186
|
+
# No use cases for this yet, but we could do something like this:
|
|
187
|
+
# return (actual_key_object for actual_key_object, _ in self.inner.values())
|
|
188
|
+
|
|
189
|
+
def __len__(self):
|
|
190
|
+
return len(self.inner)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
_SOURCE_CACHE: MutableMapping[object, Tuple[str, int, Tuple[str, ...]]] = IdKeyedDict()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def sourcelines(thing: object) -> Tuple[str, int, Tuple[str, ...]]:
|
|
197
|
+
# If it's a bound method, pull the function out:
|
|
198
|
+
while hasattr(thing, "__func__"):
|
|
199
|
+
thing = thing.__func__ # type: ignore
|
|
200
|
+
# Unwrap decorators as necessary:
|
|
201
|
+
while hasattr(thing, "__wrapped__"):
|
|
202
|
+
thing = thing.__wrapped__ # type: ignore
|
|
203
|
+
filename, start_line, lines = "<unknown file>", 0, ()
|
|
204
|
+
ret = _SOURCE_CACHE.get(thing, None)
|
|
205
|
+
if ret is None:
|
|
206
|
+
try:
|
|
207
|
+
filename = getsourcefile(thing) # type: ignore
|
|
208
|
+
(lines, start_line) = getsourcelines(thing) # type: ignore
|
|
209
|
+
except (OSError, TypeError):
|
|
210
|
+
pass
|
|
211
|
+
ret = (filename, start_line, tuple(lines))
|
|
212
|
+
_SOURCE_CACHE[thing] = ret
|
|
213
|
+
return ret
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def frame_summary_for_fn(
|
|
217
|
+
fn: Callable, frames: traceback.StackSummary
|
|
218
|
+
) -> Tuple[Optional[str], int]:
|
|
219
|
+
fn_name = fn.__name__
|
|
220
|
+
try:
|
|
221
|
+
fn_file = getsourcefile(fn) # Can return None OR raise TypeError
|
|
222
|
+
except TypeError:
|
|
223
|
+
fn_file = None
|
|
224
|
+
if fn_file is None:
|
|
225
|
+
return (None, 0)
|
|
226
|
+
for frame in reversed(frames):
|
|
227
|
+
if frame.name == fn_name and samefile(frame.filename, fn_file):
|
|
228
|
+
return (frame.filename, frame.lineno or 1)
|
|
229
|
+
return sourcelines(fn)[:2]
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def set_debug(new_debug: bool, output: TextIO = sys.stderr):
|
|
233
|
+
global _DEBUG_STREAM
|
|
234
|
+
if new_debug:
|
|
235
|
+
_DEBUG_STREAM = output
|
|
236
|
+
else:
|
|
237
|
+
_DEBUG_STREAM = None
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def in_debug() -> bool:
|
|
241
|
+
return bool(_DEBUG_STREAM)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def debug(*a):
|
|
245
|
+
"""
|
|
246
|
+
Print debugging information in CrossHair's nested log output.
|
|
247
|
+
|
|
248
|
+
Arguments are serialized with ``str()`` and printed when running in CrossHair's
|
|
249
|
+
verbose mode.
|
|
250
|
+
|
|
251
|
+
Avoid passing symbolic values, as taking the string of a
|
|
252
|
+
symbolic will change the path exploration that CrossHair normally takes, leading to
|
|
253
|
+
different outcomes in verbose and non-verbose mode.
|
|
254
|
+
"""
|
|
255
|
+
if not _DEBUG_STREAM:
|
|
256
|
+
return
|
|
257
|
+
with NoTracing():
|
|
258
|
+
stack = traceback.extract_stack()
|
|
259
|
+
frame = stack[-2]
|
|
260
|
+
indent = len(stack) - 3
|
|
261
|
+
print(
|
|
262
|
+
"{:06.3f}|{}|{}() {}".format(
|
|
263
|
+
monotonic(), " " * indent, frame.name, " ".join(map(str, a))
|
|
264
|
+
),
|
|
265
|
+
file=_DEBUG_STREAM,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def warn(*a):
|
|
270
|
+
"""
|
|
271
|
+
Display a warning to the user.
|
|
272
|
+
|
|
273
|
+
It currently does not do more than printing `WARNING:`, followed by the arguments
|
|
274
|
+
serialized with `str` to the `stderr` stream.
|
|
275
|
+
"""
|
|
276
|
+
debug("WARNING:", *a)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
TracebackLike = Union[None, TracebackType, Sequence[traceback.FrameSummary]]
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def ch_stack(
|
|
283
|
+
tb: TracebackLike = None,
|
|
284
|
+
last_n_frames: int = sys.maxsize,
|
|
285
|
+
currently_handling: Optional[BaseException] = None,
|
|
286
|
+
) -> str:
|
|
287
|
+
with NoTracing():
|
|
288
|
+
if currently_handling:
|
|
289
|
+
if tb is not None:
|
|
290
|
+
raise CrossHairInternal
|
|
291
|
+
lower_frames = traceback.extract_tb(currently_handling.__traceback__)
|
|
292
|
+
higher_frames = traceback.extract_stack()[:-2]
|
|
293
|
+
frames: Sequence[traceback.FrameSummary] = higher_frames + lower_frames
|
|
294
|
+
elif tb is None:
|
|
295
|
+
frames = traceback.extract_stack()[:-1]
|
|
296
|
+
elif isinstance(tb, TracebackType):
|
|
297
|
+
frames = traceback.extract_tb(tb)
|
|
298
|
+
else:
|
|
299
|
+
frames = tb
|
|
300
|
+
output: List[str] = []
|
|
301
|
+
for frame in frames[-last_n_frames:]:
|
|
302
|
+
filename = os.path.split(frame.filename)[1]
|
|
303
|
+
output.append(f"({frame.name} {filename}:{frame.lineno})")
|
|
304
|
+
return " ".join(output)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class ErrorDuringImport(Exception):
|
|
308
|
+
pass
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
@contextlib.contextmanager
|
|
312
|
+
def add_to_pypath(*paths: Union[str, pathlib.Path]) -> Generator:
|
|
313
|
+
old_path = sys.path[:]
|
|
314
|
+
for path in paths:
|
|
315
|
+
sys.path.insert(0, str(path))
|
|
316
|
+
try:
|
|
317
|
+
yield
|
|
318
|
+
finally:
|
|
319
|
+
sys.path[:] = old_path
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
class _TypingAccessDetector:
|
|
323
|
+
accessed = False
|
|
324
|
+
|
|
325
|
+
def __bool__(self):
|
|
326
|
+
self.accessed = True
|
|
327
|
+
return False
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def import_module(module_name):
|
|
331
|
+
# Some packages like to write tmp files on import,
|
|
332
|
+
# e.g. https://github.com/pschanely/CrossHair/issues/172
|
|
333
|
+
with opened_auditwall():
|
|
334
|
+
orig_modules = set(sys.modules.values())
|
|
335
|
+
result_module = importlib.import_module(module_name)
|
|
336
|
+
|
|
337
|
+
return result_module
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def load_file(filename: str) -> types.ModuleType:
|
|
341
|
+
"""
|
|
342
|
+
Load a module from a file.
|
|
343
|
+
|
|
344
|
+
:raises ErrorDuringImport: if the file cannot be imported
|
|
345
|
+
"""
|
|
346
|
+
try:
|
|
347
|
+
root_path, module_name = extract_module_from_file(filename)
|
|
348
|
+
with add_to_pypath(root_path):
|
|
349
|
+
return import_module(module_name)
|
|
350
|
+
except Exception as e:
|
|
351
|
+
raise ErrorDuringImport from e
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@contextlib.contextmanager
|
|
355
|
+
def imported_alternative(name: str, suppress: Tuple[str, ...] = ()):
|
|
356
|
+
"""Load an alternative version of a module with some modules suppressed."""
|
|
357
|
+
modules = sys.modules
|
|
358
|
+
orig_module = importlib.import_module(name) # Ensure the regular version is loaded
|
|
359
|
+
modules.update({k: None for k in suppress}) # type: ignore
|
|
360
|
+
alternative = importlib.reload(orig_module)
|
|
361
|
+
try:
|
|
362
|
+
yield
|
|
363
|
+
finally:
|
|
364
|
+
for k in suppress:
|
|
365
|
+
del modules[k]
|
|
366
|
+
importlib.reload(alternative)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def format_boundargs_as_dictionary(bound_args: BoundArguments) -> str:
|
|
370
|
+
body = ", ".join(f'"{k}": {repr(v)}' for k, v in bound_args.arguments.items())
|
|
371
|
+
return "{" + body + "}"
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def format_boundargs(bound_args: BoundArguments) -> str:
|
|
375
|
+
arg_strings: List[str] = []
|
|
376
|
+
for name, param in bound_args.signature.parameters.items():
|
|
377
|
+
param_kind = param.kind
|
|
378
|
+
vals = bound_args.arguments.get(name, param.default)
|
|
379
|
+
if param_kind == Parameter.VAR_POSITIONAL:
|
|
380
|
+
arg_strings.extend(map(repr, vals))
|
|
381
|
+
elif param_kind == Parameter.VAR_KEYWORD:
|
|
382
|
+
arg_strings.extend(f"{k}={repr(v)}" for k, v in vals.items())
|
|
383
|
+
else:
|
|
384
|
+
if param_kind == Parameter.POSITIONAL_ONLY:
|
|
385
|
+
use_keyword = False
|
|
386
|
+
elif param_kind == Parameter.KEYWORD_ONLY:
|
|
387
|
+
use_keyword = True
|
|
388
|
+
else:
|
|
389
|
+
use_keyword = param.default is not Parameter.empty
|
|
390
|
+
if use_keyword:
|
|
391
|
+
arg_strings.append(f"{name}={repr(vals)}")
|
|
392
|
+
else:
|
|
393
|
+
arg_strings.append(repr(vals))
|
|
394
|
+
return ", ".join(arg_strings)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
UNABLE_TO_REPR_TEXT = "<unable to repr>"
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def eval_friendly_repr(obj: object) -> str:
|
|
401
|
+
assert not is_tracing()
|
|
402
|
+
with ResumedTracing(), EvalFriendlyReprContext() as ctx:
|
|
403
|
+
try:
|
|
404
|
+
# TODO: probably only the repr should have tracing enabled
|
|
405
|
+
return ctx.cleanup(repr(obj))
|
|
406
|
+
except Exception as e:
|
|
407
|
+
if isinstance(e, (IgnoreAttempt, UnexploredPath)):
|
|
408
|
+
raise
|
|
409
|
+
debug("Repr failed, ", type(e), ":", str(e))
|
|
410
|
+
debug("Repr failed at:", ch_stack(currently_handling=e))
|
|
411
|
+
return UNABLE_TO_REPR_TEXT
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
@dataclass(frozen=True)
|
|
415
|
+
class ReferencedIdentifier:
|
|
416
|
+
modulename: str
|
|
417
|
+
qualname: str
|
|
418
|
+
|
|
419
|
+
def __str__(self):
|
|
420
|
+
if self.modulename in ("builtins", ""):
|
|
421
|
+
return self.qualname
|
|
422
|
+
else:
|
|
423
|
+
return f"{self.modulename}.{self.qualname}"
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def callable_identifier(cls: Callable) -> ReferencedIdentifier:
|
|
427
|
+
return ReferencedIdentifier(cls.__module__, cls.__qualname__)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def method_identifier(fn: Callable) -> ReferencedIdentifier:
|
|
431
|
+
if getattr(fn, "__objclass__", None):
|
|
432
|
+
clsref = callable_identifier(fn.__objclass__) # type: ignore
|
|
433
|
+
return ReferencedIdentifier(
|
|
434
|
+
clsref.modulename, f"{clsref.qualname}.{fn.__name__}"
|
|
435
|
+
)
|
|
436
|
+
return callable_identifier(fn)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
# Objects of these types are known to always be *deeply* immutable:
|
|
440
|
+
ATOMIC_IMMUTABLE_TYPES = (
|
|
441
|
+
type(None),
|
|
442
|
+
bool,
|
|
443
|
+
int,
|
|
444
|
+
str,
|
|
445
|
+
float,
|
|
446
|
+
complex,
|
|
447
|
+
types.FunctionType,
|
|
448
|
+
types.BuiltinFunctionType,
|
|
449
|
+
types.LambdaType,
|
|
450
|
+
types.MethodType,
|
|
451
|
+
types.BuiltinMethodType,
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
class EvalFriendlyReprContext:
|
|
456
|
+
"""
|
|
457
|
+
Monkey-patch repr() to make some cases more ammenible to eval().
|
|
458
|
+
|
|
459
|
+
In particular:
|
|
460
|
+
* object instances repr as "object()" rather than "<object object at ...>"
|
|
461
|
+
* non-finite floats like inf repr as 'float("inf")' rather than just 'inf'
|
|
462
|
+
* functions repr as their fully qualified names
|
|
463
|
+
* enums repr like "Color.RED" instead of "<Color.RED: 0>"
|
|
464
|
+
* uses the walrus (:=) operator to faithfully represent aliased values
|
|
465
|
+
|
|
466
|
+
Use the cleanup method to strip unnecessary assignments from the output.
|
|
467
|
+
|
|
468
|
+
>>> with EvalFriendlyReprContext() as ctx:
|
|
469
|
+
... ctx.cleanup(repr(object()))
|
|
470
|
+
'object()'
|
|
471
|
+
>>> with EvalFriendlyReprContext() as ctx:
|
|
472
|
+
... ctx.cleanup(repr(float("nan")))
|
|
473
|
+
'float("nan")'
|
|
474
|
+
|
|
475
|
+
The same context can be re-used to perform aliasing across multiple calls to repr:
|
|
476
|
+
|
|
477
|
+
>>> lst = []
|
|
478
|
+
>>> ctx = EvalFriendlyReprContext()
|
|
479
|
+
>>> with ctx:
|
|
480
|
+
... part1 = repr(lst)
|
|
481
|
+
>>> with ctx:
|
|
482
|
+
... part2 = repr(lst)
|
|
483
|
+
>>> ctx.cleanup(part1 + " and also " + part2)
|
|
484
|
+
'v1:=[] and also v1'
|
|
485
|
+
"""
|
|
486
|
+
|
|
487
|
+
def __init__(self, instance_overrides: Optional[IdKeyedDict] = None):
|
|
488
|
+
self.instance_overrides = (
|
|
489
|
+
IdKeyedDict() if instance_overrides is None else instance_overrides
|
|
490
|
+
)
|
|
491
|
+
self.repr_references: Set[ReferencedIdentifier] = set()
|
|
492
|
+
|
|
493
|
+
def __enter__(self):
|
|
494
|
+
if not is_tracing():
|
|
495
|
+
raise CrossHairInternal
|
|
496
|
+
OVERRIDES: Dict[type, Callable[[Any], Union[str, ReferencedIdentifier]]] = {
|
|
497
|
+
object: lambda o: "object()",
|
|
498
|
+
list: lambda o: f"[{', '.join(map(repr, o))}]", # (de-optimize C-level repr)
|
|
499
|
+
memoryview: lambda o: f"memoryview({repr(o.obj)})",
|
|
500
|
+
FunctionType: callable_identifier,
|
|
501
|
+
BuiltinFunctionType: callable_identifier,
|
|
502
|
+
MethodDescriptorType: method_identifier,
|
|
503
|
+
}
|
|
504
|
+
instance_overrides = self.instance_overrides
|
|
505
|
+
|
|
506
|
+
@functools.wraps(builtins.repr)
|
|
507
|
+
def _eval_friendly_repr(obj) -> str:
|
|
508
|
+
oid = id(obj)
|
|
509
|
+
typ = type(obj)
|
|
510
|
+
if obj in instance_overrides:
|
|
511
|
+
repr_fn: Callable[[Any], Union[str, ReferencedIdentifier]] = (
|
|
512
|
+
instance_overrides[obj]
|
|
513
|
+
)
|
|
514
|
+
elif typ == float:
|
|
515
|
+
if math.isfinite(obj):
|
|
516
|
+
repr_fn = repr
|
|
517
|
+
else:
|
|
518
|
+
repr_fn = lambda o: f'float("{o}")'
|
|
519
|
+
elif typ in OVERRIDES:
|
|
520
|
+
repr_fn = OVERRIDES[typ]
|
|
521
|
+
elif isinstance(obj, Enum) and obj in typ:
|
|
522
|
+
repr_fn = lambda _: ReferencedIdentifier(
|
|
523
|
+
typ.__module__, f"{typ.__qualname__}.{obj.name}"
|
|
524
|
+
)
|
|
525
|
+
elif isinstance(obj, type):
|
|
526
|
+
repr_fn = callable_identifier
|
|
527
|
+
else:
|
|
528
|
+
repr_fn = repr
|
|
529
|
+
str_or_ref = repr_fn(obj)
|
|
530
|
+
if isinstance(str_or_ref, ReferencedIdentifier):
|
|
531
|
+
self.repr_references.add(str_or_ref)
|
|
532
|
+
return str_or_ref.qualname
|
|
533
|
+
value_str = str_or_ref
|
|
534
|
+
if isinstance(obj, (ATOMIC_IMMUTABLE_TYPES, Enum)):
|
|
535
|
+
return value_str
|
|
536
|
+
name = f"_ch_efr_{oid}_"
|
|
537
|
+
instance_overrides[obj] = lambda _: name
|
|
538
|
+
return value_str if value_str == name else f"{name}:={value_str}"
|
|
539
|
+
|
|
540
|
+
self.patches = {repr: _eval_friendly_repr}
|
|
541
|
+
COMPOSITE_TRACER.patching_module.add(self.patches)
|
|
542
|
+
return self
|
|
543
|
+
|
|
544
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
545
|
+
COMPOSITE_TRACER.patching_module.pop(self.patches)
|
|
546
|
+
|
|
547
|
+
def cleanup(self, output: str) -> str:
|
|
548
|
+
counts = collections.Counter(re.compile(r"\b_ch_efr_\d+_\b").findall(output))
|
|
549
|
+
assignment_remaps = {}
|
|
550
|
+
nextvarnum = 1
|
|
551
|
+
for varname, count in counts.items():
|
|
552
|
+
if count > 1:
|
|
553
|
+
assignment_remaps[varname + ":="] = f"v{nextvarnum}:="
|
|
554
|
+
assignment_remaps[varname] = f"v{nextvarnum}"
|
|
555
|
+
nextvarnum += 1
|
|
556
|
+
return re.compile(r"\b(_ch_efr_\d+_)\b(\:\=)?").sub(
|
|
557
|
+
lambda match: assignment_remaps.get(match.group(), ""), output
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def extract_module_from_file(filename: str) -> Tuple[str, str]:
|
|
562
|
+
module_name = getmodulename(filename)
|
|
563
|
+
dirs = []
|
|
564
|
+
if module_name and module_name != "__init__":
|
|
565
|
+
dirs.append(module_name)
|
|
566
|
+
path = os.path.split(os.path.realpath(filename))[0]
|
|
567
|
+
while os.path.exists(os.path.join(path, "__init__.py")):
|
|
568
|
+
path, cur = os.path.split(path)
|
|
569
|
+
dirs.append(cur)
|
|
570
|
+
dirs.reverse()
|
|
571
|
+
module = ".".join(dirs)
|
|
572
|
+
return path, module
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def renamed_function(fn: FunctionType, new_name: str):
|
|
576
|
+
"""Produced a completely renamed function"""
|
|
577
|
+
return FunctionType(
|
|
578
|
+
fn.__code__.replace(co_name=new_name, co_filename=new_name + ".py"),
|
|
579
|
+
fn.__globals__,
|
|
580
|
+
new_name,
|
|
581
|
+
fn.__defaults__,
|
|
582
|
+
fn.__closure__,
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
_T = TypeVar("_T")
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
class DynamicScopeVar(Generic[_T]):
|
|
590
|
+
"""
|
|
591
|
+
Manage a hidden value that can get passed through the callstack.
|
|
592
|
+
|
|
593
|
+
This has similar downsides to threadlocals/globals; it should be
|
|
594
|
+
used sparingly.
|
|
595
|
+
|
|
596
|
+
>>> _VAR = DynamicScopeVar(int)
|
|
597
|
+
>>> with _VAR.open(42):
|
|
598
|
+
... _VAR.get()
|
|
599
|
+
42
|
|
600
|
+
"""
|
|
601
|
+
|
|
602
|
+
def __init__(self, typ: Type[_T], name_for_debugging: str = ""):
|
|
603
|
+
self._local = threading.local()
|
|
604
|
+
self._name = name_for_debugging
|
|
605
|
+
|
|
606
|
+
@contextlib.contextmanager
|
|
607
|
+
def open(self, value: _T, reentrant: bool = True):
|
|
608
|
+
_local = self._local
|
|
609
|
+
old_value = getattr(_local, "value", None)
|
|
610
|
+
if not reentrant:
|
|
611
|
+
assert old_value is None, f"Already in a {self._name} context"
|
|
612
|
+
_local.value = value
|
|
613
|
+
try:
|
|
614
|
+
yield value
|
|
615
|
+
finally:
|
|
616
|
+
assert getattr(_local, "value", None) is value
|
|
617
|
+
_local.value = old_value
|
|
618
|
+
|
|
619
|
+
def get(self, default: Optional[_T] = None) -> _T:
|
|
620
|
+
ret = getattr(self._local, "value", None)
|
|
621
|
+
if ret is not None:
|
|
622
|
+
return ret
|
|
623
|
+
if default is not None:
|
|
624
|
+
return default
|
|
625
|
+
assert False, f"Not in a {self._name} context"
|
|
626
|
+
|
|
627
|
+
def get_if_in_scope(self) -> Optional[_T]:
|
|
628
|
+
return getattr(self._local, "value", None)
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
class AttributeHolder:
|
|
632
|
+
def __init__(self, attrs: Mapping[str, object]):
|
|
633
|
+
for k, v in attrs.items():
|
|
634
|
+
self.__dict__[k] = v
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
class CrossHairValue:
|
|
638
|
+
"""Base class for values that are pretending to be other values."""
|
|
639
|
+
|
|
640
|
+
pass
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
class ControlFlowException(BaseException):
|
|
644
|
+
# CrossHair sometimes uses exceptions to abort a path mid-execution.
|
|
645
|
+
# We extend such exceptions from BaseException instead of Exception,
|
|
646
|
+
# because expect that user code will usually only handle Exception.
|
|
647
|
+
pass
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
class CrossHairInternal(ControlFlowException):
|
|
651
|
+
def __init__(self, *a):
|
|
652
|
+
ControlFlowException.__init__(self, *a)
|
|
653
|
+
if in_debug():
|
|
654
|
+
debug("CrossHairInternal:", str(self))
|
|
655
|
+
debug("CrossHairInternal stack trace:")
|
|
656
|
+
for entry in traceback.format_stack()[:-1]:
|
|
657
|
+
for line in entry.splitlines():
|
|
658
|
+
debug("", line)
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
class UnexploredPath(ControlFlowException):
|
|
662
|
+
pass
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
class UnknownSatisfiability(UnexploredPath):
|
|
666
|
+
def __init__(self, *a):
|
|
667
|
+
UnexploredPath.__init__(self, *a)
|
|
668
|
+
debug("UnknownSatisfiability", str(self))
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
class NotDeterministic(Exception):
|
|
672
|
+
pass
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
class PathTimeout(UnexploredPath):
|
|
676
|
+
pass
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
class CrosshairUnsupported(UnexploredPath):
|
|
680
|
+
def __init__(self, *a):
|
|
681
|
+
debug("CrosshairUnsupported: ", str(self))
|
|
682
|
+
debug(" Stack trace:\n" + "".join(traceback.format_stack()))
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
class IgnoreAttempt(ControlFlowException):
|
|
686
|
+
def __init__(self, *a):
|
|
687
|
+
if in_debug():
|
|
688
|
+
debug(f"IgnoreAttempt", *a)
|
|
689
|
+
debug("IgnoreAttempt stack:", ch_stack())
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
if (3, 10) <= sys.version_info < (3, 14):
|
|
693
|
+
# Specialize some definitions for the Python versions where
|
|
694
|
+
# typing.Union != types.UnionType:
|
|
695
|
+
|
|
696
|
+
def origin_of(typ: Type) -> Type:
|
|
697
|
+
if hasattr(typ, "__origin__"):
|
|
698
|
+
return typ.__origin__
|
|
699
|
+
elif isinstance(typ, types.UnionType):
|
|
700
|
+
return cast(Type, Union)
|
|
701
|
+
else:
|
|
702
|
+
return typ
|
|
703
|
+
|
|
704
|
+
def type_args_of(typ: Type) -> Tuple[Type, ...]:
|
|
705
|
+
if getattr(typ, "__args__", None):
|
|
706
|
+
if isinstance(typ, types.UnionType):
|
|
707
|
+
return typ.__args__
|
|
708
|
+
return typing_inspect.get_args(typ, evaluate=True)
|
|
709
|
+
else:
|
|
710
|
+
return ()
|
|
711
|
+
|
|
712
|
+
else:
|
|
713
|
+
|
|
714
|
+
def type_args_of(typ: Type) -> Tuple[Type, ...]:
|
|
715
|
+
if getattr(typ, "__args__", None):
|
|
716
|
+
return typing_inspect.get_args(typ, evaluate=True)
|
|
717
|
+
else:
|
|
718
|
+
return ()
|
|
719
|
+
|
|
720
|
+
def origin_of(typ: Type) -> Type:
|
|
721
|
+
origin = getattr(typ, "__origin__", None)
|
|
722
|
+
# 3.14 unifies typing.Union and types.Union, so that's good!
|
|
723
|
+
# But a of 3.14.0a6, types.Union.__origin__ yields a data descriptor, so we need to check that.
|
|
724
|
+
return typ if origin is None or isdatadescriptor(origin) else origin
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def type_arg_of(typ: Type, index: int) -> Type:
|
|
728
|
+
args = type_args_of(typ)
|
|
729
|
+
return args[index] if index < len(args) else object
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def mem_usage_kb():
|
|
733
|
+
try:
|
|
734
|
+
import resource
|
|
735
|
+
except ImportError:
|
|
736
|
+
return 0 # do not bother monitoring memory on windows
|
|
737
|
+
usage = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
|
|
738
|
+
if sys.platform == "darwin":
|
|
739
|
+
return usage / 1024 # (bytes on osx)
|
|
740
|
+
else:
|
|
741
|
+
return usage # (kb)
|