Skip to content

Commit

Permalink
fixes for the emergency release
Browse files Browse the repository at this point in the history
see changelog
  • Loading branch information
devkral committed Jan 30, 2025
1 parent 246609c commit c25dae3
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 55 deletions.
10 changes: 8 additions & 2 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,18 @@

This is an emergency release. It removes the feature that implicitly evaluates settings during `__init__`. This
is very error prone and can lead to two different versions of the same library in case the sys.path is manipulated.
Also failed imports are not neccessarily side-effect free.

### Added

- `evaluate_settings` has now two extra keyword parameters: `onetime` and `ignore_preload_import_errors`.

### Changes

- `evaluate_settings` behaves like `evaluate_settings_once`. We will need this too often now and having two versions is error-prone.
- `evaluate_settings_once` is deprecated.
- `evaluate_settings` behaves like `evaluate_settings_once`. We will need this too often now and having two similar named versions is error-prone.
- `evaluate_settings_once` is now deprecated.
- Setting the `evaluate_settings` parameter in `__init__` is now an error.
- For the parameter `ignore_import_errors` of `evaluate_settings` the default value is changed to `False`.

## Version 0.2.2

Expand Down
24 changes: 22 additions & 2 deletions docs/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,26 @@ else:
monkay.evaluate_settings()
```

## Multi stage settings setup

By passing `ignore_import_errors=True` we can check multiple pathes if the config could load. We get a False as return value in case of not.

``` python
import os
from monkay import Monkay

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

def find_settings():
for path in ["a.settings", "b.settings.develop"]:
if monkay.evaluate_settings(ignore_import_errors=True):
break
```

### `evaluate_settings` method

There is also`evaluate_settings` which evaluates always, not checking for if the settings were
Expand All @@ -73,8 +93,8 @@ It has has following keyword only parameter:

- `on_conflict`: Matches the values of add_extension but defaults to `error`.
- `onetime`: Evaluates the settings only one the first call. All other calls become noops. Defaults to `True`.
- `ignore_import_errors`: Suppress import related errors. Handles unset settings lenient. Defaults to `True`.

- `ignore_import_errors`: Suppress import related errors concerning settings. Handles unset settings lenient. Defaults to `False`.
- `ignore_preload_import_errors`: Suppress import related errors concerning preloads in settings. Defaults to `True`.

!!! Note
`evaluate_settings` doesn't touch the settings when no `settings_preloads_name` and/or `settings_extensions_name` is set
Expand Down
14 changes: 14 additions & 0 deletions docs/specials.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,20 @@ with monkay.with_settings(Settings()) as new_settings:
assert monkay.settings is old_settings
```

## `evaluate_preloads`

`evaluate_preloads` is a way to load preloads everywhere in the application.

### Parameters

