|  | 
| 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