Skip to content

get_dynamic_class_hook does not handle defer properly #17402

Open
@asottile-sentry

Description

@asottile-sentry

Bug Report

typically, ctx.api.defer() can be called in a plugin when there is insufficient information to defer calling to a future pass. this "works" for get_dynamic_class_hook but causes the dynamic class assignment to be converted into a Var rendering it useless as a base class later.

I've adapted a test plugin from the mypy codebase to demonstrate this problem

To Reproduce

the plugin I've adapted responds to an EAGER=1 environment variable -- where it will not defer the class creation. EAGER=0 simulates when insufficient information is available so it must defer the class to a separate pass

# mypy.ini
[mypy]
plugins = _plugin
# _plugin.py
from __future__ import annotations

import os
from typing import Callable

from mypy.nodes import GDEF, Block, ClassDef, SymbolTable, SymbolTableNode, TypeInfo, Var
from mypy.plugin import ClassDefContext, DynamicClassDefContext, Plugin
from mypy.types import Instance, get_proper_type


_DEFERRED = os.environ.get('EAGER') == '1'


class DynPlugin(Plugin):
    def get_dynamic_class_hook(
        self, fullname: str
    ) -> Callable[[DynamicClassDefContext], None] | None:
        if fullname == "mod.declarative_base":
            return add_info_hook
        return None


def add_info_hook(ctx: DynamicClassDefContext) -> None:
    global _DEFERRED
    if not _DEFERRED:
        _DEFERRED = True
        ctx.api.defer()
        print('defering!')
        return

    class_def = ClassDef(ctx.name, Block([]))
    class_def.fullname = ctx.api.qualified_name(ctx.name)

    info = TypeInfo(SymbolTable(), class_def, ctx.api.cur_mod_id)
    class_def.info = info
    obj = ctx.api.named_type("builtins.object")
    info.mro = [info, obj.type]
    info.bases = [obj]
    ctx.api.add_symbol_table_node(ctx.name, SymbolTableNode(GDEF, info))
    print('creating!')


def plugin(version: str) -> type[DynPlugin]:
    return DynPlugin
# mod.py
def declarative_base(name: str) -> object:
    raise NotImplementedError

cls = declarative_base("wat")

class C(cls): pass

Expected Behavior

both of these should pass type checking

rm -rf .mypy_cache && EAGER=1 python -m mypy mod.py
rm -rf .mypy_cache && EAGER=0 python -m mypy mod.py

Actual Behavior

$ rm -rf .mypy_cache && EAGER=1 python -m mypy mod.py
creating!
Success: no issues found in 1 source file
$ rm -rf .mypy_cache && EAGER=0 python -m mypy mod.py
defering!
creating!
mod.py:6: error: Variable "mod.cls" is not valid as a type  [valid-type]
mod.py:6: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
mod.py:6: error: Invalid base class "cls"  [misc]
Found 2 errors in 1 file (checked 1 source file)

Your Environment

  • Mypy version used: current HEAD 10f18a8
  • Mypy command-line flags: see above
  • Mypy configuration options from mypy.ini (and other config files): see above
  • Python version used: 3.12.2 (though it doesn't seem to matter)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugmypy got something wrongtopic-pluginsThe plugin API and ideas for new plugins

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions