Skip to content

Commit 838a1d4

Browse files
Add unimported-reveal error code (#16271)
Note: `reveal_type(1) # type: ignore` is problematic, because it silences the output. So, I've added some docs to advertise not doing so. Closes #16270 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent ffe89a2 commit 838a1d4

File tree

8 files changed

+163
-9
lines changed

8 files changed

+163
-9
lines changed

docs/source/error_code_list2.rst

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,3 +481,47 @@ Example:
481481
@override
482482
def g(self, y: int) -> None:
483483
pass
484+
485+
486+
.. _code-unimported-reveal:
487+
488+
Check that ``reveal_type`` is imported from typing or typing_extensions [unimported-reveal]
489+
-------------------------------------------------------------------------------------------
490+
491+
Mypy used to have ``reveal_type`` as a special builtin
492+
that only existed during type-checking.
493+
In runtime it fails with expected ``NameError``,
494+
which can cause real problem in production, hidden from mypy.
495+
496+
But, in Python3.11 ``reveal_type``
497+
`was added to typing.py <https://docs.python.org/3/library/typing.html#typing.reveal_type>`_.
498+
``typing_extensions`` ported this helper to all supported Python versions.
499+
500+
Now users can actually import ``reveal_type`` to make the runtime code safe.
501+
502+
.. note::
503+
504+
Starting with Python 3.11, the ``reveal_type`` function can be imported from ``typing``.
505+
To use it with older Python versions, import it from ``typing_extensions`` instead.
506+
507+
.. code-block:: python
508+
509+
# Use "mypy --enable-error-code unimported-reveal"
510+
511+
x = 1
512+
reveal_type(x) # Note: Revealed type is "builtins.int" \
513+
# Error: Name "reveal_type" is not defined
514+
515+
Correct usage:
516+
517+
.. code-block:: python
518+
519+
# Use "mypy --enable-error-code unimported-reveal"
520+
from typing import reveal_type # or `typing_extensions`
521+
522+
x = 1
523+
# This won't raise an error:
524+
reveal_type(x) # Note: Revealed type is "builtins.int"
525+
526+
When this code is enabled, using ``reveal_locals`` is always an error,
527+
because there's no way one can import it.

mypy/checkexpr.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
ARG_STAR2,
3737
IMPLICITLY_ABSTRACT,
3838
LITERAL_TYPE,
39+
REVEAL_LOCALS,
3940
REVEAL_TYPE,
4041
ArgKind,
4142
AssertTypeExpr,
@@ -4498,6 +4499,7 @@ def visit_reveal_expr(self, expr: RevealExpr) -> Type:
44984499
self.msg.note(
44994500
"'reveal_type' always outputs 'Any' in unchecked functions", expr.expr
45004501
)
4502+
self.check_reveal_imported(expr)
45014503
return revealed_type
45024504
else:
45034505
# REVEAL_LOCALS
@@ -4512,8 +4514,32 @@ def visit_reveal_expr(self, expr: RevealExpr) -> Type:
45124514
)
45134515

45144516
self.msg.reveal_locals(names_to_types, expr)
4517+
self.check_reveal_imported(expr)
45154518
return NoneType()
45164519

