sqlalchemy / sqlalchemy2-stubs

PEP-484 typing stubs for SQLAlchemy 1.4
MIT License
159 stars 41 forks source link

Invalid base class "Base" when returned by a function #242

Closed iskyd closed 1 year ago

iskyd commented 1 year ago

Describe the bug mypy returns [Invald ](error: Invalid base class "Base" [misc]) when Base class is returned by a function.

To Reproduce

pip install sqlalchemy[mypy]==1.4.44
mypy --config-file=mypy.ini test.py

mypy.ini

[mypy]
plugins = sqlalchemy.ext.mypy.plugin

test.py

from sqlalchemy import String, select
from sqlalchemy.orm import declarative_base
from typing import Optional, Dict, Type
import sqlalchemy
from sqlalchemy.orm.ext import DeclarativeMeta

def get_base() -> Type[DeclarativeMeta]:
    return declarative_base()

Base: Type[DeclarativeMeta] = get_base()

class User(Base):
    __tablename__ = "user"

    id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
    name = sqlalchemy.Column(sqlalchemy.String)

Error

error: Invalid base class "Base"

Versions.

Have a nice day!

CaselIT commented 1 year ago

Hi,

yes, that's currently expect. In v2 base will be generated from a superclass using the classic class Base(superclass):.

That's not really fixable in the 1.4, add a type: ignore

iskyd commented 1 year ago

@CaselIT adding it's not a good solution because you still get all other errors related to the fact that mypy can't create a mapping.

Consider this snippet

from sqlalchemy import String, select
from sqlalchemy.orm import declarative_base
from typing import Optional, Dict, Type
import sqlalchemy
from sqlalchemy.orm.ext import DeclarativeMeta

def get_base() -> Type[DeclarativeMeta]:
    return declarative_base()

Base: Type[DeclarativeMeta] = get_base()

class User(Base): # type: ignore
    __tablename__ = "user"

    id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
    name = sqlalchemy.Column(sqlalchemy.String)

def foo(name: str):
    print(name)

u = User(id=1, name="Pippo")

mypy gives this error: error: Argument 1 to "foo" has incompatible type "Column[String]"; expected "str"

It seems that without Base class using sqlalchemy plugin for mypy isn't really helpfull. Am i missing something?

zzzeek commented 1 year ago

the mypy plugin will allow you to use the declarative_base() function directly:

Base = declarative_base()

The plugin will convince mypy that "Base" is a class. however, it won't work if you place the call to declarative_base() inside of another function. Python typing very plainly does not support classes being returned from functions.

To use the mypy plugin with a base class that isn't built using declarative_base(), in 1.4 you can use the recipe at https://docs.sqlalchemy.org/en/14/orm/declarative_styles.html#creating-an-explicit-base-non-dynamically-for-use-with-mypy-similar .

as mentioned before, in 2.0 SQLAlchemy provides new patterns that support typing without plugins from start to finish.

iskyd commented 1 year ago

@zzzeek I'm trying with the following snippet that works.

from sqlalchemy import String, select
from sqlalchemy.orm import registry
from typing import Optional, Dict, Type
import sqlalchemy
from sqlalchemy.orm.decl_api import DeclarativeMeta

mapper_registry = registry()

class Base(metaclass=DeclarativeMeta):
    __abstract__ = True

    registry = mapper_registry
    metadata = mapper_registry.metadata

    __init__ = mapper_registry.constructor

class User(Base):
    __tablename__ = "user"

    id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
    name = sqlalchemy.Column(sqlalchemy.String)

def foo(name: str):
    print(name)

u = User(id=1, name="Pippo")

foo(u.name)

But I don't get how it fits with the declarative_base() returned by a function. This way I'm going to simply override Base class. Does it change the behaviour of the system? Where the declarative_base() returns is going to be used?

zzzeek commented 1 year ago

the declarative_base() function invokes Python code that is the equivalent operation as doing your "class Base" declaration. It uses the Python type() function to create a new type. But also it could just as well have that same "class Base" declaration inside of it, then return the class; Python typing has no way to represent that returned class as something you can subclass.

zzzeek commented 1 year ago

short answer no, there's nothing different. as long as your Base class is making mappings, it's doing the thing it's supposed to do.

unode commented 1 year ago

Is there a guideline to addressing this issue in 2.0? We are on 2.0.12 and still encounter the same type checking error.

error: Invalid base class "Base"  [misc]
error: Variable "DBApp.models.Base" is not valid as a type  [valid-type]

with:

Base: DeclarativeMeta = declarative_base()

class Users(Base, DiffMixin):
    __tablename__ = "users"
   (...)
CaselIT commented 1 year ago

the declarative_base function is kinda deprecated and you should do as all the v2 examples in the docs, creating the base manually:


>>> from sqlalchemy.orm import DeclarativeBase

>>> class Base(DeclarativeBase):
...     pass
CaselIT commented 1 year ago

Closing since it's solved in v2