From 4b8855dc2d03f9e6273e6f9bc5698cca233a4684 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 10 Feb 2019 13:21:27 +0000 Subject: [PATCH 01/13] Strict equality checks --- docs/source/command_line.rst | 18 +++ docs/source/config_file.rst | 4 + mypy/checker.py | 19 ++++ mypy/checkexpr.py | 46 +++++++- mypy/errors.py | 3 + mypy/main.py | 5 + mypy/messages.py | 7 ++ mypy/options.py | 5 + test-data/unit/check-expressions.test | 141 ++++++++++++++++++++++++ test-data/unit/check-flags.test | 13 +++ test-data/unit/fixtures/bool.pyi | 2 + test-data/unit/fixtures/primitives.pyi | 10 +- test-data/unit/fixtures/typing-full.pyi | 6 +- 13 files changed, 270 insertions(+), 9 deletions(-) diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index 678d09c21a2e..8b0c956f3c2d 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -386,6 +386,24 @@ of the above sections. # 'items' now has type List[List[str]] ... +``--strict-equality`` + By default, mypy allows always-false comparisons like ``42 == 'no'``. + Use this flag to prohibit such comparisons of non-overlapping types, and + similar identity and container checks: + + .. code-block:: python + + from typing import Text + + text: Text + if b'some bytes' in text: # Error: non-overlapping check! + ... + if text != b'other bytes': # Error: non-overlapping check! + ... + + if text is not None: # Error: non-overlapping check, 'text' can't be None. + ... + ``--strict`` This flag mode enables all optional error checking flags. You can see the list of flags enabled by strict mode in the full ``mypy --help`` output. diff --git a/docs/source/config_file.rst b/docs/source/config_file.rst index 53c82f39e599..b1325af20d43 100644 --- a/docs/source/config_file.rst +++ b/docs/source/config_file.rst @@ -294,6 +294,10 @@ Miscellaneous strictness flags Allows variables to be redefined with an arbitrary type, as long as the redefinition is in the same block and nesting level as the original definition. +``strict_equality`` (bool, default false) + Prohibit equality checks, identity checks, and container checks between + non-overlapping types. + Global-only options ******************* diff --git a/mypy/checker.py b/mypy/checker.py index 525726e42f05..9e2249f6c198 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2997,6 +2997,25 @@ def analyze_iterable_item_type(self, expr: Expression) -> Tuple[Type, Type]: nextmethod = 'next' return iterator, echk.check_method_call_by_name(nextmethod, iterator, [], [], expr)[0] + def analyze_container_item_type(self, typ: Type) -> Optional[Type]: + """Check if a type is a nominal container of a union of such. + + Return the corresponding container item type. + """ + if isinstance(typ, UnionType): + types = [] # type: List[Type] + for item in typ.items: + c_type = self.analyze_container_item_type(item) + if c_type: + types.append(c_type) + return UnionType.make_union(types) + if isinstance(typ, Instance) and typ.type.has_base('typing.Container'): + supertype = self.named_type('typing.Container').type + super_instance = map_instance_to_supertype(typ, supertype) + assert len(super_instance.args) == 1 + return super_instance.args[0] + return None + def analyze_index_variables(self, index: Expression, item_type: Type, infer_lvalue_type: bool, context: Context) -> None: """Type check or infer for loop or list comprehension index vars.""" diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index eed21748c49e..6e4fd29a6f9d 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -44,7 +44,7 @@ from mypy import message_registry from mypy.infer import infer_type_arguments, infer_function_type_arguments from mypy import join -from mypy.meet import narrow_declared_type +from mypy.meet import narrow_declared_type, is_overlapping_types from mypy.subtypes import ( is_subtype, is_proper_subtype, is_equivalent, find_member, non_method_protocol_members, ) @@ -1914,6 +1914,11 @@ def visit_comparison_expr(self, e: ComparisonExpr) -> Type: _, method_type = self.check_method_call_by_name( '__contains__', right_type, [left], [ARG_POS], e, local_errors) sub_result = self.bool_type() + # Container item type for strict type overlap checks. Note: we need to only + # check for nominal type, because a usual "Unsupported operands for in" + # will be reported for types incompatible with __contains__(). + # See testCustomContainsCheckStrictEquality for an example. + cont_type = self.chk.analyze_container_item_type(right_type) if isinstance(right_type, PartialType): # We don't really know if this is an error or not, so just shut up. pass @@ -1928,17 +1933,30 @@ def visit_comparison_expr(self, e: ComparisonExpr) -> Type: self.bool_type(), self.named_type('builtins.function')) if not is_subtype(left_type, itertype): - self.msg.unsupported_operand_types('in', left_type, right_type, e) + self.msg.unsupported_operand_types('in', left_type, itertype, e) + # Only show dangerous overlap if there are no other errors. + elif (not local_errors.is_errors() and cont_type and + self.dangerous_comparison(left_type, cont_type)): + self.msg.dangerous_comparison(left_type, cont_type, 'container', e) else: self.msg.add_errors(local_errors) elif operator in nodes.op_methods: method = self.get_operator_method(operator) + err_count = self.msg.errors.total_errors() sub_result, method_type = self.check_op(method, left_type, right, e, - allow_reverse=True) + allow_reverse=True) + # Only show dangerous overlap if there are no other errors. See + # testCustomEqCheckStrictEquality for an example. + if self.msg.errors.total_errors() == err_count and operator in ('==', '!='): + right_type = self.accept(right) + if self.dangerous_comparison(left_type, right_type): + self.msg.dangerous_comparison(left_type, right_type, 'equality', e) elif operator == 'is' or operator == 'is not': - self.accept(right) # validate the right operand + right_type = self.accept(right) # validate the right operand sub_result = self.bool_type() + if self.dangerous_comparison(left_type, right_type): + self.msg.dangerous_comparison(left_type, right_type, 'identity', e) method_type = None else: raise RuntimeError('Unknown comparison operator {}'.format(operator)) @@ -1954,6 +1972,26 @@ def visit_comparison_expr(self, e: ComparisonExpr) -> Type: assert result is not None return result + def dangerous_comparison(self, left: Type, right: Type) -> bool: + """Check for dangerous non-overlapping comparisons like 42 == 'no'. + + Rules: + * X and None are non-overlapping in strict-optional mode, and + overlapping otherwise. + * Optional[X] and Optional[Y] are non-overlapping if X and Y are + non-overlapping, although technically None is overlap, it is most + likely an error. + * Any overlaps with everything, i.e. always safe. + * Promotions are ignored, so both 'abc' == b'abc' and 1 == 1.0 + are errors. + """ + if isinstance(left, UnionType) and isinstance(right, UnionType): + left = remove_optional(left) + right = remove_optional(right) + if self.chk.options.strict_equality: + return not is_overlapping_types(left, right, ignore_promotions=True) + return False + def get_operator_method(self, op: str) -> str: if op == '/' and self.chk.options.python_version[0] == 2: # TODO also check for "from __future__ import division" diff --git a/mypy/errors.py b/mypy/errors.py index a177b5d6805a..0053e3ec08c4 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -169,6 +169,9 @@ def copy(self) -> 'Errors': new.scope = self.scope return new + def total_errors(self) -> int: + return sum(len(errs) for errs in self.error_info_map.values()) + def set_ignore_prefix(self, prefix: str) -> None: """Set path prefix that will be removed from all paths.""" prefix = os.path.normpath(prefix) diff --git a/mypy/main.py b/mypy/main.py index d9ff726cc5fd..e58074779e3f 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -527,6 +527,11 @@ def add_invertible_flag(flag: str, help="Allow unconditional variable redefinition with a new type", group=strictness_group) + add_invertible_flag('--strict-equality', default=False, strict_flag=False, + help="Prohibit equality, identity, and container checks for" + " non-overlapping types", + group=strictness_group) + incremental_group = parser.add_argument_group( title='Incremental mode', description="Adjust how mypy incrementally type checks and caches modules. " diff --git a/mypy/messages.py b/mypy/messages.py index 3512bd878cb6..ce7a90ea32d1 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -975,6 +975,13 @@ def incompatible_typevar_value(self, .format(typevar_name, callable_name(callee) or 'function', self.format(typ)), context) + def dangerous_comparison(self, left: Type, right: Type, kind: str, ctx: Context) -> None: + left_str = 'element' if kind == 'container' else 'left operand' + right_str = 'container item' if kind == 'container' else 'right operand' + message = 'Non-overlapping {} check ({} type: {}, {} type: {})' + left_typ, right_typ = self.format_distinctly(left, right) + self.fail(message.format(kind, left_str, left_typ, right_str, right_typ), ctx) + def overload_inconsistently_applies_decorator(self, decorator: str, context: Context) -> None: self.fail( 'Overload does not consistently use the "@{}" '.format(decorator) diff --git a/mypy/options.py b/mypy/options.py index 5ce766929a78..3160c269b1a6 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -22,6 +22,7 @@ class BuildType: # Please keep this list sorted "allow_untyped_globals", "allow_redefinition", + "strict_equality", "always_false", "always_true", "check_untyped_defs", @@ -157,6 +158,10 @@ def __init__(self) -> None: # and the same nesting level as the initialization self.allow_redefinition = False + # Prohibit equality, identity, and container checks for non-overlapping types. + # This makes 1 == '1', 1 in ['1'], and 1 is '1' errors. + self.strict_equality = False + # Variable names considered True self.always_true = [] # type: List[str] diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index 008b697845d0..ed1dc056488f 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -1947,3 +1947,144 @@ a.__pow__() # E: Too few arguments for "__pow__" of "int" x, y = [], [] # E: Need type annotation for 'x' \ # E: Need type annotation for 'y' [builtins fixtures/list.pyi] + +[case testStrictEqualityEq] +# flags: --strict-equality +class A: ... +class B: ... +class C(B): ... + +A() == B() # E: Non-overlapping equality check (left operand type: "A", right operand type: "B") +B() == C() +C() == B() +A() != B() # E: Non-overlapping equality check (left operand type: "A", right operand type: "B") +B() != C() +C() != B() +[builtins fixtures/bool.pyi] + +[case testStrictEqualityIs] +# flags: --strict-equality +class A: ... +class B: ... +class C(B): ... + +A() is B() # E: Non-overlapping identity check (left operand type: "A", right operand type: "B") +B() is C() +C() is B() +A() is not B() # E: Non-overlapping identity check (left operand type: "A", right operand type: "B") +B() is not C() +C() is not B() +[builtins fixtures/bool.pyi] + +[case testStrictEqualityContains] +# flags: --strict-equality +class A: ... +class B: ... +class C(B): ... + +A() in [B()] # E: Non-overlapping container check (element type: "A", container item type: "B") +B() in [C()] +C() in [B()] +A() not in [B()] # E: Non-overlapping container check (element type: "A", container item type: "B") +B() not in [C()] +C() not in [B()] +[builtins fixtures/list.pyi] +[typing fixtures/typing-full.pyi] + +[case testStrictEqualityUnions] +# flags: --strict-equality +from typing import Container, Union + +class A: ... +class B: ... + +a: Union[int, str] +b: Union[A, B] + +a == 42 +b == 42 # E: Non-overlapping equality check (left operand type: "Union[A, B]", right operand type: "int") + +a is 42 +b is 42 # E: Non-overlapping identity check (left operand type: "Union[A, B]", right operand type: "int") + +ca: Union[Container[int], Container[str]] +cb: Union[Container[A], Container[B]] + +42 in ca +42 in cb # E: Non-overlapping container check (element type: "int", container item type: "Union[A, B]") +[builtins fixtures/bool.pyi] +[typing fixtures/typing-full.pyi] + +[case testStrictEqualityNoPromote] +# flags: --strict-equality +'a' == b'a' # E: Non-overlapping equality check (left operand type: "str", right operand type: "bytes") +b'a' in 'abc' # E: Non-overlapping container check (element type: "bytes", container item type: "str") + +x: str +y: bytes +x != y # E: Non-overlapping equality check (left operand type: "str", right operand type: "bytes") +[builtins fixtures/primitives.pyi] +[typing fixtures/typing-full.pyi] + +[case testStrictEqualityAny] +# flags: --strict-equality +from typing import Any, Container + +x: Any +c: Container[str] +x in c +x == 42 +x is 42 +[builtins fixtures/bool.pyi] +[typing fixtures/typing-full.pyi] + +[case testStrictEqualityStrictOptional] +# flags: --strict-equality --strict-optional + +x: str +if x is not None: # E: Non-overlapping identity check (left operand type: "str", right operand type: "None") + pass +[builtins fixtures/bool.pyi] + +[case testStrictEqualityNoStrictOptional] +# flags: --strict-equality --no-strict-optional + +x: str +if x is not None: # OK without strict-optional + pass +[builtins fixtures/bool.pyi] + +[case testStrictEqualityEqNoOptionalOverlap] +# flags: --strict-equality --strict-optional +from typing import Optional + +x: Optional[str] +y: Optional[int] +if x == y: # E: Non-overlapping equality check (left operand type: "Optional[str]", right operand type: "Optional[int]") + ... +[builtins fixtures/bool.pyi] + +[case testCustomEqCheckStrictEquality] +# flags: --strict-equality +class A: + def __eq__(self, other: A) -> bool: # type: ignore + ... +class B: + def __eq__(self, other: B) -> bool: # type: ignore + ... + +# Don't report non-overlapping check if there is already and error. +A() == B() # E: Unsupported operand types for == ("A" and "B") +[builtins fixtures/bool.pyi] + +[case testCustomContainsCheckStrictEquality] +# flags: --strict-equality +from typing import Container +class A: + def __contains__(self, other: A) -> bool: # type: ignore + ... + +# Don't report non-overlapping check if there is already and error. +42 in A() # E: Unsupported operand types for in ("int" and "A") +[builtins fixtures/bool.pyi] +[typing fixtures/typing-full.pyi] diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test index cb3156d8a39a..09818180efc2 100644 --- a/test-data/unit/check-flags.test +++ b/test-data/unit/check-flags.test @@ -1107,3 +1107,16 @@ class A(Generic[T]): def f(c: A) -> None: # E: Missing type parameters for generic type pass [out] + +[case testStrictEqualityPerFile] +# flags: --config-file tmp/mypy.ini +import b +42 == 'no' # E: Non-overlapping equality check (left operand type: "int", right operand type: "str") +[file b.py] +42 == 'no' +[file mypy.ini] +[[mypy] +strict_equality = True +[[mypy-b] +strict_equality = False +[builtins fixtures/bool.pyi] diff --git a/test-data/unit/fixtures/bool.pyi b/test-data/unit/fixtures/bool.pyi index bf506d97312f..07bc461819a0 100644 --- a/test-data/unit/fixtures/bool.pyi +++ b/test-data/unit/fixtures/bool.pyi @@ -4,6 +4,8 @@ T = TypeVar('T') class object: def __init__(self) -> None: pass + def __eq__(self, other: object) -> bool: pass + def __ne__(self, other: object) -> bool: pass class type: pass class tuple(Generic[T]): pass diff --git a/test-data/unit/fixtures/primitives.pyi b/test-data/unit/fixtures/primitives.pyi index a2c1f390f65c..796196fa08c6 100644 --- a/test-data/unit/fixtures/primitives.pyi +++ b/test-data/unit/fixtures/primitives.pyi @@ -1,10 +1,12 @@ # builtins stub with non-generic primitive types -from typing import Generic, TypeVar +from typing import Generic, TypeVar, Sequence, Iterator T = TypeVar('T') class object: def __init__(self) -> None: pass def __str__(self) -> str: pass + def __eq__(self, other: object) -> bool: pass + def __ne__(self, other: object) -> bool: pass class type: def __init__(self, x) -> None: pass @@ -15,10 +17,14 @@ class float: def __float__(self) -> float: pass class complex: pass class bool(int): pass -class str: +class str(Sequence[str]): def __add__(self, s: str) -> str: pass + def __iter__(self) -> Iterator[str]: pass + def __contains__(self, other: object) -> bool: pass + def __getitem__(self, item: int) -> str: pass def format(self, *args) -> str: pass class bytes: pass class bytearray: pass class tuple(Generic[T]): pass class function: pass +class ellipsis: pass diff --git a/test-data/unit/fixtures/typing-full.pyi b/test-data/unit/fixtures/typing-full.pyi index 82e119d043e1..0bfc46c0d992 100644 --- a/test-data/unit/fixtures/typing-full.pyi +++ b/test-data/unit/fixtures/typing-full.pyi @@ -39,10 +39,10 @@ S = TypeVar('S') # to silence the protocol variance checks. Maybe it is better to use type: ignore? @runtime -class Container(Protocol[T_contra]): +class Container(Protocol[T_co]): @abstractmethod # Use int because bool isn't in the default test builtins - def __contains__(self, arg: T_contra) -> int: pass + def __contains__(self, arg: object) -> int: pass @runtime class Sized(Protocol): @@ -117,7 +117,7 @@ class AsyncIterator(AsyncIterable[T], Protocol): @abstractmethod def __anext__(self) -> Awaitable[T]: pass -class Sequence(Iterable[T_co]): +class Sequence(Iterable[T_co], Container[T_co]): @abstractmethod def __getitem__(self, n: Any) -> T_co: pass From 436be2ef99ac65c709b49ed675c25dd0a894e068 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 10 Feb 2019 14:06:03 +0000 Subject: [PATCH 02/13] Update test fixtures --- mypy/checkexpr.py | 2 +- test-data/unit/check-expressions.test | 6 +++--- test-data/unit/fixtures/async_await.pyi | 3 ++- test-data/unit/fixtures/dict.pyi | 3 ++- test-data/unit/fixtures/isinstancelist.pyi | 1 + 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 6e4fd29a6f9d..4c8de5713f0d 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1933,7 +1933,7 @@ def visit_comparison_expr(self, e: ComparisonExpr) -> Type: self.bool_type(), self.named_type('builtins.function')) if not is_subtype(left_type, itertype): - self.msg.unsupported_operand_types('in', left_type, itertype, e) + self.msg.unsupported_operand_types('in', left_type, right_type, e) # Only show dangerous overlap if there are no other errors. elif (not local_errors.is_errors() and cont_type and self.dangerous_comparison(left_type, cont_type)): diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index ed1dc056488f..75ff0c3da983 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -490,7 +490,7 @@ class A: def __cmp__(self, o): # type: ('B') -> bool pass - def __eq__(self, o): + def __eq__(self, o): # type: ignore # type: ('int') -> bool pass class B: @@ -504,7 +504,7 @@ class C: def __cmp__(self, o): # type: ('A') -> bool pass - def __eq__(self, o): + def __eq__(self, o): # type: ignore # type: ('int') -> bool pass @@ -604,7 +604,7 @@ class X: class Y: def __lt__(self, o: 'Y') -> A: pass def __gt__(self, o: 'Y') -> A: pass - def __eq__(self, o: 'Y') -> B: pass + def __eq__(self, o: 'Y') -> B: pass # type: ignore [builtins fixtures/bool.pyi] diff --git a/test-data/unit/fixtures/async_await.pyi b/test-data/unit/fixtures/async_await.pyi index b6161f45dacf..ed64289c0d4d 100644 --- a/test-data/unit/fixtures/async_await.pyi +++ b/test-data/unit/fixtures/async_await.pyi @@ -5,6 +5,7 @@ U = typing.TypeVar('U') class list(typing.Sequence[T]): def __iter__(self) -> typing.Iterator[T]: ... def __getitem__(self, i: int) -> T: ... + def __contains__(self, item: object) -> bool: ... class object: def __init__(self) -> None: pass @@ -12,7 +13,7 @@ class type: pass class function: pass class int: pass class str: pass -class bool: pass +class bool(int): pass class dict(typing.Generic[T, U]): pass class set(typing.Generic[T]): pass class tuple(typing.Generic[T]): pass diff --git a/test-data/unit/fixtures/dict.pyi b/test-data/unit/fixtures/dict.pyi index 93648b274d98..d7e8d11b7d0b 100644 --- a/test-data/unit/fixtures/dict.pyi +++ b/test-data/unit/fixtures/dict.pyi @@ -39,11 +39,12 @@ class list(Sequence[T]): # needed by some test cases def __getitem__(self, x: int) -> T: pass def __iter__(self) -> Iterator[T]: pass def __mul__(self, x: int) -> list[T]: pass + def __contains__(self, item: object) -> bool: pass class tuple(Generic[T]): pass class function: pass class float: pass -class bool: pass +class bool(int): pass class ellipsis: pass def isinstance(x: object, t: Union[type, Tuple[type, ...]]) -> bool: pass diff --git a/test-data/unit/fixtures/isinstancelist.pyi b/test-data/unit/fixtures/isinstancelist.pyi index 6b93f16d2247..25ff5888a2cf 100644 --- a/test-data/unit/fixtures/isinstancelist.pyi +++ b/test-data/unit/fixtures/isinstancelist.pyi @@ -35,6 +35,7 @@ class list(Sequence[T]): def __setitem__(self, x: int, v: T) -> None: pass def __getitem__(self, x: int) -> T: pass def __add__(self, x: List[T]) -> T: pass + def __contains__(self, item: object) -> bool: pass class dict(Mapping[KT, VT]): @overload From 400ac32eca8a8753790edc8e1a3d6d826cfb1b5d Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 10 Feb 2019 14:37:26 +0000 Subject: [PATCH 03/13] Fix lint and remaining fixture --- mypy/checker.py | 8 ++++---- test-data/unit/check-expressions.test | 8 +++----- test-data/unit/fixtures/bool.pyi | 6 ++++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 9e2249f6c198..00f6c42c5ebf 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3010,10 +3010,10 @@ def analyze_container_item_type(self, typ: Type) -> Optional[Type]: types.append(c_type) return UnionType.make_union(types) if isinstance(typ, Instance) and typ.type.has_base('typing.Container'): - supertype = self.named_type('typing.Container').type - super_instance = map_instance_to_supertype(typ, supertype) - assert len(super_instance.args) == 1 - return super_instance.args[0] + supertype = self.named_type('typing.Container').type + super_instance = map_instance_to_supertype(typ, supertype) + assert len(super_instance.args) == 1 + return super_instance.args[0] return None def analyze_index_variables(self, index: Expression, item_type: Type, diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index 75ff0c3da983..b2ad7a6ec69e 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -490,7 +490,7 @@ class A: def __cmp__(self, o): # type: ('B') -> bool pass - def __eq__(self, o): # type: ignore + def __eq__(self, o): # type: ('int') -> bool pass class B: @@ -504,7 +504,7 @@ class C: def __cmp__(self, o): # type: ('A') -> bool pass - def __eq__(self, o): # type: ignore + def __eq__(self, o): # type: ('int') -> bool pass @@ -2079,12 +2079,10 @@ A() == B() # E: Unsupported operand types for == ("A" and "B") [case testCustomContainsCheckStrictEquality] # flags: --strict-equality -from typing import Container class A: - def __contains__(self, other: A) -> bool: # type: ignore + def __contains__(self, other: A) -> bool: ... # Don't report non-overlapping check if there is already and error. 42 in A() # E: Unsupported operand types for in ("int" and "A") [builtins fixtures/bool.pyi] -[typing fixtures/typing-full.pyi] diff --git a/test-data/unit/fixtures/bool.pyi b/test-data/unit/fixtures/bool.pyi index 07bc461819a0..4876c49e11e4 100644 --- a/test-data/unit/fixtures/bool.pyi +++ b/test-data/unit/fixtures/bool.pyi @@ -1,11 +1,13 @@ # builtins stub used in boolean-related test cases. from typing import Generic, TypeVar +import sys T = TypeVar('T') class object: def __init__(self) -> None: pass - def __eq__(self, other: object) -> bool: pass - def __ne__(self, other: object) -> bool: pass + if sys.version_info[0] >= 3: # type: ignore + def __eq__(self, other: object) -> bool: pass + def __ne__(self, other: object) -> bool: pass class type: pass class tuple(Generic[T]): pass From d1c4fcf8fab885b9b32ea6cd6cf10671e9895448 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 10 Feb 2019 14:50:41 +0000 Subject: [PATCH 04/13] Copy the fixture instaead --- test-data/unit/check-expressions.test | 2 +- test-data/unit/fixtures/bool.pyi | 6 ++---- test-data/unit/fixtures/bool_py2.pyi | 16 ++++++++++++++++ 3 files changed, 19 insertions(+), 5 deletions(-) create mode 100644 test-data/unit/fixtures/bool_py2.pyi diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index b2ad7a6ec69e..ef07eda596f1 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -508,7 +508,7 @@ class C: # type: ('int') -> bool pass -[builtins_py2 fixtures/bool.pyi] +[builtins_py2 fixtures/bool_py2.pyi] [case cmpIgnoredPy3] diff --git a/test-data/unit/fixtures/bool.pyi b/test-data/unit/fixtures/bool.pyi index 4876c49e11e4..07bc461819a0 100644 --- a/test-data/unit/fixtures/bool.pyi +++ b/test-data/unit/fixtures/bool.pyi @@ -1,13 +1,11 @@ # builtins stub used in boolean-related test cases. from typing import Generic, TypeVar -import sys T = TypeVar('T') class object: def __init__(self) -> None: pass - if sys.version_info[0] >= 3: # type: ignore - def __eq__(self, other: object) -> bool: pass - def __ne__(self, other: object) -> bool: pass + def __eq__(self, other: object) -> bool: pass + def __ne__(self, other: object) -> bool: pass class type: pass class tuple(Generic[T]): pass diff --git a/test-data/unit/fixtures/bool_py2.pyi b/test-data/unit/fixtures/bool_py2.pyi new file mode 100644 index 000000000000..b2c935132d57 --- /dev/null +++ b/test-data/unit/fixtures/bool_py2.pyi @@ -0,0 +1,16 @@ +# builtins stub used in boolean-related test cases. +from typing import Generic, TypeVar +import sys +T = TypeVar('T') + +class object: + def __init__(self) -> None: pass + +class type: pass +class tuple(Generic[T]): pass +class function: pass +class bool: pass +class int: pass +class str: pass +class unicode: pass +class ellipsis: pass From 086c225e278490593799f6b270b329c76125552a Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 11 Feb 2019 13:57:55 +0000 Subject: [PATCH 05/13] Fix capitalization --- docs/source/config_file.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/config_file.rst b/docs/source/config_file.rst index b1325af20d43..f51319a5c2f3 100644 --- a/docs/source/config_file.rst +++ b/docs/source/config_file.rst @@ -294,7 +294,7 @@ Miscellaneous strictness flags Allows variables to be redefined with an arbitrary type, as long as the redefinition is in the same block and nesting level as the original definition. -``strict_equality`` (bool, default false) +``strict_equality`` (bool, default False) Prohibit equality checks, identity checks, and container checks between non-overlapping types. From 397c3a281537c424f2d41a7491620ff14e37f293 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 14 Feb 2019 18:48:00 +0000 Subject: [PATCH 06/13] Address CR --- mypy/checkexpr.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 4c8de5713f0d..60aa0bd75e69 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1976,21 +1976,25 @@ def dangerous_comparison(self, left: Type, right: Type) -> bool: """Check for dangerous non-overlapping comparisons like 42 == 'no'. Rules: - * X and None are non-overlapping in strict-optional mode, and - overlapping otherwise. + * X and None are overlapping even in strict-optional mode. This is to allow + 'assert x is not None' for x defined as 'x = None # type: str' in class body + (otherwise mypy itself would have couple dozen errors because of this). * Optional[X] and Optional[Y] are non-overlapping if X and Y are non-overlapping, although technically None is overlap, it is most likely an error. * Any overlaps with everything, i.e. always safe. * Promotions are ignored, so both 'abc' == b'abc' and 1 == 1.0 - are errors. + are errors. This is mostly needed for bytes vs unicode, and + int vs float are added just for consistency. """ + if not self.chk.options.strict_equality: + return False + if isinstance(left, NoneTyp) or isinstance(right, NoneTyp): + return False if isinstance(left, UnionType) and isinstance(right, UnionType): left = remove_optional(left) right = remove_optional(right) - if self.chk.options.strict_equality: - return not is_overlapping_types(left, right, ignore_promotions=True) - return False + return not is_overlapping_types(left, right, ignore_promotions=True) def get_operator_method(self, op: str) -> str: if op == '/' and self.chk.options.python_version[0] == 2: From cad52813b0d231dad77a9543095a82212e9161ab Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 14 Feb 2019 19:58:30 +0000 Subject: [PATCH 07/13] Add corner case Type[...] overlap and update tests --- mypy/meet.py | 25 ++++++++++++++++++- test-data/unit/check-expressions.test | 35 ++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/mypy/meet.py b/mypy/meet.py index 62f1c9f85356..ea73f92c80fe 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -221,9 +221,32 @@ def is_none_typevar_overlap(t1: Type, t2: Type) -> bool: # As before, we degrade into 'Instance' whenever possible. if isinstance(left, TypeType) and isinstance(right, TypeType): - # TODO: Can Callable[[...], T] and Type[T] be partially overlapping? return _is_overlapping_types(left.item, right.item) + # Type[C] vs Callable[..., C], where the latter is class object. + if isinstance(left, CallableType) and left.is_type_obj() and isinstance(right, TypeType): + if _is_overlapping_types(left.ret_type, right.item): + return True + if isinstance(right, CallableType) and right.is_type_obj() and isinstance(left, TypeType): + if _is_overlapping_types(right.ret_type, left.item): + return True + + # Type[C] vs Meta, where Meta is a metaclass for C. + if (isinstance(left, TypeType) and isinstance(right, Instance) and + isinstance(left.item, Instance)): + left_meta = left.item.type.metaclass_type + if left_meta is not None and _is_overlapping_types(left_meta, right): + return True + if left_meta is None and right.type.has_base('builtins.type'): + return True + if (isinstance(right, TypeType) and isinstance(left, Instance) and + isinstance(right.item, Instance)): + right_meta = right.item.type.metaclass_type + if right_meta is not None and _is_overlapping_types(right_meta, left): + return True + if right_meta is None and left.type.has_base('builtins.type'): + return True + if isinstance(left, CallableType) and isinstance(right, CallableType): return is_callable_compatible(left, right, is_compat=_is_overlapping_types, diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index ef07eda596f1..49a13e6ec9a2 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -2042,7 +2042,7 @@ x is 42 # flags: --strict-equality --strict-optional x: str -if x is not None: # E: Non-overlapping identity check (left operand type: "str", right operand type: "None") +if x is not None: # OK even with strict-optional pass [builtins fixtures/bool.pyi] @@ -2086,3 +2086,36 @@ class A: # Don't report non-overlapping check if there is already and error. 42 in A() # E: Unsupported operand types for in ("int" and "A") [builtins fixtures/bool.pyi] + +[case testStrictEqualityTypeVsCallable] +# flags: --strict-equality +from typing import Type, List +class C: ... +class D(C): ... +class Bad: ... + +subclasses: List[Type[C]] +C in subclasses +D in subclasses +Bad in subclasses # E: Non-overlapping container check (element type: "Type[Bad]", container item type: "Type[C]") +[builtins fixtures/list.pyi] +[typing fixtures/typing-full.pyi] + +[case testStrictEqualityMetaclass] +# flags: --strict-equality +from typing import List, Type + +class Meta(type): ... + +class A(metaclass=Meta): ... +class B(metaclass=Meta): ... +class C: ... + +o: Type[object] +exp: List[Meta] + +A in exp +C in exp # E: Non-overlapping container check (element type: "Type[C]", container item type: "Meta") +o in exp +[builtins fixtures/list.pyi] +[typing fixtures/typing-full.pyi] From 85f48e31b8fed137d9dcf76833a5a1e67aab18af Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 14 Feb 2019 20:01:45 +0000 Subject: [PATCH 08/13] Improve one test --- test-data/unit/check-expressions.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index 49a13e6ec9a2..e893f5fe3db9 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -2095,7 +2095,7 @@ class D(C): ... class Bad: ... subclasses: List[Type[C]] -C in subclasses +object in subclasses D in subclasses Bad in subclasses # E: Non-overlapping container check (element type: "Type[Bad]", container item type: "Type[C]") [builtins fixtures/list.pyi] From 91f2563de5673a15e9df860e8dd98eef211c876e Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 14 Feb 2019 20:04:14 +0000 Subject: [PATCH 09/13] Update docs w.r.t. new None semantics --- docs/source/command_line.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index 8b0c956f3c2d..488d719e8f50 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -401,8 +401,7 @@ of the above sections. if text != b'other bytes': # Error: non-overlapping check! ... - if text is not None: # Error: non-overlapping check, 'text' can't be None. - ... + assert text is not None # OK, this special case is allowed. ``--strict`` This flag mode enables all optional error checking flags. You can see the From 9fa2be7edc127f1bb4f920bead75fdfc6b3c1a69 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 14 Feb 2019 20:07:37 +0000 Subject: [PATCH 10/13] Fix lint --- mypy/meet.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mypy/meet.py b/mypy/meet.py index ea73f92c80fe..c7ad5c0e5f57 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -236,16 +236,16 @@ def is_none_typevar_overlap(t1: Type, t2: Type) -> bool: isinstance(left.item, Instance)): left_meta = left.item.type.metaclass_type if left_meta is not None and _is_overlapping_types(left_meta, right): - return True + return True if left_meta is None and right.type.has_base('builtins.type'): - return True + return True if (isinstance(right, TypeType) and isinstance(left, Instance) and isinstance(right.item, Instance)): right_meta = right.item.type.metaclass_type if right_meta is not None and _is_overlapping_types(right_meta, left): - return True + return True if right_meta is None and left.type.has_base('builtins.type'): - return True + return True if isinstance(left, CallableType) and isinstance(right, CallableType): return is_callable_compatible(left, right, From b737f2d03eeddcfc52dea564d500b2b85f9b26f0 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 14 Feb 2019 23:26:11 +0000 Subject: [PATCH 11/13] Refactor repeating Type[...] checks into a nested function --- mypy/meet.py | 39 +++++++++++++++++---------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/mypy/meet.py b/mypy/meet.py index c7ad5c0e5f57..f32577c86bdc 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -223,29 +223,24 @@ def is_none_typevar_overlap(t1: Type, t2: Type) -> bool: if isinstance(left, TypeType) and isinstance(right, TypeType): return _is_overlapping_types(left.item, right.item) - # Type[C] vs Callable[..., C], where the latter is class object. - if isinstance(left, CallableType) and left.is_type_obj() and isinstance(right, TypeType): - if _is_overlapping_types(left.ret_type, right.item): - return True - if isinstance(right, CallableType) and right.is_type_obj() and isinstance(left, TypeType): - if _is_overlapping_types(right.ret_type, left.item): - return True + def _type_object_overlap(left: Type, right: Type) -> bool: + """Special cases for type object types overlaps.""" + # Type[C] vs Callable[..., C], where the latter is class object. + if isinstance(left, CallableType) and left.is_type_obj() and isinstance(right, TypeType): + if _is_overlapping_types(left.ret_type, right.item): + return True + # Type[C] vs Meta, where Meta is a metaclass for C. + if (isinstance(left, TypeType) and isinstance(right, Instance) and + isinstance(left.item, Instance)): + left_meta = left.item.type.metaclass_type + if left_meta is not None and _is_overlapping_types(left_meta, right): + return True + if left_meta is None and right.type.has_base('builtins.type'): + return True + return False - # Type[C] vs Meta, where Meta is a metaclass for C. - if (isinstance(left, TypeType) and isinstance(right, Instance) and - isinstance(left.item, Instance)): - left_meta = left.item.type.metaclass_type - if left_meta is not None and _is_overlapping_types(left_meta, right): - return True - if left_meta is None and right.type.has_base('builtins.type'): - return True - if (isinstance(right, TypeType) and isinstance(left, Instance) and - isinstance(right.item, Instance)): - right_meta = right.item.type.metaclass_type - if right_meta is not None and _is_overlapping_types(right_meta, left): - return True - if right_meta is None and left.type.has_base('builtins.type'): - return True + if _type_object_overlap(left, right) or _type_object_overlap(right, left): + return True if isinstance(left, CallableType) and isinstance(right, CallableType): return is_callable_compatible(left, right, From 3cd12d9ab635785bb201a9a593f09495516377e0 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 14 Feb 2019 23:53:09 +0000 Subject: [PATCH 12/13] Simplify logic; add comments --- mypy/meet.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/mypy/meet.py b/mypy/meet.py index f32577c86bdc..598f22cf2774 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -225,22 +225,23 @@ def is_none_typevar_overlap(t1: Type, t2: Type) -> bool: def _type_object_overlap(left: Type, right: Type) -> bool: """Special cases for type object types overlaps.""" + # TODO: these checks are a bit in gray area, adjust if they cause problems. # Type[C] vs Callable[..., C], where the latter is class object. - if isinstance(left, CallableType) and left.is_type_obj() and isinstance(right, TypeType): - if _is_overlapping_types(left.ret_type, right.item): - return True + if isinstance(left, TypeType) and isinstance(right, CallableType) and right.is_type_obj(): + return _is_overlapping_types(left.item, right.ret_type) # Type[C] vs Meta, where Meta is a metaclass for C. - if (isinstance(left, TypeType) and isinstance(right, Instance) and - isinstance(left.item, Instance)): + if (isinstance(left, TypeType) and isinstance(left.item, Instance) and + isinstance(right, Instance)): left_meta = left.item.type.metaclass_type - if left_meta is not None and _is_overlapping_types(left_meta, right): - return True - if left_meta is None and right.type.has_base('builtins.type'): - return True + if left_meta is not None: + return _is_overlapping_types(left_meta, right) + else: + # builtins.type (default metaclass) overlaps with all metaclasses + return right.type.has_base('builtins.type') return False - if _type_object_overlap(left, right) or _type_object_overlap(right, left): - return True + if isinstance(left, TypeType) or isinstance(right, TypeType): + return _type_object_overlap(left, right) or _type_object_overlap(right, left) if isinstance(left, CallableType) and isinstance(right, CallableType): return is_callable_compatible(left, right, From 143db54edb529eb0ccfdb5a88e74c6228dce1c28 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 15 Feb 2019 00:13:44 +0000 Subject: [PATCH 13/13] Save one more line and instead add a comment --- mypy/meet.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mypy/meet.py b/mypy/meet.py index 598f22cf2774..10d5b051293a 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -226,18 +226,18 @@ def is_none_typevar_overlap(t1: Type, t2: Type) -> bool: def _type_object_overlap(left: Type, right: Type) -> bool: """Special cases for type object types overlaps.""" # TODO: these checks are a bit in gray area, adjust if they cause problems. - # Type[C] vs Callable[..., C], where the latter is class object. + # 1. Type[C] vs Callable[..., C], where the latter is class object. if isinstance(left, TypeType) and isinstance(right, CallableType) and right.is_type_obj(): return _is_overlapping_types(left.item, right.ret_type) - # Type[C] vs Meta, where Meta is a metaclass for C. + # 2. Type[C] vs Meta, where Meta is a metaclass for C. if (isinstance(left, TypeType) and isinstance(left.item, Instance) and isinstance(right, Instance)): left_meta = left.item.type.metaclass_type if left_meta is not None: return _is_overlapping_types(left_meta, right) - else: - # builtins.type (default metaclass) overlaps with all metaclasses - return right.type.has_base('builtins.type') + # builtins.type (default metaclass) overlaps with all metaclasses + return right.type.has_base('builtins.type') + # 3. Callable[..., C] vs Meta is considered below, when we switch to fallbacks. return False if isinstance(left, TypeType) or isinstance(right, TypeType):