Skip to content

Commit

Permalink
Changes:
Browse files Browse the repository at this point in the history
- settings forward
- fixed provided instance when using set_instance
  • Loading branch information
devkral committed Nov 14, 2024
1 parent f392330 commit fe99b21
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 26 deletions.
11 changes: 11 additions & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Release notes

## Version 0.0.8

### Added

- Settings forwards and a liberalized settings_path parameter.
- Assignments to the settings attribute.

### Fixed

- Use the right instance for apply_settings in set_instance.

## Version 0.0.7

### Fixed
Expand Down
72 changes: 72 additions & 0 deletions docs/specials.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,75 @@ monkay.add_deprecated_lazy_import(
no_hooks=True
)
```

## Setting settings forward

Sometimes you have some packages which should work independently but
in case of a main package the packages should use the settings of the main package.

For this monkay settings have a forwarding mode, in which the cache is disabled.
It can be enabled by either setting the settings parameter to a function (most probably less common)
or simply assigning a callable to the monkay settings property.
It is expected that the assigned function returns a suitable settings object.


Child

``` python
import os
from monkay import Monkay

monkay = Monkay(
globals(),
settings_path=os.environ.get("MONKAY_CHILD_SETTINGS", "foo.test:example") or ""
)

```

Main

``` python
import os
import child

monkay = Monkay(
globals(),
settings_path=os.environ.get("MONKAY_MAIN_SETTINGS", "foo.test:example") or ""
)
child.monkay.settings = lambda: monkay.settings

```

## Lazy settings setup

Like when using a settings forward it is possible to activate the settings later by assigning a string, a class or an settings instance
to the settings attribute.
For this provide an empty string to the settings_path variable.
It ensures the initialization takes place.

``` python
import os
from monkay import Monkay

monkay = Monkay(
globals(),
# required for initializing settings
settings_path=""
)

# somewhere later

if not os.environg.get("DEBUG"):
monkay.settings = os.environ.get("MONKAY_MAIN_SETTINGS", "foo.test:example") or ""
elif os.environ.get("PERFORMANCE"):
# you can also provide a class
monkay.settings = DebugSettings
else:
monkay.settings = DebugSettings()

