Setting key of hybrid_property to attribute name rather than function name

364 views
Skip to first unread message

Michael Williamson

unread,
Nov 11, 2016, 7:20:58 AM11/11/16
to sqlalchemy
I'm using hybrid_property, and would like the key of the property to be set to the attribute name, rather than the name of the getter. This is because I'm generating a getter function based on some args, rather than having the caller directly defining the getter. As a minimal example:

from __future__ import unicode_literals

from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import Session


def prefixed_property(other_column, prefix):
    def instance_get(self):
        return prefix + getattr(self, other_column.key)

    return hybrid_property(fget=instance_get)


Base = declarative_base()

class Person(Base):
    __tablename__ = "person"

    id = Column(Integer, primary_key=True)
    name = Column(String)
    greeting = prefixed_property(name, "Hello ")


engine = create_engine("sqlite:///:memory:")

Base.metadata.create_all(engine)
session = Session(engine)

session.add(Person(name="Bob"))
session.commit()

assert session.query(Person.greeting).one() == ("Hello Bob", )
assert session.query(Person.greeting).one().greeting == "Hello Bob" # Fails with AttributeError since hybrid_property uses instance_get as the name


Is there a sensible way to do this? Or am I going about the problem the wrong way? In practice, I'm using this to define properties backed by a JSON field. In case it makes any different, here's the actual use-case I have:

from __future__ import unicode_literals

import os

from sqlalchemy import create_engine, Column, Integer
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.event import listens_for
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import Session
from sqlalchemy.pool import StaticPool


def json_property(json_column, name, type_=None):
    def instance_get(self):
        return getattr(self, json_column.key)[name]

    def instance_set(self, value):
        json_obj = getattr(self, json_column.key)
        if json_obj is None:
            setattr(self, json_column.key, {})
        getattr(self, json_column.key)[name] = value

    def cls_get(cls):
        expression = json_column[name]
        if type_:
            return expression.astext.cast(type_)
        else:
            return expression

    return hybrid_property(fget=instance_get, expr=cls_get, fset=instance_set)


Base = declarative_base()

class Person(Base):
    __tablename__ = "person"

    id = Column(Integer, primary_key=True)
    data = Column(JSONB, nullable=False)
    born = json_property(data, "born", Integer())


engine = create_engine(os.environ["TEST_DATABASE"], poolclass=StaticPool)
engine.execute("SET search_path TO pg_temp")

@listens_for(engine, "engine_connect")
def set_search_path(connection, branch):
    connection.execute("SET search_path TO pg_temp")


Base.metadata.create_all(engine)
session = Session(engine)

session.add(Person(born=1881))
session.commit()

assert session.query(Person.born).one() == (1881, ) # Works fine
assert session.query(Person.born).one().born == 1881 # Raises AttributeError since hybrid_property uses instance_get as the name

Simon King

unread,
Nov 11, 2016, 8:36:08 AM11/11/16
to sqlal...@googlegroups.com
I think your code is basically fine, you've just got a mistake on the
last line. Presumably you meant to query Person, not Person.born?

assert session.query(Person).one().born == 1881

hybrid_property does't care about the name of the "fget" function, it
just calls it, passing the instance as the only parameter:

https://bitbucket.org/zzzeek/sqlalchemy/src/f2eb4aac9517a3775411c2ecf0f588ffd0d790f6/lib/sqlalchemy/ext/hybrid.py?at=master&fileviewer=file-view-default#hybrid.py-744

Hope that helps,

Simon

Michael Williamson

unread,
Nov 11, 2016, 8:53:24 AM11/11/16
to sqlalchemy

I think your code is basically fine, you've just got a mistake on the
last line. Presumably you meant to query Person, not Person.born?

I want Person.born so that I don't have to get the entire object. It doesn't make much difference in this example, but is quite important for us in practice when we're grabbing multiple expressions.
 
hybrid_property does't care about the name of the "fget" function, it
just calls it, passing the instance as the only parameter:

https://bitbucket.org/zzzeek/sqlalchemy/src/f2eb4aac9517a3775411c2ecf0f588ffd0d790f6/lib/sqlalchemy/ext/hybrid.py?at=master&fileviewer=file-view-default#hybrid.py-744


The call to util.update_wrapper sets the property to have the same name as the fget function, which in turn is used when creating the SQL expression (the self.__name__ expression in the comparator method), which determines the label of the value in the result object.

Simon King

unread,
Nov 11, 2016, 9:22:28 AM11/11/16
to sqlal...@googlegroups.com
OK, I'm probably missing something. I don't have access to PG right
now, so I couldn't run your code directly. Having said that, I'm
surprised that the comparator object is even invoked in the
expression:

session.query(Person.born).one().born == '1881'

...because "session.query(Person.born).one()" returns a tuple-ish
object with 1 element, which won't have a "born" attribute.

Anyway, to answer your original question, would it be sufficient to
update the __name__ attribute of your instance_get function inside
json_property?

ie.

def json_property(json_column, name, type_=None):
def instance_get(self):
return getattr(self, json_column.key)[name]
instance_get.__name__ = name

Simon

Michael Williamson

unread,
Nov 11, 2016, 11:32:13 AM11/11/16
to sqlalchemy