4520+
def check_reveal_imported(self, expr: RevealExpr) -> None:
4521+
if codes.UNIMPORTED_REVEAL not in self.chk.options.enabled_error_codes:
4522+
return
4523+
4524+
name = ""
4525+
if expr.kind == REVEAL_LOCALS:
4526+
name = "reveal_locals"
4527+
elif expr.kind == REVEAL_TYPE and not expr.is_imported:
4528+
name = "reveal_type"
4529+
else:
4530+
return
4531+
4532+
self.chk.fail(f'Name "{name}" is not defined', expr, code=codes.UNIMPORTED_REVEAL)
4533+
if name == "reveal_type":
4534+
module = (
4535+
"typing" if self.chk.options.python_version >= (3, 11) else "typing_extensions"
4536+
)
4537+
hint = (
4538+
'Did you forget to import it from "{module}"?'
4539+
' (Suggestion: "from {module} import {name}")'
4540+
).format(module=module, name=name)
4541+
self.chk.note(hint, expr, code=codes.UNIMPORTED_REVEAL)
4542+
45174543
def visit_type_application(self, tapp: TypeApplication) -> Type:
45184544
"""Type check a type application (expr[type, ...]).
45194545

mypy/errorcodes.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,12 @@ def __hash__(self) -> int:
249249
"General",
250250
default_enabled=False,
251251
)
252+
UNIMPORTED_REVEAL: Final = ErrorCode(
253+
"unimported-reveal",
254+
"Require explicit import from typing or typing_extensions for reveal_type",
255+
"General",
256+
default_enabled=False,
257+
)
252258

253259

254260
# Syntax errors are often blocking.

mypy/nodes.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2135,21 +2135,26 @@ def accept(self, visitor: ExpressionVisitor[T]) -> T:
21352135
class RevealExpr(Expression):
21362136
"""Reveal type expression reveal_type(expr) or reveal_locals() expression."""
21372137

2138-
__slots__ = ("expr", "kind", "local_nodes")
2138+
__slots__ = ("expr", "kind", "local_nodes", "is_imported")
21392139

2140-
__match_args__ = ("expr", "kind", "local_nodes")
2140+
__match_args__ = ("expr", "kind", "local_nodes", "is_imported")
21412141

21422142
expr: Expression | None
21432143
kind: int
21442144
local_nodes: list[Var] | None
21452145

21462146
def __init__(
2147-
self, kind: int, expr: Expression | None = None, local_nodes: list[Var] | None = None
2147+
self,
2148+
kind: int,
2149+
expr: Expression | None = None,
2150+
local_nodes: list[Var] | None = None,
2151+
is_imported: bool = False,
21482152
) -> None:
21492153
super().__init__()
21502154
self.expr = expr
21512155
self.kind = kind
21522156
self.local_nodes = local_nodes
2157+
self.is_imported = is_imported
21532158

21542159
def accept(self, visitor: ExpressionVisitor[T]) -> T:
21552160
return visitor.visit_reveal_expr(self)

mypy/semanal.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@
243243
DATACLASS_TRANSFORM_NAMES,
244244
FINAL_DECORATOR_NAMES,
245245
FINAL_TYPE_NAMES,
246+
IMPORTED_REVEAL_TYPE_NAMES,
246247
NEVER_NAMES,
247248
OVERLOAD_NAMES,
248249
OVERRIDE_DECORATOR_NAMES,
@@ -5056,7 +5057,17 @@ def visit_call_expr(self, expr: CallExpr) -> None:
50565057
elif refers_to_fullname(expr.callee, REVEAL_TYPE_NAMES):
50575058
if not self.check_fixed_args(expr, 1, "reveal_type"):
50585059
return
5059-
expr.analyzed = RevealExpr(kind=REVEAL_TYPE, expr=expr.args[0])
5060+
reveal_imported = False
5061+
reveal_type_node = self.lookup("reveal_type", expr, suppress_errors=True)
5062+
if (
5063+
reveal_type_node
5064+
and isinstance(reveal_type_node.node, FuncBase)
5065+
and reveal_type_node.fullname in IMPORTED_REVEAL_TYPE_NAMES
5066+
):
5067+
reveal_imported = True
5068+
expr.analyzed = RevealExpr(
5069+
kind=REVEAL_TYPE, expr=expr.args[0], is_imported=reveal_imported
5070+
)
50605071
expr.analyzed.line = expr.line
50615072
expr.analyzed.column = expr.column
50625073
expr.analyzed.accept(self)

mypy/types.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,8 @@
128128
"typing.Reversible",
129129
)
130130

131-
REVEAL_TYPE_NAMES: Final = (
132-
"builtins.reveal_type",
133-
"typing.reveal_type",
134-
"typing_extensions.reveal_type",
135-
)
131+
IMPORTED_REVEAL_TYPE_NAMES: Final = ("typing.reveal_type", "typing_extensions.reveal_type")
132+
REVEAL_TYPE_NAMES: Final = ("builtins.reveal_type", *IMPORTED_REVEAL_TYPE_NAMES)
136133

137134
ASSERT_TYPE_NAMES: Final = ("typing.assert_type", "typing_extensions.assert_type")
138135

test-data/unit/check-errorcodes.test

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1086,3 +1086,65 @@ def unsafe_func(x: object) -> Union[int, str]:
10861086
else:
10871087
return "some string"
10881088
[builtins fixtures/isinstancelist.pyi]
1089+
1090+
1091+
###
1092+
# unimported-reveal
1093+
###
1094+
1095+
[case testUnimportedRevealType]
1096+
# flags: --enable-error-code=unimported-reveal
1097+
x = 1
1098+
reveal_type(x)
1099+
[out]
1100+
main:3: error: Name "reveal_type" is not defined [unimported-reveal]
1101+
main:3: note: Did you forget to import it from "typing_extensions"? (Suggestion: "from typing_extensions import reveal_type")
1102+
main:3: note: Revealed type is "builtins.int"
1103+
[builtins fixtures/isinstancelist.pyi]
1104+
1105+
[case testUnimportedRevealTypePy311]
1106+
# flags: --enable-error-code=unimported-reveal --python-version=3.11
1107+
x = 1
1108+
reveal_type(x)
1109+
[out]
1110+
main:3: error: Name "reveal_type" is not defined [unimported-reveal]
1111+
main:3: note: Did you forget to import it from "typing"? (Suggestion: "from typing import reveal_type")
1112+
main:3: note: Revealed type is "builtins.int"
1113+
[builtins fixtures/isinstancelist.pyi]
1114+
1115+
[case testUnimportedRevealTypeInUncheckedFunc]
1116+
# flags: --enable-error-code=unimported-reveal
1117+
def unchecked():
1118+
x = 1
1119+
reveal_type(x)
1120+
[out]
1121+
main:4: error: Name "reveal_type" is not defined [unimported-reveal]
1122+
main:4: note: Did you forget to import it from "typing_extensions"? (Suggestion: "from typing_extensions import reveal_type")
1123+
main:4: note: Revealed type is "Any"
1124+
main:4: note: 'reveal_type' always outputs 'Any' in unchecked functions
1125+
[builtins fixtures/isinstancelist.pyi]
1126+
1127+
[case testUnimportedRevealTypeImportedTypingExtensions]
1128+
# flags: --enable-error-code=unimported-reveal
1129+
from typing_extensions import reveal_type
1130+
x = 1
1131+
reveal_type(x) # N: Revealed type is "builtins.int"
1132+
[builtins fixtures/isinstancelist.pyi]
1133+
1134+
[case testUnimportedRevealTypeImportedTyping311]
1135+
# flags: --enable-error-code=unimported-reveal --python-version=3.11
1136+
from typing import reveal_type
1137+
x = 1
1138+
reveal_type(x) # N: Revealed type is "builtins.int"
1139+
[builtins fixtures/isinstancelist.pyi]
1140+
[typing fixtures/typing-full.pyi]
1141+
1142+
[case testUnimportedRevealLocals]
1143+
# flags: --enable-error-code=unimported-reveal
1144+
x = 1
1145+
reveal_locals()
1146+
[out]
1147+
main:3: note: Revealed local types are:
1148+
main:3: note: x: builtins.int
1149+
main:3: error: Name "reveal_locals" is not defined [unimported-reveal]
1150+
[builtins fixtures/isinstancelist.pyi]

test-data/unit/fixtures/typing-full.pyi

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,3 +192,6 @@ def dataclass_transform(
192192
**kwargs: Any,
193193
) -> Callable[[T], T]: ...
194194
def override(__arg: T) -> T: ...
195+
196+
# Was added in 3.11
197+
def reveal_type(__obj: T) -> T: ...

0 commit comments

Comments
 (0)