Skip to content

Commit

Permalink
improved edge case handling (#19)
Browse files Browse the repository at this point in the history
* improved edge case handling

During the integration in edgy some edge cases were found which are now handled better.
It affects only cases where settings are disabled or unset or the evaluated names are
unset.
  • Loading branch information
devkral authored Jan 7, 2025
1 parent 20f188f commit ae070b4
Show file tree
Hide file tree
Showing 11 changed files with 187 additions and 15 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.2.2

### Added

- `UnsetError` for simpler checking if the settings are unset.

### Fixed

- Handle edge-cases better when settings are unset or disabled.
- Don't touch settings in `evaluate_settings` when not required.

## Version 0.2.1

### Fixed
Expand Down
16 changes: 13 additions & 3 deletions docs/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,16 @@ monkay.evaluate_settings_once()
`evaluate_settings_once` has following keyword only parameter:

- `on_conflict`: Matches the values of add_extension but defaults to `error`.
- `ignore_import_errors`: Suppress import errors. Defaults to `True`.
- `ignore_import_errors`: Suppress import related errors. Handles unset settings lenient. Defaults to `True`.

When run successfully the context-aware flag `settings_evaluated` is set. If the flag is set,
the method becomes a noop until the flag is lifted by assigning new settings.

The return_value is `True` for a successful evaluation and `False` in the other case.

!!! Note
`ignore_import_errors` suppresses also UnsetError which is raised when the settings are unset.

### `evaluate_settings` method

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

It is internally used by `evaluate_settings_once` and will also set the `settings_evaluated` flag.

!!! Note
`evaluate_settings` doesn't touch the settings when no `settings_preloads_name` and/or `settings_extensions_name` is set
but will still set the `settings_evaluated` flag to `True`.

### `settings_evaluated` flag

Internally it is a property which sets the right flag. Either on the ContextVar or on the instance.
It is resetted when assigning settings and initial False for `with_settings`.
It is resetted when assigning settings and initial `False` for `with_settings`.

## Other settings types

Expand Down Expand Up @@ -125,9 +132,12 @@ class SettingsForward:
settings = cast("EdgySettings", SettingsForward())

__all__ = ["settings"]

```

!!! Note
For enabling settings modifications, you may need to define `__setattr__`, `__delattr__` too.
It is however not recommended.

## Deleting settings

You can delete settings by assigning one of "", None, False. Afterwards
Expand Down
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.2.1"
__version__ = "0.2.2"
2 changes: 2 additions & 0 deletions monkay/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from .base import (
InGlobalsDict,
UnsetError,
absolutify_import,
get_value_from_settings,
load,
Expand All @@ -29,6 +30,7 @@
"load_any",
"absolutify_import",
"InGlobalsDict",
"UnsetError",
"get_value_from_settings",
"Cage",
"TransparentCage",
Expand Down
6 changes: 2 additions & 4 deletions monkay/_monkay_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from inspect import isclass
from typing import Generic, cast

from .base import load
from .base import UnsetError, load
from .types import SETTINGS


Expand Down Expand Up @@ -70,9 +70,7 @@ def settings(self) -> SETTINGS:
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."
)
raise UnsetError("Settings are not set yet or the settings function returned None.")
return settings

@settings.setter
Expand Down
6 changes: 4 additions & 2 deletions monkay/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,10 @@ def absolutify_import(import_path: str, package: str | None) -> str:
return f"{package}.{import_path.lstrip('.')}"


class InGlobalsDict(Exception):
pass
class InGlobalsDict(Exception): ...


class UnsetError(RuntimeError): ...


def get_value_from_settings(settings: Any, name: str) -> Any:
Expand Down
24 changes: 19 additions & 5 deletions monkay/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from ._monkay_exports import MonkayExports
from ._monkay_instance import MonkayInstance
from ._monkay_settings import MonkaySettings
from .base import get_value_from_settings
from .base import UnsetError, get_value_from_settings
from .types import (
INSTANCE,
PRE_ADD_LAZY_IMPORT_HOOK,
Expand All @@ -36,7 +36,12 @@ def __init__(
with_extensions: str | bool = False,
extension_order_key_fn: None
| Callable[[ExtensionProtocol[INSTANCE, SETTINGS]], Any] = None,
settings_path: str | Callable[[], SETTINGS] | SETTINGS | type[SETTINGS] | None = None,
settings_path: str
| Callable[[], SETTINGS]
| SETTINGS
| type[SETTINGS]
| Literal[False]
| None = None,
preloads: Iterable[str] = (),
settings_preloads_name: str = "",
settings_extensions_name: str = "",
Expand Down Expand Up @@ -76,7 +81,7 @@ 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)
if settings_path is not None:
if settings_path is not None and settings_path is not False:
self._settings_var = globals_dict[settings_ctx_name] = ContextVar(
settings_ctx_name, default=None
)
Expand Down Expand Up @@ -117,7 +122,11 @@ def __init__(
module = None
if module is not None and len(splitted) == 2:
getattr(module, splitted[1])()
if evaluate_settings and self._settings_definition:
if (
evaluate_settings
and self._settings_definition is not None
and self._settings_definition != ""
):
# disables overwrite
with self.with_settings(None):
self.evaluate_settings_once(
Expand All @@ -135,6 +144,11 @@ def evaluate_settings(
*,
on_conflict: Literal["error", "keep", "replace"] = "keep",
) -> 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
Expand Down Expand Up @@ -167,7 +181,7 @@ def evaluate_settings_once(
if ignore_import_errors:
try:
self.evaluate_settings(on_conflict=on_conflict)
except (ImportError, AttributeError):
except (ImportError, AttributeError, UnsetError):
return False
else:
self.evaluate_settings(on_conflict=on_conflict)
Expand Down
36 changes: 36 additions & 0 deletions tests/targets/module_disabled_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from monkay import Monkay

extras = {"foo": lambda: "foo"}


def __getattr__(name: str):
try:
return extras[name]
except KeyError as exc:
raise AttributeError from exc


class FakeApp:
is_fake_app: bool = True


__all__ = ["foo"] # noqa
monkay = Monkay(
globals(),
with_extensions=True,
with_instance=True,
settings_path=False,
lazy_imports={
"bar": ".fn_module:bar",
"bar2": "..targets.fn_module:bar2",
"dynamic": lambda: "dynamic",
"settings": lambda: monkay.settings,
},
deprecated_lazy_imports={
"deprecated": {
"path": "tests.targets.fn_module:deprecated",
"reason": "old.",
"new_attribute": "super_new",
}
},
)
23 changes: 23 additions & 0 deletions tests/targets/module_notevaluated_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from monkay import Monkay

extras = {"foo": lambda: "foo"}


def __getattr__(name: str):
try:
return extras[name]
except KeyError as exc:
raise AttributeError from exc


class FakeApp:
is_fake_app: bool = True


__all__ = ["foo"] # noqa
monkay = Monkay(
globals(),
with_extensions=True,
with_instance=True,
settings_path="tests.targets.not_existing_settings_path:Settings",
)
25 changes: 25 additions & 0 deletions tests/targets/module_notfound_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from monkay import Monkay

extras = {"foo": lambda: "foo"}


def __getattr__(name: str):
try:
return extras[name]
except KeyError as exc:
raise AttributeError from exc


class FakeApp:
is_fake_app: bool = True


__all__ = ["foo"] # noqa
monkay = Monkay(
globals(),
with_extensions=True,
with_instance=True,
settings_path="tests.targets.not_existing_settings_path:Settings",
settings_preloads_name="preloads",
settings_extensions_name="extensions",
)
51 changes: 51 additions & 0 deletions tests/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

import pytest

from monkay import UnsetError


@pytest.fixture(autouse=True, scope="function")
def cleanup():
Expand Down Expand Up @@ -32,6 +34,55 @@ def test_settings_basic():
assert mod.monkay.settings is hurray


def test_disabled_settings():
import tests.targets.module_disabled_settings as mod

with pytest.raises(AssertionError):
mod.monkay.evaluate_settings_once()

with pytest.raises(AssertionError):
mod.monkay.settings # noqa


def test_notfound_settings():
import tests.targets.module_notfound_settings as mod

assert not mod.monkay.settings_evaluated

mod.monkay.evaluate_settings_once()
assert not mod.monkay.settings_evaluated

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


def test_notevaluated_settings():
import tests.targets.module_notevaluated_settings as mod

assert mod.monkay.settings_evaluated

mod.monkay.evaluate_settings_once()
mod.monkay.evaluate_settings_once(ignore_import_errors=False)

# now evaluate settings
with pytest.raises(ImportError):
mod.monkay.settings # noqa


@pytest.mark.parametrize("value", [False, None, ""])
def test_unset_settings(value):
import tests.targets.module_full as mod

mod.monkay.settings = value

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

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


def test_settings_overwrite():
import tests.targets.module_full as mod

Expand Down

0 comments on commit ae070b4

Please sign in to comment.