|
1 | 1 | from __future__ import annotations
|
2 | 2 |
|
3 | 3 | import typing as t
|
| 4 | +from datetime import datetime |
4 | 5 |
|
5 | 6 | import pytest
|
6 | 7 | import sqlalchemy as sa
|
@@ -80,6 +81,182 @@ class Base(sa_orm.DeclarativeBaseNoMeta, sa_orm.MappedAsDataclass):
|
80 | 81 | assert isinstance(db.Model, sa_orm.decl_api.DCTransformDeclarative)
|
81 | 82 |
|
82 | 83 |
|
| 84 | +@pytest.mark.usefixtures("app_ctx") |
| 85 | +def test_declaredattr(app: Flask, model_class: t.Any) -> None: |
| 86 | + if model_class is Model: |
| 87 | + |
| 88 | + class IdModel(Model): |
| 89 | + @sa.orm.declared_attr |
| 90 | + @classmethod |
| 91 | + def id(cls: type[Model]): # type: ignore[no-untyped-def] |
| 92 | + for base in cls.__mro__[1:-1]: |
| 93 | + if getattr(base, "__table__", None) is not None and hasattr( |
| 94 | + base, "id" |
| 95 | + ): |
| 96 | + return sa.Column(sa.ForeignKey(base.id), primary_key=True) |
| 97 | + return sa.Column(sa.Integer, primary_key=True) |
| 98 | + |
| 99 | + db = SQLAlchemy(app, model_class=IdModel) |
| 100 | + |
| 101 | + class User(db.Model): |
| 102 | + name = db.Column(db.String) |
| 103 | + |
| 104 | + class Employee(User): |
| 105 | + title = db.Column(db.String) |
| 106 | + |
| 107 | + else: |
| 108 | + |
| 109 | + class Base(sa_orm.DeclarativeBase): |
| 110 | + @sa_orm.declared_attr |
| 111 | + @classmethod |
| 112 | + def id(cls: type[sa_orm.DeclarativeBase]) -> sa_orm.Mapped[int]: |
| 113 | + for base in cls.__mro__[1:-1]: |
| 114 | + if getattr(base, "__table__", None) is not None and hasattr( |
| 115 | + base, "id" |
| 116 | + ): |
| 117 | + return sa_orm.mapped_column( |
| 118 | + db.ForeignKey(base.id), primary_key=True |
| 119 | + ) |
| 120 | + return sa_orm.mapped_column(db.Integer, primary_key=True) |
| 121 | + |
| 122 | + db = SQLAlchemy(app, model_class=Base) |
| 123 | + |
| 124 | + class User(db.Model): # type: ignore[no-redef] |
| 125 | + name: sa_orm.Mapped[str] = sa_orm.mapped_column(db.String) |
| 126 | + |
| 127 | + class Employee(User): # type: ignore[no-redef] |
| 128 | + title: sa_orm.Mapped[str] = sa_orm.mapped_column(db.String) |
| 129 | + |
| 130 | + db.create_all() |
| 131 | + db.session.add(Employee(name="Emp Loyee", title="Admin")) |
| 132 | + db.session.commit() |
| 133 | + user = db.session.execute(db.select(User)).scalar() |
| 134 | + employee = db.session.execute(db.select(Employee)).scalar() |
| 135 | + assert user is not None |
| 136 | + assert employee is not None |
| 137 | + assert user.id == 1 |
| 138 | + assert employee.id == 1 |
| 139 | + |
| 140 | + |
| 141 | +@pytest.mark.usefixtures("app_ctx") |
| 142 | +def test_abstractmodel(app: Flask, model_class: t.Any) -> None: |
| 143 | + db = SQLAlchemy(app, model_class=model_class) |
| 144 | + |
| 145 | + if issubclass(db.Model, (sa_orm.MappedAsDataclass)): |
| 146 | + |
| 147 | + class TimestampModel(db.Model): |
| 148 | + __abstract__ = True |
| 149 | + created: sa_orm.Mapped[datetime] = sa_orm.mapped_column( |
| 150 | + db.DateTime, nullable=False, insert_default=datetime.utcnow, init=False |
| 151 | + ) |
| 152 | + updated: sa_orm.Mapped[datetime] = sa_orm.mapped_column( |
| 153 | + db.DateTime, |
| 154 | + insert_default=datetime.utcnow, |
| 155 | + onupdate=datetime.utcnow, |
| 156 | + init=False, |
| 157 | + ) |
| 158 | + |
| 159 | + class Post(TimestampModel): |
| 160 | + id: sa_orm.Mapped[int] = sa_orm.mapped_column( |
| 161 | + db.Integer, primary_key=True, init=False |
| 162 | + ) |
| 163 | + title: sa_orm.Mapped[str] = sa_orm.mapped_column(db.String, nullable=False) |
| 164 | + |
| 165 | + elif issubclass(db.Model, (sa_orm.DeclarativeBase, sa_orm.DeclarativeBaseNoMeta)): |
| 166 | + |
| 167 | + class TimestampModel(db.Model): # type: ignore[no-redef] |
| 168 | + __abstract__ = True |
| 169 | + created: sa_orm.Mapped[datetime] = sa_orm.mapped_column( |
| 170 | + db.DateTime, nullable=False, default=datetime.utcnow |
| 171 | + ) |
| 172 | + updated: sa_orm.Mapped[datetime] = sa_orm.mapped_column( |
| 173 | + db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow |
| 174 | + ) |
| 175 | + |
| 176 | + class Post(TimestampModel): # type: ignore[no-redef] |
| 177 | + id: sa_orm.Mapped[int] = sa_orm.mapped_column(db.Integer, primary_key=True) |
| 178 | + title: sa_orm.Mapped[str] = sa_orm.mapped_column(db.String, nullable=False) |
| 179 | + |
| 180 | + else: |
| 181 | + |
| 182 | + class TimestampModel(db.Model): # type: ignore[no-redef] |
| 183 | + __abstract__ = True |
| 184 | + created = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) |
| 185 | + updated = db.Column( |
| 186 | + db.DateTime, onupdate=datetime.utcnow, default=datetime.utcnow |
| 187 | + ) |
| 188 | + |
| 189 | + class Post(TimestampModel): # type: ignore[no-redef] |
| 190 | + id = db.Column(db.Integer, primary_key=True) |
| 191 | + title = db.Column(db.String, nullable=False) |
| 192 | + |
| 193 | + db.create_all() |
| 194 | + db.session.add(Post(title="Admin Post")) |
| 195 | + db.session.commit() |
| 196 | + post = db.session.execute(db.select(Post)).scalar() |
| 197 | + assert post is not None |
| 198 | + assert post.created is not None |
| 199 | + assert post.updated is not None |
| 200 | + |
| 201 | + |
| 202 | +@pytest.mark.usefixtures("app_ctx") |
| 203 | +def test_mixinmodel(app: Flask, model_class: t.Any) -> None: |
| 204 | + db = SQLAlchemy(app, model_class=model_class) |
| 205 | + |
| 206 | + if issubclass(db.Model, (sa_orm.MappedAsDataclass)): |
| 207 | + |
| 208 | + class TimestampMixin(sa_orm.MappedAsDataclass): |
| 209 | + created: sa_orm.Mapped[datetime] = sa_orm.mapped_column( |
| 210 | + db.DateTime, nullable=False, insert_default=datetime.utcnow, init=False |
| 211 | + ) |
| 212 | + updated: sa_orm.Mapped[datetime] = sa_orm.mapped_column( |
| 213 | + db.DateTime, |
| 214 | + insert_default=datetime.utcnow, |
| 215 | + onupdate=datetime.utcnow, |
| 216 | + init=False, |
| 217 | + ) |
| 218 | + |
| 219 | + class Post(TimestampMixin, db.Model): |
| 220 | + id: sa_orm.Mapped[int] = sa_orm.mapped_column( |
| 221 | + db.Integer, primary_key=True, init=False |
| 222 | + ) |
| 223 | + title: sa_orm.Mapped[str] = sa_orm.mapped_column(db.String, nullable=False) |
| 224 | + |
| 225 | + elif issubclass(db.Model, (sa_orm.DeclarativeBase, sa_orm.DeclarativeBaseNoMeta)): |
| 226 | + |
| 227 | + class TimestampMixin: # type: ignore[no-redef] |
| 228 | + created: sa_orm.Mapped[datetime] = sa_orm.mapped_column( |
| 229 | + db.DateTime, nullable=False, default=datetime.utcnow |
| 230 | + ) |
| 231 | + updated: sa_orm.Mapped[datetime] = sa_orm.mapped_column( |
| 232 | + db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow |
| 233 | + ) |
| 234 | + |
| 235 | + class Post(TimestampMixin, db.Model): # type: ignore[no-redef] |
| 236 | + id: sa_orm.Mapped[int] = sa_orm.mapped_column(db.Integer, primary_key=True) |
| 237 | + title: sa_orm.Mapped[str] = sa_orm.mapped_column(db.String, nullable=False) |
| 238 | + |
| 239 | + else: |
| 240 | + |
| 241 | + class TimestampMixin: # type: ignore[no-redef] |
| 242 | + created = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) |
| 243 | + updated = db.Column( |
| 244 | + db.DateTime, onupdate=datetime.utcnow, default=datetime.utcnow |
| 245 | + ) |
| 246 | + |
| 247 | + class Post(TimestampMixin, db.Model): # type: ignore[no-redef] |
| 248 | + id = db.Column(db.Integer, primary_key=True) |
| 249 | + title = db.Column(db.String, nullable=False) |
| 250 | + |
| 251 | + db.create_all() |
| 252 | + db.session.add(Post(title="Admin Post")) |
| 253 | + db.session.commit() |
| 254 | + post = db.session.execute(db.select(Post)).scalar() |
| 255 | + assert post is not None |
| 256 | + assert post.created is not None |
| 257 | + assert post.updated is not None |
| 258 | + |
| 259 | + |
83 | 260 | @pytest.mark.usefixtures("app_ctx")
|
84 | 261 | def test_model_repr(db: SQLAlchemy) -> None:
|
85 | 262 | class User(db.Model):
|
|
0 commit comments