Skip to content

Commit

Permalink
infer return types
Browse files Browse the repository at this point in the history
  • Loading branch information
KotlinIsland committed Jan 7, 2025
1 parent 8cd497f commit e8d60a5
Show file tree
Hide file tree
Showing 15 changed files with 187 additions and 84 deletions.
46 changes: 15 additions & 31 deletions .mypy/baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -1989,15 +1989,15 @@
"code": "explicit-override",
"column": 4,
"message": "Method \"type_context\" is not using @override but is overriding a method in class \"mypy.plugin.CheckerPluginInterface\"",
"offset": 456,
"offset": 464,
"src": "def type_context(self) -> list[Type | None]:",
"target": "mypy.checker"
},
{
"code": "explicit-override",
"column": 4,
"message": "Method \"visit_overloaded_func_def\" is not using @override but is overriding a method in class \"mypy.visitor.NodeVisitor\"",
"offset": 191,
"offset": 197,
"src": "def visit_overloaded_func_def(self, defn: OverloadedFuncDef, do_items=True) -> None:",
"target": "mypy.checker.TypeChecker.visit_overloaded_func_def"
},
Expand Down Expand Up @@ -2029,7 +2029,7 @@
"code": "explicit-override",
"column": 4,
"message": "Method \"visit_class_def\" is not using @override but is overriding a method in class \"mypy.visitor.NodeVisitor\"",
"offset": 1113,
"offset": 1142,
"src": "def visit_class_def(self, defn: ClassDef) -> None:",
"target": "mypy.checker.TypeChecker.visit_class_def"
},
Expand Down Expand Up @@ -2093,7 +2093,7 @@
"code": "truthy-bool",
"column": 23,
"message": "\"signature\" has type \"Type\" which does not implement __bool__ or __len__ so it could always be true in boolean context",
"offset": 169,
"offset": 172,
"src": "if signature:",
"target": "mypy.checker.TypeChecker.check_assignment"
},
Expand Down Expand Up @@ -2245,7 +2245,7 @@
"code": "explicit-override",
"column": 4,
"message": "Method \"visit_if_stmt\" is not using @override but is overriding a method in class \"mypy.visitor.NodeVisitor\"",
"offset": 111,
"offset": 121,
"src": "def visit_if_stmt(self, s: IfStmt) -> None:",
"target": "mypy.checker.TypeChecker.visit_if_stmt"
},
Expand Down Expand Up @@ -2639,7 +2639,7 @@
"code": "truthy-bool",
"column": 34,
"message": "\"item_name_expr\" has type \"Expression\" which does not implement __bool__ or __len__ so it could always be true in boolean context",
"offset": 424,
"offset": 430,
"src": "key_context = item_name_expr or item_arg",
"target": "mypy.checkexpr.ExpressionChecker.validate_typeddict_kwargs"
},
Expand Down Expand Up @@ -3055,7 +3055,7 @@
"code": "explicit-override",
"column": 4,
"message": "Method \"visit_await_expr\" is not using @override but is overriding a method in class \"mypy.visitor.ExpressionVisitor\"",
"offset": 21,
"offset": 30,
"src": "def visit_await_expr(self, e: AwaitExpr, allow_none_return: bool = False) -> Type:",
"target": "mypy.checkexpr.ExpressionChecker.visit_await_expr"
},
Expand Down Expand Up @@ -6859,7 +6859,7 @@
"code": "redundant-expr",
"column": 19,
"message": "Condition is always false",
"offset": 375,
"offset": 377,
"src": "if e.type is None:",
"target": "mypy.errors.Errors.render_messages"
},
Expand Down Expand Up @@ -7922,31 +7922,31 @@
{
"code": "no-any-expr",
"column": 8,
"message": "Expression type contains \"Any\" (has type \"(int, (arg | Any, expr))\")",
"message": "Expression type contains \"Any\" (has type \"(int, (Any | arg, expr))\")",
"offset": 3,
"src": "for i, (a, d) in enumerate(zip(args_args[num_no_defaults:], args_defaults)):",
"target": "mypy.fastparse.ASTConverter.transform_args"
},
{
"code": "no-any-expr",
"column": 8,
"message": "Expression type contains \"Any\" (has type \"(arg | Any, expr)\")",
"message": "Expression type contains \"Any\" (has type \"(Any | arg, expr)\")",
"offset": 0,
"src": "for i, (a, d) in enumerate(zip(args_args[num_no_defaults:], args_defaults)):",
"target": "mypy.fastparse.ASTConverter.transform_args"
},
{
"code": "no-any-expr",
"column": 25,
"message": "Expression type contains \"Any\" (has type \"enumerate[(arg | Any, expr)]\")",
"message": "Expression type contains \"Any\" (has type \"enumerate[(Any | arg, expr)]\")",
"offset": 0,
"src": "for i, (a, d) in enumerate(zip(args_args[num_no_defaults:], args_defaults)):",
"target": "mypy.fastparse.ASTConverter.transform_args"
},
{
"code": "no-any-expr",
"column": 35,
"message": "Expression type contains \"Any\" (has type \"zip[(arg | Any, expr)]\")",
"message": "Expression type contains \"Any\" (has type \"zip[(Any | arg, expr)]\")",
"offset": 0,
"src": "for i, (a, d) in enumerate(zip(args_args[num_no_defaults:], args_defaults)):",
"target": "mypy.fastparse.ASTConverter.transform_args"
Expand Down Expand Up @@ -10759,7 +10759,7 @@
"code": "redundant-expr",
"column": 18,
"message": "Condition is always true",
"offset": 104,
"offset": 70,
"src": "while True:",
"target": "mypy.ipc.IPCBase.read"
}
Expand Down Expand Up @@ -33631,7 +33631,7 @@
"code": "explicit-override",
"column": 4,
"message": "Method \"describe\" is not using @override but is overriding a method in class \"mypy.types.AnyType\"",
"offset": 59,
"offset": 61,
"src": "def describe(self) -> str:",
"target": "mypy.types.UntypedType.describe"
},
Expand Down Expand Up @@ -36513,27 +36513,11 @@
"src": "setattr(new, attr, getattr(old, attr))",
"target": "mypy.util.replace_object_state"
},
{
"code": "no-any-expr",
"column": 21,
"message": "Expression type contains \"Any\" (has type \"int | Any\")",
"offset": 281,
"src": "fd = sys.stdout.fileno()",
"target": "mypy.util.FancyFormatter.initialize_unix_colors"
},
{
"code": "no-any-expr",
"column": 36,
"message": "Expression type contains \"Any\" (has type \"int | Any\")",
"offset": 5,
"src": "curses.setupterm(fd=fd)",
"target": "mypy.util.FancyFormatter.initialize_unix_colors"
},
{
"code": "no-any-explicit",
"column": 0,
"message": "Explicit \"Any\" is not allowed",
"offset": 269,
"offset": 555,
"src": "def getattr(value: object, name: str) -> Any: ...",
"target": "mypy.util.getattr"
},
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Basedmypy Changelog

