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
crosshair/test_util.py ADDED
@@ -0,0 +1,329 @@
1
+ import pathlib
2
+ import sys
3
+ from copy import deepcopy
4
+ from dataclasses import dataclass, replace
5
+ from decimal import Decimal
6
+ from math import isnan
7
+ from numbers import Real
8
+ from typing import (
9
+ Callable,
10
+ Collection,
11
+ Iterable,
12
+ List,
13
+ Mapping,
14
+ Optional,
15
+ Sequence,
16
+ Set,
17
+ Tuple,
18
+ )
19
+
20
+ from crosshair.core import (
21
+ AnalysisMessage,
22
+ Checkable,
23
+ MessageType,
24
+ analyze_function,
25
+ deep_realize,
26
+ run_checkables,
27
+ )
28
+ from crosshair.options import AnalysisOptionSet
29
+ from crosshair.statespace import context_statespace
30
+ from crosshair.tracers import NoTracing, ResumedTracing
31
+ from crosshair.util import (
32
+ assert_tracing,
33
+ ch_stack,
34
+ debug,
35
+ in_debug,
36
+ is_iterable,
37
+ is_pure_python,
38
+ name_of_type,
39
+ )
40
+
41
+ ComparableLists = Tuple[List, List]
42
+
43
+
44
+ class _Missing:
45
+ pass
46
+
47
+
48
+ _MISSING = _Missing()
49
+
50
+
51
+ def simplefs(path: pathlib.Path, files: dict) -> None:
52
+ for name, contents in files.items():
53
+ subpath = path / name
54
+ if isinstance(contents, str):
55
+ with open(subpath, "w") as fh:
56
+ fh.write(contents)
57
+ elif isinstance(contents, dict):
58
+ subpath.mkdir()
59
+ simplefs(subpath, contents)
60
+ else:
61
+ raise Exception("bad input to simplefs")
62
+
63
+
64
+ def check_states(
65
+ fn: Callable,
66
+ expected: MessageType,
67
+ optionset: AnalysisOptionSet = AnalysisOptionSet(),
68
+ ) -> None:
69
+ if expected == MessageType.POST_FAIL:
70
+ local_opts = AnalysisOptionSet(
71
+ per_condition_timeout=16,
72
+ max_uninteresting_iterations=sys.maxsize,
73
+ )
74
+ elif expected == MessageType.CONFIRMED:
75
+ local_opts = AnalysisOptionSet(
76
+ per_condition_timeout=60,
77
+ per_path_timeout=20,
78
+ max_uninteresting_iterations=sys.maxsize,
79
+ )
80
+ elif expected == MessageType.POST_ERR:
81
+ local_opts = AnalysisOptionSet(max_iterations=20)
82
+ elif expected == MessageType.CANNOT_CONFIRM:
83
+ local_opts = AnalysisOptionSet(
84
+ max_uninteresting_iterations=40,
85
+ per_condition_timeout=3,
86
+ )
87
+ else:
88
+ local_opts = AnalysisOptionSet(
89
+ max_uninteresting_iterations=40,
90
+ per_condition_timeout=5,
91
+ )
92
+ options = local_opts.overlay(optionset)
93
+ found = set([m.state for m in run_checkables(analyze_function(fn, options))])
94
+ assertmsg = f"Got {','.join(map(str, found))} instead of {expected}"
95
+ if not in_debug():
96
+ assertmsg += " (use `pytest -v` to show trace)"
97
+ assert found == {expected}, assertmsg
98
+
99
+
100
+ def check_exec_err(
101
+ fn: Callable, message_prefix="", optionset: AnalysisOptionSet = AnalysisOptionSet()
102
+ ) -> ComparableLists:
103
+ local_opts = AnalysisOptionSet(max_iterations=20)
104
+ options = local_opts.overlay(optionset)
105
+ messages = run_checkables(analyze_function(fn, options))
106
+ if all(m.message.startswith(message_prefix) for m in messages):
107
+ return ([m.state for m in messages], [MessageType.EXEC_ERR])
108
+ else:
109
+ return (
110
+ [(m.state, m.message) for m in messages],
111
+ [(MessageType.EXEC_ERR, message_prefix)],
112
+ )
113
+
114
+
115
+ def check_messages(checkables: Iterable[Checkable], **kw) -> ComparableLists:
116
+ msgs = run_checkables(checkables)
117
+ if kw.get("state") != MessageType.CONFIRMED:
118
+ # Normally, ignore confirmation messages:
119
+ msgs = [m for m in msgs if m.state != MessageType.CONFIRMED]
120
+ else:
121
+ # When we want CONFIRMED, take the message with the worst status:
122
+ msgs = [max(msgs, key=lambda m: m.state)]
123
+ default_msg = AnalysisMessage(MessageType.CANNOT_CONFIRM, "", "", 0, 0, "")
124
+ msg = msgs[0] if msgs else replace(default_msg)
125
+ fields = (
126
+ "state",
127
+ "message",
128
+ "filename",
129
+ "line",
130
+ "column",
131
+ "traceback",
132
+ "test_fn",
133
+ "condition_src",
134
+ )
135
+ for k in fields:
136
+ if k not in kw:
137
+ default_val = getattr(default_msg, k)
138
+ msg = replace(msg, **{k: default_val})
139
+ kw[k] = default_val
140
+ if msgs:
141
+ msgs[0] = msg
142
+ return (msgs, [AnalysisMessage(**kw)])
143
+
144
+
145
+ _NAN_ABLE = (Decimal, Real)
146
+
147
+
148
+ def flexible_equal(a: object, b: object) -> bool:
149
+ if a is b:
150
+ return True
151
+ if type(a) is type(b) and type(a).__eq__ is object.__eq__:
152
+ # If types match and it uses identity-equals, we can't do much. Assume equal.
153
+ return True
154
+ if isinstance(a, _NAN_ABLE) and isinstance(b, _NAN_ABLE) and isnan(a) and isnan(b):
155
+ return True
156
+ if (
157
+ is_iterable(a)
158
+ and not isinstance(a, Collection)
159
+ and is_iterable(b)
160
+ and not isinstance(b, Collection)
161
+ ): # unsized iterables compare by contents
162
+ a, b = list(a), list(b) # type: ignore
163
+ if (
164
+ type(a) == type(b)
165
+ and isinstance(a, Collection)
166
+ and not isinstance(a, (str, bytes, Set))
167
+ ):
168
+ # Recursively apply flexible_equal for most containers:
169
+ if len(a) != len(b): # type: ignore
170
+ return False
171
+ if isinstance(a, Mapping):
172
+ for k, v in a.items():
173
+ if not flexible_equal(v, b.get(k, _MISSING)): # type: ignore
174
+ return False
175
+ return True
176
+ else:
177
+ return all(flexible_equal(ai, bi) for ai, bi in zip(a, b)) # type: ignore
178
+
179
+ return a == b
180
+
181
+
182
+ @dataclass(eq=False)
183
+ class ExecutionResult:
184
+ ret: object # return value
185
+ exc: Optional[BaseException] # exception raised, if any
186
+ tb: Optional[str]
187
+ # args after the function terminates:
188
+ post_args: Sequence
189
+ post_kwargs: Mapping[str, object]
190
+
191
+ def __eq__(self, other: object) -> bool:
192
+ if not isinstance(other, ExecutionResult):
193
+ return False
194
+ return (
195
+ flexible_equal(self.ret, other.ret)
196
+ and type(self.exc) == type(other.exc)
197
+ and self.post_args == other.post_args
198
+ and self.post_kwargs == other.post_kwargs
199
+ )
200
+
201
+ def describe(self, include_postexec=False) -> str:
202
+ ret = ""
203
+ if self.exc:
204
+ exc = self.exc
205
+ exc_type = name_of_type(type(exc))
206
+ tb = self.tb or "(missing traceback)"
207
+ ret = f"exc={exc_type}: {str(exc)} {tb}"
208
+ else:
209
+ ret = f"ret={self.ret!r}"
210
+ if include_postexec:
211
+ a = [repr(a) for a in self.post_args]
212
+ a += [f"{k}={v!r}" for k, v in self.post_kwargs.items()]
213
+ ret += f' post=({", ".join(a)})'
214
+ return ret
215
+
216
+
217
+ @dataclass
218
+ class IterableResult:
219
+ values: tuple
220
+ typ: type
221
+
222
+
223
+ def summarize_execution(
224
+ fn: Callable,
225
+ args: Sequence[object] = (),
226
+ kwargs: Optional[Mapping[str, object]] = None,
227
+ detach_path: bool = True,
228
+ ) -> ExecutionResult:
229
+ if not kwargs:
230
+ kwargs = {}
231
+ ret: object = None
232
+ exc: Optional[Exception] = None
233
+ tbstr: Optional[str] = None
234
+ try:
235
+ possibly_symbolic_ret = fn(*args, **kwargs)
236
+ if detach_path:
237
+ context_statespace().detach_path()
238
+ detach_path = False
239
+ ret_type = type(possibly_symbolic_ret)
240
+ _ret = deep_realize(possibly_symbolic_ret)
241
+ if hasattr(_ret, "__next__"):
242
+ # Summarize any iterator as the values it produces, plus its type:
243
+ ret = IterableResult(tuple(_ret), ret_type)
244
+ elif callable(_ret) and not is_pure_python(_ret):
245
+ # Summarize C-based callables just based on their type:
246
+ ret = f"C-based callable {type(_ret).__name__}"
247
+ else:
248
+ ret = _ret
249
+ args = deep_realize(args)
250
+ kwargs = deep_realize(kwargs)
251
+ except Exception as e:
252
+ exc = e
253
+ if detach_path:
254
+ context_statespace().detach_path(e)
255
+ exc = deep_realize(exc)
256
+ # NOTE: deep_realize somehow empties the __traceback__ member; re-assign it:
257
+ exc.__traceback__ = e.__traceback__
258
+ tbstr = ch_stack(currently_handling=exc)
259
+ if in_debug():
260
+ debug("hit exception:", type(exc), exc, tbstr)
261
+ return ExecutionResult(ret, exc, tbstr, args, kwargs)
262
+
263
+
264
+ @dataclass
265
+ class ResultComparison:
266
+ left: ExecutionResult
267
+ right: ExecutionResult
268
+
269
+ def __bool__(self):
270
+ return self.left == self.right and type(self.left) == type(self.right)
271
+
272
+ def __repr__(self):
273
+ left, right = self.left, self.right
274
+ include_postexec = left.ret == right.ret and type(left.exc) == type(right.exc)
275
+ return (
276
+ left.describe(include_postexec)
277
+ + " <--symbolic-vs-concrete--> "
278
+ + right.describe(include_postexec)
279
+ )
280
+
281
+
282
+ def compare_returns(fn: Callable, *a: object, **kw: object) -> ResultComparison:
283
+ comparison = compare_results(fn, *a, **kw)
284
+ comparison.left.post_args = ()
285
+ comparison.left.post_kwargs = {}
286
+ comparison.right.post_args = ()
287
+ comparison.right.post_kwargs = {}
288
+ return comparison
289
+
290
+
291
+ @assert_tracing(True)
292
+ def compare_results(fn: Callable, *a: object, **kw: object) -> ResultComparison:
293
+ original_a = deepcopy(a)
294
+ original_kw = deepcopy(kw)
295
+ symbolic_result = summarize_execution(fn, a, kw)
296
+
297
+ concrete_a = deep_realize(original_a)
298
+ concrete_kw = deep_realize(original_kw)
299
+
300
+ # Check that realization worked, too:
301
+ with NoTracing():
302
+ labels_and_args = [
303
+ *(
304
+ (f"Argument {idx + 1}", a[idx], arg)
305
+ for idx, arg in enumerate(concrete_a)
306
+ ),
307
+ *((f"Keyword argument '{k}'", kw[k], v) for k, v in concrete_kw.items()),
308
+ ]
309
+ for label, symbolic_arg, concrete_arg in labels_and_args:
310
+ with ResumedTracing():
311
+ symbolic_type = type(symbolic_arg)
312
+ concrete_type = type(concrete_arg)
313
+ true_concrete_type = type(concrete_arg)
314
+ assert (
315
+ true_concrete_type == concrete_type
316
+ ), f"{label} did not realize. It is {true_concrete_type} instead of {concrete_type}."
317
+ assert (
318
+ true_concrete_type == symbolic_type
319
+ ), f"{label} should realize to {symbolic_type}; it is {true_concrete_type} instead."
320
+
321
+ with NoTracing():
322
+ concrete_result = summarize_execution(
323
+ fn, concrete_a, concrete_kw, detach_path=False
324
+ )
325
+ debug("concrete_result:", concrete_result)
326
+
327
+ ret = ResultComparison(symbolic_result, concrete_result)
328
+ bool(ret)
329
+ return ret
@@ -0,0 +1,26 @@
1
+ from crosshair.test_util import flexible_equal
2
+
3
+
4
+ def test_flexible_equal():
5
+ assert float("nan") != float("nan")
6
+ assert flexible_equal(float("nan"), float("nan"))
7
+ assert flexible_equal((42, float("nan")), (42, float("nan")))
8
+ assert not flexible_equal([float("nan"), 11], [float("nan"), 22])
9
+
10
+ def gen():
11
+ yield 11
12
+ yield 22
13
+
14
+ assert flexible_equal(gen(), iter([11, 22]))
15
+ assert not flexible_equal(gen(), iter([11, 22, 33]))
16
+ assert not flexible_equal(gen(), iter([11]))
17
+
18
+ ordered_set_1 = {10_000, 20_000} | {30_000}
19
+ ordered_set_2 = {30_000, 20_000} | {10_000}
20
+ assert list(ordered_set_1) != list(ordered_set_2) # (different orderings)
21
+ assert flexible_equal(ordered_set_1, ordered_set_2)
22
+
23
+ ordered_dict_1 = {1: 2, 3: 4}
24
+ ordered_dict_2 = {3: 4, 1: 2}
25
+ assert list(ordered_dict_1.items()) != list(ordered_dict_2.items())
26
+ assert flexible_equal(ordered_dict_1, ordered_dict_2)
File without changes
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """Check that the help snippets in the doc coincide with the actual output."""
4
+ import argparse
5
+ import os
6
+ import pathlib
7
+ import re
8
+ import subprocess
9
+ import sys
10
+ from typing import List, Optional, Tuple
11
+
12
+ import icontract
13
+
14
+
15
+ class Block:
16
+ """Represent a block in the readme that needs to be checked."""
17
+
18
+ @icontract.require(lambda command: command != "")
19
+ @icontract.require(
20
+ lambda start_line_idx, end_line_idx: start_line_idx <= end_line_idx
21
+ )
22
+ def __init__(self, command: str, start_line_idx: int, end_line_idx: int) -> None:
23
+ """
24
+ Initialize with the given values.
25
+
26
+ :param command: help command
27
+ :param start_line_idx: index of the first relevant line
28
+ :param end_line_idx: index of the first line excluded from the block
29
+ """
30
+ self.command = command
31
+ self.start_line_idx = start_line_idx
32
+ self.end_line_idx = end_line_idx
33
+
34
+
35
+ HELP_STARTS_RE = re.compile(r"^.. Help starts: (?P<command>.*)$")
36
+
37
+
38
+ def parse_rst(lines: List[str]) -> Tuple[List[Block], List[str]]:
39
+ """
40
+ Parse the code blocks that represent help commands in the RST file.
41
+
42
+ :param lines: lines of the readme file
43
+ :return: (help blocks, errors if any)
44
+ """
45
+ blocks = [] # type: List[Block]
46
+ errors = [] # type: List[str]
47
+
48
+ i = 0
49
+ while i < len(lines):
50
+ mtch = HELP_STARTS_RE.match(lines[i])
51
+ if mtch:
52
+ command = mtch.group("command")
53
+ help_ends = ".. Help ends: {}".format(command)
54
+ try:
55
+ end_index = lines.index(help_ends, i)
56
+ except ValueError:
57
+ end_index = -1
58
+
59
+ if end_index == -1:
60
+ return [], ["Could not find the end marker {!r}".format(help_ends)]
61
+
62
+ blocks.append(
63
+ Block(command=command, start_line_idx=i + 1, end_line_idx=end_index)
64
+ )
65
+
66
+ i = end_index + 1
67
+
68
+ else:
69
+ i += 1
70
+
71
+ return blocks, errors
72
+
73
+
74
+ def capture_output_lines(command: str) -> List[str]:
75
+ """Capture the output of a help command."""
76
+ command_parts = command.split(" ")
77
+ if command_parts[0] in ["python", "python3"]:
78
+ # We need to replace "python" with "sys.executable" on Windows as the environment
79
+ # is not properly inherited.
80
+ command_parts[0] = sys.executable
81
+
82
+ proc = subprocess.Popen(
83
+ command_parts,
84
+ stdout=subprocess.PIPE,
85
+ stderr=subprocess.PIPE,
86
+ encoding="utf-8",
87
+ )
88
+ output, err = proc.communicate()
89
+ if err:
90
+ raise RuntimeError(
91
+ f"The command {command!r} failed with exit code {proc.returncode} and "
92
+ f"stderr:\n{err}"
93
+ )
94
+ # Help text changed in 3.10 argparse; always use the newer text.
95
+ output = output.replace("optional arguments", "options")
96
+
97
+ return output.splitlines()
98
+
99
+
100
+ def output_lines_to_code_block(output_lines: List[str]) -> List[str]:
101
+ """Translate the output of a help command to a RST code block."""
102
+ result = (
103
+ [".. code-block:: text", ""]
104
+ + [" " + output_line for output_line in output_lines]
105
+ + [""]
106
+ )
107
+
108
+ result = [line.rstrip() for line in result]
109
+ return result
110
+
111
+
112
+ def diff(got_lines: List[str], expected_lines: List[str]) -> Optional[str]:
113
+ """
114
+ Report a difference between the ``got`` and ``expected``.
115
+
116
+ Return None if no difference.
117
+ """
118
+ if got_lines == expected_lines:
119
+ return None
120
+
121
+ result = []
122
+
123
+ result.append("Expected:")
124
+ for i, line in enumerate(expected_lines):
125
+ if i >= len(got_lines) or line != got_lines[i]:
126
+ print("DIFF: {:2d}: {!r}".format(i, line))
127
+ else:
128
+ print("OK : {:2d}: {!r}".format(i, line))
129
+
130
+ result.append("Got:")
131
+ for i, line in enumerate(got_lines):
132
+ if i >= len(expected_lines) or line != expected_lines[i]:
133
+ print("DIFF: {:2d}: {!r}".format(i, line))
134
+ else:
135
+ print("OK : {:2d}: {!r}".format(i, line))
136
+
137
+ return "\n".join(result)
138
+
139
+
140
+ def process_file(path: pathlib.Path, overwrite: bool) -> List[str]:
141
+ """
142
+ Check or overwrite the help blocks in the given file.
143
+
144
+ :param path: to the doc file
145
+ :param overwrite: if set, overwrite the help blocks
146
+ :return: list of errors, if any
147
+ """
148
+ text = path.read_text(encoding="utf-8")
149
+ lines = text.splitlines()
150
+
151
+ blocks, errors = parse_rst(lines=lines)
152
+ if errors:
153
+ return errors
154
+
155
+ if len(blocks) == 0:
156
+ return []
157
+
158
+ if overwrite:
159
+ result = [] # type: List[str]
160
+
161
+ previous_block = None # type: Optional[Block]
162
+ for block in blocks:
163
+ output_lines = capture_output_lines(command=block.command)
164
+ code_block_lines = output_lines_to_code_block(output_lines=output_lines)
165
+
166
+ if previous_block is None:
167
+ result.extend(lines[: block.start_line_idx])
168
+ else:
169
+ result.extend(lines[previous_block.end_line_idx : block.start_line_idx])
170
+
171
+ result.extend(code_block_lines)
172
+ previous_block = block
173
+ assert previous_block is not None
174
+ result.extend(lines[previous_block.end_line_idx :])
175
+ result.append("") # new line at the end of file
176
+
177
+ path.write_text("\n".join(result))
178
+ else:
179
+ for block in blocks:
180
+ output_lines = capture_output_lines(command=block.command)
181
+ code_block_lines = output_lines_to_code_block(output_lines=output_lines)
182
+
183
+ expected_lines = lines[block.start_line_idx : block.end_line_idx]
184
+ expected_lines = [line.rstrip() for line in expected_lines]
185
+
186
+ error = diff(got_lines=code_block_lines, expected_lines=expected_lines)
187
+ if error:
188
+ return [error]
189
+ return []
190
+
191
+
192
+ def main() -> int:
193
+ """Execute the main routine."""
194
+ parser = argparse.ArgumentParser(description=__doc__)
195
+ parser.add_argument(
196
+ "--overwrite",
197
+ help="If set, overwrite the relevant part of the doc in-place.",
198
+ action="store_true",
199
+ )
200
+
201
+ args = parser.parse_args()
202
+ overwrite = bool(args.overwrite)
203
+
204
+ this_dir = pathlib.Path(os.path.realpath(__file__)).parent.parent.parent
205
+
206
+ pths = [
207
+ this_dir / "doc" / "source" / "contracts.rst",
208
+ this_dir / "doc" / "source" / "cover.rst",
209
+ this_dir / "doc" / "source" / "diff_behavior.rst",
210
+ this_dir / "doc" / "source" / "contributing.rst",
211
+ ]
212
+
213
+ success = True
214
+
215
+ for pth in pths:
216
+ errors = process_file(path=pth, overwrite=overwrite)
217
+ if errors:
218
+ print("One or more errors in {}:".format(pth), file=sys.stderr)
219
+ for error in errors:
220
+ print(error, file=sys.stderr)
221
+ success = False
222
+
223
+ # Also check that the TOC in the README matches the Sphinx TOC:
224
+ indexlines = open(
225
+ this_dir / "doc" / "source" / "index.rst", encoding="utf-8"
226
+ ).readlines()
227
+ rst_links = [
228
+ f"latest/{line.strip()}.html"
229
+ for line in indexlines
230
+ if re.fullmatch(r"\s*[a-z_]+\s*", line)
231
+ ]
232
+ readme_text = open(this_dir / "README.md", encoding="utf-8").read()
233
+ readme_idx = readme_text.index(
234
+ "## [Documentation]"
235
+ ) # find the Documentation section
236
+ readme_links = list(re.findall(r"latest/\w+.html", readme_text[readme_idx:]))
237
+ if rst_links != readme_links:
238
+ success = False
239
+ chapters_only_in_rst = set(rst_links) - set(readme_links)
240
+ chapters_only_in_readme = set(readme_links) - set(rst_links)
241
+ if chapters_only_in_rst:
242
+ print(
243
+ f"Error: chapters in index.rst, but missing from README.md: {list(chapters_only_in_rst)}",
244
+ file=sys.stderr,
245
+ )
246
+ elif chapters_only_in_readme:
247
+ print(
248
+ f"Error: chapters in README.md, but missing from index.rst: {list(chapters_only_in_readme)}",
249
+ file=sys.stderr,
250
+ )
251
+ else:
252
+ print(
253
+ f"Error: chapters in README.md and index.rst have different orderings. {rst_links} != {readme_links}",
254
+ file=sys.stderr,
255
+ )
256
+
257
+ if not success:
258
+ return -1
259
+
260
+ return 0
261
+
262
+
263
+ if __name__ == "__main__":
264
+ sys.exit(main())