crosshair-tool 0.0.97__cp314-cp314-macosx_11_0_arm64.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.

Potentially problematic release.


This version of crosshair-tool might be problematic. Click here for more details.

Files changed (176) hide show
  1. _crosshair_tracers.cpython-314-darwin.so +0 -0
  2. crosshair/__init__.py +42 -0
  3. crosshair/__main__.py +8 -0
  4. crosshair/_mark_stacks.h +790 -0
  5. crosshair/_preliminaries_test.py +18 -0
  6. crosshair/_tracers.h +94 -0
  7. crosshair/_tracers_pycompat.h +522 -0
  8. crosshair/_tracers_test.py +138 -0
  9. crosshair/abcstring.py +245 -0
  10. crosshair/auditwall.py +190 -0
  11. crosshair/auditwall_test.py +77 -0
  12. crosshair/codeconfig.py +113 -0
  13. crosshair/codeconfig_test.py +117 -0
  14. crosshair/condition_parser.py +1237 -0
  15. crosshair/condition_parser_test.py +497 -0
  16. crosshair/conftest.py +30 -0
  17. crosshair/copyext.py +145 -0
  18. crosshair/copyext_test.py +74 -0
  19. crosshair/core.py +1759 -0
  20. crosshair/core_and_libs.py +149 -0
  21. crosshair/core_regestered_types_test.py +82 -0
  22. crosshair/core_test.py +1313 -0
  23. crosshair/diff_behavior.py +314 -0
  24. crosshair/diff_behavior_test.py +261 -0
  25. crosshair/dynamic_typing.py +346 -0
  26. crosshair/dynamic_typing_test.py +210 -0
  27. crosshair/enforce.py +282 -0
  28. crosshair/enforce_test.py +182 -0
  29. crosshair/examples/PEP316/__init__.py +1 -0
  30. crosshair/examples/PEP316/bugs_detected/__init__.py +0 -0
  31. crosshair/examples/PEP316/bugs_detected/getattr_magic.py +16 -0
  32. crosshair/examples/PEP316/bugs_detected/hash_consistent_with_equals.py +31 -0
  33. crosshair/examples/PEP316/bugs_detected/shopping_cart.py +24 -0
  34. crosshair/examples/PEP316/bugs_detected/showcase.py +39 -0
  35. crosshair/examples/PEP316/correct_code/__init__.py +0 -0
  36. crosshair/examples/PEP316/correct_code/arith.py +60 -0
  37. crosshair/examples/PEP316/correct_code/chess.py +77 -0
  38. crosshair/examples/PEP316/correct_code/nesting_inference.py +17 -0
  39. crosshair/examples/PEP316/correct_code/numpy_examples.py +132 -0
  40. crosshair/examples/PEP316/correct_code/rolling_average.py +35 -0
  41. crosshair/examples/PEP316/correct_code/showcase.py +104 -0
  42. crosshair/examples/__init__.py +0 -0
  43. crosshair/examples/check_examples_test.py +146 -0
  44. crosshair/examples/deal/__init__.py +1 -0
  45. crosshair/examples/icontract/__init__.py +1 -0
  46. crosshair/examples/icontract/bugs_detected/__init__.py +0 -0
  47. crosshair/examples/icontract/bugs_detected/showcase.py +41 -0
  48. crosshair/examples/icontract/bugs_detected/wrong_sign.py +8 -0
  49. crosshair/examples/icontract/correct_code/__init__.py +0 -0
  50. crosshair/examples/icontract/correct_code/arith.py +51 -0
  51. crosshair/examples/icontract/correct_code/showcase.py +94 -0
  52. crosshair/fnutil.py +391 -0
  53. crosshair/fnutil_test.py +75 -0
  54. crosshair/fuzz_core_test.py +516 -0
  55. crosshair/libimpl/__init__.py +0 -0
  56. crosshair/libimpl/arraylib.py +161 -0
  57. crosshair/libimpl/binascii_ch_test.py +30 -0
  58. crosshair/libimpl/binascii_test.py +67 -0
  59. crosshair/libimpl/binasciilib.py +150 -0
  60. crosshair/libimpl/bisectlib_test.py +23 -0
  61. crosshair/libimpl/builtinslib.py +5133 -0
  62. crosshair/libimpl/builtinslib_ch_test.py +1191 -0
  63. crosshair/libimpl/builtinslib_test.py +3705 -0
  64. crosshair/libimpl/codecslib.py +86 -0
  65. crosshair/libimpl/codecslib_test.py +86 -0
  66. crosshair/libimpl/collectionslib.py +264 -0
  67. crosshair/libimpl/collectionslib_ch_test.py +252 -0
  68. crosshair/libimpl/collectionslib_test.py +332 -0
  69. crosshair/libimpl/copylib.py +23 -0
  70. crosshair/libimpl/copylib_test.py +18 -0
  71. crosshair/libimpl/datetimelib.py +2546 -0
  72. crosshair/libimpl/datetimelib_ch_test.py +349 -0
  73. crosshair/libimpl/datetimelib_test.py +112 -0
  74. crosshair/libimpl/decimallib.py +5257 -0
  75. crosshair/libimpl/decimallib_ch_test.py +78 -0
  76. crosshair/libimpl/decimallib_test.py +76 -0
  77. crosshair/libimpl/encodings/__init__.py +23 -0
  78. crosshair/libimpl/encodings/_encutil.py +187 -0
  79. crosshair/libimpl/encodings/ascii.py +44 -0
  80. crosshair/libimpl/encodings/latin_1.py +40 -0
  81. crosshair/libimpl/encodings/utf_8.py +93 -0
  82. crosshair/libimpl/encodings_ch_test.py +83 -0
  83. crosshair/libimpl/fractionlib.py +16 -0
  84. crosshair/libimpl/fractionlib_test.py +80 -0
  85. crosshair/libimpl/functoolslib.py +34 -0
  86. crosshair/libimpl/functoolslib_test.py +56 -0
  87. crosshair/libimpl/hashliblib.py +30 -0
  88. crosshair/libimpl/hashliblib_test.py +18 -0
  89. crosshair/libimpl/heapqlib.py +47 -0
  90. crosshair/libimpl/heapqlib_test.py +21 -0
  91. crosshair/libimpl/importliblib.py +18 -0
  92. crosshair/libimpl/importliblib_test.py +38 -0
  93. crosshair/libimpl/iolib.py +216 -0
  94. crosshair/libimpl/iolib_ch_test.py +128 -0
  95. crosshair/libimpl/iolib_test.py +19 -0
  96. crosshair/libimpl/ipaddresslib.py +8 -0
  97. crosshair/libimpl/itertoolslib.py +44 -0
  98. crosshair/libimpl/itertoolslib_test.py +44 -0
  99. crosshair/libimpl/jsonlib.py +984 -0
  100. crosshair/libimpl/jsonlib_ch_test.py +42 -0
  101. crosshair/libimpl/jsonlib_test.py +51 -0
  102. crosshair/libimpl/mathlib.py +179 -0
  103. crosshair/libimpl/mathlib_ch_test.py +44 -0
  104. crosshair/libimpl/mathlib_test.py +67 -0
  105. crosshair/libimpl/oslib.py +7 -0
  106. crosshair/libimpl/pathliblib_test.py +10 -0
  107. crosshair/libimpl/randomlib.py +177 -0
  108. crosshair/libimpl/randomlib_test.py +120 -0
  109. crosshair/libimpl/relib.py +846 -0
  110. crosshair/libimpl/relib_ch_test.py +169 -0
  111. crosshair/libimpl/relib_test.py +493 -0
  112. crosshair/libimpl/timelib.py +72 -0
  113. crosshair/libimpl/timelib_test.py +82 -0
  114. crosshair/libimpl/typeslib.py +15 -0
  115. crosshair/libimpl/typeslib_test.py +36 -0
  116. crosshair/libimpl/unicodedatalib.py +75 -0
  117. crosshair/libimpl/unicodedatalib_test.py +42 -0
  118. crosshair/libimpl/urlliblib.py +23 -0
  119. crosshair/libimpl/urlliblib_test.py +19 -0
  120. crosshair/libimpl/weakreflib.py +13 -0
  121. crosshair/libimpl/weakreflib_test.py +69 -0
  122. crosshair/libimpl/zliblib.py +15 -0
  123. crosshair/libimpl/zliblib_test.py +13 -0
  124. crosshair/lsp_server.py +250 -0
  125. crosshair/lsp_server_test.py +30 -0
  126. crosshair/main.py +973 -0
  127. crosshair/main_test.py +543 -0
  128. crosshair/objectproxy.py +376 -0
  129. crosshair/objectproxy_test.py +41 -0
  130. crosshair/opcode_intercept.py +601 -0
  131. crosshair/opcode_intercept_test.py +304 -0
  132. crosshair/options.py +218 -0
  133. crosshair/options_test.py +10 -0
  134. crosshair/patch_equivalence_test.py +75 -0
  135. crosshair/path_cover.py +209 -0
  136. crosshair/path_cover_test.py +138 -0
  137. crosshair/path_search.py +161 -0
  138. crosshair/path_search_test.py +52 -0
  139. crosshair/pathing_oracle.py +271 -0
  140. crosshair/pathing_oracle_test.py +21 -0
  141. crosshair/pure_importer.py +27 -0
  142. crosshair/pure_importer_test.py +16 -0
  143. crosshair/py.typed +0 -0
  144. crosshair/register_contract.py +273 -0
  145. crosshair/register_contract_test.py +190 -0
  146. crosshair/simplestructs.py +1161 -0
  147. crosshair/simplestructs_test.py +283 -0
  148. crosshair/smtlib.py +24 -0
  149. crosshair/smtlib_test.py +14 -0
  150. crosshair/statespace.py +1196 -0
  151. crosshair/statespace_test.py +99 -0
  152. crosshair/stubs_parser.py +352 -0
  153. crosshair/stubs_parser_test.py +43 -0
  154. crosshair/test_util.py +329 -0
  155. crosshair/test_util_test.py +26 -0
  156. crosshair/tools/__init__.py +0 -0
  157. crosshair/tools/check_help_in_doc.py +264 -0
  158. crosshair/tools/check_init_and_setup_coincide.py +119 -0
  159. crosshair/tools/generate_demo_table.py +127 -0
  160. crosshair/tracers.py +525 -0
  161. crosshair/tracers_test.py +154 -0
  162. crosshair/type_repo.py +151 -0
  163. crosshair/unicode_categories.py +589 -0
  164. crosshair/unicode_categories_test.py +27 -0
  165. crosshair/util.py +736 -0
  166. crosshair/util_test.py +173 -0
  167. crosshair/watcher.py +307 -0
  168. crosshair/watcher_test.py +107 -0
  169. crosshair/z3util.py +76 -0
  170. crosshair/z3util_test.py +11 -0
  171. crosshair_tool-0.0.97.dist-info/METADATA +145 -0
  172. crosshair_tool-0.0.97.dist-info/RECORD +176 -0
  173. crosshair_tool-0.0.97.dist-info/WHEEL +6 -0
  174. crosshair_tool-0.0.97.dist-info/entry_points.txt +3 -0
  175. crosshair_tool-0.0.97.dist-info/licenses/LICENSE +93 -0
  176. crosshair_tool-0.0.97.dist-info/top_level.txt +2 -0
