Skip to content

Commit

Permalink
emergency release 0.3.0 (#20)
Browse files Browse the repository at this point in the history
emergency release

see changelog
  • Loading branch information
devkral authored Jan 31, 2025
1 parent ae070b4 commit 101cc85
Show file tree
Hide file tree
Showing 10 changed files with 222 additions and 112 deletions.
19 changes: 19 additions & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# Release notes

## Version 0.3.0

### Breaking

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 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

### Added
Expand Down
46 changes: 26 additions & 20 deletions docs/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,16 @@ 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.
Settings are only evaluated when calling `evaluate_settings`. This means you can do a lazy setup like this:

``` python
import os
from monkay import Monkay

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

# somewhere later
Expand All @@ -65,38 +61,48 @@ else:
monkay.settings = DebugSettings()

# now the settings are applied
monkay.evaluate_settings_once()
monkay.evaluate_settings()
```

### `evaluate_settings_once` method
## Multi stage settings setup

`evaluate_settings_once` has following keyword only parameter:
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.

- `on_conflict`: Matches the values of add_extension but defaults to `error`.
- `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.
``` python
import os
from monkay import Monkay

The return_value is `True` for a successful evaluation and `False` in the other case.
monkay = Monkay(
globals(),
# required for initializing settings feature
settings_path=""
)

!!! Note
`ignore_import_errors` suppresses also UnsetError which is raised when the settings are unset.
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
evaluated already and not optionally ignoring import errors.
The return_value is `True` for a successful evaluation and `False` in the other case.
It has has following keyword only parameter:

- `on_conflict`: Matches the values of add_extension but defaults to `keep`.

It is internally used by `evaluate_settings_once` and will also set the `settings_evaluated` flag.
- `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 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
but will still set the `settings_evaluated` flag to `True`.

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

### `settings_evaluated` flag

Internally it is a property which sets the right flag. Either on the ContextVar or on the instance.
Expand Down
16 changes: 15 additions & 1 deletion docs/specials.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,21 @@ 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. It returns True if all preloads succeeded.
This is useful together with `ignore_import_errors=True`.

### 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 All @@ -139,7 +154,6 @@ This is protocol is runtime checkable and has also support for both paramers.

Here a combined example:


```python
from dataclasses import dataclass

Expand Down
59 changes: 38 additions & 21 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,7 +40,18 @@ monkay = Monkay(
)
```

When providing your own `__all__` variable **after** providing Monkay or you want more controll, you can provide
``` python title="foo/main.py"
from foo import monkay
def get_application():
# sys.path updates
important_preloads =[...]
monkay.evaluate_preloads(important_preloads, ignore_import_errors=False)
extra_preloads =[...]
monkay.evaluate_preloads(extra_preloads)
monkay.evaluate_settings()
```

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

`skip_all_update=True`

Expand All @@ -49,8 +60,7 @@ and update the `__all__` value via `Monkay.update_all_var` if wanted.
!!! Warning
There is a catch when using `settings_preloads_name` and/or `settings_preloads_name`.
It is easy to run in circular dependency errors.
For fixing the initialization the errors are by default catched and ignored.
But this means, you have to apply them later via `evaluate_settings_once` later.
But this means, you have to apply them later via `evaluate_settings` later.
For more informations see [Settings preloads and/or extensions](#settings-extensions-andor-preloads)


Expand Down Expand Up @@ -128,6 +138,17 @@ class Settings(BaseSettings):
extensions: list[Any] = []
```

``` python title="main.py"

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
set via environment or defaults.
Note the `uncached_imports`. Because temporary overwrites should be possible, we should not cache this import. The settings
Expand Down Expand Up @@ -197,12 +218,11 @@ get_value_from_settings(monkay.settings, "foo")
```
#### Settings extensions and/or preloads

When using `settings_preloads_name` and/or `settings_extensions_name` it is easy to run in circular dependency issues
especcially with `pydantic_settings`.
For mitigating this such import errors are catched and silenced in init.
When using `settings_preloads_name` and/or `settings_extensions_name` we need to call in the setup of the application
`evaluate_settings()`. Otherwise we may would end up with circular depdendencies, missing imports and wrong library versions.

This means however the preloads are not loaded as well the extensions initialized.
For initializing it later, we need `evaluate_settings_once`.
For initializing it later, we need `evaluate_settings`.

Wherever the settings are expected we can add it.

Expand All @@ -214,7 +234,7 @@ from functools import lru_cache
@lru_cache
def get_edgy():
import edgy
edgy.monkay.evaluate_settings_once()
edgy.monkay.evaluate_settings(ignore_import_errors=False)
return edgy

class SettingsForward:
Expand All @@ -228,7 +248,7 @@ or
```python title="foo/main.py"
def get_application():
import foo
foo.monkay.evaluate_settings_once(ignore_import_errors=False)
foo.monkay.evaluate_settings(ignore_import_errors=False)
app = App()

foo.monkay.set_instance(app)
Expand All @@ -237,15 +257,11 @@ def get_application():
app = get_application()
```

For performance reasons you may want to skip to try to import the settings in init:

`evaluate_settings=False` will disable the evaluation.

You may want to not silence the import error like in monkay `<0.2.0`, then pass

`ignore_settings_import_errors=False` to the init.

More information can be found in [Settings `evaluate_settings_once`](./settings.md#evaluate_settings_once-method)
More information can be found in [Settings `evaluate_settings`](./settings.md#evaluate_settings-method)

#### Pathes

Expand Down Expand Up @@ -278,14 +294,18 @@ class Settings(BaseSettings):
preloads: list[str] = ["preloader:preloader"]
```

!!! 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

The instance feature is activated by providing a boolean (or a string for an explicit name) to the `with_instance`
parameter.

For entrypoints you can set now the instance via `set_instance`. A good entrypoint is the init and using the settings:


``` python title="__init__.py"
import os
from monkay import Monkay, load
Expand All @@ -299,6 +319,7 @@ monkay = Monkay(
settings_extensions_name="extensions",
)

monkay.evaluate_settings()
monkay.set_instance(load(settings.APP_PATH))
```

Expand Down Expand Up @@ -340,7 +361,6 @@ class Settings(BaseSettings):
preloads: list[str] = ["preloader:preloader"]
extensions: list[Any] = [Extension]
APP_PATH: str = "settings.App"

```

##### Reordering extension order dynamically
Expand All @@ -358,7 +378,6 @@ via the parameter `extension_order_key_fn`. It takes a key function which is exp

You can however intermix both.


## Tricks

### Type-checker friendly lazy imports
Expand All @@ -385,15 +404,13 @@ monkay = Monkay(
)
```


### Static `__all__`

For autocompletions it is helpful to have a static `__all__` variable because many tools parse the sourcecode.
Handling the `__all__` manually is for small imports easy but for bigger projects problematic.

Let's extend the former example:


``` python

import os
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.2"
__version__ = "0.3.0"
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
Loading

0 comments on commit 101cc85

Please sign in to comment.