crosshair-tool 0.0.56__cp39-cp39-macosx_11_0_arm64.whl → 0.0.100__cp39-cp39-macosx_11_0_arm64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. _crosshair_tracers.cpython-39-darwin.so +0 -0
  2. crosshair/__init__.py +1 -1
  3. crosshair/_mark_stacks.h +51 -24
  4. crosshair/_tracers.h +9 -5
  5. crosshair/_tracers_test.py +19 -9
  6. crosshair/auditwall.py +9 -8
  7. crosshair/auditwall_test.py +31 -19
  8. crosshair/codeconfig.py +3 -2
  9. crosshair/condition_parser.py +17 -133
  10. crosshair/condition_parser_test.py +54 -96
  11. crosshair/conftest.py +1 -1
  12. crosshair/copyext.py +91 -22
  13. crosshair/copyext_test.py +33 -0
  14. crosshair/core.py +259 -203
  15. crosshair/core_and_libs.py +20 -0
  16. crosshair/core_regestered_types_test.py +82 -0
  17. crosshair/core_test.py +693 -664
  18. crosshair/diff_behavior.py +76 -21
  19. crosshair/diff_behavior_test.py +132 -23
  20. crosshair/dynamic_typing.py +128 -18
  21. crosshair/dynamic_typing_test.py +91 -4
  22. crosshair/enforce.py +1 -6
  23. crosshair/enforce_test.py +15 -23
  24. crosshair/examples/check_examples_test.py +2 -1
  25. crosshair/fnutil.py +2 -3
  26. crosshair/fnutil_test.py +0 -7
  27. crosshair/fuzz_core_test.py +70 -83
  28. crosshair/libimpl/arraylib.py +10 -7
  29. crosshair/libimpl/binascii_ch_test.py +30 -0
  30. crosshair/libimpl/binascii_test.py +67 -0
  31. crosshair/libimpl/binasciilib.py +150 -0
  32. crosshair/libimpl/bisectlib_test.py +5 -5
  33. crosshair/libimpl/builtinslib.py +1002 -682
  34. crosshair/libimpl/builtinslib_ch_test.py +108 -30
  35. crosshair/libimpl/builtinslib_test.py +431 -143
  36. crosshair/libimpl/codecslib.py +22 -2
  37. crosshair/libimpl/codecslib_test.py +41 -9
  38. crosshair/libimpl/collectionslib.py +44 -8
  39. crosshair/libimpl/collectionslib_test.py +108 -20
  40. crosshair/libimpl/copylib.py +1 -1
  41. crosshair/libimpl/copylib_test.py +18 -0
  42. crosshair/libimpl/datetimelib.py +84 -67
  43. crosshair/libimpl/datetimelib_ch_test.py +12 -7
  44. crosshair/libimpl/datetimelib_test.py +5 -6
  45. crosshair/libimpl/decimallib.py +5257 -0
  46. crosshair/libimpl/decimallib_ch_test.py +78 -0
  47. crosshair/libimpl/decimallib_test.py +76 -0
  48. crosshair/libimpl/encodings/_encutil.py +21 -11
  49. crosshair/libimpl/fractionlib.py +16 -0
  50. crosshair/libimpl/fractionlib_test.py +80 -0
  51. crosshair/libimpl/functoolslib.py +19 -7
  52. crosshair/libimpl/functoolslib_test.py +22 -6
  53. crosshair/libimpl/hashliblib.py +30 -0
  54. crosshair/libimpl/hashliblib_test.py +18 -0
  55. crosshair/libimpl/heapqlib.py +32 -5
  56. crosshair/libimpl/heapqlib_test.py +15 -12
  57. crosshair/libimpl/iolib.py +7 -4
  58. crosshair/libimpl/ipaddresslib.py +8 -0
  59. crosshair/libimpl/itertoolslib_test.py +1 -1
  60. crosshair/libimpl/mathlib.py +165 -2
  61. crosshair/libimpl/mathlib_ch_test.py +44 -0
  62. crosshair/libimpl/mathlib_test.py +59 -16
  63. crosshair/libimpl/oslib.py +7 -0
  64. crosshair/libimpl/pathliblib_test.py +10 -0
  65. crosshair/libimpl/randomlib.py +1 -0
  66. crosshair/libimpl/randomlib_test.py +6 -4
  67. crosshair/libimpl/relib.py +180 -59
  68. crosshair/libimpl/relib_ch_test.py +26 -2
  69. crosshair/libimpl/relib_test.py +77 -14
  70. crosshair/libimpl/timelib.py +35 -13
  71. crosshair/libimpl/timelib_test.py +13 -3
  72. crosshair/libimpl/typeslib.py +15 -0
  73. crosshair/libimpl/typeslib_test.py +36 -0
  74. crosshair/libimpl/unicodedatalib_test.py +3 -3
  75. crosshair/libimpl/weakreflib.py +13 -0
  76. crosshair/libimpl/weakreflib_test.py +69 -0
  77. crosshair/libimpl/zliblib.py +15 -0
  78. crosshair/libimpl/zliblib_test.py +13 -0
  79. crosshair/lsp_server.py +21 -10
  80. crosshair/main.py +48 -28
  81. crosshair/main_test.py +59 -14
  82. crosshair/objectproxy.py +39 -14
  83. crosshair/objectproxy_test.py +27 -13
  84. crosshair/opcode_intercept.py +212 -24
  85. crosshair/opcode_intercept_test.py +172 -18
  86. crosshair/options.py +0 -1
  87. crosshair/patch_equivalence_test.py +5 -21
  88. crosshair/path_cover.py +7 -5
  89. crosshair/path_search.py +6 -4
  90. crosshair/path_search_test.py +1 -2
  91. crosshair/pathing_oracle.py +53 -10
  92. crosshair/pathing_oracle_test.py +21 -0
  93. crosshair/pure_importer_test.py +5 -21
  94. crosshair/register_contract.py +16 -6
  95. crosshair/register_contract_test.py +2 -14
  96. crosshair/simplestructs.py +154 -85
  97. crosshair/simplestructs_test.py +16 -2
  98. crosshair/smtlib.py +24 -0
  99. crosshair/smtlib_test.py +14 -0
  100. crosshair/statespace.py +319 -196
  101. crosshair/statespace_test.py +45 -0
  102. crosshair/stubs_parser.py +0 -2
  103. crosshair/test_util.py +87 -25
  104. crosshair/test_util_test.py +26 -0
  105. crosshair/tools/check_init_and_setup_coincide.py +0 -3
  106. crosshair/tools/generate_demo_table.py +2 -2
  107. crosshair/tracers.py +141 -49
  108. crosshair/type_repo.py +11 -4
  109. crosshair/unicode_categories.py +1 -0
  110. crosshair/util.py +158 -76
  111. crosshair/util_test.py +13 -20
  112. crosshair/watcher.py +4 -4
  113. crosshair/z3util.py +1 -1
  114. {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info}/METADATA +45 -36
  115. crosshair_tool-0.0.100.dist-info/RECORD +176 -0
  116. {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info}/WHEEL +2 -1
  117. crosshair/examples/hypothesis/__init__.py +0 -2
  118. crosshair/examples/hypothesis/bugs_detected/simple_strategies.py +0 -74
  119. crosshair_tool-0.0.56.dist-info/RECORD +0 -152
  120. /crosshair/{examples/hypothesis/bugs_detected/__init__.py → py.typed} +0 -0
  121. {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info}/entry_points.txt +0 -0
  122. {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info/licenses}/LICENSE +0 -0
  123. {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info}/top_level.txt +0 -0
