4
\$\begingroup\$

I want to have a object where I can reference another object's properties dynamically while they are still considered properties. If I try to do a simple setattr I'll only get the current state when I read it and it won't act like I'm getting the property.

I can't setattr with property(<target_obj>.__class__.<property>.fget), because I can't overwrite their instances of self with the specific target object I want (as far as I know). Is there a simpler way to do this?

I can successfully attach the property, but then I have to make the target object's class singleton or borg (otherwise other instances will attach their objects to my class). In my specific use case I'm fine to accept making the class receiving the property static.

def build_lazy_linker_property(link_obj, property_name, mark_docs=True):
    """
    Detach a property from it's object to be used as another classes property.
    Note: build_lazy_linker_property is only singleton/borg safe

    Example:
    - attach property 'current' from class <cache_name> to as 'current_<cache_name>':
    setattr(self.__class__, 'current_' + cache_name, build_lazy_linker_property(cache, 'current'))

    :param link_obj: Object to link from
    :param property_name: Property name to connect from the object to the parent class
    :param mark_docs: Boolean: whether to write that this is linked in the docs
    :return: property which forwards to the other property
    """
    # We couldn't use this in the f methods or it won't lazily evaluate
    # prop = getattr(link_obj, property_name)

    def fget(self):
        return getattr(link_obj, property_name)

    def fset(self, value):
        property_ = getattr(link_obj.__class__, property_name)
        property_.__set__(link_obj, value)
        # setattr(property_, 'fset', value)

    def fdel(self):
        property_ = getattr(link_obj.__class__, property_name)
        property_.__delete__(link_obj)

    cls_prop = getattr(link_obj.__class__, property_name)

    # Find descriptors and add as property inputs
    property_inputs = {}
    desc_map = {cls_prop.fget: ('fget', fget), cls_prop.fset: ('fset', fset), cls_prop.fdel: ('fdel', fdel)}
    for test, fnc in desc_map.items():
        # If the property has this descriptor link it
        if test:
            # add descriptor as kwarg
            property_inputs[fnc[0]] = fnc[1]

    # Handle docs
    doc = ""
    # Put that this is a linked property at the top of the docstring
    if mark_docs:
        doc += "Property linked to '{}.{}'.\n".format(link_obj, property_name)
        property_inputs['doc'] = doc
    # Put the rest of the docs if they exist
    if hasattr(cls_prop, 'doc'):
        doc += cls_prop['doc']
        property_inputs['doc'] = doc

    return property(**property_inputs)


def attach_lazy_link(target_cls, prop_obj, property_name):
    """Attaches property with the same name from prop_obj to target_cls"""
    setattr(target_cls, property_name, build_lazy_linker_property(prop_obj, property_name))

My specific use case is difficult to explain so let's look at a test to show how this works as-is first.

def test_build_lazy_linker_property():
    class Prop(object):
        def __init__(self, prop):
            self._prop = prop

        @property
        def prop(self):
            return self._prop

        @prop.setter
        def prop(self, value):
            self._prop = value

    class Target(object):
        def __init__(self, prop_val):
            self.p = Prop(prop_val)

        def elevate_prop(self):
            attach_lazy_link(self.__class__, self.p, 'prop')

        def test_that_props_elevate(self):
            self.elevate_prop()
            assert self.prop == self.p.prop  # noqa this will be unreferenceable until elevate_prop is called
            self.prop = 10
            assert self.prop == 10

    Target(1).test_that_props_elevate()
    # Another instance will have the same value set in test_that_props_elevate despite creating a different Prop obj
    assert Target(2).prop == value

Here you can see after attach_lazy_link an the prop property from the instance made in Target's init is attached to the Target class. Now any Target instance can use that property from the Prop instance.

In my use case I have caches of connection types for various machines. We have different libraries for different products and hosts. Then based on what is being added (either through factory methods or Mixins) we need to have access to a lot of properties which handle the connections. I specifically want the current/active connection from each cache connected to a property called current_$MACHINE_TYPE.

    for cache_name in cache_list:
        # make simple properties
        cache = getattr(self, cache_name + '_cache')
        # This is the relevant part!
        setattr(self.__class__, 'current_' + cache_name, build_lazy_linker_property(cache, 'current'))
