Created
March 15, 2023 16:46
-
-
Save astanziola/1c9bc927ab83b858426b25c31f075e74 to your computer and use it in GitHub Desktop.
Python object with settings
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
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, | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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 :)