```

## Other settings types

All of the assignment examples are also possible as settings_path parameter.
Internally an assignment takes place.
2 changes: 1 addition & 1 deletion monkay/__about__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2024-present alex <[email protected]>
#
# SPDX-License-Identifier: BSD-3-Clauses
__version__ = "0.0.7"
__version__ = "0.0.8"
57 changes: 34 additions & 23 deletions monkay/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ class Monkay(Generic[INSTANCE, SETTINGS]):
_extensions_var: None | ContextVar[None | dict[str, ExtensionProtocol[INSTANCE, SETTINGS]]] = None
_extensions_applied: None | ContextVar[dict[str, ExtensionProtocol[INSTANCE, SETTINGS]] | None] = None
_settings_var: ContextVar[SETTINGS | None] | None = None
_settings_definition: SETTINGS | type[SETTINGS] | str | Callable[[], SETTINGS] | None = None

def __init__(
self,
Expand All @@ -165,7 +166,7 @@ def __init__(
with_instance: str | bool = False,
with_extensions: str | bool = False,
extension_order_key_fn: None | Callable[[ExtensionProtocol[INSTANCE, SETTINGS]], Any] = None,
settings_path: str | Callable[[], BaseSettings] = "",
settings_path: str | Callable[[], SETTINGS] | SETTINGS | type[SETTINGS] | None = None,
preloads: Iterable[str] = (),
settings_preload_name: str = "",
settings_preloads_name: str = "",
Expand Down Expand Up @@ -203,11 +204,9 @@ def __init__(
if deprecated_lazy_imports:
for name, deprecated_import in deprecated_lazy_imports.items():
self.add_deprecated_lazy_import(name, deprecated_import, no_hooks=True)
self.settings_path = settings_path
self.settings_ctx_name = settings_ctx_name
if self.settings_path:
self._settings_var = globals_dict[self.settings_ctx_name] = ContextVar(self.settings_ctx_name, default=None)

if settings_path is not None:
self._settings_var = globals_dict[settings_ctx_name] = ContextVar(settings_ctx_name, default=None)
self.settings = settings_path # type: ignore
if settings_preload_name:
warnings.warn(
'The "settings_preload_name" parameter is deprecated use "settings_preloads_name" instead.',
Expand Down Expand Up @@ -509,43 +508,55 @@ def find_missing(
return missing

@cached_property
def _settings(self) -> SETTINGS:
settings: Any = load(self.settings_path, package=self.package)
def _loaded_settings(self) -> SETTINGS | None:
# only class and string pathes
if isclass(self._settings_definition):
return self._settings_definition()
assert isinstance(self._settings_definition, str), f"Not a settings object: {self._settings_definition}"
if not self._settings_definition:
return None
settings: SETTINGS | type[SETTINGS] = load(self._settings_definition, package=self.package)
if isclass(settings):
settings = settings()
return settings
return cast(SETTINGS, settings)

@property
def settings(self) -> SETTINGS:
assert self.settings_path and self._settings_var is not None, "Monkay not enabled for settings"
settings = self._settings_var.get()
assert self._settings_var is not None, "Monkay not enabled for settings"
settings: SETTINGS | Callable[[], SETTINGS] | None = self._settings_var.get()
if settings is None:
# when settings_path is callable bypass the cache, for forwards
if callable(self.settings_path):
return self.settings_path()
settings = self._settings
settings = (
self._loaded_settings
if isinstance(self._settings_definition, str) or isclass(self._settings_definition)
else self._settings_definition
)
if callable(settings):
settings = settings()
if settings is None:
raise RuntimeError("Settings are not set yet. Returned settings are None or settings_path is empty.")
return settings

@settings.setter
def settings(self, value: str | Callable[[], BaseSettings]) -> None:
def settings(self, value: str | Callable[[], SETTINGS] | SETTINGS | type[SETTINGS] | None) -> None:
assert self._settings_var is not None, "Monkay not enabled for settings"
if not value:
self._settings_definition = ""
return
# init _settings_var if not initialized yet
if self._settings_var is None:
self._settings_var = self.globals_dict[self.settings_ctx_name] = ContextVar(
self.settings_ctx_name, default=None
)
self.settings_path = value
if not isinstance(value, str) and not callable(value) and not isclass(value):
self._settings_definition = lambda: value
else:
self._settings_definition = value
del self.settings

@settings.deleter
def settings(self) -> None:
# clear cache
self.__dict__.pop("_settings", None)
self.__dict__.pop("_loaded_settings", None)

@contextmanager
def with_settings(self, settings: SETTINGS | None) -> Generator:
assert self.settings_path and self._settings_var is not None, "Monkay not enabled for settings"
assert self._settings_var is not None, "Monkay not enabled for settings"
# why None, for temporary using the real settings
token = self._settings_var.set(settings)
try:
Expand Down
3 changes: 3 additions & 0 deletions tests/targets/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@ class Settings(BaseSettings):
lambda: load("tests.targets.extension:Extension")(name="settings_extension1"),
SettingsExtension,
]


hurray = Settings()
4 changes: 2 additions & 2 deletions tests/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,11 +181,11 @@ def test_caches():
assert isinstance(mod.settings, BaseSettings)
assert "settings" not in mod.monkay._cached_imports
# settings cache
assert "_settings" in mod.monkay.__dict__
assert "_loaded_settings" in mod.monkay.__dict__
mod.monkay.clear_caches()

assert not mod.monkay._cached_imports
assert "_settings" not in mod.monkay.__dict__
assert "_loaded_settings" not in mod.monkay.__dict__


def test_sorted_exports():
Expand Down
32 changes: 32 additions & 0 deletions tests/test_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import sys
from pathlib import Path

import pytest


@pytest.fixture(autouse=True, scope="function")
def cleanup():
for p in (Path(__file__).parent / "targets").iterdir():
sys.modules.pop(f"tests.targets.{p.stem}", None)
yield


def test_settings_basic():
import tests.targets.module_full as mod
from tests.targets.settings import Settings, hurray

new_settings = Settings(preloads=[], extensions=[])

old_settings = mod.monkay.settings
settings_path = mod.monkay._settings_definition
assert isinstance(settings_path, str)
assert mod.monkay.settings is old_settings
mod.monkay.settings = new_settings
assert mod.monkay.settings is new_settings

mod.monkay.settings = lambda: old_settings
assert mod.monkay.settings is old_settings
# auto generated settings
mod.monkay.settings = Settings
mod.monkay.settings = "tests.targets.settings:hurray"
assert mod.monkay.settings is hurray

0 comments on commit fe99b21

Please sign in to comment.