diff --git a/poetry.lock b/poetry.lock index c06f0df..3da470d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -178,14 +178,14 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.29" +version = "3.1.30" description = "GitPython is a python library used to interact with Git repositories" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "GitPython-3.1.29-py3-none-any.whl", hash = "sha256:41eea0deec2deea139b459ac03656f0dd28fc4a3387240ec1d3c259a2c47850f"}, - {file = "GitPython-3.1.29.tar.gz", hash = "sha256:cc36bfc4a3f913e66805a28e84703e419d9c264c1077e537b54f0e1af85dbefd"}, + {file = "GitPython-3.1.30-py3-none-any.whl", hash = "sha256:cd455b0000615c60e286208ba540271af9fe531fa6a87cc590a7298785ab2882"}, + {file = "GitPython-3.1.30.tar.gz", hash = "sha256:769c2d83e13f5d938b7688479da374c4e3d49f71549aaf462b646db9602ea6f8"}, ] [package.dependencies] @@ -205,14 +205,14 @@ files = [ [[package]] name = "importlib-metadata" -version = "5.2.0" +version = "6.0.0" description = "Read metadata from Python packages" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "importlib_metadata-5.2.0-py3-none-any.whl", hash = "sha256:0eafa39ba42bf225fc00e67f701d71f85aead9f878569caf13c3724f704b970f"}, - {file = "importlib_metadata-5.2.0.tar.gz", hash = "sha256:404d48d62bba0b7a77ff9d405efd91501bef2e67ff4ace0bed40a0cf28c3c7cd"}, + {file = "importlib_metadata-6.0.0-py3-none-any.whl", hash = "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad"}, + {file = "importlib_metadata-6.0.0.tar.gz", hash = "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d"}, ] [package.dependencies] @@ -535,14 +535,14 @@ files = [ [[package]] name = "pygments" -version = "2.13.0" +version = "2.14.0" description = "Pygments is a syntax highlighting package written in Python." category = "dev" optional = false python-versions = ">=3.6" files = [ - {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, - {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, + {file = "Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"}, + {file = "Pygments-2.14.0.tar.gz", hash = "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297"}, ] [package.extras] @@ -811,40 +811,40 @@ test = ["coverage", "flake8 (>=3.7)", "mypy", "pretend", "pytest"] [[package]] name = "watchdog" -version = "2.2.0" +version = "2.2.1" description = "Filesystem events monitoring" category = "dev" optional = false python-versions = ">=3.6" files = [ - {file = "watchdog-2.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ed91c3ccfc23398e7aa9715abf679d5c163394b8cad994f34f156d57a7c163dc"}, - {file = "watchdog-2.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:76a2743402b794629a955d96ea2e240bd0e903aa26e02e93cd2d57b33900962b"}, - {file = "watchdog-2.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:920a4bda7daa47545c3201a3292e99300ba81ca26b7569575bd086c865889090"}, - {file = "watchdog-2.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ceaa9268d81205876bedb1069f9feab3eccddd4b90d9a45d06a0df592a04cae9"}, - {file = "watchdog-2.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1893d425ef4fb4f129ee8ef72226836619c2950dd0559bba022b0818c63a7b60"}, - {file = "watchdog-2.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9e99c1713e4436d2563f5828c8910e5ff25abd6ce999e75f15c15d81d41980b6"}, - {file = "watchdog-2.2.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a5bd9e8656d07cae89ac464ee4bcb6f1b9cecbedc3bf1334683bed3d5afd39ba"}, - {file = "watchdog-2.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3a048865c828389cb06c0bebf8a883cec3ae58ad3e366bcc38c61d8455a3138f"}, - {file = "watchdog-2.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e722755d995035dd32177a9c633d158f2ec604f2a358b545bba5bed53ab25bca"}, - {file = "watchdog-2.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:af4b5c7ba60206759a1d99811b5938ca666ea9562a1052b410637bb96ff97512"}, - {file = "watchdog-2.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:619d63fa5be69f89ff3a93e165e602c08ed8da402ca42b99cd59a8ec115673e1"}, - {file = "watchdog-2.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1f2b0665c57358ce9786f06f5475bc083fea9d81ecc0efa4733fd0c320940a37"}, - {file = "watchdog-2.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:441024df19253bb108d3a8a5de7a186003d68564084576fecf7333a441271ef7"}, - {file = "watchdog-2.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1a410dd4d0adcc86b4c71d1317ba2ea2c92babaf5b83321e4bde2514525544d5"}, - {file = "watchdog-2.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:28704c71afdb79c3f215c90231e41c52b056ea880b6be6cee035c6149d658ed1"}, - {file = "watchdog-2.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2ac0bd7c206bb6df78ef9e8ad27cc1346f2b41b1fef610395607319cdab89bc1"}, - {file = "watchdog-2.2.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:27e49268735b3c27310883012ab3bd86ea0a96dcab90fe3feb682472e30c90f3"}, - {file = "watchdog-2.2.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:2af1a29fd14fc0a87fb6ed762d3e1ae5694dcde22372eebba50e9e5be47af03c"}, - {file = "watchdog-2.2.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:c7bd98813d34bfa9b464cf8122e7d4bec0a5a427399094d2c17dd5f70d59bc61"}, - {file = "watchdog-2.2.0-py3-none-manylinux2014_i686.whl", hash = "sha256:56fb3f40fc3deecf6e518303c7533f5e2a722e377b12507f6de891583f1b48aa"}, - {file = "watchdog-2.2.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:74535e955359d79d126885e642d3683616e6d9ab3aae0e7dcccd043bd5a3ff4f"}, - {file = "watchdog-2.2.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:cf05e6ff677b9655c6e9511d02e9cc55e730c4e430b7a54af9c28912294605a4"}, - {file = "watchdog-2.2.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:d6ae890798a3560688b441ef086bb66e87af6b400a92749a18b856a134fc0318"}, - {file = "watchdog-2.2.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e5aed2a700a18c194c39c266900d41f3db0c1ebe6b8a0834b9995c835d2ca66e"}, - {file = "watchdog-2.2.0-py3-none-win32.whl", hash = "sha256:d0fb5f2b513556c2abb578c1066f5f467d729f2eb689bc2db0739daf81c6bb7e"}, - {file = "watchdog-2.2.0-py3-none-win_amd64.whl", hash = "sha256:1f8eca9d294a4f194ce9df0d97d19b5598f310950d3ac3dd6e8d25ae456d4c8a"}, - {file = "watchdog-2.2.0-py3-none-win_ia64.whl", hash = "sha256:ad0150536469fa4b693531e497ffe220d5b6cd76ad2eda474a5e641ee204bbb6"}, - {file = "watchdog-2.2.0.tar.gz", hash = "sha256:83cf8bc60d9c613b66a4c018051873d6273d9e45d040eed06d6a96241bd8ec01"}, + {file = "watchdog-2.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a09483249d25cbdb4c268e020cb861c51baab2d1affd9a6affc68ffe6a231260"}, + {file = "watchdog-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5100eae58133355d3ca6c1083a33b81355c4f452afa474c2633bd2fbbba398b3"}, + {file = "watchdog-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e618a4863726bc7a3c64f95c218437f3349fb9d909eb9ea3a1ed3b567417c661"}, + {file = "watchdog-2.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:102a60093090fc3ff76c983367b19849b7cc24ec414a43c0333680106e62aae1"}, + {file = "watchdog-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:748ca797ff59962e83cc8e4b233f87113f3cf247c23e6be58b8a2885c7337aa3"}, + {file = "watchdog-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ccd8d84b9490a82b51b230740468116b8205822ea5fdc700a553d92661253a3"}, + {file = "watchdog-2.2.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6e01d699cd260d59b84da6bda019dce0a3353e3fcc774408ae767fe88ee096b7"}, + {file = "watchdog-2.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8586d98c494690482c963ffb24c49bf9c8c2fe0589cec4dc2f753b78d1ec301d"}, + {file = "watchdog-2.2.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:adaf2ece15f3afa33a6b45f76b333a7da9256e1360003032524d61bdb4c422ae"}, + {file = "watchdog-2.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:83a7cead445008e880dbde833cb9e5cc7b9a0958edb697a96b936621975f15b9"}, + {file = "watchdog-2.2.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8ac23ff2c2df4471a61af6490f847633024e5aa120567e08d07af5718c9d092"}, + {file = "watchdog-2.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d0f29fd9f3f149a5277929de33b4f121a04cf84bb494634707cfa8ea8ae106a8"}, + {file = "watchdog-2.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:967636031fa4c4955f0f3f22da3c5c418aa65d50908d31b73b3b3ffd66d60640"}, + {file = "watchdog-2.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:96cbeb494e6cbe3ae6aacc430e678ce4b4dd3ae5125035f72b6eb4e5e9eb4f4e"}, + {file = "watchdog-2.2.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:61fdb8e9c57baf625e27e1420e7ca17f7d2023929cd0065eb79c83da1dfbeacd"}, + {file = "watchdog-2.2.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4cb5ecc332112017fbdb19ede78d92e29a8165c46b68a0b8ccbd0a154f196d5e"}, + {file = "watchdog-2.2.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a480d122740debf0afac4ddd583c6c0bb519c24f817b42ed6f850e2f6f9d64a8"}, + {file = "watchdog-2.2.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:978a1aed55de0b807913b7482d09943b23a2d634040b112bdf31811a422f6344"}, + {file = "watchdog-2.2.1-py3-none-manylinux2014_armv7l.whl", hash = "sha256:8c28c23972ec9c524967895ccb1954bc6f6d4a557d36e681a36e84368660c4ce"}, + {file = "watchdog-2.2.1-py3-none-manylinux2014_i686.whl", hash = "sha256:c27d8c1535fd4474e40a4b5e01f4ba6720bac58e6751c667895cbc5c8a7af33c"}, + {file = "watchdog-2.2.1-py3-none-manylinux2014_ppc64.whl", hash = "sha256:d6b87477752bd86ac5392ecb9eeed92b416898c30bd40c7e2dd03c3146105646"}, + {file = "watchdog-2.2.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:cece1aa596027ff56369f0b50a9de209920e1df9ac6d02c7f9e5d8162eb4f02b"}, + {file = "watchdog-2.2.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:8b5cde14e5c72b2df5d074774bdff69e9b55da77e102a91f36ef26ca35f9819c"}, + {file = "watchdog-2.2.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e038be858425c4f621900b8ff1a3a1330d9edcfeaa1c0468aeb7e330fb87693e"}, + {file = "watchdog-2.2.1-py3-none-win32.whl", hash = "sha256:bc43c1b24d2f86b6e1cc15f68635a959388219426109233e606517ff7d0a5a73"}, + {file = "watchdog-2.2.1-py3-none-win_amd64.whl", hash = "sha256:17f1708f7410af92ddf591e94ae71a27a13974559e72f7e9fde3ec174b26ba2e"}, + {file = "watchdog-2.2.1-py3-none-win_ia64.whl", hash = "sha256:195ab1d9d611a4c1e5311cbf42273bc541e18ea8c32712f2fb703cfc6ff006f9"}, + {file = "watchdog-2.2.1.tar.gz", hash = "sha256:cdcc23c9528601a8a293eb4369cbd14f6b4f34f07ae8769421252e9c22718b6f"}, ] [package.extras] @@ -864,14 +864,14 @@ files = [ [[package]] name = "xinject" -version = "1.1.0" +version = "1.2.0" description = "Lazy dependency injection." category = "main" optional = false python-versions = ">=3.8,<4.0" files = [ - {file = "xinject-1.1.0-py3-none-any.whl", hash = "sha256:5ec5e3540e34f8582c2dd3c99071dc28d74c940fe22fe8ccdaff5e6618840c4e"}, - {file = "xinject-1.1.0.tar.gz", hash = "sha256:f21998a3d55a13f3a19361123d4448ce05be0287c8743a8beab091492c2332fb"}, + {file = "xinject-1.2.0-py3-none-any.whl", hash = "sha256:ff46db0660de0de98e8d636579c2f8bfc6435275310b48e2128b321952ed4e5b"}, + {file = "xinject-1.2.0.tar.gz", hash = "sha256:b982cfd84b740c27cd0a9989ae1639f6d5cc2b9307e720bd1202f91dac61c012"}, ] [package.dependencies] @@ -923,4 +923,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "f293d416dcaa21cb7eec0067c6125743822bab772fd2a72baf941ac64578c193" +content-hash = "e178e5c24c59e9cd973fdb8da68f5006a3659f608bbb44ba40833413d79285ac" diff --git a/pyproject.toml b/pyproject.toml index 8d2c5e0..d90819e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ classifiers = [ [tool.poetry.dependencies] python = "^3.8" xsentinels = "^1.2.0" -xinject = "^1.1.0" +xinject = "^1.2.0" xloop = "^1.0.1" xbool = "^1.0.0" ciso8601 = "^2.3.0" diff --git a/tests/test_fields.py b/tests/test_fields.py index 9647eb6..055e304 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -3,7 +3,8 @@ import pytest as pytest from xsettings.fields import SettingsField, generate_setting_fields -from xsettings.settings import SettingsRetriever, Settings +from xsettings.retreivers import SettingsRetriever +from xsettings import Settings @pytest.mark.parametrize( @@ -93,7 +94,7 @@ def test_property_as_retreiver(): def my_default_retreiver(*, field: SettingsField, settings: 'Settings'): return f"field.name={field.name}" - class TestSettings(Settings, retrievers=[my_default_retreiver]): + class TestSettings(Settings, default_retrievers=[my_default_retreiver]): my_str_field: str @property diff --git a/tests/test_settings.py b/tests/test_settings.py index 6712a6a..553ed7b 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -10,8 +10,8 @@ from xsettings.env_settings import EnvSettings from xsettings.fields import SettingsConversionError -from xsettings.settings import Settings, SettingsField, SettingsValueError, SettingsRetriever -from xsettings.settings import PropertyRetriever +from xsettings.settings import Settings, SettingsField, SettingsValueError +from xsettings.retreivers import SettingsRetriever, PropertyRetriever def test_set_default_value_after_settings_subclass_created(): @@ -48,7 +48,7 @@ def __call__(self, *, field: SettingsField, settings: 'Settings') -> Any: class MyForwardSettings(Settings): my_forwarded_field: str = "my_forwarded_field-value" - class MySettings(Settings, retrievers=MyRetriever()): + class MySettings(Settings, default_retrievers=MyRetriever()): my_field = "my_field-value" @property @@ -514,16 +514,16 @@ class MyChildSettings(MyParentSettings): def test_inherit_multiple_retrievers(): def r1(*, field: SettingsField, settings: Settings): - if field.name == 'a': - return 'a-val' - return None + if field.name == 'a': + return 'a-val' + return None def r2(*, field: SettingsField, settings: Settings): - if field.name == 'b_alt_name': - return True - return None + if field.name == 'b_alt_name': + return True + return None - class MyParentSettings(Settings, retrievers=[r1, r2]): + class MyParentSettings(Settings, default_retrievers=[r1, r2]): # Make them fields in our Settings subclass, default value to another settings class. a: str b: bool = SettingsField(name="b_alt_name") @@ -553,3 +553,32 @@ class MyChildSettings(MyParentSettings): # Error should be unchanged: with pytest.raises(SettingsValueError): error_getting_non_optional_value = my_parent_settings.c + + +def test_grab_setting_values_from_parent_dependency_instances(): + def r1(*, field: SettingsField, settings: Settings): + 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. + a: str + b: str + c: int + + my_settings = MySettings.proxy() + my_settings.a = "override-a" + + assert my_settings.a == 'override-a' + assert my_settings.b == 'str-val' + assert my_settings.c == 2 + + with MySettings(b='override-via-child-instance-b'): + # This value should come from the parent-instance to the one inside the above `with` + # (ie: the MySettings instance that is 'current' outside this `with` statement) + assert my_settings.a == 'override-a' + assert my_settings.b == 'override-via-child-instance-b' + assert my_settings.c == 2 + + assert my_settings.a == 'override-a' + assert my_settings.b == 'str-val' + assert my_settings.c == 2 diff --git a/xsettings/env_settings.py b/xsettings/env_settings.py index 4e05257..ef7c576 100644 --- a/xsettings/env_settings.py +++ b/xsettings/env_settings.py @@ -2,7 +2,8 @@ from typing import Any from xsettings.fields import SettingsField -from xsettings.settings import SettingsRetriever, Settings +from xsettings.settings import Settings +from xsettings.retreivers import SettingsRetriever class EnvSettingsRetriever(SettingsRetriever): @@ -10,5 +11,5 @@ def __call__(self, *, field: SettingsField, settings: 'Settings') -> Any: return os.environ.get(field.name) -class EnvSettings(Settings, retrievers=EnvSettingsRetriever()): +class EnvSettings(Settings, default_retrievers=EnvSettingsRetriever()): pass diff --git a/xsettings/fields.py b/xsettings/fields.py index 6c155e9..744d081 100644 --- a/xsettings/fields.py +++ b/xsettings/fields.py @@ -9,7 +9,8 @@ from xsentinels import unwrap_union, Default if TYPE_CHECKING: - from .settings import Settings, SettingsRetrieverCallable, PropertyRetriever + from .settings import Settings + from .retreivers import PropertyRetriever, SettingsRetriever T = TypeVar("T") @@ -101,11 +102,11 @@ class SettingsField: converter will always be called. """ - retriever: 'SettingsRetrieverCallable' = None + retriever: 'SettingsRetriever' = None """ Retriever callable to use to retrieve settings from some source. - Can be any callable that follows the `SettingsRetrieverCallable` calling interface. + Can be any callable that follows the `SettingsRetriever` calling interface. Although, subclassing `SettingsRetriever` makes things generally easier as it handles some of the expected default behavior for you (example: `xyn_config.config.ConfigRetriever`). @@ -222,7 +223,7 @@ def getter(self): # Whatever we return from this internal method is what is left on # the class afterwards. In this case, the original SettingsField (self). def wrap_property_getter_func(func): - from xsettings.settings import PropertyRetriever + from xsettings.retreivers import PropertyRetriever wrapped_property = property(fget=func) self.retriever = PropertyRetriever(wrapped_property) return self @@ -499,12 +500,12 @@ def _add_field_default_from_attrs(class_attrs: Dict[str, Any], merge_field): "We may support property setters in the future." ) - # For normal properties, we always wrap them in a PropertyRetreiver, - # and use that for the fields retreiver; we consider a normal property - # the 'retreiver' for that value. If user did not directly set the value + # For normal properties, we always wrap them in a PropertyRetriever, + # and use that for the field's retriever; we consider a normal property + # the 'retriever' for that value. If user did not directly set the value # the retriever, ie: this PropertyRetriever will be called and it will in turn - # call the wrapped property to 'retreive' the value. - from xsettings.settings import PropertyRetriever + # call the wrapped property to 'retriever' the value. + from xsettings.retreivers import PropertyRetriever field_values.retriever = PropertyRetriever(v) else: field_values.default_value = v @@ -540,5 +541,5 @@ def _assert_retriever_valid(field): return assert callable(field.retriever), ( f"Invalid retriever for field {field}, needs to be callable, see " - f"SettingsRetrieverCallable." + f"SettingsRetriever." ) diff --git a/xsettings/retreivers.py b/xsettings/retreivers.py new file mode 100644 index 0000000..e4692c8 --- /dev/null +++ b/xsettings/retreivers.py @@ -0,0 +1,58 @@ +from xsentinels.sentinel import Sentinel +from typing import Any, Protocol +from .settings import SettingsField, Settings + + +# class TryNextRetriever(Sentinel): +# pass + + +# todo: remove use of `SettingsRetrieverCallable` + +class SettingsRetriever(Protocol): + + """ + The purpose of the base SettingsRetrieverCallable is to define the base-interface for + retrieving settings values. + + The retriever can be any callable, by default `xsettings.settings.Settings` will use + an instance of `SettingsRetriever`. It provides a default retriever implementation, + see that class for more details on what happens by default. + """ + + def __call__(self, *, field: SettingsField, settings: Settings) -> Any: + """ + This is how the Settings 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, + or more complex objects to be them too (via __call__). + + Args: + field: Field we need to retrieve. + settings: Related Settings 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) + """ + raise NotImplementedError( + "Abstract Method - Must implement `__call__` function with correct arguments." + ) + + +class PropertyRetriever(SettingsRetriever): + """ + 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, + we instead use `PropertyRetriever`; as the property it's self is considered the 'retriever'. + + Will first check the property getter function when retrieving a value before + doing anything else (such as using the default_value for the field, etc, etc). + """ + property_retriever: property + + def __init__(self, property_retriever: property): + self.property_retriever = property_retriever + + def __call__(self, *, field: SettingsField, settings: 'Settings') -> Any: + return self.property_retriever.__get__(settings, type(settings)) diff --git a/xsettings/settings.py b/xsettings/settings.py index a939f97..529d954 100644 --- a/xsettings/settings.py +++ b/xsettings/settings.py @@ -5,14 +5,17 @@ """ -from typing import Dict, Any, Union, TypeVar, Protocol, Optional, Iterable, List, Type +from typing import Dict, Any, Union, TypeVar, Protocol, Optional, Iterable, List, Type, TYPE_CHECKING -from xinject import Dependency +from xinject import Dependency, XContext from xloop import xloop from xsentinels import Default from xsettings.fields import generate_setting_fields, SettingsField, SettingsClassProperty +if TYPE_CHECKING: + from .retreivers import SettingsRetriever + T = TypeVar("T") # Tell pdoc3 to document the normally private method __call__. @@ -30,70 +33,6 @@ class SettingsValueError(ValueError, AttributeError): pass -# todo: remove use of `SettingsRetrieverCallable` - -class SettingsRetriever(Protocol): - - """ - The purpose of the base SettingsRetrieverCallable is to define the base-interface for - retrieving settings values. - - The retriever can be any callable, by default `xsettings.settings.Settings` will use - an instance of `SettingsRetriever`. It provides a default retriever implementation, - see that class for more details on what happens by default. - """ - - def __call__(self, *, field: SettingsField, settings: 'Settings') -> Any: - """ - This is how the Settings 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, - or more complex objects to be them too (via __call__). - - Args: - field: Field we need to retrieve. - settings: Related Settings 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) - """ - raise NotImplementedError( - "Abstract Method - Must implement `__call__` function with correct arguments." - ) - - -class PropertyRetriever(SettingsRetriever): - """ - 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, - we instead use `PropertyRetriever`; as the property it's self is considered the 'retriever'. - - Will first check the property getter function when retrieving a value before - doing anything else (such as using the default_value for the field, etc, etc). - """ - property_retriever: property - - def __init__(self, property_retriever: property): - self.property_retriever = property_retriever - - def __call__(self, *, field: SettingsField, settings: 'Settings') -> Any: - return self.property_retriever.__get__(settings, type(settings)) - - -def _load_default_retriever(setting_subclasses_in_mro, default_retriever): - if default_retriever: - return default_retriever - - # Look for a retrieve in base classes. - for base in setting_subclasses_in_mro: - if default_retriever := getattr(base, '_default_retriever', None): - return default_retriever - - # Fallback to using a SettingsRetriever instance. - return SettingsRetriever() - - class _SettingsMeta(type): """Represents the class-type instance/obj of the `Settings` class. Any attributes in this object will be class-level attributes of @@ -105,7 +44,7 @@ class _SettingsMeta(type): # This will be a class-attributes on the normal `Settings` class/subclasses. _setting_fields: Dict[str, SettingsField] - _retrievers: List[SettingsRetriever] + _retrievers: 'List[SettingsRetriever]' _there_is_plain_superclass: bool """ There is some other superclass, other then Settings/object/Dependency. """ @@ -116,7 +55,7 @@ class _SettingsMeta(type): (but not Settings it's self); in the same order that they appears in __mro__. """ - def retrievers(cls, retriever: SettingsRetriever): + def retrievers(cls, retriever: 'SettingsRetriever'): cls._retrievers.append(retriever) def __new__( @@ -125,7 +64,7 @@ def __new__( bases, attrs: Dict[str, Any], *, - retrievers: Union[SettingsRetriever, Iterable[SettingsRetriever]] = None, + default_retrievers: 'Union[SettingsRetriever, Iterable[SettingsRetriever]]' = None, skip_field_generation: bool = False, **kwargs, ): @@ -153,7 +92,7 @@ def __new__( # These defaults may be altered later on in this method (after class is created)... attrs['_there_is_plain_superclass'] = False attrs['_setting_subclasses_in_mro'] = [] - attrs['_retrievers'] = list(xloop(retrievers)) + attrs['_default_retrievers'] = list(xloop(default_retrievers)) if skip_field_generation: # Skip doing anything special with any Settings classes created in our/this module; @@ -194,7 +133,6 @@ def __new__( for c in reversed(setting_subclasses_in_mro): parent_fields.update(c._setting_fields) - # default_retriever = _load_default_retriever(setting_subclasses_in_mro, default_retriever) setting_fields = generate_setting_fields( attrs, parent_fields ) @@ -290,12 +228,10 @@ def __setattr__(self, key: str, value: Union[SettingsField, Any]): field.default_value = value - - class Settings( Dependency, metaclass=_SettingsMeta, - retrievers=[], + 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. @@ -416,9 +352,10 @@ def __getattribute__(self, key): attr_error = None value = None already_retrieved_normal_value = False - + cls = type(self) field: Optional[SettingsField] = None - for c in type(self)._setting_subclasses_in_mro: + + for c in cls._setting_subclasses_in_mro: c: _SettingsMeta # todo: use isinstance? if c is Settings: @@ -428,19 +365,20 @@ def __getattribute__(self, key): # Found the field, break out of loop. break - def get_normal_value(): + def get_normal_value(obj: Settings = self): nonlocal value nonlocal attr_error # Keep track that we already attempted to get normal value. nonlocal already_retrieved_normal_value - already_retrieved_normal_value = True + if obj is self: + already_retrieved_normal_value = True try: # Look for an attribute on self first. - value = object.__getattribute__(self, key) + value = object.__getattribute__(obj, key) if hasattr(value, "__get__"): - value = value.__get__(self, type(self)) + value = value.__get__(obj, cls) attr_error = None except AttributeError as error: attr_error = error @@ -456,14 +394,22 @@ def get_normal_value(): 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: # If we have a field, and current value is Default, or we got AttributeError, # we attempt to retrieve the value via the field's retriever. if not already_retrieved_normal_value or attr_error or value is Default: def self_and_parent_retrievers(): - for parent_class in type(self)._setting_subclasses_in_mro: - for r in parent_class._retrievers: + for parent_class in cls._setting_subclasses_in_mro: + for r in parent_class._default_retrievers: yield r for retriever in xloop(field.retriever, self_and_parent_retrievers()): value = retriever(field=field, settings=self) @@ -473,7 +419,7 @@ def self_and_parent_retrievers(): if value is None: value = field.default_value if value and hasattr(value, '__get__'): - value = value.__get__(self, type(self)) + value = value.__get__(self, cls) if value is None: if field.required: @@ -498,7 +444,9 @@ def self_and_parent_retrievers(): return value except SettingsValueError as e: # 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... + # 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