\$\endgroup\$
  • 2
    \$\begingroup\$ Can you include an example of how you'd use these. Also have you tried any of the standard customizing attribute access methods? \$\endgroup\$ – Peilonrayz Jul 24 '17 at 8:44
  • \$\begingroup\$ @Peilonrayz I've added an example case. If I tried to use a normal attribute access method it would evaluate immediately and stay that value instead of updating when the property get changes. My first implementation for a get only property was: setattr(self.__class__, 'current_' + cache_name, property(cache.__class__.current.fget(cache))), but that doesn't act like a property. \$\endgroup\$ – Brian Jul 24 '17 at 9:05
  • 1
    \$\begingroup\$ Thank you for adding more code. However, can you also provide the class that executes the bottom code snippet. It should allow for better answers. Also if you could make the code snippet stand alone (as in C&P + F5 and it runs) then you're more likely to get an answer. \$\endgroup\$ – Peilonrayz Jul 24 '17 at 9:09
  • \$\begingroup\$ @Peilonrayz My specific use case would probably over complicate the question as it's hard to isolate the workload as this is intended to help usability in a class that is managing several modules. I wrote a runnable unit test that shows how it works. I'd be happy to clarify if needed. \$\endgroup\$ – Brian Jul 24 '17 at 9:42
  • \$\begingroup\$ Please do not update the code in your question to incorporate feedback from answers, doing so goes against the Question + Answer style of Code Review. If you have more code that you want to have reviewed, please see what you may and may not do after receiving answers. \$\endgroup\$ – Peilonrayz Jul 24 '17 at 20:44
3
\$\begingroup\$

I'm not a fan of forcing singletons. To avoid these I can see two ways you can go.

  1. Stick with using a property builder.

    To work with this, you'd need to make an insolated Type class for each instance of a Type. So type(Type()) is not Type, however it should isinstance(Type(), Type).

    I think the simplest way to do this would be via a metaclass that implicitly does this on subclass instantiation.

    Pros:

    • Can work on any property.

    Cons:

    • You have to normalize the property.
    • You need to hack the environment around the wrapped property. (If you mess this up, it may use another class as self)
    • Kinda hacky on the whole.
  2. Use the standard ways to customize attribute access.

    If you want your classes to be singletons, which I'd recommend you don't, then you can use a Singleton metaclass to add this cleanly. This requires a hard to read base class. But otherwise allows you to bind an object to this object and extract items from it.

    Pros:

    • Well known interface.
    • Requires one class.

    Cons:

    • Doesn't work with more than one object.

      This looked like your use-case from your unit-test, but I'm not sure that's what you want.


As to your code.

  • I'm not a fan of your comments, they don't help too much.
  • I am not a fan of property_inputs, desc_map, etc.
  • You don't allow this to get property's that don't use the propertys wrapper.
  • I don't think the docs code work as intended, the property never has a doc property. To get the doc read __doc__.

Instead I'd use:

from functools import partial, wraps

def build_property(obj, name, mark_docs=True):
    prop = getattr(type(obj), name, None)
    if isinstance(prop, property):
        fget = prop.fget,
        fset = prop.fset,
        fdel = prop.fdel,
        doc = prop.__doc__
    else:
        fget = getattr, name
        fset = setattr, name
        fdel = delattr, name
        doc = None

    def make_call(fn, *org_args):
        if fn is None:
            return
        f = partial(fn, obj, *org_args)
        @wraps(fn)
        def inner(_, *args):
            return f(*args)
        return inner

    prop = property(*(make_call(*fn) for fn in (fget, fset, fdel)), doc)

    if mark_docs:
        text = (
            "Property linked to '{}.{}'.".format(obj, name),
            prop.__doc__
        )
        prop.__doc__ = '\n'.join(t for t in text if t)
    return prop

You can then use the following to get the first way you could do this. This passes your unit test, except Target(2).prop != Target(1).prop.

class Classington(type):
    def __call__(cls, *args, **kwargs):
        cls = type(cls.__name__, (cls,), {})
        return type.__call__(cls, *args, **kwargs)


