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'))
setattr(self.__class__, 'current_' + cache_name, property(cache.__class__.current.fget(cache))), but that doesn't act like a property. \$\endgroup\$