Skip to content

Commit

Permalink
Add unimported-reveal error code (#16271)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
sobolevn and pre-commit-ci[bot] authored Oct 18, 2023
1 parent ffe89a2 commit 838a1d4
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 9 deletions.
44 changes: 44 additions & 0 deletions docs/source/error_code_list2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -481,3 +481,47 @@ Example:
@override
def g(self, y: int) -> None:
pass
.. _code-unimported-reveal:

Check that ``reveal_type`` is imported from typing or typing_extensions [unimported-reveal]
-------------------------------------------------------------------------------------------

Mypy used to have ``reveal_type`` as a special builtin
that only existed during type-checking.
In runtime it fails with expected ``NameError``,
which can cause real problem in production, hidden from mypy.

But, in Python3.11 ``reveal_type``
`was added to typing.py <https://docs.python.org/3/library/typing.html#typing.reveal_type>`_.
``typing_extensions`` ported this helper to all supported Python versions.

Now users can actually import ``reveal_type`` to make the runtime code safe.

.. note::

Starting with Python 3.11, the ``reveal_type`` function can be imported from ``typing``.
To use it with older Python versions, import it from ``typing_extensions`` instead.

.. code-block:: python
# Use "mypy --enable-error-code unimported-reveal"
x = 1
reveal_type(x) # Note: Revealed type is "builtins.int" \
# Error: Name "reveal_type" is not defined
Correct usage:

.. code-block:: python
# Use "mypy --enable-error-code unimported-reveal"
from typing import reveal_type # or `typing_extensions`
x = 1
# This won't raise an error:
reveal_type(x) # Note: Revealed type is "builtins.int"
When this code is enabled, using ``reveal_locals`` is always an error,
because there's no way one can import it.
26 changes: 26 additions & 0 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
ARG_STAR2,
IMPLICITLY_ABSTRACT,
LITERAL_TYPE,
REVEAL_LOCALS,
REVEAL_TYPE,
ArgKind,
AssertTypeExpr,
Expand Down Expand Up @@ -4498,6 +4499,7 @@ def visit_reveal_expr(self, expr: RevealExpr) -> Type:
self.msg.note(
"'reveal_type' always outputs 'Any' in unchecked functions", expr.expr
)
self.check_reveal_imported(expr)
return revealed_type
else:
# REVEAL_LOCALS
Expand All @@ -4512,8 +4514,32 @@ def visit_reveal_expr(self, expr: RevealExpr) -> Type:
)

self.msg.reveal_locals(names_to_types, expr)
self.check_reveal_imported(expr)
return NoneType()

def check_reveal_imported(self, expr: RevealExpr) -> None:
if codes.UNIMPORTED_REVEAL not in self.chk.options.enabled_error_codes:
return

name = ""
if expr.kind == REVEAL_LOCALS:
name = "reveal_locals"
elif expr.kind == REVEAL_TYPE and not expr.is_imported:
name = "reveal_type"
else:
return

self.chk.fail(f'Name "{name}" is not defined', expr, code=codes.UNIMPORTED_REVEAL)
if name == "reveal_type":
module = (
"typing" if self.chk.options.python_version >= (3, 11) else "typing_extensions"
)
hint = (
'Did you forget to import it from "{module}"?'
' (Suggestion: "from {module} import {name}")'
).format(module=module, name=name)
self.chk.note(hint, expr, code=codes.UNIMPORTED_REVEAL)

