Skip to content

Commit 0b79bf7

Browse files
committed
type-guard --work-properly
Also update some messages
1 parent fe8858d commit 0b79bf7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1158
-246
lines changed

.mypy/baseline.json

+162-98
Large diffs are not rendered by default.

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22

33
## [Unreleased]
44
### Added
5+
- type-guards have been reworked from the ground up (#516)
56
- `TypeGuard` is retained in inferred types (#504)
67
- Type narrowing is applied from lambda execution (#504)
78
- `--ide` flag (#501)
89
### Enhancements
910
- `show-error-context`/`pretty` are now on by default (#501)
1011
- Show fake column number when `--show-error-end` (#501)
12+
- Error messages point to basedmypy docs (#516)
13+
- `Callable` types in error messages don't contain `mypy_extensions` (#516)
1114
### Fixes
1215
- Don't show "X defined here" when error context is hidden (#498)
1316
- Fix issue with reveal code in ignore message (#490)

docs/source/based_features.rst

+85
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,91 @@ So you are able to have functions with polymorphic generic parameters:
9292
reveal_type(foo(["based"], "mypy")) # N: Revealed type is "list[str]"
9393
reveal_type(foo({1, 2}, 3)) # N: Revealed type is "set[int]"
9494
95+
Reinvented type guards
96+
----------------------
97+
98+
``TypeGuard`` acts similar to ``cast``, which is often sub-optimal and dangerous:
99+
100+
.. code-block:: python
101+
102+
def is_str_list(val: list[object]) -> TypeGuard[list[str]]:
103+
return all(isinstance(x, str) for x in val)
104+
105+
l1: list[object] = []
106+
l2 = l1
107+
108+
if is_str_list(l1):
109+
l2.append(100)
110+
reveal_type(l1[0]) # Revealed type is "str", at runtime it is 100
111+
112+
113+
class A: ...
114+
class B(A): ...
115+
def is_a(val: object) -> TypeGuard[A]: ...
116+
117+
b = B()
118+
if is_a(b):
119+
reveal_type(b) # A, not B
120+
121+
122+
Basedmypy introduces a simpler and more powerful denotation for type-guards, and changes their behavior
123+
to be safer.
124+
125+
.. code-block:: python
126+
127+
def is_int(value: object) -> value is int: ...
128+
129+
Type-guards don't widen:
130+
131+
.. code-block:: python
132+
133+
a: bool
134+
if is_int(a):
135+
reveal_type(a) # Revealed type is "bool"
136+
137+
Type-guards work on the implicit ``self`` and ``cls`` parameters:
138+
139+
.. code-block:: python
140+
141+
class A:
142+
def guard(self) -> self is B: ...
143+
class B(A): ...
144+
145+
a = A()
146+
if a.guard():
147+
reveal_type(a) # Revealed type is "B"
148+
149+
Invalid type-guards show an error:
150+
151+
.. code-block:: python
152+
153+
def guard(x: str) -> x is int: # error: A type-guard's type must be assignable to its parameter's type.
154+
155+
If you want to achieve something similar to the old ``TypeGuard``:
156+
157+
.. code-block:: python
158+
159+
def as_str_list(val: list[object]) -> list[str] | None:
160+
return (
161+
cast(list[str], val)
162+
if all(isinstance(x, str) for x in val)
163+
else None
164+
)
165+
166+
a: list[object]
167+
if (str_a := as_str_list(a)) is not None:
168+
...
169+
170+
# or
171+
172+
def is_str_list(val: list[object]) -> bool:
173+
return all(isinstance(x, str) for x in val)
174+
175+
a: list[object]
176+
if is_str_list(a):
177+
str_a = cast(list[str], a)
178+
...
179+
95180
Overload Implementation Inference
96181
---------------------------------
97182

docs/source/error_code_list3.rst

+27
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,31 @@ Check that type variables are used in accordance with their variance [unsafe-var
5454
) -> inT: # This usage of this contravariant type variable is unsafe as a return type. [unsafe-variance]
5555
pass
5656
57+
.. _code-typeguard-limitation:
58+
59+
Unsupported usages of typeguards [typeguard-limitation]
60+
-------------------------------------------------------
61+
62+
Mypy does not yet support typeguarding a star argument:
63+
64+
.. code-block:: python
65+
66+
def guard(x: object) -> x is int: ...
67+
68+
x: object
69+
xs = x,
70+
assert guard(*xs) # Type guard on star argument is not yet supported [typeguard-limitation]
71+
reveal_type(x) # object
72+
73+
.. _code-typeguard-subtype:
74+
75+
Check that typeguard definitions are valid [typeguard-subtype]
76+
--------------------------------------------------------------
77+
78+
.. code-block:: python
79+
80+
def guard(x: str) -> x is int: # A type-guard's type must be assignable to its parameter's type. (guard has type "int", parameter has type "str") [typeguard-subtype]
81+
...
82+
83+
5784
.. _code-reveal:

mypy/applytype.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from typing import Callable, Sequence
3+
from typing import Callable, Sequence, cast
44

55
import mypy.subtypes
66
from mypy.expandtype import expand_type, expand_unpack_with_variables
@@ -14,6 +14,7 @@
1414
PartialType,
1515
TupleType,
1616
Type,
17+
TypeGuardType,
1718
TypeVarId,
1819
TypeVarLikeType,
1920
TypeVarTupleType,
@@ -175,7 +176,7 @@ def apply_generic_arguments(
175176

176177
# Apply arguments to TypeGuard if any.
177178
if callable.type_guard is not None:
178-
type_guard = expand_type(callable.type_guard, id_to_type)
179+
type_guard = cast(TypeGuardType, expand_type(callable.type_guard, id_to_type))
179180
else:
180181
type_guard = None
181182

mypy/checker.py

+125-49
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@
168168
false_only,
169169
fixup_partial_type,
170170
function_type,
171+
get_type_type_item,
171172
get_type_vars,
172173
is_literal_type_like,
173174
is_singleton_type,
@@ -202,7 +203,6 @@
202203
Type,
203204
TypeAliasType,
204205
TypedDictType,
205-
TypeGuardedType,
206206
TypeOfAny,
207207
TypeTranslator,
208208
TypeType,
@@ -1397,6 +1397,31 @@ def check_func_def(
13971397
body_is_trivial = is_trivial_body(defn.body)
13981398
self.check_default_args(item, body_is_trivial)
13991399

1400+
if mypy.options._based and typ.type_guard:
1401+
if typ.type_guard.target_value in typ.arg_names:
1402+
idx = typ.arg_names.index(typ.type_guard.target_value) # type: ignore[arg-type]
1403+
elif typ.type_guard.target_is_positional:
1404+
idx = typ.type_guard.target_value # type: ignore[assignment]
1405+
else:
1406+
assert isinstance(typ.definition, FuncDef)
1407+
idx = [arg.variable.name for arg in typ.definition.arguments].index(
1408+
typ.type_guard.target_value # type: ignore[arg-type]
1409+
)
1410+
self.check_subtype(
1411+
typ.type_guard.type_guard,
1412+
typ.arg_types[idx],
1413+
typ.type_guard.type_guard,
1414+
ErrorMessage(
1415+
"A type-guard's type must be assignable to its parameter's type.",
1416+
code=codes.TYPEGUARD_SUBTYPE,
1417+
),
1418+
"guard has type",
1419+
"parameter has type",
1420+
notes=[
1421+
"If this is correct, try making it an intersection with the parameter type"
1422+
],
1423+
)
1424+
14001425
# Type check body in a new scope.
14011426
with self.binder.top_frame_context():
14021427
# Copy some type narrowings from an outer function when it seems safe enough
@@ -5675,64 +5700,115 @@ def find_isinstance_check_helper(self, node: Expression) -> tuple[TypeMap, TypeM
56755700
if is_false_literal(node):
56765701
return None, {}
56775702

5678-
if isinstance(node, CallExpr) and len(node.args) != 0:
5679-
expr = collapse_walrus(node.args[0])
5680-
if refers_to_fullname(node.callee, "builtins.isinstance"):
5681-
if len(node.args) != 2: # the error will be reported elsewhere
5682-
return {}, {}
5683-
if literal(expr) == LITERAL_TYPE:
5684-
return conditional_types_to_typemaps(
5685-
expr,
5686-
*self.conditional_types_with_intersection(
5687-
self.lookup_type(expr), self.get_isinstance_type(node.args[1]), expr
5688-
),
5689-
)
5690-
elif refers_to_fullname(node.callee, "builtins.issubclass"):
5691-
if len(node.args) != 2: # the error will be reported elsewhere
5692-
return {}, {}
5693-
if literal(expr) == LITERAL_TYPE:
5694-
return self.infer_issubclass_maps(node, expr)
5695-
elif refers_to_fullname(node.callee, "builtins.callable"):
5696-
if len(node.args) != 1: # the error will be reported elsewhere
5697-
return {}, {}
5698-
if literal(expr) == LITERAL_TYPE:
5699-
vartype = self.lookup_type(expr)
5700-
return self.conditional_callable_type_map(expr, vartype)
5701-
elif refers_to_fullname(node.callee, "builtins.hasattr"):
5702-
if len(node.args) != 2: # the error will be reported elsewhere
5703-
return {}, {}
5704-
attr = try_getting_str_literals(node.args[1], self.lookup_type(node.args[1]))
5705-
if literal(expr) == LITERAL_TYPE and attr and len(attr) == 1:
5706-
return self.hasattr_type_maps(expr, self.lookup_type(expr), attr[0])
5707-
elif isinstance(node.callee, (RefExpr, CallExpr, LambdaExpr)):
5703+
if isinstance(node, CallExpr):
5704+
if len(node.args) != 0:
5705+
expr = collapse_walrus(node.args[0])
5706+
if refers_to_fullname(node.callee, "builtins.isinstance"):
5707+
if len(node.args) != 2: # the error will be reported elsewhere
5708+
return {}, {}
5709+
if literal(expr) == LITERAL_TYPE:
5710+
return conditional_types_to_typemaps(
5711+
expr,
5712+
*self.conditional_types_with_intersection(
5713+
self.lookup_type(expr),
5714+
self.get_isinstance_type(node.args[1]),
5715+
expr,
5716+
),
5717+
)
5718+
elif refers_to_fullname(node.callee, "builtins.issubclass"):
5719+
if len(node.args) != 2: # the error will be reported elsewhere
5720+
return {}, {}
5721+
if literal(expr) == LITERAL_TYPE:
5722+
return self.infer_issubclass_maps(node, expr)
5723+
elif refers_to_fullname(node.callee, "builtins.callable"):
5724+
if len(node.args) != 1: # the error will be reported elsewhere
5725+
return {}, {}
5726+
if literal(expr) == LITERAL_TYPE:
5727+
vartype = self.lookup_type(expr)
5728+
return self.conditional_callable_type_map(expr, vartype)
5729+
elif refers_to_fullname(node.callee, "builtins.hasattr"):
5730+
if len(node.args) != 2: # the error will be reported elsewhere
5731+
return {}, {}
5732+
attr = try_getting_str_literals(node.args[1], self.lookup_type(node.args[1]))
5733+
if literal(expr) == LITERAL_TYPE and attr and len(attr) == 1:
5734+
return self.hasattr_type_maps(expr, self.lookup_type(expr), attr[0])
5735+
if isinstance(node.callee, (RefExpr, CallExpr, LambdaExpr)):
5736+
# TODO: AssignmentExpr `(a := A())()`
57085737
if node.callee.type_guard is not None:
57095738
# TODO: Follow *args, **kwargs
5710-
if node.arg_kinds[0] != nodes.ARG_POS:
5711-
# the first argument might be used as a kwarg
5712-
called_type = get_proper_type(self.lookup_type(node.callee))
5713-
assert isinstance(called_type, (CallableType, Overloaded))
5714-
5715-
# *assuming* the overloaded function is correct, there's a couple cases:
5716-
# 1) The first argument has different names, but is pos-only. We don't
5717-
# care about this case, the argument must be passed positionally.
5718-
# 2) The first argument allows keyword reference, therefore must be the
5719-
# same between overloads.
5720-
name = called_type.items[0].arg_names[0]
5721-
5722-
if name in node.arg_names:
5723-
idx = node.arg_names.index(name)
5724-
# we want the idx-th variable to be narrowed
5725-
expr = collapse_walrus(node.args[idx])
5739+
called_type = get_proper_type(self.lookup_type(node.callee))
5740+
if isinstance(called_type, Instance):
5741+
called_type = get_proper_type(
5742+
analyze_member_access(
5743+
name="__call__",
5744+
typ=called_type,
5745+
context=node,
5746+
is_lvalue=False,
5747+
is_super=False,
5748+
is_operator=True,
5749+
msg=self.msg,
5750+
original_type=called_type,
5751+
chk=self,
5752+
)
5753+
)
5754+
assert isinstance(called_type, (CallableType, Overloaded))
5755+
guard = node.callee.type_guard
5756+
target = guard.target_value
5757+
if guard.target_is_class or guard.target_is_self:
5758+
if isinstance(node.callee, (CallExpr, NameExpr)):
5759+
expr = node.callee
5760+
elif isinstance(node.callee, MemberExpr):
5761+
expr = node.callee.expr
57265762
else:
5727-
self.fail(message_registry.TYPE_GUARD_POS_ARG_REQUIRED, node)
5763+
raise AssertionError("What is this?")
5764+
if guard.target_is_class and isinstance(
5765+
get_proper_type(self.lookup_type(expr)), Instance
5766+
):
5767+
guarded = get_type_type_item(
5768+
get_proper_type(node.callee.type_guard.type_guard)
5769+
)
5770+
if not guarded:
5771+
return {}, {}
5772+
else:
5773+
guarded = node.callee.type_guard.type_guard
5774+
return {collapse_walrus(expr): guarded}, {}
5775+
if isinstance(target, int):
5776+
idx = target
5777+
if called_type.items[0].def_extras.get("first_arg"):
5778+
self.fail(
5779+
"type-guard on positional class function is unsupported",
5780+
node,
5781+
code=codes.TYPEGUARD_LIMITATION,
5782+
)
5783+
return {}, {}
5784+
elif target in called_type.items[0].arg_names:
5785+
idx = called_type.items[0].arg_names.index(target)
5786+
elif target == "first argument":
5787+
idx = 0
5788+
if not node.args:
57285789
return {}, {}
5790+
else:
5791+
definition = called_type.items[0].definition
5792+
assert isinstance(definition, FuncDef)
5793+
idx = [arg.variable.name for arg in definition.arguments].index(target)
5794+
if target in node.arg_names:
5795+
idx = node.arg_names.index(target) # type: ignore[arg-type]
5796+
if len(node.arg_kinds) <= idx:
5797+
return {}, {}
5798+
if node.arg_kinds[idx].is_star():
5799+
self.fail(message_registry.TYPE_GUARD_POS_LIMITATION, node)
5800+
return {}, {}
5801+
# we want the idx-th variable to be narrowed
5802+
expr = collapse_walrus(node.args[idx])
57295803
if literal(expr) == LITERAL_TYPE:
57305804
# Note: we wrap the target type, so that we can special case later.
57315805
# Namely, for isinstance() we use a normal meet, while TypeGuard is
57325806
# considered "always right" (i.e. even if the types are not overlapping).
57335807
# Also note that a care must be taken to unwrap this back at read places
57345808
# where we use this to narrow down declared type.
5735-
return {expr: TypeGuardedType(node.callee.type_guard)}, {}
5809+
#
5810+
# Based: We don't do any of that.
5811+
return {expr: node.callee.type_guard.type_guard}, {}
57365812
if isinstance(node, CallExpr) and isinstance(node.callee, LambdaExpr):
57375813
return self.find_isinstance_check_helper(
57385814
safe(cast(ReturnStmt, node.callee.body.body[0]).expr)

0 commit comments

Comments
 (0)