4040 require_bool_literal_argument ,
4141)
4242from mypy .state import state
43- from mypy .subtypes import is_equivalent , is_subtype
4443from mypy .typeanal import check_for_explicit_any , has_any_from_unimported_type
4544from mypy .types import (
4645 TPDICT_NAMES ,
4746 AnyType ,
4847 ReadOnlyType ,
4948 RequiredType ,
5049 Type ,
50+ TypedDictFieldConstraint ,
5151 TypedDictType ,
5252 TypeOfAny ,
5353 TypeVarLikeType ,
@@ -182,7 +182,7 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> tuple[bool, TypeInfo | N
182182 new_field_sources , new_statements = self .analyze_typeddict_classdef_fields (defn )
183183 if new_field_sources is None :
184184 return True , None # Defer
185- field_types , required_keys , readonly_keys , is_closed , any_placeholders = (
185+ field_types , required_keys , readonly_keys , is_closed , constraints = (
186186 self .resolve_field_inheritance (bases_info , new_field_sources , is_closed , defn )
187187 )
188188 info = self .build_typeddict_typeinfo (
@@ -193,7 +193,7 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> tuple[bool, TypeInfo | N
193193 is_closed ,
194194 defn .line ,
195195 existing_info ,
196- analysis_incomplete = any_placeholders ,
196+ constraints = constraints ,
197197 )
198198 defn .analyzed = TypedDictExpr (info )
199199 defn .analyzed .line = defn .line
@@ -266,45 +266,16 @@ def primary_source(self, sources: list[FieldSource]) -> FieldSource:
266266 mutable_sources = (s for s in reversed (sources ) if not s .is_readonly )
267267 return next (mutable_sources , sources [- 1 ])
268268
269- def verify_field_compatibility (
269+ def verify_requiredness_compatibility (
270270 self ,
271271 field_name : str ,
272272 source : FieldSource ,
273- field_type : Type ,
274- is_readonly : bool ,
275273 is_required : bool ,
276274 primary_source_base_name : str | None ,
277275 ctx : Context ,
278276 ) -> None :
279- """Verify compatibility of the final child type field with a base class source.
280-
281- Verifies type compatibility, requiredness, and mutability.
282- """
283- if has_placeholder (field_type ) or has_placeholder (source .field_type ):
284- # Cannot verify type compatibility yet, but analysis will be deferred
285- # and rerun later
286- is_type_compatible = True
287- elif source .is_readonly :
288- is_type_compatible = is_subtype (field_type , source .field_type )
289- else :
290- is_type_compatible = is_equivalent (field_type , source .field_type )
291- if not is_type_compatible :
292- if primary_source_base_name is None :
293- self .fail (
294- f'Definition of field "{ field_name } " incompatible with base class "{ source .base_name } "' ,
295- ctx ,
296- )
297- else :
298- self .fail (
299- f'Incompatible definitions of field "{ field_name } " in base classes "{ source .base_name } " and "{ primary_source_base_name } "' ,
300- ctx ,
301- )
302- if is_readonly :
303- self .note (
304- f'This can be resolved by redeclaring the field "{ field_name } " with a mutually compatible type' ,
305- ctx ,
306- )
307- elif source .is_required and not is_required :
277+ """Verify requiredness compatibility of the final child type field with a base class source."""
278+ if source .is_required and not is_required :
308279 if primary_source_base_name is None :
309280 self .fail (
310281 f'Field "{ field_name } " is required in base class "{ source .base_name } "' , ctx
@@ -325,6 +296,36 @@ def verify_field_compatibility(
325296 ctx ,
326297 )
327298
299+ def field_compatibility_constraint (
300+ self ,
301+ field_name : str ,
302+ source : FieldSource ,
303+ is_readonly : bool ,
304+ primary_source_base_name : str | None ,
305+ ctx : Context ,
306+ ) -> TypedDictFieldConstraint :
307+ """Information needed to verify compatibility of the final child type field with a base class source.
308+
309+ This cannot be done during semantic analysis, as it is not safe to check subtyping.
310+ """
311+ failure_note = None
312+ if primary_source_base_name is None :
313+ failure_message = f'Definition of field "{ field_name } " incompatible with base class "{ source .base_name } "'
314+ else :
315+ failure_message = f'Incompatible definitions of field "{ field_name } " in base classes "{ source .base_name } " and "{ primary_source_base_name } "'
316+ if is_readonly :
317+ failure_note = f'This can be resolved by redeclaring the field "{ field_name } " with a mutually compatible type'
318+
319+ constraint = TypedDictFieldConstraint (
320+ field_name = field_name ,
321+ field_type = source .field_type ,
322+ is_readonly = source .is_readonly ,
323+ failure_message = failure_message ,
324+ failure_note = failure_note ,
325+ ctx = ctx ,
326+ )
327+ return constraint
328+
328329 def verify_field_against_closed_bases (
329330 self ,
330331 field_name : str ,
@@ -353,17 +354,13 @@ def resolve_field_inheritance(
353354 child_field_sources : dict [str , FieldSource ],
354355 child_is_closed : bool | None ,
355356 ctx : Context ,
356- ) -> tuple [dict [str , Type ], set [str ], set [str ], bool , bool ]:
357- """Determine field types, requiredness, readonlyness, and closedness.
358-
359- Additionally returns if any placeholders were seen, as they will prevent full
360- analysis, but may not result in placeholders in the final type.
361- """
357+ ) -> tuple [dict [str , Type ], set [str ], set [str ], bool , list [TypedDictFieldConstraint ]]:
358+ """Determine field types, requiredness, readonlyness, closedness, and delayed constraints."""
362359 field_sources = self .field_sources_in_reverse_mro (bases , child_field_sources )
363360 field_types : dict [str , Type ] = {}
364361 required_keys : set [str ] = set ()
365362 readonly_keys : set [str ] = set ()
366- any_placeholders = False
363+ constraints : list [ TypedDictFieldConstraint ] = []
367364 closed_bases = [
368365 (base_info , base_fields .keys ())
369366 for (base_info , base_fields ) in bases
@@ -401,18 +398,22 @@ def resolve_field_inheritance(
401398 readonly_keys .add (field_name )
402399
403400 for source in sources :
404- if has_placeholder (source .field_type ):
405- any_placeholders = True
406401 if source is not primary_source :
407- self .verify_field_compatibility (
402+ self .verify_requiredness_compatibility (
408403 field_name ,
409404 source ,
410- field_types [field_name ],
411- is_readonly ,
412405 is_required ,
413406 primary_source .base_name ,
414407 primary_source .child_field_ctx or ctx ,
415408 )
409+ constraint = self .field_compatibility_constraint (
410+ field_name ,
411+ source ,
412+ is_readonly ,
413+ primary_source .base_name ,
414+ primary_source .child_field_ctx or ctx ,
415+ )
416+ constraints .append (constraint )
416417 self .verify_field_against_closed_bases (
417418 field_name ,
418419 closed_bases ,
@@ -421,7 +422,7 @@ def resolve_field_inheritance(
421422 )
422423
423424 is_closed = bool (closed_bases ) if child_is_closed is None else child_is_closed
424- return field_types , required_keys , readonly_keys , is_closed , any_placeholders
425+ return field_types , required_keys , readonly_keys , is_closed , constraints
425426
426427 def _parse_typeddict_base (self , base : Expression , ctx : Context ) -> TypeInfo :
427428 if isinstance (base , RefExpr ):
@@ -814,7 +815,7 @@ def build_typeddict_typeinfo(
814815 is_closed : bool ,
815816 line : int ,
816817 existing_info : TypeInfo | None ,
817- analysis_incomplete : bool = False ,
818+ constraints : list [ TypedDictFieldConstraint ] | None = None ,
818819 ) -> TypeInfo :
819820 # Prefer typing then typing_extensions if available.
820821 fallback = (
@@ -827,7 +828,11 @@ def build_typeddict_typeinfo(
827828 typeddict_type = TypedDictType (
828829 item_types , required_keys , readonly_keys , is_closed , fallback
829830 )
830- if has_placeholder (typeddict_type ) or analysis_incomplete :
831+ if constraints :
832+ typeddict_type .field_constraints .extend (constraints )
833+ if has_placeholder (typeddict_type ) or any (
834+ has_placeholder (c .field_type ) for c in typeddict_type .field_constraints
835+ ):
831836 typeddict_type .analysis_incomplete = True
832837 self .api .process_placeholder (
833838 None , "TypedDict item" , info , force_progress = typeddict_type != info .typeddict_type
0 commit comments