From 07f24873fa6cd76a764db68e4ca1753d8d6da833 Mon Sep 17 00:00:00 2001 From: Josh Orr Date: Tue, 21 Feb 2023 08:14:33 -0700 Subject: [PATCH] feat: rename Settings to BaseSettings; retained backwards compatibility. --- README.md | 10 +- docs/index.md | 172 ++++++++++++++++++++------------ tests/test_examples.py | 40 ++++---- tests/test_fields.py | 24 ++--- tests/test_settings.py | 84 ++++++++-------- xsettings/__init__.py | 10 +- xsettings/default_converters.py | 2 +- xsettings/env_settings.py | 6 +- xsettings/fields.py | 34 +++---- xsettings/retreivers.py | 16 +-- xsettings/settings.py | 89 +++++++++-------- 11 files changed, 272 insertions(+), 215 deletions(-) diff --git a/README.md b/README.md index 41ae166..0bba723 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Helps document and centralizing settings in a python project/library. -Facilitates looking up Settings from `retrievers`, such as an environmental variable retriever. +Facilitates looking up BaseSettings from `retrievers`, such as an environmental variable retriever. Converts and standardizes any retrieved values to the type-hint on the setting attribute (such as bool, int, datetime, etc). @@ -77,9 +77,9 @@ class MySettings(EnvVarSettings): converter=DBConfig.from_dict ) -# Settings subclasses are singleton-like dependencies that are +# BaseSettings subclasses are singleton-like dependencies that are # also injectables and lazily-created on first-use. -# YOu can use a special `Settings.grab()` class-method to +# YOu can use a special `BaseSettings.grab()` class-method to # get the current settings object. # # So you can grab the current MySettings object lazily via @@ -127,7 +127,7 @@ assert my_settings.app_env == 'dev' # explicitly set to anything on settings object: assert my_settings.app_version == '1.2.3' -# Any Settings subclass can use dependency-injection: +# Any BaseSettings subclass can use dependency-injection: assert my_settings.token is None with MySettings(token='my-token'): @@ -149,7 +149,7 @@ assert my_settings.token is None try: # If a setting is undefined and required (ie: not-optional), # and it was not set to anything nor is there a default or an env-var for it; - # Settings will raise an exception when getting it: + # BaseSettings will raise an exception when getting it: print(my_settings.api_endpoint_url) except SettingsValueError as e: assert True diff --git a/docs/index.md b/docs/index.md index 94e9795..a693a8e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -16,7 +16,7 @@ poetry add xloop Helps document and centralizing settings in a python project/library. -Facilitates looking up Settings from `retrievers`, such as an environmental variable retriever. +Facilitates looking up BaseSettings from `retrievers`, such as an environmental variable retriever. Converts and standardizes any retrieved values to the type-hint on the setting attribute (such as bool, int, datetime, etc). @@ -66,9 +66,9 @@ class MySettings(EnvVarSettings): converter=DBConfig.from_dict ) -# Settings subclasses are singleton-like dependencies that are +# BaseSettings subclasses are singleton-like dependencies that are # also injectables and lazily-created on first-use. -# YOu can use a special `Settings.grab()` class-method to +# YOu can use a special `BaseSettings.grab()` class-method to # get the current settings object. # # So you can grab the current MySettings object lazily via @@ -116,7 +116,7 @@ assert my_settings.app_env == 'dev' # explicitly set to anything on settings object: assert my_settings.app_version == '1.2.3' -# Any Settings subclass can use dependency-injection: +# Any BaseSettings subclass can use dependency-injection: assert my_settings.token is None with MySettings(token='my-token'): @@ -138,7 +138,7 @@ assert my_settings.token is None try: # If a setting is undefined and required (ie: not-optional), # and it was not set to anything nor is there a default or an env-var for it; - # Settings will raise an exception when getting it: + # BaseSettings will raise an exception when getting it: print(my_settings.api_endpoint_url) except SettingsValueError as e: assert True @@ -161,30 +161,32 @@ else: The settings library is seperated into a few core components. -- Settings +- BaseSettings - SettingsField - SettingsRetrieverProtocol -# Settings +# BaseSettings -This is the core class that Settings implementations will inherit from. -Settings can be used as source for external settings / variables that a given +This is the core class that BaseSettings implementations will inherit from. +BaseSettings can be used as source for external settings / variables that a given application / library needs in order to function. It provides future developers an easy way to see what these external variables are and where they are derived from. -In its simplest form a Settings class implementation is simply a class -similar to a `@dataclass` that inherits from a given Settings base class. +In its simplest form a BaseSettings class implementation is simply a class +similar to a `@dataclass` that inherits from a given BaseSettings base class. + +Example BaseSettings File -Example Settings File ```python -from xsettings import Settings, SettingsField +from xsettings import BaseSettings, SettingsField + -class MySettings(Settings): +class MySettings(BaseSettings): a: int b: int = 1 c = "1" - + # For Full Customization, allocate SettingsField: d: str = SettingsField(...) ``` @@ -195,7 +197,7 @@ default_value) will be reflected in the SettingsField. If you want more customization you can set a SettingsField() as your default value and the fields set in that will be overridden in the main SettingsField object. -## Settings usage +## BaseSettings usage ### Class/Lazy Attribute Lookup @@ -205,15 +207,17 @@ to do property chaining, or you want to use a property as a property in another class ```python -from xsettings import Settings, EnvVarSettings +from xsettings import BaseSettings, EnvVarSettings import os -class MySettings(Settings): +class MySettings(BaseSettings): table_name: str + MySettings.grab().table_name = "the-t-name" - + + class MyTable: class Meta: # Here, we set a forward-ref class property @@ -222,6 +226,7 @@ class MyTable: # its asked for). table_name = MySettings.table_name + # Forward-ref is resolved via lazy-forward-ref, # each time it's asked for: assert MyTable.Meta.table_name == 'the-t-name' @@ -229,12 +234,14 @@ assert MyTable.Meta.table_name == 'the-t-name' with MySettings(table_name='alt-table-name'): assert MyTable.Meta.table_name == 'alt-table-name' + # Inherit from EnvVarSettings, so it will retrieve our settings # via environmental variables # (will use env-vars on-demand if value is not set directly on it). class MyEnvSettings(EnvVarSettings): my_table_name: str + os.environ['MY_TABLE_NAME'] = 'env-table-name' # We can directly set the setting on MySettings to a lazy-prop-ref @@ -247,10 +254,12 @@ MySettings.grab().table_name = MyEnvSettings.my_table_name assert MySettings.grab().table_name == 'env-table-name' + # Example 3, default value of settings field can be a lazy-property-ref -class MyOtherSettings(Settings): +class MyOtherSettings(BaseSettings): my_setting_attr: str = MyEnvSettings.my_table_name + my_other_settings = MyOtherSettings.proxy() assert my_other_settings.my_setting_attr == 'env-table-name' @@ -261,23 +270,27 @@ assert my_other_settings.my_setting_attr == 'env-table-2' ### Change Default Value -You can now (as of v1.3) change the default value on an already created Settings subclass: +You can now (as of v1.3) change the default value on an already created BaseSettings subclass: ```python -from xsettings import Settings, SettingsField +from xsettings import BaseSettings, SettingsField + -class MySettings(Settings): +class MySettings(BaseSettings): a: int b: int = 1 + # Change default value later on; # Now the `MySettings.a` will have a # default/fallback value of `2`: MySettings.a = 2 -class MyOtherSettings(Settings): + +class MyOtherSettings(BaseSettings): some_other_setting: str + # You can also set a lazy-ref as setting field's # default value after it's class is created. # (also if the type-hint's don't match it will convert @@ -291,7 +304,7 @@ assert MyOtherSettings.grab().some_other_setting == '1' ### Setting New Setting on Class Attributes -You can't create new settings attributes/fields on a Settings subclass after the class +You can't create new settings attributes/fields on a BaseSettings subclass after the class is created (only during class creation). You can set/change the default value for an existing settings attribute by simply assigning @@ -326,14 +339,14 @@ to happen. ### Inheriting from Plain Classes Currently, there is a something to watch out for when you also inherit from a plain class -for you Settings subclass. +for you BaseSettings subclass. For now, we treat all plain super-class values on attributes as-if they were directly assigned to the settings instance; ie: we will NOT try to 'retrieve' the value unless the value is set to `xsentinels.Default` in either the instance or superclass (whatever value it finds via normal python attribute retrieval rules). -You can use `xsentinels.Default` to force Settings to lookup the value and/or use its default value if it +You can use `xsentinels.Default` to force BaseSettings to lookup the value and/or use its default value if it has one. May in the future create a v2 of xsettings someday that will look at attributes directly @@ -350,7 +363,7 @@ converted `xsettings.fields.SettingsField.converter`. If there is not SettingsField used/generated for an attribute, then it's just a normal attribute. -No special features of the Settings class such as lazy/forward-references will work with them. +No special features of the BaseSettings class such as lazy/forward-references will work with them. ## How SettingsField is Generated @@ -373,26 +386,27 @@ After a class is created, you can't change or add SettingFields to it anymore, c Examples: ```python -from xsettings import Settings, SettingsField +from xsettings import BaseSettings, SettingsField -class MySettings(Settings): + +class MySettings(BaseSettings): custom_field: str = SettingsField(required=False) - + # SettingsField auto-generated for `a`: a: str - + # A Field is generated for `b`, the type-hint will be `type(2)`. b = 2 - + # A field is NOT generated for anything that starts with an underscore (`_`), # They are considered internal attributes, and no Field is ever generated for them. _some_private_attr = 4 - + # Field generated for `my_property`, it will be the fields 'retriever'. @property def my_property(self) -> str: return "hello" - + # No field is generated for `normal_method`: # Currently, any directly callable object/function as a class attribute value # will never allow you to have a Field generated for it. @@ -402,7 +416,7 @@ class MySettings(Settings): # Converters -When Settings gets a value due to someone asking for an attribute on it's self, it will +When BaseSettings gets a value due to someone asking for an attribute on it's self, it will attempt to convert the value if the value does not match the type-hint. To find a converter, we check these in order, first one found is what we use: @@ -422,7 +436,7 @@ retrieved. It can also be the field's default-value if noting is set/retreived. ## Supports Read-Only Properties -The Settings class also supports read-only properties, they are placed in a SettingField's +The BaseSettings class also supports read-only properties, they are placed in a SettingField's retriever (ie: `xsettings.fields.SettingField.retriever`). When you access a value from a read-only property, when the value needs to be retrieved, @@ -430,11 +444,11 @@ it will use the property as a way to fetch the 'storage' aspect of the field/att All other aspects of the process behave as it would for any other field. -This means in particular, after the field returns its value Settings will check the returned values +This means in particular, after the field returns its value BaseSettings will check the returned values type against the field's type_hint, and if needed will convert it if needed before handing it to the thing that originally requested the attribute/value. -It also means that, if the Settings instance has a plain-value directly assigned to it, +It also means that, if the BaseSettings instance has a plain-value directly assigned to it, that value will be used and the property will not be called at all (since no value needs to be 'retrieved'). @@ -445,58 +459,65 @@ Here is an example below. Notice I have a type-hint for the return-type of the p This is used if there is no type-annotation defined for the field. ```python -from xsettings import Settings +from xsettings import BaseSettings from decimal import Decimal -class MySettings(Settings): + +class MySettings(BaseSettings): @property def some_setting(self) -> Decimal: return "1.34" + assert MySettings.grab().some_setting == Decimal("1.34") ``` You can also define a type-annotation at the class level for the property like so: ```python -from xsettings import Settings +from xsettings import BaseSettings from decimal import Decimal -class MySettings(Settings): + +class MySettings(BaseSettings): # Does not matter if this is before or after the property, # Python stores type annotations in a separate area # vs normal class attribute values in Python. some_setting: Decimal - + @property def some_setting(self): return "1.34" + assert MySettings.grab().some_setting == Decimal("1.34") ``` ## Getter Properties Supported as a Forward/Lazy-Reference -You can get a forward-ref for a property field on a Settings class, -just like any other field attribute on a Settings class: +You can get a forward-ref for a property field on a BaseSettings class, +just like any other field attribute on a BaseSettings class: ```python -from xsettings import Settings +from xsettings import BaseSettings from decimal import Decimal -class MySettings(Settings): + +class MySettings(BaseSettings): # Does not matter if this is before or after the property, # Python stores type annotations in a separate area # vs normal class attribute values in Python. some_setting: Decimal - + @property def some_setting(self) -> Decimal: return "1.34" -class OtherSettings(Settings): + +class OtherSettings(BaseSettings): other_setting: str = MySettings.some_setting - + + assert OtherSettings.grab().other_setting == "1.34" ``` @@ -511,8 +532,10 @@ You can also specify a custom SettingsField and still use a property with it. Below is an example. See `xsettings.fields.SettingsField.getter` for more details. ```python -from xsettings import Settings, SettingsField -class MySettings(Settings): +from xsettings import BaseSettings, SettingsField + + +class MySettings(BaseSettings): # Does not matter if this is before or after the property, # Python stores type annotations in a separate area # vs normal class attribute values in @@ -523,6 +546,7 @@ class MySettings(Settings): def some_setting(self): return "1.36" + assert MySettings.grab().some_setting == "1.36" ``` @@ -533,7 +557,7 @@ You can't currently have a setter defined for a property on a class. This is something we CAN support without too much trouble, but have decided to put off for a future, if we end up wanting to do it. -If you define a setter property on a Settings class, it will currently raise an error. +If you define a setter property on a BaseSettings class, it will currently raise an error. # SettingsRetriever @@ -549,8 +573,10 @@ no other value is set or other non-default retrievers can't find a value by using a class-argument like so: ```python -from xsettings import Settings -class MySettings(Settings, default_retrievers=my_retriever): +from xsettings import BaseSettings + + +class MySettings(BaseSettings, default_retrievers=my_retriever): some_setting: str ``` @@ -564,8 +590,8 @@ to follow: 1. Value set directly on Setting-subclass instance. - via `MySettings.grab().some_setting = 'some-set-value` 2. Value set on a parent-instance in [`XContext.dependency_chain(for_type=SettingsSubclass)`](api/xinject/context.html#xinject.context.XContext.dependency_chain){target=_blank}. - - Settings can be used as context-managers via `with` and decorators `@`. - - When a new Settings instance is activated via decorator/with and previously active setting is in it's parent-chain + - BaseSettings can be used as context-managers via `with` and decorators `@`. + - When a new BaseSettings instance is activated via decorator/with and previously active setting is in it's parent-chain which is resolved via it's dependency-chain ([`XContext.grab().dependency_chain(for_type=SettingsSubclass)`](api/xinject/context.html#xinject.context.XContext.dependency_chain){target=_blank}). - Example: `with MySetting(some_setting='parent-value'):` @@ -573,13 +599,13 @@ to follow: 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. 2. Next, instance retriever(s) set directly on the Setting object that is being asked for its field value is checked. - 1. via [`Settings.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.add_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. Finally, any default-value for the field is consulted. - If the default-value is a property, or forward-ref then that is followed. - - ie: `Settings.some_attr = OtherSettings.another_attr_to_forward_ref_with` - - This ^ will change the default value for `some_attr` to a forward-ref from another Settings class. + - ie: `BaseSettings.some_attr = OtherSettings.another_attr_to_forward_ref_with` + - This ^ will change the default value for `some_attr` to a forward-ref from another BaseSettings class. 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). @@ -592,14 +618,17 @@ Checks self first, if not found will next check (returns a list of each instance currently in the dependency-chain, each one is checked in order; see link for details). ```python -from xsettings import Settings, SettingsField +from xsettings import BaseSettings, SettingsField + -def my_retriever(*, field: SettingsField, settings: Settings): +def my_retriever(*, field: SettingsField, settings: BaseSettings): return f"retrieved-{field.name}" -class MySettings(Settings, default_retrievers=my_retriever): + +class MySettings(BaseSettings, default_retrievers=my_retriever): some_setting: str - + + assert MySettings.grab().some_setting == 'retrieved-some_setting' ``` @@ -612,7 +641,7 @@ After the individual field retrievers are consulted, instance retrievers are che before finally checking the default-retrievers for the entire class. You can also add one or more retrievers to this `instance` of settings via the -[`Settings.add_instance_retrievers`](api/xsettings/settings.html#xsettings.settings.Settings.add_instance_retrievers){target=_blank} +[`BaseSettings.add_instance_retrievers`](api/xsettings/settings.html#xsettings.settings.Settings.add_instance_retrievers){target=_blank} method (won't modify default_retrievers for the entire class, only modifies this specific instance). They are checked in the order added. @@ -631,6 +660,17 @@ 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. +## Callable Defaults + +If a default value is callable, when a default value is needed during field value resolution, +it will be called without any arguments and the returned value will be used. + +Example: + +```python +# todo: Think about a mutable callable default (such as list) with this feature... +``` + # Things to Watch Out For - If a field has not type-hint, but does have a normal (non-property) default-value, diff --git a/tests/test_examples.py b/tests/test_examples.py index 35da6a8..7df392b 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -37,9 +37,9 @@ class MySettings(EnvVarSettings): converter=DBConfig.from_dict ) - # Settings subclasses are singleton-like dependencies that are + # BaseSettings subclasses are singleton-like dependencies that are # also injectables and lazily-created on first-use. - # YOu can use a special `Settings.grab()` class-method to + # YOu can use a special `BaseSettings.grab()` class-method to # get the current settings object. # # So you can grab the current MySettings object lazily via @@ -85,7 +85,7 @@ class MySettings(EnvVarSettings): # explicitly set to anything on settings object: assert my_settings.app_version == '1.2.3' - # Any Settings subclass can use dependency-injection: + # Any BaseSettings subclass can use dependency-injection: assert my_settings.token is None with MySettings(token='my-token'): @@ -107,7 +107,7 @@ class MySettings(EnvVarSettings): try: # If a setting is undefined and required (ie: not-optional), # and it was not set to anything nor is there a default or an env-var for it; - # Settings will raise an exception when getting it: + # BaseSettings will raise an exception when getting it: print(my_settings.api_endpoint_url) except SettingsValueError as e: assert True @@ -126,10 +126,10 @@ class MySettings(EnvVarSettings): def test_class_lazy_attr_forward_ref(): - from xsettings import Settings, EnvVarSettings + from xsettings import BaseSettings, EnvVarSettings import os - class MySettings(Settings): + class MySettings(BaseSettings): table_name: str MySettings.grab().table_name = "the-t-name" @@ -168,7 +168,7 @@ class MyEnvSettings(EnvVarSettings): assert MySettings.grab().table_name == 'env-table-name' # Example 3, default value of settings field can be a lazy-property-ref - class MyOtherSettings(Settings): + class MyOtherSettings(BaseSettings): my_setting_attr: str = MyEnvSettings.my_table_name my_other_settings = MyOtherSettings.proxy() @@ -179,9 +179,9 @@ class MyOtherSettings(Settings): def test_change_default_example(): - from xsettings import Settings, SettingsField + from xsettings import BaseSettings, SettingsField - class MySettings(Settings): + class MySettings(BaseSettings): a: int b: int = 1 @@ -190,7 +190,7 @@ class MySettings(Settings): # default/fallback value of `2`: MySettings.a = 2 - class MyOtherSettings(Settings): + class MyOtherSettings(BaseSettings): some_other_setting: str # You can also set a lazy-ref as setting field's @@ -205,10 +205,10 @@ class MyOtherSettings(Settings): def test_read_only_props_1(): - from xsettings import Settings + from xsettings import BaseSettings from decimal import Decimal - class MySettings(Settings): + class MySettings(BaseSettings): @property def some_setting(self) -> Decimal: return "1.34" @@ -217,10 +217,10 @@ def some_setting(self) -> Decimal: def test_read_only_props_2(): - from xsettings import Settings + from xsettings import BaseSettings from decimal import Decimal - class MySettings(Settings): + class MySettings(BaseSettings): # Does not matter if this is before or after the property, # Python stores type annotations in a separate area vs # normal class attribute values in Python. @@ -234,10 +234,10 @@ def some_setting(self): def test_forward_ref_example(): - from xsettings import Settings + from xsettings import BaseSettings from decimal import Decimal - class MySettings(Settings): + class MySettings(BaseSettings): # Does not matter if this is before or after the property, # Python stores type annotations in a separate area # vs normal class attribute values in Python. @@ -247,19 +247,19 @@ class MySettings(Settings): def some_setting(self) -> Decimal: return "1.34" - class OtherSettings(Settings): + class OtherSettings(BaseSettings): other_setting: str = MySettings.some_setting assert OtherSettings.grab().other_setting == "1.34" def test_index_doc_example(): - from xsettings import Settings, SettingsField + from xsettings import BaseSettings, SettingsField - def my_retriever(*, field: SettingsField, settings: Settings): + def my_retriever(*, field: SettingsField, settings: BaseSettings): return f"retrieved-{field.name}" - class MySettings(Settings, default_retrievers=my_retriever): + class MySettings(BaseSettings, default_retrievers=my_retriever): some_setting: str assert MySettings.grab().some_setting == 'retrieved-some_setting' diff --git a/tests/test_fields.py b/tests/test_fields.py index 0f7e7f8..905dc74 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -4,7 +4,7 @@ from xsettings.fields import SettingsField, generate_setting_fields from xsettings.retreivers import SettingsRetrieverProtocol -from xsettings import Settings +from xsettings import BaseSettings @pytest.mark.parametrize( @@ -54,7 +54,7 @@ def test_verify_retriever_on_property_field(): def prop_field(self) -> Optional[str]: return None - class TestClass(Settings): + class TestClass(BaseSettings): def method(self): pass @@ -69,7 +69,7 @@ def method(self): def test_verify_retriever_on_normal_field(): with pytest.raises(AssertionError, match='Invalid retriever for field .*some_field'): - class TestClass(Settings): + class TestClass(BaseSettings): def method(self): pass @@ -83,10 +83,10 @@ def my_property_field(self) -> str: def test_property_as_retreiver(): - def my_default_retreiver(*, field: SettingsField, settings: 'Settings'): + def my_default_retreiver(*, field: SettingsField, settings: 'BaseSettings'): return f"field.name={field.name}" - class TestSettings(Settings, default_retrievers=[my_default_retreiver]): + class TestSettings(BaseSettings, default_retrievers=[my_default_retreiver]): my_str_field: str @property @@ -101,7 +101,7 @@ def my_prop(self) -> str: def test_attrs_default_no_typehint(): - class TestClass(Settings): + class TestClass(BaseSettings): val = 1 fields = TestClass._setting_fields @@ -117,7 +117,7 @@ class TestClass(Settings): def test_attrs_default_with_typehint(): - class TestClass(Settings): + class TestClass(BaseSettings): val: str = 1 fields = TestClass._setting_fields @@ -133,7 +133,7 @@ class TestClass(Settings): def test_attrs_no_default(): - class TestClass(Settings): + class TestClass(BaseSettings): val: str fields = TestClass._setting_fields @@ -145,12 +145,12 @@ class TestClass(Settings): def test_attrs_merge(): class Retriever(SettingsRetrieverProtocol): - def get(self, field: SettingsField, *, settings: Settings) -> Any: + def get(self, field: SettingsField, *, settings: BaseSettings) -> Any: pass new_retriever = Retriever() - class TestClass(Settings): + class TestClass(BaseSettings): val: str = SettingsField( name="name", required="required", @@ -177,12 +177,12 @@ class TestClass(Settings): def test_retriever_type(): with pytest.raises(AssertionError): - class TestClass(Settings): + class TestClass(BaseSettings): val: str = SettingsField(retriever="abc") def test_with_generic_typehint(): - class SomeSettings(Settings): + class SomeSettings(BaseSettings): generic_settings_field: Sequence[str] SomeSettings.grab().generic_settings_field = ['a', '1', '!'] diff --git a/tests/test_settings.py b/tests/test_settings.py index a1ab7eb..ce6343b 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -10,13 +10,13 @@ from xsettings.env_settings import EnvVarSettings from xsettings.fields import SettingsConversionError, _PropertyRetriever -from xsettings.settings import Settings, SettingsField +from xsettings.settings import BaseSettings, SettingsField from xsettings.errors import SettingsValueError from xsettings.retreivers import SettingsRetrieverProtocol def test_set_default_value_after_settings_subclass_created(): - class MySettings(Settings): + class MySettings(BaseSettings): my_str: str my_settings = MySettings.proxy() @@ -27,7 +27,7 @@ class MySettings(Settings): MySettings.my_str = 'default-value' assert my_settings.my_str == 'default-value' - class OtherSettings(Settings): + class OtherSettings(BaseSettings): other_str: str MySettings.my_str = OtherSettings.other_str @@ -42,14 +42,14 @@ def test_use_property_on_settings_subclass(): value_to_retrieve = "RetrievedValue" class MyRetriever(SettingsRetrieverProtocol): - def __call__(self, *, field: SettingsField, settings: 'Settings') -> Any: + def __call__(self, *, field: SettingsField, settings: 'BaseSettings') -> Any: nonlocal value_to_retrieve return value_to_retrieve - class MyForwardSettings(Settings): + class MyForwardSettings(BaseSettings): my_forwarded_field: str = "my_forwarded_field-value" - class MySettings(Settings, default_retrievers=MyRetriever()): + class MySettings(BaseSettings, default_retrievers=MyRetriever()): my_field = "my_field-value" @property @@ -83,7 +83,7 @@ def test_default_converters(): def my_converter(value): return Decimal(1.654) - class MySettings(Settings): + class MySettings(BaseSettings): my_bool: bool my_date: dt.date = "2023-03-04" my_datetime: dt.datetime = "2020-01-09T12:00:02" @@ -104,7 +104,7 @@ class MySettings(Settings): def test_defaults(): - class MySettings(Settings): + class MySettings(BaseSettings): no_default: int default_convert_str_to_int: int = "3" default_no_conversion_needed: int = 6 @@ -124,7 +124,7 @@ class MySettings(Settings): def test_conversion_returns_none(): - class MySettings(Settings): + class MySettings(BaseSettings): requried: str = SettingsField(default_value=3, converter=lambda x: None) not_requried: str = SettingsField( default_value=3, converter=lambda x: None, required=False @@ -141,7 +141,7 @@ class MyEnum(Enum): FIVE = 5 SIX = 6 - class MySettings(Settings): + class MySettings(BaseSettings): enum: MyEnum enum2: MyEnum = SettingsField(converter=MyEnum) @@ -153,7 +153,7 @@ class MySettings(Settings): def test_field_in_class_reuse(): - class MySettings(Settings): + class MySettings(BaseSettings): a: int = 1 class MyClass: @@ -164,7 +164,7 @@ class MyClass: def test_field_overwriting(): - class MySettings(Settings): + class MySettings(BaseSettings): a: str class MyEnvSettings(EnvVarSettings): @@ -200,7 +200,7 @@ def test_field_overwriting_classlevel(): class KevinEnvSettings(EnvVarSettings): b: int - class KevinSettings(Settings): + class KevinSettings(BaseSettings): a: str = KevinEnvSettings.b my_env_settings = KevinEnvSettings.grab() @@ -217,7 +217,7 @@ class KevinSettings(Settings): def test_class_field_overwrite(): - class MySettings(Settings): + class MySettings(BaseSettings): a: str with pytest.raises(AttributeError): @@ -225,7 +225,7 @@ class MySettings(Settings): def test_settings_inheritance(): - class MySettings(Settings): + class MySettings(BaseSettings): a: int = 1 class MySubSettings(MySettings): @@ -246,7 +246,7 @@ class MySubSettings(MySettings): def test_property_as_forward_ref_works_via_return_type(): did_call_property = False - class ASettings(Settings): + class ASettings(BaseSettings): other_setting = 3 @property @@ -256,7 +256,7 @@ def prop_to_forward_ref(self) -> str: assert isinstance(self, ASettings) return self.other_setting - class BSettings(Settings): + class BSettings(BaseSettings): b_settings_forward_from_a: Decimal = ASettings.prop_to_forward_ref assert ASettings.grab().other_setting == 3 @@ -289,14 +289,14 @@ def prop_to_forward_ref(self): assert isinstance(self, ASettings) return self.other_setting - class ASettings(Settings): + class ASettings(BaseSettings): other_setting = 3 # Also testing if annotation if defined separately still works vs a property/default-value. prop_to_forward_ref = AProperty.prop_to_forward_ref prop_to_forward_ref: str - class BSettings(Settings): + class BSettings(BaseSettings): b_settings_forward_from_a: Decimal = ASettings.prop_to_forward_ref assert ASettings.grab().other_setting == 3 @@ -322,7 +322,7 @@ class BSettings(Settings): def test_property_field_detects_no_type_hint(): with pytest.raises(AssertionError, match='Must have type-hint for field'): - class ASettings(Settings): + class ASettings(BaseSettings): # We specify no type-annotation or return-type for property, # we should get an error while class is being constructed. @property @@ -332,7 +332,7 @@ def prop_to_forward_ref(self): def test_property_field_detects_setter_being_used(): with pytest.raises(AssertionError, match='You can only use read-only properties'): - class ASettings(Settings): + class ASettings(BaseSettings): @property def random_property_field(self): return None @@ -347,7 +347,7 @@ def random_property_field(self, value): def test_settings_inheritance_without_fields_allowed(): - class MySettings(Settings): + class MySettings(BaseSettings): # Methods don't have fields generated for them. def test_method(self) -> int: return 1 @@ -387,7 +387,7 @@ class MySubSettings(MySettings): def test_source_class(): - class MySettings(Settings): + class MySettings(BaseSettings): a: int field: SettingsField = MySettings._setting_fields["a"] @@ -395,7 +395,7 @@ class MySettings(Settings): def test_new_fields(): - class MySettings(Settings): + class MySettings(BaseSettings): a: int my_settings = MySettings() @@ -412,7 +412,7 @@ class MySettings(Settings): def test_property_that_returns_diffrent_type(): - class MySettings(Settings): + class MySettings(BaseSettings): # Does not matter if this is before or after the property, # Python stores type annotations in a separate area vs normal class attribute values in # Python. @@ -426,7 +426,7 @@ def some_setting(self): def test_property_with_custom_field(): - class MySettings(Settings): + class MySettings(BaseSettings): # Does not matter if this is before or after the property, # Python stores type annotations in a separate area vs normal class attribute values in # Python. @@ -440,7 +440,7 @@ def some_setting(self): def test_converter_error_has_good_message(): - class TestSettings(Settings): + class TestSettings(BaseSettings): # Default value is a blank string (can't convert to int directly). some_int_setting: int = '' @@ -460,12 +460,12 @@ class PlainInterface: another_attr = 2 - class PlainSettings(Settings): + class PlainSettings(BaseSettings): str_attr: str = "my-str" bool_attr: bool = False - class MySettings(Settings, PlainInterface): - # Make them fields in our Settings subclass, default value to another settings class. + class MySettings(BaseSettings, PlainInterface): + # Make them fields in our BaseSettings subclass, default value to another settings class. some_default_attr: str = PlainSettings.str_attr some_other_attr: bool = PlainSettings.bool_attr another_attr: int @@ -479,13 +479,13 @@ class MySettings(Settings, PlainInterface): # There is a retrieved/default value, so that's used over the superclass value. assert my_settings.some_other_attr is False - # If Settings can't get field value, it will get it from super-class. + # If BaseSettings can't get field value, it will get it from super-class. assert my_settings.another_attr == 2 def test_inherit_settings_fields_from_parent_and_override_in_child(): - class MyParentSettings(Settings): - # Make them fields in our Settings subclass, default value to another settings class. + class MyParentSettings(BaseSettings): + # Make them fields in our BaseSettings subclass, default value to another settings class. a: str b: bool = SettingsField(name="b_alt_name") c: int @@ -514,18 +514,18 @@ class MyChildSettings(MyParentSettings): def test_inherit_multiple_retrievers(): - def r1(*, field: SettingsField, settings: Settings): + def r1(*, field: SettingsField, settings: BaseSettings): if field.name == 'a': return 'a-val' return None - def r2(*, field: SettingsField, settings: Settings): + def r2(*, field: SettingsField, settings: BaseSettings): if field.name == 'b_alt_name': return True return None - class MyParentSettings(Settings, default_retrievers=[r1, r2]): - # Make them fields in our Settings subclass, default value to another settings class. + class MyParentSettings(BaseSettings, default_retrievers=[r1, r2]): + # Make them fields in our BaseSettings subclass, default value to another settings class. a: str b: bool = SettingsField(name="b_alt_name") c: int @@ -557,11 +557,11 @@ class MyChildSettings(MyParentSettings): def test_grab_setting_values_from_parent_dependency_instances(): - def r1(*, field: SettingsField, settings: Settings): + def r1(*, field: SettingsField, settings: BaseSettings): return 2 if field.name == 'c' else 'str-val' - class MySettings(Settings, default_retrievers=[r1]): - # Make them fields in our Settings subclass, default value to another settings class. + class MySettings(BaseSettings, default_retrievers=[r1]): + # Make them fields in our BaseSettings subclass, default value to another settings class. a: str b: str c: int @@ -584,13 +584,13 @@ class MySettings(Settings, default_retrievers=[r1]): assert my_settings.b == 'str-val' assert my_settings.c == 2 - def r2(*, field: SettingsField, settings: Settings): + def r2(*, field: SettingsField, settings: BaseSettings): if field.name == 'b': return 'str-val-r2' with MySettings(r2): # These values come from the `r2` retriever, which should be checked first - # before the default-retriever at the Settings class level (ie: r1 further above). + # 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 diff --git a/xsettings/__init__.py b/xsettings/__init__.py index 4d1bf3b..450e5f6 100644 --- a/xsettings/__init__.py +++ b/xsettings/__init__.py @@ -1,3 +1,11 @@ -from .settings import Settings +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`. +""" diff --git a/xsettings/default_converters.py b/xsettings/default_converters.py index 7d3b1b4..32653fa 100644 --- a/xsettings/default_converters.py +++ b/xsettings/default_converters.py @@ -26,7 +26,7 @@ def to_datetime(value): return dt.datetime(value.year, value.month, value.day, tzinfo=tz.tzutc()) raise ValueError( - f"Tried to convert a datetime from unsupported Settings value ({value})." + f"Tried to convert a datetime from unsupported BaseSettings value ({value})." ) diff --git a/xsettings/env_settings.py b/xsettings/env_settings.py index 5ad96a5..03d8e2b 100644 --- a/xsettings/env_settings.py +++ b/xsettings/env_settings.py @@ -1,10 +1,10 @@ -from xsettings.settings import Settings +from xsettings.settings import BaseSettings from .retreivers import EnvVarRetriever -class EnvVarSettings(Settings, default_retrievers=EnvVarRetriever()): +class EnvVarSettings(BaseSettings, default_retrievers=EnvVarRetriever()): """ - Base subclass of `xsettings.settings.Settings` with the default retriever + Base subclass of `xsettings.settings.BaseSettings` with the default retriever set as the `xsettings.retrievers.EnvVarRetriever`. This means when a settings field is defined without a retriever diff --git a/xsettings/fields.py b/xsettings/fields.py index 0bbbd2c..10b60db 100644 --- a/xsettings/fields.py +++ b/xsettings/fields.py @@ -10,7 +10,7 @@ from xsentinels import unwrap_union, Default if TYPE_CHECKING: - from .settings import Settings + from .settings import BaseSettings from .retreivers import SettingsRetrieverProtocol T = TypeVar("T") @@ -35,10 +35,10 @@ def __get__(self, owner_self, owner_cls) -> T: class _PropertyRetriever: """ Special case, internally used retriever only assigned to individual fields - (and not as a default retriever for the entire Settings subclass). + (and not as a default retriever for the entire BaseSettings subclass). - What is used to wrap a `@property` on a Settings subclass. - We don't use the default retriever for any defined properties on a Settings subclass, + What is used to wrap a `@property` on a BaseSettings subclass. + We don't use the default retriever for any defined properties on a BaseSettings subclass, we instead use `PropertyRetriever`; as the property it's self is considered the 'retriever'. Will check the property getter function to retrieve the value by calling its @@ -51,7 +51,7 @@ class _PropertyRetriever: def __init__(self, property_retriever: property): self.property_retriever = property_retriever - def __call__(self, *, field: 'SettingsField', settings: 'Settings') -> Any: + def __call__(self, *, field: 'SettingsField', settings: 'BaseSettings') -> Any: return self.property_retriever.__get__(settings, type(settings)) @@ -63,7 +63,7 @@ class SettingsField: Example Use Case: - For plain Settings classes, name is not really used. + For plain BaseSettings classes, name is not really used. But it can be useful in custom/special retrievers. An example of such a one is ConfigSettings/ConfigRetriever. @@ -76,10 +76,10 @@ class SettingsField: is different then the one used to retrieve the values. """ - source_class: 'Type[Settings]' = None + source_class: 'Type[BaseSettings]' = None """ For debug purposes only. Will be set when the class level SettingsField is created. It is a - way to get back to the source Settings class. + way to get back to the source BaseSettings class. It's positioned just after `name` so it's printed earlier in a log-line. """ @@ -98,7 +98,7 @@ class SettingsField: If `required` is set to False, a None will be returned instead of raising an exception. This here in the dataclass defaults to None so we can detect if user has set this or not. - When fields are finalized into a Settings subclass, and this is still a None, + When fields are finalized into a BaseSettings subclass, and this is still a None, we will determine the `required` value like so: if field-type-hint is wrapped in an Optional, ie: `Optional[str]`, @@ -135,8 +135,8 @@ class SettingsField: System will try this retriever first (if set to something), before trying other retrievers such as instance-retrievers - `xsettings.settings.Settings.add_instance_retrievers` - or default-retrievers `xsettings.settings.Settings.__init_subclass__`. + `xsettings.settings.BaseSettings.add_instance_retrievers` + or default-retrievers `xsettings.settings.BaseSettings.__init_subclass__`. See those links for more details (such as how dependency-chain and mro parents are resolved when looking for other retrievers). @@ -152,7 +152,7 @@ class SettingsField: This field settings defaults to whatever is assigned to the class-attribute: ```python - class MySettings(Settings): + class MySettings(BaseSettings): my_attribute_with_default_value: str = "some-default-value" ``` @@ -163,7 +163,7 @@ class MySettings(Settings): (such as ConfigRetriever from xyn-config). The default value can also be a property object - (such as forward-reference from another Settings class). + (such as forward-reference from another BaseSettings class). If it's a property object, we will ask the object for it's property value and use that for the default-value when needed. @@ -204,7 +204,7 @@ def getter(self): But, if you NEED to customize the Field object, this is where the `.getter` on SettingsField becomes handy. - >>> class MySettings(Settings): + >>> class MySettings(BaseSettings): ... ... # You can easily setup a field like normal, and then use getter to setup ... # the getter function for the field. @@ -253,7 +253,7 @@ def merge(self, override: "SettingsField"): self.default_value = copy(override.default_value) return self - def retrieve_value(self, *, settings: 'Settings'): + def retrieve_value(self, *, settings: 'BaseSettings'): """Convenience method for getting the value from the retriever.""" return self.retriever(self, settings=settings) @@ -455,7 +455,7 @@ def merge_field(attr_key, merge_field): if field.type_hint in (None, Any, type(None)): raise AssertionError( f"Must have type-hint for field ({field}). This may be because there is a " - f"property defined on Settings subclass that has no return type-hint," + f"property defined on BaseSettings subclass that has no return type-hint," f"or type annotation for it somewhere else in the class. " f"Or it could be the type-hint is `Any` or `NoneType` which are also " f"not supported. Or there may be some other reason there is no type hint. " @@ -524,7 +524,7 @@ def _add_field_default_from_attrs(class_attrs: Dict[str, Any], merge_field): # TODO: Support property setters someday. if v.fset or v.fdel: raise AssertionError( - "Settings and SettingsField's currently don't have the ability to " + "BaseSettings and SettingsField's currently don't have the ability to " "support a property setter/deleter. You can only use read-only properties " "with them. However, you can set a value on a settings field that has a " "property getter, and that value will be used like you would expect. " diff --git a/xsettings/retreivers.py b/xsettings/retreivers.py index c695086..38af946 100644 --- a/xsettings/retreivers.py +++ b/xsettings/retreivers.py @@ -1,6 +1,6 @@ from xsentinels.sentinel import Sentinel from typing import Any, Protocol, Callable -from .settings import SettingsField, Settings +from .settings import SettingsField, BaseSettings import os # Tell pdoc3 to document the normally private method __call__. @@ -14,9 +14,9 @@ class SettingsRetrieverProtocol(Protocol): The purpose of the base SettingsRetrieverProtocol is to define the base-interface for retrieving settings values. - The retriever can be any callable, by default `xsettings.settings.Settings` will + The retriever can be any callable, by default `xsettings.settings.BaseSettings` will not use a default retriever; normally a subclass or some sort of generic - base-subclass of `xsettings.settings.Settings` will be used to specify a default + base-subclass of `xsettings.settings.BaseSettings` will be used to specify a default retriever to use. A retriever can also be specified per-field via `xsettings.fields.SettingsField.retriever`. @@ -25,7 +25,7 @@ class SettingsRetrieverProtocol(Protocol): is the one that is used. You can also add one or more retrievers to this `instance` of settings via the - `xsettings.setting.Settings.add_instance_retrievers` method + `xsettings.setting.BaseSettings.add_instance_retrievers` method (won't modify default_retrievers for the entire class, only modifies this specific instance). .. note:: As a side-note, values set directly on Setting instances are first checked for and @@ -54,9 +54,9 @@ class SettingsRetrieverProtocol(Protocol): before checking any super-classes for default-retrievers. """ - def __call__(self, *, field: SettingsField, settings: Settings) -> Any: + def __call__(self, *, field: SettingsField, settings: BaseSettings) -> Any: """ - This is how the Settings field, when retrieving its value, will call us. + This is how the BaseSettings field, when retrieving its value, will call us. You must override this (or simply use a normal function with the same parameters). This convention gives flexibility: It allows simple methods to be retrievers, @@ -64,7 +64,7 @@ def __call__(self, *, field: SettingsField, settings: Settings) -> Any: Args: field: Field we need to retrieve. - settings: Related Settings object that has the field we are retrieving. + settings: Related BaseSettings object that has the field we are retrieving. Returns: Retrieved value, or None if no value can be found. By default, we return `None` (as we are a basic/abstract retriever) @@ -76,7 +76,7 @@ def __call__(self, *, field: SettingsField, settings: Settings) -> Any: class EnvVarRetriever(SettingsRetrieverProtocol): """ Used to """ - def __call__(self, *, field: SettingsField, settings: 'Settings') -> Any: + def __call__(self, *, field: SettingsField, settings: 'BaseSettings') -> Any: environ = os.environ # First try to get field using the same case as the original field name: diff --git a/xsettings/settings.py b/xsettings/settings.py index 8d2a399..0809b07 100644 --- a/xsettings/settings.py +++ b/xsettings/settings.py @@ -1,6 +1,6 @@ """ -See doc-comments for `Settings` below. +See doc-comments for `BaseSettings` below. """ @@ -26,25 +26,25 @@ class _SettingsMeta(type): - """Represents the class-type instance/obj of the `Settings` class. + """Represents the class-type instance/obj of the `BaseSettings` class. Any attributes in this object will be class-level attributes of - a class or subclass of `Settings`. + a class or subclass of `BaseSettings`. - ie: A `_SettingsMeta` instance is created each time a new Settings or Settings subclass + ie: A `_SettingsMeta` instance is created each time a new BaseSettings or BaseSettings subclass type/class is created (it represents the class/type its self). """ - # This will be a class-attributes on the normal `Settings` class/subclasses. + # This will be a class-attributes on the normal `BaseSettings` class/subclasses. _setting_fields: Dict[str, SettingsField] _default_retrievers: 'List[SettingsRetrieverProtocol]' _there_is_plain_superclass: bool - """ There is some other superclass, other then Settings/object/Dependency. """ + """ There is some other superclass, other then BaseSettings/object/Dependency. """ - _setting_subclasses_in_mro: 'List[Type[Settings]]' + _setting_subclasses_in_mro: 'List[Type[BaseSettings]]' """ - Includes self/cls plus all superclasses who are Settings subclasses in __mro__ - (but not Settings it's self); in the same order that they appears in __mro__. + Includes self/cls plus all superclasses who are BaseSettings subclasses in __mro__ + (but not BaseSettings it's self); in the same order that they appears in __mro__. """ def __new__( @@ -58,11 +58,11 @@ def __new__( **kwargs, ): """ - The instance of `mcls` is a Settings class or subclass - (not Settings object, the class its self). + The instance of `mcls` is a BaseSettings class or subclass + (not BaseSettings object, the class its self). Objective in this method is to create a set of SettingsField object(s) for use by the new - Settings subclass, and set their default-settings correctly if the user did not + BaseSettings subclass, and set their default-settings correctly if the user did not provide an explict setting. Args: @@ -74,7 +74,7 @@ def __new__( no set retriever (ie: not set directly be user). This can be passed in like so: - `class MySettings(Settings, default_retriever=...)` + `class MySettings(BaseSettings, default_retriever=...)` **kwargs: Any extra arguments get supplied to the super-class-type. """ @@ -84,7 +84,7 @@ def __new__( attrs['_default_retrievers'] = list(xloop(default_retrievers)) if skip_field_generation: - # Skip doing anything special with any Settings classes created in our/this module; + # Skip doing anything special with any BaseSettings classes created in our/this module; # They are abstract classes and are need to be sub-classed to do anything with them. attrs['_setting_fields'] = {} cls = super().__new__(mcls, name, bases, attrs, **kwargs) # noqa @@ -102,17 +102,17 @@ def __new__( attrs['_setting_subclasses_in_mro'] = setting_subclasses_in_mro for c in types_in_mro: # Skip the ones that are always present, and don't need to examined... - if c is Settings: + if c is BaseSettings: continue if c is object: continue if c is Dependency: continue - # We want to know the order if aby Settings subclasses that we are inheriting from. + # We want to know the order if aby BaseSettings subclasses that we are inheriting from. # Also want to know if there are any plain/non-setting classes in our parent hierarchy # (that are not object/Dependency, as they both will always be present). - if issubclass(c, Settings): + if issubclass(c, BaseSettings): setting_subclasses_in_mro.append(c) else: attrs['_there_is_plain_superclass'] = True @@ -133,7 +133,7 @@ def __new__( for k in setting_fields.keys(): attrs.pop(k, None) - # This creates the new Settings class/subclass. + # This creates the new BaseSettings class/subclass. cls = super().__new__(mcls, name, bases, attrs, **kwargs) # Insert newly crated class into top of its setting subclasses list. @@ -153,7 +153,7 @@ def __getattr__(self, key: str) -> SettingsClassProperty: Example: - >>> class MySettings(Settings): + >>> class MySettings(BaseSettings): ... my_url_setting: str >>> >>> class SomeClass: @@ -169,12 +169,12 @@ def __getattr__(self, key: str) -> SettingsClassProperty: c: _SettingsMeta if key in c._setting_fields: break - # We got to `Settings` without finding anything, Settings has no fields, + # We got to `BaseSettings` without finding anything, BaseSettings has no fields, # raise exception about how we could not find field. - if c is Settings: + if c is BaseSettings: raise AttributeError( f"Have no class-attribute or defined SettingsField for " - f"attribute name ({key}) on Settings subclass ({self})." + f"attribute name ({key}) on BaseSettings subclass ({self})." ) @SettingsClassProperty @@ -199,17 +199,17 @@ def __setattr__(self, key: str, value: Union[SettingsField, Any]): c: _SettingsMeta if field := c._setting_fields.get(key): break - # We got to `Settings` without finding anything, Settings has no fields, + # We got to `BaseSettings` without finding anything, BaseSettings has no fields, # give-up searching for field. - if c is Settings: + if c is BaseSettings: break if not field: - # Right now we don't support making new SettingField's after the Settings subclass + # Right now we don't support making new SettingField's after the BaseSettings subclass # has been created. We could decide to do that in the future, but for now we # are keeping things simpler. raise AttributeError( - f"Setting new fields on Settings subclass unsupported currently, attempted to " + f"Setting new fields on BaseSettings subclass unsupported currently, attempted to " f"set key ({key}) with value ({value})." ) @@ -217,13 +217,13 @@ def __setattr__(self, key: str, value: Union[SettingsField, Any]): field.default_value = value -class Settings( +class BaseSettings( Dependency, metaclass=_SettingsMeta, default_retrievers=[], - # Settings has no fields, it's a special abstract-type of class skip field generation. - # You should never use this option in a Settings subclass. + # 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 ): """ @@ -238,7 +238,7 @@ class Settings( 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(Settings): + >>> class MySettings(BaseSettings): ... name: type_hint = default_value A default `SettingsField` will be configured using the name, type_hint, and default_value as @@ -255,22 +255,22 @@ class Settings( Example of various ways to allocate a SettingsField on a Settings subclass: - >>> class MySettings(Settings): + >>> class MySettings(BaseSettings): ... setting_1: int Allocates >>> SettingsField(name="setting_1", type_hint=int, resolver=SettingsResolver) - >>> class MySettings(Settings): + >>> class MySettings(BaseSettings): ... setting_1: int = 3 Allocates >>> SettingsField(name="setting_1", type_hint=int, resolver=SettingsResolver, default_value=3) - >>> class MySettings(Settings): + >>> class MySettings(BaseSettings): ... setting_1 = 3 Allocates >>> SettingsField(name="setting_1", type_hint=int, resolver=SettingsResolver, default_value=3) - >>> class MySettings(Settings): + >>> class MySettings(BaseSettings): ... setting_1: int = SettingsField(name="other", required=False) Allocates >>> SettingsField(name="other", type_hint=int, resolver=SettingsResolver, required=False) @@ -283,9 +283,9 @@ class Settings( Examples of how you might use this - >>> class MySettings(Settings): + >>> class MySettings(BaseSettings): ... my_url_setting: str - >>> class MySubSettings(Settings): + >>> class MySubSettings(BaseSettings): ... my_field: str >>> class SomeClass: ... some_attr = MySettings.my_url_setting @@ -405,14 +405,14 @@ def __getattribute__(self, key): for c in cls._setting_subclasses_in_mro: c: _SettingsMeta # todo: use isinstance? - if c is Settings: - # We got to the 'Settings' base-class it's self, no need to go any further. + 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: Settings = self): + def get_normal_value(obj: BaseSettings = self): nonlocal value nonlocal attr_error @@ -507,7 +507,14 @@ def get_normal_value(obj: Settings = self): return value -def _resolve_field_value(settings: Settings, field: SettingsField, key: str, value: Any): +Settings = BaseSettings +""" +Deprecated; Use `BaseSettings` instead. + +Here for backwards compatability, renamed original class from `Settings` to `BaseSettings`. +""" + +def _resolve_field_value(settings: BaseSettings, field: SettingsField, key: str, value: Any): cls = type(settings) # If we have a field, and current value is Default, or we got AttributeError, @@ -535,6 +542,8 @@ def self_and_parent_retrievers(): if value is None: value = field.default_value + if callable(value): + value = value() if value is None: if field.required: