Hybrid decorators' "mutator decorators" actually mutate state, preventing specialising in a subclass

Issue #3912 resolved
Markus Meskanen created an issue

As it stands, hybrid_method and hybrid_property both mutate their state with the modifier decorators (setter, deleter, expression, comparator) in the following manner:

class hybrid_property:
    ...

    def setter(self, fset):
        self.fset = fset
        return self

But this prevents me from specialising the property in a subclass:

class Foo(Model):
    first_name = Column(String)
    last_name = Column(String)

    @hybrid_property
    def name(self):
        return self.first_name + ' ' + self.last_name

    @name.setter
    def name(self, value):
        self.first_name, self.last_name = value.split(' ', maxsplit=1)


class Bar(Foo):

    @Foo.name.setter
    def name(self, value):
        if run_name_check(value) > 0:  # Error code received
            raise InvalidName
        super().name.fset(value)

Since that would result into modifying the original Foo.name:

>>> f = Foo(first_name='Markus', last_name='Meskanen')
>>> f.name = 'Markus^% Meskanen'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
__main__.InvalidName

The correct way to implement these mutator decorators is to have them return a copy instance instead:

class hybrid_property:
    ...
    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.expr)

I would PR such fix myself, but I'm in a hurry and couldn't quicky figure out how the expressions and comparators work exactly, and wasn't sure what is the intended behavior after this suggested fix would be applied.

Comments (7)

  1. Markus Meskanen reporter

    For what it's worth, this is how Python's built-in property works too; it always returns a new instance:

    >>> class Foo:
    ...
    ...   @property
    ...   def x(self):
    ...     pass
    ...
    ...   _x = x  # Copy a reference of the current property
    ...
    ...   @x.setter
    ...   def x(self, value):
    ...     pass
    ...
    ...   print(_x is x)
    ...   print(_x, x)
    ... 
    False
    <property object at 0x7fa243d599a8> <property object at 0x7fa243d59958>
    
  2. Mike Bayer repo owner

    I'm putting this as "enhancement" since I hadn't realized this use case originally, else I would have made it copy from the start. This would be bundled with #3911 and noted in changes to hybrids in the migration document. targeting for 1.2 but this may move out unless I can get contributions including tests.

  3. Markus Meskanen reporter

    Yeah I guess it's a bit of both, but fair enough.

    I've never contributed to SQLAlchemy before but would love to help, any ETA when 1.2 will be out? Got a bit busy schedule and will have to investigate the project further before contributing too much.

  4. Mike Bayer repo owner

    1.2 is a "big" release and I haven't really started organizing all the patches, it's at best for spring 2017 and probably later. for now I'd consider vendoring your own hybrid decorator as this is a simple extension.

  5. Mike Bayer repo owner

    Allow reuse of hybrid_property across subclasses

    The :class:sqlalchemy.ext.hybrid.hybrid_property class now supports calling mutators like @setter, @expression etc. multiple times across subclasses, and now provides a @getter mutator, so that a particular hybrid can be repurposed across subclasses or other classes. This now matches the behavior of @property in standard Python.

    Co-authored-by: Mike Bayer mike_mp@zzzcomputing.com Fixes: #3911 Fixes: #3912 Change-Id: Iff033d8ccaae20ded9289cbfa789c376759381f5

    → <<cset caeb274e287f>>

  6. Log in to comment