crosshair-tool 0.0.83__cp39-cp39-macosx_10_9_universal2.whl → 0.0.85__cp39-cp39-macosx_10_9_universal2.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 (46) hide show
  1. _crosshair_tracers.cpython-39-darwin.so +0 -0
  2. crosshair/__init__.py +1 -1
  3. crosshair/_mark_stacks.h +0 -25
  4. crosshair/_tracers.h +2 -0
  5. crosshair/_tracers_test.py +8 -2
  6. crosshair/auditwall.py +0 -1
  7. crosshair/auditwall_test.py +5 -0
  8. crosshair/condition_parser.py +5 -5
  9. crosshair/condition_parser_test.py +50 -63
  10. crosshair/copyext.py +23 -7
  11. crosshair/copyext_test.py +11 -1
  12. crosshair/core.py +23 -17
  13. crosshair/core_test.py +625 -584
  14. crosshair/diff_behavior_test.py +14 -21
  15. crosshair/dynamic_typing.py +90 -1
  16. crosshair/dynamic_typing_test.py +73 -1
  17. crosshair/enforce_test.py +15 -22
  18. crosshair/fnutil_test.py +4 -8
  19. crosshair/libimpl/arraylib.py +2 -5
  20. crosshair/libimpl/binasciilib.py +2 -3
  21. crosshair/libimpl/builtinslib.py +28 -21
  22. crosshair/libimpl/builtinslib_test.py +1 -8
  23. crosshair/libimpl/collectionslib.py +18 -3
  24. crosshair/libimpl/collectionslib_test.py +89 -15
  25. crosshair/libimpl/encodings/_encutil.py +8 -3
  26. crosshair/libimpl/mathlib_test.py +0 -7
  27. crosshair/libimpl/relib_ch_test.py +2 -2
  28. crosshair/libimpl/timelib.py +34 -15
  29. crosshair/libimpl/timelib_test.py +12 -2
  30. crosshair/lsp_server.py +1 -1
  31. crosshair/main.py +3 -1
  32. crosshair/objectproxy_test.py +7 -11
  33. crosshair/opcode_intercept.py +24 -8
  34. crosshair/opcode_intercept_test.py +13 -2
  35. crosshair/py.typed +0 -0
  36. crosshair/tracers.py +27 -9
  37. crosshair/type_repo.py +2 -2
  38. crosshair/unicode_categories.py +1 -0
  39. crosshair/util.py +45 -16
  40. crosshair/watcher.py +2 -2
  41. {crosshair_tool-0.0.83.dist-info → crosshair_tool-0.0.85.dist-info}/METADATA +4 -3
  42. {crosshair_tool-0.0.83.dist-info → crosshair_tool-0.0.85.dist-info}/RECORD +46 -45
  43. {crosshair_tool-0.0.83.dist-info → crosshair_tool-0.0.85.dist-info}/WHEEL +1 -1
  44. {crosshair_tool-0.0.83.dist-info → crosshair_tool-0.0.85.dist-info}/entry_points.txt +0 -0
  45. {crosshair_tool-0.0.83.dist-info → crosshair_tool-0.0.85.dist-info/licenses}/LICENSE +0 -0
  46. {crosshair_tool-0.0.83.dist-info → crosshair_tool-0.0.85.dist-info}/top_level.txt +0 -0
Binary file
crosshair/__init__.py CHANGED
@@ -15,7 +15,7 @@ from crosshair.statespace import StateSpace
15
15
  from crosshair.tracers import NoTracing, ResumedTracing
16
16
  from crosshair.util import IgnoreAttempt, debug
17
17
 
18
- __version__ = "0.0.83" # Do not forget to update in setup.py!
18
+ __version__ = "0.0.85" # Do not forget to update in setup.py!
19
19
  __author__ = "Phillip Schanely"
20
20
  __license__ = "MIT"
21
21
  __status__ = "Alpha"