- `preloads` (also positional): import strings to import. See [Preloads](./tutorial.md#preloads) for the special syntax.
- `ignore_import_errors`: Ignore import errors of preloads. When `True` (default) not all import
strings must be available. When `False` all must be available.
- `package` (Optional). Provide a different package as parent package. By default (when empty) the package of the Monkay instance is used.

Note: `monkay.base` contains a slightly different `evaluate_preloads` which uses when no package is provided the None package. It doesn't require
an Monkay instance either.

## Typings

Monkay is fully typed and its main class Monkay is a Generic supporting 2 type parameters:
Expand Down
22 changes: 17 additions & 5 deletions docs/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ pip install monkay

### Usage

Probably in the main `__init__.py` you define something like this:
Probably you define something like this:

``` python
``` python title="foo/__init__.py"
from monkay import Monkay

monkay = Monkay(
Expand Down Expand Up @@ -40,6 +40,15 @@ monkay = Monkay(
)
```

``` python title="foo/main.py"
from foo import monkay
def get_application():
# sys.path updates
monkay.evaluate_settings(prelo)


```

When providing your own `__all__` variable **after** providing Monkay or you want more controll, you can provide

`skip_all_update=True`
Expand Down Expand Up @@ -131,10 +140,11 @@ class Settings(BaseSettings):

def get_application():
# initialize the loaders/sys.path
# add additional preloads
monkay.evaluate_preloads(...)
monkay.evaluate_settings()

app = get_application()

```

And voila settings are now available from monkay.settings as well as settings. This works only when all settings arguments are
Expand Down Expand Up @@ -286,8 +296,10 @@ class Settings(BaseSettings):
preloads: list[str] = ["preloader:preloader"]
```

!!! Note
Settings preloads are only executed after executing `evaluate_settings()`. Preloads given in the `__init__` instantly.
!!! Warning
Settings preloads are only executed after executing `evaluate_settings()`. Preloads given in the `__init__` are evaluated instantly.
You can however call `evaluate_preloads` directly.


#### Using the instance feature

Expand Down
20 changes: 19 additions & 1 deletion monkay/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

import warnings
from collections.abc import Collection
from collections.abc import Collection, Iterable
from importlib import import_module
from typing import Any

Expand Down Expand Up @@ -70,3 +70,21 @@ def get_value_from_settings(settings: Any, name: str) -> Any:
return getattr(settings, name)
except AttributeError:
return settings[name]


def evaluate_preloads(
preloads: Iterable[str], *, ignore_import_errors: bool = True, package: str | None = None
) -> bool:
no_errors: bool = True
for preload in preloads:
splitted = preload.rsplit(":", 1)
try:
module = import_module(splitted[0], package)
except (ImportError, AttributeError) as exc:
if not ignore_import_errors:
raise exc
no_errors = False
continue
if len(splitted) == 2:
getattr(module, splitted[1])()
return no_errors
106 changes: 65 additions & 41 deletions monkay/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from ._monkay_exports import MonkayExports
from ._monkay_instance import MonkayInstance
from ._monkay_settings import MonkaySettings
from .base import UnsetError, get_value_from_settings
from .base import UnsetError, evaluate_preloads, get_value_from_settings
from .types import (
INSTANCE,
PRE_ADD_LAZY_IMPORT_HOOK,
Expand Down Expand Up @@ -57,6 +57,7 @@ def __init__(
ignore_settings_import_errors: bool = True,
pre_add_lazy_import_hook: None | PRE_ADD_LAZY_IMPORT_HOOK = None,
post_add_lazy_import_hook: None | Callable[[str], None] = None,
ignore_preload_import_errors: bool = True,
package: str | None = "",
) -> None:
self.globals_dict = globals_dict
Expand Down Expand Up @@ -115,14 +116,7 @@ def __init__(
and "__dir__" not in globals_dict
):
self._init_global_dir_hook()
for preload in preloads:
splitted = preload.rsplit(":", 1)
try:
module = import_module(splitted[0], self.package)
except ImportError:
module = None
if module is not None and len(splitted) == 2:
getattr(module, splitted[1])()
self.evaluate_preloads(preloads, ignore_import_errors=ignore_preload_import_errors)
if evaluate_settings is not None:
raise Exception(
"This feature and the evaluate_settings parameter are removed in monkay 0.3"
Expand All @@ -134,53 +128,83 @@ def clear_caches(self, settings_cache: bool = True, import_cache: bool = True) -
if import_cache:
self._cached_imports.clear()

def evaluate_preloads(
self,
preloads: Iterable[str],
*,
ignore_import_errors: bool = True,
package: str | None = None,
) -> bool:
return evaluate_preloads(
preloads, ignore_import_errors=ignore_import_errors, package=package or self.package
)
no_errors: bool = True
for preload in preloads:
splitted = preload.rsplit(":", 1)
try:
module = import_module(splitted[0], self.package)
except (ImportError, AttributeError) as exc:
if not ignore_import_errors:
raise exc
no_errors = False
continue
if len(splitted) == 2:
getattr(module, splitted[1])()
return no_errors

def _evaluate_settings(
self,
*,
on_conflict: Literal["error", "keep", "replace"] = "keep",
settings: SETTINGS,
on_conflict: Literal["error", "keep", "replace"],
ignore_preload_import_errors: bool,
initial_settings_evaluated: bool,
) -> None:
# don't access settings when there is nothing to evaluate
if not self.settings_extensions_name and not self.settings_extensions_name:
self.settings_evaluated = True
return

# load settings one time and before setting settings_evaluated to True
settings = self.settings
self.settings_evaluated = True

preloads = None
if self.settings_preloads_name:
preloads = get_value_from_settings(settings, self.settings_preloads_name)
if preloads:
for preload in preloads:
splitted = preload.rsplit(":", 1)
try:
module = import_module(splitted[0], self.package)
except ImportError:
module = None
if module is not None and len(splitted) == 2:
getattr(module, splitted[1])()

if self.settings_extensions_name:
for extension in get_value_from_settings(settings, self.settings_extensions_name):
self.add_extension(extension, use_overwrite=True, on_conflict=on_conflict)
try:
if self.settings_preloads_name:
settings_preloads = get_value_from_settings(settings, self.settings_preloads_name)
self.evaluate_preloads(
settings_preloads, ignore_import_errors=ignore_preload_import_errors
)
if self.settings_extensions_name:
for extension in get_value_from_settings(settings, self.settings_extensions_name):
self.add_extension(extension, use_overwrite=True, on_conflict=on_conflict)
except Exception as exc:
if not initial_settings_evaluated:
self.settings_evaluated = False
raise exc

def evaluate_settings(
self,
*,
on_conflict: Literal["error", "keep", "replace"] = "error",
ignore_import_errors: bool = True,
ignore_import_errors: bool = False,
ignore_preload_import_errors: bool = True,
onetime: bool = True,
) -> bool:
if onetime and self.settings_evaluated:
initial_settings_evaluated = self.settings_evaluated
if onetime and initial_settings_evaluated:
return True
if ignore_import_errors:
try:
self._evaluate_settings(on_conflict=on_conflict)
except (ImportError, AttributeError, UnsetError):
# don't access settings when there is nothing to evaluate
if not self.settings_extensions_name and not self.settings_extensions_name:
self.settings_evaluated = True
return

try:
# load settings one time and before setting settings_evaluated to True
settings = self.settings
except Exception as exc:
if ignore_import_errors and isinstance(exc, (UnsetError, ImportError, AttributeError)):
return False
else:
self._evaluate_settings(on_conflict=on_conflict)
raise exc
self._evaluate_settings(
on_conflict=on_conflict,
settings=settings,
ignore_preload_import_errors=ignore_preload_import_errors,
initial_settings_evaluated=initial_settings_evaluated,
)
return True

def evaluate_settings_once(
Expand Down
8 changes: 4 additions & 4 deletions tests/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ def test_notfound_settings():

assert not mod.monkay.settings_evaluated

mod.monkay.evaluate_settings()
mod.monkay.evaluate_settings(ignore_import_errors=True)
assert not mod.monkay.settings_evaluated

with pytest.raises(ImportError):
mod.monkay.evaluate_settings(ignore_import_errors=False)
mod.monkay.evaluate_settings()


def test_notevaluated_settings():
Expand All @@ -75,9 +75,9 @@ def test_unset_settings(value):

mod.monkay.settings = value

mod.monkay.evaluate_settings()
mod.monkay.evaluate_settings(ignore_import_errors=True)
with pytest.raises(UnsetError):
mod.monkay.evaluate_settings(ignore_import_errors=False)
mod.monkay.evaluate_settings()

with pytest.raises(UnsetError):
mod.monkay.settings # noqa
Expand Down

0 comments on commit c25dae3

Please sign in to comment.