def visit_type_application(self, tapp: TypeApplication) -> Type:
"""Type check a type application (expr[type, ...]).
Expand Down
6 changes: 6 additions & 0 deletions mypy/errorcodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,12 @@ def __hash__(self) -> int:
"General",
default_enabled=False,
)
UNIMPORTED_REVEAL: Final = ErrorCode(
"unimported-reveal",
"Require explicit import from typing or typing_extensions for reveal_type",
"General",
default_enabled=False,
)


# Syntax errors are often blocking.
Expand Down
11 changes: 8 additions & 3 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2135,21 +2135,26 @@ def accept(self, visitor: ExpressionVisitor[T]) -> T:
class RevealExpr(Expression):
"""Reveal type expression reveal_type(expr) or reveal_locals() expression."""

__slots__ = ("expr", "kind", "local_nodes")
__slots__ = ("expr", "kind", "local_nodes", "is_imported")

__match_args__ = ("expr", "kind", "local_nodes")
__match_args__ = ("expr", "kind", "local_nodes", "is_imported")

expr: Expression | None
kind: int
local_nodes: list[Var] | None

def __init__(
self, kind: int, expr: Expression | None = None, local_nodes: list[Var] | None = None
self,
kind: int,
expr: Expression | None = None,
local_nodes: list[Var] | None = None,
is_imported: bool = False,
) -> None:
super().__init__()
self.expr = expr
self.kind = kind
self.local_nodes = local_nodes
self.is_imported = is_imported

def accept(self, visitor: ExpressionVisitor[T]) -> T:
return visitor.visit_reveal_expr(self)
Expand Down
13 changes: 12 additions & 1 deletion mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@
DATACLASS_TRANSFORM_NAMES,
FINAL_DECORATOR_NAMES,
FINAL_TYPE_NAMES,
IMPORTED_REVEAL_TYPE_NAMES,
NEVER_NAMES,
OVERLOAD_NAMES,
OVERRIDE_DECORATOR_NAMES,
Expand Down Expand Up @@ -5056,7 +5057,17 @@ def visit_call_expr(self, expr: CallExpr) -> None:
elif refers_to_fullname(expr.callee, REVEAL_TYPE_NAMES):
if not self.check_fixed_args(expr, 1, "reveal_type"):
return
expr.analyzed = RevealExpr(kind=REVEAL_TYPE, expr=expr.args[0])
reveal_imported = False
reveal_type_node = self.lookup("reveal_type", expr, suppress_errors=True)
if (
reveal_type_node
and isinstance(reveal_type_node.node, FuncBase)
and reveal_type_node.fullname in IMPORTED_REVEAL_TYPE_NAMES
):
reveal_imported = True
expr.analyzed = RevealExpr(
kind=REVEAL_TYPE, expr=expr.args[0], is_imported=reveal_imported
)
expr.analyzed.line = expr.line
expr.analyzed.column = expr.column
expr.analyzed.accept(self)
Expand Down
7 changes: 2 additions & 5 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,8 @@
"typing.Reversible",
)

REVEAL_TYPE_NAMES: Final = (
"builtins.reveal_type",
"typing.reveal_type",
"typing_extensions.reveal_type",
)
IMPORTED_REVEAL_TYPE_NAMES: Final = ("typing.reveal_type", "typing_extensions.reveal_type")
REVEAL_TYPE_NAMES: Final = ("builtins.reveal_type", *IMPORTED_REVEAL_TYPE_NAMES)

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

Expand Down
62 changes: 62 additions & 0 deletions test-data/unit/check-errorcodes.test
Original file line number Diff line number Diff line change
Expand Up @@ -1086,3 +1086,65 @@ def unsafe_func(x: object) -> Union[int, str]:
else:
return "some string"
[builtins fixtures/isinstancelist.pyi]


###
# unimported-reveal
###

[case testUnimportedRevealType]
# flags: --enable-error-code=unimported-reveal
x = 1
reveal_type(x)
[out]
main:3: error: Name "reveal_type" is not defined [unimported-reveal]
main:3: note: Did you forget to import it from "typing_extensions"? (Suggestion: "from typing_extensions import reveal_type")
main:3: note: Revealed type is "builtins.int"
[builtins fixtures/isinstancelist.pyi]

[case testUnimportedRevealTypePy311]
# flags: --enable-error-code=unimported-reveal --python-version=3.11
x = 1
reveal_type(x)
[out]
main:3: error: Name "reveal_type" is not defined [unimported-reveal]
main:3: note: Did you forget to import it from "typing"? (Suggestion: "from typing import reveal_type")
main:3: note: Revealed type is "builtins.int"
[builtins fixtures/isinstancelist.pyi]

[case testUnimportedRevealTypeInUncheckedFunc]
# flags: --enable-error-code=unimported-reveal
def unchecked():
x = 1
reveal_type(x)
[out]
main:4: error: Name "reveal_type" is not defined [unimported-reveal]
main:4: note: Did you forget to import it from "typing_extensions"? (Suggestion: "from typing_extensions import reveal_type")
main:4: note: Revealed type is "Any"
main:4: note: 'reveal_type' always outputs 'Any' in unchecked functions
[builtins fixtures/isinstancelist.pyi]

[case testUnimportedRevealTypeImportedTypingExtensions]
# flags: --enable-error-code=unimported-reveal
from typing_extensions import reveal_type
x = 1
reveal_type(x) # N: Revealed type is "builtins.int"
[builtins fixtures/isinstancelist.pyi]

[case testUnimportedRevealTypeImportedTyping311]
# flags: --enable-error-code=unimported-reveal --python-version=3.11
from typing import reveal_type
x = 1
reveal_type(x) # N: Revealed type is "builtins.int"
[builtins fixtures/isinstancelist.pyi]
[typing fixtures/typing-full.pyi]

[case testUnimportedRevealLocals]
# flags: --enable-error-code=unimported-reveal
x = 1
reveal_locals()
[out]
main:3: note: Revealed local types are:
main:3: note: x: builtins.int
main:3: error: Name "reveal_locals" is not defined [unimported-reveal]
[builtins fixtures/isinstancelist.pyi]
3 changes: 3 additions & 0 deletions test-data/unit/fixtures/typing-full.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,6 @@ def dataclass_transform(
**kwargs: Any,
) -> Callable[[T], T]: ...
def override(__arg: T) -> T: ...

# Was added in 3.11
def reveal_type(__obj: T) -> T: ...

0 comments on commit 838a1d4

Please sign in to comment.