Skip to content

Commit 3ea6398

Browse files
committed
feat: add type validation for nested Meta class
1 parent 3e973dc commit 3ea6398

1 file changed

Lines changed: 37 additions & 26 deletions

File tree

mypy_django_plugin/transformers/models.py

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -260,39 +260,50 @@ class InjectAnyAsBaseForNestedMeta(ModelClassInitializer):
260260

261261
@override
262262
def run(self) -> None:
263-
meta_node = helpers.get_nested_meta_node_for_current_class(
264-
self.model_classdef.info
265-
)
263+
# 1. Get the Meta node safely
264+
meta_node = helpers.get_nested_meta_node_for_current_class(self.model_classdef.info)
265+
if meta_node is None:
266+
if "Meta" in self.model_classdef.info.names:
267+
sym = self.model_classdef.info.names["Meta"]
268+
if sym and isinstance(sym.node, TypeInfo):
269+
meta_node = sym.node
270+
266271
if meta_node is None:
267272
return None
268273

269-
meta_node.fallback_to_any = True
270-
typed_model_meta_info = self.lookup_typeinfo(
271-
fullnames.TYPED_MODEL_META_FULLNAME
272-
)
274+
# 2. Look up TypedModelMeta from django-stubs-ext
275+
typed_model_meta_info = self.lookup_typeinfo(fullnames.TYPED_MODEL_META_FULLNAME)
276+
277+
# If TypedModelMeta is not resolved, we fallback to Any to maintain compatibility
278+
if not typed_model_meta_info:
279+
meta_node.fallback_to_any = True
280+
return None
281+
282+
# 3. Validation Logic: Compare Meta attributes against TypedModelMeta
283+
meta_node.fallback_to_any = False
273284

274-
if typed_model_meta_info:
275-
for name, sym in meta_node.names.items():
276-
if name in typed_model_meta_info.names:
277-
parent_sym = typed_model_meta_info.names[name]
285+
for name, sym in meta_node.names.items():
286+
# Skip empty or uninitialized symbols to prevent internal errors
287+
if not sym or not sym.node or not hasattr(sym, 'type') or sym.type is None:
288+
continue
278289

290+
# Only validate attributes defined in TypedModelMeta (e.g., verbose_name, db_table)
291+
if name in typed_model_meta_info.names:
292+
parent_sym = typed_model_meta_info.names[name]
293+
if parent_sym and parent_sym.type:
279294
actual_type = get_proper_type(sym.type)
280295
expected_type = get_proper_type(parent_sym.type)
281-
282-
if actual_type is None or expected_type is None:
283-
if not self.api.final_iteration:
284-
raise helpers.IncompleteDefnException()
285-
continue
286-
287-
if not is_subtype(actual_type, expected_type):
288-
node_context = sym.node if sym.node is not None else self.ctx.api
289-
error_context = cast(Context, node_context)
290-
291-
self.ctx.api.fail(
292-
f'Incompatible type for "{name}" in Meta '
293-
f'(expected "{expected_type}", got "{actual_type}")',
294-
error_context,
295-
)
296+
297+
if actual_type and expected_type:
298+
# Check if the attribute type matches the expected Django type
299+
if not is_subtype(actual_type, expected_type):
300+
self.api.fail(
301+
f'Incompatible type for "{name}" in Meta (expected "{expected_type}", got "{actual_type}")',
302+
sym.node
303+
)
304+
305+
# 4. Re-enable fallback for custom/third-party Meta options
306+
meta_node.fallback_to_any = True
296307
return None
297308

298309
class AddDefaultPrimaryKey(ModelClassInitializer):

0 commit comments

Comments
 (0)