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.
- _crosshair_tracers.cpython-312-darwin.so +0 -0
- crosshair/__init__.py +42 -0
- crosshair/__main__.py +8 -0
- crosshair/_mark_stacks.h +790 -0
- crosshair/_preliminaries_test.py +18 -0
- crosshair/_tracers.h +94 -0
- crosshair/_tracers_pycompat.h +522 -0
- crosshair/_tracers_test.py +138 -0
- crosshair/abcstring.py +245 -0
- crosshair/auditwall.py +190 -0
- crosshair/auditwall_test.py +77 -0
- crosshair/codeconfig.py +113 -0
- crosshair/codeconfig_test.py +117 -0
- crosshair/condition_parser.py +1237 -0
- crosshair/condition_parser_test.py +497 -0
- crosshair/conftest.py +30 -0
- crosshair/copyext.py +155 -0
- crosshair/copyext_test.py +84 -0
- crosshair/core.py +1763 -0
- crosshair/core_and_libs.py +149 -0
- crosshair/core_regestered_types_test.py +82 -0
- crosshair/core_test.py +1316 -0
- crosshair/diff_behavior.py +314 -0
- crosshair/diff_behavior_test.py +261 -0
- crosshair/dynamic_typing.py +346 -0
- crosshair/dynamic_typing_test.py +210 -0
- crosshair/enforce.py +282 -0
- crosshair/enforce_test.py +182 -0
- crosshair/examples/PEP316/__init__.py +1 -0
- crosshair/examples/PEP316/bugs_detected/__init__.py +0 -0
- crosshair/examples/PEP316/bugs_detected/getattr_magic.py +16 -0
- crosshair/examples/PEP316/bugs_detected/hash_consistent_with_equals.py +31 -0
- crosshair/examples/PEP316/bugs_detected/shopping_cart.py +24 -0
- crosshair/examples/PEP316/bugs_detected/showcase.py +39 -0
- crosshair/examples/PEP316/correct_code/__init__.py +0 -0
- crosshair/examples/PEP316/correct_code/arith.py +60 -0
- crosshair/examples/PEP316/correct_code/chess.py +77 -0
- crosshair/examples/PEP316/correct_code/nesting_inference.py +17 -0
- crosshair/examples/PEP316/correct_code/numpy_examples.py +132 -0
- crosshair/examples/PEP316/correct_code/rolling_average.py +35 -0
- crosshair/examples/PEP316/correct_code/showcase.py +104 -0
- crosshair/examples/__init__.py +0 -0
- crosshair/examples/check_examples_test.py +146 -0
- crosshair/examples/deal/__init__.py +1 -0
- crosshair/examples/icontract/__init__.py +1 -0
- crosshair/examples/icontract/bugs_detected/__init__.py +0 -0
- crosshair/examples/icontract/bugs_detected/showcase.py +41 -0
- crosshair/examples/icontract/bugs_detected/wrong_sign.py +8 -0
- crosshair/examples/icontract/correct_code/__init__.py +0 -0
- crosshair/examples/icontract/correct_code/arith.py +51 -0
- crosshair/examples/icontract/correct_code/showcase.py +94 -0
- crosshair/fnutil.py +391 -0
- crosshair/fnutil_test.py +75 -0
- crosshair/fuzz_core_test.py +516 -0
- crosshair/libimpl/__init__.py +0 -0
- crosshair/libimpl/arraylib.py +161 -0
- crosshair/libimpl/binascii_ch_test.py +30 -0
- crosshair/libimpl/binascii_test.py +67 -0
- crosshair/libimpl/binasciilib.py +150 -0
- crosshair/libimpl/bisectlib_test.py +23 -0
- crosshair/libimpl/builtinslib.py +5228 -0
- crosshair/libimpl/builtinslib_ch_test.py +1191 -0
- crosshair/libimpl/builtinslib_test.py +3735 -0
- crosshair/libimpl/codecslib.py +86 -0
- crosshair/libimpl/codecslib_test.py +86 -0
- crosshair/libimpl/collectionslib.py +264 -0
- crosshair/libimpl/collectionslib_ch_test.py +252 -0
- crosshair/libimpl/collectionslib_test.py +332 -0
- crosshair/libimpl/copylib.py +23 -0
- crosshair/libimpl/copylib_test.py +18 -0
- crosshair/libimpl/datetimelib.py +2559 -0
- crosshair/libimpl/datetimelib_ch_test.py +354 -0
- crosshair/libimpl/datetimelib_test.py +112 -0
- crosshair/libimpl/decimallib.py +5257 -0
- crosshair/libimpl/decimallib_ch_test.py +78 -0
- crosshair/libimpl/decimallib_test.py +76 -0
- crosshair/libimpl/encodings/__init__.py +23 -0
- crosshair/libimpl/encodings/_encutil.py +187 -0
- crosshair/libimpl/encodings/ascii.py +44 -0
- crosshair/libimpl/encodings/latin_1.py +40 -0
- crosshair/libimpl/encodings/utf_8.py +93 -0
- crosshair/libimpl/encodings_ch_test.py +83 -0
- crosshair/libimpl/fractionlib.py +16 -0
- crosshair/libimpl/fractionlib_test.py +80 -0
- crosshair/libimpl/functoolslib.py +34 -0
- crosshair/libimpl/functoolslib_test.py +56 -0
- crosshair/libimpl/hashliblib.py +30 -0
- crosshair/libimpl/hashliblib_test.py +18 -0
- crosshair/libimpl/heapqlib.py +47 -0
- crosshair/libimpl/heapqlib_test.py +21 -0
- crosshair/libimpl/importliblib.py +18 -0
- crosshair/libimpl/importliblib_test.py +38 -0
- crosshair/libimpl/iolib.py +216 -0
- crosshair/libimpl/iolib_ch_test.py +128 -0
- crosshair/libimpl/iolib_test.py +19 -0
- crosshair/libimpl/ipaddresslib.py +8 -0
- crosshair/libimpl/itertoolslib.py +44 -0
- crosshair/libimpl/itertoolslib_test.py +44 -0
- crosshair/libimpl/jsonlib.py +984 -0
- crosshair/libimpl/jsonlib_ch_test.py +42 -0
- crosshair/libimpl/jsonlib_test.py +51 -0
- crosshair/libimpl/mathlib.py +179 -0
- crosshair/libimpl/mathlib_ch_test.py +44 -0
- crosshair/libimpl/mathlib_test.py +67 -0
- crosshair/libimpl/oslib.py +7 -0
- crosshair/libimpl/pathliblib_test.py +10 -0
- crosshair/libimpl/randomlib.py +178 -0
- crosshair/libimpl/randomlib_test.py +120 -0
- crosshair/libimpl/relib.py +846 -0
- crosshair/libimpl/relib_ch_test.py +169 -0
- crosshair/libimpl/relib_test.py +493 -0
- crosshair/libimpl/timelib.py +72 -0
- crosshair/libimpl/timelib_test.py +82 -0
- crosshair/libimpl/typeslib.py +15 -0
- crosshair/libimpl/typeslib_test.py +36 -0
- crosshair/libimpl/unicodedatalib.py +75 -0
- crosshair/libimpl/unicodedatalib_test.py +42 -0
- crosshair/libimpl/urlliblib.py +23 -0
- crosshair/libimpl/urlliblib_test.py +19 -0
- crosshair/libimpl/weakreflib.py +13 -0
- crosshair/libimpl/weakreflib_test.py +69 -0
- crosshair/libimpl/zliblib.py +15 -0
- crosshair/libimpl/zliblib_test.py +13 -0
- crosshair/lsp_server.py +261 -0
- crosshair/lsp_server_test.py +30 -0
- crosshair/main.py +973 -0
- crosshair/main_test.py +543 -0
- crosshair/objectproxy.py +376 -0
- crosshair/objectproxy_test.py +41 -0
- crosshair/opcode_intercept.py +601 -0
- crosshair/opcode_intercept_test.py +304 -0
- crosshair/options.py +218 -0
- crosshair/options_test.py +10 -0
- crosshair/patch_equivalence_test.py +75 -0
- crosshair/path_cover.py +209 -0
- crosshair/path_cover_test.py +138 -0
- crosshair/path_search.py +161 -0
- crosshair/path_search_test.py +52 -0
- crosshair/pathing_oracle.py +271 -0
- crosshair/pathing_oracle_test.py +21 -0
- crosshair/pure_importer.py +27 -0
- crosshair/pure_importer_test.py +16 -0
- crosshair/py.typed +0 -0
- crosshair/register_contract.py +273 -0
- crosshair/register_contract_test.py +190 -0
- crosshair/simplestructs.py +1165 -0
- crosshair/simplestructs_test.py +283 -0
- crosshair/smtlib.py +24 -0
- crosshair/smtlib_test.py +14 -0
- crosshair/statespace.py +1199 -0
- crosshair/statespace_test.py +108 -0
- crosshair/stubs_parser.py +352 -0
- crosshair/stubs_parser_test.py +43 -0
- crosshair/test_util.py +329 -0
- crosshair/test_util_test.py +26 -0
- crosshair/tools/__init__.py +0 -0
- crosshair/tools/check_help_in_doc.py +264 -0
- crosshair/tools/check_init_and_setup_coincide.py +119 -0
- crosshair/tools/generate_demo_table.py +127 -0
- crosshair/tracers.py +544 -0
- crosshair/tracers_test.py +154 -0
- crosshair/type_repo.py +151 -0
- crosshair/unicode_categories.py +589 -0
- crosshair/unicode_categories_test.py +27 -0
- crosshair/util.py +741 -0
- crosshair/util_test.py +173 -0
- crosshair/watcher.py +307 -0
- crosshair/watcher_test.py +107 -0
- crosshair/z3util.py +76 -0
- crosshair/z3util_test.py +11 -0
- crosshair_tool-0.0.99.dist-info/METADATA +144 -0
- crosshair_tool-0.0.99.dist-info/RECORD +176 -0
- crosshair_tool-0.0.99.dist-info/WHEEL +6 -0
- crosshair_tool-0.0.99.dist-info/entry_points.txt +3 -0
- crosshair_tool-0.0.99.dist-info/licenses/LICENSE +93 -0
- 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)
|
crosshair/fnutil_test.py
ADDED
|
@@ -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
|