I am writing a class in Python for some settings which looks like this:
class _CanvasSettings:
def __init__(self, **kwargs):
super().__init__()
self._size_x = _int(kwargs, 'size_x', 320)
self._size_y = _int(kwargs, 'size_y', 240)
self._lock_ratio = _bool(kwargs'lock_ratio', True)
def get_size_x_var(self):
return self._size_x
def _get_size_x(self):
return self._size_x.get()
def _set_size_x(self, value):
self._size_x.set(value)
size_x = property(_get_size_x, _set_size_x)
def get_size_y_var(self):
return self._size_y
def _get_size_y(self):
return self._size_y.get()
def _set_size_y(self, value):
self._size_y.set(value)
size_y = property(_get_size_y, _set_size_y)
def get_lock_ratio_var(self):
return self._lock_ratio
def _get_lock_ratio(self):
return self._lock_ratio.get()
def _set_lock_ratio(self, value):
self._lock_ratio.set(value)
lock_ratio = property(_get_lock_ratio, _set_lock_ratio)
as you can see I add the block:
def get_something_var(self):
return self._something
def _get_something(self):
return self._something.get()
def _set_something(self, value):
self._something.set(value)
something = property(_get_something, _set_something)
For every single setting.
Is it possible to automate this task with a decorator
?
I would like to do it like this (pseudocode):
def my_settings_class(cls):
result = cls
for field in cls:
result.add_getter_setter_and_property( field )
return result
@my_settings_class
class _CanvasSettings:
def __init__(self, **kwargs):
super().__init__()
self._size_x = _int(kwargs, 'size_x', 320)
self._size_y = _int(kwargs, 'size_y', 240)
self._lock_ratio = _bool(kwargs'lock_ratio', True)
# Done !
Is this possible?
If yes, how?
How to implement the add_getter_setter_and_property()
method?
Edit:
There is a pretty similar question here: How to decorate a class?
from the answers there I suspect that it is possible to achive something like I have asked, but can you give me a clue on how I could implement the add_getter_setter_and_property()
function/method?
Note:
the _int()
, _bool()
functions just return a tkinter Int/Bool-var eighter from the kwargs if the string (f.e. 'size_x') exist or from the default value (f.e. 320).
My Final Solution: I think I have found a pretty good solution. I have to add a settings name only once, which in my opinion is awesome
import tkinter as tk
def _add_var_getter_property(cls, attr):
""" this function is used in the settings_class decorator to add a
getter for the tk-stringvar and a read/write property to the class.
cls: is the class where the attributes are added.
attr: is the name of the property and for the get_XYZ_var() method.
"""
field = '_' + attr
setattr(cls, 'get_{}_var'.format(attr), lambda self: getattr(self, field))
setattr(cls, attr,
property(lambda self: getattr(self, field).get(),
lambda self, value: getattr(self, field).set(value)))
def settings_class(cls):
""" this is the decorator function for SettingsBase subclasses.
it adds getters for the tk-stringvars and properties. it reads the
names described in the class-variable _SETTINGS.
"""
for name in cls._SETTINGS:
_add_var_getter_property(cls, name)
return cls
class SettingsBase:
""" this is the base class for a settings class. it automatically
adds fields to the class described in the class variable _SETTINGS.
when you subclass SettingsBase you should overwrite _SETTINGS.
a minimal example could look like this:
@settings_class
class MySettings(SettingsBase):
_SETTINGS = {
'x': 42,
'y': 23}
this would result in a class with a _x tk-intvar and a _y tk-doublevar
field with the getters get_x_var() and get_y_var() and the properties
x and y.
"""
_SETTINGS = {}
def __init__(self, **kwargs):
""" creates the fields described in _SETTINGS and initialize
eighter from the kwargs or from the default values
"""
super().__init__()
fields = self._SETTINGS.copy()
if kwargs:
for key in kwargs:
if key in fields:
typ = type(fields[key])
fields[key] = typ(kwargs[key])
else:
raise KeyError(key)
for key in fields:
value = fields[key]
typ = type(value)
name = '_' + key
if typ is int:
var = tk.IntVar()
elif typ is str:
var = tk.StringVar()
elif typ is bool:
var = tk.BooleanVar()
elif typ is float:
var = tk.DoubleVar()
else:
raise TypeError(typ)
var.set(value)
setattr(self, name, var)
after that my settings classes simply look like this:
@settings_class
class _CanvasSettings(SettingsBase):
_SETTINGS = {
'size_x': 320,
'size_y': 240,
'lock_ratio': True
}
It's certainly possible to do what you want, using setattr
to bind the functions and property as attributes of the class object:
def add_getter_setter_property(cls, attrib_name):
escaped_name = "_" + attrib_name
setattr(cls, "get_{}_var".format(attrib_name),
lambda self: getattr(self, escaped_name))
setattr(cls, attrib_name,
property(lambda self: getattr(self, escaped_name).get()
lambda self, value: getattr(self, escaped_name).set(value)))
Here I'm skipping giving names to the getter
and setter
methods used by the property
. You could add them to the class if you really want to, but I think it's probably unnecessary.
The tricky bit may actually be finding which attribute names you need to apply this to. Unlike in your example, you can't iterate over a class object to get its attributes.
The easiest solution (from the implementation standpoint) would be to require the class to specify the names in a class variable:
def my_settings_class(cls):
for field in cls._settings_vars:
add_getter_setter_and_property(cls, field)
return cls
@my_settings_class
class _CanvasSettings:
_settings_vars = ["size_x", "size_y", "lock_ratio"]
def __init__(self, **kwargs):
super().__init__()
self._size_x = _int(kwargs, 'size_x', 320)
self._size_y = _int(kwargs, 'size_y', 240)
self._lock_ratio = _bool(kwargs, 'lock_ratio', True)
A more user-friendly approach might use dir
or vars
to examine the classes variables and pick out the ones that need to be wrapped automatically. You could use isinstance
to check if the value has a specific type, or look for a specific pattern in the attribute name. I don't know what is best for your specific use, so I'll leave this up to you.