From 844b9f22d6f5f21f079323bb1740f97b041d27b1 Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Sat, 8 Dec 2018 17:47:48 -0800 Subject: [PATCH 1/2] Add tests for literals and generics This pull request adds some tests verifying that basic interactions between literals and generics work as expected. It also mildly tweaks checkexpr so it actually considers the *whole* type context instead of just part of it when deciding whether some literal expression ought to be a Literal[...] type or not. --- mypy/checkexpr.py | 13 +- test-data/unit/check-literal.test | 195 ++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+), 3 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index e1beea078ca9..da5bccc31fdd 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -210,7 +210,7 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type: def analyze_var_ref(self, var: Var, context: Context) -> Type: if var.type: - if is_literal_type_like(self.type_context[-1]) and var.name() in {'True', 'False'}: + if self.context_contains_literal_like_type() and var.name() in {'True', 'False'}: return LiteralType(var.name() == 'True', self.named_type('builtins.bool')) else: return var.type @@ -1747,14 +1747,14 @@ def analyze_external_member_access(self, member: str, base_type: Type, def visit_int_expr(self, e: IntExpr) -> Type: """Type check an integer literal (trivial).""" typ = self.named_type('builtins.int') - if is_literal_type_like(self.type_context[-1]): + if self.context_contains_literal_like_type(): return LiteralType(value=e.value, fallback=typ) return typ def visit_str_expr(self, e: StrExpr) -> Type: """Type check a string literal (trivial).""" typ = self.named_type('builtins.str') - if is_literal_type_like(self.type_context[-1]): + if self.context_contains_literal_like_type(): return LiteralType(value=e.value, fallback=typ) return typ @@ -3346,6 +3346,10 @@ def narrow_type_from_binder(self, expr: Expression, known_type: Type) -> Type: return ans return known_type + def context_contains_literal_like_type(self) -> bool: + """Returns 'true' if the context contains anything that resembles a LiteralType""" + return any(is_literal_type_like(item) for item in self.type_context) + def has_any_type(t: Type) -> bool: """Whether t contains an Any type""" @@ -3627,5 +3631,8 @@ def is_literal_type_like(t: Optional[Type]) -> bool: return True elif isinstance(t, UnionType): return any(is_literal_type_like(item) for item in t.items) + elif isinstance(t, TypeVarType): + return (is_literal_type_like(t.upper_bound) + or any(is_literal_type_like(item) for item in t.values)) else: return False diff --git a/test-data/unit/check-literal.test b/test-data/unit/check-literal.test index c7f35c94ec5e..3d4555934279 100644 --- a/test-data/unit/check-literal.test +++ b/test-data/unit/check-literal.test @@ -385,6 +385,18 @@ b: bt # E: Invalid type "__main__.bt" [builtins fixtures/set.pyi] [out] +[case testLiteralDisallowTypeVar] +from typing import TypeVar +from typing_extensions import Literal + +T = TypeVar('T') + +at = Literal[T] # E: Parameter 1 of Literal[...] is invalid +a: at + +def foo(b: Literal[T]) -> T: pass # E: Parameter 1 of Literal[...] is invalid +[out] + -- -- Test mixing and matching literals with other types @@ -1180,3 +1192,186 @@ b = b * a c = c.strip() # E: Incompatible types in assignment (expression has type "str", variable has type "Literal['foo']") [builtins fixtures/ops.pyi] [out] + + +-- +-- Test to make sure literals interact with generics as expected +-- + +[case testLiteralAndGenericsWithSimpleFunctions] +from typing import TypeVar +from typing_extensions import Literal + +T = TypeVar('T') +def foo(x: T) -> T: pass +def expects_literal(x: Literal[3]) -> None: pass +def expects_int(x: int) -> None: pass + +a: Literal[3] +reveal_type(foo(3)) # E: Revealed type is 'builtins.int*' +reveal_type(foo(a)) # E: Revealed type is 'Literal[3]' + +expects_literal(3) +expects_literal(foo(3)) +expects_literal(foo(foo(3))) + +expects_literal(a) +expects_literal(foo(a)) +expects_literal(foo(foo(a))) + +expects_literal(5) # E: Argument 1 to "expects_literal" has incompatible type "Literal[5]"; expected "Literal[3]" +expects_literal(foo(5)) # E: Argument 1 to "expects_literal" has incompatible type "Literal[5]"; expected "Literal[3]" +expects_literal(foo(foo(5))) # E: Argument 1 to "expects_literal" has incompatible type "Literal[5]"; expected "Literal[3]" + +expects_int(a) +expects_int(foo(a)) +expects_int(foo(foo(a))) +[out] + +[case testLiteralAndGenericsWithSimpleClasses] +from typing import TypeVar, Generic +from typing_extensions import Literal + +T = TypeVar('T') +class Wrapper(Generic[T]): + def __init__(self, val: T) -> None: + self.val = val + def inner(self) -> T: + return self.val + +def expects_literal(a: Literal[3]) -> None: pass +def expects_literal_wrapper(x: Wrapper[Literal[3]]) -> None: pass + +a: Literal[3] +reveal_type(Wrapper(3)) # E: Revealed type is '__main__.Wrapper[builtins.int*]' +reveal_type(Wrapper[Literal[3]](3)) # E: Revealed type is '__main__.Wrapper[Literal[3]]' +reveal_type(Wrapper(a)) # E: Revealed type is '__main__.Wrapper[Literal[3]]' + +expects_literal(Wrapper(3).inner()) +expects_literal(Wrapper(a).inner()) + +expects_literal_wrapper(Wrapper(3)) +expects_literal_wrapper(Wrapper(a)) + +expects_literal(Wrapper(5).inner()) # E: Argument 1 to "expects_literal" has incompatible type "Literal[5]"; expected "Literal[3]" +expects_literal_wrapper(Wrapper(5)) # E: Argument 1 to "Wrapper" has incompatible type "Literal[5]"; expected "Literal[3]" +[out] + +[case testLiteralAndGenericsRespectsUpperBound] +from typing import TypeVar +from typing_extensions import Literal + +TLiteral = TypeVar('TLiteral', bound=Literal[3]) +TInt = TypeVar('TInt', bound=int) + +def func1(x: TLiteral) -> TLiteral: pass +def func2(x: TInt) -> TInt: pass + +def func3(x: TLiteral) -> TLiteral: + y = func2(x) + return y +def func4(x: TInt) -> TInt: + y = func1(x) # E: Value of type variable "TLiteral" of "func1" cannot be "TInt" + return y + +a: Literal[3] +b: Literal[4] +c: int + +reveal_type(func1) # E: Revealed type is 'def [TLiteral <: Literal[3]] (x: TLiteral`-1) -> TLiteral`-1' + +reveal_type(func1(3)) # E: Revealed type is 'Literal[3]' +reveal_type(func1(a)) # E: Revealed type is 'Literal[3]' +reveal_type(func1(4)) # E: Revealed type is 'Literal[4]' \ + # E: Value of type variable "TLiteral" of "func1" cannot be "Literal[4]" +reveal_type(func1(b)) # E: Revealed type is 'Literal[4]' \ + # E: Value of type variable "TLiteral" of "func1" cannot be "Literal[4]" +reveal_type(func1(c)) # E: Revealed type is 'builtins.int*' \ + # E: Value of type variable "TLiteral" of "func1" cannot be "int" + +reveal_type(func2(3)) # E: Revealed type is 'builtins.int*' +reveal_type(func2(a)) # E: Revealed type is 'Literal[3]' +reveal_type(func2(4)) # E: Revealed type is 'builtins.int*' +reveal_type(func2(b)) # E: Revealed type is 'Literal[4]' +reveal_type(func2(c)) # E: Revealed type is 'builtins.int*' +[out] + +[case testLiteralAndGenericsRespectsValueRestriction] +from typing import TypeVar +from typing_extensions import Literal + +TLiteral = TypeVar('TLiteral', Literal[3], Literal['foo']) +TNormal = TypeVar('TNormal', int, str) + +def func1(x: TLiteral) -> TLiteral: pass +def func2(x: TNormal) -> TNormal: pass + +def func3(x: TLiteral) -> TLiteral: + y = func2(x) + return y # E: Incompatible return value type (got "int", expected "Literal[3]") \ + # E: Incompatible return value type (got "str", expected "Literal['foo']") +def func4(x: TNormal) -> TNormal: + y = func1(x) # E: Value of type variable "TLiteral" of "func1" cannot be "int" \ + # E: Value of type variable "TLiteral" of "func1" cannot be "str" + return y + +i1: Literal[3] +i2: Literal[4] +i: int + +s1: Literal['foo'] +s2: Literal['bar'] +s: str + +reveal_type(func1) # E: Revealed type is 'def [TLiteral in (Literal[3], Literal['foo'])] (x: TLiteral`-1) -> TLiteral`-1' + +reveal_type(func1(3)) # E: Revealed type is 'Literal[3]' +reveal_type(func1(i1)) # E: Revealed type is 'Literal[3]' +reveal_type(func1(4)) # E: Revealed type is 'Literal[4]' \ + # E: Value of type variable "TLiteral" of "func1" cannot be "Literal[4]" +reveal_type(func1(i2)) # E: Revealed type is 'Literal[4]' \ + # E: Value of type variable "TLiteral" of "func1" cannot be "Literal[4]" +reveal_type(func1(i)) # E: Revealed type is 'builtins.int*' \ + # E: Value of type variable "TLiteral" of "func1" cannot be "int" + +reveal_type(func1("foo")) # E: Revealed type is 'Literal['foo']' +reveal_type(func1(s1)) # E: Revealed type is 'Literal['foo']' +reveal_type(func1("bar")) # E: Revealed type is 'Literal['bar']' \ + # E: Value of type variable "TLiteral" of "func1" cannot be "Literal['bar']" +reveal_type(func1(s2)) # E: Revealed type is 'Literal['bar']' \ + # E: Value of type variable "TLiteral" of "func1" cannot be "Literal['bar']" +reveal_type(func1(s)) # E: Revealed type is 'builtins.str*' \ + # E: Value of type variable "TLiteral" of "func1" cannot be "str" + +reveal_type(func2(3)) # E: Revealed type is 'builtins.int*' +reveal_type(func2(i1)) # E: Revealed type is 'builtins.int*' +reveal_type(func2(4)) # E: Revealed type is 'builtins.int*' +reveal_type(func2(i2)) # E: Revealed type is 'builtins.int*' +reveal_type(func2("foo")) # E: Revealed type is 'builtins.str*' +reveal_type(func2(s1)) # E: Revealed type is 'builtins.str*' +reveal_type(func2("bar")) # E: Revealed type is 'builtins.str*' +reveal_type(func2(s2)) # E: Revealed type is 'builtins.str*' +[out] + +[case testLiteralAndGenericsWithOverloads] +from typing import TypeVar, overload, Union +from typing_extensions import Literal + +@overload +def func1(x: Literal[4]) -> Literal[19]: ... +@overload +def func1(x: int) -> int: ... +def func1(x: int) -> int: pass + +T = TypeVar('T') +def identity(x: T) -> T: pass + +a: Literal[4] +b: Literal[5] + +reveal_type(func1(identity(4))) # E: Revealed type is 'Literal[19]' +reveal_type(func1(identity(5))) # E: Revealed type is 'builtins.int' +reveal_type(func1(identity(a))) # E: Revealed type is 'Literal[19]' +reveal_type(func1(identity(b))) # E: Revealed type is 'builtins.int' + +[out] From ce1b1878b3c495888ff0a7a3ac30afc18557285b Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Tue, 11 Dec 2018 08:56:53 -0800 Subject: [PATCH 2/2] WIP towards code review --- mypy/checkexpr.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index a1a4ed82d101..f7f0923ed310 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -210,7 +210,7 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type: def analyze_var_ref(self, var: Var, context: Context) -> Type: if var.type: - if self.context_contains_literal_like_type() and var.name() in {'True', 'False'}: + if is_literal_type_like(self.type_context[-1]) and var.name() in {'True', 'False'}: return LiteralType(var.name() == 'True', self.named_type('builtins.bool')) else: return var.type @@ -1750,14 +1750,14 @@ def analyze_external_member_access(self, member: str, base_type: Type, def visit_int_expr(self, e: IntExpr) -> Type: """Type check an integer literal (trivial).""" typ = self.named_type('builtins.int') - if self.context_contains_literal_like_type(): + if is_literal_type_like(self.type_context[-1]): return LiteralType(value=e.value, fallback=typ) return typ def visit_str_expr(self, e: StrExpr) -> Type: """Type check a string literal (trivial).""" typ = self.named_type('builtins.str') - if self.context_contains_literal_like_type(): + if is_literal_type_like(self.type_context[-1]): return LiteralType(value=e.value, fallback=typ) return typ @@ -3349,10 +3349,6 @@ def narrow_type_from_binder(self, expr: Expression, known_type: Type) -> Type: return ans return known_type - def context_contains_literal_like_type(self) -> bool: - """Returns 'true' if the context contains anything that resembles a LiteralType""" - return any(is_literal_type_like(item) for item in self.type_context) - def has_any_type(t: Type) -> bool: """Whether t contains an Any type"""