From f1c41c1219e134b785785ba296c327410855b3f6 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sun, 25 Sep 2022 12:54:36 -0700 Subject: [PATCH 1/3] Suggest using upper bound for unbound tvar Also don't complain about other TypeVarLikeTypes Implements https://github.com/python/mypy/pull/13166#issuecomment-1186582122 --- mypy/checker.py | 15 ++++++++++++--- test-data/unit/check-typevar-unbound.test | 16 +++++++++++----- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index de98fa0fa179..58506930bfe7 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1231,13 +1231,22 @@ def check_func_def(self, defn: FuncItem, typ: CallableType, name: str | None) -> def check_unbound_return_typevar(self, typ: CallableType) -> None: """Fails when the return typevar is not defined in arguments.""" - if typ.ret_type in typ.variables: - arg_type_visitor = CollectArgTypes() + if isinstance(typ.ret_type, TypeVarType) and typ.ret_type in typ.variables: + arg_type_visitor = CollectArgTypeVarTypes() for argtype in typ.arg_types: argtype.accept(arg_type_visitor) if typ.ret_type not in arg_type_visitor.arg_types: self.fail(message_registry.UNBOUND_TYPEVAR, typ.ret_type, code=TYPE_VAR) + if not ( + isinstance(typ.ret_type.upper_bound, Instance) + and typ.ret_type.upper_bound.type.fullname == "builtins.object" + ): + self.note( + "Consider using the upper bound " + f"{format_type(typ.ret_type.upper_bound)} instead", + context=typ.ret_type, + ) def check_default_args(self, item: FuncItem, body_is_trivial: bool) -> None: for arg in item.arguments: @@ -6375,7 +6384,7 @@ def has_valid_attribute(self, typ: Type, name: str) -> bool: return not watcher.has_new_errors() -class CollectArgTypes(TypeTraverserVisitor): +class CollectArgTypeVarTypes(TypeTraverserVisitor): """Collects the non-nested argument types in a set.""" def __init__(self) -> None: diff --git a/test-data/unit/check-typevar-unbound.test b/test-data/unit/check-typevar-unbound.test index d7df9ad6d94a..d3e54c75e373 100644 --- a/test-data/unit/check-typevar-unbound.test +++ b/test-data/unit/check-typevar-unbound.test @@ -1,4 +1,3 @@ - [case testUnboundTypeVar] from typing import TypeVar @@ -6,9 +5,19 @@ T = TypeVar('T') def f() -> T: # E: A function returning TypeVar should receive at least one argument containing the same TypeVar ... - f() +U = TypeVar('U', bound=int) + +def g() -> U: # E: A function returning TypeVar should receive at least one argument containing the same TypeVar \ + # N: Consider using the upper bound "int" instead + ... + +V = TypeVar('V', int, str) + +# TODO: this should also give an error +def h() -> V: + ... [case testInnerFunctionTypeVar] @@ -21,7 +30,6 @@ def g(a: T) -> T: ... return f() - [case testUnboundIterableOfTypeVars] from typing import Iterable, TypeVar @@ -29,7 +37,6 @@ T = TypeVar('T') def f() -> Iterable[T]: ... - f() [case testBoundTypeVar] @@ -40,7 +47,6 @@ T = TypeVar('T') def f(a: T, b: T, c: int) -> T: ... - [case testNestedBoundTypeVar] from typing import Callable, List, Union, Tuple, TypeVar From 823fae64ab7f350ca374a2e6a94504f645c62c58 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sun, 25 Sep 2022 12:57:14 -0700 Subject: [PATCH 2/3] test --- test-data/unit/check-classes.test | 1 + 1 file changed, 1 insertion(+) diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 5f1c23b756ed..dff14f197be5 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -3233,6 +3233,7 @@ def error(u_c: Type[U]) -> P: # Error here, see below [out] main:11: note: Revealed type is "__main__.WizUser" main:12: error: A function returning TypeVar should receive at least one argument containing the same TypeVar +main:12: note: Consider using the upper bound "ProUser" instead main:13: error: Value of type variable "P" of "new_pro" cannot be "U" main:13: error: Incompatible return value type (got "U", expected "P") From 6b49cedb2bf1a18623760a19bd21a8d17a656892 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sun, 25 Sep 2022 13:26:45 -0700 Subject: [PATCH 3/3] self check --- mypy/checker.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 58506930bfe7..cc35e401f74c 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1238,9 +1238,10 @@ def check_unbound_return_typevar(self, typ: CallableType) -> None: if typ.ret_type not in arg_type_visitor.arg_types: self.fail(message_registry.UNBOUND_TYPEVAR, typ.ret_type, code=TYPE_VAR) + upper_bound = get_proper_type(typ.ret_type.upper_bound) if not ( - isinstance(typ.ret_type.upper_bound, Instance) - and typ.ret_type.upper_bound.type.fullname == "builtins.object" + isinstance(upper_bound, Instance) + and upper_bound.type.fullname == "builtins.object" ): self.note( "Consider using the upper bound "