crosshair-tool 0.0.99__cp312-cp312-macosx_10_13_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. _crosshair_tracers.cpython-312-darwin.so +0 -0
  2. crosshair/__init__.py +42 -0
  3. crosshair/__main__.py +8 -0
  4. crosshair/_mark_stacks.h +790 -0
  5. crosshair/_preliminaries_test.py +18 -0
  6. crosshair/_tracers.h +94 -0
  7. crosshair/_tracers_pycompat.h +522 -0
  8. crosshair/_tracers_test.py +138 -0
  9. crosshair/abcstring.py +245 -0
  10. crosshair/auditwall.py +190 -0
  11. crosshair/auditwall_test.py +77 -0
  12. crosshair/codeconfig.py +113 -0
  13. crosshair/codeconfig_test.py +117 -0
  14. crosshair/condition_parser.py +1237 -0
  15. crosshair/condition_parser_test.py +497 -0
  16. crosshair/conftest.py +30 -0
  17. crosshair/copyext.py +155 -0
  18. crosshair/copyext_test.py +84 -0
  19. crosshair/core.py +1763 -0
  20. crosshair/core_and_libs.py +149 -0
  21. crosshair/core_regestered_types_test.py +82 -0
  22. crosshair/core_test.py +1316 -0
  23. crosshair/diff_behavior.py +314 -0
  24. crosshair/diff_behavior_test.py +261 -0
  25. crosshair/dynamic_typing.py +346 -0
  26. crosshair/dynamic_typing_test.py +210 -0
  27. crosshair/enforce.py +282 -0
  28. crosshair/enforce_test.py +182 -0
  29. crosshair/examples/PEP316/__init__.py +1 -0
  30. crosshair/examples/PEP316/bugs_detected/__init__.py +0 -0
  31. crosshair/examples/PEP316/bugs_detected/getattr_magic.py +16 -0
  32. crosshair/examples/PEP316/bugs_detected/hash_consistent_with_equals.py +31 -0
  33. crosshair/examples/PEP316/bugs_detected/shopping_cart.py +24 -0
  34. crosshair/examples/PEP316/bugs_detected/showcase.py +39 -0
  35. crosshair/examples/PEP316/correct_code/__init__.py +0 -0
  36. crosshair/examples/PEP316/correct_code/arith.py +60 -0
  37. crosshair/examples/PEP316/correct_code/chess.py +77 -0
  38. crosshair/examples/PEP316/correct_code/nesting_inference.py +17 -0
  39. crosshair/examples/PEP316/correct_code/numpy_examples.py +132 -0
  40. crosshair/examples/PEP316/correct_code/rolling_average.py +35 -0
  41. crosshair/examples/PEP316/correct_code/showcase.py +104 -0
  42. crosshair/examples/__init__.py +0 -0
  43. crosshair/examples/check_examples_test.py +146 -0
  44. crosshair/examples/deal/__init__.py +1 -0
  45. crosshair/examples/icontract/__init__.py +1 -0
  46. crosshair/examples/icontract/bugs_detected/__init__.py +0 -0
  47. crosshair/examples/icontract/bugs_detected/showcase.py +41 -0
  48. crosshair/examples/icontract/bugs_detected/wrong_sign.py +8 -0
  49. crosshair/examples/icontract/correct_code/__init__.py +0 -0
  50. crosshair/examples/icontract/correct_code/arith.py +51 -0
  51. crosshair/examples/icontract/correct_code/showcase.py +94 -0
  52. crosshair/fnutil.py +391 -0
  53. crosshair/fnutil_test.py +75 -0
  54. crosshair/fuzz_core_test.py +516 -0
  55. crosshair/libimpl/__init__.py +0 -0
  56. crosshair/libimpl/arraylib.py +161 -0
  57. crosshair/libimpl/binascii_ch_test.py +30 -0
  58. crosshair/libimpl/binascii_test.py +67 -0
  59. crosshair/libimpl/binasciilib.py +150 -0
  60. crosshair/libimpl/bisectlib_test.py +23 -0
  61. crosshair/libimpl/builtinslib.py +5228 -0
  62. crosshair/libimpl/builtinslib_ch_test.py +1191 -0
  63. crosshair/libimpl/builtinslib_test.py +3735 -0
  64. crosshair/libimpl/codecslib.py +86 -0
  65. crosshair/libimpl/codecslib_test.py +86 -0
  66. crosshair/libimpl/collectionslib.py +264 -0
  67. crosshair/libimpl/collectionslib_ch_test.py +252 -0
  68. crosshair/libimpl/collectionslib_test.py +332 -0
  69. crosshair/libimpl/copylib.py +23 -0
  70. crosshair/libimpl/copylib_test.py +18 -0
  71. crosshair/libimpl/datetimelib.py +2559 -0
  72. crosshair/libimpl/datetimelib_ch_test.py +354 -0
  73. crosshair/libimpl/datetimelib_test.py +112 -0
  74. crosshair/libimpl/decimallib.py +5257 -0
  75. crosshair/libimpl/decimallib_ch_test.py +78 -0
  76. crosshair/libimpl/decimallib_test.py +76 -0
  77. crosshair/libimpl/encodings/__init__.py +23 -0
  78. crosshair/libimpl/encodings/_encutil.py +187 -0
  79. crosshair/libimpl/encodings/ascii.py +44 -0
  80. crosshair/libimpl/encodings/latin_1.py +40 -0
  81. crosshair/libimpl/encodings/utf_8.py +93 -0
  82. crosshair/libimpl/encodings_ch_test.py +83 -0
  83. crosshair/libimpl/fractionlib.py +16 -0
  84. crosshair/libimpl/fractionlib_test.py +80 -0
  85. crosshair/libimpl/functoolslib.py +34 -0
  86. crosshair/libimpl/functoolslib_test.py +56 -0
  87. crosshair/libimpl/hashliblib.py +30 -0
  88. crosshair/libimpl/hashliblib_test.py +18 -0
  89. crosshair/libimpl/heapqlib.py +47 -0
  90. crosshair/libimpl/heapqlib_test.py +21 -0
  91. crosshair/libimpl/importliblib.py +18 -0
  92. crosshair/libimpl/importliblib_test.py +38 -0
  93. crosshair/libimpl/iolib.py +216 -0
  94. crosshair/libimpl/iolib_ch_test.py +128 -0
  95. crosshair/libimpl/iolib_test.py +19 -0
  96. crosshair/libimpl/ipaddresslib.py +8 -0
  97. crosshair/libimpl/itertoolslib.py +44 -0
  98. crosshair/libimpl/itertoolslib_test.py +44 -0
  99. crosshair/libimpl/jsonlib.py +984 -0
  100. crosshair/libimpl/jsonlib_ch_test.py +42 -0
  101. crosshair/libimpl/jsonlib_test.py +51 -0
  102. crosshair/libimpl/mathlib.py +179 -0
  103. crosshair/libimpl/mathlib_ch_test.py +44 -0
  104. crosshair/libimpl/mathlib_test.py +67 -0
  105. crosshair/libimpl/oslib.py +7 -0
  106. crosshair/libimpl/pathliblib_test.py +10 -0
  107. crosshair/libimpl/randomlib.py +178 -0
  108. crosshair/libimpl/randomlib_test.py +120 -0
  109. crosshair/libimpl/relib.py +846 -0
  110. crosshair/libimpl/relib_ch_test.py +169 -0
  111. crosshair/libimpl/relib_test.py +493 -0
  112. crosshair/libimpl/timelib.py +72 -0
  113. crosshair/libimpl/timelib_test.py +82 -0
  114. crosshair/libimpl/typeslib.py +15 -0
  115. crosshair/libimpl/typeslib_test.py +36 -0
  116. crosshair/libimpl/unicodedatalib.py +75 -0
  117. crosshair/libimpl/unicodedatalib_test.py +42 -0
  118. crosshair/libimpl/urlliblib.py +23 -0
  119. crosshair/libimpl/urlliblib_test.py +19 -0
  120. crosshair/libimpl/weakreflib.py +13 -0
  121. crosshair/libimpl/weakreflib_test.py +69 -0
  122. crosshair/libimpl/zliblib.py +15 -0
  123. crosshair/libimpl/zliblib_test.py +13 -0
  124. crosshair/lsp_server.py +261 -0
  125. crosshair/lsp_server_test.py +30 -0
  126. crosshair/main.py +973 -0
  127. crosshair/main_test.py +543 -0
  128. crosshair/objectproxy.py +376 -0
  129. crosshair/objectproxy_test.py +41 -0
  130. crosshair/opcode_intercept.py +601 -0
  131. crosshair/opcode_intercept_test.py +304 -0
  132. crosshair/options.py +218 -0
  133. crosshair/options_test.py +10 -0
  134. crosshair/patch_equivalence_test.py +75 -0
  135. crosshair/path_cover.py +209 -0
  136. crosshair/path_cover_test.py +138 -0
  137. crosshair/path_search.py +161 -0
  138. crosshair/path_search_test.py +52 -0
  139. crosshair/pathing_oracle.py +271 -0
  140. crosshair/pathing_oracle_test.py +21 -0
  141. crosshair/pure_importer.py +27 -0
  142. crosshair/pure_importer_test.py +16 -0
  143. crosshair/py.typed +0 -0
  144. crosshair/register_contract.py +273 -0
  145. crosshair/register_contract_test.py +190 -0
  146. crosshair/simplestructs.py +1165 -0
  147. crosshair/simplestructs_test.py +283 -0
  148. crosshair/smtlib.py +24 -0
  149. crosshair/smtlib_test.py +14 -0
  150. crosshair/statespace.py +1199 -0
  151. crosshair/statespace_test.py +108 -0
  152. crosshair/stubs_parser.py +352 -0
  153. crosshair/stubs_parser_test.py +43 -0
  154. crosshair/test_util.py +329 -0
  155. crosshair/test_util_test.py +26 -0
  156. crosshair/tools/__init__.py +0 -0
  157. crosshair/tools/check_help_in_doc.py +264 -0
  158. crosshair/tools/check_init_and_setup_coincide.py +119 -0
  159. crosshair/tools/generate_demo_table.py +127 -0
  160. crosshair/tracers.py +544 -0
  161. crosshair/tracers_test.py +154 -0
  162. crosshair/type_repo.py +151 -0
  163. crosshair/unicode_categories.py +589 -0
  164. crosshair/unicode_categories_test.py +27 -0
  165. crosshair/util.py +741 -0
  166. crosshair/util_test.py +173 -0
  167. crosshair/watcher.py +307 -0
  168. crosshair/watcher_test.py +107 -0
  169. crosshair/z3util.py +76 -0
  170. crosshair/z3util_test.py +11 -0
  171. crosshair_tool-0.0.99.dist-info/METADATA +144 -0
  172. crosshair_tool-0.0.99.dist-info/RECORD +176 -0
  173. crosshair_tool-0.0.99.dist-info/WHEEL +6 -0
  174. crosshair_tool-0.0.99.dist-info/entry_points.txt +3 -0
  175. crosshair_tool-0.0.99.dist-info/licenses/LICENSE +93 -0
  176. crosshair_tool-0.0.99.dist-info/top_level.txt +2 -0
