From fb1a7b22d86ec70d6b83c82c34dd8d4c40b8aa79 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 15 Feb 2026 14:12:37 -0600 Subject: [PATCH 1/4] docs: add comment regarding association proxy for dict(obj) --- src/wuttjamaican/db/util.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wuttjamaican/db/util.py b/src/wuttjamaican/db/util.py index da4720f..27aef29 100644 --- a/src/wuttjamaican/db/util.py +++ b/src/wuttjamaican/db/util.py @@ -56,6 +56,7 @@ class ModelBase: # pylint: disable=empty-docstring def __iter__(self): # nb. we override this to allow for `dict(self)` + # nb. this does *not* include association proxy values state = sa.inspect(self) fields = [attr.key for attr in state.attrs] return iter([(field, getattr(self, field)) for field in fields]) From 5ec0a8e82d1bc85e2016f2feef039fee5e6645ac Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 16 Feb 2026 18:11:11 -0600 Subject: [PATCH 2/4] fix: add `get_value()` convenience function --- src/wuttjamaican/app.py | 17 +++++++++++++++++ src/wuttjamaican/db/util.py | 3 ++- src/wuttjamaican/util.py | 28 ++++++++++++++++++++++++++++ tests/test_app.py | 26 ++++++++++++++++++++++++++ tests/test_util.py | 29 +++++++++++++++++++++++++++++ 5 files changed, 102 insertions(+), 1 deletion(-) diff --git a/src/wuttjamaican/app.py b/src/wuttjamaican/app.py index d8483b4..6314f9d 100644 --- a/src/wuttjamaican/app.py +++ b/src/wuttjamaican/app.py @@ -38,6 +38,7 @@ from webhelpers2.html import HTML from wuttjamaican.util import ( get_timezone_by_name, + get_value, localtime, load_entry_points, load_object, @@ -374,6 +375,22 @@ class AppHandler: # pylint: disable=too-many-public-methods self.__dict__["enum"] = importlib.import_module(spec) return self.enum + def get_value(self, obj, key): + """ + Convenience wrapper around + :func:`wuttjamaican.util.get_value()`. + + :param obj: Arbitrary dict or object of any kind which would + have named attributes. + + :param key: Key/name of the field to get. + + :returns: Whatever value is found. Or maybe an + ``AttributeError`` is raised if the object does not have + the key/attr set. + """ + return get_value(obj, key) + def load_object(self, spec): """ Import and/or load and return the object designated by the diff --git a/src/wuttjamaican/db/util.py b/src/wuttjamaican/db/util.py index 27aef29..d01917d 100644 --- a/src/wuttjamaican/db/util.py +++ b/src/wuttjamaican/db/util.py @@ -56,7 +56,8 @@ class ModelBase: # pylint: disable=empty-docstring def __iter__(self): # nb. we override this to allow for `dict(self)` - # nb. this does *not* include association proxy values + # nb. this does *not* include association proxy values; + # see also wuttjamaican.util.get_value() state = sa.inspect(self) fields = [attr.key for attr in state.attrs] return iter([(field, getattr(self, field)) for field in fields]) diff --git a/src/wuttjamaican/util.py b/src/wuttjamaican/util.py index a129887..b1ef966 100644 --- a/src/wuttjamaican/util.py +++ b/src/wuttjamaican/util.py @@ -82,6 +82,34 @@ def get_class_hierarchy(klass, topfirst=True): return hierarchy +def get_value(obj, key): + """ + Convenience function to retrive a value by name from the given + object. This will first try to assume the object is a dict but + will fallback to using ``getattr()`` on it. + + :param obj: Arbitrary dict or object of any kind which would have + named attributes. + + :param key: Key/name of the field to get. + + :returns: Whatever value is found. Or maybe an ``AttributeError`` + is raised if the object does not have the key/attr set. + """ + # nb. we try dict access first, since wutta data model objects + # should all support that anyway, so it's 2 birds 1 stone. + try: + return obj[key] + + except (KeyError, TypeError): + # nb. key error means the object supports key lookup (i.e. is + # dict-like) but did not have that key set. which is actually + # an expected scenario for association proxy fields, but for + # those a getattr() should still work; see also + # wuttjamaican.db.util.ModelBase + return getattr(obj, key) + + def load_entry_points(group, ignore_errors=False): """ Load a set of ``setuptools``-style entry points. diff --git a/tests/test_app.py b/tests/test_app.py index 1070e28..78a8862 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -48,6 +48,32 @@ class TestAppHandler(FileTestCase): def test_get_enum(self): self.assertIs(self.app.get_enum(), wuttjamaican.enum) + def test_get_value(self): + + class Object: + def __init__(self, **kw): + self.__dict__.update(kw) + + class Dict(dict): + pass + + # dict object + obj = {"foo": "bar"} + self.assertEqual(self.app.get_value(obj, "foo"), "bar") + + # object w/ attrs + obj = Object(foo="bar") + self.assertEqual(self.app.get_value(obj, "foo"), "bar") + + # dict-like w/ attrs + obj = Dict({"foo": "bar"}) + obj.baz = "yyy" + self.assertEqual(self.app.get_value(obj, "baz"), "yyy") + + # missing attr + obj = Object(foo="bar") + self.assertRaises(AttributeError, self.app.get_value, obj, "baz") + def test_load_object(self): # just confirm the method works on a basic level; the diff --git a/tests/test_util.py b/tests/test_util.py index 31b0527..ddf220e 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -41,6 +41,35 @@ class TestGetClassHierarchy(TestCase): self.assertEqual(classes, [C, B, A]) +class TestGetValue(TestCase): + + def test_basic(self): + + class Object: + def __init__(self, **kw): + self.__dict__.update(kw) + + class Dict(dict): + pass + + # dict object + obj = {"foo": "bar"} + self.assertEqual(mod.get_value(obj, "foo"), "bar") + + # object w/ attrs + obj = Object(foo="bar") + self.assertEqual(mod.get_value(obj, "foo"), "bar") + + # dict-like w/ attrs + obj = Dict({"foo": "bar"}) + obj.baz = "yyy" + self.assertEqual(mod.get_value(obj, "baz"), "yyy") + + # missing attr + obj = Object(foo="bar") + self.assertRaises(AttributeError, mod.get_value, obj, "baz") + + class TestLoadEntryPoints(TestCase): def test_empty(self): From 589f279f04d43f3dd6e983eab9f9a50a22cc0ada Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 16 Feb 2026 18:39:09 -0600 Subject: [PATCH 3/4] fix: add assocation proxies for `User.first_name` and `User.last_name` for a rather dubious purpose though..so may need to undo this, we'll see --- src/wuttjamaican/db/model/auth.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/wuttjamaican/db/model/auth.py b/src/wuttjamaican/db/model/auth.py index 59fc900..9a5ab3b 100644 --- a/src/wuttjamaican/db/model/auth.py +++ b/src/wuttjamaican/db/model/auth.py @@ -44,7 +44,7 @@ from sqlalchemy import orm from sqlalchemy.ext.associationproxy import association_proxy from wuttjamaican.db.util import uuid_column, uuid_fk_column -from wuttjamaican.db.model.base import Base +from wuttjamaican.db.model.base import Base, Person from wuttjamaican.util import make_utc @@ -204,8 +204,6 @@ class User(Base): # pylint: disable=too-few-public-methods person_uuid = uuid_fk_column("person.uuid", nullable=True) person = orm.relationship( "Person", - # TODO: seems like this is not needed? - # uselist=False, back_populates="users", cascade_backrefs=False, doc=""" @@ -214,6 +212,21 @@ class User(Base): # pylint: disable=too-few-public-methods """, ) + # TODO: these may or may not be good ideas? i added them mostly + # for sake of testing association proxy behavior in wuttaweb, b/c + # i was lazy and didn't want to write proper fixtures. so if + # they are a problem then doing that should fix it.. + first_name = association_proxy( + "person", + "first_name", + creator=lambda n: Person(first_name=n, full_name=n), + ) + last_name = association_proxy( + "person", + "last_name", + creator=lambda n: Person(last_name=n, full_name=n), + ) + active = sa.Column( sa.Boolean(), nullable=False, From 69a83ea47f9f6fc7bbb30781a0938017c0364479 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 17 Feb 2026 14:11:21 -0600 Subject: [PATCH 4/4] =?UTF-8?q?bump:=20version=200.28.6=20=E2=86=92=200.28?= =?UTF-8?q?.7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 9 +++++++++ pyproject.toml | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d4a089..1897120 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to WuttJamaican will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.28.7 (2026-02-17) + +### Fix + +- add assocation proxies for `User.first_name` and `User.last_name` +- add `get_value()` convenience function +- comment out app_title in default wutta.conf for installer +- add `app.today()` convenience method + ## v0.28.6 (2026-01-04) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 1c3f165..1cd82a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "WuttJamaican" -version = "0.28.6" +version = "0.28.7" description = "Base package for Wutta Framework" readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]