From 131e664af14c0e11aa6e972313097fb97d98697f Mon Sep 17 00:00:00 2001 From: sobolevn Date: Mon, 16 Oct 2023 17:03:06 +0300 Subject: [PATCH 1/3] Add `unimported-reveal` error code --- docs/source/error_code_list2.rst | 48 +++++++++++++++++++ mypy/checkexpr.py | 26 +++++++++++ mypy/errorcodes.py | 6 +++ mypy/nodes.py | 11 +++-- mypy/semanal.py | 11 ++++- mypy/types.py | 7 ++- test-data/unit/check-errorcodes.test | 62 +++++++++++++++++++++++++ test-data/unit/fixtures/typing-full.pyi | 3 ++ 8 files changed, 168 insertions(+), 6 deletions(-) diff --git a/docs/source/error_code_list2.rst b/docs/source/error_code_list2.rst index 30fad0793771..8e6d212e64db 100644 --- a/docs/source/error_code_list2.rst +++ b/docs/source/error_code_list2.rst @@ -481,3 +481,51 @@ 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 `_. +``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) # 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 + reveal_type(x) # OK + +When this code is enabled, using ``reveal_locals`` is always an error, +because there's no way one can import it. + +.. note:: + + Using ``type: ignore`` can be problematic with this error, + because it can silence ``reveal_type`` output. + So, it is better not to do it. diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index a1dd6d830758..a27bf717ea58 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -37,6 +37,7 @@ IMPLICITLY_ABSTRACT, LITERAL_TYPE, REVEAL_TYPE, + REVEAL_LOCALS, ArgKind, AssertTypeExpr, AssignmentExpr, @@ -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 @@ -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, ...]). diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py index cd9978c2f31c..98600679da53 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -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. diff --git a/mypy/nodes.py b/mypy/nodes.py index 6556cd910b46..0e5c078d0227 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -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) diff --git a/mypy/semanal.py b/mypy/semanal.py index a476b62b31ec..73e56a4302b3 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -247,6 +247,7 @@ OVERRIDE_DECORATOR_NAMES, PROTOCOL_NAMES, REVEAL_TYPE_NAMES, + IMPORTED_REVEAL_TYPE_NAMES, TPDICT_NAMES, TYPE_ALIAS_NAMES, TYPED_NAMEDTUPLE_NAMES, @@ -5040,7 +5041,15 @@ 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) diff --git a/mypy/types.py b/mypy/types.py index 09ba68aae88a..af0f0802d26f 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -128,11 +128,14 @@ "typing.Reversible", ) -REVEAL_TYPE_NAMES: Final = ( - "builtins.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") diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index ac7c8b4c9f9d..2282f21bcfa6 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -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] diff --git a/test-data/unit/fixtures/typing-full.pyi b/test-data/unit/fixtures/typing-full.pyi index 417ae6baf491..e9f0aa199bb4 100644 --- a/test-data/unit/fixtures/typing-full.pyi +++ b/test-data/unit/fixtures/typing-full.pyi @@ -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: ... From 3d9161edc3b8543c0c487a1430fd9ea03424fe50 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 14:06:06 +0000 Subject: [PATCH 2/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checkexpr.py | 2 +- mypy/semanal.py | 12 +++++++----- mypy/types.py | 10 ++-------- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index a27bf717ea58..5ea80ca11bd9 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -36,8 +36,8 @@ ARG_STAR2, IMPLICITLY_ABSTRACT, LITERAL_TYPE, - REVEAL_TYPE, REVEAL_LOCALS, + REVEAL_TYPE, ArgKind, AssertTypeExpr, AssignmentExpr, diff --git a/mypy/semanal.py b/mypy/semanal.py index 73e56a4302b3..981e580f1e67 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -242,12 +242,12 @@ DATACLASS_TRANSFORM_NAMES, FINAL_DECORATOR_NAMES, FINAL_TYPE_NAMES, + IMPORTED_REVEAL_TYPE_NAMES, NEVER_NAMES, OVERLOAD_NAMES, OVERRIDE_DECORATOR_NAMES, PROTOCOL_NAMES, REVEAL_TYPE_NAMES, - IMPORTED_REVEAL_TYPE_NAMES, TPDICT_NAMES, TYPE_ALIAS_NAMES, TYPED_NAMEDTUPLE_NAMES, @@ -5043,12 +5043,14 @@ def visit_call_expr(self, expr: CallExpr) -> None: return 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: + 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, + kind=REVEAL_TYPE, expr=expr.args[0], is_imported=reveal_imported ) expr.analyzed.line = expr.line expr.analyzed.column = expr.column diff --git a/mypy/types.py b/mypy/types.py index af0f0802d26f..1285cf3b0b07 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -128,14 +128,8 @@ "typing.Reversible", ) -IMPORTED_REVEAL_TYPE_NAMES: Final = ( - "typing.reveal_type", - "typing_extensions.reveal_type", -) -REVEAL_TYPE_NAMES: Final = ( - "builtins.reveal_type", - *IMPORTED_REVEAL_TYPE_NAMES, -) +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") From 8a7395a41487f8865209c1505edff625ce5d1633 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Wed, 18 Oct 2023 10:57:45 +0300 Subject: [PATCH 3/3] Update docs --- docs/source/error_code_list2.rst | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/docs/source/error_code_list2.rst b/docs/source/error_code_list2.rst index 8e6d212e64db..cc5c9b0a1bc6 100644 --- a/docs/source/error_code_list2.rst +++ b/docs/source/error_code_list2.rst @@ -488,10 +488,10 @@ Example: Check that ``reveal_type`` is imported from typing or typing_extensions [unimported-reveal] ------------------------------------------------------------------------------------------- -MyPy used to have ``reveal_type`` as a special builtin +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. +which can cause real problem in production, hidden from mypy. But, in Python3.11 ``reveal_type`` `was added to typing.py `_. @@ -509,7 +509,8 @@ Now users can actually import ``reveal_type`` to make the runtime code safe. # Use "mypy --enable-error-code unimported-reveal" x = 1 - reveal_type(x) # Error: Name "reveal_type" is not defined + reveal_type(x) # Note: Revealed type is "builtins.int" \ + # Error: Name "reveal_type" is not defined Correct usage: @@ -519,13 +520,8 @@ Correct usage: from typing import reveal_type # or `typing_extensions` x = 1 - reveal_type(x) # OK + # 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. - -.. note:: - - Using ``type: ignore`` can be problematic with this error, - because it can silence ``reveal_type`` output. - So, it is better not to do it.