class PropertyHolder(metaclass=Classington):
    def add_property(self, obj, name, mark_docs=True):
        setattr(type(self), name, build_property(obj, name, mark_docs))


class Target(PropertyHolder):
    def __init__(self, prop):
        self.p = Prop(prop)
        self.add_property(self.p, 'prop')

However, the second method uses _object rather than p to hold the object. The below achieves the same as your code, however, if you remove the Singleton metaclass from Target, then it, just like the above code, work as Target(2).prop != Target(1).prop.

class TransparentProxy:
    def __init__(self, obj, props=None):
        object.__setattr__(self, '_object', obj)
        object.__setattr__(self, '_props', set(props or dir(obj)))

    def __getattribute__(self, name):
        _props = object.__getattribute__(self, '_props')
        if name in _props:
            _object = object.__getattribute__(self, '_object')
            return getattr(_object, name)
        else:
            return object.__getattribute__(self, name)

    def __setattr__(self, name, value):
        _props = object.__getattribute__(self, '_props')
        if name in _props:
            _object = object.__getattribute__(self, '_object')
            return setattr(_object, name, value)
        else:
            return object.__setattr__(self, name, value)

    def __delattr__(self, name):
        _props = object.__getattribute__(self, '_props')
        if name in _props:
            _object = object.__getattribute__(self, '_object')
            return delattr(_object, name)
        else:
            return object.__delattr__(self, name)


class Singleton(type):
    _instances = {}
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
        else:
            cls._instances[cls].__init__(*args, **kwargs)
        return cls._instances[cls]


class Target(TransparentProxy, metaclass=Singleton):
    def __init__(self, prop):
        super().__init__(Prop(prop), ['prop'])
\$\endgroup\$
  • \$\begingroup\$ Thanks, the build_property is cleaner and that is a huge improvement! \$\endgroup\$ – Brian Jul 24 '17 at 20:13
  • \$\begingroup\$ I'm a little confused on the inheritance. In my use case the objects aren't subclasses of each other. The Target class is managing many PropertyHolder classes. In my specific use case only the Target class had to be a singleton. None of the PropertyHolder classes needed to be singletons. I don't even want that class to be a singleton, but I was unsure how to resolve that. Is there a way to have Target(2).prop != Target(1).prop. WIthout having that inheritance. \$\endgroup\$ – Brian Jul 24 '17 at 20:19
  • \$\begingroup\$ I updated my use case and will add more when I try to implement one that won't force TesterLib to be a singleton. \$\endgroup\$ – Brian Jul 24 '17 at 20:27
  • \$\begingroup\$ @Brian Please read through my answer again. The methods above allow Target to a singleton or a not via the metaclasses. \$\endgroup\$ – Peilonrayz Jul 24 '17 at 20:31
0
\$\begingroup\$

@Peilonrayz provided the correct answer, but I lost the ability to rename the property while grabbing it. To solve my issue because I'm iterating through a list of classes with same name properties and trying to make them accessible as properties in the Target class I had to add a new_name param to add_property.

For the specific case that attach_lazy_link covered his answer is correct, for my larger use case PropertyHolder needs to be changed. I copied from his code just so that it's all in one place. All credit to @Peilonrayz!

class Classington(type):
    def __call__(cls, *args, **kwargs):
        cls = type(cls.__name__, (cls,), {})
        return type.__call__(cls, *args, **kwargs)


class PropertyHolder(metaclass=Classington):
    def add_property(self, obj, new_name, name, mark_docs=True):
        setattr(type(self), new_name, build_property(obj, name, mark_docs))


class TesterLib(PropertyHolder):
    def __init__(self):
        self.appliance_cache = ApplianceCache()
        self.host_cache = HostCache()

        self._make_cache_helpers(['appliance', 'host'])

    def _make_cache_helpers(self, cache_list):
        cache = getattr(self, cache_name + '_cache')
        self.add_property(cache, 'current_' + cache_name, 'current')
        # add more properties and methods using those names.

The result of this simplistic version would be that the ApplianceCache instance's current property would be accessible in an instance of TesterLib with current_appliance and HostCache's current property would be accessible with current_host.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.