@@ -0,0 +1,108 @@
1
+ import time
2
+
3
+ import pytest
4
+ import z3 # type: ignore
5
+
6
+ from crosshair.core import Patched, proxy_for_type
7
+ from crosshair.statespace import (
8
+ HeapRef,
9
+ RootNode,
10
+ SimpleStateSpace,
11
+ SnapshotRef,
12
+ StateSpace,
13
+ StateSpaceContext,
14
+ model_value_to_python,
15
+ )
16
+ from crosshair.tracers import COMPOSITE_TRACER
17
+ from crosshair.util import UnknownSatisfiability
18
+
19
+ _HEAD_SNAPSHOT = SnapshotRef(-1)
20
+
21
+
22
+ def test_find_key_in_heap():
23
+ space = SimpleStateSpace()
24
+ listref = z3.Const("listref", HeapRef)
25
+ listval1 = space.find_key_in_heap(listref, list, lambda t: [], _HEAD_SNAPSHOT)
26
+ assert isinstance(listval1, list)
27
+ listval2 = space.find_key_in_heap(listref, list, lambda t: [], _HEAD_SNAPSHOT)
28
+ assert listval1 is listval2
29
+ dictref = z3.Const("dictref", HeapRef)
30
+ dictval = space.find_key_in_heap(dictref, dict, lambda t: {}, _HEAD_SNAPSHOT)
31
+ assert dictval is not listval1
32
+ assert isinstance(dictval, dict)
33
+
34
+
35
+ def test_timeout() -> None:
36
+ num_ints = 100
37
+ space = StateSpace(time.monotonic() + 60_000, 0.1, RootNode())
38
+ with pytest.raises(UnknownSatisfiability):
39
+ with Patched(), StateSpaceContext(space), COMPOSITE_TRACER:
40
+ ints = [proxy_for_type(int, f"i{i}") for i in range(num_ints)]
41
+ for i in range(num_ints - 2):
42
+ t0 = time.monotonic()
43
+ if ints[i] * ints[i + 1] == ints[i + 2]:
44
+ pass
45
+ ints[i + 1] += ints[i]
46
+ solve_time = time.monotonic() - t0
47
+ assert 0.05 < solve_time < 0.5
48
+
49
+
50
+ def test_infinite_timeout() -> None:
51
+ space = StateSpace(time.monotonic() + 1000, float("+inf"), RootNode())
52
+ assert space.solver.check(True) == z3.sat
53
+
54
+
55
+ def test_checkpoint() -> None:
56
+ space = SimpleStateSpace()
57
+ ref = z3.Const("ref", HeapRef)
58
+
59
+ def find_key(snapshot):
60
+ return space.find_key_in_heap(ref, list, lambda t: [], snapshot)
61
+
62
+ orig_snapshot = space.current_snapshot()
63
+ listval = find_key(_HEAD_SNAPSHOT)
64
+ space.checkpoint()
65
+
66
+ head_listval = find_key(_HEAD_SNAPSHOT)
67
+ head_listval.append(42)
68
+ assert len(head_listval) == 1
69
+ assert listval is not head_listval
70
+ assert len(listval) == 0
71
+
72
+ listval_again = find_key(orig_snapshot)
73
+ assert listval_again is listval
74
+ head_listval_again = find_key(_HEAD_SNAPSHOT)
75
+ assert head_listval_again is head_listval
76
+
77
+
78
+ def test_model_value_to_python_AlgebraicNumRef():
79
+ # Tests that z3.AlgebraicNumRef is handled properly.
80
+ # See https://github.com/pschanely/CrossHair/issues/242
81
+ rt2 = z3.simplify(z3.Sqrt(2))
82
+ assert type(rt2) == z3.AlgebraicNumRef
83
+ model_value_to_python(rt2)
84
+
85
+
86
+ def test_model_value_to_python_ArithRef():
87
+ # Tests that a plain z3.ArithRef can be exported as Python
88
+ # See https://github.com/pschanely/CrossHair/issues/381
89
+ rt2 = z3.ToInt(2 ** z3.Int("x"))
90
+ print("type(rt2)", type(rt2))
91
+ assert type(rt2) == z3.ArithRef
92
+ model_value_to_python(rt2)
93
+
94
+
95
+ def test_smt_fanout(space: SimpleStateSpace):
96
+ option1 = z3.Bool("option1")
97
+ option2 = z3.Bool("option2")
98
+ space.add(z3.Xor(option1, option2)) # Ensure exactly one option can be set
99
+ exprs_and_results = [(option1, "result1"), (option2, "result2")]
100
+
101
+ result = space.smt_fanout(exprs_and_results, desc="choose_one")
102
+ assert result in ("result1", "result2")
103
+ if result == "result1":
104
+ assert space.is_possible(option1)
105
+ assert not space.is_possible(option2)
106
+ else:
107
+ assert not space.is_possible(option1)
108
+ assert space.is_possible(option2)
@@ -0,0 +1,352 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import re
5
+ import sys
6
+ from collections.abc import __all__ as abc_all
7
+ from importlib import import_module
8
+ from inspect import Parameter, Signature, signature
9
+ from pathlib import Path
10
+ from types import ClassMethodDescriptorType, MethodDescriptorType, WrapperDescriptorType
11
+ from typing import Any, Callable, Dict, List, Optional, Tuple, Type, TypeVar, Union
12
+ from typing import __all__ as typing_all # type: ignore
13
+
14
+ from typeshed_client import get_stub_ast # type: ignore
15
+ from typeshed_client import get_search_context, get_stub_file
16
+
17
+ from crosshair.fnutil import resolve_signature
18
+ from crosshair.util import debug
19
+
20
+
21
+ def signature_from_stubs(fn: Callable) -> Tuple[List[Signature], bool]:
22
+ """
23
+ Try to find signature(s) for the given function in the stubs.
24
+
25
+ For overloaded functions, all signatures found will be returned.
26
+
27
+ :param fn: The function to lookup a signature for.
28
+ :return: A list containing the signature(s) found, if any and a validity boolean.\
29
+ If the boolean is False, signatures returned might be incomplete (some error\
30
+ occured while parsing).
31
+ """
32
+ # ast.get_source_segment requires Python 3.8
33
+ if sys.version_info < (3, 8):
34
+ return [], True
35
+ if getattr(fn, "__module__", None) and getattr(fn, "__qualname__", None):
36
+ module_name = fn.__module__
37
+ else:
38
+ # Some builtins and some C functions are wrapped into Descriptors.
39
+ if isinstance(
40
+ fn, (MethodDescriptorType, WrapperDescriptorType, ClassMethodDescriptorType)
41
+ ) and getattr(fn, "__qualname__", None):
42
+ module_name = fn.__objclass__.__module__
43
+ else:
44
+ # Builtins classmethods have their module available only via __self__.
45
+ fn_self = getattr(fn, "__self__", None)
46
+ if isinstance(fn_self, type):
47
+ module_name = fn_self.__module__
48
+ else:
49
+ return [], True
50
+
51
+ # Use the `qualname` to find the function inside its module.
52
+ path_in_module: List[str] = fn.__qualname__.split(".")
53
+ # Find the stub_file and corresponding AST using `typeshed_client`.
54
+ search_path = [Path(path) for path in sys.path if path]
55
+ search_context = get_search_context(search_path=search_path)
56
+ stub_file = get_stub_file(module_name, search_context=search_context)
57
+ module = get_stub_ast(module_name, search_context=search_context)
58
+ if not stub_file or not module or not isinstance(module, ast.Module):
59
+ debug("No stub found for module", module_name)
60
+ return [], True
61
+ glo = globals().copy()
62
+ return _sig_from_ast(module.body, path_in_module, stub_file.read_text(), glo)
63
+
64
+
65
+ def _get_source_segment(source: str, node: ast.AST) -> Optional[str]:
66
+ """Get source code segment of the *source* that generated *node*."""
67
+ if sys.version_info >= (3, 8):
68
+ return ast.get_source_segment(source, node)
69
+ raise NotImplementedError("ast.get_source_segment not available for python < 3.8.")
70
+
71
+
72
+ def _sig_from_ast(
73
+ stmts: List[ast.stmt],
74
+ next_steps: List[str],
75
+ stub_text: str,
76
+ glo: Dict[str, Any],
77
+ ) -> Tuple[List[Signature], bool]:
78
+ """Lookup in the given ast for a function signature, following `next_steps` path."""
79
+ if len(next_steps) == 0:
80
+ return [], True
81
+
82
+ # First walk through the nodes to execute imports and assignments
83
+ for node in stmts:
84
+ # If we encounter an import statement, add it to the namespace
85
+ if isinstance(node, (ast.Import, ast.ImportFrom)):
86
+ _exec_import(node, glo)
87
+
88
+ # If we encounter the definition of a `TypeVar`, add it to the namespace
89
+ elif isinstance(node, ast.Assign):
90
+ value_text = _get_source_segment(stub_text, node.value)
91
+ if value_text and "TypeVar" in value_text:
92
+ assign_text = _get_source_segment(stub_text, node)
93
+ if assign_text:
94
+ try:
95
+ exec(assign_text, glo)
96
+ except Exception:
97
+ debug("Not able to evaluate TypeVar assignment:", assign_text)
98
+
99
+ # Walk through the nodes to find the next node
100
+ next_node_name = next_steps[0]
101
+ sigs = []
102
+ is_valid = True
103
+ for node in stmts:
104
+ # Only one step remaining => look for the function itself
105
+ if (
106
+ len(next_steps) == 1
107
+ and isinstance(node, ast.FunctionDef)
108
+ and node.name == next_node_name
109
+ ):
110
+ sig, valid = _sig_from_functiondef(node, stub_text, glo)
111
+ if sig:
112
+ sigs.append(sig)
113
+ is_valid = is_valid and valid
114
+
115
+ # More than one step remaining => look for the next step
116
+ elif (
117
+ isinstance(node, (ast.Module, ast.ClassDef, ast.FunctionDef))
118
+ and node.name == next_node_name
119
+ ):
120
+ new_sigs, valid = _sig_from_ast(node.body, next_steps[1:], stub_text, glo)
121
+ sigs.extend(new_sigs)
122
+ is_valid = is_valid and valid
123
+
124
+ # Additionally, we might need to look for the next node into if statements
125
+ for node in stmts:
126
+ if isinstance(node, (ast.If)):
127
+ assign_text = _get_source_segment(stub_text, node.test)
128
+ # Some function depends on the execution environment
129
+ if assign_text and "sys." in assign_text:
130
+ condition = None
131
+ try:
132
+ condition = eval(assign_text, glo)
133
+ except Exception:
134
+ debug("Not able to evaluate condition:", assign_text)
135
+ if condition is not None:
136
+ new_sigs, valid = _sig_from_ast(
137
+ node.body if condition else node.orelse,
138
+ next_steps,
139
+ stub_text,
140
+ glo,
141
+ )
142
+ sigs.extend(new_sigs)
143
+ is_valid = is_valid and valid
144
+
145
+ return sigs, is_valid
146
+
147
+
148
+ def _exec_import(imp: Union[ast.Import, ast.ImportFrom], glo: Dict[str, Any]):
149
+ """Try to execute the import statement and add it to the `glo` namespace."""
150
+ if isinstance(imp, ast.Import):
151
+ for n in imp.names:
152
+ name = n.name
153
+ asname = n.asname or name
154
+ if name != "_typeshed":
155
+ try:
156
+ glo[asname] = import_module(name)
157
+ except Exception:
158
+ debug("Not able to import", name)
159
+
160
+ elif isinstance(imp, ast.ImportFrom):
161
+ # Replace imports from `_typeshed` by their equivalent
162
+ if imp.module == "_typeshed":
163
+ for n in imp.names:
164
+ name = n.name
165
+ asname = n.asname or name
166
+ if name in _REPLACE_TYPESHED:
167
+ new_module, replace = _REPLACE_TYPESHED[name]
168
+ glo[asname] = getattr(import_module(new_module), replace)
169
+ elif name == "Self":
170
+ Self = TypeVar("Self")
171
+ glo["Self"] = Self
172
+ elif imp.module:
173
+ try:
174
+ module = import_module(imp.module)
175
+ except Exception:
176
+ debug("Not able to import", imp.module)
177
+ return
178
+ for n in imp.names:
179
+ name = n.name
180
+ asname = n.asname or name
181
+ try:
182
+ glo[asname] = getattr(module, name)
183
+ except Exception:
184
+ debug("Not able to import", name, "from", imp.module)
185
+
186
+
187
+ # Replace _typeshed imports by their closest equivalent
188
+ _collection_module = "typing" if sys.version_info < (3, 9) else "collections.abc"
189
+ _REPLACE_TYPESHED: Dict[str, Tuple[str, str]] = {
190
+ "SupportsLenAndGetItem": (_collection_module, "Collection"),
191
+ "SupportsNext": (_collection_module, "Iterator"),
192
+ "SupportsAnext": (_collection_module, "AsyncIterator"),
193
+ # One might wish to add more if needed, but exact equivalents do not exist.
194
+ }
195
+
196
+
197
+ def _sig_from_functiondef(
198
+ fn_def: ast.FunctionDef, stub_text: str, glo: Dict[str, Any]
199
+ ) -> Tuple[Optional[Signature], bool]:
200
+ """Given an ast FunctionDef, return the corresponding signature."""
201
+ # Get the source text for the function stub and parse the signature from it.
202
+ function_text = _get_source_segment(stub_text, fn_def)
203
+ if function_text:
204
+ exec(function_text, glo)
205
+ sig_or_error = resolve_signature(glo[fn_def.name])
206
+ if isinstance(sig_or_error, str):
207
+ try:
208
+ sig_or_error = signature(glo[fn_def.name])
209
+ except Exception:
210
+ debug("Not able to perform function evaluation:", function_text)
211
+ return None, False
212
+ parsed_sig, valid = _parse_sig(sig_or_error, glo)
213
+ # If the function is @classmethod, remove cls from the signature.
214
+ for decorator in fn_def.decorator_list:
215
+ if isinstance(decorator, ast.Name) and decorator.id == "classmethod":
216
+ oldparams = list(parsed_sig.parameters.values())
217
+ newparams = oldparams[1:]
218
+ slf = "Self"
219
+ if (
220
+ slf in glo
221
+ and oldparams[0].annotation == Type[glo[slf]]
222
+ and parsed_sig.return_annotation == glo[slf]
223
+ ):
224
+ # We don't support return type "Self" in classmethods.
225
+ return (
226
+ parsed_sig.replace(
227
+ parameters=newparams,
228
+ return_annotation=Parameter.empty,
229
+ ),
230
+ False,
231
+ )
232
+ return parsed_sig.replace(parameters=newparams), valid
233
+ return parsed_sig, valid
234
+ return None, False
235
+
236
+
237
+ def _parse_sig(sig: Signature, glo: Dict[str, Any]) -> Tuple[Signature, bool]:
238
+ """
239
+ Signature annotations are escaped into strings.
240
+
241
+ This is due to `from __future__ import annotations`.
242
+ """
243
+ is_valid = True
244
+ ret_type, valid = _parse_annotation(sig.return_annotation, glo)
245
+ is_valid = is_valid and valid
246
+ params: List[Parameter] = []
247
+ for param in sig.parameters.values():
248
+ annot, valid = _parse_annotation(param.annotation, glo)
249
+ params.append(param.replace(annotation=annot))
250
+ is_valid = is_valid and valid
251
+ return sig.replace(parameters=params, return_annotation=ret_type), is_valid
252
+
253
+
254
+ def _parse_annotation(annotation: Any, glo: Dict[str, Any]) -> Tuple[Any, bool]:
255
+ if isinstance(annotation, str):
256
+ if sys.version_info < (3, 10):
257
+ annotation = _rewrite_with_union(annotation)
258
+ if sys.version_info < (3, 9):
259
+ annotation = _rewrite_with_typing_types(annotation, glo)
260
+ try:
261
+ return eval(annotation, glo), True
262
+ except Exception as e:
263
+ debug("Not able to parse annotation:", annotation, "Error:", e)
264
+ return Parameter.empty, False
265
+ return annotation, True
266
+
267
+
268
+ def _rewrite_with_union(s: str) -> str:
269
+ """
270
+ Undo PEP 604 to be compliant with Python < 3.10.
271
+
272
+ For example `Dict[str | int]` will become `Dict[Union[str, int]]`
273
+
274
+ Main idea of the algorithm:
275
+ - Walk through the string and remember each opening parenthesis or bracket (push the
276
+ current state to the saved states).
277
+ - Uppon closing a parenthesis or bracket, if a `|` was found since the opening
278
+ parenthesis, surround with `Union[]` and replace `|` by `,`. Then pop the state
279
+ from the saved states.
280
+ Note: the given string is assumed to have a valid syntax.
281
+ """
282
+ s_new = s # The new string being built
283
+ saved_states: List[Tuple[int, bool]] = [] # Stack of saved states
284
+ start: int = 0 # Index (in s_new) where Union would begin
285
+ found: bool = False # True if a `|` was found since `start`
286
+ idx: int = 0 # Current index in `s_new`
287
+
288
+ for char in s:
289
+ if char == "|":
290
+ found = True
291
+
292
+ # Closing the current scope. Surround with `Union[]` if a `|` was found.
293
+ if char == ")" or char == "]" or char == ",":
294
+ if found:
295
+ s_new = (
296
+ s_new[: start + 1]
297
+ + "Union["
298
+ + s_new[start + 1 : idx].replace("|", ",")
299
+ + "]"
300
+ + s_new[idx:]
301
+ )
302
+ idx += len("Union[]")
303
+ if char != ",":
304
+ start, found = saved_states.pop() # Restore previous scope.
305
+
306
+ # Opening a new scope.
307
+ if char == "(" or char == "[" or char == ",":
308
+ if char != ",":
309
+ saved_states.append((start, found)) # Save the current scope.
310
+ start = idx
311
+ found = False
312
+ idx += 1
313
+
314
+ if found:
315
+ s_new = "Union[" + s_new.replace("|", ",") + "]"
316
+ return s_new
317
+
318
+
319
+ _REPLACEMENTS_PEP_585: Dict[re.Pattern[str], str] = {}
320
+ """Dictionnary of regexes and replacement strings to revert PEP 585."""
321
+
322
+ if sys.version_info < (3, 9):
323
+ # 1. Replace type subscription by types from typing
324
+ base = r"(?<![\.\w])"
325
+ for t in typing_all:
326
+ replacement = "typing." + t + "["
327
+ _REPLACEMENTS_PEP_585[re.compile(base + t.lower() + r"\[")] = replacement
328
+
329
+ # 2. Replace collections.abc by typing
330
+ # (?<![\.\w]) is to avoid match if the char before is alphanumerical or a dot
331
+ bases = [r"(?<![\.\w])collections\.abc\.", r"(?<![\.\w])"]
332
+ for t in set(typing_all).intersection(abc_all):
333
+ replacement = "typing." + t + "["
334
+ for base in bases:
335
+ _REPLACEMENTS_PEP_585[re.compile(base + t + r"\[")] = replacement
336
+ # Special case for `from collections.abc import Set as AbstractSet`
337
+ _REPLACEMENTS_PEP_585[re.compile(r"(?<![\.\w])AbstractSet\[")] = "typing.Set["
338
+
339
+
340
+ def _rewrite_with_typing_types(s: str, glo: Dict[str, Any]) -> str:
341
+ """
342
+ Undo PEP 585 to be compliant with Python < 3.9.
343
+
344
+ For example `list[int]` will become `typing.List[int]` and types from
345
+ collections.abc will be replaced by those of typing.
346
+ """
347
+ for regx, replace in _REPLACEMENTS_PEP_585.items():
348
+ s_new = regx.sub(replace, s)
349
+ if s != s_new and replace.startswith("typing.") and "typing" not in glo:
350
+ glo["typing"] = import_module("typing")
351
+ s = s_new
352
+ return s
@@ -0,0 +1,43 @@
1
+ import re
2
+ import sys
3
+ from random import Random
4
+
5
+ from crosshair.stubs_parser import (
6
+ _rewrite_with_typing_types,
7
+ _rewrite_with_union,
8
+ signature_from_stubs,
9
+ )
10
+
11
+
12
+ def test_rewrite_with_union():
13
+ test_str = "List[str | int] | Callable[int | str, int]"
14
+ expect = "Union[List[Union[str , int]] , Callable[Union[int , str], int]]"
15
+ assert expect == _rewrite_with_union(test_str)
16
+
17
+
18
+ if sys.version_info < (3, 9):
19
+
20
+ def test_rewrite_with_typing_types():
21
+ test_str = "list[dict[int, list]]"
22
+ expect = "typing.List[typing.Dict[int, list]]"
23
+ glo = dict()
24
+ assert expect == _rewrite_with_typing_types(test_str, glo)
25
+ assert "typing" in glo
26
+
27
+
28
+ def test_signature_from_stubs():
29
+ s, valid = signature_from_stubs(Random.randint)
30
+ if sys.version_info >= (3, 8):
31
+ assert valid and str(s[0]) == "(self, a: int, b: int) -> int"
32
+ s, valid = signature_from_stubs(Random.sample)
33
+ expect_re = re.compile(
34
+ r"""
35
+ \( self .*
36
+ population .* sequence .* _T .*
37
+ \) \s \- \> .* _T
38
+ """,
39
+ re.VERBOSE | re.IGNORECASE,
40
+ )
41
+ assert valid and expect_re.match(str(s[0]))
42
+ else:
43
+ assert not s