From 4a6897c6dee57eb5a04b640b033b4911128ea143 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 25 Jan 2025 16:26:14 -0600 Subject: [PATCH] fix: add `make_proxy()` convenience method for data model Base --- src/wuttjamaican/db/model/base.py | 88 ++++++++++++++++++++++++++++++- tests/db/model/test_base.py | 23 ++++++++ 2 files changed, 109 insertions(+), 2 deletions(-) diff --git a/src/wuttjamaican/db/model/base.py b/src/wuttjamaican/db/model/base.py index 24c850b..8086cd0 100644 --- a/src/wuttjamaican/db/model/base.py +++ b/src/wuttjamaican/db/model/base.py @@ -25,19 +25,103 @@ Base Models .. class:: Base - This is the base class for all data models. + This is the base class for all :term:`data models ` in + the :term:`app database`. You should inherit from this class when + defining custom models. + + This class inherits from :class:`WuttaModelBase`. """ import sqlalchemy as sa from sqlalchemy import orm +from sqlalchemy.ext.associationproxy import association_proxy from wuttjamaican.db.util import (naming_convention, ModelBase, uuid_column, uuid_fk_column) +class WuttaModelBase(ModelBase): + """ + Base class for data models, from which :class:`Base` inherits. + + Custom models should inherit from :class:`Base` instead of this + class. + """ + + @classmethod + def make_proxy(cls, main_class, extension, name, proxy_name=None): + """ + Convenience method to declare an "association proxy" for the + main class, per the params. + + For more info see + :doc:`sqlalchemy:orm/extensions/associationproxy`. + + :param main_class: Reference to the "parent" model class, upon + which the proxy will be defined. + + :param extension: Attribute name on the main class, which + references the extension record. + + :param name: Attribute name on the extension class, which + provides the proxied value. + + :param proxy_name: Optional attribute name on the main class, + which will reference the proxy. If not specified, ``name`` + will be used. + + As a simple example consider this model, which extends the + :class:`~wuttjamaican.db.model.auth.User` class. In + particular note the last line which is what we're documenting + here:: + + import sqlalchemy as sa + from sqlalchemy import orm + from wuttjamaican.db import model + + class PoserUser(model.Base): + \""" Poser extension for User \""" + __tablename__ = 'poser_user' + + uuid = model.uuid_column(sa.ForeignKey('user.uuid'), default=None) + user = orm.relationship( + model.User, + doc="Reference to the main User record.", + backref=orm.backref( + '_poser', + uselist=False, + cascade='all, delete-orphan', + doc="Reference to the Poser extension record.")) + + favorite_color = sa.Column(sa.String(length=100), nullable=False, doc=\""" + User's favorite color. + \""") + + def __str__(self): + return str(self.user) + + # nb. this is the method call + PoserUser.make_proxy(model.User, '_poser', 'favorite_color') + + That code defines a ``PoserUser`` model but also defines a + ``favorite_color`` attribute on the main ``User`` class, such + that it can be used normally:: + + user = model.User(username='barney', favorite_color='green') + session.add(user) + + user = session.query(model.User).filter_by(username='bambam').one() + print(user.favorite_color) + """ + proxy = association_proxy( + extension, proxy_name or name, + creator=lambda value: cls(**{name: value})) + setattr(main_class, name, proxy) + + metadata = sa.MetaData(naming_convention=naming_convention) -Base = orm.declarative_base(metadata=metadata, cls=ModelBase) +Base = orm.declarative_base(metadata=metadata, cls=WuttaModelBase) class Setting(Base): diff --git a/tests/db/model/test_base.py b/tests/db/model/test_base.py index 50e95a5..d7e495a 100644 --- a/tests/db/model/test_base.py +++ b/tests/db/model/test_base.py @@ -3,12 +3,34 @@ from unittest import TestCase try: + import sqlalchemy as sa + from sqlalchemy import orm from wuttjamaican.db.model import base as mod from wuttjamaican.db.model.auth import User except ImportError: pass else: + + class MockUser(mod.Base): + __tablename__ = 'mock_user' + uuid = mod.uuid_column(sa.ForeignKey('user.uuid'), default=False) + user = orm.relationship( + User, + backref=orm.backref('_mock', uselist=False, cascade='all, delete-orphan')) + favorite_color = sa.Column(sa.String(length=100), nullable=False) + + + class TestWuttaModelBase(TestCase): + + def test_make_proxy(self): + self.assertFalse(hasattr(User, 'favorite_color')) + MockUser.make_proxy(User, '_mock', 'favorite_color') + self.assertTrue(hasattr(User, 'favorite_color')) + user = User(favorite_color='green') + self.assertEqual(user.favorite_color, 'green') + + class TestSetting(TestCase): def test_basic(self): @@ -17,6 +39,7 @@ else: setting.name = 'foo' self.assertEqual(str(setting), "foo") + class TestPerson(TestCase): def test_basic(self):