typespy 0.1.0__tar.gz
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.
- typespy-0.1.0/LICENSE +21 -0
- typespy-0.1.0/PKG-INFO +40 -0
- typespy-0.1.0/README.md +23 -0
- typespy-0.1.0/pyproject.toml +31 -0
- typespy-0.1.0/setup.cfg +4 -0
- typespy-0.1.0/tests/test_analyze.py +280 -0
- typespy-0.1.0/typespy.egg-info/PKG-INFO +40 -0
- typespy-0.1.0/typespy.egg-info/SOURCES.txt +10 -0
- typespy-0.1.0/typespy.egg-info/dependency_links.txt +1 -0
- typespy-0.1.0/typespy.egg-info/entry_points.txt +2 -0
- typespy-0.1.0/typespy.egg-info/top_level.txt +1 -0
- typespy-0.1.0/typespy.py +311 -0
typespy-0.1.0/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 RasmusNygren
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
typespy-0.1.0/PKG-INFO
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: typespy
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: Detect python calling types
|
5
|
+
Author: Rasmus Nygren
|
6
|
+
Maintainer: Rasmus Nygren
|
7
|
+
Project-URL: Homepage, https://github.com/rasmusnygren/typespy
|
8
|
+
Project-URL: Repository, https://github.com/rasmusnygren/typespy
|
9
|
+
Keywords: type detection,type tracing,typing
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
13
|
+
Requires-Python: >=3.10
|
14
|
+
Description-Content-Type: text/markdown
|
15
|
+
License-File: LICENSE
|
16
|
+
Dynamic: license-file
|
17
|
+
|
18
|
+
Typespy
|
19
|
+
=======
|
20
|
+
Figure out which types your modules are being called with.
|
21
|
+
|
22
|
+
## Installation
|
23
|
+
```bash
|
24
|
+
pip install typespy
|
25
|
+
```
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
```bash
|
29
|
+
usage: typespy [-h] [--verbose] module_names [module_names ...] dir
|
30
|
+
|
31
|
+
Analyze function call type information
|
32
|
+
|
33
|
+
positional arguments:
|
34
|
+
module_names Name of the modules to analyze
|
35
|
+
dir Path to the root directory of repositories
|
36
|
+
|
37
|
+
options:
|
38
|
+
-h, --help show this help message and exit
|
39
|
+
--verbose Print function calls with file locations
|
40
|
+
```
|
typespy-0.1.0/README.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
Typespy
|
2
|
+
=======
|
3
|
+
Figure out which types your modules are being called with.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
```bash
|
7
|
+
pip install typespy
|
8
|
+
```
|
9
|
+
|
10
|
+
## Usage
|
11
|
+
```bash
|
12
|
+
usage: typespy [-h] [--verbose] module_names [module_names ...] dir
|
13
|
+
|
14
|
+
Analyze function call type information
|
15
|
+
|
16
|
+
positional arguments:
|
17
|
+
module_names Name of the modules to analyze
|
18
|
+
dir Path to the root directory of repositories
|
19
|
+
|
20
|
+
options:
|
21
|
+
-h, --help show this help message and exit
|
22
|
+
--verbose Print function calls with file locations
|
23
|
+
```
|
@@ -0,0 +1,31 @@
|
|
1
|
+
[build-system]
|
2
|
+
requires = ["setuptools >= 77.0.3", "wheel"]
|
3
|
+
build-backend = "setuptools.build_meta"
|
4
|
+
|
5
|
+
[project]
|
6
|
+
name = "typespy"
|
7
|
+
version = "0.1.0"
|
8
|
+
description = "Detect python calling types"
|
9
|
+
readme = "README.md"
|
10
|
+
license-files = ["LICENSE"]
|
11
|
+
keywords = ["type detection", "type tracing", "typing"]
|
12
|
+
authors = [
|
13
|
+
{name = "Rasmus Nygren"}
|
14
|
+
]
|
15
|
+
maintainers = [
|
16
|
+
{name = "Rasmus Nygren"}
|
17
|
+
]
|
18
|
+
requires-python = ">=3.10"
|
19
|
+
dependencies = []
|
20
|
+
classifiers = [
|
21
|
+
"Programming Language :: Python :: 3.10",
|
22
|
+
"Programming Language :: Python :: 3.11",
|
23
|
+
"Programming Language :: Python :: 3.12",
|
24
|
+
]
|
25
|
+
|
26
|
+
[project.urls]
|
27
|
+
Homepage = "https://github.com/rasmusnygren/typespy"
|
28
|
+
Repository = "https://github.com/rasmusnygren/typespy"
|
29
|
+
|
30
|
+
[project.scripts]
|
31
|
+
typespy = "typespy:main"
|
typespy-0.1.0/setup.cfg
ADDED
@@ -0,0 +1,280 @@
|
|
1
|
+
import ast
|
2
|
+
|
3
|
+
import pytest
|
4
|
+
|
5
|
+
from typespy import ModuleCallVisitor
|
6
|
+
from typespy import analyze_file
|
7
|
+
from typespy import find_import_aliases
|
8
|
+
from typespy import infer_type
|
9
|
+
|
10
|
+
|
11
|
+
@pytest.fixture
|
12
|
+
def tmp_file(tmp_path):
|
13
|
+
yield tmp_path / "a.py"
|
14
|
+
|
15
|
+
|
16
|
+
@pytest.mark.parametrize(
|
17
|
+
"content, expected_type", [
|
18
|
+
("'hello'", "str"),
|
19
|
+
("42", "int"),
|
20
|
+
("[1, 2, 3]", "list[int]"),
|
21
|
+
("['a', 'b']", "list[str]"),
|
22
|
+
("[1, 'a']", "list[int | str]"),
|
23
|
+
("[]", "list[]"),
|
24
|
+
("{'a': 1}", "dict[str, int]"),
|
25
|
+
("{'a': 1, 'b': 3.0}", "dict[str, float | int]"),
|
26
|
+
("{}", "dict[]"),
|
27
|
+
("(1, 'a')", "tuple[int, str]"),
|
28
|
+
("()", "tuple[]"),
|
29
|
+
],
|
30
|
+
)
|
31
|
+
def test_infer_type_constants(content, expected_type):
|
32
|
+
node = ast.parse(content).body[0].value
|
33
|
+
assert str(infer_type(node)) == expected_type
|
34
|
+
|
35
|
+
|
36
|
+
@pytest.mark.parametrize(
|
37
|
+
"content, var_types, expected_type", [
|
38
|
+
("foo", {}, "var:foo"),
|
39
|
+
("foo", {"foo": "int"}, "int"),
|
40
|
+
("func()", {}, "unknown"),
|
41
|
+
],
|
42
|
+
)
|
43
|
+
def test_infer_type_variables(content, var_types, expected_type):
|
44
|
+
node = ast.parse(content).body[0].value
|
45
|
+
assert str(infer_type(node, var_types)) == expected_type
|
46
|
+
|
47
|
+
|
48
|
+
@pytest.mark.parametrize(
|
49
|
+
"content, module_name, expected_aliases, expected_imports", [
|
50
|
+
("import foo", "foo", {"foo"}, {}),
|
51
|
+
("import foo as f", "foo", {"f"}, {}),
|
52
|
+
(
|
53
|
+
"from foo import bar, baz", "foo",
|
54
|
+
set(), {"bar": "bar", "baz": "baz"},
|
55
|
+
),
|
56
|
+
(
|
57
|
+
"from foo import bar as b, baz", "foo",
|
58
|
+
set(), {"b": "bar", "baz": "baz"},
|
59
|
+
),
|
60
|
+
("import unrelated", "foo", set(), {}),
|
61
|
+
("from unrelated import something", "foo", set(), {}),
|
62
|
+
],
|
63
|
+
)
|
64
|
+
def test_find_import_aliases(content, module_name, expected_aliases, expected_imports):
|
65
|
+
tree = ast.parse(content)
|
66
|
+
module_aliases, direct_imports = find_import_aliases(tree, module_name)
|
67
|
+
|
68
|
+
assert module_aliases == expected_aliases
|
69
|
+
assert direct_imports == expected_imports
|
70
|
+
|
71
|
+
|
72
|
+
def test_library_call_visitor_module_alias():
|
73
|
+
content = """
|
74
|
+
import foo as f
|
75
|
+
f.bar([1, 2, 3])
|
76
|
+
f.baz("hello")
|
77
|
+
"""
|
78
|
+
tree = ast.parse(content)
|
79
|
+
|
80
|
+
lib_aliases = {"f"}
|
81
|
+
direct_imports = {}
|
82
|
+
visitor = ModuleCallVisitor(lib_aliases, direct_imports)
|
83
|
+
visitor.visit(tree)
|
84
|
+
|
85
|
+
# Convert to string representation for comparison
|
86
|
+
calls_str = [
|
87
|
+
(func, pos, str(arg_type))
|
88
|
+
for func, pos, arg_type in visitor.calls
|
89
|
+
]
|
90
|
+
assert ("bar", 0, "list[int]") in calls_str
|
91
|
+
assert ("baz", 0, "str") in calls_str
|
92
|
+
|
93
|
+
|
94
|
+
def test_library_call_visitor_direct_import():
|
95
|
+
content = """
|
96
|
+
from foo import bar, baz as b
|
97
|
+
bar({'x': 1})
|
98
|
+
b(42)
|
99
|
+
"""
|
100
|
+
tree = ast.parse(content)
|
101
|
+
|
102
|
+
lib_aliases = set()
|
103
|
+
direct_imports = {"bar": "bar", "b": "baz"}
|
104
|
+
visitor = ModuleCallVisitor(lib_aliases, direct_imports)
|
105
|
+
visitor.visit(tree)
|
106
|
+
|
107
|
+
# Convert to string representation for comparison
|
108
|
+
calls_str = [
|
109
|
+
(func, pos, str(arg_type))
|
110
|
+
for func, pos, arg_type in visitor.calls
|
111
|
+
]
|
112
|
+
assert ("bar", 0, "dict[str, int]") in calls_str
|
113
|
+
assert ("baz", 0, "int") in calls_str
|
114
|
+
|
115
|
+
|
116
|
+
def test_library_call_visitor_multiple_args():
|
117
|
+
content = """
|
118
|
+
import foo as f
|
119
|
+
my_var = "hello"
|
120
|
+
f.bar([1, 2], 80, my_var)
|
121
|
+
"""
|
122
|
+
tree = ast.parse(content)
|
123
|
+
|
124
|
+
lib_aliases = {"f"}
|
125
|
+
direct_imports = {}
|
126
|
+
visitor = ModuleCallVisitor(lib_aliases, direct_imports)
|
127
|
+
visitor.visit(tree)
|
128
|
+
|
129
|
+
# Convert to string representation for comparison
|
130
|
+
calls_str = [
|
131
|
+
(func, pos, str(arg_type))
|
132
|
+
for func, pos, arg_type in visitor.calls
|
133
|
+
]
|
134
|
+
assert ("bar", 0, "list[int]") in calls_str
|
135
|
+
assert ("bar", 1, "int") in calls_str
|
136
|
+
assert ("bar", 2, "str") in calls_str
|
137
|
+
|
138
|
+
|
139
|
+
def test_analyze_file_simple(tmp_file):
|
140
|
+
content = """
|
141
|
+
import foo as f
|
142
|
+
from foo import bar
|
143
|
+
|
144
|
+
def example():
|
145
|
+
data = [{"x": 1}, {"y": 2}]
|
146
|
+
f.bar(data, 40)
|
147
|
+
bar("string")
|
148
|
+
bar(123)
|
149
|
+
bar([1, 2, 3])
|
150
|
+
|
151
|
+
my_list = [1, 2, 3]
|
152
|
+
f.bar(my_list)
|
153
|
+
"""
|
154
|
+
|
155
|
+
tmp_file.write_text(content)
|
156
|
+
usages = analyze_file(tmp_file, 'foo')
|
157
|
+
|
158
|
+
assert usages["bar"][0] == {
|
159
|
+
'int',
|
160
|
+
'list[dict[str, int]]',
|
161
|
+
'list[int]',
|
162
|
+
'str',
|
163
|
+
}
|
164
|
+
|
165
|
+
assert usages["bar"][1] == {"int"}
|
166
|
+
|
167
|
+
|
168
|
+
def test_dict_expansion(tmp_file):
|
169
|
+
content = """
|
170
|
+
import f
|
171
|
+
|
172
|
+
dd = {'a': 5.0}
|
173
|
+
dd2 = {'b': 1, **dd}
|
174
|
+
dd3 = {'c': True, **dd2}
|
175
|
+
dd4 = {**dd3}
|
176
|
+
f.foo(dd2)
|
177
|
+
f.bar(dd3)
|
178
|
+
f.foobar(dd4)
|
179
|
+
"""
|
180
|
+
|
181
|
+
tmp_file.write_text(content)
|
182
|
+
usages = analyze_file(tmp_file, 'f')
|
183
|
+
|
184
|
+
assert usages["foo"][0] == {'dict[str, float | int]'}
|
185
|
+
assert usages["bar"][0] == {'dict[str, bool | float | int]'}
|
186
|
+
assert usages["foobar"][0] == {'dict[str, bool | float | int]'}
|
187
|
+
|
188
|
+
|
189
|
+
def test_list_unpacking(tmp_file):
|
190
|
+
content = """
|
191
|
+
import f
|
192
|
+
|
193
|
+
a = [1, 2]
|
194
|
+
b = [*a, 3.0, "x"]
|
195
|
+
c = ["y", *a]
|
196
|
+
f.foo(b)
|
197
|
+
f.bar(c)
|
198
|
+
"""
|
199
|
+
|
200
|
+
tmp_file.write_text(content)
|
201
|
+
usages = analyze_file(tmp_file, 'f')
|
202
|
+
|
203
|
+
assert usages["foo"][0] == {'list[float | int | str]'}
|
204
|
+
assert usages["bar"][0] == {'list[int | str]'}
|
205
|
+
|
206
|
+
|
207
|
+
def test_tuple_unpacking(tmp_file):
|
208
|
+
content = """
|
209
|
+
import f
|
210
|
+
|
211
|
+
a = (1, 2)
|
212
|
+
b = (*a, 3.0, "x")
|
213
|
+
c = ("y", *a)
|
214
|
+
f.foo(b)
|
215
|
+
f.bar(c)
|
216
|
+
"""
|
217
|
+
|
218
|
+
tmp_file.write_text(content)
|
219
|
+
usages = analyze_file(tmp_file, 'f')
|
220
|
+
|
221
|
+
assert usages["foo"][0] == {'tuple[int, int, float, str]'}
|
222
|
+
assert usages["bar"][0] == {'tuple[str, int, int]'}
|
223
|
+
|
224
|
+
|
225
|
+
def test_mixed_unpacking(tmp_file):
|
226
|
+
content = """
|
227
|
+
import f
|
228
|
+
|
229
|
+
a = [1, 2]
|
230
|
+
b = (3.0, "x")
|
231
|
+
c = [*a, *b, True]
|
232
|
+
d = (*a, *b, False)
|
233
|
+
f.foo(c)
|
234
|
+
f.bar(d)
|
235
|
+
"""
|
236
|
+
|
237
|
+
tmp_file.write_text(content)
|
238
|
+
usages = analyze_file(tmp_file, 'f')
|
239
|
+
|
240
|
+
assert usages["foo"][0] == {'list[bool | float | int | str]'}
|
241
|
+
assert usages["bar"][0] == {'tuple[bool | float | int | str]'}
|
242
|
+
|
243
|
+
|
244
|
+
def test_exact_tuple_unpacking(tmp_file):
|
245
|
+
content = """
|
246
|
+
import f
|
247
|
+
|
248
|
+
# Exact cases - tuple variables and literals give exact positional types
|
249
|
+
t1 = (1, "x")
|
250
|
+
t2 = (*t1, 3.0) # tuple variable expansion - exact
|
251
|
+
t3 = (*(1, "x"), 3.0) # tuple literal expansion - exact
|
252
|
+
|
253
|
+
f.exact1(t2)
|
254
|
+
f.exact2(t3)
|
255
|
+
"""
|
256
|
+
|
257
|
+
tmp_file.write_text(content)
|
258
|
+
usages = analyze_file(tmp_file, 'f')
|
259
|
+
|
260
|
+
# Exact positional types (comma-separated)
|
261
|
+
assert usages["exact1"][0] == {'tuple[int, str, float]'}
|
262
|
+
assert usages["exact2"][0] == {'tuple[int, str, float]'}
|
263
|
+
|
264
|
+
|
265
|
+
def test_inexact_list_to_tuple_unpacking(tmp_file):
|
266
|
+
content = """
|
267
|
+
import f
|
268
|
+
|
269
|
+
# Inexact cases - list variables give union types
|
270
|
+
l1 = [1, 2]
|
271
|
+
t4 = (*l1, "x") # list variable expansion - inexact
|
272
|
+
|
273
|
+
f.inexact(t4)
|
274
|
+
"""
|
275
|
+
|
276
|
+
tmp_file.write_text(content)
|
277
|
+
usages = analyze_file(tmp_file, 'f')
|
278
|
+
|
279
|
+
# Inexact union types (pipe-separated)
|
280
|
+
assert usages["inexact"][0] == {'tuple[int | str]'}
|
@@ -0,0 +1,40 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: typespy
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: Detect python calling types
|
5
|
+
Author: Rasmus Nygren
|
6
|
+
Maintainer: Rasmus Nygren
|
7
|
+
Project-URL: Homepage, https://github.com/rasmusnygren/typespy
|
8
|
+
Project-URL: Repository, https://github.com/rasmusnygren/typespy
|
9
|
+
Keywords: type detection,type tracing,typing
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
13
|
+
Requires-Python: >=3.10
|
14
|
+
Description-Content-Type: text/markdown
|
15
|
+
License-File: LICENSE
|
16
|
+
Dynamic: license-file
|
17
|
+
|
18
|
+
Typespy
|
19
|
+
=======
|
20
|
+
Figure out which types your modules are being called with.
|
21
|
+
|
22
|
+
## Installation
|
23
|
+
```bash
|
24
|
+
pip install typespy
|
25
|
+
```
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
```bash
|
29
|
+
usage: typespy [-h] [--verbose] module_names [module_names ...] dir
|
30
|
+
|
31
|
+
Analyze function call type information
|
32
|
+
|
33
|
+
positional arguments:
|
34
|
+
module_names Name of the modules to analyze
|
35
|
+
dir Path to the root directory of repositories
|
36
|
+
|
37
|
+
options:
|
38
|
+
-h, --help show this help message and exit
|
39
|
+
--verbose Print function calls with file locations
|
40
|
+
```
|
@@ -0,0 +1 @@
|
|
1
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
typespy
|
typespy-0.1.0/typespy.py
ADDED
@@ -0,0 +1,311 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import argparse
|
4
|
+
import ast
|
5
|
+
import os
|
6
|
+
from collections import defaultdict
|
7
|
+
from collections.abc import Collection
|
8
|
+
from typing import NamedTuple
|
9
|
+
from typing import TypeAlias
|
10
|
+
|
11
|
+
ModuleUsageTypes: TypeAlias = dict[str, dict[int, set[str]]]
|
12
|
+
|
13
|
+
|
14
|
+
class ListType(NamedTuple):
|
15
|
+
element_types: frozenset[InferredType] = frozenset()
|
16
|
+
|
17
|
+
def __str__(self) -> str:
|
18
|
+
if not self.element_types:
|
19
|
+
return "list[]"
|
20
|
+
return f"list[{' | '.join(sorted(str(x) for x in self.element_types))}]"
|
21
|
+
|
22
|
+
@classmethod
|
23
|
+
def from_set(cls, s: set[InferredType]) -> ListType:
|
24
|
+
return cls(element_types=frozenset(s))
|
25
|
+
|
26
|
+
|
27
|
+
class TupleType(NamedTuple):
|
28
|
+
element_types: tuple[InferredType, ...] = ()
|
29
|
+
# Use union types if it's not possible to determine
|
30
|
+
# the # of args and their exact type by position
|
31
|
+
use_union_types: bool = False
|
32
|
+
|
33
|
+
def __str__(self) -> str:
|
34
|
+
if self.use_union_types:
|
35
|
+
unique_types = sorted({str(x) for x in self.element_types})
|
36
|
+
return f"tuple[{' | '.join(unique_types)}]"
|
37
|
+
else:
|
38
|
+
return f"tuple[{', '.join(str(x) for x in self.element_types)}]"
|
39
|
+
|
40
|
+
|
41
|
+
class DictType(NamedTuple):
|
42
|
+
key_types: frozenset[InferredType] = frozenset()
|
43
|
+
value_types: frozenset[InferredType] = frozenset()
|
44
|
+
|
45
|
+
def __str__(self) -> str:
|
46
|
+
if not self.key_types and not self.value_types:
|
47
|
+
return "dict[]"
|
48
|
+
key_type = ' | '.join(sorted(str(x) for x in self.key_types))
|
49
|
+
value_type = ' | '.join(sorted(str(x) for x in self.value_types))
|
50
|
+
return f"dict[{key_type}, {value_type}]"
|
51
|
+
|
52
|
+
@classmethod
|
53
|
+
def from_sets(cls, keys: set[InferredType], values: set[InferredType]) -> DictType:
|
54
|
+
return cls(key_types=frozenset(keys), value_types=frozenset(values))
|
55
|
+
|
56
|
+
|
57
|
+
class VarType(NamedTuple):
|
58
|
+
name: str
|
59
|
+
|
60
|
+
def __str__(self) -> str:
|
61
|
+
return f"var:{self.name}"
|
62
|
+
|
63
|
+
|
64
|
+
InferredType = str | ListType | TupleType | DictType | VarType
|
65
|
+
|
66
|
+
|
67
|
+
def _extract_collection_types(inferred_type: InferredType) -> Collection[InferredType]:
|
68
|
+
if isinstance(inferred_type, (ListType, TupleType)):
|
69
|
+
return inferred_type.element_types
|
70
|
+
return [inferred_type]
|
71
|
+
|
72
|
+
|
73
|
+
def _infer_list_type(node: ast.List, var_types: dict[str, InferredType] | None = None) -> ListType:
|
74
|
+
if not node.elts:
|
75
|
+
return ListType()
|
76
|
+
|
77
|
+
element_types: set[InferredType] = set()
|
78
|
+
|
79
|
+
for elt in node.elts:
|
80
|
+
if isinstance(elt, ast.Starred):
|
81
|
+
expanded_type = infer_type(elt.value, var_types)
|
82
|
+
inner_types = _extract_collection_types(expanded_type)
|
83
|
+
element_types.update(inner_types)
|
84
|
+
else:
|
85
|
+
element_types.add(infer_type(elt, var_types))
|
86
|
+
|
87
|
+
return ListType.from_set(element_types)
|
88
|
+
|
89
|
+
|
90
|
+
def _infer_tuple_type(
|
91
|
+
node: ast.Tuple, var_types: dict[str, InferredType] | None = None,
|
92
|
+
) -> TupleType:
|
93
|
+
"""
|
94
|
+
If we can infer the exact number of arguments and types of the tuple
|
95
|
+
arguments then return an exact representation (tuple[int, int, int, str])
|
96
|
+
whereas if we can't, then return a unionized representation of the
|
97
|
+
tuple types (tuple[int | str]).
|
98
|
+
"""
|
99
|
+
if not node.elts:
|
100
|
+
return TupleType()
|
101
|
+
|
102
|
+
tuple_types = []
|
103
|
+
use_union_types = False
|
104
|
+
|
105
|
+
for elt in node.elts:
|
106
|
+
if isinstance(elt, ast.Starred):
|
107
|
+
if isinstance(elt.value, (ast.List, ast.Tuple)):
|
108
|
+
for sub_elt in elt.value.elts:
|
109
|
+
tuple_types.append(infer_type(sub_elt, var_types))
|
110
|
+
else:
|
111
|
+
expanded_type = infer_type(elt.value, var_types)
|
112
|
+
inner_types = _extract_collection_types(expanded_type)
|
113
|
+
tuple_types.extend(inner_types)
|
114
|
+
if isinstance(expanded_type, ListType):
|
115
|
+
use_union_types = True
|
116
|
+
else:
|
117
|
+
tuple_types.append(infer_type(elt, var_types))
|
118
|
+
|
119
|
+
return TupleType(
|
120
|
+
element_types=tuple(tuple_types),
|
121
|
+
use_union_types=use_union_types,
|
122
|
+
)
|
123
|
+
|
124
|
+
|
125
|
+
def _infer_dict_type(node: ast.Dict, var_types: dict[str, InferredType] | None = None) -> DictType:
|
126
|
+
if not node.keys and not node.values:
|
127
|
+
return DictType()
|
128
|
+
|
129
|
+
key_types: set[InferredType] = set()
|
130
|
+
value_types: set[InferredType] = set()
|
131
|
+
|
132
|
+
for key, value in zip(node.keys, node.values):
|
133
|
+
if key is None:
|
134
|
+
# If the key is None, we have a dict-unpacking expression (**dict)
|
135
|
+
# and need to recursively infer the inner types and determine
|
136
|
+
# if they are part of the key types or value types.
|
137
|
+
expanded_type = infer_type(value, var_types)
|
138
|
+
if isinstance(expanded_type, DictType):
|
139
|
+
key_types.update(expanded_type.key_types)
|
140
|
+
value_types.update(expanded_type.value_types)
|
141
|
+
|
142
|
+
else:
|
143
|
+
key_types.add(infer_type(key, var_types))
|
144
|
+
value_types.add(infer_type(value, var_types))
|
145
|
+
|
146
|
+
return DictType.from_sets(key_types, value_types)
|
147
|
+
|
148
|
+
|
149
|
+
def infer_type(node: ast.AST, var_types: dict[str, InferredType] | None = None) -> InferredType:
|
150
|
+
if isinstance(node, ast.Constant):
|
151
|
+
return type(node.value).__name__
|
152
|
+
elif isinstance(node, ast.List):
|
153
|
+
return _infer_list_type(node, var_types)
|
154
|
+
elif isinstance(node, ast.Dict):
|
155
|
+
return _infer_dict_type(node, var_types)
|
156
|
+
elif isinstance(node, ast.Tuple):
|
157
|
+
return _infer_tuple_type(node, var_types)
|
158
|
+
elif isinstance(node, ast.Name):
|
159
|
+
if var_types and node.id in var_types:
|
160
|
+
return var_types[node.id]
|
161
|
+
return VarType(node.id)
|
162
|
+
return "unknown"
|
163
|
+
|
164
|
+
|
165
|
+
def find_import_aliases(
|
166
|
+
tree: ast.AST,
|
167
|
+
module_name: str,
|
168
|
+
) -> tuple[set[str], dict[str, str]]:
|
169
|
+
module_aliases: set[str] = set()
|
170
|
+
direct_imports: dict[str, str] = {}
|
171
|
+
|
172
|
+
for node in ast.walk(tree):
|
173
|
+
if isinstance(node, ast.Import):
|
174
|
+
for alias in node.names:
|
175
|
+
if alias.name == module_name:
|
176
|
+
module_aliases.add(alias.asname or alias.name)
|
177
|
+
elif isinstance(node, ast.ImportFrom):
|
178
|
+
if node.module and node.module.startswith(module_name):
|
179
|
+
for alias in node.names:
|
180
|
+
local_name = alias.asname or alias.name
|
181
|
+
direct_imports[local_name] = alias.name
|
182
|
+
|
183
|
+
return module_aliases, direct_imports
|
184
|
+
|
185
|
+
|
186
|
+
class ModuleCallVisitor(ast.NodeVisitor):
|
187
|
+
def __init__(
|
188
|
+
self,
|
189
|
+
lib_aliases: set[str],
|
190
|
+
direct_imports: dict[str, str],
|
191
|
+
filepath: str | None = None,
|
192
|
+
verbose: bool = False,
|
193
|
+
) -> None:
|
194
|
+
self.lib_aliases = lib_aliases
|
195
|
+
self.direct_imports = direct_imports
|
196
|
+
self.filepath = filepath
|
197
|
+
self.verbose = verbose
|
198
|
+
self.calls: list[tuple[str, int, InferredType]] = []
|
199
|
+
self.var_types: dict[str, InferredType] = {}
|
200
|
+
|
201
|
+
def visit_Assign(self, node: ast.Assign) -> None:
|
202
|
+
if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
|
203
|
+
var_name = node.targets[0].id
|
204
|
+
var_type = infer_type(node.value, self.var_types)
|
205
|
+
if not isinstance(var_type, VarType):
|
206
|
+
self.var_types[var_name] = var_type
|
207
|
+
|
208
|
+
self.generic_visit(node)
|
209
|
+
|
210
|
+
def visit_Call(self, node: ast.Call) -> None:
|
211
|
+
func_name: str | None = None
|
212
|
+
|
213
|
+
if (
|
214
|
+
isinstance(node.func, ast.Attribute)
|
215
|
+
and isinstance(node.func.value, ast.Name)
|
216
|
+
):
|
217
|
+
if node.func.value.id in self.lib_aliases:
|
218
|
+
func_name = node.func.attr
|
219
|
+
|
220
|
+
elif isinstance(node.func, ast.Name) and node.func.id in self.direct_imports:
|
221
|
+
func_name = self.direct_imports[node.func.id]
|
222
|
+
|
223
|
+
if func_name:
|
224
|
+
arg_types: list[InferredType] = []
|
225
|
+
for i, arg in enumerate(node.args):
|
226
|
+
arg_type = infer_type(arg, self.var_types)
|
227
|
+
arg_types.append(arg_type)
|
228
|
+
self.calls.append((func_name, i, arg_type))
|
229
|
+
|
230
|
+
if self.verbose:
|
231
|
+
args_str = ', '.join(str(t) for t in arg_types)
|
232
|
+
print(
|
233
|
+
f"[{self.filepath}:{node.lineno}] {func_name}({args_str})",
|
234
|
+
)
|
235
|
+
|
236
|
+
self.generic_visit(node)
|
237
|
+
|
238
|
+
|
239
|
+
def analyze_file(filepath: str, module_name: str, verbose: bool = False) -> ModuleUsageTypes:
|
240
|
+
with open(filepath, encoding="utf-8") as f:
|
241
|
+
source = f.read()
|
242
|
+
tree = ast.parse(source, filename=filepath)
|
243
|
+
module_aliases, direct_imports = find_import_aliases(tree, module_name)
|
244
|
+
|
245
|
+
usages: ModuleUsageTypes = defaultdict(lambda: defaultdict(set))
|
246
|
+
if module_aliases or direct_imports:
|
247
|
+
visitor = ModuleCallVisitor(
|
248
|
+
module_aliases, direct_imports, filepath, verbose,
|
249
|
+
)
|
250
|
+
visitor.visit(tree)
|
251
|
+
for func_name, arg_pos, arg_type in visitor.calls:
|
252
|
+
usages[func_name][arg_pos].add(str(arg_type))
|
253
|
+
|
254
|
+
return usages
|
255
|
+
|
256
|
+
|
257
|
+
def walk_repo(
|
258
|
+
root_dir: str,
|
259
|
+
module_name: str,
|
260
|
+
verbose: bool = False,
|
261
|
+
) -> ModuleUsageTypes:
|
262
|
+
usages: ModuleUsageTypes = defaultdict(lambda: defaultdict(set))
|
263
|
+
for dirpath, _, filenames in os.walk(root_dir):
|
264
|
+
filenames = [x for x in filenames if x.endswith(".py")]
|
265
|
+
for filename in filenames:
|
266
|
+
for func_name, args_data in analyze_file(
|
267
|
+
os.path.join(dirpath, filename),
|
268
|
+
module_name, verbose,
|
269
|
+
).items():
|
270
|
+
for arg_pos, types in args_data.items():
|
271
|
+
usages[func_name][arg_pos].update(types)
|
272
|
+
return usages
|
273
|
+
|
274
|
+
|
275
|
+
def main() -> int:
|
276
|
+
parser = argparse.ArgumentParser(
|
277
|
+
description="Analyze function call type information",
|
278
|
+
)
|
279
|
+
parser.add_argument(
|
280
|
+
"module_names", nargs="+",
|
281
|
+
help="Name of the modules to analyze",
|
282
|
+
)
|
283
|
+
parser.add_argument(
|
284
|
+
"dir", help="Path to the root directory of repositories",
|
285
|
+
)
|
286
|
+
parser.add_argument(
|
287
|
+
"--verbose", action="store_true",
|
288
|
+
help="Print function calls with file locations",
|
289
|
+
)
|
290
|
+
args = parser.parse_args()
|
291
|
+
|
292
|
+
module_usages: dict[str, ModuleUsageTypes] = {}
|
293
|
+
|
294
|
+
for module in args.module_names:
|
295
|
+
usages = walk_repo(
|
296
|
+
args.dir, module, verbose=args.verbose,
|
297
|
+
)
|
298
|
+
module_usages[module] = usages
|
299
|
+
|
300
|
+
for module, usages in module_usages.items():
|
301
|
+
print(f"=== Module <{module}> summary ===")
|
302
|
+
for func, args_types in usages.items():
|
303
|
+
print(f"Function: {func}")
|
304
|
+
for pos, types in args_types.items():
|
305
|
+
print(f" Arg {pos}: {', '.join(sorted(types))}")
|
306
|
+
print()
|
307
|
+
return 0
|
308
|
+
|
309
|
+
|
310
|
+
if __name__ == "__main__":
|
311
|
+
SystemExit(main())
|