flask-sqlalchemy icon indicating copy to clipboard operation
flask-sqlalchemy copied to clipboard

Type (typehint) error for `db.relationship`

Open cainmagi opened this issue 1 year ago • 5 comments

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:

  • Python version: 3.10.13
  • Flask-SQLAlchemy version: 3.1.1
  • SQLAlchemy version: 2.0.28

cainmagi avatar Mar 26 '24 16:03 cainmagi

Happy to review a PR.

davidism avatar Mar 26 '24 17:03 davidism