crosshair-tool 0.0.83__cp313-cp313-macosx_11_0_arm64.whl → 0.0.84__cp313-cp313-macosx_11_0_arm64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (32) hide show
  1. _crosshair_tracers.cpython-313-darwin.so +0 -0
  2. crosshair/__init__.py +1 -1
  3. crosshair/auditwall.py +0 -1
  4. crosshair/auditwall_test.py +5 -0
  5. crosshair/condition_parser_test.py +50 -63
  6. crosshair/core.py +23 -17
  7. crosshair/core_test.py +625 -584
  8. crosshair/diff_behavior_test.py +14 -21
  9. crosshair/dynamic_typing.py +91 -2
  10. crosshair/dynamic_typing_test.py +73 -1
  11. crosshair/enforce_test.py +15 -22
  12. crosshair/fnutil_test.py +0 -7
  13. crosshair/libimpl/arraylib.py +13 -3
  14. crosshair/libimpl/binasciilib.py +2 -3
  15. crosshair/libimpl/builtinslib.py +6 -7
  16. crosshair/libimpl/builtinslib_test.py +1 -8
  17. crosshair/libimpl/collectionslib.py +5 -1
  18. crosshair/libimpl/collectionslib_test.py +79 -13
  19. crosshair/libimpl/encodings/_encutil.py +8 -3
  20. crosshair/libimpl/mathlib_test.py +0 -7
  21. crosshair/libimpl/relib_ch_test.py +2 -2
  22. crosshair/main.py +3 -1
  23. crosshair/objectproxy_test.py +7 -11
  24. crosshair/opcode_intercept.py +1 -0
  25. crosshair/py.typed +0 -0
  26. crosshair/watcher.py +2 -2
  27. {crosshair_tool-0.0.83.dist-info → crosshair_tool-0.0.84.dist-info}/METADATA +4 -3
  28. {crosshair_tool-0.0.83.dist-info → crosshair_tool-0.0.84.dist-info}/RECORD +32 -31
  29. {crosshair_tool-0.0.83.dist-info → crosshair_tool-0.0.84.dist-info}/WHEEL +1 -1
  30. {crosshair_tool-0.0.83.dist-info → crosshair_tool-0.0.84.dist-info}/entry_points.txt +0 -0
  31. {crosshair_tool-0.0.83.dist-info → crosshair_tool-0.0.84.dist-info/licenses}/LICENSE +0 -0
  32. {crosshair_tool-0.0.83.dist-info → crosshair_tool-0.0.84.dist-info}/top_level.txt +0 -0
@@ -81,7 +81,7 @@ def _sum_list_rewrite_2(int_list):
81
81
  return count
82
82
 
83
83
 
84
- class BehaviorDiffTest(unittest.TestCase):
84
+ class TestBehaviorDiff:
85
85
  def test_diff_method(self):
86
86
  diffs = diff_behavior(
87
87
  walk_qualname(Base, "foo"),
@@ -89,10 +89,9 @@ class BehaviorDiffTest(unittest.TestCase):
89
89
  DEFAULT_OPTIONS.overlay(max_iterations=10),
90
90
  )
91
91
  assert isinstance(diffs, list)
92
- self.assertEqual(
93
- [(d.result1.return_repr, d.result2.return_repr) for d in diffs],
94
- [("10", "11")],
95
- )
92
+ assert [(d.result1.return_repr, d.result2.return_repr) for d in diffs] == [
93
+ ("10", "11")
94
+ ]
96
95
 
97
96
  def test_diff_staticmethod(self):
98
97
  diffs = diff_behavior(
@@ -100,20 +99,20 @@ class BehaviorDiffTest(unittest.TestCase):
100
99
  foo2,
101
100
  DEFAULT_OPTIONS.overlay(max_iterations=10),
102
101
  )
103
- self.assertEqual(diffs, [])
102
+ assert diffs == []
104
103
 
105
104
  def test_diff_behavior_same(self) -> None:
106
105
  diffs = diff_behavior(foo1, foo2, DEFAULT_OPTIONS.overlay(max_iterations=10))
107
- self.assertEqual(diffs, [])
106
+ assert diffs == []
108
107
 
109
108
  def test_diff_behavior_different(self) -> None:
110
109
  diffs = diff_behavior(foo1, foo3, DEFAULT_OPTIONS.overlay(max_iterations=10))
111
- self.assertEqual(len(diffs), 1)
110
+ assert len(diffs) == 1
112
111
  diff = diffs[0]
113
112
  assert isinstance(diff, BehaviorDiff)
114
- self.assertGreater(int(diff.args["x"]), 1000)
115
- self.assertEqual(diff.result1.return_repr, "100")
116
- self.assertEqual(diff.result2.return_repr, "1000")
113
+ assert int(diff.args["x"]) > 1000
114
+ assert diff.result1.return_repr == "100"
115
+ assert diff.result2.return_repr == "1000"
117
116
 
118
117
  def test_diff_behavior_mutation(self) -> None:
119
118
  def cut_out_item1(a: List[int], i: int):
@@ -130,10 +129,10 @@ class BehaviorDiffTest(unittest.TestCase):
130
129
  opts,
131
130
  )
132
131
  assert not isinstance(diffs, str)
133
- self.assertEqual(len(diffs), 1)
132
+ assert len(diffs) == 1
134
133
  diff = diffs[0]
135
- self.assertGreater(len(diff.args["a"]), 1)
136
- self.assertEqual(diff.args["i"], "-1")
134
+ assert len(diff.args["a"]) > 1
135
+ assert diff.args["i"] == "-1"
137
136
 
138
137
  def test_example_coverage(self) -> None:
139
138
  # Try to get examples that highlist the differences in the code.
@@ -159,7 +158,7 @@ class BehaviorDiffTest(unittest.TestCase):
159
158
  debug("diffs=", diffs)
160
159
  assert not isinstance(diffs, str)
161
160
  return_vals = set((d.result1.return_repr, d.result2.return_repr) for d in diffs)
162
- self.assertEqual(return_vals, {("False", "None"), ("False", "True")})
161
+ assert return_vals == {("False", "None"), ("False", "True")}
163
162
 
164
163
 
165
164
  def test_diff_behavior_lambda() -> None:
@@ -262,9 +261,3 @@ def test_diff_behavior_nan() -> None:
262
261
  DEFAULT_OPTIONS,
263
262
  )
264
263
  assert diffs == []
265
-
266
-
267
- if __name__ == "__main__":
268
- if ("-v" in sys.argv) or ("--verbose" in sys.argv):
269
- set_debug(True)
270
- unittest.main()
@@ -1,8 +1,12 @@
1
1
  import collections.abc
2
2
  import typing
3
- from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Type
3
+ from inspect import Parameter, Signature
4
+ from itertools import zip_longest
5
+ from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple, Type
4
6
 
5
- import typing_inspect # type: ignore
7
+ import typing_inspect
8
+
9
+ from crosshair.util import debug # type: ignore
6
10
 
7
11
  _EMPTYSET: frozenset = frozenset()
8
12
 
@@ -234,3 +238,88 @@ def realize(pytype: Type, bindings: Mapping[object, type]) -> object:
234
238
  if pytype_origin is Callable: # Callable args get flattened
235
239
  newargs = [newargs[:-1], newargs[-1]]
236
240
  return pytype_origin.__getitem__(tuple(newargs))
