Is there any trick to pacify type checkers when using a creative use of the "set" event on an attribute? #13135
-
|
Hi all, I always been a fan of the imperative mapping style, in particular in complex applications, but I started to change my mind and to appreciate the advantages that the declarative style brings, with its PEP 484 support. So I took one of my biggest app and modernized it on that aspects, and I'm delighted of the result. There is one single issue, albeit marginal, that I could not figure out how to solve, without resorting to A few entities use the set event on some of their relationship, that allows me to have a kind of shortcut where I can assign a raw value instead of a proper instance, that the handler validates, elaborates and eventually morphs into a concrete related entity instance. Here is a stripped down and fictional example: from typing import Union
from sqlalchemy import ForeignKey
from sqlalchemy import String
from sqlalchemy import create_engine
from sqlalchemy import event
from sqlalchemy import select
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import Session
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "user_account"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(30), unique=True)
fullname: Mapped[str | None]
address_id: Mapped[int] = mapped_column(ForeignKey("address.id"))
address: Mapped["Address"] = relationship(back_populates="user") # TRIGGERS ERR#1
#address: Mapped[Union["Address", str]] = relationship(back_populates="user") # TRIGGERS ERR#2
def __repr__(self) -> str:
return f"User(id={self.id!r}, name={self.name!r}, fullname={self.fullname!r})"
class Address(Base):
__tablename__ = "address"
id: Mapped[int] = mapped_column(primary_key=True)
email_address: Mapped[str]
user: Mapped["User"] = relationship(back_populates="address")
def __repr__(self) -> str:
return f"Address(id={self.id!r}, email_address={self.email_address!r})"
def handle_plain_email(target, value, oldvalue, initiator):
if isinstance(value, str):
return Address(email_address=value)
else:
return value
event.listen(User.address, 'set', handle_plain_email, retval=True)
engine = create_engine("sqlite://", echo=True)
Base.metadata.create_all(engine)
with Session(engine) as session:
spongebob = User(
name="spongebob",
fullname="Spongebob Squarepants",
address=Address(email_address="spongebob@sqlalchemy.org"),
)
sandy = User(
name="sandy",
fullname="Sandy Cheeks",
address=Address(email_address="sandy@sqlalchemy.org"),
)
patrick = User(
name="patrick",
fullname="Patrick Star",
address='patrick@star.example.com',
)
session.add_all([spongebob, sandy, patrick])
session.commit()
buzz = User()
buzz.name = 'buzz'
buzz.fullname = 'Buzz Lighyear'
buzz.address = 'buzz@toy.com' # ERR#1
session.add(buzz)
session.commit()
print(buzz.address.email_address) # ERR#2
for user in session.scalars(select(User)):
print(user, user.address)The above works but, for example, this is what while If I use the while As you can see, this is rather marginal: I have a dozens of such direct assignments in the code, and a Thanks&bye, lele. |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 1 reply
-
|
Hi, I don't think this is solvable out of the box without specifically catering for this use case An option could be to trade type ignores when setting the "shortcut" type for type ignores in the model definition using something like this if TYPE_CHECKING:
_T_co = TypeVar("_T_co", covariant=True)
class MappedAndString(orm.Mapped[_T_co]):
if TYPE_CHECKING:
def __set__(
self,
instance: sa.Any,
value: SQLCoreOperations[_T_co] | _T_co | str,
) -> None: ...
else:
MappedAndString = orm.Mappedyou then use it as class User(Base):
__tablename__ = "user_account"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(30), unique=True)
fullname: Mapped[str | None]
address_id: Mapped[int] = mapped_column(ForeignKey("address.id"))
address: MappedAndString["Address"] = relationship(back_populates="user") # type: ignoreThe type ignore there is needed since the type checker now thinks that relationship does not return the proper subclass. But this now works fine |
Beta Was this translation helpful? Give feedback.
Hi,
I don't think this is solvable out of the box without specifically catering for this use case
An option could be to trade type ignores when setting the "shortcut" type for type ignores in the model definition using something like this
you then use it as