crosshair/_mark_stacks.h CHANGED
@@ -538,31 +538,6 @@ static const uint8_t _ch_DE_INSTRUMENT[256] = {
538
538
  #endif
539
539
  #endif
540
540
 
541
- static const uint8_t _ch_TRACABLE_INSTRUCTIONS[256] = {
542
- // This must be manually kept in sync the the various
543
- // instructions that we care about on the python side.
544
- [MAP_ADD] = 1,
545
- [BINARY_SUBSCR] = 1,
546
- [BINARY_SLICE] = 1,
547
- [CONTAINS_OP] = 1,
548
- [BUILD_STRING] = 1,
549
- #if PY_VERSION_HEX < 0x030D0000
550
- // <= 3.12
551
- [FORMAT_VALUE] = 1,
552
- #elif PY_VERSION_HEX < 0x030E0000
553
- // 3.13
554
- [CALL_KW] = 1,
555
- [CONVERT_VALUE] = 1,
556
- #endif
557
- [UNARY_NOT] = 1,
558
- [SET_ADD] = 1,
559
- [IS_OP] = 1,
560
- [BINARY_OP] = 1,
561
- [CALL] = 1,
562
- [CALL_FUNCTION_EX] = 1,
563
- };
564
-
565
-
566
541
  /* Get the underlying opcode, stripping instrumentation */
567
542
  int _ch_Py_GetBaseOpcode(PyCodeObject *code, int i)
568
543
  {
crosshair/_tracers.h CHANGED
@@ -88,4 +88,6 @@ typedef struct TraceSwap {
88
88
 
89
89
  extern PyTypeObject TraceSwapType;
90
90
 
91
+ extern const uint8_t _ch_TRACABLE_INSTRUCTIONS[256];
92
+
91
93
  #endif /* _COVERAGE_TRACER_H */
@@ -89,7 +89,10 @@ def _log_execution_stacks(fn, *a, **kw):
89
89
  return stacks
90
90
 
91
91
 
92
- @pytest.mark.skipif(sys.version_info < (3, 12), reason="stack depth on 3.12+")
92
+ @pytest.mark.skipif(
93
+ sys.version_info < (3, 12) or sys.version_info >= (3, 14),
94
+ reason="stack depths only in 3.12 & 3.13",
95
+ )
93
96
  def test_one_function_stack_depth():
94
97
  _E = (TypeError, KeyboardInterrupt)
95
98
 
@@ -100,7 +103,10 @@ def test_one_function_stack_depth():
100
103
  _log_execution_stacks(a, 4)
101
104
 
102
105
 
103
- @pytest.mark.skipif(sys.version_info < (3, 12), reason="stack depth on 3.12+")
106
+ @pytest.mark.skipif(
107
+ sys.version_info < (3, 12) or sys.version_info >= (3, 14),
108
+ reason="stack depths only in 3.12 & 3.13",
109
+ )
104
110
  def test_stack_get():
105
111
  def to_be_traced(x):
106
112
  r = 8 - x
crosshair/auditwall.py CHANGED
@@ -135,7 +135,6 @@ def make_handler(event: str) -> Callable[[str, Tuple], None]:
135
135
  "imaplib",
136
136
  "msvcrt",
137
137
  "nntplib",
138
- "os",
139
138
  "pathlib",
140
139
  "poplib",
141
140
  "shutil",
@@ -42,6 +42,10 @@ def test_popen_disallowed():
42
42
  assert call([pyexec, __file__, "popen", "withwall"]) == 10
43
43
 
44
44
 
45
+ def test_chdir_allowed():
46
+ assert call([pyexec, __file__, "chdir", "withwall"]) == 0
47
+
48
+
45
49
  @pytest.mark.skipif(sys.version_info < (3, 9), reason="Python 3.9+ required")
46
50
  def test_popen_via_platform_allowed():
47
51
  assert call([pyexec, __file__, "popen_via_platform", "withwall"]) == 0
@@ -58,6 +62,7 @@ _ACTIONS = {
58
62
  "popen_via_platform": lambda: platform._syscmd_ver( # type: ignore
59
63
  supported_platforms=(sys.platform,)
60
64
  ),
65
+ "chdir": lambda: os.chdir("."),
61
66
  }
62
67
 
63
68
  if __name__ == "__main__":
@@ -47,6 +47,7 @@ from crosshair.options import AnalysisKind
47
47
  from crosshair.register_contract import get_contract
48
48
  from crosshair.tracers import NoTracing
49
49
  from crosshair.util import (
50
+ CrossHairInternal,
50
51
  DynamicScopeVar,
51
52
  EvalFriendlyReprContext,
52
53
  IdKeyedDict,
@@ -485,6 +486,7 @@ class ConcreteConditionParser(ConditionParser):
485
486
  method = cls.__dict__.get(method_name, None)
486
487
  super_method_conditions = super_methods.get(method_name)
487
488
  if super_method_conditions is not None:
489
+ # Re-type the super's `self` argument to be this class:
488
490
  revised_sig = set_first_arg_type(super_method_conditions.sig, cls)
489
491
  super_method_conditions = replace(
490
492
  super_method_conditions, sig=revised_sig
@@ -511,17 +513,15 @@ class ConcreteConditionParser(ConditionParser):
511
513
  final_pre = list(conditions.pre)
512
514
  final_post = list(conditions.post)
513
515
  if method_name in (
514
- "__new__", # isn't passed a concrete instance.
516
+ "__new__", # a staticmethod, but not isinstance(staticmethod)
515
517
  "__repr__", # is itself required for reporting problems with invariants.
516
518
  # [set/del]attr can do anything; we can't resonably enforce invariants:
517
519
  "__setattr__",
518
520
  "__delattr__",
521
+ "__replace__", # Will raise an exception with most arbitrary **kwargs.
522
+ "__annotate__", # a staticmethod, but not isinstance(staticmethod)
519
523
  ):
520
524
  pass
521
- elif method_name == "__replace__":
522
- # TODO: remove this case when fixed in 3.13
523
- # see https://github.com/python/cpython/issues/114198
524
- pass
525
525
  elif method_name == "__del__":
526
526
  final_pre.extend(inv)
527
527
  elif method_name == "__init__":
@@ -133,41 +133,32 @@ def test_parse_sphinx_raises() -> None:
133
133
  assert parse_sphinx_raises(sphinx_raises) == {LocallyDefiendException}
134
134
 
135
135
 
136
- class Pep316ParserTest(unittest.TestCase):
136
+ class TestPep316Parser:
137
137
  def test_class_parse(self) -> None:
138
138
  class_conditions = Pep316Parser().get_class_conditions(Foo)
139
- self.assertEqual(
140
- set([c.expr_source for c in class_conditions.inv]),
141
- set(["self.x >= 0", "self.y >= 0"]),
142
- )
143
- self.assertEqual(
144
- set(class_conditions.methods.keys()), set(["isready", "__init__"])
145
- )
139
+ assert set([c.expr_source for c in class_conditions.inv]) == {
140
+ "self.x >= 0",
141
+ "self.y >= 0",
142
+ }
143
+ assert {"isready", "__init__"} <= set(class_conditions.methods.keys())
146
144
  method = class_conditions.methods["isready"]
147
- self.assertEqual(
148
- set([c.expr_source for c in method.pre]),
149
- set(["self.x >= 0", "self.y >= 0"]),
150
- )
145
+ assert set([c.expr_source for c in method.pre]) == {
146
+ "self.x >= 0",
147
+ "self.y >= 0",
148
+ }
151
149
  startlineno = inspect.getsourcelines(Foo)[1]
152
- self.assertEqual(
153
- set([(c.expr_source, c.line) for c in method.post]),
154
- set(
155
- [
156
- ("self.x >= 0", startlineno + 7),
157
- ("self.y >= 0", startlineno + 12),
158
- ("__return__ == (self.x == 0)", startlineno + 24),
159
- ]
160
- ),
161
- )
150
+ assert set([(c.expr_source, c.line) for c in method.post]) == {
151
+ ("self.x >= 0", startlineno + 7),
152
+ ("self.y >= 0", startlineno + 12),
153
+ ("__return__ == (self.x == 0)", startlineno + 24),
154
+ }
162
155
 
163
156
  def test_single_line_condition(self) -> None:
164
157
  conditions = Pep316Parser().get_fn_conditions(
165
158
  FunctionInfo.from_fn(single_line_condition)
166
159
  )
167
160
  assert conditions is not None
168
- self.assertEqual(
169
- set([c.expr_source for c in conditions.post]), set(["__return__ >= x"])
170
- )
161
+ assert set([c.expr_source for c in conditions.post]) == {"__return__ >= x"}
171
162
 
172
163
  def test_implies_condition(self):
173
164
  conditions = Pep316Parser().get_fn_conditions(
@@ -182,37 +173,35 @@ class Pep316ParserTest(unittest.TestCase):
182
173
  FunctionInfo.from_fn(locally_defined_raises_condition)
183
174
  )
184
175
  assert conditions is not None
185
- self.assertEqual([], list(conditions.syntax_messages()))
186
- self.assertEqual(set([LocallyDefiendException]), conditions.raises)
176
+ assert [] == list(conditions.syntax_messages())
177
+ assert set([LocallyDefiendException]) == conditions.raises
187
178
 
188
179
  def test_tricky_raises_condition(self) -> None:
189
180
  conditions = Pep316Parser().get_fn_conditions(
190
181
  FunctionInfo.from_fn(tricky_raises_condition)
191
182
  )
192
183
  assert conditions is not None
193
- self.assertEqual([], list(conditions.syntax_messages()))
194
- self.assertEqual(conditions.raises, set([KeyError, json.JSONDecodeError]))
184
+ assert [] == list(conditions.syntax_messages())
185
+ assert conditions.raises == {KeyError, json.JSONDecodeError}
195
186
 
196
187
  def test_invariant_is_inherited(self) -> None:
197
188
  class_conditions = Pep316Parser().get_class_conditions(SubClassExample)
198
- self.assertEqual(set(class_conditions.methods.keys()), set(["foo", "__init__"]))
189
+ assert set(class_conditions.methods.keys()) == {"foo", "__init__"}
199
190
  method = class_conditions.methods["foo"]
200
- self.assertEqual(len(method.pre), 1)
201
- self.assertEqual(set([c.expr_source for c in method.pre]), set(["True"]))
202
- self.assertEqual(len(method.post), 2)
203
- self.assertEqual(
204
- set([c.expr_source for c in method.post]), set(["True", "False"])
205
- )
191
+ assert len(method.pre) == 1
192
+ assert set([c.expr_source for c in method.pre]) == {"True"}
193
+ assert len(method.post) == 2
194
+ assert set([c.expr_source for c in method.post]) == {"True", "False"}
206
195
 
207
196
  def test_invariant_applies_to_init(self) -> None:
208
197
  class_conditions = Pep316Parser().get_class_conditions(BaseClassExample)
209
- self.assertEqual(set(class_conditions.methods.keys()), set(["__init__", "foo"]))
198
+ assert set(class_conditions.methods.keys()) == {"__init__", "foo"}
210
199
 
211
200
  @pytest.mark.skipif(
212
201
  sys.version_info >= (3, 13), reason="builtins have signatures in 3.13"
213
202
  )
214
203
  def test_builtin_conditions_are_null(self) -> None:
215
- self.assertIsNone(Pep316Parser().get_fn_conditions(FunctionInfo.from_fn(zip)))
204
+ assert Pep316Parser().get_fn_conditions(FunctionInfo.from_fn(zip)) is None
216
205
 
217
206
  def test_conditions_with_closure_references_and_string_type(self) -> None:
218
207
  # This is a function that refers to something in its closure.
@@ -228,7 +217,7 @@ class Pep316ParserTest(unittest.TestCase):
228
217
 
229
218
 
230
219
  @pytest.mark.skipif(not icontract, reason="icontract is not installed")
231
- class IcontractParserTest(unittest.TestCase):
220
+ class TestIcontractParser:
232
221
  def test_simple_parse(self):
233
222
  @icontract.require(lambda ls: len(ls) > 0)
234
223
  @icontract.ensure(lambda ls, result: min(ls) <= result <= max(ls))
@@ -237,17 +226,17 @@ class IcontractParserTest(unittest.TestCase):
237
226
 
238
227
  conditions = IcontractParser().get_fn_conditions(FunctionInfo.from_fn(avg))
239
228
  assert conditions is not None
240
- self.assertEqual(len(conditions.pre), 1)
241
- self.assertEqual(len(conditions.post), 1)
242
- self.assertEqual(conditions.pre[0].evaluate({"ls": []}), False)
229
+ assert len(conditions.pre) == 1
230
+ assert len(conditions.post) == 1
231
+ assert conditions.pre[0].evaluate({"ls": []}) is False
243
232
  post_args = {
244
233
  "ls": [42, 43],
245
234
  "__old__": AttributeHolder({}),
246
235
  "__return__": 40,
247
236
  "_": 40,
248
237
  }
249
- self.assertEqual(conditions.post[0].evaluate(post_args), False)
250
- self.assertEqual(len(post_args), 4) # (check args are unmodified)
238
+ assert conditions.post[0].evaluate(post_args) is False
239
+ assert len(post_args) == 4 # (check args are unmodified)
251
240
 
252
241
  def test_simple_class_parse(self):
253
242
  @icontract.invariant(lambda self: self.i >= 0)
@@ -268,14 +257,14 @@ class IcontractParserTest(unittest.TestCase):
268
257
  self.i -= 1
269
258
 
270
259
  conditions = IcontractParser().get_class_conditions(Counter)
271
- self.assertEqual(len(conditions.inv), 1)
260
+ assert len(conditions.inv) == 1
272
261
 
273
262
  decr_conditions = conditions.methods["decr"]
274
- self.assertEqual(len(decr_conditions.pre), 2)
263
+ assert len(decr_conditions.pre) == 2
275
264
  # decr() precondition: count > 0
276
- self.assertEqual(decr_conditions.pre[0].evaluate({"self": Counter()}), False)
265
+ assert decr_conditions.pre[0].evaluate({"self": Counter()}) is False
277
266
  # invariant: count >= 0
278
- self.assertEqual(decr_conditions.pre[1].evaluate({"self": Counter()}), True)
267
+ assert decr_conditions.pre[1].evaluate({"self": Counter()}) is True
279
268
 
280
269
  class TruncatedCounter(Counter):
281
270
  @icontract.require(
@@ -287,21 +276,19 @@ class IcontractParserTest(unittest.TestCase):
287
276
 
288
277
  conditions = IcontractParser().get_class_conditions(TruncatedCounter)
289
278
  decr_conditions = conditions.methods["decr"]
290
- self.assertEqual(
291
- decr_conditions.pre[0].evaluate({"self": TruncatedCounter()}), True
292
- )
279
+ assert decr_conditions.pre[0].evaluate({"self": TruncatedCounter()}) is True
293
280
 
294
281
  # check the weakened precondition
295
- self.assertEqual(
296
- len(decr_conditions.pre), 2
282
+ assert (
283
+ len(decr_conditions.pre) == 2
297
284
  ) # one for the invariant, one for the disjunction
298
285
  ctr = TruncatedCounter()
299
286
  ctr.i = 1
300
- self.assertEqual(decr_conditions.pre[1].evaluate({"self": ctr}), True)
301
- self.assertEqual(decr_conditions.pre[0].evaluate({"self": ctr}), True)
287
+ assert decr_conditions.pre[1].evaluate({"self": ctr}) is True
288
+ assert decr_conditions.pre[0].evaluate({"self": ctr}) is True
302
289
  ctr.i = 0
303
- self.assertEqual(decr_conditions.pre[1].evaluate({"self": ctr}), True)
304
- self.assertEqual(decr_conditions.pre[0].evaluate({"self": ctr}), True)
290
+ assert decr_conditions.pre[1].evaluate({"self": ctr}) is True
291
+ assert decr_conditions.pre[0].evaluate({"self": ctr}) is True
305
292
 
306
293
 
307
294
  @pytest.mark.skipif(not deal, reason="deal is not installed")
@@ -395,20 +382,20 @@ def fn_with_docstring_comments_and_assert(numbers: List[int]) -> None:
395
382
  assert min(numbers) > smallest
396
383
 
397
384
 
398
- class AssertsParserTest(unittest.TestCase):
385
+ class TestAssertsParser:
399
386
  def tests_simple_parse(self) -> None:
400
387
  conditions = AssertsParser().get_fn_conditions(
401
388
  FunctionInfo.from_fn(avg_with_asserts)
402
389
  )
403
390
  assert conditions is not None
404
391
  conditions.fn([])
405
- self.assertEqual(conditions.fn([2.2]), 2.2)
406
- with self.assertRaises(AssertionError):
392
+ assert conditions.fn([2.2]) == 2.2
393
+ with pytest.raises(AssertionError):
407
394
  conditions.fn([9.2, 17.8])
408
395
 
409
396
  def tests_empty_parse(self) -> None:
410
397
  conditions = AssertsParser().get_fn_conditions(FunctionInfo.from_fn(debug))
411
- self.assertEqual(conditions, None)
398
+ assert conditions is None
412
399
 
413
400
  def tests_extra_ast_nodes(self) -> None:
414
401
  conditions = AssertsParser().get_fn_conditions(
@@ -422,10 +409,10 @@ class AssertsParserTest(unittest.TestCase):
422
409
  # normal, passing case:
423
410
  nums = [3, 1, 2]
424
411
  conditions.fn(nums)
425
- self.assertEqual(nums, [3, 2])
412
+ assert nums == [3, 2]
426
413
 
427
414
  # Failing case (duplicate minimum values):
428
- with self.assertRaises(AssertionError):
415
+ with pytest.raises(AssertionError):
429
416
  nums = [3, 1, 1, 2]
430
417
  conditions.fn(nums)
431
418
 
crosshair/copyext.py CHANGED
@@ -1,4 +1,9 @@
1
- from copy import _deepcopy_atomic # type: ignore
1
+ import sys
2
+
3
+ if sys.version_info >= (3, 14):
4
+ from copy import _atomic_types
5
+ else:
6
+ from copy import _deepcopy_atomic # type: ignore
2
7
  from copy import _deepcopy_dict # type: ignore
3
8
  from copy import _deepcopy_dispatch # type: ignore
4
9
  from copy import _deepcopy_list # type: ignore
@@ -9,7 +14,7 @@ from copy import Error
9
14
  from copyreg import dispatch_table # type: ignore
10
15
  from enum import Enum
11
16
  from types import MappingProxyType
12
- from typing import Any, Dict, Tuple
17
+ from typing import Any, Callable, Dict, Tuple
13
18
 
14
19
  from crosshair.tracers import ResumedTracing
15
20
  from crosshair.util import (
@@ -85,17 +90,28 @@ def deepcopyext(obj: object, mode: CopyMode, memo: Dict) -> Any:
85
90
  return cpy
86
91
 
87
92
 
93
+ if sys.version_info >= (3, 14):
94
+
95
+ def lookup_dispatch(cls: type) -> Callable:
96
+ if cls in _atomic_types:
97
+ return lambda obj, memo: obj
98
+ return _deepcopy_dispatch.get(cls)
99
+
100
+ else:
101
+
102
+ def lookup_dispatch(cls: type) -> Callable:
103
+ return _deepcopy_dispatch.get(cls)
104
+
105
+
88
106
  def _deepconstruct(obj: object, mode: CopyMode, memo: Dict):
89
107
  cls = type(obj)
90
108
 
91
109
  def subdeepcopy(obj: object, memo: Dict):
92
110
  return deepcopyext(obj, mode, memo)
93
111
 
94
- if cls in _deepcopy_dispatch:
95
- creator = _deepcopy_dispatch[cls]
96
- if creator is _deepcopy_atomic:
97
- return obj
98
- elif creator in (_deepcopy_dict, _deepcopy_list, _deepcopy_tuple):
112
+ creator = lookup_dispatch(cls)
113
+ if creator is not None:
114
+ if creator in (_deepcopy_dict, _deepcopy_list, _deepcopy_tuple):
99
115
  return creator(obj, memo, deepcopy=subdeepcopy)
100
116
  else:
101
117
  # TODO: We loose subdeepcopy in this case - won't
crosshair/copyext_test.py CHANGED
@@ -7,7 +7,7 @@ import pytest
7
7
  from crosshair.copyext import CopyMode, deepcopyext
8
8
  from crosshair.core_and_libs import proxy_for_type, standalone_statespace
9
9
  from crosshair.libimpl.builtinslib import SymbolicInt
10
- from crosshair.tracers import NoTracing, ResumedTracing
10
+ from crosshair.tracers import NoTracing
11
11
 
12
12
 
13
13
  def test_deepcopyext_best_effort():
@@ -33,6 +33,16 @@ def test_deepcopyext_symbolic_set():
33
33
  deepcopyext(s, CopyMode.REALIZE, {})
34
34
 
35
35
 
36
+ def test_deepcopyext_realize_simple(space):
37
+ x = SymbolicInt("x")
38
+ input = (x,)
39
+ output = deepcopyext(input, CopyMode.REALIZE, {})
40
+ assert input is not output
41
+ assert input[0] is not output[0]
42
+ assert type(input[0]) is SymbolicInt
43
+ assert type(output[0]) is int
44
+
45
+
36
46
  def test_deepcopyext_realize(space):
37
47
  x = SymbolicInt("x")
38
48
  lock = RLock()
crosshair/core.py CHANGED
@@ -429,24 +429,30 @@ def get_constructor_signature(cls: Type) -> Optional[inspect.Signature]:
429
429
  sig = resolve_signature(cls)
430
430
  if isinstance(sig, inspect.Signature):
431
431
  return sig
432
+
433
+ applicable_sigs: List[Signature] = []
432
434
  new_fn = cls.__new__
433
- sig = resolve_signature(new_fn)
434
- # TODO: merge the type signatures of __init__ and __new__, pulling the
435
- # most specific types from each.
436
- # Fall back to __init__ if we don't have types:
437
- if isinstance(sig, str) or all(
438
- p.annotation is Signature.empty for p in sig.parameters.values()
439
- ):
440
- init_fn = cls.__init__
441
- if init_fn is not object.__init__:
442
- sig = resolve_signature(init_fn)
443
- else:
444
- return inspect.Signature([])
445
- if isinstance(sig, inspect.Signature):
446
- # strip first argument
447
- newparams = list(sig.parameters.values())[1:]
448
- return sig.replace(parameters=newparams)
449
- return None
435
+ if new_fn is not object.__new__:
436
+ sig = resolve_signature(new_fn)
437
+ if not isinstance(sig, str):
438
+ applicable_sigs.append(sig)
439
+ init_fn = cls.__init__
440
+ if init_fn is not object.__init__:
441
+ sig = resolve_signature(init_fn)
442
+ if not isinstance(sig, str):
443
+ sig = sig.replace(
444
+ return_annotation=object
445
+ ) # make return types compatible (& use __new__'s return)
446
+ applicable_sigs.append(sig)
447
+ if len(applicable_sigs) == 0:
448
+ return inspect.Signature([])
449
+ if len(applicable_sigs) == 2:
450
+ sig = dynamic_typing.intersect_signatures(*applicable_sigs)
451
+ else:
452
+ sig = applicable_sigs[0]
453
+ # strip first argument ("self" or "cls")
454
+ newparams = list(sig.parameters.values())[1:]
455
+ return sig.replace(parameters=newparams)
450
456
 
451
457
 
452
458
  _TYPE_HINTS = IdKeyedDict()