diff --git a/mypy/nodes.py b/mypy/nodes.py index e2ea348d2df11..95f8778c4e6d9 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -1664,6 +1664,7 @@ class ClassDef(Statement): "has_incompatible_baseclass", "docstring", "removed_statements", + "is_runtime_unreachable", ) __match_args__ = ("name", "defs") @@ -1714,6 +1715,8 @@ def __init__( self.has_incompatible_baseclass = False self.docstring: str | None = None self.removed_statements = [] + # Set for definitions in an if branch known not to execute at runtime. + self.is_runtime_unreachable = False @property def fullname(self) -> str: @@ -1862,6 +1865,7 @@ class AssignmentStmt(Statement): "is_alias_def", "is_final_def", "invalid_recursive_alias", + "is_runtime_unreachable", ) __match_args__ = ("lvalues", "rvalues", "type") @@ -1904,6 +1908,8 @@ def __init__( self.is_alias_def = False self.is_final_def = False self.invalid_recursive_alias = False + # Set for definitions in an if branch known not to execute at runtime. + self.is_runtime_unreachable = False def accept(self, visitor: StatementVisitor[T]) -> T: return visitor.visit_assignment_stmt(self) diff --git a/mypy/reachability.py b/mypy/reachability.py index 37c7a9715600a..6a3f37aea32d4 100644 --- a/mypy/reachability.py +++ b/mypy/reachability.py @@ -8,8 +8,10 @@ from mypy.nodes import ( LITERAL_YES, AssertStmt, + AssignmentStmt, Block, CallExpr, + ClassDef, ComparisonExpr, Expression, FuncDef, @@ -23,6 +25,7 @@ MemberExpr, NameExpr, OpExpr, + RefExpr, SliceExpr, StrExpr, TupleExpr, @@ -168,6 +171,63 @@ def infer_condition_value(expr: Expression, options: Options) -> int: return result +def infer_runtime_condition_value(expr: Expression, options: Options) -> int: + """Infer whether a condition is definitely true or false at runtime. + + Unlike ``infer_condition_value()``, this intentionally doesn't use the + ``MYPY_TRUE`` and ``MYPY_FALSE`` states. It only returns a definite result + when the expression's runtime value follows from the configured target. + """ + if isinstance(expr, UnaryExpr) and expr.op == "not": + positive = infer_runtime_condition_value(expr.expr, options) + return { + ALWAYS_TRUE: ALWAYS_FALSE, + ALWAYS_FALSE: ALWAYS_TRUE, + TRUTH_VALUE_UNKNOWN: TRUTH_VALUE_UNKNOWN, + }[positive] + + if isinstance(expr, OpExpr): + if expr.op not in ("or", "and"): + return TRUTH_VALUE_UNKNOWN + left = infer_runtime_condition_value(expr.left, options) + right = infer_runtime_condition_value(expr.right, options) + if expr.op == "or": + if ALWAYS_TRUE in (left, right): + return ALWAYS_TRUE + if left == right == ALWAYS_FALSE: + return ALWAYS_FALSE + else: + if ALWAYS_FALSE in (left, right): + return ALWAYS_FALSE + if left == right == ALWAYS_TRUE: + return ALWAYS_TRUE + return TRUTH_VALUE_UNKNOWN + + if isinstance(expr, RefExpr) and expr.fullname in ( + "typing.TYPE_CHECKING", + "typing_extensions.TYPE_CHECKING", + ): + return ALWAYS_FALSE + + if isinstance(expr, (NameExpr, MemberExpr)): + if expr.name == "MYPY": + return ALWAYS_FALSE + if expr.name == "PY2": + return ALWAYS_FALSE + if expr.name == "PY3": + return ALWAYS_TRUE + if expr.name in options.always_true: + return ALWAYS_TRUE + if expr.name in options.always_false: + return ALWAYS_FALSE + return TRUTH_VALUE_UNKNOWN + + result = consider_sys_version_info(expr, options.python_version, use_fullname=True) + if result == TRUTH_VALUE_UNKNOWN: + result = consider_sys_platform(expr, options.platform, use_fullname=True) + return result + + def infer_pattern_value(pattern: Pattern) -> int: if isinstance(pattern, AsPattern) and pattern.pattern is None: return ALWAYS_TRUE @@ -179,7 +239,9 @@ def infer_pattern_value(pattern: Pattern) -> int: return TRUTH_VALUE_UNKNOWN -def consider_sys_version_info(expr: Expression, pyversion: tuple[int, ...]) -> int: +def consider_sys_version_info( + expr: Expression, pyversion: tuple[int, ...], *, use_fullname: bool = False +) -> int: """Consider whether expr is a comparison involving sys.version_info. Return ALWAYS_TRUE, ALWAYS_FALSE, or TRUTH_VALUE_UNKNOWN. @@ -198,10 +260,10 @@ def consider_sys_version_info(expr: Expression, pyversion: tuple[int, ...]) -> i if op not in ("==", "!=", "<=", ">=", "<", ">"): return TRUTH_VALUE_UNKNOWN - index = contains_sys_version_info(expr.operands[0]) + index = contains_sys_version_info(expr.operands[0], use_fullname=use_fullname) thing = contains_int_or_tuple_of_ints(expr.operands[1]) if index is None or thing is None: - index = contains_sys_version_info(expr.operands[1]) + index = contains_sys_version_info(expr.operands[1], use_fullname=use_fullname) thing = contains_int_or_tuple_of_ints(expr.operands[0]) op = reverse_op[op] if isinstance(index, int) and isinstance(thing, int): @@ -223,7 +285,7 @@ def consider_sys_version_info(expr: Expression, pyversion: tuple[int, ...]) -> i return TRUTH_VALUE_UNKNOWN -def consider_sys_platform(expr: Expression, platform: str) -> int: +def consider_sys_platform(expr: Expression, platform: str, *, use_fullname: bool = False) -> int: """Consider whether expr is a comparison involving sys.platform. Return ALWAYS_TRUE, ALWAYS_FALSE, or TRUTH_VALUE_UNKNOWN. @@ -239,7 +301,7 @@ def consider_sys_platform(expr: Expression, platform: str) -> int: op = expr.operators[0] if op not in ("==", "!="): return TRUTH_VALUE_UNKNOWN - if not is_sys_attr(expr.operands[0], "platform"): + if not is_sys_attr(expr.operands[0], "platform", use_fullname=use_fullname): return TRUTH_VALUE_UNKNOWN right = expr.operands[1] if not isinstance(right, StrExpr): @@ -250,7 +312,7 @@ def consider_sys_platform(expr: Expression, platform: str) -> int: return TRUTH_VALUE_UNKNOWN if len(expr.args) != 1 or not isinstance(expr.args[0], StrExpr): return TRUTH_VALUE_UNKNOWN - if not is_sys_attr(expr.callee.expr, "platform"): + if not is_sys_attr(expr.callee.expr, "platform", use_fullname=use_fullname): return TRUTH_VALUE_UNKNOWN if expr.callee.name != "startswith": return TRUTH_VALUE_UNKNOWN @@ -296,10 +358,14 @@ def contains_int_or_tuple_of_ints(expr: Expression) -> None | int | tuple[int, . return None -def contains_sys_version_info(expr: Expression) -> None | int | tuple[int | None, int | None]: - if is_sys_attr(expr, "version_info"): +def contains_sys_version_info( + expr: Expression, *, use_fullname: bool = False +) -> None | int | tuple[int | None, int | None]: + if is_sys_attr(expr, "version_info", use_fullname=use_fullname): return (None, None) # Same as sys.version_info[:] - if isinstance(expr, IndexExpr) and is_sys_attr(expr.base, "version_info"): + if isinstance(expr, IndexExpr) and is_sys_attr( + expr.base, "version_info", use_fullname=use_fullname + ): index = expr.index if isinstance(index, IntExpr): return index.value @@ -320,11 +386,13 @@ def contains_sys_version_info(expr: Expression) -> None | int | tuple[int | None return None -def is_sys_attr(expr: Expression, name: str) -> bool: +def is_sys_attr(expr: Expression, name: str, *, use_fullname: bool = False) -> bool: # TODO: This currently doesn't work with code like this: # - import sys as _sys # - from sys import version_info if isinstance(expr, MemberExpr) and expr.name == name: + if use_fullname: + return expr.fullname == f"sys.{name}" if isinstance(expr.expr, NameExpr) and expr.expr.name == "sys": # TODO: Guard against a local named sys, etc. # (Though later passes will still do most checking.) @@ -354,6 +422,10 @@ def mark_block_mypy_only(block: Block) -> None: block.accept(MarkImportsMypyOnlyVisitor()) +def mark_block_runtime_unreachable(block: Block) -> None: + block.accept(MarkRuntimeUnreachableVisitor()) + + class MarkImportsMypyOnlyVisitor(TraverserVisitor): """Visitor that sets is_mypy_only (which affects priority).""" @@ -368,3 +440,17 @@ def visit_import_all(self, node: ImportAll) -> None: def visit_func_def(self, node: FuncDef) -> None: node.is_mypy_only = True + + +class MarkRuntimeUnreachableVisitor(TraverserVisitor): + """Mark definitions contained in code that cannot run at runtime.""" + + def visit_func_def(self, node: FuncDef) -> None: + super().visit_func_def(node) + + def visit_class_def(self, node: ClassDef) -> None: + node.is_runtime_unreachable = True + super().visit_class_def(node) + + def visit_assignment_stmt(self, node: AssignmentStmt) -> None: + node.is_runtime_unreachable = True diff --git a/mypy/semanal.py b/mypy/semanal.py index e010273b0781f..7e746d4088b9a 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -220,9 +220,12 @@ ALWAYS_TRUE, MYPY_FALSE, MYPY_TRUE, + TRUTH_VALUE_UNKNOWN, infer_condition_value, infer_reachability_of_if_statement, infer_reachability_of_match_statement, + infer_runtime_condition_value, + mark_block_runtime_unreachable, ) from mypy.scope import Scope from mypy.semanal_enum import EnumCallAnalyzer @@ -3676,7 +3679,7 @@ def analyze_typeddict_assign(self, s: AssignmentStmt) -> bool: namespace = self.qualified_name(name) with self.tvar_scope_frame(self.tvar_scope.class_frame(namespace)): is_typed_dict, info, tvar_defs = self.typed_dict_analyzer.check_typeddict( - s.rvalue, name + s.rvalue, name, s.is_runtime_unreachable ) if not is_typed_dict: return False @@ -5607,9 +5610,22 @@ def visit_continue_stmt(self, s: ContinueStmt) -> None: def visit_if_stmt(self, s: IfStmt) -> None: self.statement = s infer_reachability_of_if_statement(s, self.options) + remaining = ALWAYS_TRUE for i in range(len(s.expr)): s.expr[i].accept(self) + condition = infer_runtime_condition_value(s.expr[i], self.options) + if remaining == ALWAYS_FALSE or condition == ALWAYS_FALSE: + mark_block_runtime_unreachable(s.body[i]) + + if remaining == ALWAYS_FALSE or condition == ALWAYS_TRUE: + remaining = ALWAYS_FALSE + elif remaining == ALWAYS_TRUE and condition == ALWAYS_FALSE: + remaining = ALWAYS_TRUE + else: + remaining = TRUTH_VALUE_UNKNOWN self.visit_block(s.body[i]) + if s.else_body and remaining == ALWAYS_FALSE: + mark_block_runtime_unreachable(s.else_body) self.visit_block_maybe(s.else_body) def visit_try_stmt(self, s: TryStmt) -> None: diff --git a/mypy/semanal_typeddict.py b/mypy/semanal_typeddict.py index b1ba9f6c3abec..5c0701ff6c8f3 100644 --- a/mypy/semanal_typeddict.py +++ b/mypy/semanal_typeddict.py @@ -40,6 +40,7 @@ from mypy.semanal_shared import ( SemanticAnalyzerInterface, has_placeholder, + parse_bool, require_bool_literal_argument, ) from mypy.state import state @@ -122,6 +123,19 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> tuple[bool, TypeInfo | N and isinstance(defn.base_type_exprs[0], RefExpr) and defn.base_type_exprs[0].fullname in TPDICT_NAMES ): + if ( + is_closed is not None + and parse_bool(defn.keywords["closed"]) is not None + and defn.base_type_exprs[0].fullname == "typing.TypedDict" + and self.options.python_version < (3, 15) + and not self.api.is_stub_file + and not defn.is_runtime_unreachable + ): + self.fail( + '"closed" argument to TypedDict is only available in Python 3.15 and later', + defn.keywords["closed"], + ) + is_closed = None # Building a new TypedDict field_sources, statements = self.analyze_typeddict_classdef_fields(defn) if field_sources is None: @@ -148,7 +162,9 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> tuple[bool, TypeInfo | N typeddict_bases: list[Expression] = [] typeddict_bases_set = set() for i, expr in enumerate(defn.base_type_exprs): - ok, maybe_type_info, _ = self.check_typeddict(expr, inline_base(defn.name, i)) + ok, maybe_type_info, _ = self.check_typeddict( + expr, inline_base(defn.name, i), defn.is_runtime_unreachable + ) if ok and maybe_type_info is not None: # expr is a CallExpr info = maybe_type_info @@ -576,7 +592,7 @@ def extract_meta_info( return typ, is_required, readonly def check_typeddict( - self, node: Expression, name: str + self, node: Expression, name: str, is_runtime_unreachable: bool = False ) -> tuple[bool, TypeInfo | None, list[TypeVarLikeType]]: """Check if a call defines a TypedDict. @@ -599,7 +615,7 @@ def check_typeddict( fullname = callee.fullname if fullname not in TPDICT_NAMES: return False, None, [] - res = self.parse_typeddict_args(call) + res = self.parse_typeddict_args(call, fullname, is_runtime_unreachable) if res is None: # This is a valid typed dict, but some type is not ready. # The caller should defer this until next iteration. @@ -662,7 +678,7 @@ def check_typeddict( return True, info, tvar_defs def parse_typeddict_args( - self, call: CallExpr + self, call: CallExpr, fullname: str, is_runtime_unreachable: bool ) -> tuple[str, list[str], list[Type], bool, bool, list[TypeVarLikeType], bool] | None: """Parse typed dict call expression. @@ -702,7 +718,22 @@ def parse_typeddict_args( closed: bool = False for arg_name, arg in zip(call.arg_names[2:], call.args[2:]): assert arg_name - value = require_bool_literal_argument(self.api, arg, arg_name) + if arg_name == "closed": + value = require_bool_literal_argument(self.api, arg, "closed", False) + if ( + parse_bool(arg) is not None + and fullname == "typing.TypedDict" + and self.options.python_version < (3, 15) + and not self.api.is_stub_file + and not is_runtime_unreachable + ): + self.fail( + '"closed" argument to TypedDict is only available in Python 3.15 and later', + arg, + ) + return "", [], [], True, False, [], False + else: + value = require_bool_literal_argument(self.api, arg, arg_name) if value is None: return "", [], [], True, False, [], False if arg_name == "closed": diff --git a/mypy/treetransform.py b/mypy/treetransform.py index 25092de66a149..ffc11ec7b8536 100644 --- a/mypy/treetransform.py +++ b/mypy/treetransform.py @@ -272,6 +272,7 @@ def visit_class_def(self, node: ClassDef) -> ClassDef: new.fullname = node.fullname new.info = node.info new.decorators = [self.expr(decorator) for decorator in node.decorators] + new.is_runtime_unreachable = node.is_runtime_unreachable return new def visit_global_decl(self, node: GlobalDecl) -> GlobalDecl: @@ -327,6 +328,7 @@ def duplicate_assignment(self, node: AssignmentStmt) -> AssignmentStmt: ) new.line = node.line new.is_final_def = node.is_final_def + new.is_runtime_unreachable = node.is_runtime_unreachable new.type = self.optional_type(node.type) return new diff --git a/test-data/unit/check-serialize.test b/test-data/unit/check-serialize.test index f504e00688cff..069b6e290190f 100644 --- a/test-data/unit/check-serialize.test +++ b/test-data/unit/check-serialize.test @@ -1089,6 +1089,7 @@ main:2: note: Revealed type is "TypedDict('m.D', {'x'?: builtins.int, 'y'?: buil main:2: note: Revealed type is "TypedDict('m.D', {'x'?: builtins.int, 'y'?: builtins.str})" [case testSerializeClosedTotalTypedDict] +# flags: --python-version 3.15 from m import d reveal_type(d) [file m.py] @@ -1098,9 +1099,9 @@ d: D [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] [out1] -main:2: note: Revealed type is "TypedDict('m.D', {'x': builtins.int, 'y': builtins.str}, closed=True)" +main:3: note: Revealed type is "TypedDict('m.D', {'x': builtins.int, 'y': builtins.str}, closed=True)" [out2] -main:2: note: Revealed type is "TypedDict('m.D', {'x': builtins.int, 'y': builtins.str}, closed=True)" +main:3: note: Revealed type is "TypedDict('m.D', {'x': builtins.int, 'y': builtins.str}, closed=True)" -- -- Modules diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index 01414ee2f457a..665ff62c1f93d 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -5203,6 +5203,7 @@ reveal_type(j(c, b)) # N: Revealed type is "TypedDict({'x'?=: builtins.int})" # See https://peps.python.org/pep-0728/ [case testTypedDictWithClosedFalse] +# flags: --python-version 3.15 from typing import TypedDict D = TypedDict('D', {'x': int, 'y': str}, closed=False) d: D @@ -5216,6 +5217,7 @@ reveal_type(e) # N: Revealed type is "TypedDict('__main__.E', {'x': builtins.in [typing fixtures/typing-typeddict.pyi] [case testTypedDictWithClosedTrue] +# flags: --python-version 3.15 from typing import TypedDict D = TypedDict('D', {'x': int, 'y': str}, closed=True) d: D @@ -5228,6 +5230,169 @@ reveal_type(e) # N: Revealed type is "TypedDict('__main__.E', {'x': builtins.in [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] +[case testTypedDictClosedArgumentBeforePython315] +# flags: --python-version 3.14 +from typing import TypedDict +from typing_extensions import TYPE_CHECKING +MYPY = False +unknown: bool + +class ClassClosed(TypedDict, closed=True): # E: "closed" argument to TypedDict is only available in Python 3.15 and later + x: int +class ClassOpen(TypedDict, closed=False): # E: "closed" argument to TypedDict is only available in Python 3.15 and later + x: int +FunctionalClosed = TypedDict('FunctionalClosed', {'x': int}, closed=True) # E: "closed" argument to TypedDict is only available in Python 3.15 and later +FunctionalOpen = TypedDict('FunctionalOpen', {'x': int}, closed=False) # E: "closed" argument to TypedDict is only available in Python 3.15 and later +class InvalidClosed(TypedDict, closed=0): # E: "closed" argument must be a True or False literal + x: int +class WithoutClosed(TypedDict): + x: int + +if TYPE_CHECKING: + class TypeCheckingClassClosed(TypedDict, closed=True): + x: int + class TypeCheckingClassOpen(TypedDict, closed=False): + x: int + TypeCheckingFunctionalClosed = TypedDict('TypeCheckingFunctionalClosed', {'x': int}, closed=True) + TypeCheckingFunctionalOpen = TypedDict('TypeCheckingFunctionalOpen', {'x': int}, closed=False) + class TypeCheckingInvalidClosed(TypedDict, closed=0): # E: "closed" argument must be a True or False literal + x: int + def type_checking_function() -> None: + class NestedClassClosed(TypedDict, closed=True): + x: int + NestedFunctionalOpen = TypedDict('NestedFunctionalOpen', {'x': int}, closed=False) +if MYPY: + class MypyClassClosed(TypedDict, closed=True): + x: int + MypyFunctionalOpen = TypedDict('MypyFunctionalOpen', {'x': int}, closed=False) +if not TYPE_CHECKING: + pass +else: + class ElseClassClosed(TypedDict, closed=True): + x: int + ElseFunctionalOpen = TypedDict('ElseFunctionalOpen', {'x': int}, closed=False) +if not TYPE_CHECKING: + pass +elif unknown: + class ElifClassClosed(TypedDict, closed=True): + x: int + ElifFunctionalOpen = TypedDict('ElifFunctionalOpen', {'x': int}, closed=False) +runtime_flag: bool +if TYPE_CHECKING or runtime_flag: + class RuntimeOrClassClosed(TypedDict, closed=True): # E: "closed" argument to TypedDict is only available in Python 3.15 and later + x: int + RuntimeOrFunctionalOpen = TypedDict('RuntimeOrFunctionalOpen', {'x': int}, closed=False) # E: "closed" argument to TypedDict is only available in Python 3.15 and later +if not TYPE_CHECKING and runtime_flag: + pass +else: + class RuntimeElseClassClosed(TypedDict, closed=True): # E: "closed" argument to TypedDict is only available in Python 3.15 and later + x: int + RuntimeElseFunctionalOpen = TypedDict('RuntimeElseFunctionalOpen', {'x': int}, closed=False) # E: "closed" argument to TypedDict is only available in Python 3.15 and later +if TYPE_CHECKING and runtime_flag: + class RuntimeAndClassClosed(TypedDict, closed=True): + x: int + RuntimeAndFunctionalOpen = TypedDict('RuntimeAndFunctionalOpen', {'x': int}, closed=False) +from stub import StubClassClosed, StubClassOpen, StubFunctionalClosed, StubFunctionalOpen +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] +[file stub.pyi] +from typing import TypedDict + +class StubClassClosed(TypedDict, closed=True): + x: int +class StubClassOpen(TypedDict, closed=False): + x: int +StubFunctionalClosed = TypedDict('StubFunctionalClosed', {'x': int}, closed=True) +StubFunctionalOpen = TypedDict('StubFunctionalOpen', {'x': int}, closed=False) + +[case testTypedDictClosedArgumentInAliasedTypingOnlyContextBeforePython315] +# flags: --python-version 3.14 +import typing +from typing import TYPE_CHECKING as TC, TypedDict +from typing_extensions import TYPE_CHECKING as EXT_TC + +if TC: + class AliasedClassClosed(TypedDict, closed=True): + x: int + AliasedFunctionalOpen = TypedDict('AliasedFunctionalOpen', {'x': int}, closed=False) +if not not TC: + class DoubleNegatedClassClosed(TypedDict, closed=True): + x: int + DoubleNegatedFunctionalOpen = TypedDict( + 'DoubleNegatedFunctionalOpen', {'x': int}, closed=False + ) +if EXT_TC: + class ExtensionsAliasedClassClosed(TypedDict, closed=True): + x: int + ExtensionsAliasedFunctionalOpen = TypedDict( + 'ExtensionsAliasedFunctionalOpen', {'x': int}, closed=False + ) +if typing.TYPE_CHECKING: + class QualifiedClassClosed(TypedDict, closed=True): + x: int + QualifiedFunctionalOpen = TypedDict('QualifiedFunctionalOpen', {'x': int}, closed=False) +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictClosedArgumentWithShadowedTypeCheckingBeforePython315] +# flags: --python-version 3.14 +from typing import TypedDict + +TYPE_CHECKING = True +if TYPE_CHECKING: + class ShadowedClassClosed(TypedDict, closed=True): # E: "closed" argument to TypedDict is only available in Python 3.15 and later + x: int + ShadowedFunctionalOpen = TypedDict('ShadowedFunctionalOpen', {'x': int}, closed=False) # E: "closed" argument to TypedDict is only available in Python 3.15 and later +def shadowed_locally() -> None: + TYPE_CHECKING = True + if TYPE_CHECKING: + class LocallyShadowedClassClosed(TypedDict, closed=True): # E: "closed" argument to TypedDict is only available in Python 3.15 and later + x: int + LocallyShadowedFunctionalOpen = TypedDict('LocallyShadowedFunctionalOpen', {'x': int}, closed=False) # E: "closed" argument to TypedDict is only available in Python 3.15 and later +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypingExtensionsTypedDictClosedArgumentBeforePython315] +# flags: --python-version 3.14 +from typing_extensions import TypedDict + +class ClassClosed(TypedDict, closed=True): + x: int +class ClassOpen(TypedDict, closed=False): + x: int +FunctionalClosed = TypedDict('FunctionalClosed', {'x': int}, closed=True) +FunctionalOpen = TypedDict('FunctionalOpen', {'x': int}, closed=False) +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] +[file typing_extensions.pyi] +TypedDict = 0 + +[case testMypyExtensionsTypedDictClosedArgumentBeforePython315] +# flags: --python-version 3.14 +from mypy_extensions import TypedDict + +class ClassClosed(TypedDict, closed=True): + x: int +class ClassOpen(TypedDict, closed=False): + x: int +FunctionalClosed = TypedDict('FunctionalClosed', {'x': int}, closed=True) +FunctionalOpen = TypedDict('FunctionalOpen', {'x': int}, closed=False) +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictClosedArgumentInPython315] +# flags: --python-version 3.15 +from typing import TypedDict + +class ClassClosed(TypedDict, closed=True): + x: int +class ClassOpen(TypedDict, closed=False): + x: int +FunctionalClosed = TypedDict('FunctionalClosed', {'x': int}, closed=True) +FunctionalOpen = TypedDict('FunctionalOpen', {'x': int}, closed=False) +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + [case testTypedDictWithInvalidClosedArgument] from typing import TypedDict A = TypedDict('A', {'x': int}, closed=0) # E: "closed" argument must be a True or False literal @@ -5240,6 +5405,7 @@ class D(TypedDict, closed=bool): # E: "closed" argument must be a True or False [typing fixtures/typing-typeddict.pyi] [case testTypedDictWithClosedAndTotal] +# flags: --python-version 3.15 from typing import TypedDict A = TypedDict('A', {'x': int, 'y': str}, total=False, closed=True) B = TypedDict('B', {'x': int, 'y': str}, closed=True, total=False) @@ -5261,6 +5427,7 @@ B = TypedDict('B', {'x': int}, total=True, total=False) # E: Repeated keyword a [typing fixtures/typing-typeddict.pyi] [case testTypedDictSubclassingClosed] +# flags: --python-version 3.15 from typing import TypedDict class D(TypedDict, closed=True): x: int @@ -5308,6 +5475,7 @@ reveal_type(d8) # N: Revealed type is "TypedDict('__main__.D8', {'x': builtins. [typing fixtures/typing-typeddict.pyi] [case testTypedDictSubclassingOpenAndClosed] +# flags: --python-version 3.15 from typing import TypedDict class B1(TypedDict, closed=True): x: int @@ -5332,6 +5500,7 @@ reveal_type(d4) # N: Revealed type is "TypedDict('__main__.D4', {'x': builtins. [typing fixtures/typing-typeddict.pyi] [case testCanSubclassClosedTypedDictWithForwardDeclarations] +# flags: --python-version 3.15 from typing import TypedDict, final class D1(TypedDict, closed=True): @@ -5349,6 +5518,7 @@ reveal_type(d2) # N: Revealed type is "TypedDict('__main__.D2', {'forward_decla [typing fixtures/typing-full.pyi] [case testTypedDictSubtypingClosedMustRemainClosed] +# flags: --python-version 3.15 from typing import TypedDict A = TypedDict('A', {'x': int}, closed=True) B = TypedDict('B', {'x': int}, closed=True) @@ -5368,6 +5538,7 @@ g(b) [typing fixtures/typing-typeddict.pyi] [case testTypedDictSubtypingCannotAddKeysToAClosedSupertype] +# flags: --python-version 3.15 from typing import TypedDict A = TypedDict('A', {'x': int}, closed=True) B = TypedDict('B', {'x': int, 'y': int}, closed=True) @@ -5383,6 +5554,7 @@ g(d) [typing fixtures/typing-typeddict.pyi] [case testTypedDictSubtypingCanDropOptionalReadonlyKeysInAClosedSubtype] +# flags: --python-version 3.15 from typing import ReadOnly, TypedDict # Optional, readonly, closed: permitted A = TypedDict('A', {'x': int, 'y': ReadOnly[int]}, total=False, closed=True) @@ -5416,6 +5588,7 @@ f_g(h) # E: Argument 1 to "f_g" has incompatible type "H"; expected "G" [typing fixtures/typing-typeddict.pyi] [case testTypedDictSubtypingClosedTypeVarBound] +# flags: --python-version 3.15 from typing import TypedDict, TypeVar A = TypedDict('A', {'x': int}, closed=True) B = TypedDict('B', {'x': int}) @@ -5433,6 +5606,7 @@ fB(b) [typing fixtures/typing-typeddict.pyi] [case testTypedDictSubtypingClosedExplicitNever] +# flags: --python-version 3.15 from typing import NotRequired, ReadOnly, TypedDict from typing_extensions import Never A = TypedDict('A', {}, closed=True) @@ -5457,6 +5631,7 @@ f_b(c) # E: Argument 1 to "f_b" has incompatible type "C"; expected "B" [typing fixtures/typing-typeddict.pyi] [case testJoinOfClosedTypedDict] +# flags: --python-version 3.15 from typing import Any, TypedDict, TypeVar A = TypedDict('A', {'x': int}, closed=True) B = TypedDict('B', {'y': int}) @@ -5484,6 +5659,7 @@ reveal_type(j(e, b)) # N: Revealed type is "TypedDict({'y': Any})" [typing fixtures/typing-typeddict.pyi] [case testMeetOfClosedTypedDictsWithMatchingRequiredKeysIsNotAnonymous] +# flags: --python-version 3.15 from typing import TypedDict, TypeVar, Callable X = TypedDict('X', {'x': int, 'y': int}, closed=True) Y = TypedDict('Y', {'x': int, 'y': int}, closed=True) @@ -5502,6 +5678,7 @@ reveal_type(meet(fZX)) # N: Revealed type is "TypedDict('__main__.X', {'x': bui [typing fixtures/typing-typeddict.pyi] [case testMeetOfClosedTypedDictsWithDifferentKeys] +# flags: --python-version 3.15 from typing import TypedDict, TypeVar, Callable, ReadOnly T = TypeVar('T') def meet(x: Callable[[T, T], None]) -> T: pass @@ -5538,6 +5715,7 @@ if int(): [typing fixtures/typing-typeddict.pyi] [case testMeetOfTypedDictsExplicitAndImplicitClosedMissing] +# flags: --python-version 3.15 from typing import Any, NotRequired, ReadOnly, TypedDict, TypeVar, Callable from typing_extensions import Never A = TypedDict('A', {'x': ReadOnly[NotRequired[Never]]}, closed=True) @@ -5550,6 +5728,7 @@ reveal_type(f(g)) # N: Revealed type is "TypedDict({}, closed=True)" [typing fixtures/typing-typeddict.pyi] [case testMeetOfTypedDictClosedMissingAndAny] +# flags: --python-version 3.15 from typing import Any, TypedDict, TypeVar, Callable, ReadOnly T = TypeVar('T') def meet(x: Callable[[T, T], None]) -> T: pass @@ -5570,6 +5749,7 @@ reveal_type(meet(fCA)) # N: Revealed type is "TypedDict({}, closed=True)" [typing fixtures/typing-typeddict.pyi] [case testMeetOfTypedDictsClosedNotRequiredNeverAndAny] +# flags: --python-version 3.15 from typing import Any, NotRequired, TypedDict, TypeVar, Callable from typing_extensions import Never A = TypedDict('A', {'x': NotRequired[Any], 'y': NotRequired[Never]}, closed=True) @@ -5582,6 +5762,7 @@ reveal_type(f(g)) # N: Revealed type is "TypedDict({}, closed=True)" [typing fixtures/typing-typeddict.pyi] [case testMeetOfOpenAndClosedTypedDictsExtraKeyInClosed] +# flags: --python-version 3.15 from typing import TypedDict, TypeVar, Callable, ReadOnly T = TypeVar('T') def meet(x: Callable[[T, T], None]) -> T: pass @@ -5595,6 +5776,7 @@ reveal_type(meet(fAyBAx)) # N: Revealed type is "TypedDict({'a'=: builtins.int, [typing fixtures/typing-typeddict.pyi] [case testMeetOfOpenAndClosedTypedDictsExtraKeyInOpen] +# flags: --python-version 3.15 from typing import TypedDict, TypeVar, Callable, ReadOnly T = TypeVar('T') def meet(x: Callable[[T, T], None]) -> T: pass @@ -5629,6 +5811,7 @@ reveal_type(meet(fD4C)) # N: Revealed type is "TypedDict({'a'=: builtins.int}, [typing fixtures/typing-typeddict.pyi] [case testTypedDictGetMethodClosed] +# flags: --python-version 3.15 from typing import TypedDict, Literal class Unrelated: pass D = TypedDict('D', {'x': int, 'y': str}, closed=True) @@ -5666,6 +5849,7 @@ reveal_type(d.get(x_or_y_or_z, u)) # N: Revealed type is "builtins.int | builtin [typing fixtures/typing-typeddict.pyi] [case testOperatorContainsNarrowsTypedDicts_unionWithList_closed] +# flags: --python-version 3.15 from __future__ import annotations from typing import assert_type, TypedDict, Union @@ -5685,6 +5869,7 @@ else: [typing fixtures/typing-full.pyi] [case testOperatorContainsNarrowsTypedDicts_total_closed] +# flags: --python-version 3.15 from __future__ import annotations from typing import assert_type, Literal, TypedDict, TypeVar, Union @@ -5729,7 +5914,7 @@ def f(arg: TD) -> None: [typing fixtures/typing-full.pyi] [case testOperatorContainsNarrowsTypedDicts_closed] -# flags: --warn-unreachable +# flags: --python-version 3.15 --warn-unreachable from __future__ import annotations from typing import assert_type, TypedDict, Union @@ -5768,6 +5953,7 @@ else: [typing fixtures/typing-full.pyi] [case testOperatorContainsNarrowsTypedDicts_partialThroughTotalFalse_closed] +# flags: --python-version 3.15 from __future__ import annotations from typing import assert_type, Literal, TypedDict, Union @@ -5798,6 +5984,7 @@ else: [typing fixtures/typing-full.pyi] [case testOperatorContainsNarrowsTypedDicts_partialThroughNotRequired_closed] +# flags: --python-version 3.15 from __future__ import annotations from typing import assert_type, TypedDict, Union from typing_extensions import Required, NotRequired @@ -5825,6 +6012,7 @@ else: [typing fixtures/typing-full.pyi] [case testOperatorContainsNarrowsTypeVarWithClosedTypedDictBound] +# flags: --python-version 3.15 from typing import TypedDict, TypeVar from typing_extensions import Never, assert_type @@ -5840,7 +6028,7 @@ def func(t: T): [typing fixtures/typing-typeddict.pyi] [case testTypedDictUnpackFromClosedMissingKey] -# flags: --extra-checks +# flags: --python-version 3.15 --extra-checks from typing import TypedDict from typing_extensions import Never, NotRequired D1 = TypedDict("D1", {"a": int, "b": str}, closed=True) @@ -5853,7 +6041,7 @@ d3: D3 = {**d1} [typing fixtures/typing-typeddict.pyi] [case testTypedDictUnpackIntoClosed] -# flags: --extra-checks +# flags: --python-version 3.15 --extra-checks from typing import Any, Mapping, TypedDict, Union from typing_extensions import Never, NotRequired D1 = TypedDict("D1", {"a": int, "b": str}, closed=True) diff --git a/test-data/unit/diff.test b/test-data/unit/diff.test index c4bbf7d3f2283..c42826a53f3af 100644 --- a/test-data/unit/diff.test +++ b/test-data/unit/diff.test @@ -677,6 +677,7 @@ __main__.Point __main__.p [case testTypedDict5] +# flags: --python-version 3.15 from typing import TypedDict class Point(TypedDict): x: int diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index ef2e8c0b343bf..1c4c0f4091697 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -3746,6 +3746,7 @@ b.py:4: error: ReadOnly TypedDict key "y" TypedDict is mutated b.py:3: error: ReadOnly TypedDict key "x" TypedDict is mutated [case testTypedDictUpdateClosed] +# flags: --python-version 3.15 import b [file a.py] from typing import TypedDict, Union @@ -3789,6 +3790,7 @@ b.py:9: error: TypedDict "B" has no key "a" == [case testTypedDictUpdateClosedPassingToFailing] +# flags: --python-version 3.15 import b [file a.py] from typing import TypedDict, Union diff --git a/test-data/unit/fixtures/typing-typeddict.pyi b/test-data/unit/fixtures/typing-typeddict.pyi index 0bc5637b32708..90c5a71b8d037 100644 --- a/test-data/unit/fixtures/typing-typeddict.pyi +++ b/test-data/unit/fixtures/typing-typeddict.pyi @@ -30,6 +30,7 @@ NotRequired = 0 ReadOnly = 0 Self = 0 ClassVar = 0 +TYPE_CHECKING = False T = TypeVar('T') T_co = TypeVar('T_co', covariant=True)