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 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
+ ```
@@ -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"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,10 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ typespy.py
5
+ tests/test_analyze.py
6
+ typespy.egg-info/PKG-INFO
7
+ typespy.egg-info/SOURCES.txt
8
+ typespy.egg-info/dependency_links.txt
9
+ typespy.egg-info/entry_points.txt
10
+ typespy.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ typespy = typespy:main
@@ -0,0 +1 @@
1
+ typespy
@@ -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())