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,1237 @@
1
+ import ast
2
+ import collections
3
+ import contextlib
4
+ import enum
5
+ import inspect
6
+ import re
7
+ import sys
8
+ import textwrap
9
+ import traceback
10
+ import types
11
+ from dataclasses import dataclass, replace
12
+ from functools import partial, wraps
13
+ from inspect import BoundArguments, Signature
14
+ from itertools import chain
15
+ from typing import (
16
+ Any,
17
+ Callable,
18
+ ContextManager,
19
+ Dict,
20
+ FrozenSet,
21
+ Iterable,
22
+ Iterator,
23
+ List,
24
+ Mapping,
25
+ MutableMapping,
26
+ Optional,
27
+ Sequence,
28
+ Set,
29
+ Tuple,
30
+ Type,
31
+ cast,
32
+ )
33
+
34
+ try:
35
+ import icontract # type: ignore
36
+ except ModuleNotFoundError:
37
+ icontract = None # type: ignore
38
+
39
+ try:
40
+ import deal # type: ignore
41
+ except ModuleNotFoundError:
42
+ deal = None # type: ignore
43
+
44
+ from crosshair.auditwall import opened_auditwall
45
+ from crosshair.fnutil import FunctionInfo, fn_globals, set_first_arg_type
46
+ from crosshair.options import AnalysisKind
47
+ from crosshair.register_contract import get_contract
48
+ from crosshair.tracers import NoTracing
49
+ from crosshair.util import (
50
+ CrossHairInternal,
51
+ DynamicScopeVar,
52
+ EvalFriendlyReprContext,
53
+ IdKeyedDict,
54
+ debug,
55
+ eval_friendly_repr,
56
+ format_boundargs,
57
+ frame_summary_for_fn,
58
+ is_pure_python,
59
+ sourcelines,
60
+ )
61
+
62
+
63
+ class ConditionExprType(enum.Enum):
64
+ INVARIANT = "invariant"
65
+ PRECONDITION = "precondition"
66
+ POSTCONDITION = "postcondition"
67
+
68
+ def __str__(self):
69
+ return self.value
70
+
71
+
72
+ # For convience
73
+ INVARIANT = ConditionExprType.INVARIANT
74
+ PRECONDITION = ConditionExprType.PRECONDITION
75
+ POSTCONDITION = ConditionExprType.POSTCONDITION
76
+
77
+
78
+ class NoEnforce:
79
+ """
80
+ Signal to suppress contract enforcement.
81
+
82
+ This function wrapper does nothing on its own. But the enforcement tracer
83
+ looks for it and will skip conditions on `fn` when this wrapper is detected.
84
+ """
85
+
86
+ def __init__(self, fn):
87
+ self.fn = fn
88
+
89
+ def __call__(self, *a, **kw) -> object:
90
+ return self.fn(*a, **kw)
91
+
92
+
93
+ def strip_comment_line(line: str) -> str:
94
+ line = line.strip()
95
+ if line.startswith("'''") or line.startswith('"""'):
96
+ line = line[3:]
97
+ if line.endswith("'''") or line.endswith('"""'):
98
+ line = line[:-3]
99
+ return line.strip()
100
+
101
+
102
+ def get_doc_lines(thing: object) -> Iterable[Tuple[int, str]]:
103
+ _filename, line_num, lines = sourcelines(thing) # type:ignore
104
+ if not lines:
105
+ return
106
+ try:
107
+ module = ast.parse(textwrap.dedent("".join(lines)))
108
+ except SyntaxError:
109
+ debug(f"Unable to parse {thing} into an AST; will not detect PEP316 contracts.")
110
+ return
111
+ fndef = module.body[0]
112
+ if not isinstance(fndef, (ast.ClassDef, ast.FunctionDef)):
113
+ return
114
+ firstnode = fndef.body[0]
115
+ if not isinstance(firstnode, ast.Expr):
116
+ return
117
+ strnode = firstnode.value
118
+ if not (isinstance(strnode, ast.Constant) and isinstance(strnode.value, str)):
119
+ return
120
+ end_lineno = getattr(strnode, "end_lineno", None)
121
+ if end_lineno is not None:
122
+ candidates = enumerate(lines[strnode.lineno - 1 : end_lineno])
123
+ line_num += strnode.lineno - 1
124
+ else:
125
+ candidates = enumerate(lines[: strnode.lineno + 1])
126
+ OPEN_RE = re.compile("^\\s*r?('''|\"\"\")")
127
+ CLOSE_RE = re.compile("('''|\"\"\")\\s*(#.*)?$")
128
+ started = False
129
+ for idx, line in candidates:
130
+ if not started:
131
+ (line, replaced) = OPEN_RE.subn("", line)
132
+ if replaced:
133
+ started = True
134
+ if started:
135
+ (line, replaced) = CLOSE_RE.subn("", line)
136
+ yield (line_num + idx, line)
137
+ if replaced:
138
+ return
139
+
140
+
141
+ class ImpliesTransformer(ast.NodeTransformer):
142
+ """
143
+ Transform AST to rewrite implies operation.
144
+
145
+ Pre- and post-conditions commonly want an implies(X, Y) operation.
146
+ But it's important to only evaluate Y when X is true; so we rewrite
147
+ this function into "Y if X else True"
148
+ """
149
+
150
+ def visit_Call(self, node):
151
+ self.generic_visit(node)
152
+ if isinstance(node.func, ast.Name) and node.func.id == "implies":
153
+ if len(node.args) != 2:
154
+ raise SyntaxError("implies() must have exactly two arguments")
155
+ condition, implication = node.args
156
+ pos = {"lineno": node.lineno, "col_offset": node.col_offset}
157
+ return ast.IfExp(condition, implication, ast.Constant(True, **pos), **pos)
158
+ return node
159
+
160
+
161
+ def compile_expr(src: str) -> types.CodeType:
162
+ parsed = ast.parse(src, "<string>", "eval")
163
+ parsed = ImpliesTransformer().visit(parsed)
164
+ return compile(parsed, "<string>", "eval")
165
+
166
+
167
+ def default_counterexample(
168
+ fn_name: str,
169
+ bound_args: BoundArguments,
170
+ return_val: object,
171
+ repr_overrides: IdKeyedDict,
172
+ ) -> Tuple[str, str]:
173
+ from crosshair.tracers import ResumedTracing
174
+
175
+ with ResumedTracing(), EvalFriendlyReprContext(repr_overrides) as ctx:
176
+ args_string = format_boundargs(bound_args)
177
+ call_desc = f"{fn_name}({ctx.cleanup(args_string)})"
178
+ return (call_desc, eval_friendly_repr(return_val))
179
+
180
+
181
+ @dataclass()
182
+ class ConditionSyntaxMessage:
183
+ filename: str
184
+ line_num: int
185
+ message: str
186
+
187
+
188
+ @dataclass
189
+ class ConditionExpr:
190
+ condition_type: ConditionExprType
191
+ evaluate: Optional[Callable[[Mapping[str, object]], bool]]
192
+ filename: str
193
+ line: int
194
+ expr_source: str
195
+ compile_err: Optional[ConditionSyntaxMessage] = None
196
+
197
+ def __repr__(self):
198
+ return (
199
+ f"ConditionExpr(filename={self.filename!r}, "
200
+ f"line={self.line!r}, "
201
+ f"expr_source={self.expr_source!r}, "
202
+ f"compile_err={self.compile_err!r})"
203
+ )
204
+
205
+
206
+ @dataclass(frozen=True)
207
+ class Conditions:
208
+ """Describe the contract of a function."""
209
+
210
+ fn: Callable
211
+ """
212
+ The body of the function to analyze.
213
+ Ideally, this is just the body of the function and does not include checking
214
+ pre- or post-conditions. (though this is not always possible)
215
+ """
216
+
217
+ src_fn: Callable
218
+ """
219
+ The body of the function to use for error reporting. Usually the same as
220
+ `fn`, but sometimes the original is wrapped in shell for exception handling
221
+ or other reasons.
222
+ """
223
+
224
+ pre: Sequence[ConditionExpr]
225
+ """ The preconditions of the function. """
226
+
227
+ post: Sequence[ConditionExpr]
228
+ """ The postconditions of the function. """
229
+
230
+ raises: FrozenSet[Type[BaseException]]
231
+ """
232
+ A set of expection types that are expected.
233
+ Subtypes of expected exceptions are also considered to be expected.
234
+ CrossHair will attempt to report when this function raises an
235
+ unexpected exception.
236
+ """
237
+
238
+ sig: inspect.Signature
239
+ """
240
+ The signature of the funtion. Argument and return type
241
+ annotations should be resolved to real python types when possible.
242
+ """
243
+
244
+ # TODO: can mutation checking be implemented as just another kind of postcondition?
245
+ mutable_args: Optional[FrozenSet[str]]
246
+ """
247
+ A set of arguments that are deeply immutable.
248
+ When None, no assertion about mutability is provided.
249
+ OTOH, an empty set asserts that the function does not mutate any argument.
250
+ """
251
+
252
+ fn_syntax_messages: Sequence[ConditionSyntaxMessage]
253
+ """
254
+ A list of errors resulting from the parsing of the contract.
255
+ In general, conditions should not be checked when such messages exist.
256
+ """
257
+
258
+ counterexample_description_maker: Optional[
259
+ Callable[[BoundArguments, object, IdKeyedDict], Tuple[str, str]]
260
+ ] = None
261
+ """
262
+ An optional callback that formats a counterexample invocation as text.
263
+ It takes the example arguments and the returned value.
264
+ It returns string representations of the invocation and return value.
265
+ """
266
+
267
+ def has_any(self) -> bool:
268
+ return bool(self.pre or self.post or self.fn_syntax_messages)
269
+
270
+ def syntax_messages(self) -> Iterator[ConditionSyntaxMessage]:
271
+ for cond in chain(self.pre, self.post):
272
+ if cond.compile_err is not None:
273
+ yield cond.compile_err
274
+ yield from self.fn_syntax_messages
275
+
276
+ def format_counterexample(
277
+ self, args: BoundArguments, return_val: object, repr_overrides: IdKeyedDict
278
+ ) -> Tuple[str, str]:
279
+ if self.counterexample_description_maker is not None:
280
+ return self.counterexample_description_maker(
281
+ args, return_val, repr_overrides
282
+ )
283
+ return default_counterexample(
284
+ self.src_fn.__name__, args, return_val, repr_overrides
285
+ )
286
+
287
+
288
+ @dataclass(frozen=True)
289
+ class ClassConditions:
290
+ inv: List[ConditionExpr]
291
+ """
292
+ Invariants declared explicitly on the class.
293
+ Does not include invariants of superclasses.
294
+ """
295
+
296
+ methods: Mapping[str, Conditions]
297
+ """
298
+ Maps member names to the conditions for that member.
299
+
300
+ Conditions reflect not only what's directly declared to the method, but also:
301
+ * Conditions from superclass implementations of the same method.
302
+ * Conditions inferred from class invariants.
303
+ * Conditions inferred from superclass invariants.
304
+ """
305
+
306
+ def has_any(self) -> bool:
307
+ return bool(self.inv) or any(c.has_any() for c in self.methods.values())
308
+
309
+
310
+ def merge_fn_conditions(
311
+ sub_conditions: Conditions, super_conditions: Conditions
312
+ ) -> Conditions:
313
+
314
+ # TODO: resolve the warning below:
315
+ # (1) the type of self always changes
316
+ # (2) paramter renames (or *a, **kws) could result in varied bindings
317
+ if sub_conditions.sig is not None and sub_conditions.sig != super_conditions.sig:
318
+ debug(
319
+ "WARNING: inconsistent signatures", sub_conditions.sig, super_conditions.sig
320
+ )
321
+
322
+ pre = sub_conditions.pre if sub_conditions.pre else super_conditions.pre
323
+ post = list(chain(super_conditions.post, sub_conditions.post))
324
+ raises = sub_conditions.raises | super_conditions.raises
325
+ mutable_args = (
326
+ sub_conditions.mutable_args
327
+ if sub_conditions.mutable_args is not None
328
+ else super_conditions.mutable_args
329
+ )
330
+ fn = sub_conditions.fn
331
+ return Conditions(
332
+ fn,
333
+ fn,
334
+ pre,
335
+ post,
336
+ raises,
337
+ sub_conditions.sig,
338
+ mutable_args,
339
+ sub_conditions.fn_syntax_messages,
340
+ )
341
+
342
+
343
+ def merge_method_conditions(
344
+ class_conditions: List[ClassConditions],
345
+ ) -> Dict[str, Conditions]:
346
+ methods: Dict[str, Conditions] = {}
347
+ # reverse because mro searches left side first
348
+ for class_condition in reversed(class_conditions):
349
+ methods.update(class_condition.methods)
350
+ return methods
351
+
352
+
353
+ _HEADER_LINE = re.compile(
354
+ r"""^(\s*)\:? # whitespace with optional leading colon
355
+ ((?:post)|(?:pre)|(?:raises)|(?:inv)) # noncapturing keywords
356
+ (?:\[([\w\s\,\.]*)\])? # optional params in square brackets
357
+ \:\:?\s* # single or double colons
358
+ (.*?) # The (non-greedy) content
359
+ \s*$""",
360
+ re.VERBOSE,
361
+ )
362
+ _SECTION_LINE = re.compile(r"^(\s*)(.*?)\s*$")
363
+
364
+
365
+ @dataclass(init=False)
366
+ class SectionParse:
367
+ syntax_messages: List[ConditionSyntaxMessage]
368
+ sections: Dict[str, List[Tuple[int, str]]]
369
+ mutable_expr: Optional[str] = None
370
+
371
+ def __init__(self):
372
+ self.sections = collections.defaultdict(list)
373
+ self.syntax_messages = []
374
+
375
+
376
+ def has_expr(line: str) -> bool:
377
+ line = line.strip()
378
+ return bool(line) and not line.startswith("#")
379
+
380
+
381
+ def parse_sections(
382
+ lines: List[Tuple[int, str]], sections: Tuple[str, ...], filename: str
383
+ ) -> SectionParse:
384
+ parse = SectionParse()
385
+ cur_section: Optional[Tuple[str, int]] = None
386
+ for line_num, line in lines:
387
+ if line.strip() == "":
388
+ continue
389
+ if cur_section:
390
+ section, indent = cur_section
391
+ match = _SECTION_LINE.match(line)
392
+ if match:
393
+ this_indent = len(match.groups()[0])
394
+ if this_indent > indent:
395
+ if has_expr(match.groups()[1]):
396
+ parse.sections[section].append((line_num, match.groups()[1]))
397
+ # Still in the current section; continue:
398
+ continue
399
+ cur_section = None
400
+ match = _HEADER_LINE.match(line)
401
+ if match:
402
+ indentstr, section, bracketed, inline_expr = match.groups()
403
+ if section not in sections:
404
+ continue
405
+ if bracketed is not None:
406
+ if section != "post":
407
+ parse.syntax_messages.append(
408
+ ConditionSyntaxMessage(
409
+ filename,
410
+ line_num,
411
+ f"brackets not allowed in {section} section",
412
+ )
413
+ )
414
+ continue
415
+ if parse.mutable_expr is not None:
416
+ parse.syntax_messages.append(
417
+ ConditionSyntaxMessage(
418
+ filename, line_num, f"duplicate post section"
419
+ )
420
+ )
421
+ continue
422
+ else:
423
+ parse.mutable_expr = bracketed
424
+ if has_expr(inline_expr):
425
+ parse.sections[section].append((line_num, inline_expr))
426
+ continue
427
+ else:
428
+ cur_section = (section, len(indentstr))
429
+ return parse
430
+
431
+
432
+ class ConditionParser:
433
+ def get_fn_conditions(self, fn: FunctionInfo) -> Optional[Conditions]:
434
+ """
435
+ Return conditions declared (directly) on a function.
436
+
437
+ Does not include conditions inferred from invariants or superclasses.
438
+ Return None if it is impossible for this method to have conditions, even if
439
+ gained via subclass invariants. (i.e. `fn` is not a function or has no
440
+ signature)
441
+ """
442
+ raise NotImplementedError
443
+
444
+ def get_class_conditions(self, cls: type) -> ClassConditions:
445
+ raise NotImplementedError
446
+
447
+ def class_can_have_conditions(self, cls: type) -> bool:
448
+ raise NotImplementedError
449
+
450
+
451
+ class ConcreteConditionParser(ConditionParser):
452
+ def __init__(self, toplevel_parser: Optional[ConditionParser] = None):
453
+ if toplevel_parser is None:
454
+ toplevel_parser = self
455
+ self._toplevel_parser = toplevel_parser
456
+
457
+ def get_toplevel_parser(self):
458
+ return self._toplevel_parser
459
+
460
+ def get_class_invariants(self, cls: type) -> List[ConditionExpr]:
461
+ """
462
+ Return invariants declared explicitly on the given class.
463
+
464
+ Does not include invarants of superclasses.
465
+ """
466
+ raise NotImplementedError
467
+
468
+ def class_can_have_conditions(self, cls: type) -> bool:
469
+ # We can't get conditions/line numbers for classes written in C.
470
+ return is_pure_python(cls)
471
+
472
+ def get_class_conditions(self, cls: type) -> ClassConditions:
473
+ if not self.class_can_have_conditions(cls):
474
+ return ClassConditions([], {})
475
+
476
+ toplevel_parser = self.get_toplevel_parser()
477
+ methods = {}
478
+ super_methods = merge_method_conditions(
479
+ [toplevel_parser.get_class_conditions(base) for base in cls.__bases__]
480
+ )
481
+ inv = self.get_class_invariants(cls)
482
+ # TODO: consider the case where superclass defines methods w/o contracts and
483
+ # then subclass adds an invariant.
484
+ method_names = set(cls.__dict__.keys()) | super_methods.keys()
485
+ for method_name in method_names:
486
+ method = cls.__dict__.get(method_name, None)
487
+ super_method_conditions = super_methods.get(method_name)
488
+ if super_method_conditions is not None:
489
+ # Re-type the super's `self` argument to be this class:
490
+ revised_sig = set_first_arg_type(super_method_conditions.sig, cls)
491
+ super_method_conditions = replace(
492
+ super_method_conditions, sig=revised_sig
493
+ )
494
+ if method is None:
495
+ if super_method_conditions is None:
496
+ continue
497
+ else:
498
+ conditions: Conditions = super_method_conditions
499
+ else:
500
+ parsed_conditions = toplevel_parser.get_fn_conditions(
501
+ FunctionInfo.from_class(cls, method_name)
502
+ )
503
+ if parsed_conditions is None:
504
+ # debug(f'Skipping "{method_name}": Unable to determine the function signature.')
505
+ continue
506
+ if super_method_conditions is None:
507
+ conditions = parsed_conditions
508
+ else:
509
+ conditions = merge_fn_conditions(
510
+ parsed_conditions, super_method_conditions
511
+ )
512
+ # Selectively add conditions inferred from invariants:
513
+ final_pre = list(conditions.pre)
514
+ final_post = list(conditions.post)
515
+ if method_name in (
516
+ "__new__", # a staticmethod, but not isinstance(staticmethod)
517
+ "__repr__", # is itself required for reporting problems with invariants.
518
+ # [set/del]attr can do anything; we can't resonably enforce invariants:
519
+ "__setattr__",
520
+ "__delattr__",
521
+ "__replace__", # Will raise an exception with most arbitrary **kwargs.
522
+ "__annotate__", # a staticmethod, but not isinstance(staticmethod)
523
+ "__annotate_func__",
524
+ ):
525
+ pass
526
+ elif method_name == "__del__":
527
+ final_pre.extend(inv)
528
+ elif method_name == "__init__":
529
+ final_post.extend(inv)
530
+ elif method_name.startswith("__") and method_name.endswith("__"):
531
+ final_pre.extend(inv)
532
+ final_post.extend(inv)
533
+ elif method_name.startswith("_"):
534
+ pass
535
+ else:
536
+ final_pre.extend(inv)
537
+ final_post.extend(inv)
538
+ conditions = replace(conditions, pre=final_pre, post=final_post)
539
+ if conditions.has_any():
540
+ methods[method_name] = conditions
541
+
542
+ if inv and "__init__" not in methods:
543
+ # We assume that the default methods on `object` won't break invariants.
544
+ # Except `__init__`! That's what this conditional is for.
545
+
546
+ # Note that we don't check contracts on __init__ directly (but we do check
547
+ # them in while checking other contracts). Therefore, we're a little loose
548
+ # with the paramters (like signature) because many of them don't really
549
+ # matter.
550
+ initfn = getattr(cls, "__init__")
551
+ init_sig = inspect.signature(initfn)
552
+ methods["__init__"] = Conditions(
553
+ initfn, initfn, [], inv[:], frozenset(), init_sig, None, [], None
554
+ )
555
+ return ClassConditions(inv, methods)
556
+
557
+
558
+ class CompositeConditionParser(ConditionParser):
559
+ def __init__(self):
560
+ self.parsers = []
561
+ self.class_cache: Dict[type, ClassConditions] = {}
562
+
563
+ def get_toplevel_parser(self) -> ConditionParser:
564
+ return self
565
+
566
+ def get_fn_conditions(self, fn: FunctionInfo) -> Optional[Conditions]:
567
+ ret = None
568
+ for parser in self.parsers:
569
+ conditions = parser.get_fn_conditions(fn)
570
+ if conditions is not None:
571
+ ret = conditions
572
+ if conditions.has_any():
573
+ break
574
+ return ret
575
+
576
+ def get_class_conditions(self, cls: type) -> ClassConditions:
577
+ cached_ret = self.class_cache.get(cls)
578
+ if cached_ret is not None:
579
+ return cached_ret
580
+ ret = ClassConditions([], {})
581
+ # We skip the "typing" module because class condition computation fails for some
582
+ # typing classes:
583
+ if cls.__module__ != "typing":
584
+ for parser in self.parsers:
585
+ conditions = parser.get_class_conditions(cls)
586
+ if conditions.has_any():
587
+ ret = conditions
588
+ break
589
+ self.class_cache[cls] = ret
590
+ return ret
591
+
592
+
593
+ def condition_from_source_text(
594
+ condition_type: ConditionExprType,
595
+ filename: str,
596
+ line: int,
597
+ expr_source: str,
598
+ namespace: Dict[str, object],
599
+ ) -> ConditionExpr:
600
+ evaluate, compile_err = None, None
601
+ try:
602
+ compiled = compile_expr(expr_source)
603
+
604
+ def evaluatefn(bindings: Mapping[str, object]) -> bool:
605
+ # TODO: eval() is oddly expensive when tracing is on.
606
+ # Consider eval()ing this as an entire function.
607
+ return eval(compiled, {**namespace, **bindings})
608
+
609
+ evaluate = evaluatefn
610
+ except Exception:
611
+ e = sys.exc_info()[1]
612
+ compile_err = ConditionSyntaxMessage(filename, line, str(e))
613
+ return ConditionExpr(
614
+ condition_type=condition_type,
615
+ filename=filename,
616
+ line=line,
617
+ expr_source=expr_source,
618
+ evaluate=evaluate,
619
+ compile_err=compile_err,
620
+ )
621
+
622
+
623
+ _RAISE_SPHINX_RE = re.compile(
624
+ r"""
625
+ (?: ^ \s* \: raises \s+ ( [\w\.]+ ) \: ) |
626
+ (?: ^ \s* \:? raises \s* \: ( [^\r\n#]+ ) )
627
+ """,
628
+ re.MULTILINE | re.VERBOSE,
629
+ )
630
+
631
+
632
+ def parse_sphinx_raises(fn: Callable) -> Set[Type[BaseException]]:
633
+ raises: Set[Type[BaseException]] = set()
634
+ doc = getattr(fn, "__doc__", None)
635
+ if doc is None:
636
+ return raises
637
+ for group1, group2 in _RAISE_SPHINX_RE.findall(doc):
638
+ if group1:
639
+ excnamelist = [group1]
640
+ else:
641
+ excnamelist = group2.split(",")
642
+ for excname in excnamelist:
643
+ try:
644
+ exc_type = eval(excname, fn_globals(fn))
645
+ except Exception as e:
646
+ continue
647
+ if not isinstance(exc_type, type):
648
+ continue
649
+ if not issubclass(exc_type, BaseException):
650
+ continue
651
+ raises.add(exc_type)
652
+ return raises
653
+
654
+
655
+ class Pep316Parser(ConcreteConditionParser):
656
+ def get_fn_conditions(self, ctxfn: FunctionInfo) -> Optional[Conditions]:
657
+ fn_and_sig = ctxfn.get_callable()
658
+ if fn_and_sig is None:
659
+ return None
660
+ (fn, sig) = fn_and_sig
661
+ filename, first_fn_lineno, _lines = sourcelines(fn)
662
+ if isinstance(fn, types.BuiltinFunctionType):
663
+ return Conditions(fn, fn, [], [], frozenset(), sig, frozenset(), [])
664
+ lines = list(get_doc_lines(fn))
665
+ parse = parse_sections(lines, ("pre", "post"), filename)
666
+ pre: List[ConditionExpr] = []
667
+ post_conditions: List[ConditionExpr] = []
668
+ mutable_args: Optional[FrozenSet[str]] = None
669
+ if parse.mutable_expr is not None:
670
+ mutable_args = frozenset(
671
+ expr.strip().split(".")[0]
672
+ for expr in parse.mutable_expr.split(",")
673
+ if expr != ""
674
+ )
675
+ for line_num, expr in parse.sections["pre"]:
676
+ pre.append(
677
+ condition_from_source_text(
678
+ PRECONDITION,
679
+ filename,
680
+ line_num,
681
+ expr,
682
+ fn_globals(fn),
683
+ )
684
+ )
685
+ for line_num, expr in parse.sections["post"]:
686
+ post_conditions.append(
687
+ condition_from_source_text(
688
+ POSTCONDITION,
689
+ filename,
690
+ line_num,
691
+ expr,
692
+ fn_globals(fn),
693
+ )
694
+ )
695
+ if pre and not post_conditions:
696
+ post_conditions.append(
697
+ ConditionExpr(
698
+ POSTCONDITION, lambda vars: True, filename, first_fn_lineno, ""
699
+ )
700
+ )
701
+ return Conditions(
702
+ fn,
703
+ fn,
704
+ pre,
705
+ post_conditions,
706
+ frozenset(parse_sphinx_raises(fn)),
707
+ sig,
708
+ mutable_args,
709
+ parse.syntax_messages,
710
+ )
711
+
712
+ def get_class_invariants(self, cls: type) -> List[ConditionExpr]:
713
+ try:
714
+ filename = inspect.getsourcefile(cls)
715
+ except TypeError: # raises TypeError for builtins
716
+ filename = None
717
+ if filename is None:
718
+ return []
719
+ namespace = sys.modules[cls.__module__].__dict__
720
+
721
+ parse = parse_sections(list(get_doc_lines(cls)), ("inv",), filename)
722
+ inv = []
723
+ for line_num, line in parse.sections["inv"]:
724
+ inv.append(
725
+ condition_from_source_text(
726
+ INVARIANT, filename, line_num, line, namespace
727
+ )
728
+ )
729
+ return inv
730
+
731
+
732
+ class IcontractParser(ConcreteConditionParser):
733
+ def __init__(self, toplevel_parser: Optional[ConditionParser] = None):
734
+ super().__init__(toplevel_parser)
735
+
736
+ def contract_text(self, contract) -> str:
737
+ ls = icontract._represent.inspect_lambda_condition(condition=contract.condition)
738
+ return ls.text if ls else ""
739
+
740
+ def get_fn_conditions(self, ctxfn: FunctionInfo) -> Optional[Conditions]:
741
+ if icontract is None:
742
+ return None
743
+ fn_and_sig = ctxfn.get_callable()
744
+ if fn_and_sig is None:
745
+ return None
746
+ (fn, sig) = fn_and_sig
747
+
748
+ checker = icontract._checkers.find_checker(func=fn) # type: ignore
749
+ contractless_fn = fn # type: ignore
750
+ while (
751
+ hasattr(contractless_fn, "__is_invariant_check__")
752
+ or hasattr(contractless_fn, "__preconditions__")
753
+ or hasattr(contractless_fn, "__postconditions__")
754
+ ):
755
+ contractless_fn = contractless_fn.__wrapped__ # type: ignore
756
+ if checker is None:
757
+ return Conditions(
758
+ contractless_fn, contractless_fn, [], [], frozenset(), sig, None, []
759
+ )
760
+
761
+ pre: List[ConditionExpr] = []
762
+ post: List[ConditionExpr] = []
763
+
764
+ def eval_contract(contract, kwargs: Mapping) -> bool:
765
+ condition_kwargs = icontract._checkers.select_condition_kwargs(
766
+ contract=contract, resolved_kwargs=kwargs
767
+ )
768
+ return contract.condition(**condition_kwargs)
769
+
770
+ disjunction = checker.__preconditions__ # type: ignore
771
+ if len(disjunction) == 0:
772
+ pass
773
+ elif len(disjunction) == 1:
774
+ for contract in disjunction[0]:
775
+ evalfn = partial(eval_contract, contract)
776
+ filename, line_num, _lines = sourcelines(contract.condition)
777
+ pre.append(
778
+ ConditionExpr(
779
+ PRECONDITION,
780
+ evalfn,
781
+ filename,
782
+ line_num,
783
+ self.contract_text(contract),
784
+ )
785
+ )
786
+ else:
787
+
788
+ def eval_disjunction(disjunction, kwargs: Mapping) -> bool:
789
+ for conjunction in disjunction:
790
+ ok = True
791
+ for contract in conjunction:
792
+ if not eval_contract(contract, kwargs):
793
+ ok = False
794
+ break
795
+ if ok:
796
+ return True
797
+ return False
798
+
799
+ evalfn = partial(eval_disjunction, disjunction)
800
+ filename, line_num, _lines = sourcelines(contractless_fn)
801
+ source = (
802
+ "("
803
+ + ") or (".join(
804
+ [
805
+ " and ".join([self.contract_text(c) for c in conj])
806
+ for conj in disjunction
807
+ ]
808
+ )
809
+ + ")"
810
+ )
811
+ pre.append(ConditionExpr(PRECONDITION, evalfn, filename, line_num, source))
812
+
813
+ snapshots = checker.__postcondition_snapshots__ # type: ignore
814
+
815
+ def take_snapshots(**kwargs):
816
+ old_as_mapping: MutableMapping[str, Any] = {}
817
+ for snap in snapshots:
818
+ snap_kwargs = icontract._checkers.select_capture_kwargs(
819
+ a_snapshot=snap, resolved_kwargs=kwargs
820
+ )
821
+ old_as_mapping[snap.name] = snap.capture(**snap_kwargs)
822
+ return icontract._checkers.Old(mapping=old_as_mapping)
823
+
824
+ def post_eval(contract, orig_kwargs: Mapping) -> bool:
825
+ kwargs = dict(orig_kwargs)
826
+ _old = kwargs.pop("__old__")
827
+ kwargs["OLD"] = take_snapshots(**_old.__dict__)
828
+ kwargs["result"] = kwargs.pop("__return__")
829
+ del kwargs["_"]
830
+ condition_kwargs = icontract._checkers.select_condition_kwargs(
831
+ contract=contract, resolved_kwargs=kwargs
832
+ )
833
+ return contract.condition(**condition_kwargs)
834
+
835
+ for postcondition in checker.__postconditions__: # type: ignore
836
+ evalfn = partial(post_eval, postcondition)
837
+ filename, line_num, _lines = sourcelines(postcondition.condition)
838
+ post.append(
839
+ ConditionExpr(
840
+ POSTCONDITION,
841
+ evalfn,
842
+ filename,
843
+ line_num,
844
+ self.contract_text(postcondition),
845
+ )
846
+ )
847
+ if pre and not post:
848
+ filename, line_num, _lines = sourcelines(contractless_fn)
849
+ post.append(
850
+ ConditionExpr(POSTCONDITION, lambda vars: True, filename, line_num, "")
851
+ )
852
+ return Conditions(
853
+ contractless_fn,
854
+ contractless_fn,
855
+ pre,
856
+ post,
857
+ raises=frozenset(parse_sphinx_raises(fn)),
858
+ sig=sig,
859
+ mutable_args=None,
860
+ fn_syntax_messages=[],
861
+ )
862
+
863
+ def get_class_invariants(self, cls: type) -> List[ConditionExpr]:
864
+ invariants = getattr(cls, "__invariants__", ()) # type: ignore
865
+ ret = []
866
+
867
+ def inv_eval(contract, kwargs):
868
+ return contract.condition(self=kwargs["self"])
869
+
870
+ for contract in invariants:
871
+ filename, line_num, _lines = sourcelines(contract.condition)
872
+ ret.append(
873
+ ConditionExpr(
874
+ INVARIANT,
875
+ partial(inv_eval, contract),
876
+ filename,
877
+ line_num,
878
+ self.contract_text(contract),
879
+ )
880
+ )
881
+ return ret
882
+
883
+
884
+ _DEALL_MARKERS_TO_SKIP = frozenset(
885
+ [
886
+ # NOTE: These are (re-)enumerated in kinds_of_contracts.rst
887
+ # TODO: Make this list customizable?
888
+ "write",
889
+ "network",
890
+ "stdin",
891
+ "syscall",
892
+ ]
893
+ )
894
+
895
+
896
+ class DealParser(ConcreteConditionParser):
897
+ def _contract_validates(
898
+ self,
899
+ contract: "deal.introspection.ValidatedContract",
900
+ args: Sequence,
901
+ kwargs: Mapping[str, object],
902
+ ) -> bool:
903
+ try:
904
+ contract.validate(*args, **kwargs)
905
+ return True
906
+ except contract.exception_type:
907
+ return False
908
+
909
+ def _extract_a_and_kw(
910
+ self, bindings: Mapping[str, object], sig: Signature
911
+ ) -> Tuple[List[object], Dict[str, object]]:
912
+ positional_args = []
913
+ keyword_args = {}
914
+ for param in sig.parameters.values():
915
+ if param.kind == inspect.Parameter.KEYWORD_ONLY:
916
+ keyword_args[param.name] = bindings[param.name]
917
+ positional_args.append(bindings[param.name])
918
+ return (positional_args, keyword_args)
919
+
920
+ def _make_pre_expr(
921
+ self, contract: "deal.introspection.Pre", sig: Signature
922
+ ) -> Callable[[Mapping[str, object]], bool]:
923
+ def evaluatefn(bindings: Mapping[str, object]) -> bool:
924
+ args, kwargs = self._extract_a_and_kw(bindings, sig)
925
+ return self._contract_validates(contract, args, kwargs)
926
+
927
+ return evaluatefn
928
+
929
+ def _make_post_expr(
930
+ self, contract: "deal.introspection.Post", sig: Signature
931
+ ) -> Callable[[Mapping[str, object]], bool]:
932
+ return lambda b: self._contract_validates(contract, (b["__return__"],), {})
933
+
934
+ def _make_ensure_expr(
935
+ self, contract: "deal.introspection.Ensure", sig: Signature
936
+ ) -> Callable[[Mapping[str, object]], bool]:
937
+ def evaluatefn(bindings: Mapping[str, object]) -> bool:
938
+ args, kwargs = self._extract_a_and_kw(bindings, sig)
939
+ kwargs["result"] = bindings["__return__"]
940
+ return self._contract_validates(contract, args, kwargs)
941
+
942
+ return evaluatefn
943
+
944
+ def get_fn_conditions(self, ctxfn: FunctionInfo) -> Optional[Conditions]:
945
+ if deal is None:
946
+ return None
947
+ fn_and_sig = ctxfn.get_callable()
948
+ if fn_and_sig is None:
949
+ return None
950
+ (fn, sig) = fn_and_sig
951
+
952
+ contracts = list(deal.introspection.get_contracts(fn))
953
+ if not contracts:
954
+ return None
955
+ deal.introspection.init_all(fn)
956
+
957
+ pre: List[ConditionExpr] = []
958
+ post: List[ConditionExpr] = []
959
+ exceptions: List[Type[Exception]] = []
960
+ for contract in contracts:
961
+ if isinstance(contract, deal.introspection.Raises):
962
+ exceptions.extend(contract.exceptions)
963
+ continue
964
+ if isinstance(contract, deal.introspection.Has):
965
+ for marker in contract.markers:
966
+ if marker in _DEALL_MARKERS_TO_SKIP:
967
+ debug(
968
+ f"Skipping analysis of {fn.__name__} because it is marked with '{marker}'"
969
+ )
970
+ return None
971
+ if not isinstance(contract, deal.introspection.ValidatedContract):
972
+ continue
973
+ fname, lineno, _lines = sourcelines(fn)
974
+ exprsrc = contract.source
975
+ if isinstance(contract, deal.introspection.Pre):
976
+ expr = self._make_pre_expr(contract, sig)
977
+ pre.append(ConditionExpr(PRECONDITION, expr, fname, lineno, exprsrc))
978
+ elif isinstance(contract, deal.introspection.Post):
979
+ expr = self._make_post_expr(contract, sig)
980
+ post.append(ConditionExpr(POSTCONDITION, expr, fname, lineno, exprsrc))
981
+ elif isinstance(contract, deal.introspection.Ensure):
982
+ expr = self._make_ensure_expr(contract, sig)
983
+ post.append(ConditionExpr(POSTCONDITION, expr, fname, lineno, exprsrc))
984
+
985
+ if pre and not post:
986
+ filename, line_num, _lines = sourcelines(fn)
987
+ post.append(
988
+ ConditionExpr(POSTCONDITION, lambda vars: True, filename, line_num, "")
989
+ )
990
+ raw_fn = deal.introspection.unwrap(fn)
991
+ return Conditions(
992
+ fn=raw_fn,
993
+ src_fn=raw_fn,
994
+ pre=pre,
995
+ post=post,
996
+ raises=frozenset(exceptions),
997
+ sig=sig,
998
+ mutable_args=None,
999
+ fn_syntax_messages=[],
1000
+ )
1001
+
1002
+ def get_class_invariants(self, cls: type) -> List[ConditionExpr]:
1003
+ return []
1004
+
1005
+
1006
+ class AssertsParser(ConcreteConditionParser):
1007
+ def __init__(self, toplevel_parser: Optional[ConditionParser] = None):
1008
+ super().__init__(toplevel_parser)
1009
+
1010
+ @staticmethod
1011
+ def is_string_literal(node: ast.AST) -> bool:
1012
+ if sys.version_info >= (3, 8):
1013
+ return (
1014
+ isinstance(node, ast.Expr)
1015
+ and isinstance(node.value, ast.Constant)
1016
+ and isinstance(node.value.value, str)
1017
+ )
1018
+ else:
1019
+ return isinstance(node, ast.Expr) and isinstance(node.value, ast.Str)
1020
+
1021
+ @staticmethod
1022
+ def get_first_body_line(fn: Callable) -> Optional[int]:
1023
+ """
1024
+ Retrieve the first line of the body of the function ``fn``.
1025
+
1026
+ :return:
1027
+ the line number of the first non-assert statement in the given function.
1028
+
1029
+ :return:
1030
+ None if the function does not start with at least one assert statement.
1031
+ """
1032
+ _filename, first_fn_lineno, lines = sourcelines(fn)
1033
+ if not lines:
1034
+ return None
1035
+ ast_module = ast.parse(textwrap.dedent("".join(lines)))
1036
+ ast_fn = ast_module.body[0]
1037
+ if not isinstance(ast_fn, ast.FunctionDef):
1038
+ return None
1039
+ found_any_preconditions = False
1040
+ for statement in ast_fn.body:
1041
+ if isinstance(statement, ast.Assert):
1042
+ found_any_preconditions = True
1043
+ continue
1044
+ elif AssertsParser.is_string_literal(statement):
1045
+ # A docstring, keep looking:
1046
+ continue
1047
+ break
1048
+ if found_any_preconditions:
1049
+ return first_fn_lineno + (statement.lineno - 1)
1050
+ else:
1051
+ return None
1052
+
1053
+ def get_fn_conditions(self, ctxfn: FunctionInfo) -> Optional[Conditions]:
1054
+ fn_and_sig = ctxfn.get_callable()
1055
+ if fn_and_sig is None:
1056
+ return None
1057
+ (fn, sig) = fn_and_sig
1058
+ # TODO replace this guard with package-level configuration?
1059
+ if (
1060
+ getattr(fn, "__module__", False)
1061
+ and fn.__module__.startswith("crosshair.")
1062
+ and not fn.__module__.endswith("_test")
1063
+ ):
1064
+ return None
1065
+ try:
1066
+ first_body_line = AssertsParser.get_first_body_line(fn)
1067
+ except OSError:
1068
+ return None
1069
+ if first_body_line is None:
1070
+ return None
1071
+
1072
+ filename, first_line, _lines = sourcelines(fn)
1073
+
1074
+ @wraps(fn)
1075
+ def wrappedfn(*a, **kw):
1076
+ try:
1077
+ return NoEnforce(fn)(*a, **kw)
1078
+ except AssertionError as e:
1079
+ # TODO: check that this isn't failing at an early line in a different
1080
+ # file?
1081
+ _, lineno = frame_summary_for_fn(
1082
+ fn, traceback.extract_tb(e.__traceback__)
1083
+ )
1084
+ if lineno >= first_body_line:
1085
+ raise
1086
+
1087
+ post = [
1088
+ ConditionExpr(
1089
+ POSTCONDITION,
1090
+ lambda _: True,
1091
+ filename,
1092
+ first_line,
1093
+ "",
1094
+ )
1095
+ ]
1096
+ return Conditions(
1097
+ wrappedfn,
1098
+ fn,
1099
+ [], # (pre)
1100
+ post,
1101
+ raises=frozenset(parse_sphinx_raises(fn)),
1102
+ sig=sig,
1103
+ mutable_args=None,
1104
+ fn_syntax_messages=[],
1105
+ )
1106
+
1107
+ def get_class_invariants(self, cls: type) -> List[ConditionExpr]:
1108
+ return []
1109
+
1110
+
1111
+ class RegisteredContractsParser(ConcreteConditionParser):
1112
+ """Parser for manually registered contracts."""
1113
+
1114
+ def __init__(self, toplevel_parser: Optional[ConditionParser] = None):
1115
+ super().__init__(toplevel_parser)
1116
+
1117
+ def get_fn_conditions(self, ctxfn: FunctionInfo) -> Optional[Conditions]:
1118
+ fn_and_sig = ctxfn.get_callable()
1119
+ if fn_and_sig is not None:
1120
+ (fn, sig) = fn_and_sig
1121
+ sigs = [sig]
1122
+ contract = get_contract(fn)
1123
+ if not contract:
1124
+ return None
1125
+ else:
1126
+ # ctxfn.get_callable() returns None if no signature was found
1127
+ desc = ctxfn.descriptor
1128
+ if isinstance(desc, Callable): # type: ignore
1129
+ fn = cast(Callable, desc)
1130
+ contract = get_contract(fn)
1131
+ # Ensure we have at least one signature
1132
+ if not contract or not contract.sigs:
1133
+ return None
1134
+ sigs = contract.sigs
1135
+ else:
1136
+ return None
1137
+
1138
+ # Signatures registered in contracts have higher precedence
1139
+ if contract.sigs:
1140
+ sigs = contract.sigs
1141
+ pre: List[ConditionExpr] = []
1142
+ post: List[ConditionExpr] = []
1143
+
1144
+ filename, line_num, _lines = sourcelines(fn)
1145
+
1146
+ if contract.pre:
1147
+ pre_cond = contract.pre
1148
+
1149
+ def evaluatefn(kwargs: Mapping):
1150
+ kwargs = dict(kwargs)
1151
+ pre_args = inspect.signature(pre_cond).parameters.keys()
1152
+ new_kwargs = {arg: kwargs[arg] for arg in pre_args}
1153
+ return pre_cond(**new_kwargs)
1154
+
1155
+ pre.append(
1156
+ ConditionExpr(
1157
+ PRECONDITION,
1158
+ evaluatefn,
1159
+ filename,
1160
+ line_num,
1161
+ inspect.getsource(pre_cond),
1162
+ )
1163
+ )
1164
+ if contract.post:
1165
+ post_cond = contract.post
1166
+
1167
+ def post_eval(orig_kwargs: Mapping) -> bool:
1168
+ kwargs = dict(orig_kwargs)
1169
+ post_args = inspect.signature(post_cond).parameters.keys()
1170
+ new_kwargs = {arg: kwargs[arg] for arg in post_args}
1171
+ return post_cond(**new_kwargs)
1172
+
1173
+ post.append(
1174
+ ConditionExpr(
1175
+ POSTCONDITION,
1176
+ post_eval,
1177
+ filename,
1178
+ line_num,
1179
+ inspect.getsource(post_cond),
1180
+ )
1181
+ )
1182
+ else:
1183
+ # Ensure at least one postcondition to allow short-circuiting the body.
1184
+ post.append(
1185
+ ConditionExpr(POSTCONDITION, lambda vars: True, filename, line_num, "")
1186
+ )
1187
+ return Conditions(
1188
+ fn,
1189
+ fn,
1190
+ pre,
1191
+ post,
1192
+ raises=frozenset(parse_sphinx_raises(fn)),
1193
+ sig=sigs[0], # TODO: in the future, should return all sigs.
1194
+ mutable_args=None,
1195
+ fn_syntax_messages=[],
1196
+ )
1197
+
1198
+ def class_can_have_conditions(self, cls: type) -> bool:
1199
+ # We might have registered contracts for classes written in C, so we don't want
1200
+ # to skip evaluating conditions on the class methods.
1201
+ return True
1202
+
1203
+ def get_class_invariants(self, cls: type) -> List[ConditionExpr]:
1204
+ # TODO: Should we add a way of registering class invariants?
1205
+ return []
1206
+
1207
+
1208
+ _PARSER_MAP = {
1209
+ AnalysisKind.asserts: AssertsParser,
1210
+ AnalysisKind.PEP316: Pep316Parser,
1211
+ AnalysisKind.icontract: IcontractParser,
1212
+ AnalysisKind.deal: DealParser,
1213
+ }
1214
+
1215
+
1216
+ # Condition parsers may be needed at various places in the stack.
1217
+ # We configure them through the use of a magic threadlocal value:
1218
+ _CALLTREE_PARSER = DynamicScopeVar(ConditionParser, "calltree parser")
1219
+
1220
+
1221
+ def condition_parser(
1222
+ analysis_kinds: Sequence[AnalysisKind],
1223
+ ) -> ContextManager[ConditionParser]:
1224
+ current = _CALLTREE_PARSER.get_if_in_scope()
1225
+ if current is not None:
1226
+ return contextlib.nullcontext(current)
1227
+ debug("Using parsers: ", analysis_kinds)
1228
+ condition_parser = CompositeConditionParser()
1229
+ condition_parser.parsers.extend(
1230
+ _PARSER_MAP[k](condition_parser) for k in analysis_kinds
1231
+ )
1232
+ condition_parser.parsers.append(RegisteredContractsParser(condition_parser))
1233
+ return _CALLTREE_PARSER.open(condition_parser)
1234
+
1235
+
1236
+ def get_current_parser() -> ConditionParser:
1237
+ return _CALLTREE_PARSER.get()