Skip to content

Commit

Permalink
feat: add ability to dynamically alter default retrievers for a class.
Browse files Browse the repository at this point in the history
  • Loading branch information
joshorr committed Mar 27, 2023
1 parent bbdd506 commit 2f5df69
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 15 deletions.
8 changes: 5 additions & 3 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -597,11 +597,11 @@ to follow:
- Example: `with MySetting(some_setting='parent-value'):`
3. Retrievers are consulted next.
1. First, retriever set directly on field `xsettings.fields.SettingsField.retriever`.
1. This can include any field properties `@property`, they are set as the field retriever.
1. This can include any field properties `@property`, as they are set as the field retriever.
2. Next, instance retriever(s) set directly on the Setting object that is being asked for its field value is checked.
1. via [`BaseSettings.add_instance_retrievers`](api/xsettings/settings.html#xsettings.settings.Settings.add_instance_retrievers){target=_blank}.
1. via [`BaseSettings.add_instance_retrievers`](api/xsettings/settings.html#xsettings.settings.Settings.settings__instance_retrievers){target=_blank}.
3. Then any instance-retrievers in the dependency-chain are checked next (see step 2 above for more details).
4. Default-retrievers assigned to the class(es) are next checked, in `mro` order.
4. Default-retrievers assigned to the class(es) are next checked, in `mro` order (ie: parent/super-classes are checked last).
4. Finally, any default-value for the field is consulted.
- If the default-value is a property, or forward-ref then that is followed.
- ie: `BaseSettings.some_attr = OtherSettings.another_attr_to_forward_ref_with`
Expand All @@ -610,6 +610,8 @@ to follow:
Keep in mind that generally, if a value is a `property` (including forward-refs, which are also properties),
they are followed via the standard `__get__` mechanism (see earlier in document for forward-ref details).

You can add

## Resolution Details

Values set directly on Setting instances are first checked for and used if one is found.
Expand Down
21 changes: 20 additions & 1 deletion tests/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -560,12 +560,16 @@ def test_grab_setting_values_from_parent_dependency_instances():
def r1(*, field: SettingsField, settings: BaseSettings):
return 2 if field.name == 'c' else 'str-val'

class MySettings(BaseSettings, default_retrievers=[r1]):
class MySettings(BaseSettings):
# Make them fields in our BaseSettings subclass, default value to another settings class.
a: str
b: str
c: int

# we could have added this via `MySettings(..., default_retrievers=[r1])` above,
# but this lets us easily test adding it at run-time.
MySettings.settings__default_retrievers.append(r1)

my_settings = MySettings.proxy()
my_settings.a = "override-a"

Expand All @@ -580,6 +584,7 @@ class MySettings(BaseSettings, default_retrievers=[r1]):
assert my_settings.b == 'override-via-child-instance-b'
assert my_settings.c == 2

# ensure values are reverted...
assert my_settings.a == 'override-a'
assert my_settings.b == 'str-val'
assert my_settings.c == 2
Expand All @@ -594,3 +599,17 @@ def r2(*, field: SettingsField, settings: BaseSettings):
assert my_settings.a == 'override-a'
assert my_settings.b == 'str-val-r2'
assert my_settings.c == 2

# ensure values are reverted...
assert my_settings.a == 'override-a'
assert my_settings.b == 'str-val'
assert my_settings.c == 2

# Try adding instance retriever after class was created.
MySettings.grab().settings__instance_retrievers.append(r2)

# These values come from the `r2` retriever, which should be checked first
# before the default-retriever at the BaseSettings class level (ie: r1 further above).
assert my_settings.a == 'override-a'
assert my_settings.b == 'str-val-r2'
assert my_settings.c == 2
113 changes: 102 additions & 11 deletions xsettings/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,59 @@ def __new__(

return cls

settings__default_retrievers: 'List[SettingsRetrieverProtocol]'

# @SettingsClassProperty
@property
def settings__default_retrievers(self) -> 'List[SettingsRetrieverProtocol]':
"""
You can add one or more retrievers to this `subclass` of BaseSettings
(modifies default_retrievers for the entire class + subclasses, only modifies this specific
class).
You can add or modify the list of default-retrievers via
`BaseSettings.settings__default_retrievers`. It's a list that you can directly modify;
ie: `MySettings.settings__default_retrievers.append(my_retriever)`.
## Background
Below is a quick summary, you can see more detailed information in main docs under the
`"How Setting Field Values Are Resolved"` heading.
Directly set values (ie: `self.some_settings = 'some-value'`)
are first checked for in self, and next in `xinject.context.XContext.dependency_chain`
(looking at each instance currently in the dependency-chain, see link for details).
If value can't be found set on self or in dependency chain,
the retrievers are checked next.
First the field's individual retriever is checked (directly on field object,
this includes any `@property` fields too as the property getter method is stored on
field's individual retriever).
After the individual field retrievers are consulted, instance retrievers are checked next
before finally checking the default-retrievers for the entire class.
They are checked in the order added.
Child dependencies (of the same exactly class/type) in the
`xinject.context.XContext.dependency_chain` will also check these instance-retrievers.
The dependency chain is checked in the expected order of first consulting self,
then the chain in most recent parent first order.
For more details on how parent/child dependencies work see
`xinject.context.XContext.dependency_chain`.
After the dependency-chain is checked, the default-retrievers are checked
in python's `mro` (method-resolution-order), checking its own class first
before checking any super-classes for default-retrievers.
Returns:
A list of default-retrievers you can examine and/or modify as needed.
"""
return self._default_retrievers

def __getattr__(self, key: str) -> SettingsClassProperty:
"""
We will return a `ClassProperty` object setup to retrieve the value asked for as
Expand All @@ -162,8 +215,17 @@ def __getattr__(self, key: str) -> SettingsClassProperty:
>>> MySettings.grab().my_url_setting = "my-url"
>>> assert SomeClass.some_attr == "my-url"
"""
if key.startswith("_"):
return super().__getattr__(key)

# Anything that starts with `_` or starts with `settings__`
# is handled like a normal pythonattribute.
if key.startswith("_") or key.startswith("settings__"):
raise AttributeError(
f"An attribute lookup that start with `_` or `settings__` just happened ({key}) "
f"and it does not exist. "
f"Attributes name this way can't be fields, but they should also exist so there "
f"must be some sort of bug.... details: "
f"attribute name ({key}) on BaseSettings subclass ({self})."
)

for c in self._setting_subclasses_in_mro:
c: _SettingsMeta
Expand Down Expand Up @@ -346,8 +408,11 @@ def __init__(
obj = SomeSettings()
obj.some_keyword_arg="hello"
```
Args:
retrievers: can be used to populate new instance's retrievers,
see `BaseSettings.settings__instance_retrievers`.
"""
self._instance_retrievers = list(xloop(retrievers))

Expand All @@ -357,16 +422,41 @@ def __init__(
def add_instance_retrievers(
self, retrievers: 'Union[List[SettingsRetrieverProtocol], SettingsRetrieverProtocol]'
):
from warnings import warn
warn(
f"BaseSettings.add_instance_retrievers is now deprecated, "
f"was used on subclass ({type(self)}); "
f"use property `settings__instance_retrievers` and call 'append' on result; "
f"ie: `my_settings.settings__instance_retrievers.append(retriever)"
)
self.settings__instance_retrievers.extend(xloop(retrievers))

@property
def settings__instance_retrievers(self) -> 'List[SettingsRetrieverProtocol]':
"""
You can add one or more retrievers to this `instance` of settings
(won't modify default_retrievers for the entire class, only modifies this specific
instance).
Directly set values are first checked for in self, and next in
`xinject.context.XContext.dependency_chain`
You can add or modify the list of instance-retrievers via
`BaseSettings.settings__instance_retrievers`. It's a list that you can directly modify;
ie: `my_settings.settings__instance_retrievers.append(my_retriever)`.
## Background
Below is a quick summary, you can see more detailed information in main docs under the
`"How Setting Field Values Are Resolved"` heading.
Directly set values (ie: `self.some_settings = 'some-value'`)
are first checked for in self, and next in `xinject.context.XContext.dependency_chain`
(looking at each instance currently in the dependency-chain, see link for details).
If value can't be found the retrievers are next checked.
If value can't be found set on self or in dependency chain,
the retrievers are checked next.
First the field's individual retriever is checked (directly on field object,
this includes any `@property` fields too as the property getter method is stored on
field's individual retriever).
After the individual field retrievers are consulted, instance retrievers are checked next
before finally checking the default-retrievers for the entire class.
Expand All @@ -386,14 +476,15 @@ def add_instance_retrievers(
in python's `mro` (method-resolution-order), checking its own class first
before checking any super-classes for default-retrievers.
Args:
retrievers (Union[List[SettingsRetrieverProtocol], SettingsRetrieverProtocol]): The
retriever(s) to add to the instance.
Returns:
A list of instance-retrievers you can examine and/or modify as needed.
"""
self._instance_retrievers.extend(xloop(retrievers))
return self._instance_retrievers

def __getattribute__(self, key):
if key.startswith("_"):
# Anything that starts with `_` or starts with `settings__`
# is handled like a normal python attribute.
if key.startswith("_") or key.startswith("settings__"):
return object.__getattribute__(self, key)

attr_error = None
Expand Down

0 comments on commit 2f5df69

Please sign in to comment.