@@ -1,13 +1,15 @@
1
1
  import copy
2
2
  import dataclasses
3
3
  import dis
4
+ import enum
4
5
  import inspect
5
6
  import sys
6
7
  import time
7
- from typing import Callable, Dict, Iterable, List, Optional, Set, Tuple, Union
8
+ from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union
8
9
 
10
+ from crosshair import IgnoreAttempt
9
11
  from crosshair.condition_parser import condition_parser
10
- from crosshair.core import ExceptionFilter, Patched, deep_realize, gen_args, realize
12
+ from crosshair.core import ExceptionFilter, Patched, deep_realize, gen_args
11
13
  from crosshair.fnutil import FunctionInfo
12
14
  from crosshair.options import AnalysisOptions
13
15
  from crosshair.statespace import (
@@ -17,14 +19,22 @@ from crosshair.statespace import (
17
19
  StateSpaceContext,
18
20
  VerificationStatus,
19
21
  )
22
+ from crosshair.test_util import flexible_equal
20
23
  from crosshair.tracers import (
21
24
  COMPOSITE_TRACER,
22
25
  CoverageResult,
23
26
  CoverageTracingModule,
24
27
  NoTracing,
25
28
  PushedModule,
29
+ ResumedTracing,
26
30
  )
27
- from crosshair.util import IgnoreAttempt, UnexploredPath, debug
31
+ from crosshair.util import CrosshairUnsupported, IgnoreAttempt, UnexploredPath, debug
32
+
33
+
34
+ class ExceptionEquivalenceType(enum.Enum):
35
+ ALL = "ALL"
36
+ SAME_TYPE = "SAME_TYPE"
37
+ TYPE_AND_MESSAGE = "TYPE_AND_MESSAGE"
28
38
 
29
39
 
30
40
  @dataclasses.dataclass
@@ -65,16 +75,16 @@ class Result:
65
75
 
66
76
  def describe_behavior(
67
77
  fn: Callable, args: inspect.BoundArguments
68
- ) -> Tuple[object, Optional[str]]:
78
+ ) -> Tuple[Any, Optional[BaseException]]:
69
79
  with ExceptionFilter() as efilter:
70
80
  ret = fn(*args.args, **args.kwargs)
71
81
  return (ret, None)
72
82
  if efilter.user_exc is not None:
73
83
  exc = efilter.user_exc[0]
74
84
  debug("user-level exception found", repr(exc), *efilter.user_exc[1])
75
- return (None, repr(exc))
85
+ return (None, exc)
76
86
  if efilter.ignore:
77
- return (None, "IgnoreAttempt")
87
+ return (None, IgnoreAttempt())
78
88
  assert False
79
89
 
80
90
 
@@ -118,21 +128,30 @@ def diff_scorer(
118
128
 
119
129
 
120
130
  def diff_behavior(
121
- ctxfn1: FunctionInfo, ctxfn2: FunctionInfo, options: AnalysisOptions
131
+ ctxfn1: FunctionInfo,
132
+ ctxfn2: FunctionInfo,
133
+ options: AnalysisOptions,
134
+ exception_equivalence: ExceptionEquivalenceType = ExceptionEquivalenceType.TYPE_AND_MESSAGE,
122
135
  ) -> Union[str, List[BehaviorDiff]]:
123
136
  fn1, sig1 = ctxfn1.callable()
124
137
  fn2, sig2 = ctxfn2.callable()
125
138
  debug("Resolved signature:", sig1)
126
139
  all_diffs: List[BehaviorDiff] = []
127
140
  half1, half2 = options.split_limits(0.5)
128
- with condition_parser(options.analysis_kind), Patched(), COMPOSITE_TRACER:
141
+ with condition_parser(
142
+ options.analysis_kind
143
+ ), Patched(), COMPOSITE_TRACER, NoTracing():
129
144
  # We attempt both orderings of functions. This helps by:
130
145
  # (1) avoiding code path explosions in one of the functions
131
146
  # (2) using both signatures (in case they differ)
132
- all_diffs.extend(diff_behavior_with_signature(fn1, fn2, sig1, half1))
147
+ all_diffs.extend(
148
+ diff_behavior_with_signature(fn1, fn2, sig1, half1, exception_equivalence)
149
+ )
133
150
  all_diffs.extend(
134
151
  diff.reverse()
135
- for diff in diff_behavior_with_signature(fn2, fn1, sig2, half2)
152
+ for diff in diff_behavior_with_signature(
153
+ fn2, fn1, sig2, half2, exception_equivalence
154
+ )
136
155
  )
137
156
  debug("diff candidates:", all_diffs)
138
157
  # greedily pick results:
@@ -156,7 +175,11 @@ def diff_behavior(
156
175
 
157
176
 
158
177
  def diff_behavior_with_signature(
159
- fn1: Callable, fn2: Callable, sig: inspect.Signature, options: AnalysisOptions
178
+ fn1: Callable,
179
+ fn2: Callable,
180
+ sig: inspect.Signature,
181
+ options: AnalysisOptions,
182
+ exception_equivalence: ExceptionEquivalenceType,
160
183
  ) -> Iterable[BehaviorDiff]:
161
184
  search_root = RootNode()
162
185
  condition_start = time.monotonic()
@@ -180,7 +203,10 @@ def diff_behavior_with_signature(
180
203
  with StateSpaceContext(space):
181
204
  output = None
182
205
  try:
183
- (verification_status, output) = run_iteration(fn1, fn2, sig, space)
206
+ with ResumedTracing():
207
+ (verification_status, output) = run_iteration(
208
+ fn1, fn2, sig, space, exception_equivalence
209
+ )
184
210
  except IgnoreAttempt:
185
211
  verification_status = None
186
212
  except UnexploredPath:
@@ -209,8 +235,30 @@ def diff_behavior_with_signature(
209
235
  break
210
236
 
211
237
 
238
+ def check_exception_equivalence(
239
+ exception_equivalence_type: ExceptionEquivalenceType,
240
+ exc1: Optional[BaseException],
241
+ exc2: Optional[BaseException],
242
+ ) -> bool:
243
+ if exc1 is not None and exc2 is not None:
244
+ if exception_equivalence_type == ExceptionEquivalenceType.ALL:
245
+ return True
246
+ elif exception_equivalence_type == ExceptionEquivalenceType.SAME_TYPE:
247
+ return type(exc1) == type(exc2)
248
+ elif exception_equivalence_type == ExceptionEquivalenceType.TYPE_AND_MESSAGE:
249
+ return repr(exc1) == repr(exc2)
250
+ else:
251
+ raise CrosshairUnsupported("Invalid exception_equivalence type")
252
+ else:
253
+ return (exc1 is None) and (exc2 is None)
254
+
255
+
212
256
  def run_iteration(
213
- fn1: Callable, fn2: Callable, sig: inspect.Signature, space: StateSpace
257
+ fn1: Callable,
258
+ fn2: Callable,
259
+ sig: inspect.Signature,
260
+ space: StateSpace,
261
+ exception_equivalence: ExceptionEquivalenceType,
214
262
  ) -> Tuple[Optional[VerificationStatus], Optional[BehaviorDiff]]:
215
263
  with NoTracing():
216
264
  original_args = gen_args(sig)
@@ -220,12 +268,19 @@ def run_iteration(
220
268
  with NoTracing():
221
269
  coverage_manager = CoverageTracingModule(fn1, fn2)
222
270
  with ExceptionFilter() as efilter, PushedModule(coverage_manager):
223
- result1 = describe_behavior(fn1, args1)
224
- result2 = describe_behavior(fn2, args2)
225
- space.detach_path()
226
- if result1 == result2 and args1 == args2:
271
+ return1, exc1 = describe_behavior(fn1, args1)
272
+ return2, exc2 = describe_behavior(fn2, args2)
273
+ if (
274
+ flexible_equal(return1, return2)
275
+ and flexible_equal(args1.arguments, args2.arguments)
276
+ and check_exception_equivalence(exception_equivalence, exc1, exc2)
277
+ ):
278
+ # Functions are equivalent if both have the same result,
279
+ # and deemed to have the same kind of error.
280
+ space.detach_path()
227
281
  debug("Functions equivalent")
228
282
  return (VerificationStatus.CONFIRMED, None)
283
+ space.detach_path()
229
284
  debug("Functions differ")
230
285
  realized_args = {
231
286
  k: repr(deep_realize(v)) for (k, v) in original_args.arguments.items()
@@ -239,13 +294,13 @@ def run_iteration(
239
294
  diff = BehaviorDiff(
240
295
  realized_args,
241
296
  Result(
242
- repr(deep_realize(result1[0])),
243
- realize(result1[1]),
297
+ repr(deep_realize(return1)),
298
+ repr(deep_realize(exc1)) if exc1 is not None else None,
244
299
  post_execution_args1,
245
300
  ),
246
301
  Result(
247
- repr(deep_realize(result2[0])),
248
- realize(result2[1]),
302
+ repr(deep_realize(return2)),
303
+ repr(deep_realize(exc2)) if exc2 is not None else None,
249
304
  post_execution_args2,
250
305
  ),
251
306
  coverage_manager.get_results(fn1),
@@ -1,11 +1,13 @@
1
- import sys
2
- import unittest
3
1
  from typing import Callable, List, Optional
4
2
 
5
- from crosshair.diff_behavior import BehaviorDiff, diff_behavior
3
+ from crosshair.diff_behavior import (
4
+ BehaviorDiff,
5
+ ExceptionEquivalenceType,
6
+ diff_behavior,
7
+ )
6
8
  from crosshair.fnutil import FunctionInfo, walk_qualname
7
9
  from crosshair.options import DEFAULT_OPTIONS
8
- from crosshair.util import IgnoreAttempt, debug, set_debug
10
+ from crosshair.util import debug, set_debug
9
11
 
10
12
 
11
13
  def _foo1(x: int) -> int:
@@ -50,7 +52,34 @@ class Derived(Base):
50
52
  return 11
51
53
 
52
54
 
53
- class BehaviorDiffTest(unittest.TestCase):
55
+ def _sum_list_original(int_list):
56
+ count = 0
57
+ for i in int_list:
58
+ count += i
59
+ return count
60
+
61
+
62
+ def _sum_list_rewrite(int_list):
63
+ count = 0
64
+ for i in range(len(int_list)):
65
+ count += int_list[i]
66
+ return count
67
+
68
+
69
+ def _sum_list_rewrite_2(int_list):
70
+ class CustomException(Exception):
71
+ pass
72
+
73
+ try:
74
+ count = 0
75
+ for i in range(len(int_list)):
76
+ count += int_list[i]
77
+ except: # noqa E722
78
+ raise CustomException()
79
+ return count
80
+
81
+
82
+ class TestBehaviorDiff:
54
83
  def test_diff_method(self):
55
84
  diffs = diff_behavior(
56
85
  walk_qualname(Base, "foo"),
@@ -58,10 +87,9 @@ class BehaviorDiffTest(unittest.TestCase):
58
87
  DEFAULT_OPTIONS.overlay(max_iterations=10),
59
88
  )
60
89
  assert isinstance(diffs, list)
61
- self.assertEqual(
62
- [(d.result1.return_repr, d.result2.return_repr) for d in diffs],
63
- [("10", "11")],
64
- )
90
+ assert [(d.result1.return_repr, d.result2.return_repr) for d in diffs] == [
91
+ ("10", "11")
92
+ ]
65
93
 
66
94
  def test_diff_staticmethod(self):
67
95
  diffs = diff_behavior(
@@ -69,20 +97,20 @@ class BehaviorDiffTest(unittest.TestCase):
69
97
  foo2,
70
98
  DEFAULT_OPTIONS.overlay(max_iterations=10),
71
99
  )
72
- self.assertEqual(diffs, [])
100
+ assert diffs == []
73
101
 
74
102
  def test_diff_behavior_same(self) -> None:
75
103
  diffs = diff_behavior(foo1, foo2, DEFAULT_OPTIONS.overlay(max_iterations=10))
76
- self.assertEqual(diffs, [])
104
+ assert diffs == []
77
105
 
78
106
  def test_diff_behavior_different(self) -> None:
79
107
  diffs = diff_behavior(foo1, foo3, DEFAULT_OPTIONS.overlay(max_iterations=10))
80
- self.assertEqual(len(diffs), 1)
108
+ assert len(diffs) == 1
81
109
  diff = diffs[0]
82
110
  assert isinstance(diff, BehaviorDiff)
83
- self.assertGreater(int(diff.args["x"]), 1000)
84
- self.assertEqual(diff.result1.return_repr, "100")
85
- self.assertEqual(diff.result2.return_repr, "1000")
111
+ assert int(diff.args["x"]) > 1000
112
+ assert diff.result1.return_repr == "100"
113
+ assert diff.result2.return_repr == "1000"
86
114
 
87
115
  def test_diff_behavior_mutation(self) -> None:
88
116
  def cut_out_item1(a: List[int], i: int):
@@ -99,10 +127,10 @@ class BehaviorDiffTest(unittest.TestCase):
99
127
  opts,
100
128
  )
101
129
  assert not isinstance(diffs, str)
102
- self.assertEqual(len(diffs), 1)
130
+ assert len(diffs) == 1
103
131
  diff = diffs[0]
104
- self.assertGreater(len(diff.args["a"]), 1)
105
- self.assertEqual(diff.args["i"], "-1")
132
+ assert len(diff.args["a"]) > 1
133
+ assert diff.args["i"] == "-1"
106
134
 
107
135
  def test_example_coverage(self) -> None:
108
136
  # Try to get examples that highlist the differences in the code.
@@ -128,7 +156,7 @@ class BehaviorDiffTest(unittest.TestCase):
128
156
  debug("diffs=", diffs)
129
157
  assert not isinstance(diffs, str)
130
158
  return_vals = set((d.result1.return_repr, d.result2.return_repr) for d in diffs)
131
- self.assertEqual(return_vals, {("False", "None"), ("False", "True")})
159
+ assert return_vals == {("False", "None"), ("False", "True")}
132
160
 
133
161
 
134
162
  def test_diff_behavior_lambda() -> None:
@@ -146,7 +174,88 @@ def test_diff_behavior_lambda() -> None:
146
174
  assert diffs == []
147
175
 
148
176
 
149
- if __name__ == "__main__":
150
- if ("-v" in sys.argv) or ("--verbose" in sys.argv):
151
- set_debug(True)
152
- unittest.main()
177
+ def test_diffbehavior_exceptions_default() -> None:
178
+ """
179
+ Default behavior of `diffbehavior` - treating exceptions as different.
180
+ """
181
+
182
+ diffs = diff_behavior(
183
+ FunctionInfo.from_fn(_sum_list_original),
184
+ FunctionInfo.from_fn(_sum_list_rewrite),
185
+ DEFAULT_OPTIONS,
186
+ )
187
+ debug("diffs=", diffs)
188
+ assert len(diffs) == 1 # finds a counter-example
189
+ assert isinstance(diffs[0], BehaviorDiff)
190
+ assert diffs[0].result1
191
+ assert isinstance(diffs[0].result1.error, str)
192
+ assert isinstance(diffs[0].result2.error, str)
193
+ assert diffs[0].result1.error.startswith("TypeError")
194
+ assert diffs[0].result2.error.startswith("TypeError")
195
+ assert (
196
+ diffs[0].result1.error != diffs[0].result2.error
197
+ ) # Both code-blocks raise a different type error
198
+
199
+
200
+ def test_diffbehavior_exceptions_same_type() -> None:
201
+ """
202
+ Treat exceptions of the same type as equivalent.
203
+ """
204
+
205
+ diffs = diff_behavior(
206
+ FunctionInfo.from_fn(_sum_list_original),
207
+ FunctionInfo.from_fn(_sum_list_rewrite),
208
+ DEFAULT_OPTIONS,
209
+ exception_equivalence=ExceptionEquivalenceType.SAME_TYPE,
210
+ )
211
+ debug("diffs=", diffs)
212
+ assert len(diffs) == 0 # No-counter example, because all TypeErrors are equal
213
+
214
+
215
+ def test_diffbehavior_exceptions_all() -> None:
216
+ """
217
+ Treat exceptions of all types as equivalent.
218
+ """
219
+
220
+ diffs = diff_behavior(
221
+ FunctionInfo.from_fn(_sum_list_original),
222
+ FunctionInfo.from_fn(_sum_list_rewrite_2),
223
+ DEFAULT_OPTIONS,
224
+ exception_equivalence=ExceptionEquivalenceType.ALL,
225
+ )
226
+ debug("diffs=", diffs)
227
+ assert len(diffs) == 0 # No-counter example, because all TypeErrors are equal
228
+
229
+
230
+ def test_diffbehavior_exceptions_same_type_different() -> None:
231
+ """
232
+ Find a counter-example when raising different exception types.
233
+ """
234
+
235
+ diffs = diff_behavior(
236
+ FunctionInfo.from_fn(_sum_list_original),
237
+ FunctionInfo.from_fn(_sum_list_rewrite_2),
238
+ DEFAULT_OPTIONS,
239
+ exception_equivalence=ExceptionEquivalenceType.SAME_TYPE,
240
+ )
241
+ debug("diffs=", diffs)
242
+ assert (
243
+ len(diffs) == 1
244
+ ) # finds a counter-example, because TypeError!=CustomException
245
+ assert isinstance(diffs[0], BehaviorDiff)
246
+ assert isinstance(diffs[0].result1.error, str)
247
+ assert isinstance(diffs[0].result2.error, str)
248
+ assert diffs[0].result1.error.startswith("TypeError")
249
+ assert diffs[0].result2.error.startswith("CustomException")
250
+
251
+
252
+ def test_diff_behavior_nan() -> None:
253
+ def f(x: float):
254
+ return x
255
+
256
+ diffs = diff_behavior(
257
+ FunctionInfo.from_fn(f),
258
+ FunctionInfo.from_fn(f),
259
+ DEFAULT_OPTIONS,
260
+ )
261
+ assert diffs == []
@@ -1,9 +1,14 @@
1
1
  import collections.abc
2
+ import sys
2
3
  import typing
3
- from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Type
4
+ from inspect import Parameter, Signature
5
+ from itertools import zip_longest
6
+ from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple, Type
4
7
 
5
8
  import typing_inspect # type: ignore
6
9
 
10
+ from crosshair.util import debug # type: ignore
11
+
7
12
  _EMPTYSET: frozenset = frozenset()
8
13
 
9
14
 
@@ -54,7 +59,7 @@ def unify_callable_args(
54
59
  return True
55
60
  if len(value_types) != len(recv_types):
56
61
  return False
57
- for (varg, rarg) in zip(value_types, recv_types):
62
+ for varg, rarg in zip(value_types, recv_types):
58
63
  # note reversal here: Callable is contravariant in argument types
59
64
  if not unify(rarg, varg, bindings):
60
65
  return False
@@ -201,7 +206,7 @@ def unify(
201
206
  vargs = [object for _ in rargs]
202
207
  else:
203
208
  return False
204
- for (varg, targ) in zip(vargs, rargs):
209
+ for varg, targ in zip(vargs, rargs):
205
210
  if not unify(varg, targ, bindings):
206
211
  return False
207
212
  return True
@@ -219,18 +224,123 @@ def get_bindings_from_type_arguments(pytype: Type) -> Mapping[object, type]:
219
224
  return {}
220
225
 
221
226
 
222
- def realize(pytype: Type, bindings: Mapping[object, type]) -> object:
223
- if typing_inspect.is_typevar(pytype):
224
- return bindings[pytype]
225
- if not hasattr(pytype, "__args__"):
226
- return pytype
227
- newargs: List = []
228
- for arg in pytype.__args__: # type:ignore
229
- newargs.append(realize(arg, bindings))
230
- # print('realizing pytype', repr(pytype), 'newargs', repr(newargs))
231
- pytype_origin = origin_of(pytype)
232
- if not hasattr(pytype_origin, "_name"):
233
- pytype_origin = getattr(typing, pytype._name) # type:ignore
234
- if pytype_origin is Callable: # Callable args get flattened
235
- newargs = [newargs[:-1], newargs[-1]]
236
- return pytype_origin.__getitem__(tuple(newargs))
227
+ if sys.version_info >= (3, 9):
228
+
229
+ def realize(pytype: Type, bindings: Mapping[object, type]) -> object:
230
+ if typing_inspect.is_typevar(pytype):
231
+ return bindings[pytype]
232
+ if not hasattr(pytype, "__args__"):
233
+ return pytype
234
+ newargs: List = []
235
+ for arg in pytype.__args__: # type:ignore
236
+ newargs.append(realize(arg, bindings))
237
+ pytype_origin = origin_of(pytype)
238
+ if pytype_origin in (
239
+ collections.abc.Callable,
240
+ typing.Callable,
241
+ ): # Callable args get flattened
242
+ newargs = [newargs[:-1], newargs[-1]]
243
+ return pytype_origin.__class_getitem__(tuple(newargs))
244
+
245
+ else:
246
+
247
+ def realize(pytype: Type, bindings: Mapping[object, type]) -> object:
248
+ if typing_inspect.is_typevar(pytype):
249
+ return bindings[pytype]
250
+ if not hasattr(pytype, "__args__"):
251
+ return pytype
252
+ newargs: List = []
253
+ for arg in pytype.__args__: # type:ignore
254
+ newargs.append(realize(arg, bindings))
255
+ # print('realizing pytype', repr(pytype), 'newargs', repr(newargs))
256
+ pytype_origin = origin_of(pytype)
257
+ if not hasattr(pytype_origin, "_name"):
258
+ pytype_origin = getattr(typing, pytype._name) # type:ignore
259
+ if pytype_origin is Callable: # Callable args get flattened
260
+ newargs = [newargs[:-1], newargs[-1]]
261
+ return pytype_origin.__getitem__(tuple(newargs))
262
+
263
+
264
+ def isolate_var_params(
265
+ sig: Signature,
266
+ ) -> Tuple[
267
+ List[Parameter], Dict[str, Parameter], Optional[Parameter], Optional[Parameter]
268
+ ]:
269
+ pos_only_params: List[Parameter] = []
270
+ keyword_params: Dict[str, Parameter] = {}
271
+ var_positional: Optional[Parameter] = None
272
+ var_keyword: Optional[Parameter] = None
273
+ for name, param in sig.parameters.items():
274
+ if param.kind == Parameter.VAR_POSITIONAL:
275
+ var_positional = param
276
+ elif param.kind == Parameter.VAR_KEYWORD:
277
+ var_keyword = param
278
+ elif param.kind == Parameter.POSITIONAL_ONLY:
279
+ pos_only_params.append(param)
280
+ else:
281
+ keyword_params[name] = param
282
+ return pos_only_params, keyword_params, var_positional, var_keyword
283
+
284
+
285
+ def intersect_signatures(
286
+ sig1: Signature,
287
+ sig2: Signature,
288
+ ) -> Signature:
289
+ """
290
+ Approximate the intersection of two signatures.
291
+ The resulting signature may be overly loose
292
+ (matching some inputs that neither of the original signatures would match),
293
+ but it should cover all the inputs for each original signature.
294
+
295
+ One minor exception: All arguments that are allowed to be called as
296
+ keyword arguments will be converted to keyword-only arguments.
297
+ We do this to resolve the abiguity when position-or-keyword arguments
298
+ appear in the same position but with different names.
299
+ """
300
+ pos1, key1, var_pos1, var_key1 = isolate_var_params(sig1)
301
+ pos2, key2, var_pos2, var_key2 = isolate_var_params(sig2)
302
+ is_squishy1 = var_pos1 is not None or var_key1 is not None
303
+ is_squishy2 = var_pos2 is not None or var_key2 is not None
304
+ out_params: Dict[str, Parameter] = {}
305
+ for p1, p2 in zip_longest(pos1, pos2):
306
+ if p1 is None:
307
+ if is_squishy1:
308
+ out_params[p2.name] = p2
309
+ elif p2 is None:
310
+ if is_squishy2:
311
+ out_params[p1.name] = p1
312
+ elif unify(p1.annotation, p2.annotation):
313
+ out_params[p1.name] = p1
314
+ else:
315
+ out_params[p2.name] = p2
316
+ for key in [
317
+ k
318
+ for pair in zip_longest(key1.keys(), key2.keys())
319
+ for k in pair
320
+ if k is not None
321
+ ]:
322
+ if key not in key2:
323
+ if is_squishy2:
324
+ out_params[key] = key1[key].replace(kind=Parameter.KEYWORD_ONLY)
325
+ continue
326
+ if key not in key1:
327
+ if is_squishy1:
328
+ out_params[key] = key2[key].replace(kind=Parameter.KEYWORD_ONLY)
329
+ continue
330
+ if unify(key1[key].annotation, key2[key].annotation):
331
+ out_params[key] = key1[key].replace(kind=Parameter.KEYWORD_ONLY)
332
+ else:
333
+ out_params[key] = key2[key].replace(kind=Parameter.KEYWORD_ONLY)
334
+ if var_pos1 and var_pos2:
335
+ out_params[var_pos1.name] = var_pos1
336
+ if var_key1 and var_key2:
337
+ out_params[var_key1.name] = var_key1
338
+ if unify(sig1.return_annotation, sig2.return_annotation):
339
+ out_return_annotation = sig1.return_annotation
340
+ else:
341
+ out_return_annotation = sig2.return_annotation
342
+ result = Signature(
343
+ parameters=list(out_params.values()), return_annotation=out_return_annotation
344
+ )
345
+ debug("Combined __init__ and __new__ signatures", sig1, "and", sig2, "into", result)
346
+ return result