241
+
242
+
243
+ def isolate_var_params(
244
+ sig: Signature,
245
+ ) -> Tuple[
246
+ List[Parameter], Dict[str, Parameter], Optional[Parameter], Optional[Parameter]
247
+ ]:
248
+ pos_only_params: List[Parameter] = []
249
+ keyword_params: Dict[str, Parameter] = {}
250
+ var_positional: Optional[Parameter] = None
251
+ var_keyword: Optional[Parameter] = None
252
+ for name, param in sig.parameters.items():
253
+ if param.kind == Parameter.VAR_POSITIONAL:
254
+ var_positional = param
255
+ elif param.kind == Parameter.VAR_KEYWORD:
256
+ var_keyword = param
257
+ elif param.kind == Parameter.POSITIONAL_ONLY:
258
+ pos_only_params.append(param)
259
+ else:
260
+ keyword_params[name] = param
261
+ return pos_only_params, keyword_params, var_positional, var_keyword
262
+
263
+
264
+ def intersect_signatures(
265
+ sig1: Signature,
266
+ sig2: Signature,
267
+ ) -> Signature:
268
+ """
269
+ Approximate the intersection of two signatures.
270
+ The resulting signature may be overly loose
271
+ (matching some inputs that neither of the original signatures would match),
272
+ but it should cover all the inputs for each original signature.
273
+
274
+ One minor exception: All arguments that are allowed to be called as
275
+ keyword arguments will be converted to keyword-only arguments.
276
+ We do this to resolve the abiguity when position-or-keyword arguments
277
+ appear in the same position but with different names.
278
+ """
279
+ pos1, key1, var_pos1, var_key1 = isolate_var_params(sig1)
280
+ pos2, key2, var_pos2, var_key2 = isolate_var_params(sig2)
281
+ is_squishy1 = var_pos1 is not None or var_key1 is not None
282
+ is_squishy2 = var_pos2 is not None or var_key2 is not None
283
+ out_params: Dict[str, Parameter] = {}
284
+ for (p1, p2) in zip_longest(pos1, pos2):
285
+ if p1 is None:
286
+ if is_squishy1:
287
+ out_params[p2.name] = p2
288
+ elif p2 is None:
289
+ if is_squishy2:
290
+ out_params[p1.name] = p1
291
+ elif unify(p1.annotation, p2.annotation):
292
+ out_params[p1.name] = p1
293
+ else:
294
+ out_params[p2.name] = p2
295
+ for key in [
296
+ k
297
+ for pair in zip_longest(key1.keys(), key2.keys())
298
+ for k in pair
299
+ if k is not None
300
+ ]:
301
+ if key not in key2:
302
+ if is_squishy2:
303
+ out_params[key] = key1[key].replace(kind=Parameter.KEYWORD_ONLY)
304
+ continue
305
+ if key not in key1:
306
+ if is_squishy1:
307
+ out_params[key] = key2[key].replace(kind=Parameter.KEYWORD_ONLY)
308
+ continue
309
+ if unify(key1[key].annotation, key2[key].annotation):
310
+ out_params[key] = key1[key].replace(kind=Parameter.KEYWORD_ONLY)
311
+ else:
312
+ out_params[key] = key2[key].replace(kind=Parameter.KEYWORD_ONLY)
313
+ if var_pos1 and var_pos2:
314
+ out_params[var_pos1.name] = var_pos1
315
+ if var_key1 and var_key2:
316
+ out_params[var_key1.name] = var_key1
317
+ if unify(sig1.return_annotation, sig2.return_annotation):
318
+ out_return_annotation = sig1.return_annotation
319
+ else:
320
+ out_return_annotation = sig2.return_annotation
321
+ result = Signature(
322
+ parameters=list(out_params.values()), return_annotation=out_return_annotation
323
+ )
324
+ debug("Combined __init__ and __new__ signatures", sig1, "and", sig2, "into", result)
325
+ return result
@@ -1,4 +1,5 @@
1
1
  import collections