On Friday, November 11, 2016 at 2:22:28 PM UTC, Simon King wrote:
On Fri, Nov 11, 2016 at 1:53 PM, Michael Williamson <mic...@healx.io> wrote:
>
>> I think your code is basically fine, you've just got a mistake on the
>> last line. Presumably you meant to query Person, not Person.born?
>
>
> I want Person.born so that I don't have to get the entire object. It doesn't
> make much difference in this example, but is quite important for us in
> practice when we're grabbing multiple expressions.
>
>>
>> hybrid_property does't care about the name of the "fget" function, it
>> just calls it, passing the instance as the only parameter:
>>
>>
>> https://bitbucket.org/zzzeek/sqlalchemy/src/f2eb4aac9517a3775411c2ecf0f588ffd0d790f6/lib/sqlalchemy/ext/hybrid.py?at=master&fileviewer=file-view-default#hybrid.py-744
>>
>
> The call to util.update_wrapper sets the property to have the same name as
> the fget function, which in turn is used when creating the SQL expression
> (the self.__name__ expression in the comparator method), which determines
> the label of the value in the result object.
>

OK, I'm probably missing something. I don't have access to PG right
now, so I couldn't run your code directly. Having said that, I'm
surprised that the comparator object is even invoked in the
expression:

    session.query(Person.born).one().born == '1881'

...because "session.query(Person.born).one()" returns a tuple-ish
object with 1 element, which won't have a "born" attribute.

It's a namedtuple-ish object -- it behaves as a tuple, but you can also get values out by name. The comparator is invoked during construction of the hybrid property. Specifically, the constructor calls self.expression, which then calls self.comparator.
 
Anyway, to answer your original question, would it be sufficient to
update the __name__ attribute of your instance_get function inside
json_property?

ie.

def json_property(json_column, name, type_=None):
    def instance_get(self):
        return getattr(self, json_column.key)[name]
    instance_get.__name__ = name

Unfortunately not: the JSON property may not have the same name as the attribute e.g.

year_born = json_property(data, "yearBorn", Integer())
 
Simon

mike bayer

unread,
Nov 11, 2016, 11:59:58 AM11/11/16
to sqlal...@googlegroups.com
this example fails because the object returned is not a Person instance,
it is the raw value of the column returned by your hybrid which in this
case is anonymously named, as the SQL expression is "x + y". You need
to give it the label of your choice for it to be present in the row in a
particular way. There is some logic these days inside of expression()
that seems to get in the way, so just subclass or use a plain descriptor:

class prefixed_property(hybrid_property):
def __init__(self, other_column, prefix, labelname):
self.other_column = other_column
self.prefix = prefix
self.labelname = labelname

def __get__(self, instance, owner):
if instance is None:
retval = self.prefix + getattr(owner, self.other_column.key)
return retval.label(self.labelname)
else:
return self.prefix + getattr(instance, self.other_column.key)


class Person(Base):
__tablename__ = "person"

id = Column(Integer, primary_key=True)
name = Column(String)
greeting = prefixed_property(name, "Hello ", "greeting")
> --
> SQLAlchemy -
> The Python SQL Toolkit and Object Relational Mapper
>
> http://www.sqlalchemy.org/
>
> To post example code, please provide an MCVE: Minimal, Complete, and
> Verifiable Example. See http://stackoverflow.com/help/mcve for a full
> description.
> ---
> You received this message because you are subscribed to the Google
> Groups "sqlalchemy" group.
> To unsubscribe from this group and stop receiving emails from it, send
> an email to sqlalchemy+...@googlegroups.com
> <mailto:sqlalchemy+...@googlegroups.com>.
> To post to this group, send email to sqlal...@googlegroups.com
> <mailto:sqlal...@googlegroups.com>.
> Visit this group at https://groups.google.com/group/sqlalchemy.
> For more options, visit https://groups.google.com/d/optout.

Michael Williamson

unread,
Nov 11, 2016, 12:18:20 PM11/11/16
to mike bayer, sqlal...@googlegroups.com
That still requires the repetition of the name of the attribute, which
I'd rather avoid. I've put together a variation on hybrid_property
which automatically assigns the label by scanning through the class
dict. It could probably do with a bit of memoization and and proper
handling of subtyping (i.e. checking __dict__ of supertypes), but does
this seem sane to you? Is there a neater way?

class hybrid_property2(hybrid_property):
def __get__(self, instance, owner):
if instance is None:
expression = self.expr(owner)
for key, value in owner.__dict__.items():
if value == self:
return expression.label(key)

return expression
else:
return self.fget(instance)

Thanks for the suggestions so far, Mike and Simon.

mike bayer

unread,
Nov 11, 2016, 1:27:15 PM11/11/16
to Michael Williamson, sqlal...@googlegroups.com


On 11/11/2016 12:18 PM, Michael Williamson wrote:
>
> That still requires the repetition of the name of the attribute, which
> I'd rather avoid. I've put together a variation on hybrid_property
> which automatically assigns the label by scanning through the class
> dict. It could probably do with a bit of memoization and and proper
> handling of subtyping (i.e. checking __dict__ of supertypes), but does
> this seem sane to you? Is there a neater way?
>
> class hybrid_property2(hybrid_property):
> def __get__(self, instance, owner):
> if instance is None:
> expression = self.expr(owner)
> for key, value in owner.__dict__.items():
> if value == self:
> return expression.label(key)
>
> return expression
> else:
> return self.fget(instance)
>
> Thanks for the suggestions so far, Mike and Simon.

Getting the name of the attribute you're set on is not something I know
that there's any way to do without the kind of thing you're doing.
Decorators usually work out because fn.__name__ is there to give us that
info.

Michael Williamson

unread,
Nov 14, 2016, 6:06:12 AM11/14/16
to mike bayer, sqlal...@googlegroups.com
Thanks Mike, in the end I defined my own metatype that inherits from
DeclarativeMeta, and set __name__ on each property in __new__.
Reply all
Reply to author
Forward
0 new messages