crosshair/tracers.py ADDED
@@ -0,0 +1,525 @@
1
+ """Provide access to and overrides for functions as they are called."""
2
+
3
+ import ctypes
4
+ import dataclasses
5
+ import dis
6
+ import sys
7
+ import types
8
+ from collections import defaultdict
9
+ from sys import _getframe
10
+ from types import CodeType
11
+ from typing import (
12
+ Any,
13
+ Callable,
14
+ DefaultDict,
15
+ Dict,
16
+ FrozenSet,
17
+ Iterable,
18
+ List,
19
+ Optional,
20
+ Set,
21
+ Tuple,
22
+ TypeVar,
23
+ )
24
+
25
+ from _crosshair_tracers import CTracer, TraceSwap, supported_opcodes # type: ignore
26
+
27
+ SYS_MONITORING_TOOL_ID = 4
28
+ USE_C_TRACER = True
29
+
30
+ PyObjPtr = ctypes.POINTER(ctypes.py_object)
31
+ Py_IncRef = ctypes.pythonapi.Py_IncRef
32
+ Py_DecRef = ctypes.pythonapi.Py_DecRef
33
+
34
+
35
+ _debug_header: Tuple[Tuple[str, type], ...] = (
36
+ (
37
+ ("_ob_next", PyObjPtr),
38
+ ("_ob_prev", PyObjPtr),
39
+ )
40
+ if sys.flags.debug
41
+ else ()
42
+ )
43
+
44
+
45
+ from _crosshair_tracers import frame_stack_read, frame_stack_write
46
+
47
+ CALL_FUNCTION = dis.opmap.get("CALL_FUNCTION", 256)
48
+ CALL_FUNCTION_KW = dis.opmap.get("CALL_FUNCTION_KW", 256) # Removed as of 3.11
49
+ CALL_FUNCTION_EX = dis.opmap.get("CALL_FUNCTION_EX", 256)
50
+ CALL_METHOD = dis.opmap.get("CALL_METHOD", 256)
51
+ BUILD_TUPLE_UNPACK_WITH_CALL = dis.opmap.get("BUILD_TUPLE_UNPACK_WITH_CALL", 256)
52
+ CALL = dis.opmap.get("CALL", 256)
53
+ CALL_KW = dis.opmap.get("CALL_KW", 256) # New in 3.13
54
+
55
+
56
+ class RawNullPointer:
57
+ pass
58
+
59
+
60
+ NULL_POINTER = RawNullPointer()
61
+ CallStackInfo = (
62
+ Tuple[ # Information about the interpreter stack just before calling a function
63
+ int, # stack index of the callable
64
+ Callable, # the callable object itself
65
+ Optional[int], # index of kwargs dict (if used in this call)
66
+ ]
67
+ )
68
+
69
+
70
+ def handle_build_tuple_unpack_with_call(frame) -> CallStackInfo:
71
+ idx = -(
72
+ frame.f_code.co_code[frame.f_lasti + 1] + 1
73
+ ) # TODO: account for EXTENDED_ARG, here and elsewhere
74
+ try:
75
+ return (idx, frame_stack_read(frame, idx), None)
76
+ except ValueError:
77
+ return (idx, NULL_POINTER) # type: ignore
78
+
79
+
80
+ def handle_call_3_11(frame) -> CallStackInfo:
81
+ idx = -(frame.f_code.co_code[frame.f_lasti + 1] + 1)
82
+ try:
83
+ ret = (idx - 1, frame_stack_read(frame, idx - 1), None)
84
+ except ValueError:
85
+ ret = (idx, frame_stack_read(frame, idx), None)
86
+ return ret
87
+
88
+
89
+ def handle_call_3_13(frame) -> CallStackInfo:
90
+ idx = -(frame.f_code.co_code[frame.f_lasti + 1] + 2)
91
+ return (idx, frame_stack_read(frame, idx), None)
92
+
93
+
94
+ def handle_call_function(frame) -> CallStackInfo:
95
+ idx = -(frame.f_code.co_code[frame.f_lasti + 1] + 1)
96
+ try:
97
+ return (idx, frame_stack_read(frame, idx), None)
98
+ except ValueError:
99
+ return (idx, NULL_POINTER) # type: ignore
100
+
101
+
102
+ def handle_call_function_kw(frame) -> CallStackInfo:
103
+ idx = -(frame.f_code.co_code[frame.f_lasti + 1] + 2)
104
+ try:
105
+ return (idx, frame_stack_read(frame, idx), None)
106
+ except ValueError:
107
+ return (idx, NULL_POINTER) # type: ignore
108
+
109
+
110
+ def handle_call_kw(frame) -> CallStackInfo:
111
+ idx = -(frame.f_code.co_code[frame.f_lasti + 1] + 3)
112
+ return (idx, frame_stack_read(frame, idx), None)
113
+
114
+
115
+ def handle_call_function_ex_3_6(frame) -> CallStackInfo:
116
+ has_kwargs = frame.f_code.co_code[frame.f_lasti + 1] & 1
117
+ idx = -(has_kwargs + 2)
118
+ kwargs_idx = -1 if has_kwargs else None
119
+ try:
120
+ return (idx, frame_stack_read(frame, idx), kwargs_idx)
121
+ except ValueError:
122
+ return (idx, NULL_POINTER, kwargs_idx) # type: ignore
123
+
124
+
125
+ def handle_call_function_ex_3_13(frame) -> CallStackInfo:
126
+ has_kwargs = frame.f_code.co_code[frame.f_lasti + 1] & 1
127
+ idx = -(has_kwargs + 3)
128
+ kwargs_idx = -1 if has_kwargs else None
129
+ try:
130
+ return (idx, frame_stack_read(frame, idx), kwargs_idx)
131
+ except ValueError:
132
+ return (idx, NULL_POINTER, kwargs_idx) # type: ignore
133
+
134
+
135
+ def handle_call_function_ex_3_14(frame) -> CallStackInfo:
136
+ callable_idx, kwargs_idx = -4, -1
137
+ try:
138
+ return (callable_idx, frame_stack_read(frame, callable_idx), kwargs_idx)
139
+ except ValueError:
140
+ return (callable_idx, NULL_POINTER, kwargs_idx) # type: ignore
141
+
142
+
143
+ def handle_call_method(frame) -> CallStackInfo:
144
+ idx = -(frame.f_code.co_code[frame.f_lasti + 1] + 2)
145
+ try:
146
+ return (idx, frame_stack_read(frame, idx), None)
147
+ except ValueError:
148
+ # not a sucessful method lookup; no call happens here
149
+ idx += 1
150
+ return (idx, frame_stack_read(frame, idx), None)
151
+
152
+
153
+ _CALL_HANDLERS: Dict[int, Callable[[object], CallStackInfo]] = {
154
+ BUILD_TUPLE_UNPACK_WITH_CALL: handle_build_tuple_unpack_with_call,
155
+ CALL: handle_call_3_13 if sys.version_info >= (3, 13) else handle_call_3_11,
156
+ CALL_KW: handle_call_kw,
157
+ CALL_FUNCTION: handle_call_function,
158
+ CALL_FUNCTION_KW: handle_call_function_kw,
159
+ CALL_FUNCTION_EX: (
160
+ handle_call_function_ex_3_14
161
+ if sys.version_info >= (3, 14)
162
+ else (
163
+ handle_call_function_ex_3_13
164
+ if sys.version_info >= (3, 13)
165
+ else handle_call_function_ex_3_6
166
+ )
167
+ ),
168
+ CALL_METHOD: handle_call_method,
169
+ }
170
+
171
+
172
+ class Untracable:
173
+ pass
174
+
175
+
176
+ class TraceException(BaseException):
177
+ # We extend BaseException instead of Exception, because it won't be considered a
178
+ # user-level exception by CrossHair. (this is for internal assertions)
179
+ pass
180
+
181
+
182
+ def check_opcode_support(opcodes: FrozenSet[int]):
183
+ if sys.version_info < (3, 12):
184
+ return
185
+ missing_opcodes = opcodes - set(supported_opcodes())
186
+ if missing_opcodes:
187
+ raise TraceException(
188
+ f"The C-level tracer does not support these opcodes: {','.join(map(dis.opname.__getitem__, missing_opcodes))}"
189
+ )
190
+
191
+
192
+ check_opcode_support(frozenset(_CALL_HANDLERS.keys()))
193
+
194
+
195
+ wrapper_descriptor_type = type(int.__bool__)
196
+ assert str(wrapper_descriptor_type) == "<class 'wrapper_descriptor'>"
197
+
198
+ _NORMAL_CALLABLE_TYPES = (
199
+ type,
200
+ types.FunctionType, #': <class 'function'>,
201
+ types.MethodDescriptorType, #': <class 'method_descriptor'>,
202
+ types.MethodType, #': <class 'method'>,
203
+ types.MethodWrapperType, #': <class 'method-wrapper'>}
204
+ types.BuiltinFunctionType, #': <class 'builtin_function_or_method'>,
205
+ types.BuiltinMethodType, #: <class 'builtin_function_or_method'>,
206
+ types.ClassMethodDescriptorType, #': <class 'classmethod_descriptor'>,
207
+ wrapper_descriptor_type,
208
+ )
209
+
210
+
211
+ class TracingModule:
212
+ # override these!:
213
+ opcodes_wanted = frozenset(_CALL_HANDLERS.keys())
214
+
215
+ def __call__(self, frame, codeobj, opcodenum):
216
+ return self.trace_op(frame, codeobj, opcodenum)
217
+
218
+ def trace_op(self, frame, codeobj, opcodenum):
219
+ if is_tracing():
220
+ raise TraceException
221
+ call_handler = _CALL_HANDLERS.get(opcodenum)
222
+ if not call_handler:
223
+ return None
224
+ (fn_idx, target, kwargs_idx) = call_handler(frame)
225
+ binding_target = None
226
+
227
+ __self = None
228
+ try:
229
+ __self = object.__getattribute__(target, "__self__")
230
+ except AttributeError:
231
+ pass
232
+ if (__self is None) and (not isinstance(target, _NORMAL_CALLABLE_TYPES)):
233
+ try:
234
+ target = object.__getattribute__(target, "__call__")
235
+ __self = object.__getattribute__(target, "__self__")
236
+ except AttributeError:
237
+ pass
238
+ if __self is not None:
239
+ try:
240
+ __func = object.__getattribute__(target, "__func__")
241
+ except AttributeError:
242
+ # The implementation is likely in C.
243
+ # Attempt to get a function via the type:
244
+ typelevel_target = getattr(type(__self), target.__name__, None)
245
+ if typelevel_target is not None:
246
+ binding_target = __self
247
+ target = typelevel_target
248
+ else:
249
+ binding_target = __self
250
+ target = __func
251
+
252
+ if kwargs_idx is not None:
253
+ try:
254
+ kwargs_dict = frame_stack_read(frame, kwargs_idx)
255
+ except ValueError:
256
+ pass
257
+ else:
258
+ replacement_kwargs = {
259
+ # TODO: I don't think it's safe to realize in the middle of a tracing operation.
260
+ # Need to confirm with test. I guess we have to wrap the callable instead?
261
+ key.__ch_realize__() if hasattr(key, "__ch_realize__") else key: val
262
+ for key, val in kwargs_dict.items()
263
+ }
264
+ frame_stack_write(frame, kwargs_idx, replacement_kwargs)
265
+
266
+ if isinstance(target, Untracable):
267
+ return None
268
+ replacement = self.trace_call(frame, target, binding_target)
269
+ if replacement is not None:
270
+ target = replacement
271
+ if binding_target is None:
272
+ overwrite_target = target
273
+ else:
274
+ # re-bind a function object if it was originally a bound method
275
+ # on the stack.
276
+ overwrite_target = target.__get__(binding_target, binding_target.__class__) # type: ignore
277
+ frame_stack_write(frame, fn_idx, overwrite_target)
278
+ return None
279
+
280
+ def trace_call(
281
+ self,
282
+ frame: Any,
283
+ fn: Callable,
284
+ binding_target: object,
285
+ ) -> Optional[Callable]:
286
+ return None
287
+
288
+
289
+ TracerConfig = Tuple[Tuple[TracingModule, ...], DefaultDict[int, List[TracingModule]]]
290
+
291
+
292
+ class PatchingModule(TracingModule):
293
+ """Hot-swap functions on the interpreter stack."""
294
+
295
+ def __init__(
296
+ self,
297
+ overrides: Optional[Dict[Callable, Callable]] = None,
298
+ ):
299
+ # NOTE: you might imagine that we should use an IdKeyedDict for self.overrides
300
+ # However, some builtin bound methods have no way to get identity for their code:
301
+ #
302
+ # >>> float.fromhex is float.fromhex
303
+ # False
304
+ #
305
+ self.overrides: Dict[Callable, Callable] = {}
306
+ self.nextfn: Dict[object, Callable] = {} # code object to next, lower layer
307
+ if overrides:
308
+ self.add(overrides)
309
+
310
+ def add(self, new_overrides: Dict[Callable, Callable]):
311
+ for orig, new_override in new_overrides.items():
312
+ prev_override = self.overrides.get(orig, orig)
313
+ assert (
314
+ prev_override is not new_override
315
+ ), f"Function patch {new_override} has already been applied"
316
+ self.nextfn[(new_override.__code__, orig)] = prev_override
317
+ self.overrides[orig] = new_override
318
+
319
+ def pop(self, overrides: Dict[Callable, Callable]):
320
+ for orig, the_override in overrides.items():
321
+ assert self.overrides[orig] is the_override
322
+ self.overrides[orig] = self.nextfn.pop((the_override.__code__, orig))
323
+
324
+ def __repr__(self):
325
+ return f"PatchingModule({list(self.overrides.keys())})"
326
+
327
+ def trace_call(
328
+ self,
329
+ frame: Any,
330
+ fn: Callable,
331
+ binding_target: object,
332
+ ) -> Optional[Callable]:
333
+ try:
334
+ target = self.overrides.get(fn)
335
+ except TypeError as exc:
336
+ # The function is not hashable.
337
+ # This can happen when attempting to invoke a non-function,
338
+ # or possibly it is a method on a non-hashable object that was
339
+ # not properly unbound by `TracingModule.trace_op`.
340
+ return None
341
+ if target is None:
342
+ return None
343
+ caller_code = frame.f_code
344
+ if caller_code.co_name == "_crosshair_wrapper":
345
+ return None
346
+ target_name = getattr(fn, "__name__", "")
347
+ if target_name.endswith("_crosshair_wrapper"):
348
+ return None
349
+ nextfn = self.nextfn.get((caller_code, fn))
350
+ if nextfn is not None:
351
+ if nextfn is fn:
352
+ return None
353
+ return nextfn
354
+ return target
355
+
356
+
357
+ class CompositeTracer:
358
+ def __init__(self):
359
+ self.ctracer = CTracer()
360
+ self.patching_module = PatchingModule()
361
+
362
+ def get_modules(self) -> List[TracingModule]:
363
+ return self.ctracer.get_modules()
364
+
365
+ def set_postop_callback(self, callback, frame):
366
+ self.ctracer.push_postop_callback(frame, callback)
367
+
368
+ if sys.version_info >= (3, 12):
369
+
370
+ def push_module(self, module: TracingModule) -> None:
371
+ sys.monitoring.restart_events()
372
+ self.ctracer.push_module(module)
373
+
374
+ def pop_config(self, module: TracingModule) -> None:
375
+ self.ctracer.pop_module(module)
376
+
377
+ def __enter__(self) -> object:
378
+ self.ctracer.push_module(self.patching_module)
379
+ tool_id = SYS_MONITORING_TOOL_ID
380
+ sys.monitoring.use_tool_id(tool_id, "CrossHair")
381
+ sys.monitoring.register_callback(
382
+ tool_id,
383
+ sys.monitoring.events.INSTRUCTION,
384
+ self.ctracer.instruction_monitor,
385
+ )
386
+ sys.monitoring.set_events(tool_id, sys.monitoring.events.INSTRUCTION)
387
+ sys.monitoring.restart_events()
388
+ self.ctracer.start()
389
+ assert not self.ctracer.is_handling()
390
+ assert self.ctracer.enabled()
391
+ return self
392
+
393
+ def __exit__(self, _etype, exc, _etb):
394
+ tool_id = SYS_MONITORING_TOOL_ID
395
+ sys.monitoring.register_callback(
396
+ tool_id, sys.monitoring.events.INSTRUCTION, None
397
+ )
398
+ sys.monitoring.free_tool_id(tool_id)
399
+ self.ctracer.stop()
400
+ self.ctracer.pop_module(self.patching_module)
401
+
402
+ def trace_caller(self):
403
+ pass
404
+
405
+ else:
406
+
407
+ def push_module(self, module: TracingModule) -> None:
408
+ self.ctracer.push_module(module)
409
+
410
+ def pop_config(self, module: TracingModule) -> None:
411
+ self.ctracer.pop_module(module)
412
+
413
+ def __enter__(self) -> object:
414
+ self.old_traceobj = sys.gettrace()
415
+ # Enable opcode tracing before setting trace function, since Python 3.12; see https://github.com/python/cpython/issues/103615
416
+ sys._getframe().f_trace_opcodes = True
417
+ self.ctracer.push_module(self.patching_module)
418
+ self.ctracer.start()
419
+ assert not self.ctracer.is_handling()
420
+ assert self.ctracer.enabled()
421
+ return self
422
+
423
+ def __exit__(self, _etype, exc, _etb):
424
+ self.ctracer.stop()
425
+ self.ctracer.pop_module(self.patching_module)
426
+ sys.settrace(self.old_traceobj)
427
+
428
+ def trace_caller(self):
429
+ # Frame 0 is the trace_caller method itself
430
+ # Frame 1 is the frame requesting its caller be traced
431
+ # Frame 2 is the caller that we're targeting
432
+ frame = _getframe(2)
433
+ frame.f_trace_opcodes = True
434
+ frame.f_trace = self.ctracer
435
+
436
+
437
+ # We expect the composite tracer to be used like a singleton.
438
+ # (you can only have one tracer active at a time anyway)
439
+ # TODO: Thread-unsafe global. Make this a thread local?
440
+ COMPOSITE_TRACER = CompositeTracer()
441
+
442
+
443
+ @dataclasses.dataclass
444
+ class CoverageResult:
445
+ offsets_covered: Set[int]
446
+ all_offsets: Set[int]
447
+ opcode_coverage: float
448
+
449
+
450
+ class CoverageTracingModule(TracingModule):
451
+ opcodes_wanted = frozenset(i for i in range(256))
452
+
453
+ # TODO: this needs to be moved into a separate kind of monitor to
454
+ # support threading (sys.monitoring probes are global)
455
+
456
+ def __init__(self, *fns: Callable):
457
+ assert not is_tracing()
458
+ self.fns = fns
459
+ self.codeobjects = set(fn.__code__ for fn in fns)
460
+ self.opcode_offsets = {
461
+ code: set(i.offset for i in dis.get_instructions(code))
462
+ for code in self.codeobjects
463
+ }
464
+ self.offsets_seen: Dict[CodeType, Set[int]] = defaultdict(set)
465
+
466
+ def trace_op(self, frame, codeobj, opcodenum):
467
+ code = frame.f_code
468
+ if code not in self.codeobjects:
469
+ return
470
+ lasti = frame.f_lasti
471
+ assert lasti in self.opcode_offsets[code]
472
+ self.offsets_seen[code].add(lasti)
473
+
474
+ def get_results(self, fn: Optional[Callable] = None):
475
+ if fn is None:
476
+ assert len(self.fns) == 1
477
+ fn = self.fns[0]
478
+ possible = self.opcode_offsets[fn.__code__]
479
+ seen = self.offsets_seen[fn.__code__]
480
+ return CoverageResult(
481
+ offsets_covered=seen,
482
+ all_offsets=possible,
483
+ opcode_coverage=len(seen) / len(possible),
484
+ )
485
+
486
+
487
+ class PushedModule:
488
+ def __init__(self, module: TracingModule):
489
+ self.module = module
490
+
491
+ def __enter__(self):
492
+ COMPOSITE_TRACER.push_module(self.module)
493
+
494
+ def __exit__(self, *a):
495
+ COMPOSITE_TRACER.pop_config(self.module)
496
+ return False
497
+
498
+
499
+ def is_tracing():
500
+ return COMPOSITE_TRACER.ctracer.enabled()
501
+
502
+
503
+ def NoTracing():
504
+ return TraceSwap(COMPOSITE_TRACER.ctracer, True)
505
+
506
+
507
+ def ResumedTracing():
508
+ return TraceSwap(COMPOSITE_TRACER.ctracer, False)
509
+
510
+
511
+ _T = TypeVar("_T")
512
+
513
+
514
+ def tracing_iter(itr: Iterable[_T]) -> Iterable[_T]:
515
+ """Selectively re-enable tracing only during iteration."""
516
+ assert not is_tracing()
517
+ # TODO: should we protect his line with ResumedTracing() too?:
518
+ itr = iter(itr)
519
+ while True:
520
+ try:
521
+ with ResumedTracing():
522
+ value = next(itr)
523
+ except StopIteration:
524
+ return
525
+ yield value
@@ -0,0 +1,154 @@
1
+ import dis
2
+
3
+ import pytest
4
+
5
+ from crosshair.tracers import (
6
+ COMPOSITE_TRACER,
7
+ CompositeTracer,
8
+ CoverageTracingModule,
9
+ PatchingModule,
10
+ PushedModule,
11
+ TraceSwap,
12
+ TracingModule,
13
+ is_tracing,
14
+ )
15
+
16
+
17
+ def overridefn(*a, **kw):
18
+ assert a[0] == 42
19
+ return 2
20
+
21
+
22
+ def examplefn(x: int, *a, **kw) -> int:
23
+ return 1
24
+
25
+
26
+ def overridemethod(*a, **kw):
27
+ assert a[1] == 42
28
+ return 2
29
+
30
+
31
+ class Example:
32
+ def example_method(self, a: int, **kw) -> int:
33
+ return 1
34
+
35
+
36
+ tracer = CompositeTracer()
37
+
38
+ tracer.push_module(
39
+ PatchingModule(
40
+ {
41
+ examplefn: overridefn,
42
+ Example.__dict__["example_method"]: overridemethod,
43
+ tuple.__len__: (lambda a: 42),
44
+ }
45
+ )
46
+ )
47
+
48
+
49
+ @pytest.fixture(autouse=True)
50
+ def check_tracer_state():
51
+ assert not is_tracing()
52
+ assert not tracer.ctracer.enabled()
53
+ yield None
54
+ assert not is_tracing()
55
+ assert not tracer.ctracer.enabled()
56
+
57
+
58
+ def test_CALL_FUNCTION():
59
+ with tracer:
60
+ assert examplefn(42) == 2
61
+
62
+
63
+ def test_CALL_FUNCTION_KW():
64
+ with tracer:
65
+ assert examplefn(42, option=1) == 2
66
+
67
+
68
+ def test_CALL_FUNCTION_EX():
69
+ with tracer:
70
+ a = (42, 1, 2, 3)
71
+ assert examplefn(*a, option=1) == 2
72
+
73
+
74
+ def test_CALL_METHOD():
75
+ with tracer:
76
+ assert Example().example_method(42) == 2
77
+
78
+
79
+ def test_override_method_in_c():
80
+ with tracer:
81
+ assert (1, 2, 3).__len__() == 42
82
+
83
+
84
+ def test_no_tracing():
85
+ with tracer:
86
+ # TraceSwap(tracer.ctracer, True) is the same as NoTracing() for `tracer`:
87
+ with TraceSwap(tracer.ctracer, True):
88
+ assert (1, 2, 3).__len__() == 3
89
+
90
+
91
+ def test_measure_fn_coverage() -> None:
92
+ def called_by_foo(x: int) -> int:
93
+ return x
94
+
95
+ def foo(x: int) -> int:
96
+ if called_by_foo(x) < 50:
97
+ return x
98
+ else:
99
+ return (x - 50) + (called_by_foo(2 + 1) > 3) + -abs(x)
100
+
101
+ def calls_foo(x: int) -> int:
102
+ return foo(x)
103
+
104
+ cov1 = CoverageTracingModule(foo)
105
+ cov2 = CoverageTracingModule(foo)
106
+ cov3 = CoverageTracingModule(foo)
107
+ with COMPOSITE_TRACER:
108
+ with PushedModule(cov1):
109
+ calls_foo(5)
110
+
111
+ with PushedModule(cov2):
112
+ calls_foo(100)
113
+
114
+ with PushedModule(cov3):
115
+ calls_foo(5)
116
+ calls_foo(100)
117
+
118
+ assert 0.4 > cov1.get_results().opcode_coverage > 0.1
119
+ assert 0.95 > cov2.get_results().opcode_coverage > 0.6
120
+ # Note that we can't get 100% - there's an extra "return None"
121
+ # at the end that's unreachable.
122
+ assert cov3.get_results().opcode_coverage > 0.85
123
+
124
+
125
+ class Explode(ValueError):
126
+ pass
127
+
128
+
129
+ class ExplodingModule(TracingModule):
130
+ opcodes_wanted = frozenset(
131
+ [
132
+ dis.opmap.get("BINARY_ADD", 256),
133
+ dis.opmap.get("BINARY_OP", 256), # on >3.11
134
+ ]
135
+ )
136
+ was_called = False
137
+
138
+ def __call__(self, frame, codeobj, codenum):
139
+ self.was_called = True
140
+ raise Explode("I explode")
141
+
142
+
143
+ def test_tracer_propagates_errors():
144
+ mod = ExplodingModule()
145
+ COMPOSITE_TRACER.push_module(mod)
146
+ try:
147
+ with COMPOSITE_TRACER:
148
+ x, y = 1, 3
149
+ print(x + y)
150
+ except Explode:
151
+ pass
152
+ else:
153
+ assert mod.was_called
154
+ COMPOSITE_TRACER.pop_config(mod)