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
crosshair/main.py CHANGED
@@ -36,7 +36,7 @@ from crosshair.core_and_libs import (
36
36
  installed_plugins,
37
37
  run_checkables,
38
38
  )
39
- from crosshair.diff_behavior import diff_behavior
39
+ from crosshair.diff_behavior import ExceptionEquivalenceType, diff_behavior
40
40
  from crosshair.fnutil import (
41
41
  FUNCTIONINFO_DESCRIPTOR_TYPES,
42
42
  FunctionInfo,
@@ -60,9 +60,9 @@ from crosshair.path_cover import (
60
60
  from crosshair.path_search import OptimizationKind, path_search
61
61
  from crosshair.pure_importer import prefer_pure_python_imports
62
62
  from crosshair.register_contract import REGISTERED_CONTRACTS
63
- from crosshair.statespace import NotDeterministic
64
63
  from crosshair.util import (
65
64
  ErrorDuringImport,
65
+ NotDeterministic,
66
66
  add_to_pypath,
67
67
  debug,
68
68
  format_boundargs,
@@ -85,14 +85,6 @@ def analysis_kind(argstr: str) -> Sequence[AnalysisKind]:
85
85
  ret = [AnalysisKind[part.strip()] for part in argstr.split(",")]
86
86
  except KeyError:
87
87
  raise ValueError
88
- if AnalysisKind.hypothesis in ret:
89
- try:
90
- import hypothesis
91
-
92
- if hypothesis.__version_info__ < (6, 0, 0):
93
- raise Exception("CrossHair requires hypothesis version >= 6.0.0")
94
- except ImportError as e:
95
- raise Exception("Unable to import the hypothesis library") from e
96
88
  return ret
97
89
 
98
90
 
@@ -273,6 +265,21 @@ def command_line_parser() -> argparse.ArgumentParser:
273
265
  type=str,
274
266
  help="second fully-qualified function to compare",
275
267
  )
268
+ diffbehavior_parser.add_argument(
269
+ "--exception_equivalence",
270
+ metavar="EXCEPTION_EQUIVALENCE",
271
+ type=ExceptionEquivalenceType,
272
+ default=ExceptionEquivalenceType.TYPE_AND_MESSAGE,
273
+ choices=ExceptionEquivalenceType.__members__.values(),
274
+ help=textwrap.dedent(
275
+ """\
276
+ Decide how to treat exceptions, while searching for a counter-example.
277
+ `ALL` treats all exceptions as equivalent,
278
+ `SAME_TYPE`, considers matches on the type.
279
+ `TYPE_AND_MESSAGE` matches for the same type and message.
280
+ """
281
+ ),
282
+ )
276
283
  cover_parser = subparsers.add_parser(
277
284
  "cover",
278
285
  formatter_class=argparse.RawTextHelpFormatter,
@@ -413,7 +420,6 @@ def command_line_parser() -> argparse.ArgumentParser:
413
420
  PEP316 : check PEP316 contracts (docstring-based)
414
421
  icontract : check icontract contracts (decorator-based)
415
422
  deal : check deal contracts (decorator-based)
416
- hypothesis : check hypothesis tests
417
423
  """
418
424
  ),
419
425
  )
@@ -442,7 +448,9 @@ def run_watch_loop(
442
448
  active_messages = {}
443
449
  else:
444
450
  time.sleep(0.1)
445
- max_uninteresting_iterations *= 2
451
+ max_uninteresting_iterations = min(
452
+ max_uninteresting_iterations * 2, 100_000_000
453
+ )
446
454
  for curstats, messages in watcher.run_iteration(max_uninteresting_iterations):
447
455
  messages = [m for m in messages if m.state > MessageType.PRE_UNSAT]
448
456
  stats.update(curstats)
@@ -529,10 +537,10 @@ def messages_merged(
529
537
  _MOTD = [
530
538
  "Did I miss a counterexample? Let me know: https://github.com/pschanely/CrossHair/issues/new",
531
539
  "Help me be faster! Add to my benchmark suite: https://github.com/pschanely/crosshair-benchmark",
532
- "Consider sharing your CrossHair experience on YouTube, Twitter, your blog, ... even TikTok.",
540
+ "Please consider sharing your CrossHair experience with others on social media.",
533
541
  "Questions? Ask at https://github.com/pschanely/CrossHair/discussions/new?category=q-a",
534
542
  "Consider signing up for CrossHair updates at https://pschanely.github.io",
535
- # Use CrossHair? We'd like to reference your work here: ...
543
+ "Come say hello in the discord chat; we are friendly! https://discord.gg/rUeTaYTWbb",
536
544
  ]
537
545
 
538
546
 
@@ -688,10 +696,11 @@ def diffbehavior(
688
696
  (fn_name1, fn_name2) = (args.fn1, args.fn2)
689
697
  fn1 = checked_fn_load(fn_name1, stderr)
690
698
  fn2 = checked_fn_load(fn_name2, stderr)
699
+ exception_equivalence = args.exception_equivalence
691
700
  if fn1 is None or fn2 is None:
692
701
  return 2
693
702
  options.stats = Counter()
694
- diffs = diff_behavior(fn1, fn2, options)
703
+ diffs = diff_behavior(fn1, fn2, options, exception_equivalence)
695
704
  debug("stats", options.stats)
696
705
  if isinstance(diffs, str):
697
706
  print(diffs, file=stderr)
@@ -765,9 +774,11 @@ def cover(
765
774
  ctxfn,
766
775
  options,
767
776
  args.coverage_type,
768
- arg_formatter=format_boundargs_as_dictionary
769
- if example_output_format == ExampleOutputFormat.ARG_DICTIONARY
770
- else format_boundargs,
777
+ arg_formatter=(
778
+ format_boundargs_as_dictionary
779
+ if example_output_format == ExampleOutputFormat.ARG_DICTIONARY
780
+ else format_boundargs
781
+ ),
771
782
  )
772
783
  except NotDeterministic:
773
784
  print(
@@ -851,16 +862,24 @@ def check(
851
862
  if isinstance(entities, int):
852
863
  return entities
853
864
  full_options = DEFAULT_OPTIONS.overlay(report_verbose=False).overlay(options)
854
- for entity in entities:
855
- debug("Check ", getattr(entity, "__name__", str(entity)))
856
- for message in run_checkables(analyze_any(entity, options)):
857
- line = describe_message(message, full_options)
858
- if line is None:
859
- continue
860
- stdout.write(line + "\n")
861
- debug("Traceback for output message:\n", message.traceback)
862
- if message.state > MessageType.PRE_UNSAT:
863
- any_problems = True
865
+ checkables = [c for e in entities for c in analyze_any(e, options)]
866
+ if not checkables:
867
+ extra_help = ""
868
+ if full_options.analysis_kind == [AnalysisKind.asserts]:
869
+ extra_help = "\nHINT: Ensure that your functions to analyze lead with assert statements."
870
+ print(
871
+ "WARNING: Targets found, but contain no checkable functions." + extra_help,
872
+ file=stderr,
873
+ )
874
+
875
+ for message in run_checkables(checkables):
876
+ line = describe_message(message, full_options)
877
+ if line is None:
878
+ continue
879
+ stdout.write(line + "\n")
880
+ debug("Traceback for output message:\n", message.traceback)
881
+ if message.state > MessageType.PRE_UNSAT:
882
+ any_problems = True
864
883
  return 1 if any_problems else 0
865
884
 
866
885
 
@@ -936,6 +955,7 @@ def mypy_and_check(cmd_args: Optional[List[str]] = None) -> None:
936
955
  _mypy_out, mypy_err, mypy_ret = api.run(mypy_cmd_args)
937
956
  print(mypy_err, file=sys.stderr)
938
957
  if mypy_ret != 0:
958
+ print(_mypy_out, file=sys.stdout)
939
959
  sys.exit(mypy_ret)
940
960
  engage_auditwall()
941
961
  debug("Running crosshair with these args:", check_args)
crosshair/main_test.py CHANGED
@@ -61,7 +61,12 @@ def call_check(
61
61
  def call_diffbehavior(fn1: str, fn2: str) -> Tuple[int, List[str]]:
62
62
  buf: io.StringIO = io.StringIO()
63
63
  errbuf: io.StringIO = io.StringIO()
64
- retcode = diffbehavior(Namespace(fn1=fn1, fn2=fn2), DEFAULT_OPTIONS, buf, errbuf)
64
+ retcode = diffbehavior(
65
+ Namespace(fn1=fn1, fn2=fn2, exception_equivalence="type_and_message"),
66
+ DEFAULT_OPTIONS,
67
+ buf,
68
+ errbuf,
69
+ )
65
70
  lines = [
66
71
  ls for ls in buf.getvalue().split("\n") + errbuf.getvalue().split("\n") if ls
67
72
  ]
@@ -246,19 +251,28 @@ def test_no_args_prints_usage(root):
246
251
  assert re.search(r"^usage", out)
247
252
 
248
253
 
249
- def DISABLE_TODO_test_assert_mode_e2e(root):
254
+ def test_assert_mode_e2e(root, capsys: pytest.CaptureFixture[str]):
250
255
  simplefs(root, ASSERT_BASED_FOO)
251
- try:
252
- sys.stdout = io.StringIO()
253
- exitcode = unwalled_main(["check", root / "foo.py", "--analysis_kind=asserts"])
254
- finally:
255
- out = sys.stdout.getvalue()
256
- sys.stdout = sys.__stdout__
257
- assert exitcode == 1
256
+ exitcode = unwalled_main(["check", str(root / "foo.py"), "--analysis_kind=asserts"])
257
+ (out, err) = capsys.readouterr()
258
+ assert err == ""
258
259
  assert re.search(
259
- r"foo.py\:8\: error\: AssertionError\: when calling foofn\(x \= 100\)", out
260
+ r"foo.py\:8\: error\: AssertionError\: when calling foofn\(100\)", out
260
261
  )
261
262
  assert len([ls for ls in out.split("\n") if ls]) == 1
263
+ assert exitcode == 1
264
+
265
+
266
+ def test_assert_without_checkable(root, capsys: pytest.CaptureFixture[str]):
267
+ simplefs(root, SIMPLE_FOO)
268
+ exitcode = unwalled_main(["check", str(root / "foo.py"), "--analysis_kind=asserts"])
269
+ (out, err) = capsys.readouterr()
270
+ assert (
271
+ err
272
+ == "WARNING: Targets found, but contain no checkable functions.\nHINT: Ensure that your functions to analyze lead with assert statements.\n"
273
+ )
274
+ assert out == ""
275
+ assert exitcode == 0
262
276
 
263
277
 
264
278
  def test_directives(root):
@@ -359,6 +373,35 @@ def test_check_circular_with_guard(root):
359
373
  assert retcode == 0
360
374
 
361
375
 
376
+ def test_check_not_deterministic(root) -> None:
377
+ NOT_DETERMINISTIC_FOO = {
378
+ "foo.py": """
379
+ _GLOBAL_THING = [42]
380
+
381
+ def wonky_foo(i: int) -> int:
382
+ '''post: True'''
383
+ _GLOBAL_THING[0] += 1
384
+ if i > _GLOBAL_THING[0]:
385
+ pass
386
+ return True
387
+ _GLOBAL_THING = [True]
388
+
389
+ def regular_foo(i: int) -> int:
390
+ '''post: True'''
391
+ return i
392
+ """
393
+ }
394
+ simplefs(root, NOT_DETERMINISTIC_FOO)
395
+ with add_to_pypath(root):
396
+ retcode, lines, errlines = call_check([str(root / "foo.py")])
397
+ assert errlines == []
398
+ assert (
399
+ "NotDeterministic: Found a different execution paths after making the same decisions"
400
+ in lines[0]
401
+ )
402
+ assert retcode == 1
403
+
404
+
362
405
  def test_watch(root):
363
406
  # Just to make sure nothing explodes
364
407
  simplefs(root, SIMPLE_FOO)
@@ -456,7 +499,7 @@ def test_main_as_subprocess(tmp_path: Path):
456
499
  # the testing process.
457
500
  simplefs(tmp_path, SIMPLE_FOO)
458
501
  completion = subprocess.run(
459
- [sys.executable, "-m", "crosshair", "check", str(tmp_path)],
502
+ [sys.executable, "-Werror", "-m", "crosshair", "check", str(tmp_path)],
460
503
  stdin=subprocess.DEVNULL,
461
504
  capture_output=True,
462
505
  text=True,
@@ -468,7 +511,7 @@ def test_main_as_subprocess(tmp_path: Path):
468
511
 
469
512
  def test_mypycrosshair_command():
470
513
  example_file = join(
471
- split(__file__)[0], "examples", "icontract", "bugs_detected", "wrong_sign.py"
514
+ split(__file__)[0], "examples", "PEP316", "bugs_detected", "showcase.py"
472
515
  )
473
516
  completion = subprocess.run(
474
517
  [
@@ -481,8 +524,10 @@ def test_mypycrosshair_command():
481
524
  capture_output=True,
482
525
  text=True,
483
526
  )
484
- assert completion.stderr.strip() == ""
485
- assert completion.returncode == 1
527
+ assert completion.stderr.strip() == "", f"stderr was '''{completion.stderr}'''"
528
+ if completion.returncode != 1:
529
+ print(completion.stdout)
530
+ assert False, f"Return code was {completion.returncode}"
486
531
 
487
532
 
488
533
  def test_describe_message():
crosshair/objectproxy.py CHANGED
@@ -10,11 +10,36 @@ from crosshair.tracers import NoTracing
10
10
  # (which is BSD licenced)
11
11
  #
12
12
 
13
+ _MISSING = object()
14
+
15
+
16
+ def proxy_inplace_op(proxy, op, *args):
17
+ my_original_value = proxy._wrapped()
18
+ my_new_value = op(my_original_value, *args)
19
+ # We need to return our own identity if (and only if!) the underlying value does.
20
+ if my_new_value is my_original_value:
21
+ return proxy
22
+ else:
23
+ object.__setattr__(proxy, "_inner", my_new_value)
24
+ return my_new_value
25
+
13
26
 
14
27
  class ObjectProxy:
15
- def _wrapped(self):
28
+ def _realize(self):
16
29
  raise NotImplementedError
17
30
 
31
+ def _wrapped(self):
32
+ with NoTracing():
33
+ inner = _MISSING
34
+ try:
35
+ inner = object.__getattribute__(self, "_inner")
36
+ except AttributeError:
37
+ pass
38
+ if inner is _MISSING:
39
+ inner = self._realize()
40
+ object.__setattr__(self, "_inner", inner)
41
+ return inner
42
+
18
43
  def __get_module__(self) -> str:
19
44
  return self._wrapped().__module__
20
45
 
@@ -233,40 +258,40 @@ class ObjectProxy:
233
258
  return other | self._wrapped()
234
259
 
235
260
  def __iadd__(self, other):
236
- return operator.iadd(self._wrapped(), other)
261
+ return proxy_inplace_op(self, operator.iadd, other)
237
262
 
238
263
  def __isub__(self, other):
239
- return operator.isub(self._wrapped(), other)
264
+ return proxy_inplace_op(self, operator.isub, other)
240
265
 
241
266
  def __imul__(self, other):
242
- return operator.imul(self._wrapped(), other)
267
+ return proxy_inplace_op(self, operator.imul, other)
243
268
 
244
269
  def __itruediv__(self, other):
245
- return operator.itruediv(self._wrapped(), other)
270
+ return proxy_inplace_op(self, operator.itruediv, other)
246
271
 
247
272
  def __ifloordiv__(self, other):
248
- return operator.iflootdiv(self._wrapped(), other)
273
+ return proxy_inplace_op(self, operator.ifloordiv, other)
249
274
 
250
275
  def __imod__(self, other):
251
- return operator.imod(self._wrapped(), other)
276
+ return proxy_inplace_op(self, operator.imod, other)
252
277
 
253
278
  def __ipow__(self, other, *args):
254
- return operator.ipow(self._wrapped(), other, *args)
279
+ return proxy_inplace_op(self, operator.ipow, other, *args)
255
280
 
256
281
  def __ilshift__(self, other):
257
- return operator.ilshift(self._wrapped(), other)
282
+ return proxy_inplace_op(self, operator.ilshift, other)
258
283
 
259
284
  def __irshift__(self, other):
260
- return operator.irshift(self._wrapped(), other)
285
+ return proxy_inplace_op(self, operator.irshift, other)
261
286
 
262
287
  def __iand__(self, other):
263
- return operator.iand(self._wrapped(), other)
288
+ return proxy_inplace_op(self, operator.iand, other)
264
289
 
265
290
  def __ixor__(self, other):
266
- return operator.ixor(self._wrapped(), other)
291
+ return proxy_inplace_op(self, operator.ixor, other)
267
292
 
268
293
  def __ior__(self, other):
269
- return operator.ior(self._wrapped(), other)
294
+ return proxy_inplace_op(self, operator.ior, other)
270
295
 
271
296
  def __neg__(self):
272
297
  return -self._wrapped()
@@ -335,7 +360,7 @@ class ObjectProxy:
335
360
  return copy.copy(self._wrapped())
336
361
 
337
362
  def __deepcopy__(self, memo):
338
- ret = copy.deepcopy(self._wrapped())
363
+ ret = copy.deepcopy(self._wrapped(), memo)
339
364
  memo[id(self)] = ret
340
365
  return ret
341
366
 
@@ -1,4 +1,4 @@
1
- import unittest
1
+ import pytest
2
2
 
3
3
  from crosshair.objectproxy import ObjectProxy
4
4
 
@@ -7,21 +7,35 @@ class ObjectWrap(ObjectProxy):
7
7
  def __init__(self, obj):
8
8
  object.__setattr__(self, "_o", obj)
9
9
 
10
- def _wrapped(self):
10
+ def _realize(self):
11
11
  return object.__getattribute__(self, "_o")
12
12
 
13
13
 
14
- class ObjectProxyTest(unittest.TestCase):
15
- def test_object_proxy(self) -> None:
14
+ class TestObjectProxy:
15
+ def test_object_proxy_over_list(self) -> None:
16
16
  i = [1, 2, 3]
17
17
  proxy = ObjectWrap(i)
18
- self.assertEqual(i, proxy)
18
+ assert i == proxy
19
19
  proxy.append(4)
20
- self.assertEqual([1, 2, 3, 4], proxy)
21
- self.assertEqual([1, 2, 3, 4, 5], proxy + [5])
22
- self.assertEqual([2, 3], proxy[1:3])
23
- self.assertEqual([1, 2, 3, 4], proxy)
24
-
25
-
26
- if __name__ == "__main__":
27
- unittest.main()
20
+ assert [1, 2, 3, 4] == proxy
21
+ assert [1, 2, 3, 4, 5] == proxy + [5]
22
+ assert [2, 3] == proxy[1:3]
23
+ assert [1, 2, 3, 4] == proxy
24
+
25
+ def test_inplace_identities(self) -> None:
26
+ proxy = ObjectWrap(3.0)
27
+ orig_proxy = proxy
28
+ proxy += 1.0
29
+ assert proxy is not orig_proxy
30
+ proxy = ObjectWrap([1, 2])
31
+ orig_proxy = proxy
32
+ proxy += [3, 4]
33
+ assert proxy is orig_proxy
34
+
35
+ def test_object_proxy_over_float(self) -> None:
36
+ proxy = ObjectWrap(4.5)
37
+ proxy //= 2.0
38
+ assert 2.0 == proxy
39
+ proxy = ObjectWrap(5.0)
40
+ proxy /= 2.0
41
+ assert 2.5 == proxy