From 8a30073099fa2f2f7e65f0f33c1fd5895546228f Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 8 Feb 2024 17:49:16 -0800 Subject: [PATCH 01/27] Basic work on TypeNarrower --- mypy/applytype.py | 7 ++++++- mypy/checker.py | 16 ++++++++++++---- mypy/checkexpr.py | 8 +++++--- mypy/constraints.py | 1 + mypy/expandtype.py | 2 ++ mypy/fixup.py | 2 ++ mypy/meet.py | 2 ++ mypy/message_registry.py | 2 +- mypy/messages.py | 4 ++++ mypy/nodes.py | 3 +++ mypy/semanal.py | 7 +++++++ mypy/subtypes.py | 7 +++++++ mypy/typeanal.py | 26 +++++++++++++++++++++++--- mypy/types.py | 28 +++++++++++++++++++++++++++- 14 files changed, 102 insertions(+), 13 deletions(-) diff --git a/mypy/applytype.py b/mypy/applytype.py index b00372855d9c..b7b2cbd8928b 100644 --- a/mypy/applytype.py +++ b/mypy/applytype.py @@ -137,11 +137,15 @@ def apply_generic_arguments( arg_types=[expand_type(at, id_to_type) for at in callable.arg_types] ) - # Apply arguments to TypeGuard if any. + # Apply arguments to TypeGuard and TypeNarrower if any. if callable.type_guard is not None: type_guard = expand_type(callable.type_guard, id_to_type) else: type_guard = None + if callable.type_narrower is not None: + type_narrower = expand_type(callable.type_narrower, id_to_type) + else: + type_narrower = None # The callable may retain some type vars if only some were applied. # TODO: move apply_poly() logic from checkexpr.py here when new inference @@ -153,4 +157,5 @@ def apply_generic_arguments( ret_type=expand_type(callable.ret_type, id_to_type), variables=remaining_tvars, type_guard=type_guard, + type_narrower=type_narrower, ) diff --git a/mypy/checker.py b/mypy/checker.py index 391f28e93b1d..a9b499a24b9b 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -199,6 +199,7 @@ TupleType, Type, TypeAliasType, + TypeNarrowerType, TypedDictType, TypeGuardedType, TypeOfAny, @@ -2177,6 +2178,8 @@ def check_override( elif isinstance(original, CallableType) and isinstance(override, CallableType): if original.type_guard is not None and override.type_guard is None: fail = True + if original.type_narrower is not None and override.type_narrower is None: + fail = True if is_private(name): fail = False @@ -5629,7 +5632,7 @@ def combine_maps(list_maps: list[TypeMap]) -> TypeMap: def find_isinstance_check(self, node: Expression) -> tuple[TypeMap, TypeMap]: """Find any isinstance checks (within a chain of ands). Includes implicit and explicit checks for None and calls to callable. - Also includes TypeGuard functions. + Also includes TypeGuard and TypeNarrower functions. Return value is a map of variables to their types if the condition is true and a map of variables to their types if the condition is false. @@ -5681,7 +5684,7 @@ def find_isinstance_check_helper(self, node: Expression) -> tuple[TypeMap, TypeM if literal(expr) == LITERAL_TYPE and attr and len(attr) == 1: return self.hasattr_type_maps(expr, self.lookup_type(expr), attr[0]) elif isinstance(node.callee, RefExpr): - if node.callee.type_guard is not None: + if node.callee.type_guard is not None or node.callee.type_narrower is not None: # TODO: Follow *args, **kwargs if node.arg_kinds[0] != nodes.ARG_POS: # the first argument might be used as a kwarg @@ -5707,7 +5710,8 @@ def find_isinstance_check_helper(self, node: Expression) -> tuple[TypeMap, TypeM # we want the idx-th variable to be narrowed expr = collapse_walrus(node.args[idx]) else: - self.fail(message_registry.TYPE_GUARD_POS_ARG_REQUIRED, node) + kind = "guard" if node.callee.type_guard is not None else "narrower" + self.fail(message_registry.TYPE_GUARD_POS_ARG_REQUIRED.format(kind), node) return {}, {} if literal(expr) == LITERAL_TYPE: # Note: we wrap the target type, so that we can special case later. @@ -5715,7 +5719,11 @@ def find_isinstance_check_helper(self, node: Expression) -> tuple[TypeMap, TypeM # considered "always right" (i.e. even if the types are not overlapping). # Also note that a care must be taken to unwrap this back at read places # where we use this to narrow down declared type. - return {expr: TypeGuardedType(node.callee.type_guard)}, {} + if node.callee.type_guard is not None: + guard_type = TypeGuardedType(node.callee.type_guard) + else: + guard_type = TypeNarrowerType(node.callee.type_narrower) + return {expr: guard_type}, {} elif isinstance(node, ComparisonExpr): # Step 1: Obtain the types of each operand and whether or not we can # narrow their types. (For example, we shouldn't try narrowing the diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index ff7b7fa2ff58..ca815b9ac02f 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1454,10 +1454,12 @@ def check_call_expr_with_callee_type( if ( isinstance(e.callee, RefExpr) and isinstance(proper_callee, CallableType) - and proper_callee.type_guard is not None ): # Cache it for find_isinstance_check() - e.callee.type_guard = proper_callee.type_guard + if proper_callee.type_guard is not None: + e.callee.type_guard = proper_callee.type_guard + if proper_callee.type_narrower is not None: + e.callee.type_narrower = proper_callee.type_narrower return ret_type def check_union_call_expr(self, e: CallExpr, object_type: UnionType, member: str) -> Type: @@ -5277,7 +5279,7 @@ def infer_lambda_type_using_context( # is a constructor -- but this fallback doesn't make sense for lambdas. callable_ctx = callable_ctx.copy_modified(fallback=self.named_type("builtins.function")) - if callable_ctx.type_guard is not None: + if callable_ctx.type_guard is not None or callable_ctx.type_narrower is not None: # Lambda's return type cannot be treated as a `TypeGuard`, # because it is implicit. And `TypeGuard`s must be explicit. # See https://github.com/python/mypy/issues/9927 diff --git a/mypy/constraints.py b/mypy/constraints.py index c4eba2ca1ede..a79dda9e078b 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -1018,6 +1018,7 @@ def visit_callable_type(self, template: CallableType) -> list[Constraint]: param_spec = template.param_spec() template_ret_type, cactual_ret_type = template.ret_type, cactual.ret_type + # TODO(jelle): TypeNarrower if template.type_guard is not None: template_ret_type = template.type_guard if cactual.type_guard is not None: diff --git a/mypy/expandtype.py b/mypy/expandtype.py index d2d294fb77f3..52e83b2db7b2 100644 --- a/mypy/expandtype.py +++ b/mypy/expandtype.py @@ -342,6 +342,7 @@ def visit_callable_type(self, t: CallableType) -> CallableType: arg_names=t.arg_names[:-2] + repl.arg_names, ret_type=t.ret_type.accept(self), type_guard=(t.type_guard.accept(self) if t.type_guard is not None else None), + type_narrower=(t.type_narrower.accept(self) if t.type_narrower is not None else None), imprecise_arg_kinds=(t.imprecise_arg_kinds or repl.imprecise_arg_kinds), variables=[*repl.variables, *t.variables], ) @@ -375,6 +376,7 @@ def visit_callable_type(self, t: CallableType) -> CallableType: arg_types=arg_types, ret_type=t.ret_type.accept(self), type_guard=(t.type_guard.accept(self) if t.type_guard is not None else None), + type_narrower=(t.type_narrower.accept(self) if t.type_narrower is not None else None), ) if needs_normalization: return expanded.with_normalized_var_args() diff --git a/mypy/fixup.py b/mypy/fixup.py index 02c6ab93f29e..a76a5ebe867b 100644 --- a/mypy/fixup.py +++ b/mypy/fixup.py @@ -270,6 +270,8 @@ def visit_callable_type(self, ct: CallableType) -> None: arg.accept(self) if ct.type_guard is not None: ct.type_guard.accept(self) + if ct.type_narrower is not None: + ct.type_narrower.accept(self) def visit_overloaded(self, t: Overloaded) -> None: for ct in t.items: diff --git a/mypy/meet.py b/mypy/meet.py index df8b960cdf3f..73c45718ee6b 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -115,6 +115,7 @@ def narrow_declared_type(declared: Type, narrowed: Type) -> Type: if isinstance(narrowed, TypeGuardedType): # type: ignore[misc] # A type guard forces the new type even if it doesn't overlap the old. return narrowed.type_guard + # TODO(jelle): TypeNarrower original_declared = declared original_narrowed = narrowed @@ -275,6 +276,7 @@ def is_overlapping_types( ): # A type guard forces the new type even if it doesn't overlap the old. return True + # TODO(jelle): TypeNarrower if seen_types is None: seen_types = set() diff --git a/mypy/message_registry.py b/mypy/message_registry.py index fb430b63c74b..85f9f0feaf6e 100644 --- a/mypy/message_registry.py +++ b/mypy/message_registry.py @@ -262,7 +262,7 @@ def with_additional_msg(self, info: str) -> ErrorMessage: CONTIGUOUS_ITERABLE_EXPECTED: Final = ErrorMessage("Contiguous iterable with same type expected") ITERABLE_TYPE_EXPECTED: Final = ErrorMessage("Invalid type '{}' for *expr (iterable expected)") -TYPE_GUARD_POS_ARG_REQUIRED: Final = ErrorMessage("Type guard requires positional argument") +TYPE_GUARD_POS_ARG_REQUIRED: Final = ErrorMessage("Type {} requires positional argument") # Match Statement MISSING_MATCH_ARGS: Final = 'Class "{}" doesn\'t define "__match_args__"' diff --git a/mypy/messages.py b/mypy/messages.py index c107e874f4fc..88016f7b8fda 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -2634,6 +2634,8 @@ def format_literal_value(typ: LiteralType) -> str: elif isinstance(func, CallableType): if func.type_guard is not None: return_type = f"TypeGuard[{format(func.type_guard)}]" + elif func.type_narrower is not None: + return_type = f"TypeNarrower[{format(func.type_narrower)}]" else: return_type = format(func.ret_type) if func.is_ellipsis_args: @@ -2850,6 +2852,8 @@ def [T <: int] f(self, x: int, y: T) -> None s += " -> " if tp.type_guard is not None: s += f"TypeGuard[{format_type_bare(tp.type_guard, options)}]" + elif tp.type_narrower is not None: + s += f"TypeNarrower[{format_type_bare(tp.type_narrower, options)}]" else: s += format_type_bare(tp.ret_type, options) diff --git a/mypy/nodes.py b/mypy/nodes.py index 1c781320580a..f77a59065804 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -1755,6 +1755,7 @@ class RefExpr(Expression): "is_inferred_def", "is_alias_rvalue", "type_guard", + "type_narrower", ) def __init__(self) -> None: @@ -1776,6 +1777,8 @@ def __init__(self) -> None: self.is_alias_rvalue = False # Cache type guard from callable_type.type_guard self.type_guard: mypy.types.Type | None = None + # And same for TypeNarrower + self.type_narrower: mypy.types.Type | None = None @property def fullname(self) -> str: diff --git a/mypy/semanal.py b/mypy/semanal.py index 4bf9f0c3eabb..f00cbcd607e4 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -879,6 +879,13 @@ def analyze_func_def(self, defn: FuncDef) -> None: ) # in this case, we just kind of just ... remove the type guard. result = result.copy_modified(type_guard=None) + if result.type_narrower and ARG_POS not in result.arg_kinds[skip_self:]: + self.fail( + "TypeNarrower functions must have a positional argument", + result, + code=codes.VALID_TYPE, + ) + result = result.copy_modified(type_narrower=None) result = self.remove_unpack_kwargs(defn, result) if has_self_type and self.type is not None: diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 2d536f892a2a..4d1565f23ca3 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -683,10 +683,17 @@ def visit_callable_type(self, left: CallableType) -> bool: if left.type_guard is not None and right.type_guard is not None: if not self._is_subtype(left.type_guard, right.type_guard): return False + elif left.type_narrower is not None and right.type_narrower is not None: + if not self._is_subtype(left.type_narrower, right.type_narrower): + return False elif right.type_guard is not None and left.type_guard is None: # This means that one function has `TypeGuard` and other does not. # They are not compatible. See https://github.com/python/mypy/issues/11307 return False + elif right.type_narrower is not None and left.type_narrower is not None: + # Similarly, if one function has typeNarrower and the other does not, + # they are not compatible. + return False return is_callable_compatible( left, right, diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 530793730f35..8db6786eb5d2 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -663,7 +663,7 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ ) return AnyType(TypeOfAny.from_error) return RequiredType(self.anal_type(t.args[0]), required=False) - elif self.anal_type_guard_arg(t, fullname) is not None: + elif self.anal_type_guard_arg(t, fullname) is not None or self.anal_type_narrower_arg(t, fullname) is not None: # In most contexts, TypeGuard[...] acts as an alias for bool (ignoring its args) return self.named_type("builtins.bool") elif fullname in ("typing.Unpack", "typing_extensions.Unpack"): @@ -981,7 +981,8 @@ def visit_callable_type(self, t: CallableType, nested: bool = True) -> Type: variables = t.variables else: variables, _ = self.bind_function_type_variables(t, t) - special = self.anal_type_guard(t.ret_type) + type_guard = self.anal_type_guard(t.ret_type) + type_narrower = self.anal_type_narrower(t.ret_type) arg_kinds = t.arg_kinds if len(arg_kinds) >= 2 and arg_kinds[-2] == ARG_STAR and arg_kinds[-1] == ARG_STAR2: arg_types = self.anal_array(t.arg_types[:-2], nested=nested) + [ @@ -1036,7 +1037,8 @@ def visit_callable_type(self, t: CallableType, nested: bool = True) -> Type: # its type will be the falsey FakeInfo fallback=(t.fallback if t.fallback.type else self.named_type("builtins.function")), variables=self.anal_var_defs(variables), - type_guard=special, + type_guard=type_guard, + type_narrower=type_narrower, unpack_kwargs=unpacked_kwargs, ) return ret @@ -1059,6 +1061,24 @@ def anal_type_guard_arg(self, t: UnboundType, fullname: str) -> Type | None: return self.anal_type(t.args[0]) return None + def anal_type_narrower(self, t: Type) -> Type | None: + if isinstance(t, UnboundType): + sym = self.lookup_qualified(t.name, t) + if sym is not None and sym.node is not None: + return self.anal_type_narrower_arg(t, sym.node.fullname) + # TODO: What if it's an Instance? Then use t.type.fullname? + return None + + def anal_type_narrower_arg(self, t: UnboundType, fullname: str) -> Type | None: + if fullname in ("typing_extensions.TypeNarrower", "typing.TypeNarrower"): + if len(t.args) != 1: + self.fail( + "TypeNarrower must have exactly one type argument", t, code=codes.VALID_TYPE + ) + return AnyType(TypeOfAny.from_error) + return self.anal_type(t.args[0]) + return None + def anal_star_arg_type(self, t: Type, kind: ArgKind, nested: bool) -> Type: """Analyze signature argument type for *args and **kwargs argument.""" if isinstance(t, UnboundType) and t.name and "." in t.name and not t.args: diff --git a/mypy/types.py b/mypy/types.py index b1119c9447e2..47ad065c88db 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -449,6 +449,19 @@ def __repr__(self) -> str: return f"TypeGuard({self.type_guard})" +class TypeNarrowerType(Type): + """Only used by find_isinstance_check() etc.""" + + __slots__ = ("type_narrower",) + + def __init__(self, type_narrower: Type) -> None: + super().__init__(line=type_narrower.line, column=type_narrower.column) + self.type_narrower = type_narrower + + def __repr__(self) -> str: + return f"TypeNarrower({self.type_narrower})" + + class RequiredType(Type): """Required[T] or NotRequired[T]. Only usable at top-level of a TypedDict definition.""" @@ -1791,6 +1804,7 @@ class CallableType(FunctionLike): "def_extras", # Information about original definition we want to serialize. # This is used for more detailed error messages. "type_guard", # T, if -> TypeGuard[T] (ret_type is bool in this case). + "type_narrower", # T, if -> TypeNarrower[T] (ret_type is bool in this case). "from_concatenate", # whether this callable is from a concatenate object # (this is used for error messages) "imprecise_arg_kinds", @@ -1817,6 +1831,7 @@ def __init__( bound_args: Sequence[Type | None] = (), def_extras: dict[str, Any] | None = None, type_guard: Type | None = None, + type_narrower: Type | None = None, from_concatenate: bool = False, imprecise_arg_kinds: bool = False, unpack_kwargs: bool = False, @@ -1866,6 +1881,7 @@ def __init__( else: self.def_extras = {} self.type_guard = type_guard + self.type_narrower = type_narrower self.unpack_kwargs = unpack_kwargs def copy_modified( @@ -1887,6 +1903,7 @@ def copy_modified( bound_args: Bogus[list[Type | None]] = _dummy, def_extras: Bogus[dict[str, Any]] = _dummy, type_guard: Bogus[Type | None] = _dummy, + type_narrower: Bogus[Type | None] = _dummy, from_concatenate: Bogus[bool] = _dummy, imprecise_arg_kinds: Bogus[bool] = _dummy, unpack_kwargs: Bogus[bool] = _dummy, @@ -1911,6 +1928,7 @@ def copy_modified( bound_args=bound_args if bound_args is not _dummy else self.bound_args, def_extras=def_extras if def_extras is not _dummy else dict(self.def_extras), type_guard=type_guard if type_guard is not _dummy else self.type_guard, + type_narrower=type_narrower if type_narrower is not _dummy else self.type_narrower, from_concatenate=( from_concatenate if from_concatenate is not _dummy else self.from_concatenate ), @@ -2224,6 +2242,7 @@ def serialize(self) -> JsonDict: "bound_args": [(None if t is None else t.serialize()) for t in self.bound_args], "def_extras": dict(self.def_extras), "type_guard": self.type_guard.serialize() if self.type_guard is not None else None, + "type_narrower": self.type_narrower.serialize() if self.type_narrower is not None else None, "from_concatenate": self.from_concatenate, "imprecise_arg_kinds": self.imprecise_arg_kinds, "unpack_kwargs": self.unpack_kwargs, @@ -2248,6 +2267,9 @@ def deserialize(cls, data: JsonDict) -> CallableType: type_guard=( deserialize_type(data["type_guard"]) if data["type_guard"] is not None else None ), + type_narrower=( + deserialize_type(data["type_narrower"]) if data["type_narrower"] is not None else None + ), from_concatenate=data["from_concatenate"], imprecise_arg_kinds=data["imprecise_arg_kinds"], unpack_kwargs=data["unpack_kwargs"], @@ -3072,6 +3094,8 @@ def get_proper_type(typ: Type | None) -> ProperType | None: return None if isinstance(typ, TypeGuardedType): # type: ignore[misc] typ = typ.type_guard + if isinstance(typ, TypeNarrowerType): # type: ignore[misc] + typ = typ.type_narrower while isinstance(typ, TypeAliasType): typ = typ._expand_once() # TODO: store the name of original type alias on this type, so we can show it in errors. @@ -3096,7 +3120,7 @@ def get_proper_types( typelist = types # Optimize for the common case so that we don't need to allocate anything if not any( - isinstance(t, (TypeAliasType, TypeGuardedType)) for t in typelist # type: ignore[misc] + isinstance(t, (TypeAliasType, TypeGuardedType, TypeNarrowerType)) for t in typelist # type: ignore[misc] ): return cast("list[ProperType]", typelist) return [get_proper_type(t) for t in typelist] @@ -3306,6 +3330,8 @@ def visit_callable_type(self, t: CallableType) -> str: if not isinstance(get_proper_type(t.ret_type), NoneType): if t.type_guard is not None: s += f" -> TypeGuard[{t.type_guard.accept(self)}]" + elif t.type_narrower is not None: + s += f" -> TypeNarrower[{t.type_narrower.accept(self)}]" else: s += f" -> {t.ret_type.accept(self)}" From 58e840308fce788cf6effc6f300af3980e86ad32 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 8 Feb 2024 18:08:00 -0800 Subject: [PATCH 02/27] Initial tests (many failing) --- mypy/subtypes.py | 6 +- test-data/unit/check-typenarrower.test | 686 ++++++++++++++++++ test-data/unit/lib-stub/typing_extensions.pyi | 1 + 3 files changed, 692 insertions(+), 1 deletion(-) create mode 100644 test-data/unit/check-typenarrower.test diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 4d1565f23ca3..b33b0b034861 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -684,7 +684,11 @@ def visit_callable_type(self, left: CallableType) -> bool: if not self._is_subtype(left.type_guard, right.type_guard): return False elif left.type_narrower is not None and right.type_narrower is not None: - if not self._is_subtype(left.type_narrower, right.type_narrower): + # For TypeNarrower we have to check both ways; it is unsafe to pass + # a TypeNarrower[Child] when a TypeNarrower[Parent] is expected, because + # if the narrower returns False, we assume that the narrowed value is + # *not* a Parent. + if not self._is_subtype(left.type_narrower, right.type_narrower) or not self._is_subtype(right.type_narrower, left.type_narrower): return False elif right.type_guard is not None and left.type_guard is None: # This means that one function has `TypeGuard` and other does not. diff --git a/test-data/unit/check-typenarrower.test b/test-data/unit/check-typenarrower.test new file mode 100644 index 000000000000..3dd8f578f644 --- /dev/null +++ b/test-data/unit/check-typenarrower.test @@ -0,0 +1,686 @@ +[case testTypeNarrowerBasic] +from typing_extensions import TypeNarrower +class Point: pass +def is_point(a: object) -> TypeNarrower[Point]: pass +def main(a: object) -> None: + if is_point(a): + reveal_type(a) # N: Revealed type is "__main__.Point" + else: + reveal_type(a) # N: Revealed type is "builtins.object" +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerTypeArgsNone] +from typing_extensions import TypeNarrower +def foo(a: object) -> TypeNarrower: # E: TypeNarrower must have exactly one type argument + pass +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerTypeArgsTooMany] +from typing_extensions import TypeNarrower +def foo(a: object) -> TypeNarrower[int, int]: # E: TypeNarrower must have exactly one type argument + pass +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerTypeArgType] +from typing_extensions import TypeNarrower +def foo(a: object) -> TypeNarrower[42]: # E: Invalid type: try using Literal[42] instead? + pass +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerRepr] +from typing_extensions import TypeNarrower +def foo(a: object) -> TypeNarrower[int]: + pass +reveal_type(foo) # N: Revealed type is "def (a: builtins.object) -> TypeNarrower[builtins.int]" +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerCallArgsNone] +from typing_extensions import TypeNarrower +class Point: pass + +def is_point() -> TypeNarrower[Point]: pass # E: TypeNarrower functions must have a positional argument +def main(a: object) -> None: + if is_point(): + reveal_type(a) # N: Revealed type is "builtins.object" +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerCallArgsMultiple] +from typing_extensions import TypeNarrower +class Point: pass +def is_point(a: object, b: object) -> TypeNarrower[Point]: pass +def main(a: object, b: object) -> None: + if is_point(a, b): + reveal_type(a) # N: Revealed type is "__main__.Point" + reveal_type(b) # N: Revealed type is "builtins.object" +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerIsBool] +from typing_extensions import TypeNarrower +def f(a: TypeNarrower[int]) -> None: pass +reveal_type(f) # N: Revealed type is "def (a: builtins.bool)" +a: TypeNarrower[int] +reveal_type(a) # N: Revealed type is "builtins.bool" +class C: + a: TypeNarrower[int] +reveal_type(C().a) # N: Revealed type is "builtins.bool" +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerWithTypeVar] +from typing import TypeVar, Tuple, Type +from typing_extensions import TypeNarrower +T = TypeVar('T') +def is_tuple_of_type(a: Tuple[object, ...], typ: Type[T]) -> TypeNarrower[Tuple[T, ...]]: pass +def main(a: Tuple[object, ...]): + if is_tuple_of_type(a, int): + reveal_type(a) # N: Revealed type is "Tuple[int, ...]" +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerUnionIn] +from typing import Union +from typing_extensions import TypeNarrower +def is_foo(a: Union[int, str]) -> TypeNarrower[str]: pass +def main(a: Union[str, int]) -> None: + if is_foo(a): + reveal_type(a) # N: Revealed type is "builtins.str" + else: + reveal_type(a) # N: Revealed type is "builtins.int" + reveal_type(a) # N: Revealed type is "Union[builtins.str, builtins.int]" +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerUnionOut] +from typing import Union +from typing_extensions import TypeNarrower +def is_foo(a: object) -> TypeNarrower[Union[int, str]]: pass +def main(a: object) -> None: + if is_foo(a): + reveal_type(a) # N: Revealed type is "Union[builtins.int, builtins.str]" +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerNonzeroFloat] +from typing_extensions import TypeNarrower +def is_nonzero(a: object) -> TypeNarrower[float]: pass +def main(a: int): + if is_nonzero(a): + reveal_type(a) # N: Revealed type is "builtins.float" +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerHigherOrder] +from typing import Callable, TypeVar, Iterable, List +from typing_extensions import TypeNarrower +T = TypeVar('T') +R = TypeVar('R') +def filter(f: Callable[[T], TypeNarrower[R]], it: Iterable[T]) -> Iterable[R]: pass +def is_float(a: object) -> TypeNarrower[float]: pass +a: List[object] = ["a", 0, 0.0] +b = filter(is_float, a) +reveal_type(b) # N: Revealed type is "typing.Iterable[builtins.float]" +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerMethod] +from typing_extensions import TypeNarrower +class C: + def main(self, a: object) -> None: + if self.is_float(a): + reveal_type(self) # N: Revealed type is "__main__.C" + reveal_type(a) # N: Revealed type is "builtins.float" + def is_float(self, a: object) -> TypeNarrower[float]: pass +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerCrossModule] +import guard +from points import Point +def main(a: object) -> None: + if guard.is_point(a): + reveal_type(a) # N: Revealed type is "points.Point" +[file guard.py] +from typing_extensions import TypeNarrower +import points +def is_point(a: object) -> TypeNarrower[points.Point]: pass +[file points.py] +class Point: pass +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerBodyRequiresBool] +from typing_extensions import TypeNarrower +def is_float(a: object) -> TypeNarrower[float]: + return "not a bool" # E: Incompatible return value type (got "str", expected "bool") +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerNarrowToTypedDict] +from typing import Mapping, TypedDict +from typing_extensions import TypeNarrower +class User(TypedDict): + name: str + id: int +def is_user(a: Mapping[str, object]) -> TypeNarrower[User]: + return isinstance(a.get("name"), str) and isinstance(a.get("id"), int) +def main(a: Mapping[str, object]) -> None: + if is_user(a): + reveal_type(a) # N: Revealed type is "TypedDict('__main__.User', {'name': builtins.str, 'id': builtins.int})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypeNarrowerInAssert] +from typing_extensions import TypeNarrower +def is_float(a: object) -> TypeNarrower[float]: pass +def main(a: object) -> None: + assert is_float(a) + reveal_type(a) # N: Revealed type is "builtins.float" +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerFromAny] +from typing import Any +from typing_extensions import TypeNarrower +def is_objfloat(a: object) -> TypeNarrower[float]: pass +def is_anyfloat(a: Any) -> TypeNarrower[float]: pass +def objmain(a: object) -> None: + if is_objfloat(a): + reveal_type(a) # N: Revealed type is "builtins.float" + if is_anyfloat(a): + reveal_type(a) # N: Revealed type is "builtins.float" +def anymain(a: Any) -> None: + if is_objfloat(a): + reveal_type(a) # N: Revealed type is "builtins.float" + if is_anyfloat(a): + reveal_type(a) # N: Revealed type is "builtins.float" +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerNegatedAndElse] +from typing import Union +from typing_extensions import TypeNarrower +def is_int(a: object) -> TypeNarrower[int]: pass +def is_str(a: object) -> TypeNarrower[str]: pass +def intmain(a: Union[int, str]) -> None: + if not is_int(a): + reveal_type(a) # N: Revealed type is "builtins.str" + else: + reveal_type(a) # N: Revealed type is "builtins.int" +def strmain(a: Union[int, str]) -> None: + if is_str(a): + reveal_type(a) # N: Revealed type is "builtins.str" + else: + reveal_type(a) # N: Revealed type is "builtins.int" +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerClassMethod] +from typing_extensions import TypeNarrower +class C: + @classmethod + def is_float(cls, a: object) -> TypeNarrower[float]: pass + def method(self, a: object) -> None: + if self.is_float(a): + reveal_type(a) # N: Revealed type is "builtins.float" +def main(a: object) -> None: + if C.is_float(a): + reveal_type(a) # N: Revealed type is "builtins.float" +[builtins fixtures/classmethod.pyi] + +[case testTypeNarrowerRequiresPositionalArgs] +from typing_extensions import TypeNarrower +def is_float(a: object, b: object = 0) -> TypeNarrower[float]: pass +def main1(a: object) -> None: + if is_float(a=a, b=1): + reveal_type(a) # N: Revealed type is "builtins.float" + + if is_float(b=1, a=a): + reveal_type(a) # N: Revealed type is "builtins.float" + +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerOverload] +from typing import overload, Any, Callable, Iterable, Iterator, List, Optional, TypeVar +from typing_extensions import TypeNarrower + +T = TypeVar("T") +R = TypeVar("R") + +@overload +def filter(f: Callable[[T], TypeNarrower[R]], it: Iterable[T]) -> Iterator[R]: ... +@overload +def filter(f: Callable[[T], bool], it: Iterable[T]) -> Iterator[T]: ... +def filter(*args): pass + +def is_int_typeguard(a: object) -> TypeNarrower[int]: pass +def is_int_bool(a: object) -> bool: pass + +def main(a: List[Optional[int]]) -> None: + bb = filter(lambda x: x is not None, a) + reveal_type(bb) # N: Revealed type is "typing.Iterator[Union[builtins.int, None]]" + # Also, if you replace 'bool' with 'Any' in the second overload, bb is Iterator[Any] + cc = filter(is_int_typeguard, a) + reveal_type(cc) # N: Revealed type is "typing.Iterator[builtins.int]" + dd = filter(is_int_bool, a) + reveal_type(dd) # N: Revealed type is "typing.Iterator[Union[builtins.int, None]]" + +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testTypeNarrowerDecorated] +from typing import TypeVar +from typing_extensions import TypeNarrower +T = TypeVar("T") +def decorator(f: T) -> T: pass +@decorator +def is_float(a: object) -> TypeNarrower[float]: + pass +def main(a: object) -> None: + if is_float(a): + reveal_type(a) # N: Revealed type is "builtins.float" +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerMethodOverride] +from typing_extensions import TypeNarrower +class C: + def is_float(self, a: object) -> TypeNarrower[float]: pass +class D(C): + def is_float(self, a: object) -> bool: pass # Fail +[builtins fixtures/tuple.pyi] +[out] +main:5: error: Signature of "is_float" incompatible with supertype "C" +main:5: note: Superclass: +main:5: note: def is_float(self, a: object) -> TypeNarrower[float] +main:5: note: Subclass: +main:5: note: def is_float(self, a: object) -> bool + +[case testTypeNarrowerInAnd] +from typing import Any +from typing_extensions import TypeNarrower +import types +def isclass(a: object) -> bool: + pass +def ismethod(a: object) -> TypeNarrower[float]: + pass +def isfunction(a: object) -> TypeNarrower[str]: + pass +def isclassmethod(obj: Any) -> bool: + if ismethod(obj) and obj.__self__ is not None and isclass(obj.__self__): # E: "float" has no attribute "__self__" + return True + + return False +def coverage(obj: Any) -> bool: + if not (ismethod(obj) or isfunction(obj)): + return True + return False +[builtins fixtures/classmethod.pyi] + +[case testAssignToTypeNarroweredVariable1] +from typing_extensions import TypeNarrower + +class A: pass +class B(A): pass + +def guard(a: A) -> TypeNarrower[B]: + pass + +a = A() +if not guard(a): + a = A() +[builtins fixtures/tuple.pyi] + +[case testAssignToTypeNarroweredVariable2] +from typing_extensions import TypeNarrower + +class A: pass +class B: pass + +def guard(a: A) -> TypeNarrower[B]: + pass + +a = A() +if not guard(a): + a = A() +[builtins fixtures/tuple.pyi] + +[case testAssignToTypeNarroweredVariable3] +from typing_extensions import TypeNarrower + +class A: pass +class B: pass + +def guard(a: A) -> TypeNarrower[B]: + pass + +a = A() +if guard(a): + reveal_type(a) # N: Revealed type is "__main__.B" + a = B() # E: Incompatible types in assignment (expression has type "B", variable has type "A") + reveal_type(a) # N: Revealed type is "__main__.B" + a = A() + reveal_type(a) # N: Revealed type is "__main__.A" +reveal_type(a) # N: Revealed type is "__main__.A" +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerNestedRestrictionAny] +from typing_extensions import TypeNarrower +from typing import Any + +class A: ... +def f(x: object) -> TypeNarrower[A]: ... +def g(x: object) -> None: ... + +def test(x: Any) -> None: + if not(f(x) or x): + return + g(reveal_type(x)) # N: Revealed type is "Union[__main__.A, Any]" +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerNestedRestrictionUnionOther] +from typing_extensions import TypeNarrower +from typing import Any + +class A: ... +class B: ... +def f(x: object) -> TypeNarrower[A]: ... +def f2(x: object) -> TypeNarrower[B]: ... +def g(x: object) -> None: ... + +def test(x: object) -> None: + if not(f(x) or f2(x)): + return + g(reveal_type(x)) # N: Revealed type is "Union[__main__.A, __main__.B]" +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerComprehensionSubtype] +from typing import List +from typing_extensions import TypeNarrower + +class Base: ... +class Foo(Base): ... +class Bar(Base): ... + +def is_foo(item: object) -> TypeNarrower[Foo]: + return isinstance(item, Foo) + +def is_bar(item: object) -> TypeNarrower[Bar]: + return isinstance(item, Bar) + +def foobar(items: List[object]): + a: List[Base] = [x for x in items if is_foo(x) or is_bar(x)] + b: List[Base] = [x for x in items if is_foo(x)] + c: List[Bar] = [x for x in items if is_foo(x)] # E: List comprehension has incompatible type List[Foo]; expected List[Bar] +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerNestedRestrictionUnionIsInstance] +from typing_extensions import TypeNarrower +from typing import Any, List + +class A: ... +def f(x: List[Any]) -> TypeNarrower[List[str]]: ... +def g(x: object) -> None: ... + +def test(x: List[Any]) -> None: + if not(f(x) or isinstance(x, A)): + return + g(reveal_type(x)) # N: Revealed type is "Union[builtins.list[builtins.str], __main__.]" +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerMultipleCondition-xfail] +from typing_extensions import TypeNarrower +from typing import Any, List + +class Foo: ... +class Bar: ... + +def is_foo(item: object) -> TypeNarrower[Foo]: + return isinstance(item, Foo) + +def is_bar(item: object) -> TypeNarrower[Bar]: + return isinstance(item, Bar) + +def foobar(x: object): + if not isinstance(x, Foo) or not isinstance(x, Bar): + return + reveal_type(x) # N: Revealed type is "__main__." + +def foobar_typeguard(x: object): + if not is_foo(x) or not is_bar(x): + return + reveal_type(x) # N: Revealed type is "__main__." +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerAsFunctionArgAsBoolSubtype] +from typing import Callable +from typing_extensions import TypeNarrower + +def accepts_bool(f: Callable[[object], bool]): pass + +def with_bool_typeguard(o: object) -> TypeNarrower[bool]: pass +def with_str_typeguard(o: object) -> TypeNarrower[str]: pass +def with_bool(o: object) -> bool: pass + +accepts_bool(with_bool_typeguard) +accepts_bool(with_str_typeguard) +accepts_bool(with_bool) +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerAsFunctionArg] +from typing import Callable +from typing_extensions import TypeNarrower + +def accepts_typeguard(f: Callable[[object], TypeNarrower[bool]]): pass +def different_typeguard(f: Callable[[object], TypeNarrower[str]]): pass + +def with_typeguard(o: object) -> TypeNarrower[bool]: pass +def with_bool(o: object) -> bool: pass + +accepts_typeguard(with_typeguard) +accepts_typeguard(with_bool) # E: Argument 1 to "accepts_typeguard" has incompatible type "Callable[[object], bool]"; expected "Callable[[object], TypeNarrower[bool]]" + +different_typeguard(with_typeguard) # E: Argument 1 to "different_typeguard" has incompatible type "Callable[[object], TypeNarrower[bool]]"; expected "Callable[[object], TypeNarrower[str]]" +different_typeguard(with_bool) # E: Argument 1 to "different_typeguard" has incompatible type "Callable[[object], bool]"; expected "Callable[[object], TypeNarrower[str]]" +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerAsGenericFunctionArg] +from typing import Callable, TypeVar +from typing_extensions import TypeNarrower + +T = TypeVar('T') + +def accepts_typeguard(f: Callable[[object], TypeNarrower[T]]): pass + +def with_bool_typeguard(o: object) -> TypeNarrower[bool]: pass +def with_str_typeguard(o: object) -> TypeNarrower[str]: pass +def with_bool(o: object) -> bool: pass + +accepts_typeguard(with_bool_typeguard) +accepts_typeguard(with_str_typeguard) +accepts_typeguard(with_bool) # E: Argument 1 to "accepts_typeguard" has incompatible type "Callable[[object], bool]"; expected "Callable[[object], TypeNarrower[bool]]" +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerAsOverloadedFunctionArg] +# https://github.com/python/mypy/issues/11307 +from typing import Callable, TypeVar, Generic, Any, overload +from typing_extensions import TypeNarrower + +_T = TypeVar('_T') + +class filter(Generic[_T]): + @overload + def __init__(self, function: Callable[[object], TypeNarrower[_T]]) -> None: pass + @overload + def __init__(self, function: Callable[[_T], Any]) -> None: pass + def __init__(self, function): pass + +def is_int_typeguard(a: object) -> TypeNarrower[int]: pass +def returns_bool(a: object) -> bool: pass + +reveal_type(filter(is_int_typeguard)) # N: Revealed type is "__main__.filter[builtins.int]" +reveal_type(filter(returns_bool)) # N: Revealed type is "__main__.filter[builtins.object]" +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerSubtypingVariance] +from typing import Callable +from typing_extensions import TypeNarrower + +class A: pass +class B(A): pass +class C(B): pass + +def accepts_typeguard(f: Callable[[object], TypeNarrower[B]]): pass + +def with_typeguard_a(o: object) -> TypeNarrower[A]: pass +def with_typeguard_b(o: object) -> TypeNarrower[B]: pass +def with_typeguard_c(o: object) -> TypeNarrower[C]: pass + +accepts_typeguard(with_typeguard_a) # E: Argument 1 to "accepts_typeguard" has incompatible type "Callable[[object], TypeNarrower[A]]"; expected "Callable[[object], TypeNarrower[B]]" +accepts_typeguard(with_typeguard_b) +accepts_typeguard(with_typeguard_c) # E: Argument 1 to "accepts_typeguard" has incompatible type "Callable[[object], TypeNarrower[C]]"; expected "Callable[[object], TypeNarrower[B]]" +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerWithIdentityGeneric] +from typing import TypeVar +from typing_extensions import TypeNarrower + +_T = TypeVar("_T") + +def identity(val: _T) -> TypeNarrower[_T]: + pass + +def func1(name: _T): + reveal_type(name) # N: Revealed type is "_T`-1" + if identity(name): + reveal_type(name) # N: Revealed type is "_T`-1" + +def func2(name: str): + reveal_type(name) # N: Revealed type is "builtins.str" + if identity(name): + reveal_type(name) # N: Revealed type is "builtins.str" +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerWithGenericInstance] +from typing import TypeVar, List +from typing_extensions import TypeNarrower + +_T = TypeVar("_T") + +def is_list_of_str(val: _T) -> TypeNarrower[List[_T]]: + pass + +def func(name: str): + reveal_type(name) # N: Revealed type is "builtins.str" + if is_list_of_str(name): + reveal_type(name) # N: Revealed type is "builtins.list[builtins.str]" +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerWithTupleGeneric] +from typing import TypeVar, Tuple +from typing_extensions import TypeNarrower + +_T = TypeVar("_T") + +def is_two_element_tuple(val: Tuple[_T, ...]) -> TypeNarrower[Tuple[_T, _T]]: + pass + +def func(names: Tuple[str, ...]): + reveal_type(names) # N: Revealed type is "builtins.tuple[builtins.str, ...]" + if is_two_element_tuple(names): + reveal_type(names) # N: Revealed type is "Tuple[builtins.str, builtins.str]" +[builtins fixtures/tuple.pyi] + +[case testTypeNarrowerErroneousDefinitionFails] +from typing_extensions import TypeNarrower + +class Z: + def typeguard1(self, *, x: object) -> TypeNarrower[int]: # E: TypeNarrower functions must have a positional argument + ... + + @staticmethod + def typeguard2(x: object) -> TypeNarrower[int]: + ... + + @staticmethod # E: TypeNarrower functions must have a positional argument + def typeguard3(*, x: object) -> TypeNarrower[int]: + ... + +def bad_typeguard(*, x: object) -> TypeNarrower[int]: # E: TypeNarrower functions must have a positional argument + ... + +[builtins fixtures/classmethod.pyi] + +[case testTypeNarrowerWithKeywordArg] +from typing_extensions import TypeNarrower + +class Z: + def typeguard(self, x: object) -> TypeNarrower[int]: + ... + +def typeguard(x: object) -> TypeNarrower[int]: + ... + +n: object +if typeguard(x=n): + reveal_type(n) # N: Revealed type is "builtins.int" + +if Z().typeguard(x=n): + reveal_type(n) # N: Revealed type is "builtins.int" +[builtins fixtures/tuple.pyi] + +[case testStaticMethodTypeNarrower] +from typing_extensions import TypeNarrower + +class Y: + @staticmethod + def typeguard(h: object) -> TypeNarrower[int]: + ... + +x: object +if Y().typeguard(x): + reveal_type(x) # N: Revealed type is "builtins.int" +if Y.typeguard(x): + reveal_type(x) # N: Revealed type is "builtins.int" +[builtins fixtures/classmethod.pyi] + +[case testTypeNarrowerKwargFollowingThroughOverloaded] +from typing import overload, Union +from typing_extensions import TypeNarrower + +@overload +def typeguard(x: object, y: str) -> TypeNarrower[str]: + ... + +@overload +def typeguard(x: object, y: int) -> TypeNarrower[int]: + ... + +def typeguard(x: object, y: Union[int, str]) -> Union[TypeNarrower[int], TypeNarrower[str]]: + ... + +x: object +if typeguard(x=x, y=42): + reveal_type(x) # N: Revealed type is "builtins.int" + +if typeguard(y=42, x=x): + reveal_type(x) # N: Revealed type is "builtins.int" + +if typeguard(x=x, y="42"): + reveal_type(x) # N: Revealed type is "builtins.str" + +if typeguard(y="42", x=x): + reveal_type(x) # N: Revealed type is "builtins.str" +[builtins fixtures/tuple.pyi] + +[case testGenericAliasWithTypeNarrower] +from typing import Callable, List, TypeVar +from typing_extensions import TypeNarrower, TypeAlias + +A = Callable[[object], TypeNarrower[List[T]]] +def foo(x: object) -> TypeNarrower[List[str]]: ... + +def test(f: A[T]) -> T: ... +reveal_type(test(foo)) # N: Revealed type is "builtins.str" +[builtins fixtures/list.pyi] + +[case testNoCrashOnDunderCallTypeNarrower] +from typing_extensions import TypeNarrower + +class A: + def __call__(self, x) -> TypeNarrower[int]: + return True + +a: A +assert a(x=1) + +x: object +assert a(x=x) +reveal_type(x) # N: Revealed type is "builtins.int" +[builtins fixtures/tuple.pyi] diff --git a/test-data/unit/lib-stub/typing_extensions.pyi b/test-data/unit/lib-stub/typing_extensions.pyi index 7aca6fad1b42..65694ae63b86 100644 --- a/test-data/unit/lib-stub/typing_extensions.pyi +++ b/test-data/unit/lib-stub/typing_extensions.pyi @@ -34,6 +34,7 @@ Concatenate: _SpecialForm TypeAlias: _SpecialForm TypeGuard: _SpecialForm +TypeNarrower: _SpecialForm Never: _SpecialForm TypeVarTuple: _SpecialForm From c8d2af89010225f42def25a5d8320cd2af8862da Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 9 Feb 2024 16:02:56 -0800 Subject: [PATCH 03/27] Fewer test failures --- mypy/constraints.py | 5 ++++- mypy/meet.py | 6 ++++-- mypy/subtypes.py | 2 +- test-data/unit/check-typenarrower.test | 8 ++++---- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/mypy/constraints.py b/mypy/constraints.py index a79dda9e078b..9922d25cc4df 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -1018,11 +1018,14 @@ def visit_callable_type(self, template: CallableType) -> list[Constraint]: param_spec = template.param_spec() template_ret_type, cactual_ret_type = template.ret_type, cactual.ret_type - # TODO(jelle): TypeNarrower if template.type_guard is not None: template_ret_type = template.type_guard + elif template.type_narrower is not None: + template_ret_type = template.type_narrower if cactual.type_guard is not None: cactual_ret_type = cactual.type_guard + elif cactual.type_narrower is not None: + cactual_ret_type = cactual.type_narrower res.extend(infer_constraints(template_ret_type, cactual_ret_type, self.direction)) if param_spec is None: diff --git a/mypy/meet.py b/mypy/meet.py index 73c45718ee6b..e7767b05bd0e 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -33,6 +33,7 @@ TupleType, Type, TypeAliasType, + TypeNarrowerType, TypedDictType, TypeGuardedType, TypeOfAny, @@ -114,8 +115,9 @@ def narrow_declared_type(declared: Type, narrowed: Type) -> Type: # TODO: check infinite recursion for aliases here. if isinstance(narrowed, TypeGuardedType): # type: ignore[misc] # A type guard forces the new type even if it doesn't overlap the old. - return narrowed.type_guard - # TODO(jelle): TypeNarrower + return narrowed + elif isinstance(narrowed, TypeNarrowerType): + return narrow_declared_type(declared, narrowed.type_narrower) original_declared = declared original_narrowed = narrowed diff --git a/mypy/subtypes.py b/mypy/subtypes.py index b33b0b034861..e4dcdea15300 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -694,7 +694,7 @@ def visit_callable_type(self, left: CallableType) -> bool: # This means that one function has `TypeGuard` and other does not. # They are not compatible. See https://github.com/python/mypy/issues/11307 return False - elif right.type_narrower is not None and left.type_narrower is not None: + elif right.type_narrower is not None and left.type_narrower is None: # Similarly, if one function has typeNarrower and the other does not, # they are not compatible. return False diff --git a/test-data/unit/check-typenarrower.test b/test-data/unit/check-typenarrower.test index 3dd8f578f644..868603dcf0db 100644 --- a/test-data/unit/check-typenarrower.test +++ b/test-data/unit/check-typenarrower.test @@ -559,7 +559,7 @@ def is_list_of_str(val: _T) -> TypeNarrower[List[_T]]: def func(name: str): reveal_type(name) # N: Revealed type is "builtins.str" if is_list_of_str(name): - reveal_type(name) # N: Revealed type is "builtins.list[builtins.str]" + reveal_type(name) # N: Revealed type is "Never" [builtins fixtures/tuple.pyi] [case testTypeNarrowerWithTupleGeneric] @@ -588,11 +588,11 @@ class Z: def typeguard2(x: object) -> TypeNarrower[int]: ... - @staticmethod # E: TypeNarrower functions must have a positional argument - def typeguard3(*, x: object) -> TypeNarrower[int]: + @staticmethod + def typeguard3(*, x: object) -> TypeNarrower[int]: # E: TypeNarrower functions must have a positional argument ... -def bad_typeguard(*, x: object) -> TypeNarrower[int]: # E: TypeNarrower functions must have a positional argument +def bad_typeguard(*, x: object) -> TypeNarrower[int]: # E: TypeNarrower functions must have a positional argument ... [builtins fixtures/classmethod.pyi] From f20591037352dd1e50b4ba46a7898e1f0ea0dece Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 9 Feb 2024 16:24:24 -0800 Subject: [PATCH 04/27] Fix the remaining tests --- mypy/checker.py | 17 ++++++++++++++--- test-data/unit/check-typenarrower.test | 10 +++++----- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index a9b499a24b9b..9a7ae81477ff 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5710,8 +5710,12 @@ def find_isinstance_check_helper(self, node: Expression) -> tuple[TypeMap, TypeM # we want the idx-th variable to be narrowed expr = collapse_walrus(node.args[idx]) else: - kind = "guard" if node.callee.type_guard is not None else "narrower" - self.fail(message_registry.TYPE_GUARD_POS_ARG_REQUIRED.format(kind), node) + kind = ( + "guard" if node.callee.type_guard is not None else "narrower" + ) + self.fail( + message_registry.TYPE_GUARD_POS_ARG_REQUIRED.format(kind), node + ) return {}, {} if literal(expr) == LITERAL_TYPE: # Note: we wrap the target type, so that we can special case later. @@ -5722,7 +5726,14 @@ def find_isinstance_check_helper(self, node: Expression) -> tuple[TypeMap, TypeM if node.callee.type_guard is not None: guard_type = TypeGuardedType(node.callee.type_guard) else: - guard_type = TypeNarrowerType(node.callee.type_narrower) + return conditional_types_to_typemaps( + expr, + *self.conditional_types_with_intersection( + self.lookup_type(expr), + [TypeRange(node.callee.type_narrower, is_upper_bound=False)], + expr, + ), + ) return {expr: guard_type}, {} elif isinstance(node, ComparisonExpr): # Step 1: Obtain the types of each operand and whether or not we can diff --git a/test-data/unit/check-typenarrower.test b/test-data/unit/check-typenarrower.test index 868603dcf0db..27cd66a499f4 100644 --- a/test-data/unit/check-typenarrower.test +++ b/test-data/unit/check-typenarrower.test @@ -72,7 +72,7 @@ T = TypeVar('T') def is_tuple_of_type(a: Tuple[object, ...], typ: Type[T]) -> TypeNarrower[Tuple[T, ...]]: pass def main(a: Tuple[object, ...]): if is_tuple_of_type(a, int): - reveal_type(a) # N: Revealed type is "Tuple[int, ...]" + reveal_type(a) # N: Revealed type is "builtins.tuple[builtins.int, ...]" [builtins fixtures/tuple.pyi] [case testTypeNarrowerUnionIn] @@ -101,7 +101,7 @@ from typing_extensions import TypeNarrower def is_nonzero(a: object) -> TypeNarrower[float]: pass def main(a: int): if is_nonzero(a): - reveal_type(a) # N: Revealed type is "builtins.float" + reveal_type(a) # N: Revealed type is "builtins.int" [builtins fixtures/tuple.pyi] [case testTypeNarrowerHigherOrder] @@ -342,9 +342,9 @@ def guard(a: A) -> TypeNarrower[B]: a = A() if guard(a): - reveal_type(a) # N: Revealed type is "__main__.B" + reveal_type(a) # N: Revealed type is "__main__." a = B() # E: Incompatible types in assignment (expression has type "B", variable has type "A") - reveal_type(a) # N: Revealed type is "__main__.B" + reveal_type(a) # N: Revealed type is "__main__." a = A() reveal_type(a) # N: Revealed type is "__main__.A" reveal_type(a) # N: Revealed type is "__main__.A" @@ -559,7 +559,7 @@ def is_list_of_str(val: _T) -> TypeNarrower[List[_T]]: def func(name: str): reveal_type(name) # N: Revealed type is "builtins.str" if is_list_of_str(name): - reveal_type(name) # N: Revealed type is "Never" + reveal_type(name) # N: Revealed type is "__main__." [builtins fixtures/tuple.pyi] [case testTypeNarrowerWithTupleGeneric] From 75c9dec01ca73b7f37d2a833d754ab726b5b4480 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 9 Feb 2024 16:25:27 -0800 Subject: [PATCH 05/27] Did not actually need TypeNarrowerType --- mypy/checker.py | 1 - mypy/meet.py | 4 ---- mypy/types.py | 17 +---------------- 3 files changed, 1 insertion(+), 21 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 9a7ae81477ff..26378d88d004 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -199,7 +199,6 @@ TupleType, Type, TypeAliasType, - TypeNarrowerType, TypedDictType, TypeGuardedType, TypeOfAny, diff --git a/mypy/meet.py b/mypy/meet.py index e7767b05bd0e..283f364447c1 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -33,7 +33,6 @@ TupleType, Type, TypeAliasType, - TypeNarrowerType, TypedDictType, TypeGuardedType, TypeOfAny, @@ -116,8 +115,6 @@ def narrow_declared_type(declared: Type, narrowed: Type) -> Type: if isinstance(narrowed, TypeGuardedType): # type: ignore[misc] # A type guard forces the new type even if it doesn't overlap the old. return narrowed - elif isinstance(narrowed, TypeNarrowerType): - return narrow_declared_type(declared, narrowed.type_narrower) original_declared = declared original_narrowed = narrowed @@ -278,7 +275,6 @@ def is_overlapping_types( ): # A type guard forces the new type even if it doesn't overlap the old. return True - # TODO(jelle): TypeNarrower if seen_types is None: seen_types = set() diff --git a/mypy/types.py b/mypy/types.py index 47ad065c88db..003e2fa1f0e8 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -449,19 +449,6 @@ def __repr__(self) -> str: return f"TypeGuard({self.type_guard})" -class TypeNarrowerType(Type): - """Only used by find_isinstance_check() etc.""" - - __slots__ = ("type_narrower",) - - def __init__(self, type_narrower: Type) -> None: - super().__init__(line=type_narrower.line, column=type_narrower.column) - self.type_narrower = type_narrower - - def __repr__(self) -> str: - return f"TypeNarrower({self.type_narrower})" - - class RequiredType(Type): """Required[T] or NotRequired[T]. Only usable at top-level of a TypedDict definition.""" @@ -3094,8 +3081,6 @@ def get_proper_type(typ: Type | None) -> ProperType | None: return None if isinstance(typ, TypeGuardedType): # type: ignore[misc] typ = typ.type_guard - if isinstance(typ, TypeNarrowerType): # type: ignore[misc] - typ = typ.type_narrower while isinstance(typ, TypeAliasType): typ = typ._expand_once() # TODO: store the name of original type alias on this type, so we can show it in errors. @@ -3120,7 +3105,7 @@ def get_proper_types( typelist = types # Optimize for the common case so that we don't need to allocate anything if not any( - isinstance(t, (TypeAliasType, TypeGuardedType, TypeNarrowerType)) for t in typelist # type: ignore[misc] + isinstance(t, (TypeAliasType, TypeGuardedType)) for t in typelist # type: ignore[misc] ): return cast("list[ProperType]", typelist) return [get_proper_type(t) for t in typelist] From 4666486ed7de29f42cfd8ce8018694d5a27cc1a5 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 9 Feb 2024 16:47:46 -0800 Subject: [PATCH 06/27] Error for bad narrowing --- mypy/checker.py | 9 +++++++++ mypy/errorcodes.py | 6 ++++++ mypy/message_registry.py | 3 +++ test-data/unit/check-typenarrower.test | 14 ++++++++++++++ 4 files changed, 32 insertions(+) diff --git a/mypy/checker.py b/mypy/checker.py index 26378d88d004..a7170a684bb2 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1135,6 +1135,15 @@ def check_func_def( # Check validity of __new__ signature if fdef.info and fdef.name == "__new__": self.check___new___signature(fdef, typ) + if typ.type_narrower: + if not is_subtype(typ.type_narrower, typ.arg_types[0]): + self.fail( + message_registry.TYPE_NARROWER_NOT_SUBTYPE.format( + format_type(typ.type_narrower, self.options), + format_type(typ.arg_types[0], self.options), + ), + item, + ) self.check_for_missing_annotations(fdef) if self.options.disallow_any_unimported: diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py index 72ee63a6a897..b8ccaa13cf8e 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -281,5 +281,11 @@ def __hash__(self) -> int: sub_code_of=MISC, ) +TYPE_NARROWER_NOT_SUBTYPE: Final[ErrorCode] = ErrorCode( + "type-narrower-not-subtype", + "Warn if a type narrower's narrowed type is not a subtype of the original type", + "General", +) + # This copy will not include any error codes defined later in the plugins. mypy_error_codes = error_codes.copy() diff --git a/mypy/message_registry.py b/mypy/message_registry.py index 85f9f0feaf6e..01fa3ef24578 100644 --- a/mypy/message_registry.py +++ b/mypy/message_registry.py @@ -324,3 +324,6 @@ def with_additional_msg(self, info: str) -> ErrorMessage: ARG_NAME_EXPECTED_STRING_LITERAL: Final = ErrorMessage( "Expected string literal for argument name, got {}", codes.SYNTAX ) +TYPE_NARROWER_NOT_SUBTYPE: Final = ErrorMessage( + "Narrowed type {} must be a subtype of input type {}", codes.SYNTAX +) diff --git a/test-data/unit/check-typenarrower.test b/test-data/unit/check-typenarrower.test index 27cd66a499f4..bcab2eb3e642 100644 --- a/test-data/unit/check-typenarrower.test +++ b/test-data/unit/check-typenarrower.test @@ -684,3 +684,17 @@ x: object assert a(x=x) reveal_type(x) # N: Revealed type is "builtins.int" [builtins fixtures/tuple.pyi] + +[case testTypeNarrowerMustBeSubtype] +from typing_extensions import TypeNarrower +from typing import List, Sequence, TypeVar + +def f(x: str) -> TypeNarrower[int]: # E: Narrowed type "int" must be a subtype of input type "str" + pass + +T = TypeVar('T') + +def g(x: List[T]) -> TypeNarrower[Sequence[T]]: # E: Narrowed type "Sequence[T]" must be a subtype of input type "List[T]" + pass + +[builtins fixtures/tuple.pyi] From 25a9c79010b47c1eb93354c7f02c48723fb6b464 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 9 Feb 2024 16:53:29 -0800 Subject: [PATCH 07/27] temp change typeshed --- mypy/typeshed/stdlib/typing_extensions.pyi | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mypy/typeshed/stdlib/typing_extensions.pyi b/mypy/typeshed/stdlib/typing_extensions.pyi index ea5c7b21aa87..e88a7adce82f 100644 --- a/mypy/typeshed/stdlib/typing_extensions.pyi +++ b/mypy/typeshed/stdlib/typing_extensions.pyi @@ -309,6 +309,8 @@ else: TypeGuard: _SpecialForm def is_typeddict(tp: object) -> bool: ... +TypeNarrower: _SpecialForm + # New and changed things in 3.11 if sys.version_info >= (3, 11): from typing import ( From faa4a076b91cc791abd5249392ab3939211b1d3f Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 9 Feb 2024 16:55:37 -0800 Subject: [PATCH 08/27] Fixes --- mypy/checker.py | 3 +-- mypy/meet.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index a7170a684bb2..ac692b9906b3 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5732,7 +5732,7 @@ def find_isinstance_check_helper(self, node: Expression) -> tuple[TypeMap, TypeM # Also note that a care must be taken to unwrap this back at read places # where we use this to narrow down declared type. if node.callee.type_guard is not None: - guard_type = TypeGuardedType(node.callee.type_guard) + return {expr: TypeGuardedType(node.callee.type_guard)}, {} else: return conditional_types_to_typemaps( expr, @@ -5742,7 +5742,6 @@ def find_isinstance_check_helper(self, node: Expression) -> tuple[TypeMap, TypeM expr, ), ) - return {expr: guard_type}, {} elif isinstance(node, ComparisonExpr): # Step 1: Obtain the types of each operand and whether or not we can # narrow their types. (For example, we shouldn't try narrowing the diff --git a/mypy/meet.py b/mypy/meet.py index 283f364447c1..df8b960cdf3f 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -114,7 +114,7 @@ def narrow_declared_type(declared: Type, narrowed: Type) -> Type: # TODO: check infinite recursion for aliases here. if isinstance(narrowed, TypeGuardedType): # type: ignore[misc] # A type guard forces the new type even if it doesn't overlap the old. - return narrowed + return narrowed.type_guard original_declared = declared original_narrowed = narrowed From f107e5b01bcf7b4592c848d954fce8c7444e5e63 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 10 Feb 2024 00:58:24 +0000 Subject: [PATCH 09/27] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checkexpr.py | 5 +---- mypy/expandtype.py | 4 +++- mypy/subtypes.py | 4 +++- mypy/typeanal.py | 5 ++++- mypy/types.py | 8 ++++++-- 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index ca815b9ac02f..226605e4580b 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1451,10 +1451,7 @@ def check_call_expr_with_callee_type( object_type=object_type, ) proper_callee = get_proper_type(callee_type) - if ( - isinstance(e.callee, RefExpr) - and isinstance(proper_callee, CallableType) - ): + if isinstance(e.callee, RefExpr) and isinstance(proper_callee, CallableType): # Cache it for find_isinstance_check() if proper_callee.type_guard is not None: e.callee.type_guard = proper_callee.type_guard diff --git a/mypy/expandtype.py b/mypy/expandtype.py index 52e83b2db7b2..30f7469bf2e8 100644 --- a/mypy/expandtype.py +++ b/mypy/expandtype.py @@ -342,7 +342,9 @@ def visit_callable_type(self, t: CallableType) -> CallableType: arg_names=t.arg_names[:-2] + repl.arg_names, ret_type=t.ret_type.accept(self), type_guard=(t.type_guard.accept(self) if t.type_guard is not None else None), - type_narrower=(t.type_narrower.accept(self) if t.type_narrower is not None else None), + type_narrower=( + t.type_narrower.accept(self) if t.type_narrower is not None else None + ), imprecise_arg_kinds=(t.imprecise_arg_kinds or repl.imprecise_arg_kinds), variables=[*repl.variables, *t.variables], ) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index e4dcdea15300..a3d3b73c8ef8 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -688,7 +688,9 @@ def visit_callable_type(self, left: CallableType) -> bool: # a TypeNarrower[Child] when a TypeNarrower[Parent] is expected, because # if the narrower returns False, we assume that the narrowed value is # *not* a Parent. - if not self._is_subtype(left.type_narrower, right.type_narrower) or not self._is_subtype(right.type_narrower, left.type_narrower): + if not self._is_subtype( + left.type_narrower, right.type_narrower + ) or not self._is_subtype(right.type_narrower, left.type_narrower): return False elif right.type_guard is not None and left.type_guard is None: # This means that one function has `TypeGuard` and other does not. diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 8db6786eb5d2..6c06ed85f310 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -663,7 +663,10 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ ) return AnyType(TypeOfAny.from_error) return RequiredType(self.anal_type(t.args[0]), required=False) - elif self.anal_type_guard_arg(t, fullname) is not None or self.anal_type_narrower_arg(t, fullname) is not None: + elif ( + self.anal_type_guard_arg(t, fullname) is not None + or self.anal_type_narrower_arg(t, fullname) is not None + ): # In most contexts, TypeGuard[...] acts as an alias for bool (ignoring its args) return self.named_type("builtins.bool") elif fullname in ("typing.Unpack", "typing_extensions.Unpack"): diff --git a/mypy/types.py b/mypy/types.py index 003e2fa1f0e8..99419d8a740c 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -2229,7 +2229,9 @@ def serialize(self) -> JsonDict: "bound_args": [(None if t is None else t.serialize()) for t in self.bound_args], "def_extras": dict(self.def_extras), "type_guard": self.type_guard.serialize() if self.type_guard is not None else None, - "type_narrower": self.type_narrower.serialize() if self.type_narrower is not None else None, + "type_narrower": ( + self.type_narrower.serialize() if self.type_narrower is not None else None + ), "from_concatenate": self.from_concatenate, "imprecise_arg_kinds": self.imprecise_arg_kinds, "unpack_kwargs": self.unpack_kwargs, @@ -2255,7 +2257,9 @@ def deserialize(cls, data: JsonDict) -> CallableType: deserialize_type(data["type_guard"]) if data["type_guard"] is not None else None ), type_narrower=( - deserialize_type(data["type_narrower"]) if data["type_narrower"] is not None else None + deserialize_type(data["type_narrower"]) + if data["type_narrower"] is not None + else None ), from_concatenate=data["from_concatenate"], imprecise_arg_kinds=data["imprecise_arg_kinds"], From 34700bb9229350309c2b3e59957ce1e6f721e5cf Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 9 Feb 2024 17:01:35 -0800 Subject: [PATCH 10/27] doc --- docs/source/error_code_list2.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/source/error_code_list2.rst b/docs/source/error_code_list2.rst index c966fe1f7ea6..a2c91502d000 100644 --- a/docs/source/error_code_list2.rst +++ b/docs/source/error_code_list2.rst @@ -555,3 +555,19 @@ Correct usage: When this code is enabled, using ``reveal_locals`` is always an error, because there's no way one can import it. + +.. _type-narrower-not-subtype: + +Check that TypeNarrower narrows types [type-narrower-not-subtype] +----------------------------------------------------------------- + +:pep:`742` requires that when a ``TypeNarrower`` is used, the narrowed +type must be a subtype of the original type:: + + from typing_extensions import TypeNarrower + + def f(x: int) -> TypeNarrower[str]: # Error, str is not a subtype of int + ... + + def g(x: object) -> TypeNarrower[str]: # OK + ... From 065ec92e5dd946685b0ab854e4d97407952f62b7 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 9 Feb 2024 17:02:19 -0800 Subject: [PATCH 11/27] fix self check --- mypy/checker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mypy/checker.py b/mypy/checker.py index ac692b9906b3..01da23cba2e8 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5734,6 +5734,7 @@ def find_isinstance_check_helper(self, node: Expression) -> tuple[TypeMap, TypeM if node.callee.type_guard is not None: return {expr: TypeGuardedType(node.callee.type_guard)}, {} else: + assert node.callee.type_narrower is not None return conditional_types_to_typemaps( expr, *self.conditional_types_with_intersection( From aef30363784216b60a182c54db1d4539befb0dfe Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 9 Feb 2024 17:17:51 -0800 Subject: [PATCH 12/27] like this maybe --- docs/source/error_code_list2.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/error_code_list2.rst b/docs/source/error_code_list2.rst index a2c91502d000..9a0fa79be236 100644 --- a/docs/source/error_code_list2.rst +++ b/docs/source/error_code_list2.rst @@ -556,7 +556,7 @@ Correct usage: When this code is enabled, using ``reveal_locals`` is always an error, because there's no way one can import it. -.. _type-narrower-not-subtype: +.. _code-type-narrower-not-subtype: Check that TypeNarrower narrows types [type-narrower-not-subtype] ----------------------------------------------------------------- From 6b0e749627ec977509262ab08a116544ab829f3c Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 10 Feb 2024 09:25:20 -0800 Subject: [PATCH 13/27] Fix and add tests --- mypy/checker.py | 23 +++++++----- mypy/message_registry.py | 2 +- test-data/unit/check-typenarrower.test | 49 ++++++++++++++++++++------ 3 files changed, 54 insertions(+), 20 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 01da23cba2e8..eea82b90478e 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1135,15 +1135,6 @@ def check_func_def( # Check validity of __new__ signature if fdef.info and fdef.name == "__new__": self.check___new___signature(fdef, typ) - if typ.type_narrower: - if not is_subtype(typ.type_narrower, typ.arg_types[0]): - self.fail( - message_registry.TYPE_NARROWER_NOT_SUBTYPE.format( - format_type(typ.type_narrower, self.options), - format_type(typ.arg_types[0], self.options), - ), - item, - ) self.check_for_missing_annotations(fdef) if self.options.disallow_any_unimported: @@ -1218,6 +1209,20 @@ def check_func_def( # visible from *inside* of this function/method. ref_type: Type | None = self.scope.active_self_type() + if typ.type_narrower: + arg_index = 0 + # For methods and classmethods, we want the second parameter + if ref_type is not None and (not defn.is_static or defn.name == "__new__"): + arg_index = 1 + if arg_index < len(typ.arg_types) and not is_subtype(typ.type_narrower, typ.arg_types[arg_index]): + self.fail( + message_registry.TYPE_NARROWER_NOT_SUBTYPE.format( + format_type(typ.type_narrower, self.options), + format_type(typ.arg_types[arg_index], self.options), + ), + item, + ) + # Store argument types. for i in range(len(typ.arg_types)): arg_type = typ.arg_types[i] diff --git a/mypy/message_registry.py b/mypy/message_registry.py index 01fa3ef24578..d4b5687c4de1 100644 --- a/mypy/message_registry.py +++ b/mypy/message_registry.py @@ -325,5 +325,5 @@ def with_additional_msg(self, info: str) -> ErrorMessage: "Expected string literal for argument name, got {}", codes.SYNTAX ) TYPE_NARROWER_NOT_SUBTYPE: Final = ErrorMessage( - "Narrowed type {} must be a subtype of input type {}", codes.SYNTAX + "Narrowed type {} is not a subtype of input type {}", codes.SYNTAX ) diff --git a/test-data/unit/check-typenarrower.test b/test-data/unit/check-typenarrower.test index bcab2eb3e642..e9fa299d09c9 100644 --- a/test-data/unit/check-typenarrower.test +++ b/test-data/unit/check-typenarrower.test @@ -323,7 +323,7 @@ from typing_extensions import TypeNarrower class A: pass class B: pass -def guard(a: A) -> TypeNarrower[B]: +def guard(a: object) -> TypeNarrower[B]: pass a = A() @@ -337,7 +337,7 @@ from typing_extensions import TypeNarrower class A: pass class B: pass -def guard(a: A) -> TypeNarrower[B]: +def guard(a: object) -> TypeNarrower[B]: pass a = A() @@ -548,18 +548,18 @@ def func2(name: str): [builtins fixtures/tuple.pyi] [case testTypeNarrowerWithGenericInstance] -from typing import TypeVar, List +from typing import TypeVar, List, Iterable from typing_extensions import TypeNarrower _T = TypeVar("_T") -def is_list_of_str(val: _T) -> TypeNarrower[List[_T]]: +def is_list_of_str(val: Iterable[_T]) -> TypeNarrower[List[_T]]: pass -def func(name: str): - reveal_type(name) # N: Revealed type is "builtins.str" +def func(name: Iterable[str]): + reveal_type(name) # N: Revealed type is "typing.Iterable[builtins.str]" if is_list_of_str(name): - reveal_type(name) # N: Revealed type is "__main__." + reveal_type(name) # N: Revealed type is "builtins.list[builtins.str]" [builtins fixtures/tuple.pyi] [case testTypeNarrowerWithTupleGeneric] @@ -685,16 +685,45 @@ assert a(x=x) reveal_type(x) # N: Revealed type is "builtins.int" [builtins fixtures/tuple.pyi] -[case testTypeNarrowerMustBeSubtype] +[case testTypeNarrowerMustBeSubtypeFunctions] from typing_extensions import TypeNarrower from typing import List, Sequence, TypeVar -def f(x: str) -> TypeNarrower[int]: # E: Narrowed type "int" must be a subtype of input type "str" +def f(x: str) -> TypeNarrower[int]: # E: Narrowed type "int" is not a subtype of input type "str" pass T = TypeVar('T') -def g(x: List[T]) -> TypeNarrower[Sequence[T]]: # E: Narrowed type "Sequence[T]" must be a subtype of input type "List[T]" +def g(x: List[T]) -> TypeNarrower[Sequence[T]]: # E: Narrowed type "Sequence[T]" is not a subtype of input type "List[T]" pass [builtins fixtures/tuple.pyi] + +[case testTypeNarrowerMustBeSubtypeMethods] +from typing_extensions import TypeNarrower + +class NarrowHolder: + @classmethod + def cls_narrower_good(cls, x: object) -> TypeNarrower[int]: + pass + + @classmethod + def cls_narrower_bad(cls, x: str) -> TypeNarrower[int]: # E: Narrowed type "int" is not a subtype of input type "str" + pass + + @staticmethod + def static_narrower_good(x: object) -> TypeNarrower[int]: + pass + + @staticmethod + def static_narrower_bad(x: str) -> TypeNarrower[int]: # E: Narrowed type "int" is not a subtype of input type "str" + pass + + def inst_narrower_good(self, x: object) -> TypeNarrower[int]: + pass + + def inst_narrower_bad(self, x: str) -> TypeNarrower[int]: # E: Narrowed type "int" is not a subtype of input type "str" + pass + + +[builtins fixtures/classmethod.pyi] From c9e53e65d4d41ede650d1924934deddcadb430b8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 10 Feb 2024 17:25:46 +0000 Subject: [PATCH 14/27] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checker.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index eea82b90478e..3cece88b1f24 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1214,7 +1214,9 @@ def check_func_def( # For methods and classmethods, we want the second parameter if ref_type is not None and (not defn.is_static or defn.name == "__new__"): arg_index = 1 - if arg_index < len(typ.arg_types) and not is_subtype(typ.type_narrower, typ.arg_types[arg_index]): + if arg_index < len(typ.arg_types) and not is_subtype( + typ.type_narrower, typ.arg_types[arg_index] + ): self.fail( message_registry.TYPE_NARROWER_NOT_SUBTYPE.format( format_type(typ.type_narrower, self.options), From 909e53c49c5f6da70c56d0b0a1f9285beb4b9a26 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 14 Feb 2024 08:27:58 -0800 Subject: [PATCH 15/27] Use TypeIs --- docs/source/error_code_list2.rst | 12 +- mypy/applytype.py | 10 +- mypy/checker.py | 16 +- mypy/checkexpr.py | 6 +- mypy/constraints.py | 8 +- mypy/errorcodes.py | 4 +- mypy/expandtype.py | 6 +- mypy/fixup.py | 4 +- mypy/messages.py | 8 +- mypy/nodes.py | 6 +- mypy/semanal.py | 6 +- mypy/subtypes.py | 12 +- mypy/typeanal.py | 16 +- mypy/types.py | 24 +- mypy/typeshed/stdlib/typing_extensions.pyi | 2 - ...ck-typenarrower.test => check-typeis.test} | 380 +++++++++--------- test-data/unit/lib-stub/typing_extensions.pyi | 2 +- 17 files changed, 260 insertions(+), 262 deletions(-) rename test-data/unit/{check-typenarrower.test => check-typeis.test} (58%) diff --git a/docs/source/error_code_list2.rst b/docs/source/error_code_list2.rst index 9a0fa79be236..1a1827a68a31 100644 --- a/docs/source/error_code_list2.rst +++ b/docs/source/error_code_list2.rst @@ -556,18 +556,18 @@ Correct usage: When this code is enabled, using ``reveal_locals`` is always an error, because there's no way one can import it. -.. _code-type-narrower-not-subtype: +.. _code-type-is-not-subtype: -Check that TypeNarrower narrows types [type-narrower-not-subtype] +Check that TypeIs narrows types [type-is-not-subtype] ----------------------------------------------------------------- -:pep:`742` requires that when a ``TypeNarrower`` is used, the narrowed +:pep:`742` requires that when a ``TypeIs`` is used, the narrowed type must be a subtype of the original type:: - from typing_extensions import TypeNarrower + from typing_extensions import TypeIs - def f(x: int) -> TypeNarrower[str]: # Error, str is not a subtype of int + def f(x: int) -> TypeIs[str]: # Error, str is not a subtype of int ... - def g(x: object) -> TypeNarrower[str]: # OK + def g(x: object) -> TypeIs[str]: # OK ... diff --git a/mypy/applytype.py b/mypy/applytype.py index b7b2cbd8928b..4c3a65634172 100644 --- a/mypy/applytype.py +++ b/mypy/applytype.py @@ -137,15 +137,15 @@ def apply_generic_arguments( arg_types=[expand_type(at, id_to_type) for at in callable.arg_types] ) - # Apply arguments to TypeGuard and TypeNarrower if any. + # Apply arguments to TypeGuard and TypeIs if any. if callable.type_guard is not None: type_guard = expand_type(callable.type_guard, id_to_type) else: type_guard = None - if callable.type_narrower is not None: - type_narrower = expand_type(callable.type_narrower, id_to_type) + if callable.type_is is not None: + type_is = expand_type(callable.type_is, id_to_type) else: - type_narrower = None + type_is = None # The callable may retain some type vars if only some were applied. # TODO: move apply_poly() logic from checkexpr.py here when new inference @@ -157,5 +157,5 @@ def apply_generic_arguments( ret_type=expand_type(callable.ret_type, id_to_type), variables=remaining_tvars, type_guard=type_guard, - type_narrower=type_narrower, + type_is=type_is, ) diff --git a/mypy/checker.py b/mypy/checker.py index 3cece88b1f24..fd2b4bff8ac9 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1209,17 +1209,17 @@ def check_func_def( # visible from *inside* of this function/method. ref_type: Type | None = self.scope.active_self_type() - if typ.type_narrower: + if typ.type_is: arg_index = 0 # For methods and classmethods, we want the second parameter if ref_type is not None and (not defn.is_static or defn.name == "__new__"): arg_index = 1 if arg_index < len(typ.arg_types) and not is_subtype( - typ.type_narrower, typ.arg_types[arg_index] + typ.type_is, typ.arg_types[arg_index] ): self.fail( message_registry.TYPE_NARROWER_NOT_SUBTYPE.format( - format_type(typ.type_narrower, self.options), + format_type(typ.type_is, self.options), format_type(typ.arg_types[arg_index], self.options), ), item, @@ -2193,7 +2193,7 @@ def check_override( elif isinstance(original, CallableType) and isinstance(override, CallableType): if original.type_guard is not None and override.type_guard is None: fail = True - if original.type_narrower is not None and override.type_narrower is None: + if original.type_is is not None and override.type_is is None: fail = True if is_private(name): @@ -5647,7 +5647,7 @@ def combine_maps(list_maps: list[TypeMap]) -> TypeMap: def find_isinstance_check(self, node: Expression) -> tuple[TypeMap, TypeMap]: """Find any isinstance checks (within a chain of ands). Includes implicit and explicit checks for None and calls to callable. - Also includes TypeGuard and TypeNarrower functions. + Also includes TypeGuard and TypeIs functions. Return value is a map of variables to their types if the condition is true and a map of variables to their types if the condition is false. @@ -5699,7 +5699,7 @@ def find_isinstance_check_helper(self, node: Expression) -> tuple[TypeMap, TypeM if literal(expr) == LITERAL_TYPE and attr and len(attr) == 1: return self.hasattr_type_maps(expr, self.lookup_type(expr), attr[0]) elif isinstance(node.callee, RefExpr): - if node.callee.type_guard is not None or node.callee.type_narrower is not None: + if node.callee.type_guard is not None or node.callee.type_is is not None: # TODO: Follow *args, **kwargs if node.arg_kinds[0] != nodes.ARG_POS: # the first argument might be used as a kwarg @@ -5741,12 +5741,12 @@ def find_isinstance_check_helper(self, node: Expression) -> tuple[TypeMap, TypeM if node.callee.type_guard is not None: return {expr: TypeGuardedType(node.callee.type_guard)}, {} else: - assert node.callee.type_narrower is not None + assert node.callee.type_is is not None return conditional_types_to_typemaps( expr, *self.conditional_types_with_intersection( self.lookup_type(expr), - [TypeRange(node.callee.type_narrower, is_upper_bound=False)], + [TypeRange(node.callee.type_is, is_upper_bound=False)], expr, ), ) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 226605e4580b..529e952a1fca 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1455,8 +1455,8 @@ def check_call_expr_with_callee_type( # Cache it for find_isinstance_check() if proper_callee.type_guard is not None: e.callee.type_guard = proper_callee.type_guard - if proper_callee.type_narrower is not None: - e.callee.type_narrower = proper_callee.type_narrower + if proper_callee.type_is is not None: + e.callee.type_is = proper_callee.type_is return ret_type def check_union_call_expr(self, e: CallExpr, object_type: UnionType, member: str) -> Type: @@ -5276,7 +5276,7 @@ def infer_lambda_type_using_context( # is a constructor -- but this fallback doesn't make sense for lambdas. callable_ctx = callable_ctx.copy_modified(fallback=self.named_type("builtins.function")) - if callable_ctx.type_guard is not None or callable_ctx.type_narrower is not None: + if callable_ctx.type_guard is not None or callable_ctx.type_is is not None: # Lambda's return type cannot be treated as a `TypeGuard`, # because it is implicit. And `TypeGuard`s must be explicit. # See https://github.com/python/mypy/issues/9927 diff --git a/mypy/constraints.py b/mypy/constraints.py index 9922d25cc4df..7af87bd868e4 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -1020,12 +1020,12 @@ def visit_callable_type(self, template: CallableType) -> list[Constraint]: template_ret_type, cactual_ret_type = template.ret_type, cactual.ret_type if template.type_guard is not None: template_ret_type = template.type_guard - elif template.type_narrower is not None: - template_ret_type = template.type_narrower + elif template.type_is is not None: + template_ret_type = template.type_is if cactual.type_guard is not None: cactual_ret_type = cactual.type_guard - elif cactual.type_narrower is not None: - cactual_ret_type = cactual.type_narrower + elif cactual.type_is is not None: + cactual_ret_type = cactual.type_is res.extend(infer_constraints(template_ret_type, cactual_ret_type, self.direction)) if param_spec is None: diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py index b8ccaa13cf8e..e92e883e8517 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -282,8 +282,8 @@ def __hash__(self) -> int: ) TYPE_NARROWER_NOT_SUBTYPE: Final[ErrorCode] = ErrorCode( - "type-narrower-not-subtype", - "Warn if a type narrower's narrowed type is not a subtype of the original type", + "type-is-not-subtype", + "Warn if a TypeIs function's narrowed type is not a subtype of the original type", "General", ) diff --git a/mypy/expandtype.py b/mypy/expandtype.py index 30f7469bf2e8..abecf492888e 100644 --- a/mypy/expandtype.py +++ b/mypy/expandtype.py @@ -342,8 +342,8 @@ def visit_callable_type(self, t: CallableType) -> CallableType: arg_names=t.arg_names[:-2] + repl.arg_names, ret_type=t.ret_type.accept(self), type_guard=(t.type_guard.accept(self) if t.type_guard is not None else None), - type_narrower=( - t.type_narrower.accept(self) if t.type_narrower is not None else None + type_is=( + t.type_is.accept(self) if t.type_is is not None else None ), imprecise_arg_kinds=(t.imprecise_arg_kinds or repl.imprecise_arg_kinds), variables=[*repl.variables, *t.variables], @@ -378,7 +378,7 @@ def visit_callable_type(self, t: CallableType) -> CallableType: arg_types=arg_types, ret_type=t.ret_type.accept(self), type_guard=(t.type_guard.accept(self) if t.type_guard is not None else None), - type_narrower=(t.type_narrower.accept(self) if t.type_narrower is not None else None), + type_is=(t.type_is.accept(self) if t.type_is is not None else None), ) if needs_normalization: return expanded.with_normalized_var_args() diff --git a/mypy/fixup.py b/mypy/fixup.py index a76a5ebe867b..849a6483d724 100644 --- a/mypy/fixup.py +++ b/mypy/fixup.py @@ -270,8 +270,8 @@ def visit_callable_type(self, ct: CallableType) -> None: arg.accept(self) if ct.type_guard is not None: ct.type_guard.accept(self) - if ct.type_narrower is not None: - ct.type_narrower.accept(self) + if ct.type_is is not None: + ct.type_is.accept(self) def visit_overloaded(self, t: Overloaded) -> None: for ct in t.items: diff --git a/mypy/messages.py b/mypy/messages.py index 88016f7b8fda..6ac37ffaf2b6 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -2634,8 +2634,8 @@ def format_literal_value(typ: LiteralType) -> str: elif isinstance(func, CallableType): if func.type_guard is not None: return_type = f"TypeGuard[{format(func.type_guard)}]" - elif func.type_narrower is not None: - return_type = f"TypeNarrower[{format(func.type_narrower)}]" + elif func.type_is is not None: + return_type = f"TypeIs[{format(func.type_is)}]" else: return_type = format(func.ret_type) if func.is_ellipsis_args: @@ -2852,8 +2852,8 @@ def [T <: int] f(self, x: int, y: T) -> None s += " -> " if tp.type_guard is not None: s += f"TypeGuard[{format_type_bare(tp.type_guard, options)}]" - elif tp.type_narrower is not None: - s += f"TypeNarrower[{format_type_bare(tp.type_narrower, options)}]" + elif tp.type_is is not None: + s += f"TypeIs[{format_type_bare(tp.type_is, options)}]" else: s += format_type_bare(tp.ret_type, options) diff --git a/mypy/nodes.py b/mypy/nodes.py index f77a59065804..bb278d92392d 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -1755,7 +1755,7 @@ class RefExpr(Expression): "is_inferred_def", "is_alias_rvalue", "type_guard", - "type_narrower", + "type_is", ) def __init__(self) -> None: @@ -1777,8 +1777,8 @@ def __init__(self) -> None: self.is_alias_rvalue = False # Cache type guard from callable_type.type_guard self.type_guard: mypy.types.Type | None = None - # And same for TypeNarrower - self.type_narrower: mypy.types.Type | None = None + # And same for TypeIs + self.type_is: mypy.types.Type | None = None @property def fullname(self) -> str: diff --git a/mypy/semanal.py b/mypy/semanal.py index f00cbcd607e4..aa78c5acc881 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -879,13 +879,13 @@ def analyze_func_def(self, defn: FuncDef) -> None: ) # in this case, we just kind of just ... remove the type guard. result = result.copy_modified(type_guard=None) - if result.type_narrower and ARG_POS not in result.arg_kinds[skip_self:]: + if result.type_is and ARG_POS not in result.arg_kinds[skip_self:]: self.fail( - "TypeNarrower functions must have a positional argument", + "TypeIs functions must have a positional argument", result, code=codes.VALID_TYPE, ) - result = result.copy_modified(type_narrower=None) + result = result.copy_modified(type_is=None) result = self.remove_unpack_kwargs(defn, result) if has_self_type and self.type is not None: diff --git a/mypy/subtypes.py b/mypy/subtypes.py index a3d3b73c8ef8..4b7bce8cc8d9 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -683,20 +683,20 @@ def visit_callable_type(self, left: CallableType) -> bool: if left.type_guard is not None and right.type_guard is not None: if not self._is_subtype(left.type_guard, right.type_guard): return False - elif left.type_narrower is not None and right.type_narrower is not None: - # For TypeNarrower we have to check both ways; it is unsafe to pass - # a TypeNarrower[Child] when a TypeNarrower[Parent] is expected, because + elif left.type_is is not None and right.type_is is not None: + # For TypeIs we have to check both ways; it is unsafe to pass + # a TypeIs[Child] when a TypeIs[Parent] is expected, because # if the narrower returns False, we assume that the narrowed value is # *not* a Parent. if not self._is_subtype( - left.type_narrower, right.type_narrower - ) or not self._is_subtype(right.type_narrower, left.type_narrower): + left.type_is, right.type_is + ) or not self._is_subtype(right.type_is, left.type_is): return False elif right.type_guard is not None and left.type_guard is None: # This means that one function has `TypeGuard` and other does not. # They are not compatible. See https://github.com/python/mypy/issues/11307 return False - elif right.type_narrower is not None and left.type_narrower is None: + elif right.type_is is not None and left.type_is is None: # Similarly, if one function has typeNarrower and the other does not, # they are not compatible. return False diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 6c06ed85f310..0317c67d298e 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -665,7 +665,7 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ return RequiredType(self.anal_type(t.args[0]), required=False) elif ( self.anal_type_guard_arg(t, fullname) is not None - or self.anal_type_narrower_arg(t, fullname) is not None + or self.anal_type_is_arg(t, fullname) is not None ): # In most contexts, TypeGuard[...] acts as an alias for bool (ignoring its args) return self.named_type("builtins.bool") @@ -985,7 +985,7 @@ def visit_callable_type(self, t: CallableType, nested: bool = True) -> Type: else: variables, _ = self.bind_function_type_variables(t, t) type_guard = self.anal_type_guard(t.ret_type) - type_narrower = self.anal_type_narrower(t.ret_type) + type_is = self.anal_type_is(t.ret_type) arg_kinds = t.arg_kinds if len(arg_kinds) >= 2 and arg_kinds[-2] == ARG_STAR and arg_kinds[-1] == ARG_STAR2: arg_types = self.anal_array(t.arg_types[:-2], nested=nested) + [ @@ -1041,7 +1041,7 @@ def visit_callable_type(self, t: CallableType, nested: bool = True) -> Type: fallback=(t.fallback if t.fallback.type else self.named_type("builtins.function")), variables=self.anal_var_defs(variables), type_guard=type_guard, - type_narrower=type_narrower, + type_is=type_is, unpack_kwargs=unpacked_kwargs, ) return ret @@ -1064,19 +1064,19 @@ def anal_type_guard_arg(self, t: UnboundType, fullname: str) -> Type | None: return self.anal_type(t.args[0]) return None - def anal_type_narrower(self, t: Type) -> Type | None: + def anal_type_is(self, t: Type) -> Type | None: if isinstance(t, UnboundType): sym = self.lookup_qualified(t.name, t) if sym is not None and sym.node is not None: - return self.anal_type_narrower_arg(t, sym.node.fullname) + return self.anal_type_is_arg(t, sym.node.fullname) # TODO: What if it's an Instance? Then use t.type.fullname? return None - def anal_type_narrower_arg(self, t: UnboundType, fullname: str) -> Type | None: - if fullname in ("typing_extensions.TypeNarrower", "typing.TypeNarrower"): + def anal_type_is_arg(self, t: UnboundType, fullname: str) -> Type | None: + if fullname in ("typing_extensions.TypeIs", "typing.TypeIs"): if len(t.args) != 1: self.fail( - "TypeNarrower must have exactly one type argument", t, code=codes.VALID_TYPE + "TypeIs must have exactly one type argument", t, code=codes.VALID_TYPE ) return AnyType(TypeOfAny.from_error) return self.anal_type(t.args[0]) diff --git a/mypy/types.py b/mypy/types.py index 99419d8a740c..164be95b2e5d 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1791,7 +1791,7 @@ class CallableType(FunctionLike): "def_extras", # Information about original definition we want to serialize. # This is used for more detailed error messages. "type_guard", # T, if -> TypeGuard[T] (ret_type is bool in this case). - "type_narrower", # T, if -> TypeNarrower[T] (ret_type is bool in this case). + "type_is", # T, if -> TypeIs[T] (ret_type is bool in this case). "from_concatenate", # whether this callable is from a concatenate object # (this is used for error messages) "imprecise_arg_kinds", @@ -1818,7 +1818,7 @@ def __init__( bound_args: Sequence[Type | None] = (), def_extras: dict[str, Any] | None = None, type_guard: Type | None = None, - type_narrower: Type | None = None, + type_is: Type | None = None, from_concatenate: bool = False, imprecise_arg_kinds: bool = False, unpack_kwargs: bool = False, @@ -1868,7 +1868,7 @@ def __init__( else: self.def_extras = {} self.type_guard = type_guard - self.type_narrower = type_narrower + self.type_is = type_is self.unpack_kwargs = unpack_kwargs def copy_modified( @@ -1890,7 +1890,7 @@ def copy_modified( bound_args: Bogus[list[Type | None]] = _dummy, def_extras: Bogus[dict[str, Any]] = _dummy, type_guard: Bogus[Type | None] = _dummy, - type_narrower: Bogus[Type | None] = _dummy, + type_is: Bogus[Type | None] = _dummy, from_concatenate: Bogus[bool] = _dummy, imprecise_arg_kinds: Bogus[bool] = _dummy, unpack_kwargs: Bogus[bool] = _dummy, @@ -1915,7 +1915,7 @@ def copy_modified( bound_args=bound_args if bound_args is not _dummy else self.bound_args, def_extras=def_extras if def_extras is not _dummy else dict(self.def_extras), type_guard=type_guard if type_guard is not _dummy else self.type_guard, - type_narrower=type_narrower if type_narrower is not _dummy else self.type_narrower, + type_is=type_is if type_is is not _dummy else self.type_is, from_concatenate=( from_concatenate if from_concatenate is not _dummy else self.from_concatenate ), @@ -2229,8 +2229,8 @@ def serialize(self) -> JsonDict: "bound_args": [(None if t is None else t.serialize()) for t in self.bound_args], "def_extras": dict(self.def_extras), "type_guard": self.type_guard.serialize() if self.type_guard is not None else None, - "type_narrower": ( - self.type_narrower.serialize() if self.type_narrower is not None else None + "type_is": ( + self.type_is.serialize() if self.type_is is not None else None ), "from_concatenate": self.from_concatenate, "imprecise_arg_kinds": self.imprecise_arg_kinds, @@ -2256,9 +2256,9 @@ def deserialize(cls, data: JsonDict) -> CallableType: type_guard=( deserialize_type(data["type_guard"]) if data["type_guard"] is not None else None ), - type_narrower=( - deserialize_type(data["type_narrower"]) - if data["type_narrower"] is not None + type_is=( + deserialize_type(data["type_is"]) + if data["type_is"] is not None else None ), from_concatenate=data["from_concatenate"], @@ -3319,8 +3319,8 @@ def visit_callable_type(self, t: CallableType) -> str: if not isinstance(get_proper_type(t.ret_type), NoneType): if t.type_guard is not None: s += f" -> TypeGuard[{t.type_guard.accept(self)}]" - elif t.type_narrower is not None: - s += f" -> TypeNarrower[{t.type_narrower.accept(self)}]" + elif t.type_is is not None: + s += f" -> TypeIs[{t.type_is.accept(self)}]" else: s += f" -> {t.ret_type.accept(self)}" diff --git a/mypy/typeshed/stdlib/typing_extensions.pyi b/mypy/typeshed/stdlib/typing_extensions.pyi index e88a7adce82f..ea5c7b21aa87 100644 --- a/mypy/typeshed/stdlib/typing_extensions.pyi +++ b/mypy/typeshed/stdlib/typing_extensions.pyi @@ -309,8 +309,6 @@ else: TypeGuard: _SpecialForm def is_typeddict(tp: object) -> bool: ... -TypeNarrower: _SpecialForm - # New and changed things in 3.11 if sys.version_info >= (3, 11): from typing import ( diff --git a/test-data/unit/check-typenarrower.test b/test-data/unit/check-typeis.test similarity index 58% rename from test-data/unit/check-typenarrower.test rename to test-data/unit/check-typeis.test index e9fa299d09c9..ce67040e03ab 100644 --- a/test-data/unit/check-typenarrower.test +++ b/test-data/unit/check-typeis.test @@ -1,7 +1,7 @@ -[case testTypeNarrowerBasic] -from typing_extensions import TypeNarrower +[case testTypeIsBasic] +from typing_extensions import TypeIs class Point: pass -def is_point(a: object) -> TypeNarrower[Point]: pass +def is_point(a: object) -> TypeIs[Point]: pass def main(a: object) -> None: if is_point(a): reveal_type(a) # N: Revealed type is "__main__.Point" @@ -9,76 +9,76 @@ def main(a: object) -> None: reveal_type(a) # N: Revealed type is "builtins.object" [builtins fixtures/tuple.pyi] -[case testTypeNarrowerTypeArgsNone] -from typing_extensions import TypeNarrower -def foo(a: object) -> TypeNarrower: # E: TypeNarrower must have exactly one type argument +[case testTypeIsTypeArgsNone] +from typing_extensions import TypeIs +def foo(a: object) -> TypeIs: # E: TypeIs must have exactly one type argument pass [builtins fixtures/tuple.pyi] -[case testTypeNarrowerTypeArgsTooMany] -from typing_extensions import TypeNarrower -def foo(a: object) -> TypeNarrower[int, int]: # E: TypeNarrower must have exactly one type argument +[case testTypeIsTypeArgsTooMany] +from typing_extensions import TypeIs +def foo(a: object) -> TypeIs[int, int]: # E: TypeIs must have exactly one type argument pass [builtins fixtures/tuple.pyi] -[case testTypeNarrowerTypeArgType] -from typing_extensions import TypeNarrower -def foo(a: object) -> TypeNarrower[42]: # E: Invalid type: try using Literal[42] instead? +[case testTypeIsTypeArgType] +from typing_extensions import TypeIs +def foo(a: object) -> TypeIs[42]: # E: Invalid type: try using Literal[42] instead? pass [builtins fixtures/tuple.pyi] -[case testTypeNarrowerRepr] -from typing_extensions import TypeNarrower -def foo(a: object) -> TypeNarrower[int]: +[case testTypeIsRepr] +from typing_extensions import TypeIs +def foo(a: object) -> TypeIs[int]: pass -reveal_type(foo) # N: Revealed type is "def (a: builtins.object) -> TypeNarrower[builtins.int]" +reveal_type(foo) # N: Revealed type is "def (a: builtins.object) -> TypeIs[builtins.int]" [builtins fixtures/tuple.pyi] -[case testTypeNarrowerCallArgsNone] -from typing_extensions import TypeNarrower +[case testTypeIsCallArgsNone] +from typing_extensions import TypeIs class Point: pass -def is_point() -> TypeNarrower[Point]: pass # E: TypeNarrower functions must have a positional argument +def is_point() -> TypeIs[Point]: pass # E: TypeIs functions must have a positional argument def main(a: object) -> None: if is_point(): reveal_type(a) # N: Revealed type is "builtins.object" [builtins fixtures/tuple.pyi] -[case testTypeNarrowerCallArgsMultiple] -from typing_extensions import TypeNarrower +[case testTypeIsCallArgsMultiple] +from typing_extensions import TypeIs class Point: pass -def is_point(a: object, b: object) -> TypeNarrower[Point]: pass +def is_point(a: object, b: object) -> TypeIs[Point]: pass def main(a: object, b: object) -> None: if is_point(a, b): reveal_type(a) # N: Revealed type is "__main__.Point" reveal_type(b) # N: Revealed type is "builtins.object" [builtins fixtures/tuple.pyi] -[case testTypeNarrowerIsBool] -from typing_extensions import TypeNarrower -def f(a: TypeNarrower[int]) -> None: pass +[case testTypeIsIsBool] +from typing_extensions import TypeIs +def f(a: TypeIs[int]) -> None: pass reveal_type(f) # N: Revealed type is "def (a: builtins.bool)" -a: TypeNarrower[int] +a: TypeIs[int] reveal_type(a) # N: Revealed type is "builtins.bool" class C: - a: TypeNarrower[int] + a: TypeIs[int] reveal_type(C().a) # N: Revealed type is "builtins.bool" [builtins fixtures/tuple.pyi] -[case testTypeNarrowerWithTypeVar] +[case testTypeIsWithTypeVar] from typing import TypeVar, Tuple, Type -from typing_extensions import TypeNarrower +from typing_extensions import TypeIs T = TypeVar('T') -def is_tuple_of_type(a: Tuple[object, ...], typ: Type[T]) -> TypeNarrower[Tuple[T, ...]]: pass +def is_tuple_of_type(a: Tuple[object, ...], typ: Type[T]) -> TypeIs[Tuple[T, ...]]: pass def main(a: Tuple[object, ...]): if is_tuple_of_type(a, int): reveal_type(a) # N: Revealed type is "builtins.tuple[builtins.int, ...]" [builtins fixtures/tuple.pyi] -[case testTypeNarrowerUnionIn] +[case testTypeIsUnionIn] from typing import Union -from typing_extensions import TypeNarrower -def is_foo(a: Union[int, str]) -> TypeNarrower[str]: pass +from typing_extensions import TypeIs +def is_foo(a: Union[int, str]) -> TypeIs[str]: pass def main(a: Union[str, int]) -> None: if is_foo(a): reveal_type(a) # N: Revealed type is "builtins.str" @@ -87,72 +87,72 @@ def main(a: Union[str, int]) -> None: reveal_type(a) # N: Revealed type is "Union[builtins.str, builtins.int]" [builtins fixtures/tuple.pyi] -[case testTypeNarrowerUnionOut] +[case testTypeIsUnionOut] from typing import Union -from typing_extensions import TypeNarrower -def is_foo(a: object) -> TypeNarrower[Union[int, str]]: pass +from typing_extensions import TypeIs +def is_foo(a: object) -> TypeIs[Union[int, str]]: pass def main(a: object) -> None: if is_foo(a): reveal_type(a) # N: Revealed type is "Union[builtins.int, builtins.str]" [builtins fixtures/tuple.pyi] -[case testTypeNarrowerNonzeroFloat] -from typing_extensions import TypeNarrower -def is_nonzero(a: object) -> TypeNarrower[float]: pass +[case testTypeIsNonzeroFloat] +from typing_extensions import TypeIs +def is_nonzero(a: object) -> TypeIs[float]: pass def main(a: int): if is_nonzero(a): reveal_type(a) # N: Revealed type is "builtins.int" [builtins fixtures/tuple.pyi] -[case testTypeNarrowerHigherOrder] +[case testTypeIsHigherOrder] from typing import Callable, TypeVar, Iterable, List -from typing_extensions import TypeNarrower +from typing_extensions import TypeIs T = TypeVar('T') R = TypeVar('R') -def filter(f: Callable[[T], TypeNarrower[R]], it: Iterable[T]) -> Iterable[R]: pass -def is_float(a: object) -> TypeNarrower[float]: pass +def filter(f: Callable[[T], TypeIs[R]], it: Iterable[T]) -> Iterable[R]: pass +def is_float(a: object) -> TypeIs[float]: pass a: List[object] = ["a", 0, 0.0] b = filter(is_float, a) reveal_type(b) # N: Revealed type is "typing.Iterable[builtins.float]" [builtins fixtures/tuple.pyi] -[case testTypeNarrowerMethod] -from typing_extensions import TypeNarrower +[case testTypeIsMethod] +from typing_extensions import TypeIs class C: def main(self, a: object) -> None: if self.is_float(a): reveal_type(self) # N: Revealed type is "__main__.C" reveal_type(a) # N: Revealed type is "builtins.float" - def is_float(self, a: object) -> TypeNarrower[float]: pass + def is_float(self, a: object) -> TypeIs[float]: pass [builtins fixtures/tuple.pyi] -[case testTypeNarrowerCrossModule] +[case testTypeIsCrossModule] import guard from points import Point def main(a: object) -> None: if guard.is_point(a): reveal_type(a) # N: Revealed type is "points.Point" [file guard.py] -from typing_extensions import TypeNarrower +from typing_extensions import TypeIs import points -def is_point(a: object) -> TypeNarrower[points.Point]: pass +def is_point(a: object) -> TypeIs[points.Point]: pass [file points.py] class Point: pass [builtins fixtures/tuple.pyi] -[case testTypeNarrowerBodyRequiresBool] -from typing_extensions import TypeNarrower -def is_float(a: object) -> TypeNarrower[float]: +[case testTypeIsBodyRequiresBool] +from typing_extensions import TypeIs +def is_float(a: object) -> TypeIs[float]: return "not a bool" # E: Incompatible return value type (got "str", expected "bool") [builtins fixtures/tuple.pyi] -[case testTypeNarrowerNarrowToTypedDict] +[case testTypeIsNarrowToTypedDict] from typing import Mapping, TypedDict -from typing_extensions import TypeNarrower +from typing_extensions import TypeIs class User(TypedDict): name: str id: int -def is_user(a: Mapping[str, object]) -> TypeNarrower[User]: +def is_user(a: Mapping[str, object]) -> TypeIs[User]: return isinstance(a.get("name"), str) and isinstance(a.get("id"), int) def main(a: Mapping[str, object]) -> None: if is_user(a): @@ -160,19 +160,19 @@ def main(a: Mapping[str, object]) -> None: [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] -[case testTypeNarrowerInAssert] -from typing_extensions import TypeNarrower -def is_float(a: object) -> TypeNarrower[float]: pass +[case testTypeIsInAssert] +from typing_extensions import TypeIs +def is_float(a: object) -> TypeIs[float]: pass def main(a: object) -> None: assert is_float(a) reveal_type(a) # N: Revealed type is "builtins.float" [builtins fixtures/tuple.pyi] -[case testTypeNarrowerFromAny] +[case testTypeIsFromAny] from typing import Any -from typing_extensions import TypeNarrower -def is_objfloat(a: object) -> TypeNarrower[float]: pass -def is_anyfloat(a: Any) -> TypeNarrower[float]: pass +from typing_extensions import TypeIs +def is_objfloat(a: object) -> TypeIs[float]: pass +def is_anyfloat(a: Any) -> TypeIs[float]: pass def objmain(a: object) -> None: if is_objfloat(a): reveal_type(a) # N: Revealed type is "builtins.float" @@ -185,11 +185,11 @@ def anymain(a: Any) -> None: reveal_type(a) # N: Revealed type is "builtins.float" [builtins fixtures/tuple.pyi] -[case testTypeNarrowerNegatedAndElse] +[case testTypeIsNegatedAndElse] from typing import Union -from typing_extensions import TypeNarrower -def is_int(a: object) -> TypeNarrower[int]: pass -def is_str(a: object) -> TypeNarrower[str]: pass +from typing_extensions import TypeIs +def is_int(a: object) -> TypeIs[int]: pass +def is_str(a: object) -> TypeIs[str]: pass def intmain(a: Union[int, str]) -> None: if not is_int(a): reveal_type(a) # N: Revealed type is "builtins.str" @@ -202,11 +202,11 @@ def strmain(a: Union[int, str]) -> None: reveal_type(a) # N: Revealed type is "builtins.int" [builtins fixtures/tuple.pyi] -[case testTypeNarrowerClassMethod] -from typing_extensions import TypeNarrower +[case testTypeIsClassMethod] +from typing_extensions import TypeIs class C: @classmethod - def is_float(cls, a: object) -> TypeNarrower[float]: pass + def is_float(cls, a: object) -> TypeIs[float]: pass def method(self, a: object) -> None: if self.is_float(a): reveal_type(a) # N: Revealed type is "builtins.float" @@ -215,9 +215,9 @@ def main(a: object) -> None: reveal_type(a) # N: Revealed type is "builtins.float" [builtins fixtures/classmethod.pyi] -[case testTypeNarrowerRequiresPositionalArgs] -from typing_extensions import TypeNarrower -def is_float(a: object, b: object = 0) -> TypeNarrower[float]: pass +[case testTypeIsRequiresPositionalArgs] +from typing_extensions import TypeIs +def is_float(a: object, b: object = 0) -> TypeIs[float]: pass def main1(a: object) -> None: if is_float(a=a, b=1): reveal_type(a) # N: Revealed type is "builtins.float" @@ -227,20 +227,20 @@ def main1(a: object) -> None: [builtins fixtures/tuple.pyi] -[case testTypeNarrowerOverload] +[case testTypeIsOverload] from typing import overload, Any, Callable, Iterable, Iterator, List, Optional, TypeVar -from typing_extensions import TypeNarrower +from typing_extensions import TypeIs T = TypeVar("T") R = TypeVar("R") @overload -def filter(f: Callable[[T], TypeNarrower[R]], it: Iterable[T]) -> Iterator[R]: ... +def filter(f: Callable[[T], TypeIs[R]], it: Iterable[T]) -> Iterator[R]: ... @overload def filter(f: Callable[[T], bool], it: Iterable[T]) -> Iterator[T]: ... def filter(*args): pass -def is_int_typeguard(a: object) -> TypeNarrower[int]: pass +def is_int_typeguard(a: object) -> TypeIs[int]: pass def is_int_bool(a: object) -> bool: pass def main(a: List[Optional[int]]) -> None: @@ -255,42 +255,42 @@ def main(a: List[Optional[int]]) -> None: [builtins fixtures/tuple.pyi] [typing fixtures/typing-full.pyi] -[case testTypeNarrowerDecorated] +[case testTypeIsDecorated] from typing import TypeVar -from typing_extensions import TypeNarrower +from typing_extensions import TypeIs T = TypeVar("T") def decorator(f: T) -> T: pass @decorator -def is_float(a: object) -> TypeNarrower[float]: +def is_float(a: object) -> TypeIs[float]: pass def main(a: object) -> None: if is_float(a): reveal_type(a) # N: Revealed type is "builtins.float" [builtins fixtures/tuple.pyi] -[case testTypeNarrowerMethodOverride] -from typing_extensions import TypeNarrower +[case testTypeIsMethodOverride] +from typing_extensions import TypeIs class C: - def is_float(self, a: object) -> TypeNarrower[float]: pass + def is_float(self, a: object) -> TypeIs[float]: pass class D(C): def is_float(self, a: object) -> bool: pass # Fail [builtins fixtures/tuple.pyi] [out] main:5: error: Signature of "is_float" incompatible with supertype "C" main:5: note: Superclass: -main:5: note: def is_float(self, a: object) -> TypeNarrower[float] +main:5: note: def is_float(self, a: object) -> TypeIs[float] main:5: note: Subclass: main:5: note: def is_float(self, a: object) -> bool -[case testTypeNarrowerInAnd] +[case testTypeIsInAnd] from typing import Any -from typing_extensions import TypeNarrower +from typing_extensions import TypeIs import types def isclass(a: object) -> bool: pass -def ismethod(a: object) -> TypeNarrower[float]: +def ismethod(a: object) -> TypeIs[float]: pass -def isfunction(a: object) -> TypeNarrower[str]: +def isfunction(a: object) -> TypeIs[str]: pass def isclassmethod(obj: Any) -> bool: if ismethod(obj) and obj.__self__ is not None and isclass(obj.__self__): # E: "float" has no attribute "__self__" @@ -303,13 +303,13 @@ def coverage(obj: Any) -> bool: return False [builtins fixtures/classmethod.pyi] -[case testAssignToTypeNarroweredVariable1] -from typing_extensions import TypeNarrower +[case testAssignToTypeIsedVariable1] +from typing_extensions import TypeIs class A: pass class B(A): pass -def guard(a: A) -> TypeNarrower[B]: +def guard(a: A) -> TypeIs[B]: pass a = A() @@ -317,13 +317,13 @@ if not guard(a): a = A() [builtins fixtures/tuple.pyi] -[case testAssignToTypeNarroweredVariable2] -from typing_extensions import TypeNarrower +[case testAssignToTypeIsedVariable2] +from typing_extensions import TypeIs class A: pass class B: pass -def guard(a: object) -> TypeNarrower[B]: +def guard(a: object) -> TypeIs[B]: pass a = A() @@ -331,13 +331,13 @@ if not guard(a): a = A() [builtins fixtures/tuple.pyi] -[case testAssignToTypeNarroweredVariable3] -from typing_extensions import TypeNarrower +[case testAssignToTypeIsedVariable3] +from typing_extensions import TypeIs class A: pass class B: pass -def guard(a: object) -> TypeNarrower[B]: +def guard(a: object) -> TypeIs[B]: pass a = A() @@ -350,12 +350,12 @@ if guard(a): reveal_type(a) # N: Revealed type is "__main__.A" [builtins fixtures/tuple.pyi] -[case testTypeNarrowerNestedRestrictionAny] -from typing_extensions import TypeNarrower +[case testTypeIsNestedRestrictionAny] +from typing_extensions import TypeIs from typing import Any class A: ... -def f(x: object) -> TypeNarrower[A]: ... +def f(x: object) -> TypeIs[A]: ... def g(x: object) -> None: ... def test(x: Any) -> None: @@ -364,14 +364,14 @@ def test(x: Any) -> None: g(reveal_type(x)) # N: Revealed type is "Union[__main__.A, Any]" [builtins fixtures/tuple.pyi] -[case testTypeNarrowerNestedRestrictionUnionOther] -from typing_extensions import TypeNarrower +[case testTypeIsNestedRestrictionUnionOther] +from typing_extensions import TypeIs from typing import Any class A: ... class B: ... -def f(x: object) -> TypeNarrower[A]: ... -def f2(x: object) -> TypeNarrower[B]: ... +def f(x: object) -> TypeIs[A]: ... +def f2(x: object) -> TypeIs[B]: ... def g(x: object) -> None: ... def test(x: object) -> None: @@ -380,18 +380,18 @@ def test(x: object) -> None: g(reveal_type(x)) # N: Revealed type is "Union[__main__.A, __main__.B]" [builtins fixtures/tuple.pyi] -[case testTypeNarrowerComprehensionSubtype] +[case testTypeIsComprehensionSubtype] from typing import List -from typing_extensions import TypeNarrower +from typing_extensions import TypeIs class Base: ... class Foo(Base): ... class Bar(Base): ... -def is_foo(item: object) -> TypeNarrower[Foo]: +def is_foo(item: object) -> TypeIs[Foo]: return isinstance(item, Foo) -def is_bar(item: object) -> TypeNarrower[Bar]: +def is_bar(item: object) -> TypeIs[Bar]: return isinstance(item, Bar) def foobar(items: List[object]): @@ -400,12 +400,12 @@ def foobar(items: List[object]): c: List[Bar] = [x for x in items if is_foo(x)] # E: List comprehension has incompatible type List[Foo]; expected List[Bar] [builtins fixtures/tuple.pyi] -[case testTypeNarrowerNestedRestrictionUnionIsInstance] -from typing_extensions import TypeNarrower +[case testTypeIsNestedRestrictionUnionIsInstance] +from typing_extensions import TypeIs from typing import Any, List class A: ... -def f(x: List[Any]) -> TypeNarrower[List[str]]: ... +def f(x: List[Any]) -> TypeIs[List[str]]: ... def g(x: object) -> None: ... def test(x: List[Any]) -> None: @@ -414,17 +414,17 @@ def test(x: List[Any]) -> None: g(reveal_type(x)) # N: Revealed type is "Union[builtins.list[builtins.str], __main__.]" [builtins fixtures/tuple.pyi] -[case testTypeNarrowerMultipleCondition-xfail] -from typing_extensions import TypeNarrower +[case testTypeIsMultipleCondition-xfail] +from typing_extensions import TypeIs from typing import Any, List class Foo: ... class Bar: ... -def is_foo(item: object) -> TypeNarrower[Foo]: +def is_foo(item: object) -> TypeIs[Foo]: return isinstance(item, Foo) -def is_bar(item: object) -> TypeNarrower[Bar]: +def is_bar(item: object) -> TypeIs[Bar]: return isinstance(item, Bar) def foobar(x: object): @@ -438,14 +438,14 @@ def foobar_typeguard(x: object): reveal_type(x) # N: Revealed type is "__main__." [builtins fixtures/tuple.pyi] -[case testTypeNarrowerAsFunctionArgAsBoolSubtype] +[case testTypeIsAsFunctionArgAsBoolSubtype] from typing import Callable -from typing_extensions import TypeNarrower +from typing_extensions import TypeIs def accepts_bool(f: Callable[[object], bool]): pass -def with_bool_typeguard(o: object) -> TypeNarrower[bool]: pass -def with_str_typeguard(o: object) -> TypeNarrower[str]: pass +def with_bool_typeguard(o: object) -> TypeIs[bool]: pass +def with_str_typeguard(o: object) -> TypeIs[str]: pass def with_bool(o: object) -> bool: pass accepts_bool(with_bool_typeguard) @@ -453,87 +453,87 @@ accepts_bool(with_str_typeguard) accepts_bool(with_bool) [builtins fixtures/tuple.pyi] -[case testTypeNarrowerAsFunctionArg] +[case testTypeIsAsFunctionArg] from typing import Callable -from typing_extensions import TypeNarrower +from typing_extensions import TypeIs -def accepts_typeguard(f: Callable[[object], TypeNarrower[bool]]): pass -def different_typeguard(f: Callable[[object], TypeNarrower[str]]): pass +def accepts_typeguard(f: Callable[[object], TypeIs[bool]]): pass +def different_typeguard(f: Callable[[object], TypeIs[str]]): pass -def with_typeguard(o: object) -> TypeNarrower[bool]: pass +def with_typeguard(o: object) -> TypeIs[bool]: pass def with_bool(o: object) -> bool: pass accepts_typeguard(with_typeguard) -accepts_typeguard(with_bool) # E: Argument 1 to "accepts_typeguard" has incompatible type "Callable[[object], bool]"; expected "Callable[[object], TypeNarrower[bool]]" +accepts_typeguard(with_bool) # E: Argument 1 to "accepts_typeguard" has incompatible type "Callable[[object], bool]"; expected "Callable[[object], TypeIs[bool]]" -different_typeguard(with_typeguard) # E: Argument 1 to "different_typeguard" has incompatible type "Callable[[object], TypeNarrower[bool]]"; expected "Callable[[object], TypeNarrower[str]]" -different_typeguard(with_bool) # E: Argument 1 to "different_typeguard" has incompatible type "Callable[[object], bool]"; expected "Callable[[object], TypeNarrower[str]]" +different_typeguard(with_typeguard) # E: Argument 1 to "different_typeguard" has incompatible type "Callable[[object], TypeIs[bool]]"; expected "Callable[[object], TypeIs[str]]" +different_typeguard(with_bool) # E: Argument 1 to "different_typeguard" has incompatible type "Callable[[object], bool]"; expected "Callable[[object], TypeIs[str]]" [builtins fixtures/tuple.pyi] -[case testTypeNarrowerAsGenericFunctionArg] +[case testTypeIsAsGenericFunctionArg] from typing import Callable, TypeVar -from typing_extensions import TypeNarrower +from typing_extensions import TypeIs T = TypeVar('T') -def accepts_typeguard(f: Callable[[object], TypeNarrower[T]]): pass +def accepts_typeguard(f: Callable[[object], TypeIs[T]]): pass -def with_bool_typeguard(o: object) -> TypeNarrower[bool]: pass -def with_str_typeguard(o: object) -> TypeNarrower[str]: pass +def with_bool_typeguard(o: object) -> TypeIs[bool]: pass +def with_str_typeguard(o: object) -> TypeIs[str]: pass def with_bool(o: object) -> bool: pass accepts_typeguard(with_bool_typeguard) accepts_typeguard(with_str_typeguard) -accepts_typeguard(with_bool) # E: Argument 1 to "accepts_typeguard" has incompatible type "Callable[[object], bool]"; expected "Callable[[object], TypeNarrower[bool]]" +accepts_typeguard(with_bool) # E: Argument 1 to "accepts_typeguard" has incompatible type "Callable[[object], bool]"; expected "Callable[[object], TypeIs[bool]]" [builtins fixtures/tuple.pyi] -[case testTypeNarrowerAsOverloadedFunctionArg] +[case testTypeIsAsOverloadedFunctionArg] # https://github.com/python/mypy/issues/11307 from typing import Callable, TypeVar, Generic, Any, overload -from typing_extensions import TypeNarrower +from typing_extensions import TypeIs _T = TypeVar('_T') class filter(Generic[_T]): @overload - def __init__(self, function: Callable[[object], TypeNarrower[_T]]) -> None: pass + def __init__(self, function: Callable[[object], TypeIs[_T]]) -> None: pass @overload def __init__(self, function: Callable[[_T], Any]) -> None: pass def __init__(self, function): pass -def is_int_typeguard(a: object) -> TypeNarrower[int]: pass +def is_int_typeguard(a: object) -> TypeIs[int]: pass def returns_bool(a: object) -> bool: pass reveal_type(filter(is_int_typeguard)) # N: Revealed type is "__main__.filter[builtins.int]" reveal_type(filter(returns_bool)) # N: Revealed type is "__main__.filter[builtins.object]" [builtins fixtures/tuple.pyi] -[case testTypeNarrowerSubtypingVariance] +[case testTypeIsSubtypingVariance] from typing import Callable -from typing_extensions import TypeNarrower +from typing_extensions import TypeIs class A: pass class B(A): pass class C(B): pass -def accepts_typeguard(f: Callable[[object], TypeNarrower[B]]): pass +def accepts_typeguard(f: Callable[[object], TypeIs[B]]): pass -def with_typeguard_a(o: object) -> TypeNarrower[A]: pass -def with_typeguard_b(o: object) -> TypeNarrower[B]: pass -def with_typeguard_c(o: object) -> TypeNarrower[C]: pass +def with_typeguard_a(o: object) -> TypeIs[A]: pass +def with_typeguard_b(o: object) -> TypeIs[B]: pass +def with_typeguard_c(o: object) -> TypeIs[C]: pass -accepts_typeguard(with_typeguard_a) # E: Argument 1 to "accepts_typeguard" has incompatible type "Callable[[object], TypeNarrower[A]]"; expected "Callable[[object], TypeNarrower[B]]" +accepts_typeguard(with_typeguard_a) # E: Argument 1 to "accepts_typeguard" has incompatible type "Callable[[object], TypeIs[A]]"; expected "Callable[[object], TypeIs[B]]" accepts_typeguard(with_typeguard_b) -accepts_typeguard(with_typeguard_c) # E: Argument 1 to "accepts_typeguard" has incompatible type "Callable[[object], TypeNarrower[C]]"; expected "Callable[[object], TypeNarrower[B]]" +accepts_typeguard(with_typeguard_c) # E: Argument 1 to "accepts_typeguard" has incompatible type "Callable[[object], TypeIs[C]]"; expected "Callable[[object], TypeIs[B]]" [builtins fixtures/tuple.pyi] -[case testTypeNarrowerWithIdentityGeneric] +[case testTypeIsWithIdentityGeneric] from typing import TypeVar -from typing_extensions import TypeNarrower +from typing_extensions import TypeIs _T = TypeVar("_T") -def identity(val: _T) -> TypeNarrower[_T]: +def identity(val: _T) -> TypeIs[_T]: pass def func1(name: _T): @@ -547,13 +547,13 @@ def func2(name: str): reveal_type(name) # N: Revealed type is "builtins.str" [builtins fixtures/tuple.pyi] -[case testTypeNarrowerWithGenericInstance] +[case testTypeIsWithGenericInstance] from typing import TypeVar, List, Iterable -from typing_extensions import TypeNarrower +from typing_extensions import TypeIs _T = TypeVar("_T") -def is_list_of_str(val: Iterable[_T]) -> TypeNarrower[List[_T]]: +def is_list_of_str(val: Iterable[_T]) -> TypeIs[List[_T]]: pass def func(name: Iterable[str]): @@ -562,13 +562,13 @@ def func(name: Iterable[str]): reveal_type(name) # N: Revealed type is "builtins.list[builtins.str]" [builtins fixtures/tuple.pyi] -[case testTypeNarrowerWithTupleGeneric] +[case testTypeIsWithTupleGeneric] from typing import TypeVar, Tuple -from typing_extensions import TypeNarrower +from typing_extensions import TypeIs _T = TypeVar("_T") -def is_two_element_tuple(val: Tuple[_T, ...]) -> TypeNarrower[Tuple[_T, _T]]: +def is_two_element_tuple(val: Tuple[_T, ...]) -> TypeIs[Tuple[_T, _T]]: pass def func(names: Tuple[str, ...]): @@ -577,34 +577,34 @@ def func(names: Tuple[str, ...]): reveal_type(names) # N: Revealed type is "Tuple[builtins.str, builtins.str]" [builtins fixtures/tuple.pyi] -[case testTypeNarrowerErroneousDefinitionFails] -from typing_extensions import TypeNarrower +[case testTypeIsErroneousDefinitionFails] +from typing_extensions import TypeIs class Z: - def typeguard1(self, *, x: object) -> TypeNarrower[int]: # E: TypeNarrower functions must have a positional argument + def typeguard1(self, *, x: object) -> TypeIs[int]: # E: TypeIs functions must have a positional argument ... @staticmethod - def typeguard2(x: object) -> TypeNarrower[int]: + def typeguard2(x: object) -> TypeIs[int]: ... @staticmethod - def typeguard3(*, x: object) -> TypeNarrower[int]: # E: TypeNarrower functions must have a positional argument + def typeguard3(*, x: object) -> TypeIs[int]: # E: TypeIs functions must have a positional argument ... -def bad_typeguard(*, x: object) -> TypeNarrower[int]: # E: TypeNarrower functions must have a positional argument +def bad_typeguard(*, x: object) -> TypeIs[int]: # E: TypeIs functions must have a positional argument ... [builtins fixtures/classmethod.pyi] -[case testTypeNarrowerWithKeywordArg] -from typing_extensions import TypeNarrower +[case testTypeIsWithKeywordArg] +from typing_extensions import TypeIs class Z: - def typeguard(self, x: object) -> TypeNarrower[int]: + def typeguard(self, x: object) -> TypeIs[int]: ... -def typeguard(x: object) -> TypeNarrower[int]: +def typeguard(x: object) -> TypeIs[int]: ... n: object @@ -615,12 +615,12 @@ if Z().typeguard(x=n): reveal_type(n) # N: Revealed type is "builtins.int" [builtins fixtures/tuple.pyi] -[case testStaticMethodTypeNarrower] -from typing_extensions import TypeNarrower +[case testStaticMethodTypeIs] +from typing_extensions import TypeIs class Y: @staticmethod - def typeguard(h: object) -> TypeNarrower[int]: + def typeguard(h: object) -> TypeIs[int]: ... x: object @@ -630,19 +630,19 @@ if Y.typeguard(x): reveal_type(x) # N: Revealed type is "builtins.int" [builtins fixtures/classmethod.pyi] -[case testTypeNarrowerKwargFollowingThroughOverloaded] +[case testTypeIsKwargFollowingThroughOverloaded] from typing import overload, Union -from typing_extensions import TypeNarrower +from typing_extensions import TypeIs @overload -def typeguard(x: object, y: str) -> TypeNarrower[str]: +def typeguard(x: object, y: str) -> TypeIs[str]: ... @overload -def typeguard(x: object, y: int) -> TypeNarrower[int]: +def typeguard(x: object, y: int) -> TypeIs[int]: ... -def typeguard(x: object, y: Union[int, str]) -> Union[TypeNarrower[int], TypeNarrower[str]]: +def typeguard(x: object, y: Union[int, str]) -> Union[TypeIs[int], TypeIs[str]]: ... x: object @@ -659,22 +659,22 @@ if typeguard(y="42", x=x): reveal_type(x) # N: Revealed type is "builtins.str" [builtins fixtures/tuple.pyi] -[case testGenericAliasWithTypeNarrower] +[case testGenericAliasWithTypeIs] from typing import Callable, List, TypeVar -from typing_extensions import TypeNarrower, TypeAlias +from typing_extensions import TypeIs, TypeAlias -A = Callable[[object], TypeNarrower[List[T]]] -def foo(x: object) -> TypeNarrower[List[str]]: ... +A = Callable[[object], TypeIs[List[T]]] +def foo(x: object) -> TypeIs[List[str]]: ... def test(f: A[T]) -> T: ... reveal_type(test(foo)) # N: Revealed type is "builtins.str" [builtins fixtures/list.pyi] -[case testNoCrashOnDunderCallTypeNarrower] -from typing_extensions import TypeNarrower +[case testNoCrashOnDunderCallTypeIs] +from typing_extensions import TypeIs class A: - def __call__(self, x) -> TypeNarrower[int]: + def __call__(self, x) -> TypeIs[int]: return True a: A @@ -685,44 +685,44 @@ assert a(x=x) reveal_type(x) # N: Revealed type is "builtins.int" [builtins fixtures/tuple.pyi] -[case testTypeNarrowerMustBeSubtypeFunctions] -from typing_extensions import TypeNarrower +[case testTypeIsMustBeSubtypeFunctions] +from typing_extensions import TypeIs from typing import List, Sequence, TypeVar -def f(x: str) -> TypeNarrower[int]: # E: Narrowed type "int" is not a subtype of input type "str" +def f(x: str) -> TypeIs[int]: # E: Narrowed type "int" is not a subtype of input type "str" pass T = TypeVar('T') -def g(x: List[T]) -> TypeNarrower[Sequence[T]]: # E: Narrowed type "Sequence[T]" is not a subtype of input type "List[T]" +def g(x: List[T]) -> TypeIs[Sequence[T]]: # E: Narrowed type "Sequence[T]" is not a subtype of input type "List[T]" pass [builtins fixtures/tuple.pyi] -[case testTypeNarrowerMustBeSubtypeMethods] -from typing_extensions import TypeNarrower +[case testTypeIsMustBeSubtypeMethods] +from typing_extensions import TypeIs class NarrowHolder: @classmethod - def cls_narrower_good(cls, x: object) -> TypeNarrower[int]: + def cls_narrower_good(cls, x: object) -> TypeIs[int]: pass @classmethod - def cls_narrower_bad(cls, x: str) -> TypeNarrower[int]: # E: Narrowed type "int" is not a subtype of input type "str" + def cls_narrower_bad(cls, x: str) -> TypeIs[int]: # E: Narrowed type "int" is not a subtype of input type "str" pass @staticmethod - def static_narrower_good(x: object) -> TypeNarrower[int]: + def static_narrower_good(x: object) -> TypeIs[int]: pass @staticmethod - def static_narrower_bad(x: str) -> TypeNarrower[int]: # E: Narrowed type "int" is not a subtype of input type "str" + def static_narrower_bad(x: str) -> TypeIs[int]: # E: Narrowed type "int" is not a subtype of input type "str" pass - def inst_narrower_good(self, x: object) -> TypeNarrower[int]: + def inst_narrower_good(self, x: object) -> TypeIs[int]: pass - def inst_narrower_bad(self, x: str) -> TypeNarrower[int]: # E: Narrowed type "int" is not a subtype of input type "str" + def inst_narrower_bad(self, x: str) -> TypeIs[int]: # E: Narrowed type "int" is not a subtype of input type "str" pass diff --git a/test-data/unit/lib-stub/typing_extensions.pyi b/test-data/unit/lib-stub/typing_extensions.pyi index 65694ae63b86..890b46359fd6 100644 --- a/test-data/unit/lib-stub/typing_extensions.pyi +++ b/test-data/unit/lib-stub/typing_extensions.pyi @@ -34,7 +34,7 @@ Concatenate: _SpecialForm TypeAlias: _SpecialForm TypeGuard: _SpecialForm -TypeNarrower: _SpecialForm +TypeIs: _SpecialForm Never: _SpecialForm TypeVarTuple: _SpecialForm From eb883717d2be4698452a18fef3f8f2a91fe82fbf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 14 Feb 2024 16:29:28 +0000 Subject: [PATCH 16/27] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/expandtype.py | 4 +--- mypy/subtypes.py | 6 +++--- mypy/typeanal.py | 4 +--- mypy/types.py | 10 ++-------- 4 files changed, 7 insertions(+), 17 deletions(-) diff --git a/mypy/expandtype.py b/mypy/expandtype.py index abecf492888e..df3219813515 100644 --- a/mypy/expandtype.py +++ b/mypy/expandtype.py @@ -342,9 +342,7 @@ def visit_callable_type(self, t: CallableType) -> CallableType: arg_names=t.arg_names[:-2] + repl.arg_names, ret_type=t.ret_type.accept(self), type_guard=(t.type_guard.accept(self) if t.type_guard is not None else None), - type_is=( - t.type_is.accept(self) if t.type_is is not None else None - ), + type_is=(t.type_is.accept(self) if t.type_is is not None else None), imprecise_arg_kinds=(t.imprecise_arg_kinds or repl.imprecise_arg_kinds), variables=[*repl.variables, *t.variables], ) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 4b7bce8cc8d9..3c7f26818762 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -688,9 +688,9 @@ def visit_callable_type(self, left: CallableType) -> bool: # a TypeIs[Child] when a TypeIs[Parent] is expected, because # if the narrower returns False, we assume that the narrowed value is # *not* a Parent. - if not self._is_subtype( - left.type_is, right.type_is - ) or not self._is_subtype(right.type_is, left.type_is): + if not self._is_subtype(left.type_is, right.type_is) or not self._is_subtype( + right.type_is, left.type_is + ): return False elif right.type_guard is not None and left.type_guard is None: # This means that one function has `TypeGuard` and other does not. diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 0317c67d298e..722317ac9698 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -1075,9 +1075,7 @@ def anal_type_is(self, t: Type) -> Type | None: def anal_type_is_arg(self, t: UnboundType, fullname: str) -> Type | None: if fullname in ("typing_extensions.TypeIs", "typing.TypeIs"): if len(t.args) != 1: - self.fail( - "TypeIs must have exactly one type argument", t, code=codes.VALID_TYPE - ) + self.fail("TypeIs must have exactly one type argument", t, code=codes.VALID_TYPE) return AnyType(TypeOfAny.from_error) return self.anal_type(t.args[0]) return None diff --git a/mypy/types.py b/mypy/types.py index 164be95b2e5d..a31f7ede3c77 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -2229,9 +2229,7 @@ def serialize(self) -> JsonDict: "bound_args": [(None if t is None else t.serialize()) for t in self.bound_args], "def_extras": dict(self.def_extras), "type_guard": self.type_guard.serialize() if self.type_guard is not None else None, - "type_is": ( - self.type_is.serialize() if self.type_is is not None else None - ), + "type_is": (self.type_is.serialize() if self.type_is is not None else None), "from_concatenate": self.from_concatenate, "imprecise_arg_kinds": self.imprecise_arg_kinds, "unpack_kwargs": self.unpack_kwargs, @@ -2256,11 +2254,7 @@ def deserialize(cls, data: JsonDict) -> CallableType: type_guard=( deserialize_type(data["type_guard"]) if data["type_guard"] is not None else None ), - type_is=( - deserialize_type(data["type_is"]) - if data["type_is"] is not None - else None - ), + type_is=(deserialize_type(data["type_is"]) if data["type_is"] is not None else None), from_concatenate=data["from_concatenate"], imprecise_arg_kinds=data["imprecise_arg_kinds"], unpack_kwargs=data["unpack_kwargs"], From 1b1e368dabbbd8ef7923b5abcf8027a76b05e0fe Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 22 Feb 2024 14:25:01 -0800 Subject: [PATCH 17/27] Apply suggestions from code review Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- docs/source/error_code_list2.rst | 4 ++-- mypy/subtypes.py | 2 +- test-data/unit/check-typeis.test | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/source/error_code_list2.rst b/docs/source/error_code_list2.rst index 1a1827a68a31..81fde285e767 100644 --- a/docs/source/error_code_list2.rst +++ b/docs/source/error_code_list2.rst @@ -558,10 +558,10 @@ because there's no way one can import it. .. _code-type-is-not-subtype: -Check that TypeIs narrows types [type-is-not-subtype] +Check that ``TypeIs`` narrows types [type-is-not-subtype] ----------------------------------------------------------------- -:pep:`742` requires that when a ``TypeIs`` is used, the narrowed +:pep:`742` requires that when ``TypeIs`` is used, the narrowed type must be a subtype of the original type:: from typing_extensions import TypeIs diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 3c7f26818762..4d5e7335b14f 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -697,7 +697,7 @@ def visit_callable_type(self, left: CallableType) -> bool: # They are not compatible. See https://github.com/python/mypy/issues/11307 return False elif right.type_is is not None and left.type_is is None: - # Similarly, if one function has typeNarrower and the other does not, + # Similarly, if one function has `TypeIs` and the other does not, # they are not compatible. return False return is_callable_compatible( diff --git a/test-data/unit/check-typeis.test b/test-data/unit/check-typeis.test index ce67040e03ab..3a72dbfcb8ba 100644 --- a/test-data/unit/check-typeis.test +++ b/test-data/unit/check-typeis.test @@ -285,7 +285,6 @@ main:5: note: def is_float(self, a: object) -> bool [case testTypeIsInAnd] from typing import Any from typing_extensions import TypeIs -import types def isclass(a: object) -> bool: pass def ismethod(a: object) -> TypeIs[float]: @@ -661,7 +660,7 @@ if typeguard(y="42", x=x): [case testGenericAliasWithTypeIs] from typing import Callable, List, TypeVar -from typing_extensions import TypeIs, TypeAlias +from typing_extensions import TypeIs A = Callable[[object], TypeIs[List[T]]] def foo(x: object) -> TypeIs[List[str]]: ... From 84c69d26c813319d3fabbd80ecf151f7cf0c18d1 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 22 Feb 2024 20:51:48 -0800 Subject: [PATCH 18/27] Code review feedback, new test case, fix incorrect constraints --- mypy/constraints.py | 18 +++- mypy/message_registry.py | 3 +- test-data/unit/check-typeguard.test | 2 +- test-data/unit/check-typeis.test | 136 +++++++++++++++++----------- 4 files changed, 98 insertions(+), 61 deletions(-) diff --git a/mypy/constraints.py b/mypy/constraints.py index 7af87bd868e4..cdfa39ac45f3 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -1018,14 +1018,22 @@ def visit_callable_type(self, template: CallableType) -> list[Constraint]: param_spec = template.param_spec() template_ret_type, cactual_ret_type = template.ret_type, cactual.ret_type - if template.type_guard is not None: + if template.type_guard is not None and cactual.type_guard is not None: template_ret_type = template.type_guard - elif template.type_is is not None: - template_ret_type = template.type_is - if cactual.type_guard is not None: cactual_ret_type = cactual.type_guard - elif cactual.type_is is not None: + elif template.type_guard is not None: + template_ret_type = AnyType(TypeOfAny.special_form) + elif cactual.type_guard is not None: + cactual_ret_type = AnyType(TypeOfAny.special_form) + + if template.type_is is not None and cactual.type_is is not None: + template_ret_type = template.type_is cactual_ret_type = cactual.type_is + elif template.type_is is not None: + template_ret_type = AnyType(TypeOfAny.special_form) + elif cactual.type_is is not None: + cactual_ret_type = AnyType(TypeOfAny.special_form) + res.extend(infer_constraints(template_ret_type, cactual_ret_type, self.direction)) if param_spec is None: diff --git a/mypy/message_registry.py b/mypy/message_registry.py index d4b5687c4de1..a15071f8ae4f 100644 --- a/mypy/message_registry.py +++ b/mypy/message_registry.py @@ -325,5 +325,6 @@ def with_additional_msg(self, info: str) -> ErrorMessage: "Expected string literal for argument name, got {}", codes.SYNTAX ) TYPE_NARROWER_NOT_SUBTYPE: Final = ErrorMessage( - "Narrowed type {} is not a subtype of input type {}", codes.SYNTAX + "Narrowed type {} is not a subtype of input type {}", + codes.TYPE_NARROWER_NOT_SUBTYPE, ) diff --git a/test-data/unit/check-typeguard.test b/test-data/unit/check-typeguard.test index c48887bb016a..66c21bf3abe1 100644 --- a/test-data/unit/check-typeguard.test +++ b/test-data/unit/check-typeguard.test @@ -504,7 +504,7 @@ def with_bool(o: object) -> bool: pass accepts_typeguard(with_bool_typeguard) accepts_typeguard(with_str_typeguard) -accepts_typeguard(with_bool) # E: Argument 1 to "accepts_typeguard" has incompatible type "Callable[[object], bool]"; expected "Callable[[object], TypeGuard[bool]]" +accepts_typeguard(with_bool) # E: Argument 1 to "accepts_typeguard" has incompatible type "Callable[[object], bool]"; expected "Callable[[object], TypeGuard[Never]]" [builtins fixtures/tuple.pyi] [case testTypeGuardAsOverloadedFunctionArg] diff --git a/test-data/unit/check-typeis.test b/test-data/unit/check-typeis.test index 3a72dbfcb8ba..ce2492c2510a 100644 --- a/test-data/unit/check-typeis.test +++ b/test-data/unit/check-typeis.test @@ -240,14 +240,14 @@ def filter(f: Callable[[T], TypeIs[R]], it: Iterable[T]) -> Iterator[R]: ... def filter(f: Callable[[T], bool], it: Iterable[T]) -> Iterator[T]: ... def filter(*args): pass -def is_int_typeguard(a: object) -> TypeIs[int]: pass +def is_int_typeis(a: object) -> TypeIs[int]: pass def is_int_bool(a: object) -> bool: pass def main(a: List[Optional[int]]) -> None: bb = filter(lambda x: x is not None, a) reveal_type(bb) # N: Revealed type is "typing.Iterator[Union[builtins.int, None]]" # Also, if you replace 'bool' with 'Any' in the second overload, bb is Iterator[Any] - cc = filter(is_int_typeguard, a) + cc = filter(is_int_typeis, a) reveal_type(cc) # N: Revealed type is "typing.Iterator[builtins.int]" dd = filter(is_int_bool, a) reveal_type(dd) # N: Revealed type is "typing.Iterator[Union[builtins.int, None]]" @@ -287,18 +287,23 @@ from typing import Any from typing_extensions import TypeIs def isclass(a: object) -> bool: pass -def ismethod(a: object) -> TypeIs[float]: +def isfloat(a: object) -> TypeIs[float]: pass -def isfunction(a: object) -> TypeIs[str]: +def isstr(a: object) -> TypeIs[str]: pass -def isclassmethod(obj: Any) -> bool: - if ismethod(obj) and obj.__self__ is not None and isclass(obj.__self__): # E: "float" has no attribute "__self__" - return True +def coverage1(obj: Any) -> bool: + if isfloat(obj) and obj.__self__ is not None and isclass(obj.__self__): # E: "float" has no attribute "__self__" + reveal_type(obj) # N: Revealed type is "builtins.float" + return True + reveal_type(obj) # N: Revealed type is "Any" return False -def coverage(obj: Any) -> bool: - if not (ismethod(obj) or isfunction(obj)): + +def coverage2(obj: Any) -> bool: + if not (isfloat(obj) or isstr(obj)): + reveal_type(obj) # N: Revealed type is "Any" return True + reveal_type(obj) # N: Revealed type is "Union[builtins.float, builtins.str]" return False [builtins fixtures/classmethod.pyi] @@ -431,7 +436,7 @@ def foobar(x: object): return reveal_type(x) # N: Revealed type is "__main__." -def foobar_typeguard(x: object): +def foobar_typeis(x: object): if not is_foo(x) or not is_bar(x): return reveal_type(x) # N: Revealed type is "__main__." @@ -443,12 +448,12 @@ from typing_extensions import TypeIs def accepts_bool(f: Callable[[object], bool]): pass -def with_bool_typeguard(o: object) -> TypeIs[bool]: pass -def with_str_typeguard(o: object) -> TypeIs[str]: pass +def with_bool_typeis(o: object) -> TypeIs[bool]: pass +def with_str_typeis(o: object) -> TypeIs[str]: pass def with_bool(o: object) -> bool: pass -accepts_bool(with_bool_typeguard) -accepts_bool(with_str_typeguard) +accepts_bool(with_bool_typeis) +accepts_bool(with_str_typeis) accepts_bool(with_bool) [builtins fixtures/tuple.pyi] @@ -456,17 +461,17 @@ accepts_bool(with_bool) from typing import Callable from typing_extensions import TypeIs -def accepts_typeguard(f: Callable[[object], TypeIs[bool]]): pass -def different_typeguard(f: Callable[[object], TypeIs[str]]): pass +def accepts_typeis(f: Callable[[object], TypeIs[bool]]): pass +def different_typeis(f: Callable[[object], TypeIs[str]]): pass -def with_typeguard(o: object) -> TypeIs[bool]: pass +def with_typeis(o: object) -> TypeIs[bool]: pass def with_bool(o: object) -> bool: pass -accepts_typeguard(with_typeguard) -accepts_typeguard(with_bool) # E: Argument 1 to "accepts_typeguard" has incompatible type "Callable[[object], bool]"; expected "Callable[[object], TypeIs[bool]]" +accepts_typeis(with_typeis) +accepts_typeis(with_bool) # E: Argument 1 to "accepts_typeis" has incompatible type "Callable[[object], bool]"; expected "Callable[[object], TypeIs[bool]]" -different_typeguard(with_typeguard) # E: Argument 1 to "different_typeguard" has incompatible type "Callable[[object], TypeIs[bool]]"; expected "Callable[[object], TypeIs[str]]" -different_typeguard(with_bool) # E: Argument 1 to "different_typeguard" has incompatible type "Callable[[object], bool]"; expected "Callable[[object], TypeIs[str]]" +different_typeis(with_typeis) # E: Argument 1 to "different_typeis" has incompatible type "Callable[[object], TypeIs[bool]]"; expected "Callable[[object], TypeIs[str]]" +different_typeis(with_bool) # E: Argument 1 to "different_typeis" has incompatible type "Callable[[object], bool]"; expected "Callable[[object], TypeIs[str]]" [builtins fixtures/tuple.pyi] [case testTypeIsAsGenericFunctionArg] @@ -475,15 +480,15 @@ from typing_extensions import TypeIs T = TypeVar('T') -def accepts_typeguard(f: Callable[[object], TypeIs[T]]): pass +def accepts_typeis(f: Callable[[object], TypeIs[T]]): pass -def with_bool_typeguard(o: object) -> TypeIs[bool]: pass -def with_str_typeguard(o: object) -> TypeIs[str]: pass +def with_bool_typeis(o: object) -> TypeIs[bool]: pass +def with_str_typeis(o: object) -> TypeIs[str]: pass def with_bool(o: object) -> bool: pass -accepts_typeguard(with_bool_typeguard) -accepts_typeguard(with_str_typeguard) -accepts_typeguard(with_bool) # E: Argument 1 to "accepts_typeguard" has incompatible type "Callable[[object], bool]"; expected "Callable[[object], TypeIs[bool]]" +accepts_typeis(with_bool_typeis) +accepts_typeis(with_str_typeis) +accepts_typeis(with_bool) # E: Argument 1 to "accepts_typeis" has incompatible type "Callable[[object], bool]"; expected "Callable[[object], TypeIs[Never]]" [builtins fixtures/tuple.pyi] [case testTypeIsAsOverloadedFunctionArg] @@ -500,10 +505,10 @@ class filter(Generic[_T]): def __init__(self, function: Callable[[_T], Any]) -> None: pass def __init__(self, function): pass -def is_int_typeguard(a: object) -> TypeIs[int]: pass +def is_int_typeis(a: object) -> TypeIs[int]: pass def returns_bool(a: object) -> bool: pass -reveal_type(filter(is_int_typeguard)) # N: Revealed type is "__main__.filter[builtins.int]" +reveal_type(filter(is_int_typeis)) # N: Revealed type is "__main__.filter[builtins.int]" reveal_type(filter(returns_bool)) # N: Revealed type is "__main__.filter[builtins.object]" [builtins fixtures/tuple.pyi] @@ -515,15 +520,15 @@ class A: pass class B(A): pass class C(B): pass -def accepts_typeguard(f: Callable[[object], TypeIs[B]]): pass +def accepts_typeis(f: Callable[[object], TypeIs[B]]): pass -def with_typeguard_a(o: object) -> TypeIs[A]: pass -def with_typeguard_b(o: object) -> TypeIs[B]: pass -def with_typeguard_c(o: object) -> TypeIs[C]: pass +def with_typeis_a(o: object) -> TypeIs[A]: pass +def with_typeis_b(o: object) -> TypeIs[B]: pass +def with_typeis_c(o: object) -> TypeIs[C]: pass -accepts_typeguard(with_typeguard_a) # E: Argument 1 to "accepts_typeguard" has incompatible type "Callable[[object], TypeIs[A]]"; expected "Callable[[object], TypeIs[B]]" -accepts_typeguard(with_typeguard_b) -accepts_typeguard(with_typeguard_c) # E: Argument 1 to "accepts_typeguard" has incompatible type "Callable[[object], TypeIs[C]]"; expected "Callable[[object], TypeIs[B]]" +accepts_typeis(with_typeis_a) # E: Argument 1 to "accepts_typeis" has incompatible type "Callable[[object], TypeIs[A]]"; expected "Callable[[object], TypeIs[B]]" +accepts_typeis(with_typeis_b) +accepts_typeis(with_typeis_c) # E: Argument 1 to "accepts_typeis" has incompatible type "Callable[[object], TypeIs[C]]"; expected "Callable[[object], TypeIs[B]]" [builtins fixtures/tuple.pyi] [case testTypeIsWithIdentityGeneric] @@ -580,18 +585,18 @@ def func(names: Tuple[str, ...]): from typing_extensions import TypeIs class Z: - def typeguard1(self, *, x: object) -> TypeIs[int]: # E: TypeIs functions must have a positional argument + def typeis1(self, *, x: object) -> TypeIs[int]: # E: TypeIs functions must have a positional argument ... @staticmethod - def typeguard2(x: object) -> TypeIs[int]: + def typeis2(x: object) -> TypeIs[int]: ... @staticmethod - def typeguard3(*, x: object) -> TypeIs[int]: # E: TypeIs functions must have a positional argument + def typeis3(*, x: object) -> TypeIs[int]: # E: TypeIs functions must have a positional argument ... -def bad_typeguard(*, x: object) -> TypeIs[int]: # E: TypeIs functions must have a positional argument +def bad_typeis(*, x: object) -> TypeIs[int]: # E: TypeIs functions must have a positional argument ... [builtins fixtures/classmethod.pyi] @@ -600,17 +605,17 @@ def bad_typeguard(*, x: object) -> TypeIs[int]: # E: TypeIs functions must have from typing_extensions import TypeIs class Z: - def typeguard(self, x: object) -> TypeIs[int]: + def typeis(self, x: object) -> TypeIs[int]: ... -def typeguard(x: object) -> TypeIs[int]: +def typeis(x: object) -> TypeIs[int]: ... n: object -if typeguard(x=n): +if typeis(x=n): reveal_type(n) # N: Revealed type is "builtins.int" -if Z().typeguard(x=n): +if Z().typeis(x=n): reveal_type(n) # N: Revealed type is "builtins.int" [builtins fixtures/tuple.pyi] @@ -619,13 +624,13 @@ from typing_extensions import TypeIs class Y: @staticmethod - def typeguard(h: object) -> TypeIs[int]: + def typeis(h: object) -> TypeIs[int]: ... x: object -if Y().typeguard(x): +if Y().typeis(x): reveal_type(x) # N: Revealed type is "builtins.int" -if Y.typeguard(x): +if Y.typeis(x): reveal_type(x) # N: Revealed type is "builtins.int" [builtins fixtures/classmethod.pyi] @@ -634,27 +639,27 @@ from typing import overload, Union from typing_extensions import TypeIs @overload -def typeguard(x: object, y: str) -> TypeIs[str]: +def typeis(x: object, y: str) -> TypeIs[str]: ... @overload -def typeguard(x: object, y: int) -> TypeIs[int]: +def typeis(x: object, y: int) -> TypeIs[int]: ... -def typeguard(x: object, y: Union[int, str]) -> Union[TypeIs[int], TypeIs[str]]: +def typeis(x: object, y: Union[int, str]) -> Union[TypeIs[int], TypeIs[str]]: ... x: object -if typeguard(x=x, y=42): +if typeis(x=x, y=42): reveal_type(x) # N: Revealed type is "builtins.int" -if typeguard(y=42, x=x): +if typeis(y=42, x=x): reveal_type(x) # N: Revealed type is "builtins.int" -if typeguard(x=x, y="42"): +if typeis(x=x, y="42"): reveal_type(x) # N: Revealed type is "builtins.str" -if typeguard(y="42", x=x): +if typeis(y="42", x=x): reveal_type(x) # N: Revealed type is "builtins.str" [builtins fixtures/tuple.pyi] @@ -726,3 +731,26 @@ class NarrowHolder: [builtins fixtures/classmethod.pyi] + +[case testTypeIsTypeGuardNoSubtyping] +from typing_extensions import TypeGuard, TypeIs +from typing import Callable + +def accept_typeis(x: Callable[[object], TypeIs[str]]): + pass + +def accept_typeguard(x: Callable[[object], TypeGuard[str]]): + pass + +def typeis(x: object) -> TypeIs[str]: + pass + +def typeguard(x: object) -> TypeGuard[str]: + pass + +accept_typeis(typeis) +accept_typeis(typeguard) # E: Argument 1 to "accept_typeis" has incompatible type "Callable[[object], TypeGuard[str]]"; expected "Callable[[object], TypeIs[str]]" +accept_typeguard(typeis) # E: Argument 1 to "accept_typeguard" has incompatible type "Callable[[object], TypeIs[str]]"; expected "Callable[[object], TypeGuard[str]]" +accept_typeguard(typeguard) + +[builtins fixtures/tuple.pyi] From ae294bf01d50b661ecd09e422fcb4f84f4a0b373 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 23 Feb 2024 04:52:14 +0000 Subject: [PATCH 19/27] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/message_registry.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mypy/message_registry.py b/mypy/message_registry.py index a15071f8ae4f..9442b68079eb 100644 --- a/mypy/message_registry.py +++ b/mypy/message_registry.py @@ -325,6 +325,5 @@ def with_additional_msg(self, info: str) -> ErrorMessage: "Expected string literal for argument name, got {}", codes.SYNTAX ) TYPE_NARROWER_NOT_SUBTYPE: Final = ErrorMessage( - "Narrowed type {} is not a subtype of input type {}", - codes.TYPE_NARROWER_NOT_SUBTYPE, + "Narrowed type {} is not a subtype of input type {}", codes.TYPE_NARROWER_NOT_SUBTYPE ) From dbc229db6a2f4e2bfaa1f9ccb76c3aab185e2d78 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 29 Feb 2024 17:27:42 -0800 Subject: [PATCH 20/27] Rename error code --- mypy/checker.py | 2 +- mypy/errorcodes.py | 4 ++-- mypy/message_registry.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 435e146458fe..43d3472982a2 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1212,7 +1212,7 @@ def check_func_def( typ.type_is, typ.arg_types[arg_index] ): self.fail( - message_registry.TYPE_NARROWER_NOT_SUBTYPE.format( + message_registry.NARROWED_TYPE_NOT_SUBTYPE.format( format_type(typ.type_is, self.options), format_type(typ.arg_types[arg_index], self.options), ), diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py index e92e883e8517..688bd6a4ddd5 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -281,8 +281,8 @@ def __hash__(self) -> int: sub_code_of=MISC, ) -TYPE_NARROWER_NOT_SUBTYPE: Final[ErrorCode] = ErrorCode( - "type-is-not-subtype", +NARROWED_TYPE_NOT_SUBTYPE: Final[ErrorCode] = ErrorCode( + "narrowed-type-not-subtype", "Warn if a TypeIs function's narrowed type is not a subtype of the original type", "General", ) diff --git a/mypy/message_registry.py b/mypy/message_registry.py index 9442b68079eb..ccc1443dacf0 100644 --- a/mypy/message_registry.py +++ b/mypy/message_registry.py @@ -324,6 +324,6 @@ def with_additional_msg(self, info: str) -> ErrorMessage: ARG_NAME_EXPECTED_STRING_LITERAL: Final = ErrorMessage( "Expected string literal for argument name, got {}", codes.SYNTAX ) -TYPE_NARROWER_NOT_SUBTYPE: Final = ErrorMessage( - "Narrowed type {} is not a subtype of input type {}", codes.TYPE_NARROWER_NOT_SUBTYPE +NARROWED_TYPE_NOT_SUBTYPE: Final = ErrorMessage( + "Narrowed type {} is not a subtype of input type {}", codes.NARROWED_TYPE_NOT_SUBTYPE ) From 8b2fb0b9bcf75247377a8a2870e7d2ce9f994ddd Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 29 Feb 2024 17:28:11 -0800 Subject: [PATCH 21/27] Quote name --- mypy/semanal.py | 2 +- test-data/unit/check-typeis.test | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 69431f6796d5..6bf02382a036 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -883,7 +883,7 @@ def analyze_func_def(self, defn: FuncDef) -> None: result = result.copy_modified(type_guard=None) if result.type_is and ARG_POS not in result.arg_kinds[skip_self:]: self.fail( - "TypeIs functions must have a positional argument", + '"TypeIs" functions must have a positional argument', result, code=codes.VALID_TYPE, ) diff --git a/test-data/unit/check-typeis.test b/test-data/unit/check-typeis.test index ce2492c2510a..7ab134ab46c3 100644 --- a/test-data/unit/check-typeis.test +++ b/test-data/unit/check-typeis.test @@ -38,7 +38,7 @@ reveal_type(foo) # N: Revealed type is "def (a: builtins.object) -> TypeIs[buil from typing_extensions import TypeIs class Point: pass -def is_point() -> TypeIs[Point]: pass # E: TypeIs functions must have a positional argument +def is_point() -> TypeIs[Point]: pass # E: "TypeIs" functions must have a positional argument def main(a: object) -> None: if is_point(): reveal_type(a) # N: Revealed type is "builtins.object" @@ -585,7 +585,7 @@ def func(names: Tuple[str, ...]): from typing_extensions import TypeIs class Z: - def typeis1(self, *, x: object) -> TypeIs[int]: # E: TypeIs functions must have a positional argument + def typeis1(self, *, x: object) -> TypeIs[int]: # E: "TypeIs" functions must have a positional argument ... @staticmethod @@ -593,10 +593,10 @@ class Z: ... @staticmethod - def typeis3(*, x: object) -> TypeIs[int]: # E: TypeIs functions must have a positional argument + def typeis3(*, x: object) -> TypeIs[int]: # E: "TypeIs" functions must have a positional argument ... -def bad_typeis(*, x: object) -> TypeIs[int]: # E: TypeIs functions must have a positional argument +def bad_typeis(*, x: object) -> TypeIs[int]: # E: "TypeIs" functions must have a positional argument ... [builtins fixtures/classmethod.pyi] From 816fd1a90251709d2ba2257a44626317db2ad204 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 29 Feb 2024 17:32:42 -0800 Subject: [PATCH 22/27] unxfail --- test-data/unit/check-typeis.test | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test-data/unit/check-typeis.test b/test-data/unit/check-typeis.test index 7ab134ab46c3..5bbd57a6cd89 100644 --- a/test-data/unit/check-typeis.test +++ b/test-data/unit/check-typeis.test @@ -418,7 +418,7 @@ def test(x: List[Any]) -> None: g(reveal_type(x)) # N: Revealed type is "Union[builtins.list[builtins.str], __main__.]" [builtins fixtures/tuple.pyi] -[case testTypeIsMultipleCondition-xfail] +[case testTypeIsMultipleCondition] from typing_extensions import TypeIs from typing import Any, List @@ -439,7 +439,8 @@ def foobar(x: object): def foobar_typeis(x: object): if not is_foo(x) or not is_bar(x): return - reveal_type(x) # N: Revealed type is "__main__." + # Looks like a typo but this is what our unique name generation produces + reveal_type(x) # N: Revealed type is "__main__.1" [builtins fixtures/tuple.pyi] [case testTypeIsAsFunctionArgAsBoolSubtype] From d6fcc359553caff50968e48708a59ac8f0fcd476 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 29 Feb 2024 17:34:51 -0800 Subject: [PATCH 23/27] add elif test --- test-data/unit/check-typeis.test | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test-data/unit/check-typeis.test b/test-data/unit/check-typeis.test index 5bbd57a6cd89..b437651fd42e 100644 --- a/test-data/unit/check-typeis.test +++ b/test-data/unit/check-typeis.test @@ -9,6 +9,23 @@ def main(a: object) -> None: reveal_type(a) # N: Revealed type is "builtins.object" [builtins fixtures/tuple.pyi] +[case testTypeIsElif] +from typing_extensions import TypeIs +from typing import Union +class Point: pass +def is_point(a: object) -> TypeIs[Point]: pass +class Line: pass +def is_line(a: object) -> TypeIs[Line]: pass +def main(a: Union[Point, Line, int]) -> None: + if is_point(a): + reveal_type(a) # N: Revealed type is "__main__.Point" + elif is_line(a): + reveal_type(a) # N: Revealed type is "__main__.Line" + else: + reveal_type(a) # N: Revealed type is "builtins.int" + +[builtins fixtures/tuple.pyi] + [case testTypeIsTypeArgsNone] from typing_extensions import TypeIs def foo(a: object) -> TypeIs: # E: TypeIs must have exactly one type argument From ef825ce059deb8d1a95471c4c8758e59c8107f3b Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 29 Feb 2024 17:36:17 -0800 Subject: [PATCH 24/27] type context test --- test-data/unit/check-typeis.test | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test-data/unit/check-typeis.test b/test-data/unit/check-typeis.test index b437651fd42e..42f910d8289e 100644 --- a/test-data/unit/check-typeis.test +++ b/test-data/unit/check-typeis.test @@ -418,7 +418,8 @@ def is_bar(item: object) -> TypeIs[Bar]: def foobar(items: List[object]): a: List[Base] = [x for x in items if is_foo(x) or is_bar(x)] b: List[Base] = [x for x in items if is_foo(x)] - c: List[Bar] = [x for x in items if is_foo(x)] # E: List comprehension has incompatible type List[Foo]; expected List[Bar] + c: List[Foo] = [x for x in items if is_foo(x)] + d: List[Bar] = [x for x in items if is_foo(x)] # E: List comprehension has incompatible type List[Foo]; expected List[Bar] [builtins fixtures/tuple.pyi] [case testTypeIsNestedRestrictionUnionIsInstance] From d32956db40b095d31967018e69803395fb9ad714 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 29 Feb 2024 17:41:05 -0800 Subject: [PATCH 25/27] Add test --- test-data/unit/check-typeis.test | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test-data/unit/check-typeis.test b/test-data/unit/check-typeis.test index 42f910d8289e..6f54e621fd2a 100644 --- a/test-data/unit/check-typeis.test +++ b/test-data/unit/check-typeis.test @@ -570,6 +570,28 @@ def func2(name: str): reveal_type(name) # N: Revealed type is "builtins.str" [builtins fixtures/tuple.pyi] +[case testTypeIsWithGenericOnSecondParam] +from typing import TypeVar +from typing_extensions import TypeIs + +_R = TypeVar("_R") + +def guard(val: object, param: _R) -> TypeIs[_R]: + pass + +def func1(name: object): + reveal_type(name) # N: Revealed type is "builtins.object" + if guard(name, name): + reveal_type(name) # N: Revealed type is "builtins.object" + if guard(name, 1): + reveal_type(name) # N: Revealed type is "builtins.int" + +def func2(name: int): + reveal_type(name) # N: Revealed type is "builtins.int" + if guard(name, True): + reveal_type(name) # N: Revealed type is "builtins.bool" +[builtins fixtures/tuple.pyi] + [case testTypeIsWithGenericInstance] from typing import TypeVar, List, Iterable from typing_extensions import TypeIs From a36a16a20803a7b52109c8f2a2866412a7632d6a Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 29 Feb 2024 17:44:03 -0800 Subject: [PATCH 26/27] Add error code test case --- test-data/unit/check-errorcodes.test | 8 ++++++++ test-data/unit/check-typeis.test | 1 + 2 files changed, 9 insertions(+) diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index 7f5f05d37595..9d49480539e0 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -1182,3 +1182,11 @@ class D(C): def other(self) -> None: self.bad2: int = 5 # E: Covariant override of a mutable attribute (base class "C" defined the type as "float", expression has type "int") [mutable-override] [builtins fixtures/property.pyi] + +[case testNarrowedTypeNotSubtype] +from typing_extensions import TypeIs + +def f(x: str) -> TypeIs[int]: # E: Narrowed type "int" is not a subtype of input type "str" [narrowed-type-not-subtype] + pass + +[builtins fixtures/tuple.pyi] diff --git a/test-data/unit/check-typeis.test b/test-data/unit/check-typeis.test index 6f54e621fd2a..04b64a45c8c1 100644 --- a/test-data/unit/check-typeis.test +++ b/test-data/unit/check-typeis.test @@ -708,6 +708,7 @@ if typeis(y="42", x=x): from typing import Callable, List, TypeVar from typing_extensions import TypeIs +T = TypeVar('T') A = Callable[[object], TypeIs[List[T]]] def foo(x: object) -> TypeIs[List[str]]: ... From b32ba80e8070be35ccca06eda8de1a89229c79d6 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 29 Feb 2024 17:54:20 -0800 Subject: [PATCH 27/27] update docs --- docs/source/error_code_list2.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/error_code_list2.rst b/docs/source/error_code_list2.rst index 81fde285e767..465d1c7a6583 100644 --- a/docs/source/error_code_list2.rst +++ b/docs/source/error_code_list2.rst @@ -556,10 +556,10 @@ Correct usage: When this code is enabled, using ``reveal_locals`` is always an error, because there's no way one can import it. -.. _code-type-is-not-subtype: +.. _code-narrowed-type-not-subtype: -Check that ``TypeIs`` narrows types [type-is-not-subtype] ------------------------------------------------------------------ +Check that ``TypeIs`` narrows types [narrowed-type-not-subtype] +--------------------------------------------------------------- :pep:`742` requires that when ``TypeIs`` is used, the narrowed type must be a subtype of the original type::