2
+ from inspect import Parameter, Signature, signature
2
3
  from typing import (
3
4
  Callable,
4
5
  Dict,
@@ -6,15 +7,25 @@ from typing import (
6
7
  Iterable,
7
8
  List,
8
9
  Mapping,
10
+ Optional,
9
11
  Sequence,
10
12
  Tuple,
11
13
  TypeVar,
12
14
  Union,
13
15
  )
14
16
 
17
+ import pytest
15
18
  from typing_extensions import TypedDict
16
19
 
17
- from crosshair.dynamic_typing import get_bindings_from_type_arguments, realize, unify
20
+ from crosshair.dynamic_typing import (
21
+ get_bindings_from_type_arguments,
22
+ intersect_signatures,
23
+ realize,
24
+ unify,
25
+ )
26
+ from crosshair.options import AnalysisOptionSet
27
+ from crosshair.statespace import CANNOT_CONFIRM
28
+ from crosshair.test_util import check_states
18
29
 
19
30
  _T = TypeVar("_T")
20
31
  _U = TypeVar("_U")
@@ -121,3 +132,64 @@ def test_bindings_from_type_arguments():
121
132
  var_mapping = get_bindings_from_type_arguments(Pair[int, str])
122
133
  assert var_mapping == {_U: int, _T: str}
123
134
  assert realize(List[_U], var_mapping) == List[int]
135
+
136
+
137
+ def test_intersect_signatures_basic():
138
+ def f1(x: int, y: str, **kw) -> List[bool]:
139
+ return []
140
+
141
+ def f2(x: bool, *extra: str, **kw) -> List[int]:
142
+ return []
143
+
144
+ intersection = intersect_signatures(signature(f1), signature(f2))
145
+ assert intersection is not None
146
+ assert intersection.parameters == {
147
+ "x": Parameter("x", kind=Parameter.KEYWORD_ONLY, annotation=bool),
148
+ "y": Parameter("y", kind=Parameter.KEYWORD_ONLY, annotation=str),
149
+ "kw": Parameter("kw", kind=Parameter.VAR_KEYWORD),
150
+ }
151
+ assert intersection.return_annotation == List[bool]
152
+
153
+
154
+ def test_intersect_signatures_typevars():
155
+ _T = TypeVar("_T")
156
+
157
+ def f1(cc, *args, **kwds):
158
+ pass
159
+
160
+ def f2(dd, left: Optional[_T], right: Optional[_T]):
161
+ pass
162
+
163
+ intersection = intersect_signatures(signature(f1), signature(f2))
164
+ assert intersection is not None
165
+ expected = {
166
+ "dd": Parameter("dd", kind=Parameter.KEYWORD_ONLY),
167
+ "left": Parameter("left", kind=Parameter.KEYWORD_ONLY, annotation=Optional[_T]),
168
+ "right": Parameter(
169
+ "right", kind=Parameter.KEYWORD_ONLY, annotation=Optional[_T]
170
+ ),
171
+ }
172
+ assert intersection.parameters == expected
173
+
174
+
175
+ @pytest.mark.skip(
176
+ reason="The inspect module doesn't expose runtime type information yet"
177
+ )
178
+ def test_intersect_signature_with_crosshair():
179
+ def check_intersect_signatures(
180
+ sig1: Signature, sig2: Signature, pos_args: List, kw_args: Mapping[str, object]
181
+ ) -> None:
182
+ """post: True"""
183
+
184
+ def _sig_bindable(sig: Signature) -> bool:
185
+ try:
186
+ sig.bind(*pos_args, **kw_args)
187
+ return True
188
+ except TypeError:
189
+ return False
190
+
191
+ if _sig_bindable(sig1) or _sig_bindable(sig2):
192
+ intersection = intersect_signatures(sig1, sig2)
193
+ assert _sig_bindable(intersection)
194
+
195
+ check_states(check_intersect_signatures, CANNOT_CONFIRM)
crosshair/enforce_test.py CHANGED
@@ -1,6 +1,5 @@
1
1
  import abc
2
2
  import sys
3
- import unittest
4
3
  from contextlib import ExitStack
5
4
 
6
5
  import pytest
@@ -57,21 +56,21 @@ class Enforcement(ExitStack):
57
56
  COMPOSITE_TRACER.trace_caller()
58
57
 
59
58
 
60
- class CoreTest(unittest.TestCase):
59
+ class TestCore:
61
60
  def test_enforce_conditions(self) -> None:
62
- self.assertEqual(foo(-1), -2) # unchecked
61
+ assert foo(-1) == -2 # unchecked
63
62
  with Enforcement():
64
- self.assertEqual(foo(50), 100)
65
- with self.assertRaises(PreconditionFailed):
63
+ assert foo(50) == 100
64
+ with pytest.raises(PreconditionFailed):
66
65
  foo(-1)
67
- with self.assertRaises(PostconditionFailed):
66
+ with pytest.raises(PostconditionFailed):
68
67
  foo(0)
69
68
 
70
69
  def test_class_enforce(self) -> None:
71
70
  Pokeable().pokeby(-1) # no exception (yet!)
72
71
  with Enforcement():
73
72
  Pokeable().poke()
74
- with self.assertRaises(PreconditionFailed):
73
+ with pytest.raises(PreconditionFailed):
75
74
  Pokeable().pokeby(-1)
76
75
 
77
76
  def test_enforce_on_uncopyable_value(self) -> None:
@@ -81,7 +80,7 @@ class CoreTest(unittest.TestCase):
81
80
 
82
81
  not_copyable = NotCopyable()
83
82
  with Enforcement():
84
- with self.assertRaises(AttributeError):
83
+ with pytest.raises(AttributeError):
85
84
  same_thing(not_copyable)
86
85
 
87
86
 
@@ -114,19 +113,19 @@ class DerivedFooable(BaseFooable):
114
113
  """pre: x > 0"""
115
114
 
116
115
 
117
- class TrickyCasesTest(unittest.TestCase):
116
+ class TestTrickyCases:
118
117
  def test_attrs_restored_properly(self) -> None:
119
118
  orig_class_dict = DerivedFooable.__dict__.copy()
120
119
  with Enforcement():
121
120
  pass
122
121
  for k, v in orig_class_dict.items():
123
- self.assertIs(
124
- DerivedFooable.__dict__[k], v, f'member "{k}" changed afer encforcement'
125
- )
122
+ assert (
123
+ DerivedFooable.__dict__[k] is v
124
+ ), f'member "{k}" changed afer encforcement'
126
125
 
127
126
  def test_enforcement_of_class_methods(self) -> None:
128
127
  with Enforcement():
129
- with self.assertRaises(PreconditionFailed):
128
+ with pytest.raises(PreconditionFailed):
130
129
  BaseFooable.class_foo(50)
131
130
  with Enforcement():
132
131
  DerivedFooable.class_foo(50)
@@ -134,14 +133,14 @@ class TrickyCasesTest(unittest.TestCase):
134
133
  def test_enforcement_of_static_methods(self) -> None:
135
134
  with Enforcement():
136
135
  DerivedFooable.static_foo(50)
137
- with self.assertRaises(PreconditionFailed):
136
+ with pytest.raises(PreconditionFailed):
138
137
  BaseFooable.static_foo(50)
139
138
 
140
139
  def test_super_method_enforced(self) -> None:
141
140
  with Enforcement():
142
- with self.assertRaises(PreconditionFailed):
141
+ with pytest.raises(PreconditionFailed):
143
142
  DerivedFooable().foo_only_in_super(50)
144
- with self.assertRaises(PreconditionFailed):
143
+ with pytest.raises(PreconditionFailed):
145
144
  DerivedFooable().foo(-1)
146
145
  # Derived class has a weaker precondition, so this is OK:
147
146
  DerivedFooable().foo(50)
@@ -181,9 +180,3 @@ def test_enforcement_init_on_abcmeta() -> None:
181
180
  with pytest.raises(PostconditionFailed):
182
181
  WithMetaclass(55)
183
182
  WithMetaclass(99)
184
-
185
-
186
- if __name__ == "__main__":
187
- if ("-v" in sys.argv) or ("--verbose" in sys.argv):
188
- set_debug(True)
189
- unittest.main()
crosshair/fnutil_test.py CHANGED
@@ -1,7 +1,6 @@
1
1
  import builtins
2
2
  import inspect
3
3
  import sys
4
- import unittest
5
4
  from dataclasses import dataclass
6
5
  from typing import Generic
7
6
 
@@ -74,9 +73,3 @@ def test_load_function_at_line():
74
73
 
75
74
  def test_FunctionInfo_get_callable_on_generic():
76
75
  assert FunctionInfo.from_class(Generic, "__class_getitem__").get_callable() is None
77
-
78
-
79
- if __name__ == "__main__":
80
- if ("-v" in sys.argv) or ("--verbose" in sys.argv):
81
- set_debug(True)
82
- unittest.main()
@@ -1,5 +1,6 @@
1
+ import sys
1
2
  from array import array
2
- from typing import BinaryIO, ByteString, Dict, Iterable, List, Sequence, Tuple
3
+ from typing import BinaryIO, Dict, Iterable, List, Sequence, Tuple
3
4
 
4
5
  import z3 # type: ignore
5
6
 
@@ -29,8 +30,17 @@ INT_TYPE_BOUNDS: Dict[str, Tuple[int, int]] = {
29
30
  INT_TYPE_SIZE = {c: array(c).itemsize for c in INT_TYPE_BOUNDS.keys()}
30
31
 
31
32
 
32
- def is_bytes_like(obj: object) -> bool:
33
- return isinstance(obj, (ByteString, array))
33
+ if sys.version_info >= (3, 12):
34
+ from collections.abc import Buffer
35
+
36
+ def is_bytes_like(obj: object) -> bool:
37
+ return isinstance(obj, Buffer)
38
+
39
+ else:
40
+ from collections.abc import ByteString
41
+
42
+ def is_bytes_like(obj: object) -> bool:
43
+ return isinstance(obj, (ByteString, array))
34
44
 
35
45
 
36
46
  def pick_code(space: StateSpace) -> Tuple[str, int, int]:
@@ -1,7 +1,6 @@
1
1
  import binascii
2
- from collections.abc import ByteString
3
2
  from functools import partial
4
- from typing import Dict, Iterable, Tuple
3
+ from typing import Dict, Iterable, Tuple, Union
5
4
 
6
5
  from crosshair.core import register_patch
7
6
  from crosshair.libimpl.builtinslib import _ALL_BYTES_TYPES, SymbolicBytes
@@ -89,7 +88,7 @@ _DECODE_MAPPER_BASE64_STRICT = partial(
89
88
  _ENCODE_MAPPER_BASE64 = partial(_remap, _ENCODE_BASE64_MAP)
90
89
 
91
90
 
92
- def make_bytes(arg: object) -> ByteString:
91
+ def make_bytes(arg: object) -> Union[bytes, bytearray, memoryview]:
93
92
  if isinstance(arg, (bytes, bytearray, memoryview)):
94
93
  return arg
95
94
  if isinstance(arg, str):
@@ -21,7 +21,6 @@ from sys import maxunicode
21
21
  from typing import (
22
22
  Any,
23
23
  BinaryIO,
24
- ByteString,
25
24
  Callable,
26
25
  Dict,
27
26
  FrozenSet,
@@ -127,9 +126,9 @@ from crosshair.util import (
127
126
  from crosshair.z3util import z3And, z3Eq, z3Ge, z3Gt, z3IntVal, z3Not, z3Or
128
127
 
129
128
  if sys.version_info >= (3, 12):
130
- from collections.abc import Buffer as BufferAbc
129
+ from collections.abc import Buffer
131
130
  else:
132
- from collections.abc import ByteString as BufferAbc
131
+ from collections.abc import ByteString as Buffer
133
132
 
134
133
 
135
134
  _T = TypeVar("_T")
@@ -3968,7 +3967,7 @@ def is_ascii_space_ord(char_ord: int):
3968
3967
  )
3969
3968
 
3970
3969
 
3971
- class BytesLike(BufferAbc, AbcString, CrossHairValue):
3970
+ class BytesLike(Buffer, AbcString, CrossHairValue):
3972
3971
  def __eq__(self, other) -> bool:
3973
3972
  if not isinstance(other, _ALL_BYTES_TYPES):
3974
3973
  return False
@@ -5038,11 +5037,11 @@ def _str_percent_format(self, other):
5038
5037
 
5039
5038
 
5040
5039
  def _bytes_join(self, itr) -> str:
5041
- return _join(self, itr, self_type=bytes, item_type=BufferAbc)
5040
+ return _join(self, itr, self_type=bytes, item_type=Buffer)
5042
5041
 
5043
5042
 
5044
5043
  def _bytearray_join(self, itr) -> str:
5045
- return _join(self, itr, self_type=bytearray, item_type=BufferAbc)
5044
+ return _join(self, itr, self_type=bytearray, item_type=Buffer)
5046
5045
 
5047
5046
 
5048
5047
  def _str_format(self, *a, **kw) -> Union[AnySymbolicStr, str]:
@@ -5178,7 +5177,7 @@ def make_registrations():
5178
5177
  register_type(SupportsFloat, lambda p: p(float))
5179
5178
  register_type(SupportsInt, lambda p: p(int))
5180
5179
  register_type(SupportsRound, lambda p: p(float))
5181
- register_type(SupportsBytes, lambda p: p(ByteString))
5180
+ register_type(SupportsBytes, lambda p: p(Buffer))
5182
5181
  register_type(SupportsComplex, lambda p: p(complex))
5183
5182
 
5184
5183
  # Patches
@@ -8,7 +8,6 @@ import math
8
8
  import operator
9
9
  import re
10
10
  import sys
11
- import unittest
12
11
  from abc import ABC, abstractmethod
13
12
  from array import array
14
13
  from numbers import Integral
@@ -2882,7 +2881,7 @@ def test_frozenset___or__(space):
2882
2881
  assert len(s1 | s2) == 2
2883
2882
 
2884
2883
 
2885
- class ProtocolsTest(unittest.TestCase):
2884
+ class TestProtocols:
2886
2885
  # TODO: move most of this into a collectionslib_test.py file
2887
2886
  def test_hashable_values_fail(self) -> None:
2888
2887
  def f(b: bool, i: int, t: Tuple[str, ...]) -> int:
@@ -3657,9 +3656,3 @@ def TODO_test_deepcopy_independence():
3657
3656
  with NoTracing():
3658
3657
  assert ls[0] is not lscopy[0]
3659
3658
  # Next try mutation on one and test the other...
3660
-
3661
-
3662
- if __name__ == "__main__":
3663
- if ("-v" in sys.argv) or ("--verbose" in sys.argv):
3664
- set_debug(True)
3665
- unittest.main()
@@ -1,4 +1,5 @@
1
1
  import collections
2
+ import sys
2
3
  from typing import (
3
4
  Any,
4
5
  Callable,
@@ -245,5 +246,8 @@ def make_registrations():
245
246
 
246
247
  register_type(collections.abc.MutableSet, lambda p, t=Any: p(Set[t])) # type: ignore
247
248
 
248
- register_type(collections.abc.ByteString, lambda p: p(bytes))
249
+ if sys.version_info < (3, 14):
250
+ register_type(collections.abc.ByteString, lambda p: p(bytes))
251
+ if sys.version_info >= (3, 12):
252
+ register_type(collections.abc.Buffer, lambda p: p(bytes))
249
253
  register_type(collections.abc.Hashable, lambda p: p(int))
@@ -1,14 +1,23 @@
1
- import collections
1
+ import sys
2
+ from collections import Counter, defaultdict, deque, namedtuple
2
3
  from copy import deepcopy
3
- from typing import Counter, DefaultDict, Deque, Tuple
4
+ from inspect import Parameter, Signature
5
+ from typing import Counter, DefaultDict, Deque, NamedTuple, Tuple
4
6
 
5
7
  import pytest
6
8
 
7
- from crosshair.core import deep_realize, proxy_for_type, realize, standalone_statespace
9
+ from crosshair.core import (
10
+ deep_realize,
11
+ get_constructor_signature,
12
+ proxy_for_type,
13
+ realize,
14
+ standalone_statespace,
15
+ )
8
16
  from crosshair.libimpl.collectionslib import ListBasedDeque
9
17
  from crosshair.statespace import CANNOT_CONFIRM, CONFIRMED, POST_FAIL, MessageType
10
18
  from crosshair.test_util import check_states
11
19
  from crosshair.tracers import NoTracing, ResumedTracing
20
+ from crosshair.util import CrossHairValue
12
21
 
13
22
 
14
23
  @pytest.fixture
@@ -24,7 +33,7 @@ def test_counter_symbolic_deep(space):
24
33
 
25
34
 
26
35
  def test_counter_deep(space):
27
- d = collections.Counter()
36
+ d = Counter()
28
37
  with ResumedTracing():
29
38
  deep_realize(d)
30
39
  deepcopy(d)
@@ -176,7 +185,7 @@ def test_deque_extendleft_method() -> None:
176
185
  """
177
186
  Can any deque be extended by itself and form this palindrome?
178
187
 
179
- post[ls]: ls != collections.deque([1, 2, 3, 3, 2, 1])
188
+ post[ls]: ls != deque([1, 2, 3, 3, 2, 1])
180
189
  """
181
190
  ls.extendleft(ls)
182
191
 
@@ -185,22 +194,22 @@ def test_deque_extendleft_method() -> None:
185
194
 
186
195
  def test_deque_add_symbolic_to_concrete():
187
196
  with standalone_statespace as space:
188
- d = ListBasedDeque([1, 2]) + collections.deque([3, 4])
197
+ d = ListBasedDeque([1, 2]) + deque([3, 4])
189
198
  assert list(d) == [1, 2, 3, 4]
190
199
 
191
200
 
192
201
  def test_deque_eq():
193
202
  with standalone_statespace as space:
194
203
  assert ListBasedDeque([1, 2]) == ListBasedDeque([1, 2])
195
- assert collections.deque([1, 2]) == ListBasedDeque([1, 2])
204
+ assert deque([1, 2]) == ListBasedDeque([1, 2])
196
205
  assert ListBasedDeque([1, 2]) != ListBasedDeque([1, 55])
197
- assert collections.deque([1, 2]) != ListBasedDeque([1, 55])
206
+ assert deque([1, 2]) != ListBasedDeque([1, 55])
198
207
 
199
208
 
200
209
  def test_defaultdict_repr_equiv(test_list) -> None:
201
210
  def f(symbolic: DefaultDict[int, int]) -> Tuple[dict, dict]:
202
211
  """post: _[0] == _[1]"""
203
- concrete = collections.defaultdict(symbolic.default_factory, symbolic.items())
212
+ concrete = defaultdict(symbolic.default_factory, symbolic.items())
204
213
  return (symbolic, concrete)
205
214
 
206
215
  check_states(f, CANNOT_CONFIRM)
@@ -243,16 +252,73 @@ def test_defaultdict_realize():
243
252
  with standalone_statespace:
244
253
  with NoTracing():
245
254
  d = proxy_for_type(DefaultDict[int, int], "d")
246
- assert type(realize(d)) is collections.defaultdict
255
+ assert type(realize(d)) is defaultdict
247
256
 
248
257
 
249
258
  #
250
- # We don't patch namedtuple, but namedtuple performs magic like dynamic type
259
+ # We don't patch namedtuple, but namedtuple performs magic dynamic type
251
260
  # generation, which can interfere with CrossHair.
252
261
  #
253
262
 
254
263
 
255
264
  def test_namedtuple_creation():
256
265
  with standalone_statespace:
257
- # Ensure type creation doesn't raise exception:
258
- Color = collections.namedtuple("Color", ("name", "hex"))
266
+ # Ensure type creation under trace doesn't raise exception:
267
+ Color = namedtuple("Color", ("name", "hex"))
268
+
269
+
270
+ def test_namedtuple_argument_detection_untyped():
271
+ UntypedColor = namedtuple("UntypedColor", ("name", "hex"))
272
+ expected_signature = Signature(
273
+ parameters=[
274
+ Parameter("name", Parameter.POSITIONAL_OR_KEYWORD),
275
+ Parameter("hex", Parameter.POSITIONAL_OR_KEYWORD),
276
+ ],
277
+ return_annotation=Signature.empty,
278
+ )
279
+ assert get_constructor_signature(UntypedColor) == expected_signature
280
+
281
+
282
+ def test_namedtuple_argument_detection_typed_with_subclass():
283
+ class ClassTypedColor(NamedTuple):
284
+ name: str
285
+ hex: int
286
+
287
+ expected_parameters = {
288
+ "name": Parameter("name", Parameter.POSITIONAL_OR_KEYWORD, annotation=str),
289
+ "hex": Parameter("hex", Parameter.POSITIONAL_OR_KEYWORD, annotation=int),
290
+ }
291
+ assert get_constructor_signature(ClassTypedColor).parameters == expected_parameters
292
+
293
+
294
+ @pytest.mark.skipif(
295
+ sys.version_info < (3, 9),
296
+ reason="Functional namedtuple field types supported on Python >= 3.9",
297
+ )
298
+ def test_namedtuple_argument_detection_typed_functionally():
299
+ FunctionallyTypedColor = NamedTuple(
300
+ "FunctionallyTypedColor", [("name", str), ("hex", int)]
301
+ )
302
+ expected_parameters = {
303
+ "name": Parameter("name", Parameter.POSITIONAL_OR_KEYWORD, annotation=str),
304
+ "hex": Parameter("hex", Parameter.POSITIONAL_OR_KEYWORD, annotation=int),
305
+ }
306
+ assert (
307
+ get_constructor_signature(FunctionallyTypedColor).parameters
308
+ == expected_parameters
309
+ )
310
+
311
+
312
+ @pytest.mark.skipif(
313
+ sys.version_info < (3, 9),
314
+ reason="Functional namedtuple field types supported on Python >= 3.9",
315
+ )
316
+ def test_namedtuple_symbolic_creation(space):
317
+ UntypedColor = namedtuple("Color", "name hex")
318
+ Color = NamedTuple("Color", [("name", str), ("hex", int)])
319
+ untyped_color = proxy_for_type(UntypedColor, "color")
320
+ assert isinstance(untyped_color.hex, CrossHairValue)
321
+ color = proxy_for_type(Color, "color")
322
+ with ResumedTracing():
323
+ assert space.is_possible(color.hex == 5)
324
+ assert space.is_possible(color.hex == 10)