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,94 @@
1
+ import statistics
2
+ from typing import Iterable, List, Sequence, Tuple, TypeVar
3
+
4
+ from icontract import ensure, require
5
+
6
+ T = TypeVar("T")
7
+ U = TypeVar("U")
8
+
9
+
10
+ @require(lambda numbers: len(numbers) > 0)
11
+ @ensure(lambda numbers, result: min(numbers) <= result <= max(numbers))
12
+ def average(numbers: List[float]) -> float:
13
+ return sum(numbers) / len(numbers)
14
+
15
+
16
+ @ensure(lambda a, result: len(result) == 2 * len(a))
17
+ @ensure(lambda a, result: result[: len(a)] == a)
18
+ @ensure(lambda a, result: result[-len(a) :] == a)
19
+ def duplicate_list(a: List[T]) -> List[T]:
20
+ return a + a
21
+
22
+
23
+ @require(lambda homework_scores, exam_scores: homework_scores or exam_scores)
24
+ @require(
25
+ lambda homework_scores, exam_scores: all(
26
+ 0 <= s <= 1.0 for s in homework_scores + exam_scores
27
+ )
28
+ )
29
+ @ensure(lambda result: 0 <= result <= 1.0)
30
+ def compute_grade(homework_scores: List[float], exam_scores: List[float]) -> float:
31
+ # Make exams matter more by counting them twice:
32
+ all_scores = homework_scores + exam_scores + exam_scores
33
+ return sum(all_scores) / len(all_scores)
34
+
35
+
36
+ @require(lambda objects: len(objects) > 0)
37
+ @require(lambda objects: all("," not in str(o) for o in objects))
38
+ @ensure(lambda objects, result: result.split(",") == list(map(str, objects)))
39
+ def make_csv_line(objects: Sequence) -> str:
40
+ return ",".join(map(str, objects))
41
+
42
+
43
+ @require(lambda lines: all("," in line for line in lines))
44
+ @ensure(lambda lines, result: result == [line.split(",")[0] for line in lines])
45
+ def csv_first_column(lines: List[str]) -> List[str]:
46
+ return [line[: line.index(",")] for line in lines]
47
+
48
+
49
+ @require(lambda a, b: len(a) == len(b))
50
+ @ensure(lambda a, b, result: len(result) == len(a) == len(b))
51
+ def zip_exact(a: Iterable[T], b: Iterable[U]) -> List[Tuple[T, U]]:
52
+ return list(zip(a, b))
53
+
54
+
55
+ @ensure(lambda x, result: len(result) == max(0, len(x) - 1))
56
+ def zipped_pairs(x: List[T]) -> List[Tuple[T, T]]:
57
+ return zip_exact(x[:-1], x[1:])
58
+
59
+
60
+ @require(lambda n: n >= 0)
61
+ @ensure(lambda n, result: len(result) == n)
62
+ def even_fibb(n: int) -> List[int]:
63
+ """
64
+ Return a list of the first N even fibbonacci numbers.
65
+
66
+ >>> even_fibb(2)
67
+ [2, 8]
68
+ """
69
+ prev = 1
70
+ cur = 1
71
+ result = []
72
+ while n > 0:
73
+ prev, cur = cur, prev + cur
74
+ if cur % 2 == 0:
75
+ result.append(cur)
76
+ n -= 1
77
+ return result
78
+
79
+
80
+ @ensure(lambda numbers, result: len(result) <= len(numbers))
81
+ @ensure(lambda numbers, result: not numbers or max(result) <= max(numbers))
82
+ @ensure(lambda numbers, result: not numbers or min(result) >= min(numbers))
83
+ @ensure(lambda numbers, result: all(x in numbers for x in result))
84
+ def remove_outliers(numbers: List[float], num_deviations: float = 3):
85
+ """
86
+ >>> remove_outliers([0, 1, 2, 3, 4, 5, 50, 6, 7, 8, 9], num_deviations=1)
87
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
88
+ """
89
+ if len(numbers) < 2:
90
+ return numbers
91
+ avg = statistics.mean(numbers)
92
+ allowed_range = statistics.stdev(numbers) * num_deviations
93
+ min_val, max_val = avg - allowed_range, avg + allowed_range
94
+ return [num for num in numbers if min_val <= num <= max_val]
crosshair/fnutil.py ADDED
@@ -0,0 +1,391 @@
1
+ import builtins
2
+ import importlib
3
+ import os
4
+ import re
5
+ import sys
6
+ from dataclasses import dataclass, field
7
+ from inspect import (
8
+ Signature,
9
+ getclosurevars,
10
+ getmembers,
11
+ isclass,
12
+ isfunction,
13
+ ismethod,
14
+ signature,
15
+ )
16
+ from os.path import samefile
17
+ from pathlib import Path
18
+ from types import FunctionType, ModuleType
19
+ from typing import (
20
+ Any,
21
+ Callable,
22
+ Dict,
23
+ Iterable,
24
+ Optional,
25
+ Tuple,
26
+ Type,
27
+ Union,
28
+ cast,
29
+ get_type_hints,
30
+ )
31
+
32
+ from crosshair.util import (
33
+ ErrorDuringImport,
34
+ debug,
35
+ import_module,
36
+ load_file,
37
+ sourcelines,
38
+ )
39
+
40
+ if sys.version_info >= (3, 8):
41
+ from typing import Protocol
42
+
43
+ class Descriptor(Protocol):
44
+ def __get__(self, instance: object, cls: type) -> Any: ...
45
+
46
+ else:
47
+ Descriptor = Any
48
+
49
+
50
+ def fn_globals(fn: Callable) -> Dict[str, object]:
51
+ if hasattr(fn, "__wrapped__"):
52
+ return fn_globals(fn.__wrapped__) # type: ignore
53
+ if isfunction(fn): # excludes built-ins, which don't have closurevars
54
+ closure_vars = getclosurevars(fn)
55
+ if closure_vars.nonlocals:
56
+ return {**closure_vars.nonlocals, **getattr(fn, "__globals__", {})}
57
+ if hasattr(fn, "__globals__"):
58
+ return fn.__globals__ # type:ignore
59
+ return builtins.__dict__
60
+
61
+
62
+ def resolve_signature(fn: Callable) -> Union[Signature, str]:
63
+ """
64
+ Get signature and resolve type annotations with get_type_hints.
65
+
66
+ :param fn: a function whose signature we are interested in
67
+
68
+ :return:
69
+ An annotated signature object, or an error message if the type resultion errors.
70
+ (e.g. the annotation references a type name that isn't dfined)
71
+ """
72
+ # TODO: Test resolution with members at multiple places in the hierarchy.
73
+ # e.g. https://bugs.python.org/issue29966
74
+ if not callable(fn):
75
+ return "Not callable"
76
+ try:
77
+ sig = signature(fn)
78
+ except ValueError:
79
+ # Happens, for example, on builtins
80
+ return "No signature available"
81
+ except Exception as exc:
82
+ # Catchall for other ill-behaved functions. z3 functions, for instance,
83
+ # can raise "z3.z3types.Z3Exception: Z3 AST expected"
84
+ return f"No signature ({type(exc)})"
85
+ try:
86
+ type_hints = get_type_hints(fn, fn_globals(fn))
87
+ except (
88
+ # SymbolicObject has __annotations__ as a property, which the inspect modules
89
+ # rejects with AttributeError:
90
+ AttributeError,
91
+ # type name not resolvable:
92
+ NameError,
93
+ # TODO: why does this one happen, again?:
94
+ TypeError,
95
+ ) as hints_error:
96
+ return str(hints_error)
97
+ newparams = []
98
+ for name, param in sig.parameters.items():
99
+ if name in type_hints:
100
+ param = param.replace(annotation=type_hints[name])
101
+ newparams.append(param)
102
+ newreturn = type_hints.get("return", sig.return_annotation)
103
+ return Signature(newparams, return_annotation=newreturn)
104
+
105
+
106
+ def set_first_arg_type(sig: Signature, first_arg_type: object) -> Signature:
107
+ newparams = list(sig.parameters.values())
108
+ newparams[0] = newparams[0].replace(annotation=first_arg_type)
109
+ return Signature(newparams, return_annotation=sig.return_annotation)
110
+
111
+
112
+ FUNCTIONINFO_DESCRIPTOR_TYPES = (FunctionType, staticmethod, classmethod, property)
113
+
114
+
115
+ @dataclass
116
+ class FunctionInfo:
117
+ """
118
+ Abstractions around code.
119
+
120
+ Allows you to access, inspect the signatures of, and patch
121
+ code in a module or class, even when that code is wrapped in
122
+ decorators like @staticmethod, @classmethod, and @property.
123
+ """
124
+
125
+ context: Union[type, ModuleType, None]
126
+ name: str
127
+ descriptor: Descriptor = field(compare=False)
128
+ _sig: Union[None, Signature, str] = field(init=False, compare=False, default=None)
129
+
130
+ @staticmethod
131
+ def from_module(context: ModuleType, name: str) -> "FunctionInfo":
132
+ return FunctionInfo(context, name, context.__dict__[name])
133
+
134
+ @staticmethod
135
+ def from_class(context: type, name: str) -> "FunctionInfo":
136
+ desc = context.__dict__[name]
137
+ return FunctionInfo(context, name, desc)
138
+
139
+ @staticmethod
140
+ def from_fn(fn: Callable) -> "FunctionInfo":
141
+ return FunctionInfo(None, fn.__name__, fn) # type: ignore
142
+
143
+ def callable(self) -> Tuple[Callable, Signature]:
144
+ maybe = self.get_callable()
145
+ assert maybe is not None
146
+ return maybe
147
+
148
+ def get_sig(self, fn: Callable) -> Optional[Signature]:
149
+ sig = self._sig
150
+ if sig is None:
151
+ sig = resolve_signature(fn)
152
+ self._sig = sig
153
+ return sig if isinstance(sig, Signature) else None
154
+
155
+ def get_callable(self) -> Optional[Tuple[Callable, Signature]]:
156
+ ctx, desc = self.context, self.descriptor
157
+ if isinstance(ctx, ModuleType) or ctx is None:
158
+ fn = cast(Callable, desc)
159
+ sig = self.get_sig(fn)
160
+ if sig:
161
+ return (fn, sig)
162
+ elif isinstance(desc, FUNCTIONINFO_DESCRIPTOR_TYPES):
163
+ if isinstance(desc, FunctionType):
164
+ sig = self.get_sig(desc)
165
+ if sig:
166
+ return (desc, set_first_arg_type(sig, ctx))
167
+ elif isinstance(desc, staticmethod):
168
+ sig = self.get_sig(desc.__func__)
169
+ if sig:
170
+ return (desc.__func__, sig)
171
+ elif isinstance(desc, classmethod):
172
+ sig = self.get_sig(desc.__func__)
173
+ if sig:
174
+ try:
175
+ ctx_type = Type.__getitem__(ctx)
176
+ except TypeError: # Raised by "Type[Generic]" etc
177
+ return None
178
+ return (desc.__func__, set_first_arg_type(sig, ctx_type))
179
+ elif isinstance(desc, property):
180
+ if desc.fget and not desc.fset and not desc.fdel:
181
+ sig = self.get_sig(desc.fget)
182
+ if sig:
183
+ return (desc.fget, set_first_arg_type(sig, ctx))
184
+ # Cannot get a signature:
185
+ return None
186
+
187
+ def patch_logic(self, patched: Callable) -> Union[None, Callable, Descriptor]:
188
+ desc = self.descriptor
189
+ if isinstance(desc, FunctionType):
190
+ return patched
191
+ elif isinstance(desc, staticmethod):
192
+ return staticmethod(patched)
193
+ elif isinstance(desc, classmethod):
194
+ return classmethod(patched)
195
+ elif isinstance(desc, property):
196
+ return property(fget=patched, fset=desc.fset, fdel=desc.fdel) # type: ignore
197
+ return None
198
+
199
+
200
+ class NotFound(ValueError):
201
+ pass
202
+
203
+
204
+ def walk_qualname(obj: Union[type, ModuleType], name: str) -> Union[type, FunctionInfo]:
205
+ """
206
+ Resolve the function info by walking through the ``obj``.
207
+
208
+ >>> walk_qualname(builtins, 'sum') == FunctionInfo.from_module(builtins, 'sum')
209
+ True
210
+ >>> walk_qualname(list, 'append') == FunctionInfo.from_class(list, 'append')
211
+ True
212
+ >>> class Foo:
213
+ ... class Bar:
214
+ ... def doit():
215
+ ... pass
216
+ >>> walk_qualname(Foo, 'Bar.doit') == FunctionInfo.from_class(Foo.Bar, 'doit')
217
+ True
218
+ >>> walk_qualname(Foo, 'Bar') == Foo.Bar
219
+ True
220
+ """
221
+ parts = name.split(".")
222
+ for part in parts[:-1]:
223
+ if part == "<locals>":
224
+ raise ValueError("object defined inline are non-addressable(" + name + ")")
225
+ if not hasattr(obj, part):
226
+ raise NotFound(f'Name "{part}" not found on object "{obj}"')
227
+ obj = getattr(obj, part)
228
+ lastpart = parts[-1]
229
+ if lastpart not in obj.__dict__:
230
+ raise NotFound(f'Name "{lastpart}" not found on object "{obj}"')
231
+ assert isinstance(obj, (type, ModuleType))
232
+ target = obj.__dict__[lastpart]
233
+ if isclass(target):
234
+ return target
235
+ return FunctionInfo(obj, lastpart, target)
236
+
237
+
238
+ def load_by_qualname(name: str) -> Union[type, FunctionInfo]:
239
+ """
240
+ Load the function info by the fully qualified name.
241
+
242
+ raises: NotFound
243
+
244
+ >>> type(load_by_qualname('os'))
245
+ <class 'module'>
246
+ >>> type(load_by_qualname('os.path'))
247
+ <class 'module'>
248
+ >>> type(load_by_qualname('pathlib.Path'))
249
+ <class 'type'>
250
+ >>> type(load_by_qualname('os.path.join')).__name__
251
+ 'FunctionInfo'
252
+ >>> type(load_by_qualname('pathlib.Path.is_dir')).__name__
253
+ 'FunctionInfo'
254
+ """
255
+ parts = name.split(".")
256
+ original_modules = set(sys.modules.keys())
257
+ # try progressively shorter prefixes until we can load a module:
258
+ for i in reversed(range(1, len(parts) + 1)):
259
+ cur_module_name = ".".join(parts[:i])
260
+ try:
261
+ try:
262
+ spec_exists = importlib.util.find_spec(cur_module_name) is not None
263
+ if not spec_exists:
264
+ raise ModuleNotFoundError(f"No module named '{cur_module_name}'")
265
+ except ModuleNotFoundError as exc:
266
+ if i == 1:
267
+ raise NotFound(f"Module '{cur_module_name}' was not found") from exc
268
+ else:
269
+ continue
270
+ module = import_module(cur_module_name)
271
+ except Exception as e:
272
+ raise ErrorDuringImport from e
273
+ remaining = ".".join(parts[i:])
274
+ if remaining:
275
+ return walk_qualname(module, remaining)
276
+ else:
277
+ return module
278
+ assert False
279
+
280
+
281
+ def _contains_line(entity: object, filename: str, linenum: int):
282
+ (cur_filename, start, lines) = sourcelines(entity)
283
+ end = start + len(lines)
284
+ try:
285
+ return samefile(filename, cur_filename) and start <= linenum <= end
286
+ except IOError:
287
+ return False
288
+
289
+
290
+ def load_function_at_line(
291
+ entity: Union[ModuleType, type], filename: str, linenum: int
292
+ ) -> Optional[FunctionInfo]:
293
+ """Load a function or method at a line number."""
294
+ modulename = (
295
+ entity.__name__ if isinstance(entity, ModuleType) else entity.__module__
296
+ )
297
+ for name, member in getmembers(entity):
298
+ if getattr(member, "__module__", None) != modulename:
299
+ # member was likely imported, but not defined here.
300
+ continue
301
+ if isfunction(member) and _contains_line(member, filename, linenum):
302
+ return FunctionInfo(entity, name, entity.__dict__[name])
303
+ if isclass(member):
304
+ ctxfn = load_function_at_line(member, filename, linenum)
305
+ if ctxfn:
306
+ return ctxfn
307
+ return None
308
+
309
+
310
+ def analyzable_filename(filename: str) -> bool:
311
+ """
312
+ Check whether the file can be analyzed purely based on the ``filename``.
313
+
314
+ >>> analyzable_filename('foo23.py')
315
+ True
316
+ >>> analyzable_filename('#foo.py')
317
+ False
318
+ >>> analyzable_filename('23foo.py')
319
+ False
320
+ >>> analyzable_filename('setup.py')
321
+ False
322
+ """
323
+ if not filename.endswith(".py"):
324
+ return False
325
+ lead_char = filename[0]
326
+ if (not lead_char.isalpha()) and (not lead_char.isidentifier()):
327
+ # (skip temporary editor files, backups, etc)
328
+ debug(f"Skipping {filename} because it begins with a special character.")
329
+ return False
330
+ if filename in ("setup.py",):
331
+ debug(
332
+ f"Skipping {filename} because files with this name are not usually import-able."
333
+ )
334
+ return False
335
+ return True
336
+
337
+
338
+ def walk_paths(paths: Iterable[Path], ignore_missing=False) -> Iterable[Path]:
339
+ for path in paths:
340
+ if not path.exists():
341
+ if ignore_missing:
342
+ continue
343
+ else:
344
+ raise FileNotFoundError(str(path))
345
+ if path.is_dir():
346
+ for dirpath, _dirs, files in os.walk(str(path)):
347
+ for curfile in files:
348
+ if analyzable_filename(curfile):
349
+ yield Path(dirpath) / curfile
350
+ else:
351
+ yield path
352
+
353
+
354
+ _FILE_WITH_LINE_RE = re.compile(r"^(.*\.py)\:(\d+)$")
355
+
356
+
357
+ def load_files_or_qualnames(
358
+ specifiers: Iterable[str],
359
+ ) -> Iterable[Union[ModuleType, type, FunctionInfo]]:
360
+ fspaths = []
361
+ for specifier in specifiers:
362
+ file_line_match = _FILE_WITH_LINE_RE.match(specifier)
363
+ if file_line_match:
364
+ filename, linestr = file_line_match.groups()
365
+ linenum = int(linestr)
366
+ fn = load_function_at_line(load_file(filename), filename, linenum)
367
+ if fn is None:
368
+ raise ErrorDuringImport(
369
+ f"Cannot find a function or method on line {linenum}."
370
+ )
371
+ yield fn
372
+ elif specifier.endswith(".py") or os.path.isdir(specifier):
373
+ fspaths.append(Path(specifier))
374
+ else:
375
+ yield load_by_qualname(specifier)
376
+ for path in walk_paths(fspaths):
377
+ yield load_file(str(path))
378
+
379
+
380
+ def get_top_level_classes_and_functions(
381
+ module: ModuleType,
382
+ ) -> Iterable[Tuple[str, Union[FunctionInfo, type]]]:
383
+ module_name = module.__name__
384
+ for name, member in getmembers(module):
385
+ if getattr(member, "__module__", None) != module_name:
386
+ # member was likely imported, but not defined here.
387
+ continue
388
+ if isfunction(member) or ismethod(member):
389
+ yield (name, FunctionInfo.from_module(module, name))
390
+ elif isclass(member):
391
+ yield (name, member)
@@ -0,0 +1,75 @@
1
+ import builtins
2
+ import inspect
3
+ import sys
4
+ from dataclasses import dataclass
5
+ from typing import Generic
6
+
7
+ import pytest
8
+
9
+ from crosshair.fnutil import (
10
+ FunctionInfo,
11
+ fn_globals,
12
+ load_function_at_line,
13
+ resolve_signature,
14
+ set_first_arg_type,
15
+ )
16
+ from crosshair.util import set_debug
17
+
18
+
19
+ def with_invalid_type_annotation(x: "TypeThatIsNotDefined"): # type: ignore # noqa: F821
20
+ pass
21
+
22
+
23
+ def test_fn_globals_on_builtin() -> None:
24
+ assert fn_globals(zip) is builtins.__dict__
25
+
26
+
27
+ def test_resolve_signature_invalid_annotations() -> None:
28
+ sig = resolve_signature(with_invalid_type_annotation)
29
+ assert sig == "name 'TypeThatIsNotDefined' is not defined"
30
+
31
+
32
+ @pytest.mark.skipif(
33
+ sys.version_info >= (3, 13), reason="builtins have signatures as of 3.13"
34
+ )
35
+ def test_resolve_signature_c_function() -> None:
36
+ sig = resolve_signature(map)
37
+ assert sig == "No signature available"
38
+
39
+
40
+ def test_set_first_arg_type() -> None:
41
+ sig = inspect.signature(with_invalid_type_annotation)
42
+ typed_sig = set_first_arg_type(sig, int)
43
+ assert typed_sig.parameters["x"].annotation == int
44
+
45
+
46
+ def toplevelfn():
47
+ pass
48
+
49
+
50
+ # NOTE: We test a dataclass because those will have eval()'d members that appear to
51
+ # come from the file "<string>".
52
+ @dataclass
53
+ class Outer:
54
+ def outerfn(self):
55
+ pass
56
+
57
+ class Inner:
58
+ def innerfn(self):
59
+ pass
60
+
61
+
62
+ def test_load_function_at_line():
63
+ mymodule = sys.modules[__name__]
64
+ myfile = __file__
65
+ outerfnline = Outer.outerfn.__code__.co_firstlineno
66
+ innerfnline = Outer.Inner.innerfn.__code__.co_firstlineno
67
+ toplevelfnline = toplevelfn.__code__.co_firstlineno
68
+ assert load_function_at_line(mymodule, myfile, 1) is None
69
+ assert load_function_at_line(mymodule, myfile, outerfnline).name == "outerfn"
70
+ assert load_function_at_line(mymodule, myfile, innerfnline).name == "innerfn"
71
+ assert load_function_at_line(mymodule, myfile, toplevelfnline).name == "toplevelfn"
72
+
73
+
74
+ def test_FunctionInfo_get_callable_on_generic():
75
+ assert FunctionInfo.from_class(Generic, "__class_getitem__").get_callable() is None