"""
+from __future__ import annotations
+
+from typing import Any
+from typing import TYPE_CHECKING
+
from sqlalchemy import create_engine
from sqlalchemy import ForeignKey
from sqlalchemy.ext.associationproxy import association_proxy
+from sqlalchemy.ext.associationproxy import AssociationProxy
from sqlalchemy.orm import backref
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import declared_attr
and surrogate primary key column.
"""
- @declared_attr
- def __tablename__(cls):
+ @declared_attr.directive
+ def __tablename__(cls) -> str:
return cls.__name__.lower()
id: Mapped[int] = mapped_column(primary_key=True)
discriminator: Mapped[str] = mapped_column()
"""Refers to the type of parent."""
- addresses: Mapped[list["Address"]] = relationship(
+ addresses: Mapped[list[Address]] = relationship(
back_populates="association"
)
street: Mapped[str]
city: Mapped[str]
zip: Mapped[str]
- association: Mapped["AddressAssociation"] = relationship(
+ association: Mapped[AddressAssociation] = relationship(
back_populates="addresses"
)
- parent = association_proxy("association", "parent")
+ parent: AssociationProxy[HasAddresses] = association_proxy(
+ "association", "parent"
+ )
- def __repr__(self):
+ def __repr__(self) -> str:
return "%s(street=%r, city=%r, zip=%r)" % (
self.__class__.__name__,
self.street,
the address_association table for each parent.
"""
+ if TYPE_CHECKING:
+ addresses: AssociationProxy[list[Address]]
+
@declared_attr
- def address_association_id(cls) -> Mapped[int]:
+ def address_association_id(cls: type[Any]) -> Mapped[int]:
return mapped_column(ForeignKey("address_association.id"))
@declared_attr
- def address_association(cls):
+ def address_association(cls: type[Any]) -> Mapped[AddressAssociation]:
name = cls.__name__
discriminator = name.lower()
"""
+from __future__ import annotations
+
+from typing import Any
+from typing import cast
+from typing import TYPE_CHECKING
+
from sqlalchemy import and_
from sqlalchemy import create_engine
from sqlalchemy import event
from sqlalchemy.orm import foreign
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
+from sqlalchemy.orm import Mapper
from sqlalchemy.orm import relationship
from sqlalchemy.orm import remote
from sqlalchemy.orm import Session
and surrogate primary key column.
"""
- @declared_attr
- def __tablename__(cls):
+ @declared_attr.directive
+ def __tablename__(cls) -> str:
return cls.__name__.lower()
id: Mapped[int] = mapped_column(primary_key=True)
"""
@property
- def parent(self):
+ def parent(self) -> HasAddresses:
"""Provides in-Python access to the "parent" by choosing
the appropriate relationship.
"""
- return getattr(self, f"parent_{self.discriminator}")
+ return cast(
+ HasAddresses, getattr(self, f"parent_{self.discriminator}")
+ )
- def __repr__(self):
+ def __repr__(self) -> str:
return "%s(street=%r, city=%r, zip=%r)" % (
self.__class__.__name__,
self.street,
"""
+ if TYPE_CHECKING:
+ addresses: Mapped[list[Address]]
+
@event.listens_for(HasAddresses, "mapper_configured", propagate=True)
-def setup_listener(mapper, class_):
+def setup_listener(mapper: Mapper[Any], class_: type[Any]) -> None:
name = class_.__name__
discriminator = name.lower()
class_.addresses = relationship(
)
@event.listens_for(class_.addresses, "append")
- def append_address(target, value, initiator):
+ def append_address(
+ target: HasAddresses, value: Address, initiator: Any
+ ) -> None:
value.discriminator = discriminator
"""
+from __future__ import annotations
+
from sqlalchemy import Column
from sqlalchemy import create_engine
from sqlalchemy import ForeignKey
and surrogate primary key column.
"""
- @declared_attr
- def __tablename__(cls):
+ @declared_attr.directive
+ def __tablename__(cls) -> str:
return cls.__name__.lower()
id: Mapped[int] = mapped_column(primary_key=True)
city: Mapped[str]
zip: Mapped[str]
- def __repr__(self):
+ def __repr__(self) -> str:
return "%s(street=%r, city=%r, zip=%r)" % (
self.__class__.__name__,
self.street,
"""
@declared_attr
- def addresses(cls):
+ def addresses(cls: type[DeclarativeBase]) -> Mapped[list[Address]]:
address_association = Table(
"%s_addresses" % cls.__tablename__,
cls.metadata,
"""
+from __future__ import annotations
+
+from typing import Any
+from typing import TYPE_CHECKING
+
from sqlalchemy import create_engine
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
"""
- @declared_attr
- def __tablename__(cls):
+ @declared_attr.directive
+ def __tablename__(cls) -> str:
return cls.__name__.lower()
id: Mapped[int] = mapped_column(primary_key=True)
city: Mapped[str]
zip: Mapped[str]
- def __repr__(self):
+ def __repr__(self) -> str:
return "%s(street=%r, city=%r, zip=%r)" % (
self.__class__.__name__,
self.street,
)
+if TYPE_CHECKING:
+
+ class AddressWithParent(Address):
+ """Type stub for Address subclasses created by HasAddresses.
+
+ Inherits street, city, zip from Address.
+
+ Allows mypy to understand when <class>.Address is created,
+ it will have `parent_id` and `parent` attributes.
+ If you won't use `parent_id` attribute directly,
+ there's no need to specify here, included for completeness.
+ """
+
+ parent_id: int
+ parent: HasAddresses
+
+
class HasAddresses:
"""HasAddresses mixin, creates a new Address class
for each parent.
"""
@declared_attr
- def addresses(cls):
+ def addresses(cls: type[Any]) -> Mapped[list[AddressWithParent]]:
cls.Address = type(
f"{cls.__name__}Address",
(Address, Base),
import os
+from pathlib import Path
from sqlalchemy import testing
from sqlalchemy.testing import fixtures
)
def test_mypy_no_plugin(self, mypy_typecheck_file, path):
mypy_typecheck_file(path)
+
+
+class MypyExamplesTest(fixtures.MypyTest):
+ """Test that examples pass mypy strict mode."""
+
+ # Path to examples/generic_associations relative to repo root
+ _examples_path = Path(__file__).parent.parent.parent / "examples"
+
+ @testing.combinations(
+ *(
+ (path.name, str(path))
+ for path in (_examples_path / "generic_associations").glob("*.py")
+ if path.name != "__init__.py"
+ ),
+ argnames="path",
+ id_="ia",
+ )
+ def test_generic_associations_examples(self, mypy_typecheck_file, path):
+ mypy_typecheck_file(path)