Skip to content

Instantly share code, notes, and snippets.

@astanziola
Created March 15, 2023 16:46
Show Gist options
  • Save astanziola/1c9bc927ab83b858426b25c31f075e74 to your computer and use it in GitHub Desktop.
Save astanziola/1c9bc927ab83b858426b25c31f075e74 to your computer and use it in GitHub Desktop.
Python object with settings
"""
This is a Python class named "ObjectWithSettings" that serves as a base
class for other classes to inherit from. It allows objects to add
settings as dictionary items and access them using keys as well as
attributes. The class contains methods for adding a setting, setting an
attribute, getting the settings dictionary as a property, and getting a
flattened dictionary of all settings, including nested settings.
"""
from adict import adict
def flatten_dict(d, parent_key="", sep="/"):
items = []
for k, v in d.items():
new_key = parent_key + sep + k if parent_key else k
if isinstance(v, dict):
items.extend(flatten_dict(v, new_key, sep=sep).items())
else:
items.append((new_key, v))
return dict(items)
class ObjectWithSettings:
"""
A base class that provides an easy way to add settings
to an object and access them as a dictionary.
"""
def add_setting(self, key, value, set_attribute=True):
"""
Adds a setting to the object.
Args:
key (str): The name of the setting.
value (Any): The value of the setting.
set_attribute (bool): Whether to also add the setting as an attribute of the object.
Returns:
None
"""
# Check that the _settings dictionary exists
if not hasattr(self, "_settings"):
self._settings = adict()
# Add to dictionary
self._settings[key] = value
# Add it to the object attributes too
if set_attribute:
setattr(self, key, value)
def __setattr__(self, key, value):
# Override the setattr method such that if one tries to set an attribute
# that is also in the settings dictionary, it will also set the value in the
# settings dictionary.
if hasattr(self, "_settings") and key in self._settings:
self._settings[key] = value
super().__setattr__(key, value)
@property
def settings(self) -> adict:
"""
A property that returns the settings dictionary.
Returns:
adict: The settings dictionary.
"""
return self._settings
@settings.setter
def settings(self, value) -> None:
raise RuntimeError('The settings attribute is read-only. Use the "add_setting" method instead.')
@property
def settings_as_dict(self):
"""
A property that returns the settings dictionary with
nested settings as well.
Returns:
A flattened dictionary of the settings.
"""
settings_dict = flatten_dict(self.settings)
# For every attribute, if it is a ObjectWithSettings,
# add its settings to the settings_dict. Before each
# key, add the __repr__ of the class.
for attribute_name, attribute_value in self.__dict__.items():
if isinstance(attribute_value, ObjectWithSettings):
attribute_settings = attribute_value.settings_as_dict
for key, value in attribute_settings.items():
settings_dict[f"{attribute_name}/{key}"] = value
return settings_dict
def __repr__(self):
return f"{self.__class__.__name__}"
def test_updatding_attribute_that_is_setting():
class A(ObjectWithSettings):
def __init__(self):
self.add_setting("A_setting", 1)
self.b = 2
a = A()
a.A_setting = 2
assert a.settings_as_dict == {"A_setting": 2}
assert a.A_setting == 2
a.b = 3
assert a.settings_as_dict == {"A_setting": 2}
assert not "b" in a.settings.keys()
a.add_setting("c", 4)
assert a.settings_as_dict == {"A_setting": 2, "c": 4}
assert a.c == 4
def test_adding_setting_and_attribute():
class A(ObjectWithSettings):
def __init__(self):
self.add_setting("A_setting", 1)
a = A()
assert a.settings_as_dict == {"A_setting": 1}
assert a.A_setting == 1
def test_add_setting_only():
class A(ObjectWithSettings):
def __init__(self):
self.add_setting("A_setting", 1, set_attribute=False)
a = A()
assert a.settings_as_dict == {"A_setting": 1}
assert not hasattr(a, "A_setting")
def test_warning_when_setting_attribute():
class A(ObjectWithSettings):
pass
a = A()
try:
a.settings = 1
except RuntimeError as e:
assert str(e) == 'The settings attribute is read-only. Use the "add_setting" method instead.'
def test_object_with_settings():
class A(ObjectWithSettings):
def __init__(self):
self.add_setting("A_setting", 1)
class B(ObjectWithSettings):
def __init__(self):
self.a = A()
self.add_setting("B_setting", 2)
class C(ObjectWithSettings):
def __init__(self):
self.b = B()
self.add_setting("C_setting", 3)
c = C()
assert c.settings_as_dict == {
'b/a/A_setting': 1,
'b/B_setting': 2,
'C_setting': 3,
}
@astanziola
Copy link
Author

One simple way to make this more efficient is not to duplicate the settings but just have a flag to check if an attribute is also a setting. Don't have time to do this now :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment