pallets-eco / flask-sqlalchemy

Adds SQLAlchemy support to Flask
https://flask-sqlalchemy.palletsprojects.com
BSD 3-Clause "New" or "Revised" License
4.18k stars 897 forks source link

Type (typehint) error for `db.relationship` #1318

Open cainmagi opened 3 months ago

cainmagi commented 3 months ago

Problem Description

The typehint of

db.relationship("...", secondary=..., back_populates="...")

should be sq_orm.Relationship[...], not sq_orm.RelationshipProperty[...].

The mismatch of the typehint causes the manual annotation supported by sqlalchemy fails:

image

How to fix it

Go here: https://github.com/pallets-eco/flask-sqlalchemy/blob/42a36a3cb604fd39d81d00b54ab3988bbd0ad184/src/flask_sqlalchemy/extension.py#L953-L963

Make this modification:

    def relationship(
        self, *args: t.Any, **kwargs: t.Any
-   ) -> sa_orm.RelationshipProperty[t.Any]:
+   ) -> sa_orm.Relationship[t.Any]:
        """A :func:`sqlalchemy.orm.relationship` that applies this extension's

Things will get corrected.

It is also recommended to modify this place:

https://github.com/pallets-eco/flask-sqlalchemy/blob/42a36a3cb604fd39d81d00b54ab3988bbd0ad184/src/flask_sqlalchemy/extension.py#L977-L979

But the following place should NOT be changed, because it is consistent with sq_orm: https://github.com/pallets-eco/flask-sqlalchemy/blob/42a36a3cb604fd39d81d00b54ab3988bbd0ad184/src/flask_sqlalchemy/extension.py#L965-L967

Codes with typehint errors when using flask-sqlalchemy

# -*- coding: UTF-8 -*-

try:
    from typing import List
except ImportError:
    from builtins import list as List

from flask_sqlalchemy import SQLAlchemy
import sqlalchemy as sa
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.orm import DeclarativeBase, MappedAsDataclass

class Base(DeclarativeBase, MappedAsDataclass):
    """The base class for creating SQLAlchemy models.

    All mixins are defined in the mro list.

    All metadata of are defined as attributes.
    """

db = SQLAlchemy(model_class=Base)

roles = db.Table(
    "role_users",
    sa.Column("user_id", sa.ForeignKey("user.id"), primary_key=True),
    sa.Column("role_id", sa.ForeignKey("role.id"), primary_key=True),
)

class User(db.Model):
    id: Mapped[int] = mapped_column(primary_key=True, init=False)
    # Expression of type "RelationshipProperty[Any]" cannot be assigned to declared type "Mapped[List[Role]]"
    # "RelationshipProperty[Any]" is incompatible with "Mapped[List[Role]]"Pylance[reportAssignmentType] 
    # (https://github.com/microsoft/pyright/blob/main/docs/configuration.md#reportAssignmentType)
    roles: Mapped[List["Role"]] = db.relationship(
        "Role", secondary=roles, back_populates="users", default_factory=list
    )

class Role(db.Model):
    id: Mapped[int] = mapped_column(primary_key=True, init=False)
    # Expression of type "RelationshipProperty[Any]" cannot be assigned to declared type "Mapped[List[User]]"
    #  "RelationshipProperty[Any]" is incompatible with "Mapped[List[User]]"Pylance[reportAssignmentType] 
    # (https://github.com/microsoft/pyright/blob/main/docs/configuration.md#reportAssignmentType)
    users: Mapped[List["User"]] = db.relationship(
        "User", secondary=roles, back_populates="roles", default_factory=list
    )

Codes working perfectly if only using sqlalchemy

# -*- coding: UTF-8 -*-

try:
    from typing import List
except ImportError:
    from builtins import list as List

import sqlalchemy as sa
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.orm import DeclarativeBase, MappedAsDataclass

class Base(DeclarativeBase, MappedAsDataclass):
    """The base class for creating SQLAlchemy models.

    All mixins are defined in the mro list.

    All metadata of are defined as attributes.
    """

roles = sa.Table(
    "role_users",
    Base.metadata,
    sa.Column("user_id", sa.ForeignKey("user.id"), primary_key=True),
    sa.Column("role_id", sa.ForeignKey("role.id"), primary_key=True),
)

class User(Base):
    __tablename__ = "users"
    id: Mapped[int] = mapped_column(primary_key=True, init=False)
    roles: Mapped[List["Role"]] = relationship(
        "Role", secondary=roles, back_populates="users", default_factory=list
    )

class Role(Base):
    __tablename__ = "roles"
    id: Mapped[int] = mapped_column(primary_key=True, init=False)
    users: Mapped[List["User"]] = relationship(
        "User", secondary=roles, back_populates="roles", default_factory=list
    )

Environment:

davidism commented 3 months ago

Happy to review a PR.