typespy 0.1.0__py3-none-any.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.
@@ -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,7 @@
1
+ typespy.py,sha256=HsGk9vSahA1zpQAc73kC4bkXm2r3n8qzeRwdP1_wr1Q,10525
2
+ typespy-0.1.0.dist-info/licenses/LICENSE,sha256=pdhK9ZMpWV_vjPnMCHH5Qga0bfLbDqDfS36yM2WwUHA,1069
3
+ typespy-0.1.0.dist-info/METADATA,sha256=TK9_UoceXSBoaRZVOSo6x0JrDukicDQsmaBcINoT37c,1069
4
+ typespy-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
5
+ typespy-0.1.0.dist-info/entry_points.txt,sha256=7ltHj-X4VwvZFZ57Z7qOcdFhNzR8sPbeUvy-nMN98A4,41
6
+ typespy-0.1.0.dist-info/top_level.txt,sha256=1EhqvcbjTtVnohRE-zdooUCoEloaJMJo1dQxuEFY8Bo,8
7
+ typespy-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ typespy = typespy:main
@@ -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.
@@ -0,0 +1 @@
1
+ typespy
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())