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.
Files changed (176) hide show
  1. _crosshair_tracers.cpython-312-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 +155 -0
  18. crosshair/copyext_test.py +84 -0
  19. crosshair/core.py +1763 -0
  20. crosshair/core_and_libs.py +149 -0
  21. crosshair/core_regestered_types_test.py +82 -0
  22. crosshair/core_test.py +1316 -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 +5228 -0
  62. crosshair/libimpl/builtinslib_ch_test.py +1191 -0
  63. crosshair/libimpl/builtinslib_test.py +3735 -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 +2559 -0
  72. crosshair/libimpl/datetimelib_ch_test.py +354 -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 +178 -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 +261 -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 +1165 -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 +1199 -0
  151. crosshair/statespace_test.py +108 -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 +544 -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 +741 -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.99.dist-info/METADATA +144 -0
  172. crosshair_tool-0.0.99.dist-info/RECORD +176 -0
  173. crosshair_tool-0.0.99.dist-info/WHEEL +6 -0
  174. crosshair_tool-0.0.99.dist-info/entry_points.txt +3 -0
  175. crosshair_tool-0.0.99.dist-info/licenses/LICENSE +93 -0
  176. crosshair_tool-0.0.99.dist-info/top_level.txt +2 -0
@@ -0,0 +1,271 @@
1
+ import math
2
+ from collections import defaultdict
3
+ from typing import Counter, Dict, List, Optional, Sequence, Tuple
4
+
5
+ from z3 import ExprRef # type: ignore
6
+
7
+ from crosshair.statespace import (
8
+ AbstractPathingOracle,
9
+ DetachedPathNode,
10
+ ModelValueNode,
11
+ NodeLike,
12
+ RootNode,
13
+ SearchTreeNode,
14
+ StateSpace,
15
+ WorstResultNode,
16
+ )
17
+ from crosshair.util import CrossHairInternal, debug, in_debug
18
+ from crosshair.z3util import z3And, z3Not, z3Or
19
+
20
+ CodeLoc = Tuple[str, ...]
21
+
22
+
23
+ class CoveragePathingOracle(AbstractPathingOracle):
24
+ """
25
+ A heuristic that attempts to target under-explored code locations.
26
+
27
+ Code condition counts:
28
+ {code pos: {condition => count}}
29
+ Conditions should be
30
+ a count of conditions
31
+ that lead to the code location
32
+ on some previously explored path
33
+
34
+ When beginning an iteration:
35
+ Pick a code location to target, biasing for those with few visits.
36
+ Bias our decisions based on piror ones that led to the target location.
37
+
38
+ Risks:
39
+ * Coupled decisions aren't understood, e.g. "a xor b" is challenging with underexplored
40
+ leaves in both branches.
41
+ * We may target code locations that are impossible to reach because we've exhausted
42
+ every path that leads to them. (TODO: visit frequency may not be the only appropriate
43
+ metric for selecting target locations)
44
+ * Biases for now-impossible branches "bleed" over into our path.
45
+ This sounds like a problem, but is at least partly intentional - we may hope to reach
46
+ some of the same code locations under different early decisions.
47
+
48
+ """
49
+
50
+ def __init__(self):
51
+ self.visits = Counter[CodeLoc]()
52
+ self.iters_since_discovery = 0
53
+ self.summarized_positions: Dict[CodeLoc, Counter[int]] = defaultdict(Counter)
54
+ self.current_path_probabilities: Dict[ExprRef, float] = {}
55
+ self.internalized_expressions: Tuple[Dict[ExprRef, int], Dict[int, int]] = (
56
+ {},
57
+ {},
58
+ )
59
+
60
+ # TODO: This falls apart for moderately sized with_equal_probabilities
61
+ # because that has many small probability decisions.
62
+ # (even just a 10% change could be much larger than it would be otherwise)
63
+ _delta_probabilities = {-1: 0.1, 0: 0.25, 1: 0.9}
64
+
65
+ def pre_path_hook(self, space: StateSpace) -> None:
66
+ root = space._root
67
+ visits = self.visits
68
+ _delta_probabilities = self._delta_probabilities
69
+
70
+ tweaks: Dict[ExprRef, int] = defaultdict(int)
71
+ rand = root._random
72
+ nondiscovery_iters = self.iters_since_discovery
73
+ summarized_positions = self.summarized_positions
74
+ num_positions = len(summarized_positions)
75
+ recent_discovery = rand.random() > nondiscovery_iters / (nondiscovery_iters + 3)
76
+ if recent_discovery or not num_positions:
77
+ debug("No coverage biasing in effect. (", num_positions, " code locations)")
78
+ self.current_path_probabilities = {}
79
+ return
80
+
81
+ options = list(summarized_positions.items())
82
+ options.sort(key=lambda pair: visits[pair[0]])
83
+ chosen_index = int((root._random.random() ** 2.5) * num_positions)
84
+ (loc, exprs) = options[chosen_index]
85
+ for expr in exprs.keys():
86
+ if expr >= 0:
87
+ tweaks[expr] += 1
88
+ else:
89
+ tweaks[-expr] -= 1
90
+ probabilities = {
91
+ expr: _delta_probabilities[delta] for expr, delta in tweaks.items()
92
+ }
93
+ if in_debug():
94
+ debug("Coverage biasing for code location:", loc)
95
+ debug("(", num_positions, " locations presently known)")
96
+ expr_id_map, _ = self.internalized_expressions
97
+ for expr, exprid in expr_id_map.items():
98
+ probability = probabilities.get(exprid)
99
+ if probability:
100
+ debug("coverage tweaked probability", expr, probability)
101
+ self.current_path_probabilities = probabilities
102
+
103
+ def internalize(self, expr):
104
+ expr_id, id_id = self.internalized_expressions
105
+ myid = id(expr)
106
+ unified_id = id_id.get(myid)
107
+ if unified_id:
108
+ return unified_id
109
+ unified_id = expr_id.get(expr)
110
+ if unified_id:
111
+ id_id[myid] = unified_id
112
+ return unified_id
113
+ expr_id[expr] = myid
114
+ id_id[myid] = myid
115
+ return myid
116
+
117
+ def post_path_hook(self, path: Sequence[SearchTreeNode]) -> None:
118
+ leading_locs = []
119
+ leading_conditions: List[int] = []
120
+ for step, node in enumerate(path[:-1]):
121
+ if not isinstance(node, NodeLike):
122
+ continue
123
+ if isinstance(node, WorstResultNode):
124
+ key = node.stacktail
125
+ if (key not in leading_locs) and (not isinstance(node, ModelValueNode)):
126
+ self.summarized_positions[key] += Counter(leading_conditions)
127
+ leading_locs.append(key)
128
+ next_node = path[step + 1]
129
+ if isinstance(next_node, DetachedPathNode):
130
+ break
131
+ if step + 1 < len(path):
132
+ (is_positive, root_expr) = node.normalized_expr
133
+ expr_signature = (
134
+ self.internalize(root_expr)
135
+ if is_positive
136
+ else -self.internalize(root_expr)
137
+ )
138
+ if next_node == node.positive:
139
+ leading_conditions.append(expr_signature)
140
+ elif next_node == node.negative:
141
+ leading_conditions.append(-expr_signature)
142
+ else:
143
+ raise CrossHairInternal(
144
+ f"{type(path[step])} was followed by {type(path[step+1])}"
145
+ )
146
+ visits = self.visits
147
+ prev_len = len(visits)
148
+ visits += Counter(leading_locs)
149
+ if len(visits) > prev_len:
150
+ self.iters_since_discovery = 0
151
+ else:
152
+ self.iters_since_discovery += 1
153
+
154
+ def decide(
155
+ self,
156
+ root: RootNode,
157
+ node: "WorstResultNode",
158
+ engine_probability: Optional[float],
159
+ ) -> float:
160
+ if engine_probability in (0.0, 1.0): # is not None:
161
+ return engine_probability
162
+ default_probability = 0.25
163
+
164
+ path_probabilities = self.current_path_probabilities
165
+ is_positive, n_expr = node.normalized_expr
166
+ n_expr_id = self.internalize(n_expr)
167
+ if n_expr_id not in path_probabilities:
168
+ return (
169
+ default_probability
170
+ if engine_probability is None
171
+ else engine_probability
172
+ )
173
+ true_probability = path_probabilities[n_expr_id]
174
+ return true_probability if is_positive else 1.0 - true_probability
175
+
176
+
177
+ class BreadthFirstPathingOracle(AbstractPathingOracle):
178
+ def decide(
179
+ self,
180
+ root: RootNode,
181
+ node: "WorstResultNode",
182
+ engine_probability: Optional[float],
183
+ ) -> float:
184
+ branch_counter = root._open_coverage[node.stacktail]
185
+
186
+ # If we've never taken a branch at this code location, make sure we try it!
187
+ if bool(branch_counter.pos_ct) != bool(branch_counter.neg_ct):
188
+ if engine_probability != 0.0 and engine_probability != 1.0:
189
+ return 1.0 if branch_counter.neg_ct else 0.0
190
+ if engine_probability is None:
191
+ engine_probability = 0.25
192
+ if engine_probability != 0.0 and engine_probability != 1.0:
193
+ if branch_counter.pos_ct > branch_counter.neg_ct * 2 + 1:
194
+ engine_probability /= 2.0
195
+ elif branch_counter.neg_ct > branch_counter.pos_ct * 2 + 1:
196
+ engine_probability = (1.0 + engine_probability) / 2.0
197
+ return engine_probability
198
+
199
+
200
+ class PreferNegativeOracle(AbstractPathingOracle):
201
+ def decide(
202
+ self,
203
+ root: RootNode,
204
+ node: "WorstResultNode",
205
+ engine_probability: Optional[float],
206
+ ) -> float:
207
+ # When both paths are unexplored, we bias for False.
208
+ # As a heuristic, this tends to prefer early completion:
209
+ # - Loop conditions tend to repeat on True.
210
+ # - Optional[X] turns into Union[X, None] and False conditions
211
+ # biases for the last item in the union.
212
+ # We pick a False value more than 2/3rds of the time to avoid
213
+ # explosions while constructing binary-tree-like objects.
214
+ if engine_probability is not None:
215
+ return engine_probability
216
+ return 0.25
217
+
218
+
219
+ class ConstrainedOracle(AbstractPathingOracle):
220
+ """
221
+ A pathing oracle that prefers to take a path that satisfies
222
+ explicitly provided constraints.
223
+ """
224
+
225
+ def __init__(self, inner_oracle: AbstractPathingOracle):
226
+ self.inner_oracle = inner_oracle
227
+ self.exprs: List[ExprRef] = []
228
+
229
+ def prefer(self, expr: ExprRef):
230
+ self.exprs.append(expr)
231
+
232
+ def pre_path_hook(self, space: StateSpace) -> None:
233
+ self.space = space
234
+ self.exprs = []
235
+ self.inner_oracle.pre_path_hook(space)
236
+
237
+ def post_path_hook(self, path: Sequence["SearchTreeNode"]) -> None:
238
+ self.inner_oracle.post_path_hook(path)
239
+
240
+ def decide(
241
+ self, root, node: "WorstResultNode", engine_probability: Optional[float]
242
+ ) -> float:
243
+ # We always run the inner oracle in case it's tracking something about the path.
244
+ default_probability = self.inner_oracle.decide(root, node, engine_probability)
245
+ if not self.space.is_possible(z3And(*[node.expr, *self.exprs])):
246
+ return 0.0
247
+ elif not self.space.is_possible(z3And(*[z3Not(node.expr), *self.exprs])):
248
+ return 1.0
249
+ else:
250
+ return default_probability
251
+
252
+
253
+ class RotatingOracle(AbstractPathingOracle):
254
+ def __init__(self, oracles: List[AbstractPathingOracle]):
255
+ self.oracles = oracles
256
+ self.index = -1
257
+
258
+ def pre_path_hook(self, space: StateSpace) -> None:
259
+ oracles = self.oracles
260
+ self.index = (self.index + 1) % len(oracles)
261
+ for oracle in oracles:
262
+ oracle.pre_path_hook(space)
263
+
264
+ def post_path_hook(self, path: Sequence["SearchTreeNode"]) -> None:
265
+ for oracle in self.oracles:
266
+ oracle.post_path_hook(path)
267
+
268
+ def decide(
269
+ self, root, node: "WorstResultNode", engine_probability: Optional[float]
270
+ ) -> float:
271
+ return self.oracles[self.index].decide(root, node, engine_probability)
@@ -0,0 +1,21 @@
1
+ import random
2
+
3
+ import z3 # type: ignore
4
+
5
+ from crosshair.pathing_oracle import ConstrainedOracle, PreferNegativeOracle
6
+ from crosshair.statespace import RootNode, SimpleStateSpace, WorstResultNode
7
+
8
+
9
+ def test_constrained_oracle():
10
+ oracle = ConstrainedOracle(PreferNegativeOracle())
11
+ x = z3.Int("x")
12
+ root = RootNode()
13
+ space = SimpleStateSpace()
14
+ oracle.pre_path_hook(space)
15
+ oracle.prefer(x >= 7)
16
+ rand = random.Random()
17
+ assert oracle.decide(root, WorstResultNode(rand, x < 7, space.solver), None) == 0.0
18
+ assert oracle.decide(root, WorstResultNode(rand, x >= 3, space.solver), None) == 1.0
19
+ assert (
20
+ oracle.decide(root, WorstResultNode(rand, x == 7, space.solver), None) == 0.25
21
+ )
@@ -0,0 +1,27 @@
1
+ import sys
2
+ from contextlib import contextmanager
3
+ from dataclasses import dataclass
4
+ from importlib.machinery import FileFinder
5
+ from typing import Any, Callable
6
+
7
+
8
+ @dataclass
9
+ class PreferPureLoaderHook:
10
+ orig_hook: Callable[[str], Any]
11
+
12
+ def __call__(self, path: str):
13
+ finder = self.orig_hook(path)
14
+ if isinstance(finder, FileFinder):
15
+ # Move pure python file loaders to the front
16
+ finder._loaders.sort(key=lambda pair: 0 if pair[0] in (".py", ".pyc") else 1) # type: ignore
17
+ return finder
18
+
19
+
20
+ @contextmanager
21
+ def prefer_pure_python_imports():
22
+ sys.path_hooks = [PreferPureLoaderHook(h) for h in sys.path_hooks]
23
+ sys.path_importer_cache.clear()
24
+ yield
25
+ assert all(isinstance(h, PreferPureLoaderHook) for h in sys.path_hooks)
26
+ sys.path_hooks = [h.orig_hook for h in sys.path_hooks]
27
+ sys.path_importer_cache.clear()
@@ -0,0 +1,16 @@
1
+ import importlib
2
+ import sys
3
+ from contextlib import contextmanager
4
+
5
+ import pytest
6
+
7
+ from crosshair.pure_importer import prefer_pure_python_imports
8
+
9
+
10
+ @pytest.mark.skip(
11
+ reason="We used to test pydantic here, but current version doesn't use Cython"
12
+ )
13
+ def test_prefer_pure_python_imports():
14
+ with prefer_pure_python_imports():
15
+ pydantic = importlib.import_module("pydantic")
16
+ assert not pydantic.compiled
crosshair/py.typed ADDED
File without changes
@@ -0,0 +1,273 @@
1
+ """API for registering contracts for external libraries."""
2
+
3
+ from dataclasses import dataclass
4
+ from inspect import Parameter, Signature, getmodule, ismethod, signature
5
+ from types import MethodDescriptorType, ModuleType, WrapperDescriptorType
6
+ from typing import Callable, Dict, FrozenSet, List, Optional, Set, Union
7
+ from weakref import ReferenceType
8
+
9
+ from crosshair.fnutil import resolve_signature
10
+ from crosshair.stubs_parser import signature_from_stubs
11
+ from crosshair.util import debug, warn
12
+
13
+ # TODO: One might want to add more features to overloading. Currently contracts only
14
+ # support multiple signatures, but not pre- and postconditions depending on the
15
+ # overload. REGISTERED_CONTRACTS might become Dict[Callable, List[ContractOverride]].
16
+
17
+
18
+ @dataclass
19
+ class ContractOverride:
20
+ pre: Optional[Callable[..., bool]]
21
+ post: Optional[Callable[..., bool]]
22
+ sigs: List[Signature]
23
+ skip_body: bool
24
+ # TODO: Once supported, we might want to register Exceptions ("raises") as well
25
+
26
+
27
+ REGISTERED_CONTRACTS: Dict[Callable, ContractOverride] = {}
28
+ REGISTERED_MODULES: Set[ModuleType] = set()
29
+
30
+ # Don't automatically register those functions.
31
+ _NO_AUTO_REGISTER: FrozenSet[str] = frozenset(
32
+ {
33
+ "__init__",
34
+ "__init_subclass__",
35
+ "__new__",
36
+ }
37
+ )
38
+
39
+
40
+ def required_param_names(sig: Signature) -> Set[str]:
41
+ return {k for (k, v) in sig.parameters.items() if v.default is Parameter.empty}
42
+
43
+
44
+ def _verify_signatures(
45
+ fn: Callable,
46
+ contract: ContractOverride,
47
+ ref_sig: Optional[Signature],
48
+ ) -> bool:
49
+ """Verify the provided signatures (including signatures of `pre` and `post`)."""
50
+ success = True
51
+ if ref_sig:
52
+ all_sigs = contract.sigs.copy()
53
+ all_sigs.append(ref_sig)
54
+ else:
55
+ all_sigs = contract.sigs
56
+ for sig in all_sigs:
57
+ params = set(sig.parameters.keys())
58
+ # First verify the parameters against the reference sig.
59
+ if ref_sig:
60
+ ref_params = set(ref_sig.parameters.keys())
61
+ # Cannot test for strict equality, because of overloads.
62
+ if not required_param_names(sig) <= ref_params:
63
+ warn(
64
+ f"Malformed signature for function {fn.__name__}. "
65
+ f"Expected parameters: {ref_params}, found: {params}",
66
+ )
67
+ success = False
68
+ # Verify the signature of the precondition.
69
+ if contract.pre:
70
+ pre_params = required_param_names(signature(contract.pre))
71
+ if not pre_params <= params:
72
+ warn(
73
+ f"Malformated precondition for function {fn.__name__}. "
74
+ f"Unexpected arguments: {pre_params - params}"
75
+ )
76
+ success = False
77
+ # Verify the signature of the postcondition.
78
+ if contract.post:
79
+ post_params = required_param_names(signature(contract.post))
80
+ params.add("__return__")
81
+ params.add("__old__")
82
+ if not post_params <= params:
83
+ warn(
84
+ f"Malformated postcondition for function {fn.__name__}. "
85
+ f"Unexpected parameters: {post_params - params}."
86
+ )
87
+ success = False
88
+ return success
89
+
90
+
91
+ def _add_contract(
92
+ fn: Callable,
93
+ contract: ContractOverride,
94
+ ref_sig: Optional[Signature],
95
+ ) -> bool:
96
+ """Add a contract to the function and check for consistency."""
97
+ verified = _verify_signatures(fn, contract, ref_sig)
98
+ if not verified:
99
+ return False
100
+ old_contract = REGISTERED_CONTRACTS.get(fn)
101
+ if old_contract:
102
+ if (
103
+ old_contract.pre == contract.pre
104
+ and old_contract.post == contract.post
105
+ and old_contract.skip_body == contract.skip_body
106
+ ):
107
+ old_contract.sigs.extend(contract.sigs)
108
+ else:
109
+ warn(
110
+ "Pre- and postconditons and skip_body should not differ when "
111
+ f"registering multiple contracts for the same function: {fn.__name__}."
112
+ )
113
+ return False
114
+ else:
115
+ REGISTERED_CONTRACTS[fn] = contract
116
+ return True
117
+
118
+
119
+ def _internal_register_contract(
120
+ fn: Callable,
121
+ pre: Optional[Callable[..., bool]] = None,
122
+ post: Optional[Callable[..., bool]] = None,
123
+ sig: Union[Signature, List[Signature], None] = None,
124
+ skip_body: bool = True,
125
+ ) -> bool:
126
+ reference_sig = None
127
+
128
+ sig_or_error = resolve_signature(fn)
129
+ if isinstance(sig_or_error, Signature):
130
+ reference_sig = sig_or_error
131
+
132
+ # In the case the signature is incomplete, look in the stubs.
133
+ if not sig and (
134
+ not reference_sig or reference_sig.return_annotation == Parameter.empty
135
+ ):
136
+ sigs, is_valid = signature_from_stubs(fn)
137
+ if sigs:
138
+ debug(f"Found {str(len(sigs))} signature(s) for {fn.__name__} in stubs")
139
+ if not is_valid or any(
140
+ sig.return_annotation == Parameter.empty for sig in sigs
141
+ ):
142
+ warn(
143
+ f"Incomplete signature for {fn.__name__} in stubs, consider "
144
+ f"registering the signature manually. Signatures found: "
145
+ f"{str(sigs)}"
146
+ )
147
+ contract = ContractOverride(pre, post, sigs, skip_body)
148
+ return _add_contract(fn, contract, reference_sig)
149
+ else:
150
+ if reference_sig:
151
+ warn(
152
+ f"No valid signature found for function {fn.__name__}, "
153
+ "consider registering the signature manually."
154
+ )
155
+ contract = ContractOverride(pre, post, [], skip_body)
156
+ return _add_contract(fn, contract, reference_sig)
157
+ # No signature available at all, we cannot register the function.
158
+ else:
159
+ warn(
160
+ f"Could not automatically register {fn.__name__}, reason: no "
161
+ "signature found."
162
+ )
163
+ return False
164
+ else:
165
+ # Verify the contract and register it.
166
+ if sig is None:
167
+ sig = []
168
+ elif isinstance(sig, Signature):
169
+ sig = [sig]
170
+ contract = ContractOverride(pre, post, sig, skip_body)
171
+ return _add_contract(fn, contract, reference_sig)
172
+
173
+
174
+ def register_contract(
175
+ fn: Callable,
176
+ *,
177
+ pre: Optional[Callable[..., bool]] = None,
178
+ post: Optional[Callable[..., bool]] = None,
179
+ sig: Union[Signature, List[Signature], None] = None,
180
+ skip_body: bool = True,
181
+ ) -> bool:
182
+ """
183
+ Register a contract for the given function.
184
+
185
+ :param fn: The function to add a contract for.
186
+ :param pre: The preconditon which should hold when entering the function.
187
+ :param post: The postcondition which should hold when returning from the function.
188
+ :param sig: If provided, CrossHair will use this signature for the function.\
189
+ Usefull for manually providing type annotation. You can provide multiple\
190
+ signatures for overloaded functions.
191
+ :param skip_body: By default registered functions will be skipped executing,\
192
+ assuming the postconditions hold. Set this to `False` to still execute the body.
193
+ :returns: A boolean indicating whether the contract was successfully registered.
194
+ """
195
+ # Don't allow registering bound methods.
196
+ if ismethod(fn):
197
+ cls = getattr(getattr(fn, "__self__", None), "__class__", "<not found>")
198
+ warn(
199
+ f"You registered the bound method {fn}. You should register the unbound "
200
+ f"function of the class {cls} instead."
201
+ )
202
+ return False
203
+ return _internal_register_contract(fn, pre, post, sig, skip_body)
204
+
205
+
206
+ def clear_contract_registrations():
207
+ global REGISTERED_CONTRACTS
208
+ REGISTERED_CONTRACTS.clear()
209
+ global REGISTERED_MODULES
210
+ REGISTERED_MODULES.clear()
211
+
212
+
213
+ def get_contract(fn: Callable) -> Optional[ContractOverride]:
214
+ """
215
+ Get the contract associated to the given function, it the function was registered.
216
+
217
+ :param fn: The function to retrieve the contract for.
218
+ :return: The contract associated with the function or None if the function was not\
219
+ registered.
220
+ """
221
+ # Weak references are not hashable: REGISTERED_CONTRACTS.get(fn) fails.
222
+ if isinstance(fn, ReferenceType):
223
+ return None
224
+ # Return the registered contract for the function, if any.
225
+ contract = REGISTERED_CONTRACTS.get(fn)
226
+ if contract:
227
+ return contract
228
+ fn_name = getattr(fn, "__name__", None)
229
+ # Some functions should not be automatically registered.
230
+ if fn_name in _NO_AUTO_REGISTER:
231
+ return None
232
+
233
+ # If the function belongs to a registered module, register it.
234
+ module = getmodule(fn)
235
+ # If this is a classmethod, look for the module in the mro.
236
+ fn_self = getattr(fn, "__self__", None)
237
+ if fn_name and fn_self and isinstance(fn_self, type):
238
+ for mro in fn_self.mro():
239
+ if fn_name in mro.__dict__:
240
+ module_name = mro.__module__
241
+ if module_name in map(lambda x: x.__name__, REGISTERED_MODULES):
242
+ _internal_register_contract(fn)
243
+ return REGISTERED_CONTRACTS.get(fn)
244
+ return None
245
+ if module and module in REGISTERED_MODULES:
246
+ _internal_register_contract(fn)
247
+ return REGISTERED_CONTRACTS.get(fn)
248
+ # Some builtins and some C functions are wrapped into Descriptors
249
+ if isinstance(fn, (MethodDescriptorType, WrapperDescriptorType)):
250
+ module_name = fn.__objclass__.__module__
251
+ if module_name in map(lambda x: x.__name__, REGISTERED_MODULES):
252
+ _internal_register_contract(fn)
253
+ return REGISTERED_CONTRACTS.get(fn)
254
+ return None
255
+
256
+
257
+ def register_modules(*modules: ModuleType) -> None:
258
+ """
259
+ Specify a module whose functions should all be skipped at execution.
260
+
261
+ THIS IS AN EXPERIMENTAL FEATURE!
262
+ Registering a whole module might be too much in some cases and you might fallback to
263
+ register individual functions instead.
264
+
265
+ Note that functions `__init__`, `__init_subclass__` and `__new__` are never
266
+ registered automatically.
267
+
268
+ If you wish to register all functions, except function `foo`, register that function
269
+ manually with the option `skip_body=False`.
270
+
271
+ :param modules: one or multiple modules whose functions should be skipped.
272
+ """
273
+ REGISTERED_MODULES.update(modules)