I have a class that mostly provides properties for interfacing data stored in a dictionary. Each property is responsible for (usually) one key in the dictionary, but there may be additional keys in the dictionary, that are not managed by properties.
Edit 3: To clarify, as there has been some confusion in the comments:
I have an arbitrary set of keys, for which I have a fixed subset of keys for which getters/setters are implemented, but the getters/setters are NOT identical in any meaningful way. I want a way to know for which keys I have defined such a getter/setter without having to maintain a separate list. It is more maintainable to just "tag" the getter/setter/property with the key it is responsible for and generate a list of managed keys from the tags. Also I hoped that I could then have a variable name in the getter/setter body that contains the property name, as to avoid typos.
Most properties look something like this, but they may do more complex tasks such as converting data types
@property
def name(self) -> str:
return self.header['MYKEY'] if 'MYKEY' in self.header else ""
@name.setter
def name(self, value: str):
self.header['MYKEY'] = value
I now would like to have an additional decorator @RegisterProperty('MYKEY') that
@classmethod adds the "hidden" argument cls to the function.myclass.registered_properties to see what key values are "managed" through properties and which ones aren't.From what I've read about decorators, the way to go is to define a class-based decorator inside the class that I want to use it in (as I cannot create a function based decorator in the class, as it would become a method or something). I can modify the signature of the wrapped class method (property getter setter) through the *args,**kwargs arguments, and i can also extract self from those as to write myclass.registered_properties.
But
myclass.registered_properties, as self is only available during runtime and I don't want to check for it's existence every time (for performance reasons, mostly).Edit1: After reading this I got this prototype for a decorator:
class PropertyRegister:
def __init__(self, parent, property_name):
self.property_name = property_name
parent.registered_properties.add(property_name)
def __call__(self, func):
def wrapped_f(*args, **kwargs):
args.insert(0, self.property_name)
return func(*args, **kwargs)
functools.update_wrapper(self, func)
return wrapped_f
But still not sure how to ensure the existence of parent.registered_properties.
Edit2: As pointed out in the comments, the prototype decorator doesn't work, as self cannot be passed to parent for the decorator, as it doesn't exist yet at this point. Here's an updated version that intercepts self from the function call itself. Bit ugly however.
class PropertyRegister:
def __init__(self, property_name):
self.property_name = property_name
def __call__(self, func):
def wrapped_f(*args, **kwargs):
parent = args[0]
parent.registered_properties.add(self.property_name)
args.insert(1, self.property_name)
return func(*args, **kwargs)
functools.update_wrapper(self, func)
return wrapped_f
...
@RegisterProperty('MYKEY')
@property
def name(self, property_name) -> str:
return self.header[property_name] if property_name in self.header else ""
@RegisterProperty('MYKEY')
@name.setter
def name(self, property_name, value: str):
self.header[property_name] = value
Problem with this is that the property will only be registered, once the getter/setter is actually called.
Here's a Q&D possible implementation using a custom property-like descriptor and a class decorator to collect registered keys (you may also use a custom metaclass instead):
class FlaggedProperty():
def __init__(self, key):
self._key = key
self._getter = None
self._setter = None
def __call__(self, getter):
self._getter = getter
return self
def setter(self, setter):
self._setter = setter
return self
def __get__(self, obj, cls=None):
if obj is None:
return self
return self._getter(obj, self._key)
def __set__(self, obj, value):
if self._setter is None:
raise AttributeError("Attribute is read-only")
self._setter(obj, self._key, value)
def register_flagged_properties(cls):
cls.registered_properties = set()
for parent in cls.__mro__[1:]:
parent_properties = getattr(parent, "registered_properties", None)
if parent_properties:
cls.registered_properties.update(parent_properties)
for item in cls.__dict__.values():
if isinstance(item, FlaggedProperty):
cls.registered_properties.add(item._key)
return cls
And how it's used:
@register_flagged_properties
class Foo():
def __init__(self, data):
self.data = data
@FlaggedProperty("foo")
def foo(self, key):
return self.data.get(key, None)
@foo.setter
def foo(self, key, value):
self.data[key] = value
I looked at the class creation process but I didn't get much out of it. Perhaps you are trying to point me at metaclasses?
Well, first and mainly, you have to understand how a class is created at runtime to understand what is available at which point of the process. And yes, metaclasses was supposed to be part of the possible answers - together with the init_subclass hook or class decorators (I mean: "decorators applied to classes", not "decorators defined as classes").
The descriptor protocol just seems to cover the underlying mechanics of properties, but again I fail to see the pointer to the solution I seek.
It's not only used by properties - it's also (and much more importantly) the support for methods (both instancememethods and classmethods), and for any custom descriptor.
This is an addition to bruno's answer. If you are using python 3.6 or later, you can ommit the register_flagged_properties decorator and let the FlaggedProperty instances register themselves.
Simply add this method to the FlaggesProperty class.
def __set_name__(self, cls, name):
# cls is the class where the decorator is used
# name is the function name
try:
cls.registered_properties.add(self._key)
except AttributeError:
cls.registered_properties = {self._key}
The name is a bit misleading in my opinion, but the method is called for every tagged property when the class is created.
You could even use the python name as the key if you want. For this change the __init__ method to:
def __init__(self, getter):
self._key = None
self._getter = getter
self._setter = None
Delete the __call__ method and in __set_name__ add this line:
def __set_name__(self, cls, name):
self._key = name
# leave the rest unchanged
Now you can use the decorator like this:
class Foo:
@FlaggedProperty
def bar(self, key):
# key now contains the string "bar"
# leave the rest unchanged
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