Skip to content

Commit

Permalink
settings forward & less restricted settings (#7)
Browse files Browse the repository at this point in the history
Changes:

- allow settings forward
- allow manipulating settings via assignment and deletion
- unapply in set_instance a potential overwrite
- return/yield values set
- explain how to easily build forwarders
- remove deprecated alias
  • Loading branch information
devkral authored Nov 15, 2024
1 parent 037f68b commit 7d9b52c
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 32 deletions.
21 changes: 21 additions & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
# Release notes

## Version 0.0.8

### Added

- Settings forwards
- `settings_path` parameter has now more allowed value types.
- Assignments to the settings attribute.
- `with_` and `set_` operations returning set object.

### Changed

- `settings_path=""` behaves now different (enables settings). The default switched to `None` (disabled settings).

### Removed

- Remove deprecated alias for `settings_preloads_name`.

### Fixed

- Use the right instance for apply_settings in set_instance.

## Version 0.0.7

### Fixed
Expand Down
129 changes: 129 additions & 0 deletions docs/specials.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,133 @@ 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.
When assigning a string or a class, the initialization happens on the first access to the settings
attribute and are cached.
Functions get evaluated on every access and should care for caching in case it is required (for forwards the caching
takes place in the main settings).


## Temporary disable overwrite

You can also use the `with_...` functions with None. This disables the overwrite for the scope.
It is used in set_instance when applying extensions.

## Echoed values

The `with_` and `set_` methods return the passed variable as contextmanager value.

e.g.

``` python

with monkay.with_settings(Settings()) as new_settings:
# do things with the settings overwrite

# disable the overwrite
with monkay.with_settings(None) as new_settings2:
# echoed is None
assert new_settings2 is None
# settings are the old settings again
assert monkay.settings is old_settings

```


## Forwarder

Sometimes you have an old settings place and want to forward it to the monkay one.
Here does no helper exist but a forwarder is easy to write:

``` python
from typing import Any, cast, TYPE_CHECKING

if TYPE_CHECKING:
from .global_settings import EdgySettings


class SettingsForward:
def __getattribute__(self, name: str) -> Any:
import edgy

return getattr(edgy.monkay.settings, name)

# we want to pretend the forward is the real object
settings = cast("EdgySettings", SettingsForward())

__all__ = ["settings"]



```
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"
84 changes: 55 additions & 29 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,9 +166,8 @@ 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 = "",
settings_path: str | Callable[[], SETTINGS] | SETTINGS | type[SETTINGS] | None = None,
preloads: Iterable[str] = (),
settings_preload_name: str = "",
settings_preloads_name: str = "",
settings_extensions_name: str = "",
uncached_imports: Iterable[str] = (),
Expand Down Expand Up @@ -203,18 +203,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
if self.settings_path:
if settings_path is not None:
self._settings_var = globals_dict[settings_ctx_name] = ContextVar(settings_ctx_name, default=None)

if settings_preload_name:
warnings.warn(
'The "settings_preload_name" parameter is deprecated use "settings_preloads_name" instead.',
DeprecationWarning,
stacklevel=2,
)
if not settings_preloads_name and settings_preload_name:
settings_preloads_name = settings_preload_name
self.settings = settings_path # type: ignore
self.settings_preloads_name = settings_preloads_name
self.settings_extensions_name = settings_extensions_name

Expand All @@ -240,7 +231,7 @@ def __init__(

def clear_caches(self, settings_cache: bool = True, import_cache: bool = True) -> None:
if settings_cache:
self.__dict__.pop("_settings", None)
del self.settings
if import_cache:
self._cached_imports.clear()

Expand All @@ -254,18 +245,21 @@ def instance(self) -> INSTANCE | None:

def set_instance(
self,
instance: INSTANCE,
instance: INSTANCE | None,
*,
apply_extensions: bool = True,
use_extensions_overwrite: bool = True,
) -> None:
) -> INSTANCE | None:
assert self._instance_var is not None, "Monkay not enabled for instances"
# need to address before the instance is swapped
if apply_extensions and self._extensions_applied_var.get() is not None:
raise RuntimeError("Other apply process in the same context is active.")
self._instance = instance
if apply_extensions and self._extensions_var is not None:
self.apply_extensions(use_overwrite=use_extensions_overwrite)
if apply_extensions and instance is not None and self._extensions_var is not None:
# unapply a potential instance overwrite
with self.with_instance(None):
self.apply_extensions(use_overwrite=use_extensions_overwrite)
return instance

@contextmanager
def with_instance(
Expand All @@ -274,7 +268,7 @@ def with_instance(
*,
apply_extensions: bool = False,
use_extensions_overwrite: bool = True,
) -> Generator:
) -> Generator[INSTANCE | None]:
assert self._instance_var is not None, "Monkay not enabled for instances"
# need to address before the instance is swapped
if apply_extensions and self._extensions_var is not None and self._extensions_applied_var.get() is not None:
Expand All @@ -283,7 +277,7 @@ def with_instance(
try:
if apply_extensions and self._extensions_var is not None:
self.apply_extensions(use_overwrite=use_extensions_overwrite)
yield
yield instance
finally:
self._instance_var.reset(token)

Expand Down Expand Up @@ -366,14 +360,14 @@ def with_extensions(
extensions: dict[str, ExtensionProtocol[INSTANCE, SETTINGS]] | None,
*,
apply_extensions: bool = False,
) -> Generator:
) -> Generator[dict[str, ExtensionProtocol[INSTANCE, SETTINGS]] | None]:
# why None, for temporary using the real extensions
assert self._extensions_var is not None, "Monkay not enabled for extensions"
token = self._extensions_var.set(extensions)
try:
if apply_extensions and self.instance is not None:
self.apply_extensions()
yield
yield extensions
finally:
self._extensions_var.reset(token)

Expand Down Expand Up @@ -506,27 +500,59 @@ 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_var is not None, "Monkay not enabled for settings"
settings = self._settings_var.get()
settings: SETTINGS | Callable[[], SETTINGS] | None = self._settings_var.get()
if settings is None:
settings = self._settings
# when settings_path is callable bypass the cache, for forwards
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[[], 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
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("_loaded_settings", None)

@contextmanager
def with_settings(self, settings: SETTINGS | None) -> Generator:
def with_settings(self, settings: SETTINGS | None) -> Generator[SETTINGS | None]:
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:
yield
yield settings
finally:
self._settings_var.reset(token)

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
Loading

0 comments on commit 7d9b52c

Please sign in to comment.