Package xsettings
Expand source code
from .settings import BaseSettings
from .fields import SettingsField
from .env_settings import EnvVarSettings
Settings = BaseSettings
"""
Deprecated; Use `BaseSettings` instead.
Here for backwards compatability, renamed original class from `Settings` to `BaseSettings`.
"""
Sub-modules
xsettings.default_converters
xsettings.env_settings
xsettings.errors
xsettings.fields
xsettings.retreivers
xsettings.settings
-
See doc-comments for
BaseSettings
below.
Classes
class Settings (retrievers: Optional[Union[List[SettingsRetrieverProtocol], SettingsRetrieverProtocol]] = None, **kwargs)
-
Base Settings class. For all class properties defined there will be a corresponding _settings_field["name"] = SettingsField created value that will control how this value is read and manipulated.
The purpose of the Settings class is to allow a library or project/service to define a number of settings that are needed in order to function. You define a number of Settings propertiess to indicate what settings are available to use in the project.
You define a Settings and properties very similar to how you define a dataclass. You specify a property name, type_hint, and default_value.
>>> class MySettings(BaseSettings): ... name: type_hint = default_value
A default
SettingsField
will be configured using the name, type_hint, and default_value as follows –SettingsField(name=name, type_hint=type_hint, converter=type_hint, default_value=default_value, resolver=default_resolver)
This functionality can be overridden by setting the default_value to a custom
SettingsField
. The customSettingsField
will be merged with the defaultSettingsField
overriding any fields that were defined in the custom SettingsField.It's important to note that while we are setting these attributes on the class they will not remain as attributes on the class. The _SettingsMeta will take each attribute and convert them to a SettingsField and then place them in the class's _setting_fields attribute.
Example of various ways to allocate a SettingsField on a Settings subclass:
>>> class MySettings(BaseSettings): ... setting_1: int Allocates >>> SettingsField(name="setting_1", type_hint=int, resolver=SettingsResolver)
>>> class MySettings(BaseSettings): ... setting_1: int = 3 Allocates >>> SettingsField(name="setting_1", type_hint=int, resolver=SettingsResolver, default_value=3)
>>> class MySettings(BaseSettings): ... setting_1 = 3 Allocates >>> SettingsField(name="setting_1", type_hint=int, resolver=SettingsResolver, default_value=3)
>>> class MySettings(BaseSettings): ... setting_1: int = SettingsField(name="other", required=False) Allocates >>> SettingsField(name="other", type_hint=int, resolver=SettingsResolver, required=False)
Accessing Class (not instance) Attributes = Lazy Property Reference
You can do lazy forward-refrences by simply asking the Settings class (not instance) for a attribute. Doing so will return a
SettingsClassProperty
that is a forward reference to the singleton instance class attribute.Examples of how you might use this
>>> class MySettings(BaseSettings): ... my_url_setting: str >>> class MySubSettings(BaseSettings): ... my_field: str >>> class SomeClass: ... some_attr = MySettings.my_url_setting >>> >>> MySettings.grab().my_url_setting = "my-url" >>> MySubSettings.grab().my_field = MySettings.my_url_setting >>> >>> assert SomeClass.some_attr == "my-url" >>> assert MySubSettings.grab().some_attr == "my-url" >>> assert MySettings.grab().some_attr == "my-url"
Setting public Class Attributes after creation time not allowed
Attempting to set a public class level attribute will result in an Error being raised.
Settings Attribute (Property) Inheritance not allowed
To keep things as simple as possible we don't allow SettingsClass attribute inheritance. You can however create a parent class that defines methods / @properties that can be inherited. Trying to set a regular (non-method/non-property/public) attribute will raise an error.
Accessing Instance Properties = Get Value and Convert
When calling MySettings.grab().my_setting the Settings class will attempt to retrieve and convert the corresponding value. Getting the the source value and converting the value is controlled by the SettingsField. Here is how it works.
Start by attempting to retrieve a property from the class instance
>>> MyClass.grab().my_setting
The Settings class will do the following 1. Attempt to retrieve a value. a. Lookup value from self via
object.__getattribute__(self, key)
b. If an AttributeError is thrown then lookup the value from the corresponding field viaself._settings_field[name].get_value()
2. Convert the retrieved value by callingself._settings_field[name].convert_value(value)
TODO
We don't support property setters/deleters at the moment. We would need to implement a
__setattr__
here, where it would check for a property setter/getter on field object. (Consider a explicitfget
andfset
attribute on SettingsField at that point)Set attributes to values that are passed via key-word arguments, these are the initial values for the settings instance your creating; they are set directly on the instance as if you did this:
# These two statements do the same thing: obj = SomeSettings(some_keyword_arg="hello") obj = SomeSettings() obj.some_keyword_arg="hello"
Args
retrievers
- can be used to populate new instance's retrievers,
Expand source code
class BaseSettings( Dependency, metaclass=_SettingsMeta, default_retrievers=[], # BaseSettings has no fields, it's a special abstract-type of class skip field generation. # You should never use this option in a BaseSettings subclass. skip_field_generation=True ): """ Base Settings class. For all class properties defined there will be a corresponding _settings_field["name"] = SettingsField created value that will control how this value is read and manipulated. The purpose of the Settings class is to allow a library or project/service to define a number of settings that are needed in order to function. You define a number of Settings propertiess to indicate what settings are available to use in the project. You define a Settings and properties very similar to how you define a dataclass. You specify a property name, type_hint, and default_value. >>> class MySettings(BaseSettings): ... name: type_hint = default_value A default `SettingsField` will be configured using the name, type_hint, and default_value as follows -- `SettingsField(name=name, type_hint=type_hint, converter=type_hint, default_value=default_value, resolver=default_resolver)` This functionality can be overridden by setting the default_value to a custom `SettingsField`. The custom `SettingsField` will be merged with the default `SettingsField` overriding any fields that were defined in the custom SettingsField. It's important to note that while we are setting these attributes on the class they will not remain as attributes on the class. The _SettingsMeta will take each attribute and convert them to a SettingsField and then place them in the class's _setting_fields attribute. Example of various ways to allocate a SettingsField on a Settings subclass: >>> class MySettings(BaseSettings): ... setting_1: int Allocates >>> SettingsField(name="setting_1", type_hint=int, resolver=SettingsResolver) >>> class MySettings(BaseSettings): ... setting_1: int = 3 Allocates >>> SettingsField(name="setting_1", type_hint=int, resolver=SettingsResolver, default_value=3) >>> class MySettings(BaseSettings): ... setting_1 = 3 Allocates >>> SettingsField(name="setting_1", type_hint=int, resolver=SettingsResolver, default_value=3) >>> class MySettings(BaseSettings): ... setting_1: int = SettingsField(name="other", required=False) Allocates >>> SettingsField(name="other", type_hint=int, resolver=SettingsResolver, required=False) ## Accessing Class (not instance) Attributes = Lazy Property Reference You can do lazy forward-refrences by simply asking the Settings class (not instance) for a attribute. Doing so will return a `SettingsClassProperty` that is a forward reference to the singleton instance class attribute. Examples of how you might use this >>> class MySettings(BaseSettings): ... my_url_setting: str >>> class MySubSettings(BaseSettings): ... my_field: str >>> class SomeClass: ... some_attr = MySettings.my_url_setting >>> >>> MySettings.grab().my_url_setting = "my-url" >>> MySubSettings.grab().my_field = MySettings.my_url_setting >>> >>> assert SomeClass.some_attr == "my-url" >>> assert MySubSettings.grab().some_attr == "my-url" >>> assert MySettings.grab().some_attr == "my-url" ## Setting public Class Attributes after creation time not allowed Attempting to set a public class level attribute will result in an Error being raised. ## Settings Attribute (Property) Inheritance not allowed To keep things as simple as possible we don't allow SettingsClass attribute inheritance. You can however create a parent class that defines methods / @properties that can be inherited. Trying to set a regular (non-method/non-property/public) attribute will raise an error. ## Accessing Instance Properties = Get Value and Convert When calling MySettings.grab().my_setting the Settings class will attempt to retrieve and convert the corresponding value. Getting the the source value and converting the value is controlled by the SettingsField. Here is how it works. Start by attempting to retrieve a property from the class instance >>> MyClass.grab().my_setting The Settings class will do the following 1. Attempt to retrieve a value. a. Lookup value from self via `object.__getattribute__(self, key)` b. If an AttributeError is thrown then lookup the value from the corresponding field via `self._settings_field[name].get_value()` 2. Convert the retrieved value by calling `self._settings_field[name].convert_value(value)` .. todo:: We don't support property setters/deleters at the moment. We would need to implement a `__setattr__` here, where it would check for a property setter/getter on field object. (Consider a explicit `fget` and `fset` attribute on SettingsField at that point) """ _instance_retrievers: 'List[SettingsRetrieverProtocol]' def __init__( self, retrievers: ( 'Optional[Union[List[SettingsRetrieverProtocol], SettingsRetrieverProtocol]]' ) = None, **kwargs ): """ Set attributes to values that are passed via key-word arguments, these are the initial values for the settings instance your creating; they are set directly on the instance as if you did this: ```python # These two statements do the same thing: obj = SomeSettings(some_keyword_arg="hello") 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)) for k, v in kwargs.items(): setattr(self, k, v) 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). 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 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 instance-retrievers you can examine and/or modify as needed. """ return self._instance_retrievers def __getattribute__(self, key): # 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 value = None already_retrieved_normal_value = False cls = type(self) field: Optional[SettingsField] = None for c in cls._setting_subclasses_in_mro: c: _SettingsMeta # todo: use isinstance? if c is BaseSettings: # We got to the 'BaseSettings' base-class it's self, no need to go any further. break if field := c._setting_fields.get(key): # Found the field, break out of loop. break def get_normal_value(obj: BaseSettings = self): nonlocal value nonlocal attr_error # Keep track that we already attempted to get normal value. nonlocal already_retrieved_normal_value if obj is self: already_retrieved_normal_value = True try: # Look for an attribute on self first. value = object.__getattribute__(obj, key) if hasattr(value, "__get__"): value = value.__get__(obj, cls) attr_error = None except AttributeError as error: attr_error = error # We don't want to grab the value like normal if we are a field # and DON'T have a locally/instance value defined for attribute. # This helps Setting classes that are subclasses of Plain classes # attempt to retrieve the value via the field retriever/mechanism # before using that plain-classes, class attribute. # # Otherwise, the plain-class attribute would ALWAYS be used over the field, # making the subclasses field definition somewhat useless. if not self._there_is_plain_superclass or not field or key in self.__dict__: get_normal_value() if not already_retrieved_normal_value or value is None: # See if any parent-setting-instances (not super/base classes) for parent_settings in XContext.grab().dependency_chain(cls): if key in parent_settings.__dict__: get_normal_value(parent_settings) if value is not None: break try: if field: return _resolve_field_value(settings=self, field=field, key=key, value=value) except SettingsValueError as e: # todo: Do some sort of refactoring/splitting this out of this method # (starting to get too large). # We had a field and could not retrieve the value, if we have not already attempted # to get the 'normal' value via our base-classes attributes , then attempt that; # If there is a plain class in are superclass/base-classes (ie: non-Setting) # This will check for a value in that class as well. if already_retrieved_normal_value: # Just continue the original exception raise get_normal_value() if attr_error: # Could not get the normal value from superclass, raise original exception. # todo: for Python 3.11, we can raise both exceptions (e + attr_error) # we actually have two separate trees of exceptions here! # For now we are prioritizing field value retrieval exception # as the 'from' exception and putting the plain-class attribute error # inside this exception., raise SettingsValueError( f"After attempting to get field value " f"(the 'from' exception with message [{e}]); " f"I tried to get value from plain superclass and that was also unsuccessful " f"and produced exception/error: ({attr_error}); " f"for field ({field})." ) from e if value is None and field.required: raise SettingsValueError( f"After attempting to get field value I next tried to get it from the plain " f"superclass but got None (details: via 'from' exception with message {e}); " f"for field ({field})." ) from e value = field.convert_value(value) if value is None and field.required: raise SettingsValueError( f"After attempting to get field value I next tried to get it from the plain " f"superclass, it came back with a value. But after running the `converter` on " f"the retrieved value a None was the result and this field is required; " f"for field ({field})." ) from e return value if attr_error: raise AttributeError( f"Unable to retrieve settings attribute ({key}) from ({self}), " f"there was no defined class-level settings field and no value set for " f"attribute on object, see original exception for more details." ) from attr_error return value
Ancestors
Subclasses
Static methods
def __init_subclass__(thread_sharable=Default, attributes_to_skip_while_copying: Optional[Iterable[str]] = Default, **kwargs)
-
Inherited from:
Dependency
.__init_subclass__
Args
thread_sharable
- If
False
: While a dependency is lazily auto-created, we will ensure we do it per-thread, and not make it visible …
def grab() ‑> ~T
-
Inherited from:
Dependency
.grab
Gets a potentially shared dependency from the current
udpend.context.XContext
… def proxy() ‑> ~R
-
Inherited from:
Dependency
.proxy
Returns a proxy-object, that when and attribute is asked for, it will proxy it to the current object of
cls
… def proxy_attribute(attribute_name: str) ‑> Any
-
Inherited from:
Dependency
.proxy_attribute
Returns a proxy-object, that when and attribute is asked for, it will proxy it to the current attribute value on the current object of
cls
…
Instance variables
var obj
-
Inherited from:
Dependency
.obj
class property/attribute that will return the current dependency for the subclass it's asked on by calling
Dependency.grab
, passing no extra … var settings__instance_retrievers : 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).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 inXContext.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
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
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 instance-retrievers you can examine and/or modify as needed.
Expand source code
@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). 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 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 instance-retrievers you can examine and/or modify as needed. """ return self._instance_retrievers
Methods
def __call__(self, func)
-
Inherited from:
Dependency
.__call__
This makes Resource subclasses have an ability to be used as function decorators by default unless this method is overriden to provide some other …
def __copy__(self)
-
Inherited from:
Dependency
.__copy__
Basic shallow copy protection (I am wondering if I should just remove this default copy code) …
def add_instance_retrievers(self, retrievers: Union[List[SettingsRetrieverProtocol], SettingsRetrieverProtocol])
-
Expand source code
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))