From decb5fd34a4f967f7fc728d6c7c158639ebd01ef Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 29 Sep 2018 01:29:21 +0100 Subject: [PATCH 01/12] Don't show errors before inference is done --- mypy/applytype.py | 23 ++- mypy/checkexpr.py | 7 +- test-data/unit/check-generics.test | 2 +- test-data/unit/check-inference-context.test | 157 ++++++++++++++++++++ test-data/unit/check-overloading.test | 6 +- test-data/unit/check-typevar-values.test | 2 +- test-data/unit/fixtures/list.pyi | 7 +- test-data/unit/pythoneval.test | 28 ++++ 8 files changed, 215 insertions(+), 17 deletions(-) diff --git a/mypy/applytype.py b/mypy/applytype.py index e1d81218b2f8..627438835181 100644 --- a/mypy/applytype.py +++ b/mypy/applytype.py @@ -9,7 +9,7 @@ def apply_generic_arguments(callable: CallableType, orig_types: Sequence[Optional[Type]], - msg: MessageBuilder, context: Context) -> CallableType: + msg: MessageBuilder, context: Context, silent: bool = False) -> CallableType: """Apply generic type arguments to a callable type. For example, applying [int] to 'def [T] (T) -> T' results in @@ -34,15 +34,28 @@ def apply_generic_arguments(callable: CallableType, orig_types: Sequence[Optiona if all(any(is_same_type(v, v1) for v in values) for v1 in type.values): continue + matching = [] for value in values: if mypy.subtypes.is_subtype(type, value): - types[i] = value - break + matching.append(value) + if matching: + best = matching[0] + # If there are more than one matching value, we select the narrowest + for match in matching[1:]: + if mypy.subtypes.is_subtype(match, best): + best = match + types[i] = best else: - msg.incompatible_typevar_value(callable, type, callable.variables[i].name, context) + if silent: + types[i] = None + else: + msg.incompatible_typevar_value(callable, type, callable.variables[i].name, context) upper_bound = callable.variables[i].upper_bound if type and not mypy.subtypes.is_subtype(type, upper_bound): - msg.incompatible_typevar_value(callable, type, callable.variables[i].name, context) + if silent: + types[i] = None + else: + msg.incompatible_typevar_value(callable, type, callable.variables[i].name, context) # Create a map from type variable id to target type. id_to_type = {} # type: Dict[TypeVarId, Type] diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 0ba58694792e..7ccb8cdffce2 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -845,7 +845,8 @@ def infer_function_type_arguments_using_context( new_args.append(None) else: new_args.append(arg) - return self.apply_generic_arguments(callable, new_args, error_context) + # Don't show errors after only using outer context. + return self.apply_generic_arguments(callable, new_args, error_context, silent=True) def infer_function_type_arguments(self, callee_type: CallableType, args: List[Expression], @@ -1613,9 +1614,9 @@ def check_arg(caller_type: Type, original_caller_type: Type, caller_kind: int, return False def apply_generic_arguments(self, callable: CallableType, types: Sequence[Optional[Type]], - context: Context) -> CallableType: + context: Context, silent: bool = False) -> CallableType: """Simple wrapper around mypy.applytype.apply_generic_arguments.""" - return applytype.apply_generic_arguments(callable, types, self.msg, context) + return applytype.apply_generic_arguments(callable, types, self.msg, context, silent=silent) def visit_member_expr(self, e: MemberExpr, is_lvalue: bool = False) -> Type: """Visit member expression (of form e.id).""" diff --git a/test-data/unit/check-generics.test b/test-data/unit/check-generics.test index e6fcda7ae6e2..aaa9283cd0d7 100644 --- a/test-data/unit/check-generics.test +++ b/test-data/unit/check-generics.test @@ -849,7 +849,7 @@ def fun2(v: Vec[T], scale: T) -> Vec[T]: return v reveal_type(fun1([(1, 1)])) # E: Revealed type is 'builtins.int*' -fun1(1) # E: Argument 1 to "fun1" has incompatible type "int"; expected "List[Tuple[int, int]]" +fun1(1) # E: Argument 1 to "fun1" has incompatible type "int"; expected "List[Tuple[bool, bool]]" fun1([(1, 'x')]) # E: Cannot infer type argument 1 of "fun1" reveal_type(fun2([(1, 1)], 1)) # E: Revealed type is 'builtins.list[Tuple[builtins.int*, builtins.int*]]' diff --git a/test-data/unit/check-inference-context.test b/test-data/unit/check-inference-context.test index 7b9495f62d2f..caac1ea6e5a2 100644 --- a/test-data/unit/check-inference-context.test +++ b/test-data/unit/check-inference-context.test @@ -920,3 +920,160 @@ class C: def f(self) -> None: g: Callable[[], int] = lambda: 1 or self.x self.x = int() + +[case testWideOuterContext] +from typing import Any, Mapping, Dict, TypeVar + +T = TypeVar('T', bound=Dict[str, Any]) + +def f(arg: T) -> T: + ... +def outer(x: Mapping[str, Any]) -> None: + ... +d: Dict[str, Any] + +m: Mapping[str, Any] = f(d) +outer(f(d)) +[builtins fixtures/dict.pyi] +[out] + +[case testWideOuterContext2] +from typing import Optional, Type, TypeVar + +class A: pass +class B: pass + +class Experimental: + T = TypeVar('T', 'A', 'B') + + def create_component_a(self) -> Optional[A]: + return self.create_component(A) + + def create_component(self, cls: Type[T]) -> Optional[T]: + result = cls() + if bool(): + return result + return None +[builtins fixtures/bool.pyi] +[out] + +[case testWideOuterContext3] +from typing import Callable +from typing import Iterable +from typing import List +from typing import Optional +from typing import TypeVar + + +class C: + x: str + + +T = TypeVar('T') + + +def f(i: Iterable[T], c: Callable[[T], bool]) -> Optional[T]: + for x in i: + if c(x): + return x + else: + return None + + +def g(l: List[C], x: str) -> Optional[C]: + return f(l, lambda d: d.x == x) +[builtins fixtures/list.pyi] +[out] + +[case testWideOuterContext4] +from typing import Callable +from typing import Iterable +from typing import List +from typing import Optional +from typing import TypeVar + + +class C: + x: str + + +T = TypeVar('T') + + +def f(i: Iterable[T], c: Callable[[T], bool]) -> Optional[T]: + for x in i: + if c(x): + return x + else: + return None + + +def g(l: List[C], x: str) -> Optional[C]: + def pred(d: C) -> bool: + return d.x == x + + return f(l, pred) +[builtins fixtures/list.pyi] +[out] + +[case testWideOuterContext5] +from typing import Callable +from typing import Iterable +from typing import Optional +from typing import TypeVar + +E = TypeVar('E') + + +def first( + predicate: Callable[['E'], bool], + iterable: Iterable['E'], +) -> Optional['E']: + for element in iterable: + if predicate(element): + return element + return None + + +def test() -> Optional[int]: + l = [0, 1, 2, 3] + return first(lambda x: x > 1, l) +[builtins fixtures/list.pyi] +[out] + +[case testWideOuterContext6] +from typing import Optional, Type, TypeVar + +class Custom: + pass + +T = TypeVar('T', bound=Custom) + +def a(cls: Type[T]) -> Optional[T]: + return cls() + +def b(cls: Type[T]) -> Optional[T]: + return a(cls) +[out] + +[case testWideOuterContext7] +from typing import TypeVar, List + +class A: + pass +class B(A): + pass +class C: + pass + +LooseType = TypeVar('LooseType', A, B, C) +def foo(xs: List[LooseType]) -> LooseType: + return xs[0] + + +StrictType = TypeVar('StrictType', B, C) +def bar(xs: List[StrictType]) -> StrictType: + foo(xs) + return xs[0] +[builtins fixtures/list.pyi] +[out] diff --git a/test-data/unit/check-overloading.test b/test-data/unit/check-overloading.test index a769f53f43b5..425b14c7adaa 100644 --- a/test-data/unit/check-overloading.test +++ b/test-data/unit/check-overloading.test @@ -4923,11 +4923,7 @@ reveal_type(f(g())) # E: Revealed type is 'builtins.list[builtins.int]' [builtins fixtures/list.pyi] [case testOverloadInferringArgumentsUsingContext2-skip] -# This test case ought to work, but is maybe blocked by -# https://github.com/python/mypy/issues/4872? -# -# See https://github.com/python/mypy/pull/5660#discussion_r219669409 for -# more context. +# Overloads never use outer context for overload selection from typing import Optional, List, overload, TypeVar T = TypeVar('T') diff --git a/test-data/unit/check-typevar-values.test b/test-data/unit/check-typevar-values.test index 43ab8f85a7eb..9ef65f51a76e 100644 --- a/test-data/unit/check-typevar-values.test +++ b/test-data/unit/check-typevar-values.test @@ -18,7 +18,7 @@ s = ['x'] o = [object()] i = f(1) s = f('') -o = f(1) # E: Value of type variable "T" of "f" cannot be "object" +o = f(1) # E: Incompatible types in assignment (expression has type "List[int]", variable has type "List[object]") [builtins fixtures/list.pyi] [case testCallGenericFunctionWithTypeVarValueRestrictionAndAnyArgs] diff --git a/test-data/unit/fixtures/list.pyi b/test-data/unit/fixtures/list.pyi index a80b1475bdcd..afef7c1aeb39 100644 --- a/test-data/unit/fixtures/list.pyi +++ b/test-data/unit/fixtures/list.pyi @@ -26,9 +26,12 @@ class list(Sequence[T]): class tuple(Generic[T]): pass class function: pass -class int: pass +class int: + def __eq__(self, other: object) -> bool: pass + def __gt__(self, other: int) -> bool: pass class float: pass -class str: pass +class str: + def __eq__(self, other: object) -> bool: pass class bool(int): pass property = object() # Dummy definition. diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test index bcd30c20f158..ad3961acdc0c 100644 --- a/test-data/unit/pythoneval.test +++ b/test-data/unit/pythoneval.test @@ -1340,3 +1340,31 @@ x = X(a=1, b='s') [out] _testNamedTupleNew.py:12: error: Revealed type is 'Tuple[builtins.int, fallback=_testNamedTupleNew.Child]' + +[case testWideOuterContextFullStubs] +import re +from typing import Text, Iterable + +def get_words(text: Text) -> Iterable[Text]: + return (word.lower() for word in filter(None, re.split(r'\W+', text))) + +assert list(get_words("Ham, Spam, Eggs!")) == ['ham', 'spam', 'eggs'] +[out] + +[case testWideOuterContextFullStubs2] +from typing import Optional +from tempfile import mkstemp + +def foo(path: Optional[str]): + if path is None: + f, path = mkstemp('f') +[out] + +[case testWideOuterContextFullStubs3] +def foo(a: str): + pass + + +d = {'key': 'a'} +foo(d.get('key', None) or '26') +[out] From a26269231c14d47db3dc02ec13f87a518cc13030 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 29 Sep 2018 23:47:51 +0100 Subject: [PATCH 02/12] Tweak tests --- test-data/unit/check-inference-context.test | 7 +++++++ test-data/unit/pythoneval.test | 9 --------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/test-data/unit/check-inference-context.test b/test-data/unit/check-inference-context.test index caac1ea6e5a2..847fa787687d 100644 --- a/test-data/unit/check-inference-context.test +++ b/test-data/unit/check-inference-context.test @@ -922,6 +922,7 @@ class C: self.x = int() [case testWideOuterContext] +# flags: --strict-optional from typing import Any, Mapping, Dict, TypeVar T = TypeVar('T', bound=Dict[str, Any]) @@ -938,6 +939,7 @@ outer(f(d)) [out] [case testWideOuterContext2] +# flags: --strict-optional from typing import Optional, Type, TypeVar class A: pass @@ -958,6 +960,7 @@ class Experimental: [out] [case testWideOuterContext3] +# flags: --strict-optional from typing import Callable from typing import Iterable from typing import List @@ -986,6 +989,7 @@ def g(l: List[C], x: str) -> Optional[C]: [out] [case testWideOuterContext4] +# flags: --strict-optional from typing import Callable from typing import Iterable from typing import List @@ -1017,6 +1021,7 @@ def g(l: List[C], x: str) -> Optional[C]: [out] [case testWideOuterContext5] +# flags: --strict-optional from typing import Callable from typing import Iterable from typing import Optional @@ -1042,6 +1047,7 @@ def test() -> Optional[int]: [out] [case testWideOuterContext6] +# flags: --strict-optional from typing import Optional, Type, TypeVar class Custom: @@ -1057,6 +1063,7 @@ def b(cls: Type[T]) -> Optional[T]: [out] [case testWideOuterContext7] +# flags: --strict-optional from typing import TypeVar, List class A: diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test index ad3961acdc0c..7c038f3e3c3b 100644 --- a/test-data/unit/pythoneval.test +++ b/test-data/unit/pythoneval.test @@ -1359,12 +1359,3 @@ def foo(path: Optional[str]): if path is None: f, path = mkstemp('f') [out] - -[case testWideOuterContextFullStubs3] -def foo(a: str): - pass - - -d = {'key': 'a'} -foo(d.get('key', None) or '26') -[out] From fb841b7d97ec2134c6848a99d346cd22b765188d Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 30 Sep 2018 00:06:38 +0100 Subject: [PATCH 03/12] Remove some tests --- test-data/unit/check-inference-context.test | 87 --------------------- test-data/unit/pythoneval.test | 19 ----- 2 files changed, 106 deletions(-) diff --git a/test-data/unit/check-inference-context.test b/test-data/unit/check-inference-context.test index 847fa787687d..f088c38013ae 100644 --- a/test-data/unit/check-inference-context.test +++ b/test-data/unit/check-inference-context.test @@ -959,93 +959,6 @@ class Experimental: [builtins fixtures/bool.pyi] [out] -[case testWideOuterContext3] -# flags: --strict-optional -from typing import Callable -from typing import Iterable -from typing import List -from typing import Optional -from typing import TypeVar - - -class C: - x: str - - -T = TypeVar('T') - - -def f(i: Iterable[T], c: Callable[[T], bool]) -> Optional[T]: - for x in i: - if c(x): - return x - else: - return None - - -def g(l: List[C], x: str) -> Optional[C]: - return f(l, lambda d: d.x == x) -[builtins fixtures/list.pyi] -[out] - -[case testWideOuterContext4] -# flags: --strict-optional -from typing import Callable -from typing import Iterable -from typing import List -from typing import Optional -from typing import TypeVar - - -class C: - x: str - - -T = TypeVar('T') - - -def f(i: Iterable[T], c: Callable[[T], bool]) -> Optional[T]: - for x in i: - if c(x): - return x - else: - return None - - -def g(l: List[C], x: str) -> Optional[C]: - def pred(d: C) -> bool: - return d.x == x - - return f(l, pred) -[builtins fixtures/list.pyi] -[out] - -[case testWideOuterContext5] -# flags: --strict-optional -from typing import Callable -from typing import Iterable -from typing import Optional -from typing import TypeVar - -E = TypeVar('E') - - -def first( - predicate: Callable[['E'], bool], - iterable: Iterable['E'], -) -> Optional['E']: - for element in iterable: - if predicate(element): - return element - return None - - -def test() -> Optional[int]: - l = [0, 1, 2, 3] - return first(lambda x: x > 1, l) -[builtins fixtures/list.pyi] -[out] - [case testWideOuterContext6] # flags: --strict-optional from typing import Optional, Type, TypeVar diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test index 7c038f3e3c3b..bcd30c20f158 100644 --- a/test-data/unit/pythoneval.test +++ b/test-data/unit/pythoneval.test @@ -1340,22 +1340,3 @@ x = X(a=1, b='s') [out] _testNamedTupleNew.py:12: error: Revealed type is 'Tuple[builtins.int, fallback=_testNamedTupleNew.Child]' - -[case testWideOuterContextFullStubs] -import re -from typing import Text, Iterable - -def get_words(text: Text) -> Iterable[Text]: - return (word.lower() for word in filter(None, re.split(r'\W+', text))) - -assert list(get_words("Ham, Spam, Eggs!")) == ['ham', 'spam', 'eggs'] -[out] - -[case testWideOuterContextFullStubs2] -from typing import Optional -from tempfile import mkstemp - -def foo(path: Optional[str]): - if path is None: - f, path = mkstemp('f') -[out] From d941c23fd1d4487589e33d2fe5b29f59dd80eade Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 30 Sep 2018 00:46:11 +0100 Subject: [PATCH 04/12] Update a comment in skipped test and remove now unneded fixture updates --- test-data/unit/check-overloading.test | 4 +++- test-data/unit/fixtures/list.pyi | 7 ++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/test-data/unit/check-overloading.test b/test-data/unit/check-overloading.test index 425b14c7adaa..de60b0ca097c 100644 --- a/test-data/unit/check-overloading.test +++ b/test-data/unit/check-overloading.test @@ -4923,7 +4923,9 @@ reveal_type(f(g())) # E: Revealed type is 'builtins.list[builtins.int]' [builtins fixtures/list.pyi] [case testOverloadInferringArgumentsUsingContext2-skip] -# Overloads never use outer context for overload selection +# TODO: Overloads only use outer context to infer type variables in a given ovelroad variant, +# but never use outer context to _choose_ a better overload in ambiguous situations +# like empty containers or multiple inheritance, instead just always choosing the first one. from typing import Optional, List, overload, TypeVar T = TypeVar('T') diff --git a/test-data/unit/fixtures/list.pyi b/test-data/unit/fixtures/list.pyi index afef7c1aeb39..a80b1475bdcd 100644 --- a/test-data/unit/fixtures/list.pyi +++ b/test-data/unit/fixtures/list.pyi @@ -26,12 +26,9 @@ class list(Sequence[T]): class tuple(Generic[T]): pass class function: pass -class int: - def __eq__(self, other: object) -> bool: pass - def __gt__(self, other: int) -> bool: pass +class int: pass class float: pass -class str: - def __eq__(self, other: object) -> bool: pass +class str: pass class bool(int): pass property = object() # Dummy definition. From e18a52109e0023fa004bc1f66f70c717f39ab253 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 30 Sep 2018 02:38:23 +0100 Subject: [PATCH 05/12] Add special case for optional return vs optional context --- mypy/applytype.py | 10 +- mypy/checkexpr.py | 31 +++- test-data/unit/check-inference-context.test | 171 +++++++++++++++----- 3 files changed, 166 insertions(+), 46 deletions(-) diff --git a/mypy/applytype.py b/mypy/applytype.py index 627438835181..cf1169cedd89 100644 --- a/mypy/applytype.py +++ b/mypy/applytype.py @@ -9,13 +9,17 @@ def apply_generic_arguments(callable: CallableType, orig_types: Sequence[Optional[Type]], - msg: MessageBuilder, context: Context, silent: bool = False) -> CallableType: + msg: MessageBuilder, context: Context, + only_allowed: bool = False) -> CallableType: """Apply generic type arguments to a callable type. For example, applying [int] to 'def [T] (T) -> T' results in 'def (int) -> int'. Note that each type can be None; in this case, it will not be applied. + + If `only_allowed` is True, only apply those types that satisfy type variable + bound or constraints (and replace the type with `None`), instead of giving an error. """ tvars = callable.variables assert len(tvars) == len(orig_types) @@ -46,13 +50,13 @@ def apply_generic_arguments(callable: CallableType, orig_types: Sequence[Optiona best = match types[i] = best else: - if silent: + if only_allowed: types[i] = None else: msg.incompatible_typevar_value(callable, type, callable.variables[i].name, context) upper_bound = callable.variables[i].upper_bound if type and not mypy.subtypes.is_subtype(type, upper_bound): - if silent: + if only_allowed: types[i] = None else: msg.incompatible_typevar_value(callable, type, callable.variables[i].name, context) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 7ccb8cdffce2..119dc02d8044 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -837,6 +837,26 @@ def infer_function_type_arguments_using_context( # # See also github issues #462 and #360. ret_type = NoneTyp() + if mypy.checker.is_optional(ret_type) and mypy.checker.is_optional(ctx): + # Another special case: + # If both the context and the return type are optional, unwrap the optional, + # since in 99% cases this is what a user expects. In other words, we replace + # Optional[T] <: Optional[int] + # with + # T <: int + # while the former would infer T <: Optional[int]. + ret_type = mypy.checker.remove_optional(ret_type) + erased_ctx = mypy.checker.remove_optional(erased_ctx) + # + # TODO: Instead of this hack and the one above, we need to use outer and + # inner contexts at the same time. This is however not easy because of two + # reasons: + # * We need to support constraints like [1 <: 2, 2 <: X], i.e. with variables + # on both sides. (This is not too hard.) + # * We need to update all the inference "infrastructure", so that all + # variables in an expression are inferred at the same time. + # (And this is hard, also we need to be careful with lambdas that require + # two passes.) args = infer_type_arguments(callable.type_var_ids(), ret_type, erased_ctx) # Only substitute non-Uninhabited and non-erased types. new_args = [] # type: List[Optional[Type]] @@ -845,8 +865,10 @@ def infer_function_type_arguments_using_context( new_args.append(None) else: new_args.append(arg) - # Don't show errors after only using outer context. - return self.apply_generic_arguments(callable, new_args, error_context, silent=True) + # Don't show errors after we have only used the outer context for inference. + # We will use argument context to infer more variables. + return self.apply_generic_arguments(callable, new_args, error_context, + only_allowed=True) def infer_function_type_arguments(self, callee_type: CallableType, args: List[Expression], @@ -1614,9 +1636,10 @@ def check_arg(caller_type: Type, original_caller_type: Type, caller_kind: int, return False def apply_generic_arguments(self, callable: CallableType, types: Sequence[Optional[Type]], - context: Context, silent: bool = False) -> CallableType: + context: Context, only_allowed: bool = False) -> CallableType: """Simple wrapper around mypy.applytype.apply_generic_arguments.""" - return applytype.apply_generic_arguments(callable, types, self.msg, context, silent=silent) + return applytype.apply_generic_arguments(callable, types, self.msg, context, + only_allowed=only_allowed) def visit_member_expr(self, e: MemberExpr, is_lvalue: bool = False) -> Type: """Visit member expression (of form e.id).""" diff --git a/test-data/unit/check-inference-context.test b/test-data/unit/check-inference-context.test index f088c38013ae..ad2b69b2a645 100644 --- a/test-data/unit/check-inference-context.test +++ b/test-data/unit/check-inference-context.test @@ -921,45 +921,95 @@ class C: g: Callable[[], int] = lambda: 1 or self.x self.x = int() -[case testWideOuterContext] -# flags: --strict-optional -from typing import Any, Mapping, Dict, TypeVar +[case testWideOuterContextSubClassBound] +from typing import TypeVar -T = TypeVar('T', bound=Dict[str, Any]) +class A: ... +class B(A): ... -def f(arg: T) -> T: - ... -def outer(x: Mapping[str, Any]) -> None: - ... -d: Dict[str, Any] +T = TypeVar('T', bound=B) +def f(x: T) -> T: ... +def outer(x: A) -> None: ... -m: Mapping[str, Any] = f(d) -outer(f(d)) -[builtins fixtures/dict.pyi] +outer(f(B())) +x: A = f(B()) [out] -[case testWideOuterContext2] -# flags: --strict-optional -from typing import Optional, Type, TypeVar +[case testWideOuterContextSubClassValues] +from typing import TypeVar -class A: pass -class B: pass +class A: ... +class B(A): ... + +T = TypeVar('T', B, int) +def f(x: T) -> T: ... +def outer(x: A) -> None: ... + +outer(f(B())) +x: A = f(B()) +[out] + +[case testWideOuterContextSubclassBoundGeneric] +from typing import TypeVar, Generic + +S = TypeVar('S') +class A(Generic[S]): ... +class B(A[S]): ... + +T = TypeVar('T', bound=B[int]) +def f(x: T) -> T: ... +def outer(x: A[int]) -> None: ... + +y: B[int] +outer(f(y)) +x: A[int] = f(y) +[out] + +[case testWideOuterContextSubclassValuesGeneric] +from typing import TypeVar, Generic + +S = TypeVar('S') +class A(Generic[S]): ... +class B(A[S]): ... + +T = TypeVar('T', B[int], int) +def f(x: T) -> T: ... +def outer(x: A[int]) -> None: ... + +y: B[int] +outer(f(y)) +x: A[int] = f(y) +[out] + +[case testWideOuterContextUnionBound] +from typing import TypeVar, Union + +class A: ... +class B: ... + +T = TypeVar('T', bound=B) +def f(x: T) -> T: ... +def outer(x: Union[A, B]) -> None: ... + +outer(f(B())) +x: Union[A, B] = f(B()) +[out] + +[case testWideOuterContextUnionValues] +from typing import TypeVar, Union -class Experimental: - T = TypeVar('T', 'A', 'B') +class A: ... +class B: ... - def create_component_a(self) -> Optional[A]: - return self.create_component(A) +T = TypeVar('T', B, int) +def f(x: T) -> T: ... +def outer(x: Union[A, B]) -> None: ... - def create_component(self, cls: Type[T]) -> Optional[T]: - result = cls() - if bool(): - return result - return None -[builtins fixtures/bool.pyi] +outer(f(B())) +x: Union[A, B] = f(B()) [out] -[case testWideOuterContext6] +[case testWideOuterContextOptional] # flags: --strict-optional from typing import Optional, Type, TypeVar @@ -968,15 +1018,28 @@ class Custom: T = TypeVar('T', bound=Custom) -def a(cls: Type[T]) -> Optional[T]: - return cls() +def a(x: T) -> Optional[T]: ... -def b(cls: Type[T]) -> Optional[T]: - return a(cls) +def b(x: T) -> Optional[T]: + return a(x) [out] -[case testWideOuterContext7] +[case testWideOuterContextOptionalMethod] # flags: --strict-optional +from typing import Optional, Type, TypeVar + +class A: pass +class B: pass + +T = TypeVar('T', A, B) +class C: + def meth_a(self) -> Optional[A]: + return self.meth(A) + + def meth(self, cls: Type[T]) -> Optional[T]: ... +[out] + +[case testWideOuterContextValuesOverlapping] from typing import TypeVar, List class A: @@ -986,14 +1049,44 @@ class B(A): class C: pass -LooseType = TypeVar('LooseType', A, B, C) -def foo(xs: List[LooseType]) -> LooseType: - return xs[0] - +T = TypeVar('T', A, B, C) +def foo(xs: List[T]) -> T: ... -StrictType = TypeVar('StrictType', B, C) -def bar(xs: List[StrictType]) -> StrictType: +S = TypeVar('S', B, C) +def bar(xs: List[S]) -> S: foo(xs) return xs[0] [builtins fixtures/list.pyi] [out] + +[case testWideOuterContextOptionalTypeVarReturn] +# flags: --strict-optional +from typing import Callable, Iterable, List, Optional, TypeVar + +class C: + x: str + +T = TypeVar('T') +def f(i: Iterable[T], c: Callable[[T], str]) -> Optional[T]: ... + +def g(l: List[C], x: str) -> Optional[C]: + def pred(c: C) -> str: + return c.x + return f(l, pred) +[builtins fixtures/list.pyi] +[out] + +[case testWideOuterContextOptionalTypeVarReturnLambda] +# flags: --strict-optional +from typing import Callable, Iterable, List, Optional, TypeVar + +class C: + x: str + +T = TypeVar('T') +def f(i: Iterable[T], c: Callable[[T], str]) -> Optional[T]: ... + +def g(l: List[C], x: str) -> Optional[C]: + return f(l, lambda c: reveal_type(c).x) # E: Revealed type is '__main__.C' +[builtins fixtures/list.pyi] +[out] From 517d7c2eda2577fc9129886beada6ef824e0c0c9 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 30 Sep 2018 02:53:20 +0100 Subject: [PATCH 06/12] Re-order special cases --- mypy/checkexpr.py | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 119dc02d8044..d867605f93fd 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -823,22 +823,7 @@ def infer_function_type_arguments_using_context( # valid results. erased_ctx = replace_meta_vars(ctx, ErasedType()) ret_type = callable.ret_type - if isinstance(ret_type, TypeVarType): - if ret_type.values or (not isinstance(ctx, Instance) or - not ctx.args): - # The return type is a type variable. If it has values, we can't easily restrict - # type inference to conform to the valid values. If it's unrestricted, we could - # infer a too general type for the type variable if we use context, and this could - # result in confusing and spurious type errors elsewhere. - # - # Give up and just use function arguments for type inference. As an exception, - # if the context is a generic instance type, actually use it as context, as - # this *seems* to usually be the reasonable thing to do. - # - # See also github issues #462 and #360. - ret_type = NoneTyp() if mypy.checker.is_optional(ret_type) and mypy.checker.is_optional(ctx): - # Another special case: # If both the context and the return type are optional, unwrap the optional, # since in 99% cases this is what a user expects. In other words, we replace # Optional[T] <: Optional[int] @@ -848,7 +833,7 @@ def infer_function_type_arguments_using_context( ret_type = mypy.checker.remove_optional(ret_type) erased_ctx = mypy.checker.remove_optional(erased_ctx) # - # TODO: Instead of this hack and the one above, we need to use outer and + # TODO: Instead of this hack and the one below, we need to use outer and # inner contexts at the same time. This is however not easy because of two # reasons: # * We need to support constraints like [1 <: 2, 2 <: X], i.e. with variables @@ -857,6 +842,17 @@ def infer_function_type_arguments_using_context( # variables in an expression are inferred at the same time. # (And this is hard, also we need to be careful with lambdas that require # two passes.) + if isinstance(ret_type, TypeVarType) and not (isinstance(ctx, Instance) and ctx.args): + # Another special case: the return type is a type variable. If it's unrestricted, + # we could infer a too general type for the type variable if we use context, + # and this could result in confusing and spurious type errors elsewhere. + # + # Give up and just use function arguments for type inference. As an exception, + # if the context is a generic instance type, actually use it as context, as + # this *seems* to usually be the reasonable thing to do. + # + # See also github issues #462 and #360. + ret_type = NoneTyp() args = infer_type_arguments(callable.type_var_ids(), ret_type, erased_ctx) # Only substitute non-Uninhabited and non-erased types. new_args = [] # type: List[Optional[Type]] From fc603f4f1e21ef91f33470033fdd35e97c0e63f2 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 30 Sep 2018 22:47:33 +0100 Subject: [PATCH 07/12] Fix long line --- mypy/applytype.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/applytype.py b/mypy/applytype.py index cf1169cedd89..6f822443ddd7 100644 --- a/mypy/applytype.py +++ b/mypy/applytype.py @@ -53,7 +53,8 @@ def apply_generic_arguments(callable: CallableType, orig_types: Sequence[Optiona if only_allowed: types[i] = None else: - msg.incompatible_typevar_value(callable, type, callable.variables[i].name, context) + msg.incompatible_typevar_value(callable, type, callable.variables[i].name, + context) upper_bound = callable.variables[i].upper_bound if type and not mypy.subtypes.is_subtype(type, upper_bound): if only_allowed: From 826806c2e5726d1a65c1fd86b5f43381fa536fec Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 1 Oct 2018 00:42:41 +0100 Subject: [PATCH 08/12] Updates on feedback; will add tests later --- mypy/applytype.py | 24 ++++++++++++++---------- mypy/checkexpr.py | 8 ++++---- test-data/unit/check-overloading.test | 2 +- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/mypy/applytype.py b/mypy/applytype.py index 6f822443ddd7..7e7a800d0452 100644 --- a/mypy/applytype.py +++ b/mypy/applytype.py @@ -10,7 +10,7 @@ def apply_generic_arguments(callable: CallableType, orig_types: Sequence[Optional[Type]], msg: MessageBuilder, context: Context, - only_allowed: bool = False) -> CallableType: + skip_unsatisfied: bool = False) -> CallableType: """Apply generic type arguments to a callable type. For example, applying [int] to 'def [T] (T) -> T' results in @@ -18,7 +18,7 @@ def apply_generic_arguments(callable: CallableType, orig_types: Sequence[Optiona Note that each type can be None; in this case, it will not be applied. - If `only_allowed` is True, only apply those types that satisfy type variable + If `skip_unsatisfied` is True, only apply those types that satisfy type variable bound or constraints (and replace the type with `None`), instead of giving an error. """ tvars = callable.variables @@ -29,7 +29,9 @@ def apply_generic_arguments(callable: CallableType, orig_types: Sequence[Optiona for i, type in enumerate(types): assert not isinstance(type, PartialType), "Internal error: must never apply partial type" values = callable.variables[i].values - if values and type: + if type is None: + continue + if values: if isinstance(type, AnyType): continue if isinstance(type, TypeVarType) and type.values: @@ -50,17 +52,19 @@ def apply_generic_arguments(callable: CallableType, orig_types: Sequence[Optiona best = match types[i] = best else: - if only_allowed: + if skip_unsatisfied: + types[i] = None + else: + msg.incompatible_typevar_value(callable, type, callable.variables[i].name, + context) + else: + upper_bound = callable.variables[i].upper_bound + if not mypy.subtypes.is_subtype(type, upper_bound): + if skip_unsatisfied: types[i] = None else: msg.incompatible_typevar_value(callable, type, callable.variables[i].name, context) - upper_bound = callable.variables[i].upper_bound - if type and not mypy.subtypes.is_subtype(type, upper_bound): - if only_allowed: - types[i] = None - else: - msg.incompatible_typevar_value(callable, type, callable.variables[i].name, context) # Create a map from type variable id to target type. id_to_type = {} # type: Dict[TypeVarId, Type] diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index d867605f93fd..8e0c4797ba02 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -852,7 +852,7 @@ def infer_function_type_arguments_using_context( # this *seems* to usually be the reasonable thing to do. # # See also github issues #462 and #360. - ret_type = NoneTyp() + return callable.copy_modified() args = infer_type_arguments(callable.type_var_ids(), ret_type, erased_ctx) # Only substitute non-Uninhabited and non-erased types. new_args = [] # type: List[Optional[Type]] @@ -864,7 +864,7 @@ def infer_function_type_arguments_using_context( # Don't show errors after we have only used the outer context for inference. # We will use argument context to infer more variables. return self.apply_generic_arguments(callable, new_args, error_context, - only_allowed=True) + skip_unsatisfied=True) def infer_function_type_arguments(self, callee_type: CallableType, args: List[Expression], @@ -1632,10 +1632,10 @@ def check_arg(caller_type: Type, original_caller_type: Type, caller_kind: int, return False def apply_generic_arguments(self, callable: CallableType, types: Sequence[Optional[Type]], - context: Context, only_allowed: bool = False) -> CallableType: + context: Context, skip_unsatisfied: bool = False) -> CallableType: """Simple wrapper around mypy.applytype.apply_generic_arguments.""" return applytype.apply_generic_arguments(callable, types, self.msg, context, - only_allowed=only_allowed) + skip_unsatisfied=skip_unsatisfied) def visit_member_expr(self, e: MemberExpr, is_lvalue: bool = False) -> Type: """Visit member expression (of form e.id).""" diff --git a/test-data/unit/check-overloading.test b/test-data/unit/check-overloading.test index de60b0ca097c..fb5b442b88be 100644 --- a/test-data/unit/check-overloading.test +++ b/test-data/unit/check-overloading.test @@ -4923,7 +4923,7 @@ reveal_type(f(g())) # E: Revealed type is 'builtins.list[builtins.int]' [builtins fixtures/list.pyi] [case testOverloadInferringArgumentsUsingContext2-skip] -# TODO: Overloads only use outer context to infer type variables in a given ovelroad variant, +# TODO: Overloads only use outer context to infer type variables in a given overload variant, # but never use outer context to _choose_ a better overload in ambiguous situations # like empty containers or multiple inheritance, instead just always choosing the first one. From 17146f709ba3c98531a2999bc06e6ee1a9642691 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 1 Oct 2018 11:11:49 +0100 Subject: [PATCH 09/12] Add more tests --- test-data/unit/check-inference-context.test | 118 ++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/test-data/unit/check-inference-context.test b/test-data/unit/check-inference-context.test index ad2b69b2a645..db772ab615f6 100644 --- a/test-data/unit/check-inference-context.test +++ b/test-data/unit/check-inference-context.test @@ -935,6 +935,21 @@ outer(f(B())) x: A = f(B()) [out] +[case testWideOuterContextSubClassBoundGenericReturn] +from typing import TypeVar, Iterable, List + +class A: ... +class B(A): ... + +T = TypeVar('T', bound=B) +def f(x: T) -> List[T]: ... +def outer(x: Iterable[A]) -> None: ... + +outer(f(B())) +x: Iterable[A] = f(B()) +[builtins fixtures/list.pyi] +[out] + [case testWideOuterContextSubClassValues] from typing import TypeVar @@ -949,6 +964,21 @@ outer(f(B())) x: A = f(B()) [out] +[case testWideOuterContextSubClassValuesGenericReturn] +from typing import TypeVar, Iterable, List + +class A: ... +class B(A): ... + +T = TypeVar('T', B, int) +def f(x: T) -> List[T]: ... +def outer(x: Iterable[A]) -> None: ... + +outer(f(B())) +x: Iterable[A] = f(B()) +[builtins fixtures/list.pyi] +[out] + [case testWideOuterContextSubclassBoundGeneric] from typing import TypeVar, Generic @@ -995,6 +1025,21 @@ outer(f(B())) x: Union[A, B] = f(B()) [out] +[case testWideOuterContextUnionBoundGenericReturn] +from typing import TypeVar, Union, Iterable, List + +class A: ... +class B: ... + +T = TypeVar('T', bound=B) +def f(x: T) -> List[T]: ... +def outer(x: Iterable[Union[A, B]]) -> None: ... + +outer(f(B())) +x: Iterable[Union[A, B]] = f(B()) +[builtins fixtures/list.pyi] +[out] + [case testWideOuterContextUnionValues] from typing import TypeVar, Union @@ -1009,6 +1054,21 @@ outer(f(B())) x: Union[A, B] = f(B()) [out] +[case testWideOuterContextUnionValuesGenericReturn] +from typing import TypeVar, Union, Iterable, List + +class A: ... +class B: ... + +T = TypeVar('T', B, int) +def f(x: T) -> List[T]: ... +def outer(x: Iterable[Union[A, B]]) -> None: ... + +outer(f(B())) +x: Iterable[Union[A, B]] = f(B()) +[builtins fixtures/list.pyi] +[out] + [case testWideOuterContextOptional] # flags: --strict-optional from typing import Optional, Type, TypeVar @@ -1024,6 +1084,21 @@ def b(x: T) -> Optional[T]: return a(x) [out] +[case testWideOuterContextOptionalGenericReturn] +# flags: --strict-optional +from typing import Optional, Type, TypeVar, Iterable + +class Custom: + pass + +T = TypeVar('T', bound=Custom) + +def a(x: T) -> Iterable[Optional[T]]: ... + +def b(x: T) -> Iterable[Optional[T]]: + return a(x) +[out] + [case testWideOuterContextOptionalMethod] # flags: --strict-optional from typing import Optional, Type, TypeVar @@ -1090,3 +1165,46 @@ def g(l: List[C], x: str) -> Optional[C]: return f(l, lambda c: reveal_type(c).x) # E: Revealed type is '__main__.C' [builtins fixtures/list.pyi] [out] + +[case testWideOuterContextEmpty] +from typing import List, TypeVar + +T = TypeVar('T', bound=int) +def f(x: List[T]) -> T: ... + +# mypy infers List[] here, and is a subtype of str +y: str = f([]) +[builtins fixtures/list.pyi] +[out] + +[case testWideOuterContextEmptyError] +from typing import List, TypeVar + +T = TypeVar('T', bound=int) +def f(x: List[T]) -> List[T]: ... + +# TODO: improve error message for such cases, see # 3283 +y: List[str] = f([]) # E: Incompatible types in assignment (expression has type "List[]", variable has type "List[str]") +[builtins fixtures/list.pyi] +[out] + +[case testWideOuterContextNoArgs] +# flags: --strict-optional +from typing import TypeVar, Optional + +T = TypeVar('T', bound=int) +def f(x: Optional[T] = None) -> T: ... + +y: str = f() +[out] + +[case testWideOuterContextNoArgsError] +# flags: --strict-optional +from typing import TypeVar, Optional, List + +T = TypeVar('T', bound=int) +def f(x: Optional[T] = None) -> List[T]: ... + +y: List[str] = f() # E: Incompatible types in assignment (expression has type "List[]", variable has type "List[str]") +[builtins fixtures/list.pyi] +[out] From 9a0af3c254526a87ce60fac901e8ccc1e72bc6a1 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 1 Oct 2018 16:19:21 +0100 Subject: [PATCH 10/12] Address CR --- mypy/applytype.py | 4 +- mypy/checker.py | 14 +----- mypy/checkexpr.py | 12 ++--- mypy/types.py | 11 +++++ test-data/unit/check-inference-context.test | 52 ++++++++++++--------- 5 files changed, 52 insertions(+), 41 deletions(-) diff --git a/mypy/applytype.py b/mypy/applytype.py index 7e7a800d0452..afd963928eee 100644 --- a/mypy/applytype.py +++ b/mypy/applytype.py @@ -18,8 +18,8 @@ def apply_generic_arguments(callable: CallableType, orig_types: Sequence[Optiona Note that each type can be None; in this case, it will not be applied. - If `skip_unsatisfied` is True, only apply those types that satisfy type variable - bound or constraints (and replace the type with `None`), instead of giving an error. + If `skip_unsatisfied` is True, then just skip the types that don't satisfy type variable + bound or constraints, instead of giving an error. """ tvars = callable.variables assert len(tvars) == len(orig_types) diff --git a/mypy/checker.py b/mypy/checker.py index 5efdd8221827..d9fb20aa3a00 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -31,7 +31,8 @@ Type, AnyType, CallableType, FunctionLike, Overloaded, TupleType, TypedDictType, Instance, NoneTyp, strip_type, TypeType, TypeOfAny, UnionType, TypeVarId, TypeVarType, PartialType, DeletedType, UninhabitedType, TypeVarDef, - true_only, false_only, function_type, is_named_instance, union_items, TypeQuery + true_only, false_only, function_type, is_named_instance, union_items, TypeQuery, + is_optional, remove_optional ) from mypy.sametypes import is_same_type, is_same_types from mypy.messages import MessageBuilder, make_inferred_type_note @@ -3792,17 +3793,6 @@ def is_literal_none(n: Expression) -> bool: return isinstance(n, NameExpr) and n.fullname == 'builtins.None' -def is_optional(t: Type) -> bool: - return isinstance(t, UnionType) and any(isinstance(e, NoneTyp) for e in t.items) - - -def remove_optional(typ: Type) -> Type: - if isinstance(typ, UnionType): - return UnionType.make_union([t for t in typ.items if not isinstance(t, NoneTyp)]) - else: - return typ - - def is_literal_not_implemented(n: Expression) -> bool: return isinstance(n, NameExpr) and n.fullname == 'builtins.NotImplemented' diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 8e0c4797ba02..7074c78cdb97 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -18,9 +18,9 @@ from mypy.types import ( Type, AnyType, CallableType, Overloaded, NoneTyp, TypeVarDef, TupleType, TypedDictType, Instance, TypeVarType, ErasedType, UnionType, - PartialType, DeletedType, UnboundType, UninhabitedType, TypeType, TypeOfAny, + PartialType, DeletedType, UninhabitedType, TypeType, TypeOfAny, true_only, false_only, is_named_instance, function_type, callable_type, FunctionLike, - get_typ_args, StarType + StarType, is_optional, remove_optional ) from mypy.nodes import ( NameExpr, RefExpr, Var, FuncDef, OverloadedFuncDef, TypeInfo, CallExpr, @@ -31,7 +31,7 @@ ConditionalExpr, ComparisonExpr, TempNode, SetComprehension, DictionaryComprehension, ComplexExpr, EllipsisExpr, StarExpr, AwaitExpr, YieldExpr, YieldFromExpr, TypedDictExpr, PromoteExpr, NewTypeExpr, NamedTupleExpr, TypeVarExpr, - TypeAliasExpr, BackquoteExpr, EnumCallExpr, TypeAlias, ClassDef, Block, SymbolNode, + TypeAliasExpr, BackquoteExpr, EnumCallExpr, TypeAlias, SymbolNode, ARG_POS, ARG_OPT, ARG_NAMED, ARG_STAR, ARG_STAR2, MODULE_REF, LITERAL_TYPE, REVEAL_TYPE ) from mypy.literals import literal @@ -823,15 +823,15 @@ def infer_function_type_arguments_using_context( # valid results. erased_ctx = replace_meta_vars(ctx, ErasedType()) ret_type = callable.ret_type - if mypy.checker.is_optional(ret_type) and mypy.checker.is_optional(ctx): + if is_optional(ret_type) and is_optional(ctx): # If both the context and the return type are optional, unwrap the optional, # since in 99% cases this is what a user expects. In other words, we replace # Optional[T] <: Optional[int] # with # T <: int # while the former would infer T <: Optional[int]. - ret_type = mypy.checker.remove_optional(ret_type) - erased_ctx = mypy.checker.remove_optional(erased_ctx) + ret_type = remove_optional(ret_type) + erased_ctx = remove_optional(erased_ctx) # # TODO: Instead of this hack and the one below, we need to use outer and # inner contexts at the same time. This is however not easy because of two diff --git a/mypy/types.py b/mypy/types.py index acad55a12d75..baf0d543d679 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1918,6 +1918,17 @@ def union_items(typ: Type) -> List[Type]: return [typ] +def is_optional(t: Type) -> bool: + return isinstance(t, UnionType) and any(isinstance(e, NoneTyp) for e in t.items) + + +def remove_optional(typ: Type) -> Type: + if isinstance(typ, UnionType): + return UnionType.make_union([t for t in typ.items if not isinstance(t, NoneTyp)]) + else: + return typ + + names = globals().copy() # type: Final names.pop('NOT_READY', None) deserialize_map = { diff --git a/test-data/unit/check-inference-context.test b/test-data/unit/check-inference-context.test index db772ab615f6..6977b3e2c465 100644 --- a/test-data/unit/check-inference-context.test +++ b/test-data/unit/check-inference-context.test @@ -933,7 +933,6 @@ def outer(x: A) -> None: ... outer(f(B())) x: A = f(B()) -[out] [case testWideOuterContextSubClassBoundGenericReturn] from typing import TypeVar, Iterable, List @@ -948,7 +947,6 @@ def outer(x: Iterable[A]) -> None: ... outer(f(B())) x: Iterable[A] = f(B()) [builtins fixtures/list.pyi] -[out] [case testWideOuterContextSubClassValues] from typing import TypeVar @@ -962,7 +960,6 @@ def outer(x: A) -> None: ... outer(f(B())) x: A = f(B()) -[out] [case testWideOuterContextSubClassValuesGenericReturn] from typing import TypeVar, Iterable, List @@ -977,7 +974,6 @@ def outer(x: Iterable[A]) -> None: ... outer(f(B())) x: Iterable[A] = f(B()) [builtins fixtures/list.pyi] -[out] [case testWideOuterContextSubclassBoundGeneric] from typing import TypeVar, Generic @@ -993,7 +989,21 @@ def outer(x: A[int]) -> None: ... y: B[int] outer(f(y)) x: A[int] = f(y) -[out] + +[case testWideOuterContextSubclassBoundGenericCovariant] +from typing import TypeVar, Generic + +S_co = TypeVar('S_co', covariant=True) +class A(Generic[S_co]): ... +class B(A[S_co]): ... + +T = TypeVar('T', bound=B[int]) +def f(x: T) -> T: ... +def outer(x: A[int]) -> None: ... + +y: B[int] +outer(f(y)) +x: A[int] = f(y) [case testWideOuterContextSubclassValuesGeneric] from typing import TypeVar, Generic @@ -1009,7 +1019,21 @@ def outer(x: A[int]) -> None: ... y: B[int] outer(f(y)) x: A[int] = f(y) -[out] + +[case testWideOuterContextSubclassValuesGenericCovariant] +from typing import TypeVar, Generic + +S_co = TypeVar('S_co', covariant=True) +class A(Generic[S_co]): ... +class B(A[S_co]): ... + +T = TypeVar('T', B[int], int) +def f(x: T) -> T: ... +def outer(x: A[int]) -> None: ... + +y: B[int] +outer(f(y)) +x: A[int] = f(y) [case testWideOuterContextUnionBound] from typing import TypeVar, Union @@ -1023,7 +1047,6 @@ def outer(x: Union[A, B]) -> None: ... outer(f(B())) x: Union[A, B] = f(B()) -[out] [case testWideOuterContextUnionBoundGenericReturn] from typing import TypeVar, Union, Iterable, List @@ -1038,7 +1061,6 @@ def outer(x: Iterable[Union[A, B]]) -> None: ... outer(f(B())) x: Iterable[Union[A, B]] = f(B()) [builtins fixtures/list.pyi] -[out] [case testWideOuterContextUnionValues] from typing import TypeVar, Union @@ -1052,7 +1074,6 @@ def outer(x: Union[A, B]) -> None: ... outer(f(B())) x: Union[A, B] = f(B()) -[out] [case testWideOuterContextUnionValuesGenericReturn] from typing import TypeVar, Union, Iterable, List @@ -1067,7 +1088,6 @@ def outer(x: Iterable[Union[A, B]]) -> None: ... outer(f(B())) x: Iterable[Union[A, B]] = f(B()) [builtins fixtures/list.pyi] -[out] [case testWideOuterContextOptional] # flags: --strict-optional @@ -1082,7 +1102,6 @@ def a(x: T) -> Optional[T]: ... def b(x: T) -> Optional[T]: return a(x) -[out] [case testWideOuterContextOptionalGenericReturn] # flags: --strict-optional @@ -1097,7 +1116,6 @@ def a(x: T) -> Iterable[Optional[T]]: ... def b(x: T) -> Iterable[Optional[T]]: return a(x) -[out] [case testWideOuterContextOptionalMethod] # flags: --strict-optional @@ -1112,7 +1130,6 @@ class C: return self.meth(A) def meth(self, cls: Type[T]) -> Optional[T]: ... -[out] [case testWideOuterContextValuesOverlapping] from typing import TypeVar, List @@ -1132,7 +1149,6 @@ def bar(xs: List[S]) -> S: foo(xs) return xs[0] [builtins fixtures/list.pyi] -[out] [case testWideOuterContextOptionalTypeVarReturn] # flags: --strict-optional @@ -1149,7 +1165,6 @@ def g(l: List[C], x: str) -> Optional[C]: return c.x return f(l, pred) [builtins fixtures/list.pyi] -[out] [case testWideOuterContextOptionalTypeVarReturnLambda] # flags: --strict-optional @@ -1164,7 +1179,6 @@ def f(i: Iterable[T], c: Callable[[T], str]) -> Optional[T]: ... def g(l: List[C], x: str) -> Optional[C]: return f(l, lambda c: reveal_type(c).x) # E: Revealed type is '__main__.C' [builtins fixtures/list.pyi] -[out] [case testWideOuterContextEmpty] from typing import List, TypeVar @@ -1175,7 +1189,6 @@ def f(x: List[T]) -> T: ... # mypy infers List[] here, and is a subtype of str y: str = f([]) [builtins fixtures/list.pyi] -[out] [case testWideOuterContextEmptyError] from typing import List, TypeVar @@ -1183,10 +1196,9 @@ from typing import List, TypeVar T = TypeVar('T', bound=int) def f(x: List[T]) -> List[T]: ... -# TODO: improve error message for such cases, see # 3283 +# TODO: improve error message for such cases, see #3283 and #5706 y: List[str] = f([]) # E: Incompatible types in assignment (expression has type "List[]", variable has type "List[str]") [builtins fixtures/list.pyi] -[out] [case testWideOuterContextNoArgs] # flags: --strict-optional @@ -1196,7 +1208,6 @@ T = TypeVar('T', bound=int) def f(x: Optional[T] = None) -> T: ... y: str = f() -[out] [case testWideOuterContextNoArgsError] # flags: --strict-optional @@ -1207,4 +1218,3 @@ def f(x: Optional[T] = None) -> List[T]: ... y: List[str] = f() # E: Incompatible types in assignment (expression has type "List[]", variable has type "List[str]") [builtins fixtures/list.pyi] -[out] From f3a28c22243d6d24fdb2ec13872ed39b1caad730 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 1 Oct 2018 16:41:04 +0100 Subject: [PATCH 11/12] Fix broken merge --- mypy/checkexpr.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 59c06ce87d93..f0047db8a81b 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -21,8 +21,6 @@ PartialType, DeletedType, UninhabitedType, TypeType, TypeOfAny, true_only, false_only, is_named_instance, function_type, callable_type, FunctionLike, StarType, is_optional, remove_optional - PartialType, DeletedType, UninhabitedType, TypeType, TypeOfAny, true_only, - false_only, is_named_instance, function_type, callable_type, FunctionLike, StarType, ) from mypy.nodes import ( NameExpr, RefExpr, Var, FuncDef, OverloadedFuncDef, TypeInfo, CallExpr, From 3cd7d254d2d03391f0e8da6346e1cf1f589f9c09 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 1 Oct 2018 18:06:09 +0100 Subject: [PATCH 12/12] Switch to invariant instances in special case --- mypy/checkexpr.py | 6 +++--- mypy/types.py | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index f0047db8a81b..95a4ecdf2c1a 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -20,7 +20,7 @@ TupleType, TypedDictType, Instance, TypeVarType, ErasedType, UnionType, PartialType, DeletedType, UninhabitedType, TypeType, TypeOfAny, true_only, false_only, is_named_instance, function_type, callable_type, FunctionLike, - StarType, is_optional, remove_optional + StarType, is_optional, remove_optional, is_invariant_instance ) from mypy.nodes import ( NameExpr, RefExpr, Var, FuncDef, OverloadedFuncDef, TypeInfo, CallExpr, @@ -839,13 +839,13 @@ def infer_function_type_arguments_using_context( # variables in an expression are inferred at the same time. # (And this is hard, also we need to be careful with lambdas that require # two passes.) - if isinstance(ret_type, TypeVarType) and not (isinstance(ctx, Instance) and ctx.args): + if isinstance(ret_type, TypeVarType) and not is_invariant_instance(ctx): # Another special case: the return type is a type variable. If it's unrestricted, # we could infer a too general type for the type variable if we use context, # and this could result in confusing and spurious type errors elsewhere. # # Give up and just use function arguments for type inference. As an exception, - # if the context is a generic instance type, actually use it as context, as + # if the context is an invariant instance type, actually use it as context, as # this *seems* to usually be the reasonable thing to do. # # See also github issues #462 and #360. diff --git a/mypy/types.py b/mypy/types.py index baf0d543d679..9a84cc270e2a 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1918,6 +1918,12 @@ def union_items(typ: Type) -> List[Type]: return [typ] +def is_invariant_instance(tp: Type) -> bool: + if not isinstance(tp, Instance) or not tp.args: + return False + return any(v.variance == INVARIANT for v in tp.type.defn.type_vars) + + def is_optional(t: Type) -> bool: return isinstance(t, UnionType) and any(isinstance(e, NoneTyp) for e in t.items)