From c8a0499245cc790e081a245b79b40990950c020e Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 21 Apr 2024 08:15:16 -0700 Subject: [PATCH 01/11] Initial version of RawExpr change --- mypy/fastparse.py | 11 +++-------- mypy/server/astmerge.py | 3 ++- mypy/type_visitor.py | 4 ++++ mypy/typeanal.py | 5 ++++- mypy/types.py | 22 ++++++++++++++++++++-- mypy/typetraverser.py | 3 ++- test-data/unit/check-literal.test | 4 ++++ 7 files changed, 39 insertions(+), 13 deletions(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index a155187992ec..e208e4d0b7d9 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -319,14 +319,7 @@ def parse_type_string( """ try: _, node = parse_type_comment(f"({expr_string})", line=line, column=column, errors=None) - if isinstance(node, UnboundType) and node.original_str_expr is None: - node.original_str_expr = expr_string - node.original_str_fallback = expr_fallback_name - return node - elif isinstance(node, UnionType): - return node - else: - return RawExpressionType(expr_string, expr_fallback_name, line, column) + return RawExpressionType(expr_string, expr_fallback_name, line, column, node=node) except (SyntaxError, ValueError): # Note: the parser will raise a `ValueError` instead of a SyntaxError if # the string happens to contain things like \x00. @@ -1034,6 +1027,8 @@ def set_type_optional(self, type: Type | None, initializer: Expression | None) - return # Indicate that type should be wrapped in an Optional if arg is initialized to None. optional = isinstance(initializer, NameExpr) and initializer.name == "None" + if isinstance(type, RawExpressionType) and type.node is not None: + type = type.node if isinstance(type, UnboundType): type.optional = optional diff --git a/mypy/server/astmerge.py b/mypy/server/astmerge.py index 174c2922c767..e6648fbb4be7 100644 --- a/mypy/server/astmerge.py +++ b/mypy/server/astmerge.py @@ -507,7 +507,8 @@ def visit_typeddict_type(self, typ: TypedDictType) -> None: typ.fallback.accept(self) def visit_raw_expression_type(self, t: RawExpressionType) -> None: - pass + if t.node is not None: + t.node.accept(self) def visit_literal_type(self, typ: LiteralType) -> None: typ.fallback.accept(self) diff --git a/mypy/type_visitor.py b/mypy/type_visitor.py index 1860a43eb14f..a6ae77832ceb 100644 --- a/mypy/type_visitor.py +++ b/mypy/type_visitor.py @@ -376,6 +376,8 @@ def visit_typeddict_type(self, t: TypedDictType) -> T: return self.query_types(t.items.values()) def visit_raw_expression_type(self, t: RawExpressionType) -> T: + if t.node is not None: + return t.node.accept(self) return self.strategy([]) def visit_literal_type(self, t: LiteralType) -> T: @@ -516,6 +518,8 @@ def visit_typeddict_type(self, t: TypedDictType) -> bool: return self.query_types(list(t.items.values())) def visit_raw_expression_type(self, t: RawExpressionType) -> bool: + if t.node is not None: + return t.node.accept(self) return self.default def visit_literal_type(self, t: LiteralType) -> bool: diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 3f4b86185f2d..93e98c302e13 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -1195,6 +1195,8 @@ def visit_raw_expression_type(self, t: RawExpressionType) -> Type: # make signatures like "foo(x: 20) -> None" legal, we can change # this method so it generates and returns an actual LiteralType # instead. + if t.node is not None: + return t.node.accept(self) if self.report_invalid_types: if t.base_type_name in ("builtins.int", "builtins.bool"): @@ -2528,7 +2530,8 @@ def visit_typeddict_type(self, t: TypedDictType) -> None: self.process_types(list(t.items.values())) def visit_raw_expression_type(self, t: RawExpressionType) -> None: - pass + if t.node is not None: + t.node.accept(self) def visit_literal_type(self, t: LiteralType) -> None: pass diff --git a/mypy/types.py b/mypy/types.py index b4209e9debf4..39a399c9cdba 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -13,6 +13,7 @@ Iterable, NamedTuple, NewType, + Optional, Sequence, TypeVar, Union, @@ -2646,7 +2647,7 @@ class RawExpressionType(ProperType): This synthetic type is only used at the beginning stages of semantic analysis and should be completely removing during the process for mapping UnboundTypes to - actual types: we either turn it into a LiteralType or an AnyType. + actual types: we turn it into its "node" argument, a LiteralType, or an AnyType. For example, suppose `Foo[1]` is initially represented as the following: @@ -2684,7 +2685,7 @@ class RawExpressionType(ProperType): ) """ - __slots__ = ("literal_value", "base_type_name", "note") + __slots__ = ("literal_value", "base_type_name", "note", "node") def __init__( self, @@ -2693,11 +2694,13 @@ def __init__( line: int = -1, column: int = -1, note: str | None = None, + node: ProperType | None = None, ) -> None: super().__init__(line, column) self.literal_value = literal_value self.base_type_name = base_type_name self.note = note + self.node = node def simple_name(self) -> str: return self.base_type_name.replace("builtins.", "") @@ -2707,6 +2710,16 @@ def accept(self, visitor: TypeVisitor[T]) -> T: ret: T = visitor.visit_raw_expression_type(self) return ret + def copy_modified(self, node: ProperType | None) -> RawExpressionType: + return RawExpressionType( + literal_value=self.literal_value, + base_type_name=self.base_type_name, + line=self.line, + column=self.column, + note=self.note, + node=node, + ) + def serialize(self) -> JsonDict: assert False, "Synthetic types don't serialize" @@ -3386,6 +3399,8 @@ def item_str(name: str, typ: str) -> str: return f"TypedDict({prefix}{s})" def visit_raw_expression_type(self, t: RawExpressionType) -> str: + if t.node is not None: + return f"{t.literal_value!r}={t.node.accept(self)}" return repr(t.literal_value) def visit_literal_type(self, t: LiteralType) -> str: @@ -3449,6 +3464,9 @@ def visit_ellipsis_type(self, t: EllipsisType) -> Type: return t def visit_raw_expression_type(self, t: RawExpressionType) -> Type: + if t.node is not None: + node = t.node.accept(self) + return t.copy_modified(node=node) return t def visit_type_list(self, t: TypeList) -> Type: diff --git a/mypy/typetraverser.py b/mypy/typetraverser.py index a28bbf422b61..4d740a802b55 100644 --- a/mypy/typetraverser.py +++ b/mypy/typetraverser.py @@ -130,7 +130,8 @@ def visit_partial_type(self, t: PartialType) -> None: pass def visit_raw_expression_type(self, t: RawExpressionType) -> None: - pass + if t.node is not None: + t.node.accept(self) def visit_type_alias_type(self, t: TypeAliasType) -> None: # TODO: sometimes we want to traverse target as well diff --git a/test-data/unit/check-literal.test b/test-data/unit/check-literal.test index 5604cc4b5893..3cf6e8ff17e9 100644 --- a/test-data/unit/check-literal.test +++ b/test-data/unit/check-literal.test @@ -12,8 +12,12 @@ reveal_type(g1) # N: Revealed type is "def (x: Literal['A['])" def f2(x: 'A B') -> None: pass # E: Invalid type comment or annotation def g2(x: Literal['A B']) -> None: pass +def h2(x: 'A|int') -> None: pass # E: Name "A" is not defined +def i2(x: Literal['A|B']) -> None: pass reveal_type(f2) # N: Revealed type is "def (x: Any)" reveal_type(g2) # N: Revealed type is "def (x: Literal['A B'])" +reveal_type(h2) # N: Revealed type is "def (x: Union[Any, builtins.int])" +reveal_type(i2) # N: Revealed type is "def (x: Literal['A|B'])" [builtins fixtures/tuple.pyi] [out] From 5f9d1f53e07a96a8736fa0d9a664e3bb4b725ff8 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 21 Apr 2024 08:16:18 -0700 Subject: [PATCH 02/11] Fix parser tests --- mypy/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/types.py b/mypy/types.py index 39a399c9cdba..f323929bdac6 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -3400,7 +3400,7 @@ def item_str(name: str, typ: str) -> str: def visit_raw_expression_type(self, t: RawExpressionType) -> str: if t.node is not None: - return f"{t.literal_value!r}={t.node.accept(self)}" + return t.node.accept(self) return repr(t.literal_value) def visit_literal_type(self, t: LiteralType) -> str: From 757a8e6f6710f5a0d2f43e17c35dda5020488e4b Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 21 Apr 2024 08:21:12 -0700 Subject: [PATCH 03/11] fix stubgen test --- mypy/stubutil.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/mypy/stubutil.py b/mypy/stubutil.py index 410672f89d09..55fcebef0ffc 100644 --- a/mypy/stubutil.py +++ b/mypy/stubutil.py @@ -17,7 +17,7 @@ from mypy.modulefinder import ModuleNotFoundReason from mypy.moduleinspect import InspectError, ModuleInspect from mypy.stubdoc import ArgSig, FunctionSig -from mypy.types import AnyType, NoneType, Type, TypeList, TypeStrVisitor, UnboundType, UnionType +from mypy.types import AnyType, NoneType, RawExpressionType, Type, TypeList, TypeStrVisitor, UnboundType, UnionType # Modules that may fail when imported, or that may have side effects (fully qualified). NOT_IMPORTABLE_MODULES = () @@ -291,12 +291,11 @@ def args_str(self, args: Iterable[Type]) -> str: The main difference from list_str is the preservation of quotes for string arguments """ - types = ["builtins.bytes", "builtins.str"] res = [] for arg in args: arg_str = arg.accept(self) - if isinstance(arg, UnboundType) and arg.original_str_fallback in types: - res.append(f"'{arg_str}'") + if isinstance(arg, RawExpressionType): + res.append(repr(arg.literal_value)) else: res.append(arg_str) return ", ".join(res) From ed15fdf05ffbc7665e4e0b0bbc2aca90976891dd Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 21 Apr 2024 08:30:26 -0700 Subject: [PATCH 04/11] Fix quoted "Final" --- mypy/semanal.py | 31 ++++++++++++++++++------------- test-data/unit/check-final.test | 2 ++ 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 6832e767c3a4..4a3b78143d32 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -266,6 +266,7 @@ ParamSpecType, PlaceholderType, ProperType, + RawExpressionType, TrivialSyntheticTypeTranslator, TupleType, Type, @@ -3231,10 +3232,10 @@ def analyze_typeddict_assign(self, s: AssignmentStmt) -> bool: def analyze_lvalues(self, s: AssignmentStmt) -> None: # We cannot use s.type, because analyze_simple_literal_type() will set it. explicit = s.unanalyzed_type is not None - if self.is_final_type(s.unanalyzed_type): + final_type = self.unwrap_final_type(s.unanalyzed_type) + if final_type is not None: # We need to exclude bare Final. - assert isinstance(s.unanalyzed_type, UnboundType) - if not s.unanalyzed_type.args: + if not final_type.args: explicit = False if s.rvalue: @@ -3300,19 +3301,19 @@ def unwrap_final(self, s: AssignmentStmt) -> bool: Returns True if Final[...] was present. """ - if not s.unanalyzed_type or not self.is_final_type(s.unanalyzed_type): + final_type = self.unwrap_final_type(s.unanalyzed_type) + if final_type is None: return False - assert isinstance(s.unanalyzed_type, UnboundType) - if len(s.unanalyzed_type.args) > 1: - self.fail("Final[...] takes at most one type argument", s.unanalyzed_type) + if len(final_type.args) > 1: + self.fail("Final[...] takes at most one type argument", final_type) invalid_bare_final = False - if not s.unanalyzed_type.args: + if not final_type.args: s.type = None if isinstance(s.rvalue, TempNode) and s.rvalue.no_rhs: invalid_bare_final = True self.fail("Type in Final[...] can only be omitted if there is an initializer", s) else: - s.type = s.unanalyzed_type.args[0] + s.type = final_type.args[0] if s.type is not None and self.is_classvar(s.type): self.fail("Variable should not be annotated with both ClassVar and Final", s) @@ -4713,13 +4714,17 @@ def is_classvar(self, typ: Type) -> bool: return False return sym.node.fullname == "typing.ClassVar" - def is_final_type(self, typ: Type | None) -> bool: + def unwrap_final_type(self, typ: Type | None) -> UnboundType | None: + if isinstance(typ, RawExpressionType) and typ.node is not None: + typ = typ.node if not isinstance(typ, UnboundType): - return False + return None sym = self.lookup_qualified(typ.name, typ) if not sym or not sym.node: - return False - return sym.node.fullname in FINAL_TYPE_NAMES + return None + if sym.node.fullname in FINAL_TYPE_NAMES: + return typ + return None def fail_invalid_classvar(self, context: Context) -> None: self.fail(message_registry.CLASS_VAR_OUTSIDE_OF_CLASS, context) diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index b1378a47b1b1..26a0d0782503 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -6,11 +6,13 @@ [case testFinalDefiningModuleVar] from typing import Final +w: 'Final' = int() x: Final = int() y: Final[float] = int() z: Final[int] = int() bad: Final[str] = int() # E: Incompatible types in assignment (expression has type "int", variable has type "str") +reveal_type(w) # N: Revealed type is "builtins.int" reveal_type(x) # N: Revealed type is "builtins.int" reveal_type(y) # N: Revealed type is "builtins.float" reveal_type(z) # N: Revealed type is "builtins.int" From b29ae1da8212f47b60306b45d9d9cc5bb3cd1076 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 21 Apr 2024 08:34:39 -0700 Subject: [PATCH 05/11] Extend test --- test-data/unit/check-namedtuple.test | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test-data/unit/check-namedtuple.test b/test-data/unit/check-namedtuple.test index 0ce8630e51d9..23e109e1af78 100644 --- a/test-data/unit/check-namedtuple.test +++ b/test-data/unit/check-namedtuple.test @@ -802,14 +802,20 @@ class Fraction(Real): [builtins fixtures/tuple.pyi] [case testForwardReferenceInNamedTuple] -from typing import NamedTuple +from typing import List, NamedTuple class A(NamedTuple): b: 'B' x: int + y: List['B'] class B: pass + +def f(a: A): + reveal_type(a.b) # N: Revealed type is "__main__.B" + reveal_type(a.x) # N: Revealed type is "builtins.int" + reveal_type(a.y) # N: Revealed type is "builtins.list[__main__.B]" [builtins fixtures/tuple.pyi] [case testTypeNamedTupleClassmethod] From 8dd0ed5328d2ae0716c344958dccb5fb993b3d44 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 21 Apr 2024 08:37:26 -0700 Subject: [PATCH 06/11] Fix mypyc --- mypyc/irbuild/classdef.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mypyc/irbuild/classdef.py b/mypyc/irbuild/classdef.py index fc2bb4a1fc2f..26873aafafea 100644 --- a/mypyc/irbuild/classdef.py +++ b/mypyc/irbuild/classdef.py @@ -24,7 +24,7 @@ TypeInfo, is_class_var, ) -from mypy.types import ENUM_REMOVED_PROPS, Instance, UnboundType, get_proper_type +from mypy.types import ENUM_REMOVED_PROPS, Instance, RawExpressionType, UnboundType, get_proper_type from mypyc.common import PROPSET_PREFIX from mypyc.ir.class_ir import ClassIR, NonExtClassInfo from mypyc.ir.func_ir import FuncDecl, FuncSignature @@ -602,15 +602,15 @@ def add_non_ext_class_attr_ann( # FIXME: if get_type_info is not provided, don't fall back to stmt.type? ann_type = get_proper_type(stmt.type) if ( - isinstance(stmt.unanalyzed_type, UnboundType) - and stmt.unanalyzed_type.original_str_expr is not None + isinstance(stmt.unanalyzed_type, RawExpressionType) + and isinstance(stmt.unanalyzed_type.literal_value, str) ): # Annotation is a forward reference, so don't attempt to load the actual # type and load the string instead. # # TODO: is it possible to determine whether a non-string annotation is # actually a forward reference due to the __annotations__ future? - typ = builder.load_str(stmt.unanalyzed_type.original_str_expr) + typ = builder.load_str(stmt.unanalyzed_type.literal_value) elif isinstance(ann_type, Instance): typ = load_type(builder, ann_type.type, stmt.line) else: From 9112397b3ee67177040cc64b4130616821c8ecf0 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 21 Apr 2024 08:39:10 -0700 Subject: [PATCH 07/11] Clean up original_str_expr --- mypy/typeanal.py | 12 ------------ mypy/types.py | 30 ++---------------------------- 2 files changed, 2 insertions(+), 40 deletions(-) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 93e98c302e13..5b5538abe26b 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -1537,18 +1537,6 @@ def analyze_literal_type(self, t: UnboundType) -> Type: return UnionType.make_union(output, line=t.line) def analyze_literal_param(self, idx: int, arg: Type, ctx: Context) -> list[Type] | None: - # This UnboundType was originally defined as a string. - if isinstance(arg, UnboundType) and arg.original_str_expr is not None: - assert arg.original_str_fallback is not None - return [ - LiteralType( - value=arg.original_str_expr, - fallback=self.named_type(arg.original_str_fallback), - line=arg.line, - column=arg.column, - ) - ] - # If arg is an UnboundType that was *not* originally defined as # a string, try expanding it in case it's a type alias or something. if isinstance(arg, UnboundType): diff --git a/mypy/types.py b/mypy/types.py index f323929bdac6..ec627d480f4d 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -906,8 +906,6 @@ class UnboundType(ProperType): "args", "optional", "empty_tuple_index", - "original_str_expr", - "original_str_fallback", ) def __init__( @@ -918,8 +916,6 @@ def __init__( column: int = -1, optional: bool = False, empty_tuple_index: bool = False, - original_str_expr: str | None = None, - original_str_fallback: str | None = None, ) -> None: super().__init__(line, column) if not args: @@ -931,21 +927,6 @@ def __init__( self.optional = optional # Special case for X[()] self.empty_tuple_index = empty_tuple_index - # If this UnboundType was originally defined as a str or bytes, keep track of - # the original contents of that string-like thing. This way, if this UnboundExpr - # ever shows up inside of a LiteralType, we can determine whether that - # Literal[...] is valid or not. E.g. Literal[foo] is most likely invalid - # (unless 'foo' is an alias for another literal or something) and - # Literal["foo"] most likely is. - # - # We keep track of the entire string instead of just using a boolean flag - # so we can distinguish between things like Literal["foo"] vs - # Literal[" foo "]. - # - # We also keep track of what the original base fallback type was supposed to be - # so we don't have to try and recompute it later - self.original_str_expr = original_str_expr - self.original_str_fallback = original_str_fallback def copy_modified(self, args: Bogus[Sequence[Type] | None] = _dummy) -> UnboundType: if args is _dummy: @@ -957,15 +938,13 @@ def copy_modified(self, args: Bogus[Sequence[Type] | None] = _dummy) -> UnboundT column=self.column, optional=self.optional, empty_tuple_index=self.empty_tuple_index, - original_str_expr=self.original_str_expr, - original_str_fallback=self.original_str_fallback, ) def accept(self, visitor: TypeVisitor[T]) -> T: return visitor.visit_unbound_type(self) def __hash__(self) -> int: - return hash((self.name, self.optional, tuple(self.args), self.original_str_expr)) + return hash((self.name, self.optional, tuple(self.args))) def __eq__(self, other: object) -> bool: if not isinstance(other, UnboundType): @@ -974,8 +953,6 @@ def __eq__(self, other: object) -> bool: self.name == other.name and self.optional == other.optional and self.args == other.args - and self.original_str_expr == other.original_str_expr - and self.original_str_fallback == other.original_str_fallback ) def serialize(self) -> JsonDict: @@ -983,8 +960,6 @@ def serialize(self) -> JsonDict: ".class": "UnboundType", "name": self.name, "args": [a.serialize() for a in self.args], - "expr": self.original_str_expr, - "expr_fallback": self.original_str_fallback, } @classmethod @@ -993,8 +968,6 @@ def deserialize(cls, data: JsonDict) -> UnboundType: return UnboundType( data["name"], [deserialize_type(a) for a in data["args"]], - original_str_expr=data["expr"], - original_str_fallback=data["expr_fallback"], ) @@ -2731,6 +2704,7 @@ def __eq__(self, other: object) -> bool: return ( self.base_type_name == other.base_type_name and self.literal_value == other.literal_value + and self.node == other.node ) else: return NotImplemented From 6f86cb4d2751b9524b3c02744b946965682cb6cf Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 21 Apr 2024 08:41:45 -0700 Subject: [PATCH 08/11] formatting --- mypy/stubutil.py | 11 ++++++++++- mypy/types.py | 17 +++-------------- mypyc/irbuild/classdef.py | 7 +++---- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/mypy/stubutil.py b/mypy/stubutil.py index 55fcebef0ffc..8e41d6862531 100644 --- a/mypy/stubutil.py +++ b/mypy/stubutil.py @@ -17,7 +17,16 @@ from mypy.modulefinder import ModuleNotFoundReason from mypy.moduleinspect import InspectError, ModuleInspect from mypy.stubdoc import ArgSig, FunctionSig -from mypy.types import AnyType, NoneType, RawExpressionType, Type, TypeList, TypeStrVisitor, UnboundType, UnionType +from mypy.types import ( + AnyType, + NoneType, + RawExpressionType, + Type, + TypeList, + TypeStrVisitor, + UnboundType, + UnionType, +) # Modules that may fail when imported, or that may have side effects (fully qualified). NOT_IMPORTABLE_MODULES = () diff --git a/mypy/types.py b/mypy/types.py index ec627d480f4d..f1f7c09c6b20 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -13,7 +13,6 @@ Iterable, NamedTuple, NewType, - Optional, Sequence, TypeVar, Union, @@ -901,12 +900,7 @@ def copy_modified( class UnboundType(ProperType): """Instance type that has not been bound during semantic analysis.""" - __slots__ = ( - "name", - "args", - "optional", - "empty_tuple_index", - ) + __slots__ = ("name", "args", "optional", "empty_tuple_index") def __init__( self, @@ -950,9 +944,7 @@ def __eq__(self, other: object) -> bool: if not isinstance(other, UnboundType): return NotImplemented return ( - self.name == other.name - and self.optional == other.optional - and self.args == other.args + self.name == other.name and self.optional == other.optional and self.args == other.args ) def serialize(self) -> JsonDict: @@ -965,10 +957,7 @@ def serialize(self) -> JsonDict: @classmethod def deserialize(cls, data: JsonDict) -> UnboundType: assert data[".class"] == "UnboundType" - return UnboundType( - data["name"], - [deserialize_type(a) for a in data["args"]], - ) + return UnboundType(data["name"], [deserialize_type(a) for a in data["args"]]) class CallableArgument(ProperType): diff --git a/mypyc/irbuild/classdef.py b/mypyc/irbuild/classdef.py index 26873aafafea..3f6ec0f33822 100644 --- a/mypyc/irbuild/classdef.py +++ b/mypyc/irbuild/classdef.py @@ -24,7 +24,7 @@ TypeInfo, is_class_var, ) -from mypy.types import ENUM_REMOVED_PROPS, Instance, RawExpressionType, UnboundType, get_proper_type +from mypy.types import ENUM_REMOVED_PROPS, Instance, RawExpressionType, get_proper_type from mypyc.common import PROPSET_PREFIX from mypyc.ir.class_ir import ClassIR, NonExtClassInfo from mypyc.ir.func_ir import FuncDecl, FuncSignature @@ -601,9 +601,8 @@ def add_non_ext_class_attr_ann( if typ is None: # FIXME: if get_type_info is not provided, don't fall back to stmt.type? ann_type = get_proper_type(stmt.type) - if ( - isinstance(stmt.unanalyzed_type, RawExpressionType) - and isinstance(stmt.unanalyzed_type.literal_value, str) + if isinstance(stmt.unanalyzed_type, RawExpressionType) and isinstance( + stmt.unanalyzed_type.literal_value, str ): # Annotation is a forward reference, so don't attempt to load the actual # type and load the string instead. From 36019e584d8ea8ed8f7dd225289ebd85b147bbd9 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 21 Apr 2024 08:53:33 -0700 Subject: [PATCH 09/11] fix self check --- mypy/types.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/types.py b/mypy/types.py index f1f7c09c6b20..81f885cc9dc4 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -2656,7 +2656,7 @@ def __init__( line: int = -1, column: int = -1, note: str | None = None, - node: ProperType | None = None, + node: Type | None = None, ) -> None: super().__init__(line, column) self.literal_value = literal_value @@ -2672,7 +2672,7 @@ def accept(self, visitor: TypeVisitor[T]) -> T: ret: T = visitor.visit_raw_expression_type(self) return ret - def copy_modified(self, node: ProperType | None) -> RawExpressionType: + def copy_modified(self, node: Type | None) -> RawExpressionType: return RawExpressionType( literal_value=self.literal_value, base_type_name=self.base_type_name, From 6ddb9c50fb695cd6b0610211e5f3cb6f7fa82b3a Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 21 Apr 2024 10:38:58 -0700 Subject: [PATCH 10/11] Fix string annotations in some contexts --- mypy/semanal.py | 6 ++--- mypy/typeanal.py | 3 +++ mypy/types.py | 8 +++++++ .../unit/check-parameter-specification.test | 23 ++++++++++++++++++- test-data/unit/check-typeguard.test | 11 +++++++++ test-data/unit/check-typeis.test | 11 +++++++++ 6 files changed, 58 insertions(+), 4 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 4a3b78143d32..1fc58a6c11f1 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -266,7 +266,6 @@ ParamSpecType, PlaceholderType, ProperType, - RawExpressionType, TrivialSyntheticTypeTranslator, TupleType, Type, @@ -4715,8 +4714,9 @@ def is_classvar(self, typ: Type) -> bool: return sym.node.fullname == "typing.ClassVar" def unwrap_final_type(self, typ: Type | None) -> UnboundType | None: - if isinstance(typ, RawExpressionType) and typ.node is not None: - typ = typ.node + if typ is None: + return None + typ = typ.resolve_string_annotation() if not isinstance(typ, UnboundType): return None sym = self.lookup_qualified(typ.name, typ) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 5b5538abe26b..1150c8795bd9 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -1070,6 +1070,7 @@ def visit_callable_type(self, t: CallableType, nested: bool = True) -> Type: return ret def anal_type_guard(self, t: Type) -> Type | None: + t = t.resolve_string_annotation() if isinstance(t, UnboundType): sym = self.lookup_qualified(t.name, t) if sym is not None and sym.node is not None: @@ -1088,6 +1089,7 @@ def anal_type_guard_arg(self, t: UnboundType, fullname: str) -> Type | None: return None def anal_type_is(self, t: Type) -> Type | None: + t = t.resolve_string_annotation() if isinstance(t, UnboundType): sym = self.lookup_qualified(t.name, t) if sym is not None and sym.node is not None: @@ -1105,6 +1107,7 @@ def anal_type_is_arg(self, t: UnboundType, fullname: str) -> Type | None: def anal_star_arg_type(self, t: Type, kind: ArgKind, nested: bool) -> Type: """Analyze signature argument type for *args and **kwargs argument.""" + t = t.resolve_string_annotation() if isinstance(t, UnboundType) and t.name and "." in t.name and not t.args: components = t.name.split(".") tvar_name = ".".join(components[:-1]) diff --git a/mypy/types.py b/mypy/types.py index 81f885cc9dc4..5573dc9efe0e 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -271,6 +271,9 @@ def can_be_true_default(self) -> bool: def can_be_false_default(self) -> bool: return True + def resolve_string_annotation(self) -> Type: + return self + def accept(self, visitor: TypeVisitor[T]) -> T: raise RuntimeError("Not implemented", type(self)) @@ -2682,6 +2685,11 @@ def copy_modified(self, node: Type | None) -> RawExpressionType: node=node, ) + def resolve_string_annotation(self) -> Type: + if self.node is not None: + return self.node.resolve_string_annotation() + return self + def serialize(self) -> JsonDict: assert False, "Synthetic types don't serialize" diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index 8fd9abcb9752..cab7d2bf6819 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -1193,7 +1193,28 @@ def func(callback: Callable[P, str]) -> Callable[P, str]: return inner [builtins fixtures/paramspec.pyi] -[case testParamSpecArgsAndKwargsMissmatch] +[case testParamSpecArgsAndKwargsStringified] +from typing import Callable +from typing_extensions import ParamSpec + +P1 = ParamSpec("P1") + +def func(callback: Callable[P1, str]) -> Callable[P1, str]: + def inner(*args: "P1.args", **kwargs: "P1.kwargs") -> str: + return "foo" + return inner + +@func +def outer(a: int) -> str: + return "" + +outer(1) # OK +outer("x") # E: Argument 1 to "outer" has incompatible type "str"; expected "int" +outer(a=1) # OK +outer(b=1) # E: Unexpected keyword argument "b" for "outer" +[builtins fixtures/paramspec.pyi] + +[case testParamSpecArgsAndKwargsMismatch] from typing import Callable from typing_extensions import ParamSpec diff --git a/test-data/unit/check-typeguard.test b/test-data/unit/check-typeguard.test index 27b88553fb43..e1b7a86aba63 100644 --- a/test-data/unit/check-typeguard.test +++ b/test-data/unit/check-typeguard.test @@ -9,6 +9,17 @@ def main(a: object) -> None: reveal_type(a) # N: Revealed type is "builtins.object" [builtins fixtures/tuple.pyi] +[case testTypeGuardStringified] +from typing_extensions import TypeGuard +class Point: pass +def is_point(a: object) -> "TypeGuard[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 testTypeGuardTypeArgsNone] from typing_extensions import TypeGuard def foo(a: object) -> TypeGuard: # E: TypeGuard must have exactly one type argument diff --git a/test-data/unit/check-typeis.test b/test-data/unit/check-typeis.test index 6b96845504ab..83467d5e3683 100644 --- a/test-data/unit/check-typeis.test +++ b/test-data/unit/check-typeis.test @@ -9,6 +9,17 @@ def main(a: object) -> None: reveal_type(a) # N: Revealed type is "builtins.object" [builtins fixtures/tuple.pyi] +[case testTypeIsStringified] +from typing_extensions import TypeIs +class 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" + else: + reveal_type(a) # N: Revealed type is "builtins.object" +[builtins fixtures/tuple.pyi] + [case testTypeIsElif] from typing_extensions import TypeIs from typing import Union From b41f71e67b0513e061d3bdd0f727fad192d821e7 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 21 Apr 2024 10:41:51 -0700 Subject: [PATCH 11/11] One more --- mypy/typeanal.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 1150c8795bd9..c2c578045297 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -1460,6 +1460,7 @@ def analyze_callable_args( invalid_unpacks: list[Type] = [] second_unpack_last = False for i, arg in enumerate(arglist.items): + arg = arg.resolve_string_annotation() if isinstance(arg, CallableArgument): args.append(arg.typ) names.append(arg.name)