type-less 0.1.1__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.
type_less/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .inference import guess_return_type
2
+ from .inject import fill_type_hints
3
+
4
+ __all__ = ["fill_type_hints", "guess_return_type"]
type_less/inference.py ADDED
@@ -0,0 +1,379 @@
1
+ from __future__ import annotations
2
+ import ast
3
+ import inspect
4
+ from typing import (
5
+ Any,
6
+ Callable,
7
+ Dict,
8
+ Literal,
9
+ Optional,
10
+ Tuple,
11
+ TypedDict,
12
+ Type,
13
+ Union,
14
+ )
15
+ import textwrap
16
+
17
+
18
+ def _snake_case_to_capital_case(name: str) -> str:
19
+ return "".join(word.capitalize() for word in name.split("_"))
20
+
21
+
22
+ def _sanitize_name(name: str) -> str:
23
+ return "".join(c if c.isalnum() else "" for c in name)
24
+
25
+
26
+ def _get_module_type(func: Callable, name: str) -> Type:
27
+ import sys
28
+
29
+ # TODO: support types not at the root
30
+ module = sys.modules.get(func.__module__, None)
31
+ if hasattr(module, name):
32
+ result = getattr(module, name)
33
+ if isinstance(result, type):
34
+ return result
35
+ elif isinstance(result, Callable):
36
+ return guess_return_type(result)
37
+
38
+ return Any
39
+
40
+
41
+ def guess_return_type(func: Callable, use_literals=True) -> Type:
42
+ """
43
+ Infer the return type of a Python function by analyzing its AST.
44
+ For dictionary returns, creates a TypedDict representation.
45
+
46
+ Args:
47
+ func: The function to analyze
48
+
49
+ Returns:
50
+ The inferred return type
51
+ """
52
+
53
+ # If annotations exist, return the return type
54
+ if hasattr(func, "__annotations__") and "return" in func.__annotations__:
55
+ return func.__annotations__["return"]
56
+
57
+ # Get function source code and create AST
58
+ try:
59
+ source = inspect.getsource(func)
60
+ source = textwrap.dedent(source)
61
+ except Exception:
62
+ return Any
63
+
64
+ module = ast.parse(source)
65
+
66
+ # Extract the function definition node
67
+ func_def = module.body[0]
68
+ if not isinstance(func_def, (ast.FunctionDef, ast.AsyncFunctionDef)):
69
+ raise ValueError("Input is not a function definition")
70
+
71
+ # Create a symbol table for type analysis
72
+ symbol_table = {}
73
+
74
+ # Populate the symbol table with type hints from function annotations
75
+ if func_def.returns:
76
+ # If function has a return type annotation, use it directly
77
+ return _resolve_annotation(func_def.returns, {}, func)
78
+
79
+ # Gather type information from annotations and assignments
80
+ _analyze_function_body(func_def, symbol_table, func, use_literals)
81
+
82
+ # Find all return statements
83
+ return_types = []
84
+ for node in ast.walk(func_def):
85
+ if isinstance(node, ast.Return) and node.value:
86
+ return_type = _infer_expr_type(node.value, symbol_table, func, [], use_literals)
87
+ return_types.append(return_type)
88
+
89
+ # If we found return statements
90
+ if return_types:
91
+ if len(return_types) == 1:
92
+ return return_types[0]
93
+ else:
94
+ # Multiple return types - use Union
95
+ return Union[tuple(set(return_types))]
96
+
97
+ # Default to Any if no return statements or couldn't infer
98
+ return Any
99
+
100
+
101
+ def _analyze_function_body(
102
+ func_def: ast.FunctionDef, symbol_table: dict[str, Type], func: Callable, use_literals: bool
103
+ ) -> None:
104
+ """Analyze function body to populate symbol table with type information"""
105
+ # First gather parameter types
106
+ for arg in func_def.args.args:
107
+ if arg.annotation:
108
+ symbol_table[arg.arg] = _resolve_annotation(arg.annotation, {}, func)
109
+
110
+ # Analyze assignments to track variable types
111
+ for node in ast.walk(func_def):
112
+ if isinstance(node, ast.Assign):
113
+ assigned_type = _infer_expr_type(node.value, symbol_table, func, [], use_literals)
114
+ for target in node.targets:
115
+ if isinstance(target, ast.Name):
116
+ symbol_table[target.id] = assigned_type
117
+ elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
118
+ # Handle annotated assignments
119
+ symbol_table[node.target.id] = _resolve_annotation(
120
+ node.annotation, {}, func
121
+ )
122
+
123
+
124
+ def _resolve_annotation(
125
+ annotation: ast.AST, type_context: dict[str, Any], func: Callable
126
+ ) -> Type:
127
+ """Resolve a type annotation AST node to a real type"""
128
+ if isinstance(annotation, ast.Name):
129
+ # Simple types like int, str, etc.
130
+ type_name = annotation.id
131
+ # Check Python's built-in types first
132
+ if type_name in __builtins__:
133
+ return __builtins__[type_name]
134
+
135
+ print(annotation.id)
136
+
137
+ # Otherwise check if it's imported
138
+ return _get_module_type(func, type_name)
139
+
140
+ elif isinstance(annotation, ast.Subscript):
141
+ # Handle generic types like list[int], dict[str, int], etc.
142
+ if isinstance(annotation.value, ast.Name):
143
+ base_type = annotation.value.id
144
+ if base_type == "List" or base_type == "list":
145
+ elem_type = _resolve_annotation(annotation.slice, type_context, func)
146
+ return list[elem_type]
147
+ elif base_type == "Dict" or base_type == "dict":
148
+ if isinstance(annotation.slice, ast.Tuple):
149
+ key_type = _resolve_annotation(
150
+ annotation.slice.elts[0], type_context, func
151
+ )
152
+ val_type = _resolve_annotation(
153
+ annotation.slice.elts[1], type_context, func
154
+ )
155
+ return dict[key_type, val_type]
156
+ return Dict
157
+ elif base_type == "Set" or base_type == "set":
158
+ elem_type = _resolve_annotation(annotation.slice, type_context, func)
159
+ return set[elem_type]
160
+ elif base_type == "Tuple" or base_type == "tuple":
161
+ if isinstance(annotation.slice, ast.Tuple):
162
+ elem_types = [
163
+ _resolve_annotation(e, type_context, func)
164
+ for e in annotation.slice.elts
165
+ ]
166
+ return tuple[tuple(elem_types)]
167
+ return Tuple
168
+ elif base_type == "Optional":
169
+ elem_type = _resolve_annotation(annotation.slice, type_context, func)
170
+ return Optional[elem_type]
171
+ elif base_type == "Union":
172
+ if isinstance(annotation.slice, ast.Tuple):
173
+ elem_types = [
174
+ _resolve_annotation(e, type_context, func)
175
+ for e in annotation.slice.elts
176
+ ]
177
+ return Union[tuple(elem_types)]
178
+ return Union
179
+
180
+ # Fallback for unresolved or complex annotations
181
+ print("FAIL3")
182
+ return Any
183
+
184
+
185
+ def _infer_expr_type(
186
+ node: ast.AST, symbol_table: dict[str, Type], func: Callable, nested_path: list[str], use_literals: bool
187
+ ) -> Type:
188
+ """Infer the type of an expression"""
189
+ if isinstance(node, ast.Dict):
190
+ # For dictionary literals, create a TypedDict
191
+ return _create_typed_dict_from_dict(node, symbol_table, func, nested_path, use_literals)
192
+
193
+ elif isinstance(node, ast.List):
194
+ # Handle list literals
195
+ if not node.elts:
196
+ return list[Any]
197
+ element_types = [
198
+ _infer_expr_type(elt, symbol_table, func, nested_path, use_literals) for elt in node.elts
199
+ ]
200
+ if len(set(element_types)) == 1:
201
+ return list[element_types[0]]
202
+ return list[Union[tuple(set(element_types))]]
203
+
204
+ elif isinstance(node, ast.Tuple):
205
+ # Handle tuple literals
206
+ if not node.elts:
207
+ return tuple[()]
208
+ element_types = [
209
+ _infer_expr_type(elt, symbol_table, func, nested_path, use_literals) for elt in node.elts
210
+ ]
211
+ return tuple[tuple(element_types)]
212
+
213
+ elif isinstance(node, ast.Set):
214
+ # Handle set literals
215
+ if not node.elts:
216
+ return set[Any]
217
+ element_types = [
218
+ _infer_expr_type(elt, symbol_table, func, nested_path, use_literals) for elt in node.elts
219
+ ]
220
+ if len(set(element_types)) == 1:
221
+ return set[element_types[0]]
222
+ return set[Union[tuple(set(element_types))]]
223
+
224
+ elif isinstance(node, ast.Constant):
225
+ # Handle literals
226
+ if use_literals:
227
+ return Literal[node.value]
228
+ else:
229
+ return type(node.value)
230
+
231
+ elif isinstance(node, ast.Name):
232
+ # Look up variable types in the symbol table
233
+ if node.id in symbol_table:
234
+ return symbol_table[node.id]
235
+
236
+ # Handle built-in types referenced by name
237
+ if node.id in __builtins__ and isinstance(__builtins__[node.id], type):
238
+ return __builtins__[node.id]
239
+
240
+ return _get_module_type(func, node.id)
241
+
242
+ elif isinstance(node, ast.Call):
243
+ # Handle function calls - this is complex, so we'll use a simplified approach
244
+ if isinstance(node.func, ast.Name):
245
+ func_name = node.func.id
246
+ # Handle some common built-in functions
247
+ if func_name == "int":
248
+ return int
249
+ elif func_name == "str":
250
+ return str
251
+ elif func_name == "float":
252
+ return float
253
+ elif func_name == "list":
254
+ return list[Any]
255
+ elif func_name == "dict":
256
+ return dict[Any, Any]
257
+ elif func_name == "set":
258
+ return set[Any]
259
+ elif func_name == "tuple":
260
+ return Tuple
261
+
262
+ return _get_module_type(func, func_name)
263
+
264
+ # For other function calls, we default to Any
265
+ return Any
266
+
267
+ elif isinstance(node, ast.BinOp):
268
+ # Handle binary operations
269
+ left_type = _infer_expr_type(node.left, symbol_table, func, nested_path, use_literals)
270
+ right_type = _infer_expr_type(node.right, symbol_table, func, nested_path, use_literals)
271
+
272
+ # String concatenation
273
+ if isinstance(node.op, ast.Add) and (left_type == str or right_type == str):
274
+ return str
275
+
276
+ # Numeric operations typically return numeric types
277
+ if isinstance(node.op, (ast.Add, ast.Sub, ast.Mult, ast.Div)):
278
+ if left_type == float or right_type == float:
279
+ return float
280
+ return int
281
+
282
+ return Any
283
+
284
+ elif isinstance(node, ast.Compare):
285
+ return bool
286
+
287
+ elif isinstance(node, ast.IfExp):
288
+ body_type = _infer_expr_type(node.body, symbol_table, func, nested_path, use_literals)
289
+ orelse_type = _infer_expr_type(node.orelse, symbol_table, func, nested_path, use_literals)
290
+ return body_type | orelse_type
291
+
292
+ elif isinstance(node, ast.Attribute):
293
+ # Handle attribute access (e.g., obj.attr)
294
+ if isinstance(node.value, ast.Name) and node.value.id in symbol_table:
295
+ print(f"TRYYYY {node.value.id}")
296
+ # Get the type of the object
297
+ obj_type = symbol_table[node.value.id]
298
+ print(obj_type)
299
+ print(type(obj_type))
300
+
301
+ # If the object has type annotations, try to get the attribute type
302
+ if hasattr(obj_type, "__annotations__") and node.attr in obj_type.__annotations__:
303
+ return obj_type.__annotations__[node.attr]
304
+
305
+ # If the object is a class with class variables
306
+ if isinstance(obj_type, type):
307
+ if hasattr(obj_type, node.attr):
308
+ attr_value = getattr(obj_type, node.attr)
309
+ # If it's a Literal type or other type annotation
310
+ if hasattr(attr_value, "__origin__") and attr_value.__origin__ is Literal:
311
+ return attr_value
312
+ # For regular attributes, infer their type
313
+ return type(attr_value)
314
+
315
+ # For other attribute access, default to Any
316
+ print("FAIL3,5")
317
+ return Any
318
+
319
+ # Default for complex or unknown expressions
320
+ print("FAIL4")
321
+ print(node)
322
+ print(type(node))
323
+ return Any
324
+
325
+
326
+ def _create_typed_dict_from_dict(
327
+ dict_node: ast.Dict,
328
+ symbol_table: dict[str, Type],
329
+ func: Callable,
330
+ nested_path: list[str],
331
+ use_literals: bool,
332
+ ) -> Type:
333
+ """Create a TypedDict from a dictionary literal"""
334
+ # Check if all keys are string literals
335
+ field_types = {}
336
+ is_valid_typeddict = True
337
+
338
+ for i, key in enumerate(dict_node.keys):
339
+ if isinstance(key, ast.Constant) and isinstance(key.value, str):
340
+ value_type = _infer_expr_type(
341
+ dict_node.values[i], symbol_table, func, nested_path + [key.value], use_literals
342
+ )
343
+ field_types[key.value] = value_type
344
+ else:
345
+ is_valid_typeddict = False
346
+ break
347
+
348
+ if is_valid_typeddict and field_types:
349
+ # Create a dynamic TypedDict class
350
+ # Capitalize function name and remove underscores
351
+ class_name = f"{_snake_case_to_capital_case(func.__name__)}Return"
352
+
353
+ # Add nested path components
354
+ for component in nested_path:
355
+ class_name += _snake_case_to_capital_case(_sanitize_name(component))
356
+ return TypedDict(class_name, field_types)
357
+
358
+ # If not a valid TypedDict, return a regular Dict with inferred types
359
+ if dict_node.keys:
360
+ key_types = [
361
+ _infer_expr_type(key, symbol_table, func, nested_path + [key], use_literals)
362
+ for key in dict_node.keys
363
+ ]
364
+ value_types = [
365
+ _infer_expr_type(value, symbol_table, func, nested_path, use_literals)
366
+ for value in dict_node.values
367
+ ]
368
+
369
+ # Determine common types
370
+ if len(set(key_types)) == 1 and len(set(value_types)) == 1:
371
+ return dict[key_types[0], value_types[0]]
372
+ elif len(set(key_types)) == 1:
373
+ return dict[key_types[0], Union[tuple(set(value_types))]]
374
+ elif len(set(value_types)) == 1:
375
+ return dict[Union[tuple(set(key_types))], value_types[0]]
376
+ else:
377
+ return dict[Union[tuple(set(key_types))], Union[tuple(set(value_types))]]
378
+
379
+ return dict[Any, Any]
type_less/inject.py ADDED
@@ -0,0 +1,40 @@
1
+ from .inference import guess_return_type
2
+ from functools import wraps
3
+ from typing import Callable, TypeVar
4
+
5
+
6
+ def fill_type_hints(func: Callable, use_literals=False):
7
+ if not getattr(func, "__annotations__", None):
8
+ func.__annotations__ = {}
9
+
10
+ if not "return" in func.__annotations__:
11
+ func.__annotations__["return"] = guess_return_type(func, use_literals=use_literals)
12
+
13
+
14
+ T = TypeVar("T")
15
+ def fastapi_app_inject_types(app: T, use_literals=False) -> T:
16
+ """
17
+ Auto-injects return types into FastAPI untyped routes.
18
+
19
+ Can be run before or after route initialization
20
+ Arguments:
21
+ app: FastAPI Application
22
+ """
23
+ routes = getattr(app, 'routes', None)
24
+ if not type(routes) is list:
25
+ raise ValueError("Invalid app provided, no routes found. Please be sure this is a FastAPI app.")
26
+ for route in routes:
27
+ endpoint = getattr(route, "endpoint", None)
28
+ if not endpoint:
29
+ raise ValueError(f"Route {route} is missing an endpoint function")
30
+ fill_type_hints(endpoint, use_literals=use_literals)
31
+
32
+ # Auto-fill type hings
33
+ app_add_route = app.router.add_api_route
34
+ @wraps(app_add_route)
35
+ def add_route_injected(path: str, func, *args, **kwargs):
36
+ fill_type_hints(func, use_literals=use_literals)
37
+ app_add_route(path=path, endpoint=func, *args, **kwargs)
38
+ app.router.add_api_route = add_route_injected
39
+
40
+ return app
@@ -0,0 +1,65 @@
1
+ Metadata-Version: 2.4
2
+ Name: type-less
3
+ Version: 0.1.1
4
+ Summary: Type less types with inferred return types
5
+ Requires-Python: >=3.12
6
+ Description-Content-Type: text/markdown
7
+
8
+ # Type-less
9
+
10
+ Type less with automatic type inference for Python! Inject function return types at runtime for code generation.
11
+
12
+ ## FastAPI Example
13
+
14
+ ```python
15
+ @app.get("/user/me")
16
+ def user_me():
17
+ return {
18
+ "id": 1,
19
+ "balance": 13.37,
20
+ "name": {
21
+ "first": "Testy",
22
+ "last": "McTestFace",
23
+ },
24
+ }
25
+ ```
26
+
27
+ 🚀 Generates Return Types:
28
+
29
+ ```python
30
+ class UserMeReturnName(TypedDict):
31
+ first: str
32
+ last: str
33
+
34
+ class UserMeReturn(TypedDict):
35
+ id: str
36
+ balance: str
37
+ name: UserMeReturnName
38
+ ```
39
+
40
+ 📋 OpenAPI Spec:
41
+
42
+ <img src="docs/example.png" alt="OpenAPI Example" height="170">
43
+
44
+ ## Using in FastAPI Project
45
+
46
+ Inject types before by hooking before setting up routes. Types will be automatically generated when new routes are added.
47
+
48
+ ```python
49
+ from type_less.inject import inject_fastapi_route_types
50
+ from fastapi import FastAPI
51
+
52
+ app = FastAPI()
53
+ app = inject_fastapi_route_types(app)
54
+
55
+ @app.get("/test")
56
+ def test(request):
57
+ ...
58
+ ```
59
+
60
+ ## TODO:
61
+ ### Add Support:
62
+ * Nested class inference
63
+ * Deep function call / return inference
64
+ ### Better Way?:
65
+ * Possibly use pyre, mypy, or anything else to infer the type?
@@ -0,0 +1,6 @@
1
+ type_less/__init__.py,sha256=ClsxzRxElZeEe-kqjZ_k7Z0AZVL3pgOe0Dz6g2lJ_pQ,128
2
+ type_less/inference.py,sha256=Obtucq_mAK1QkUJvSVEN_ClWZRkD8cc5kgnhzE7ZeIw,13766
3
+ type_less/inject.py,sha256=s-JGJlrTMOKV0m_alcImPMCnZQ0W2a1wRbQfR2nnE8c,1422
4
+ type_less-0.1.1.dist-info/METADATA,sha256=GMlDvalmZniLrJDP7SJ8tewMb1337sa0Exph-P19hik,1332
5
+ type_less-0.1.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
6
+ type_less-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any