## [Unreleased]
### Added
- infer return types and generator types

## [2.9.0]
### Added
Expand Down
18 changes: 18 additions & 0 deletions docs/source/based_inference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,21 @@ When a parameter is named `_`, it's type will be inferred as `object`:
reveal_type(_) # Revealed type is "object"
This is to help with writing functions for callbacks where you don't care about certain parameters.


Return Type Inferred
--------------------

.. code-block:: python
def f(): # Revealed type is "() -> 1"
return 1
Generator Type Inferred
-----------------------

.. code-block:: python
def f(): # Revealed type is "() -> Generator[1, str, 2]"
a: str = yield 1
return 2
80 changes: 68 additions & 12 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,9 @@
# Maximum length of fixed tuple types inferred when narrowing from variadic tuples.
MAX_PRECISE_TUPLE_SIZE: Final = 8

DeferredNodeType: _TypeAlias = Union[FuncDef, LambdaExpr, OverloadedFuncDef, Decorator]
DeferredNodeType: _TypeAlias = Union[
FuncDef, LambdaExpr, OverloadedFuncDef, Decorator, AssignmentStmt
]
FineGrainedDeferredNodeType: _TypeAlias = Union[FuncDef, MypyFile, OverloadedFuncDef]


Expand All @@ -260,7 +262,7 @@
class DeferredNode(NamedTuple):
node: DeferredNodeType
# And its TypeInfo (for semantic analysis self type handling
active_typeinfo: TypeInfo | None
active_typeinfo: TypeInfo | FuncItem | None


# Same as above, but for fine-grained mode targets. Only top-level functions/methods
Expand Down Expand Up @@ -452,6 +454,12 @@ def __init__(
# always the next if statement to have a redundant expression
self.allow_redundant_expr = False

self.should_defer_current_node = False
"""when a parent node should be defered"""
self.inferred_return_types: list[Type] = []
self.inferred_yield_types: list[Type] = []
self.inferred_send_types: list[Type] = []

@property
def type_context(self) -> list[Type | None]:
return self.expr_checker.type_context
Expand Down Expand Up @@ -552,8 +560,14 @@ def check_second_pass(
done.add(node)
with ExitStack() as stack:
if active_typeinfo:
stack.enter_context(self.tscope.class_scope(active_typeinfo))
stack.enter_context(self.scope.push_class(active_typeinfo))
if isinstance(active_typeinfo, TypeInfo):
stack.enter_context(self.tscope.class_scope(active_typeinfo))
stack.enter_context(self.scope.push_class(active_typeinfo))
else:
stack.enter_context(self.scope.push_function(active_typeinfo))
with self.tscope.function_scope(active_typeinfo):
self.check_partial(node)
continue
self.check_partial(node)
return True

Expand All @@ -579,7 +593,7 @@ def check_top_level(self, node: MypyFile) -> None:
assert not self.current_node_deferred
# TODO: Handle __all__

def defer_node(self, node: DeferredNodeType, enclosing_class: TypeInfo | None) -> None:
def defer_node(self, node: DeferredNodeType, enclosing_class: TypeInfo | FuncItem | None) -> None:
"""Defer a node for processing during next type-checking pass.
Args:
Expand Down Expand Up @@ -1577,6 +1591,12 @@ def check_func_def(
new_frame = self.binder.push_frame()
new_frame.types[key] = narrowed_type
self.binder.declarations[key] = old_binder.declarations[key]
inferred_return_types = self.inferred_return_types
self.inferred_return_types = []
inferred_yield_types = self.inferred_yield_types
self.inferred_yield_types = []
inferred_send_types = self.inferred_send_types
self.inferred_send_types = []
with self.scope.push_function(defn):
# We suppress reachability warnings for empty generator functions
# (return; yield) which have a "yield" that's unreachable by definition
Expand All @@ -1591,6 +1611,28 @@ def check_func_def(
if _is_empty_generator_function(item) or len(expanded) >= 2:
self.binder.suppress_unreachable_warnings()
self.accept(item.body)
if self.options.default_return:
if not self.binder.is_unreachable():
self.inferred_return_types.append(NoneType())
if item.type and isinstance(item.type, CallableType) and isinstance(ret_type, UntypedType) and ret_type.type_of_any == TypeOfAny.to_be_inferred:
ret_type = make_simplified_union(self.inferred_return_types)
item.type = item.type.copy_modified(ret_type=ret_type)
if self.inferred_yield_types or self.inferred_send_types:
yield_type = make_simplified_union(self.inferred_yield_types) if self.inferred_yield_types else self.named_type("builtins.object")
# `Never` here isn't ideal, and neither is `object`, so we just go with the default typevar value
send_type = make_simplified_intersection(self.inferred_send_types) if self.inferred_send_types else NoneType()
assert isinstance(item.type, CallableType)
item.type.ret_type = Instance(
self.lookup_typeinfo("typing.Generator"),
[
yield_type,
send_type,
item.type.ret_type,
],
)
self.inferred_return_types = inferred_return_types
self.inferred_yield_types = inferred_yield_types
self.inferred_send_types = inferred_send_types
unreachable = self.binder.is_unreachable()
if new_frame is not None:
self.binder.pop_frame(True, 0)
Expand Down Expand Up @@ -1783,13 +1825,14 @@ def check_for_missing_annotations(self, fdef: FuncItem) -> None:
if not fdef.arguments or (
len(fdef.arguments) == 1 and (fdef.arg_names[0] in ("self", "cls"))
):
self.fail(message_registry.RETURN_TYPE_EXPECTED, fdef)
if not has_return_statement(fdef) and not fdef.is_generator:
self.note(
'Use "-> None" if function does not return a value',
fdef,
code=codes.NO_UNTYPED_DEF,
)
if not self.options.default_return:
self.fail(message_registry.RETURN_TYPE_EXPECTED, fdef)
if not has_return_statement(fdef) and not fdef.is_generator:
self.note(
'Use "-> None" if function does not return a value',
fdef,
code=codes.NO_UNTYPED_DEF,
)
else:
self.fail(message_registry.FUNCTION_TYPE_EXPECTED, fdef)
elif isinstance(fdef.type, CallableType):
Expand Down Expand Up @@ -3260,6 +3303,9 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
s.new_syntax,
override_infer=s.unanalyzed_type is not None,
)
if self.should_defer_current_node:
self.defer_node(s, self.scope.top_function())
self.should_defer_current_node = False
if s.is_alias_def:
self.check_type_alias_rvalue(s)

Expand Down Expand Up @@ -5055,6 +5101,16 @@ def check_return_stmt(self, s: ReturnStmt) -> None:
if defn.is_async_generator:
self.fail(message_registry.RETURN_IN_ASYNC_GENERATOR, s)
return
if defn.type:
assert isinstance(defn.type, CallableType)
proper_type = get_proper_type(defn.type.ret_type)
infer = isinstance(proper_type, UntypedType) \
and proper_type.type_of_any == TypeOfAny.to_be_inferred
else:
infer = True
if infer:
self.inferred_return_types.append(typ)
return
# Returning a value of type Any is always fine.
if isinstance(typ, AnyType):
# (Unless you asked to be warned in that case, and the
Expand Down
Loading

0 comments on commit e8d60a5

Please sign in to comment.