How to collect information about the class attributes while the class is being constructed?
In Java, what I am asking for is possible.
In Python, that doesn't seem to be the case. Correct me if I am wrong!
I am constructing a sqlalchemy object that is defined declaratively.
class Foo(BASE):
id = Column(Integer, primaryKey=True, nullable=False)
text = Column(String(100))
I would like to define the class like this:
class Foo(declarative_base()):
@persisted_property(id=true)
id = Column(Integer, primaryKey=True, nullable=False)
@persisted_property(mutable=True)
text = Column(String(100))
@persisted_property(set_once=True)
created_by = Column(Integer)
@classmethod
def builder(cls, existing=None):
return Builder(cls, existing)
The persisted_property
class/function/? purpose is to collect the class attributes.
With that knowledge, these things would happen:
a builder()
classmethod would be added to the class Foo
that returns a generated FooBuilder. The FooBuilder
would have these methods: set_text()->FooBuilder
, set_created_by()->FooBuilder
, build()->Foo
(ideally) any attempt to directly alter a Foo
object would be blocked. (how to let sqlalchemy work?)
Example behavior:
Foo.builder().set_text("Foo text").set_created_by(1).build()
Foo.builder(existing_foo).set_text("Foo text").set_created_by(1).build()
: would raise Exception since existing_foo
already has a value for created_by
Notes:
Alternatives? suggestions?
The @callable
decorator syntax is indeed exclusive to def
function and class
class statements. However, it is just syntactic sugar.
The syntax
@name(arguments)
def functionname(...):
# ...
is translated to:
def functionname(...):
# ...
functionname = name(arguments)(functionname)
that is, the callable object produced by @[decorator]
is called and the result is assigned back to the function name (or class name, if applied to a class
statement).
You can always call the decorator directly, and assign the return value:
id = persisted_property(id=true)(Column(Integer, primaryKey=True, nullable=False))
However, decorators do not have access to the namespace in which the objects are constructed! The body of a class
statement is executed as if it were a function (albeit with different scoping rules) and the resulting local namespace is taken to produce the class attributes. A decorator is just another function call in this context, and the local namespace of the class body is not meant to be available.
Next, I'd not even begin to build your builder pattern. That's a Java pattern where class privacy and immutability are enforced to the detriment of dynamic language patterns. Python is not Java, don't try to turn it into Java. For example, you can't really make instances of Python classes immutable, that's simply not something a dynamic language lets you do. Moreover, the builder pattern is a solution to a problem that doesn’t really exist in Python, where you can build up your arguments to construct a class up front in, say, a dictionary you then apply dynamically to a class call, whereas Java has no such dynamic call support.
And you don't need to use a decorator pattern to mark your schema attributes anyway. You should instead rely on SQLAlchemy's own introspection support:
from sqlalchemy.inspection import inspect
class Builder:
def __init__(self, cls, existing=None, **attrs):
self.cls = cls
if existing is not None:
assert isinstance(existing, cls)
existing_attrs = {n: s.value for n, s in inspect(existing).attrs.items()}
# keyword arguments override existing attribute values
attrs = {**existing_attrs, **attrs}
self.attrs = attrs
def _create_attr_setter(self, attrname):
# create a bound attribute setter for the given attribute name
def attr_setter(self, value):
if attrname in self.attrs:
raise ValueError(f"{attrname} already has a value set")
return type(self)(self.cls, **self.attrs, **{attrname: value})
attr_setter.__name__ = f'set_{attrname}'
return attr_setter.__get__(self, type(self))
def __getattr__(self, name):
if name.startswith('set_'):
attrname = name[4:]
mapper = inspect(self.cls)
# valid SQLAlchemy descriptor name on the class?
if attrname in mapper.attrs:
return self._create_attr_setter(attrname)
raise AttributeError(name)
def build(self):
return self.cls(**self.attrs)
class BuilderMixin:
@classmethod
def builder(cls, existing=None):
return Builder(cls, existing)
and then just use BuilderMixin
as a mixin class:
>>> from sqlalchemy.ext.declarative import declarative_base
>>> from sqlalchemy import Column, Integer, String
>>> Base = declarative_base()
>>> class Foo(Base, BuilderMixin):
... __tablename__ = 'foo'
... id = Column(Integer, primary_key=True, nullable=False)
... text = Column(String(100))
... created_by = Column(Integer)
...
>>> Foo.builder().set_text('Demonstration text').set_created_by(1).build()
<__main__.Foo object at 0x10f8314a8>
>>> _.text, _.created_by
('Demonstration text', 1)
You can attach additional information to columns in the info
dictionary:
text = Column(String(100), info={'mutable': True})
which your builder code could then access via the mapper (e.g. mapper.attrs['text'].info.get('mutable', False)
).
But again, rather than recreate the Java builder pattern, just construct the attrs
dictionary directly, and at most encode mutability rules by using a hybrid property or with ORM events.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With