Skip to content

Commit ccc07e0

Browse files
authored
lazy settings evaluation with environment variables & with_full_overwrite (#26)
Changes: - allow lazy evaluating env variables - Allow unsetting settings via with_settings by using False or "". - docs - ruff formatting - bump version
1 parent 49f5af3 commit ccc07e0

21 files changed

+431
-148
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ repos:
1313
- id: end-of-file-fixer
1414
- id: trailing-whitespace
1515
- repo: https://github.com/charliermarsh/ruff-pre-commit
16-
rev: v0.7.3
16+
rev: v0.11.5
1717
hooks:
1818
- id: ruff
1919
args: ["--fix"]

docs/release-notes.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,19 @@ hide:
55

66
# Release notes
77

8+
## Version 0.4.0
9+
10+
### Added
11+
12+
- Add `with_full_overwrite` helper method which sets multiple contexts.
13+
- Add `evaluate_settings_with` parameter to `with_settings`.
14+
15+
### Changed
16+
17+
- When string or class is provided by a callable for settings it is parsed and cached.
18+
This allows lazy parsing of environment variables, so they can be changed programmatically.
19+
- Allow unsetting settings via with_settings by using False or "".
20+
821
## Version 0.3.0
922

1023
### Breaking

docs/settings.md

Lines changed: 9 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,13 @@ settings object.
1818
### Example: Child Package
1919

2020
```python
21-
import os
22-
from monkay import Monkay
23-
24-
monkay = Monkay(
25-
globals(),
26-
settings_path=os.environ.get("MONKAY_CHILD_SETTINGS", "foo.test:example") or ""
27-
)
21+
{!> ../docs_src/settings/forwarding_child.py !}
2822
```
2923

3024
### Example: Main Package
3125

3226
```python
33-
import os
34-
import child
35-
36-
monkay = Monkay(
37-
globals(),
38-
settings_path=os.environ.get("MONKAY_MAIN_SETTINGS", "foo.test:example") or ""
39-
)
40-
child.monkay.settings = lambda: monkay.settings
27+
{!> ../docs_src/settings/forwarding_main.py !}
4128
```
4229

4330
With this setup, the child package will use the settings from the main package, ensuring that all configurations
@@ -53,29 +40,11 @@ evaluation of settings until later in the application lifecycle.
5340
### Example:
5441

5542
```python
56-
import os
57-
from monkay import Monkay
58-
59-
monkay = Monkay(
60-
globals(),
61-
# Required for initializing settings feature
62-
settings_path=""
63-
)
64-
65-
# Lazy setup based on environment variables
66-
if not os.environ.get("DEBUG"):
67-
monkay.settings = os.environ.get("MONKAY_MAIN_SETTINGS", "foo.test:example") or ""
68-
elif os.environ.get("PERFORMANCE"):
69-
monkay.settings = DebugSettings
70-
else:
71-
monkay.settings = DebugSettings()
72-
73-
# Now the settings are applied
74-
monkay.evaluate_settings()
43+
{!> ../docs_src/settings/lazy_loader.py !}
7544
```
7645

7746
This approach allows for flexible configuration of the application, based on the environment, while deferring the
78-
actual evaluation of settings.
47+
actual evaluation of settings and os.environ.
7948

8049
---
8150

@@ -88,19 +57,7 @@ and handle errors more gracefully.
8857
### Example:
8958

9059
```python
91-
import os
92-
from monkay import Monkay
93-
94-
monkay = Monkay(
95-
globals(),
96-
# Required for initializing settings feature
97-
settings_path=""
98-
)
99-
100-
def find_settings():
101-
for path in ["a.settings", "b.settings.develop"]:
102-
if monkay.evaluate_settings(ignore_import_errors=True):
103-
break
60+
{!> ../docs_src/settings/multi_stage.py !}
10461
```
10562

10663
In this example, **Monkay** tries to evaluate settings from both `a.settings` and `b.settings.develop`, ignoring
@@ -147,16 +104,11 @@ It is reset when new settings are assigned and is initially set to `False` for i
147104
**Monkay** supports various ways of assigning settings:
148105

149106
- **String or Class**: Initialization happens the first time the settings are accessed and are cached afterward.
150-
- **Function**: The function gets evaluated each time the settings are accessed. If caching is required,
151-
- it is up to the function to handle it (e.g., forwarding settings can rely on caching in the main settings).
107+
- **Function**: The function gets evaluated each time the settings are accessed when returning an instance (useful for forwarding).
108+
Otherwise for types like str and class, the result is cached and used instead until the settings cache is cleared.
152109

153110
You can also use the `settings_path` parameter to assign a settings location directly, using either a string or
154-
class reference.
155-
156-
### Caching Behavior:
157-
- **String or Class**: These types are cached after the first evaluation.
158-
- **Function**: Functions are re-evaluated on every access. If needed, the caching mechanism can be handled within
159-
the function (e.g., caching results in the main settings).
111+
class reference. The caching behavior is the same.
160112

161113
---
162114

@@ -166,20 +118,7 @@ Sometimes, you may need to forward old settings to the **Monkay** settings. Whil
166118
creating a forwarder is easy. Here's an example:
167119

168120
```python
169-
from typing import Any, cast, TYPE_CHECKING
170-
171-
if TYPE_CHECKING:
172-
from .global_settings import EdgySettings
173-
174-
class SettingsForward:
175-
def __getattribute__(self, name: str) -> Any:
176-
import edgy
177-
return getattr(edgy.monkay.settings, name)
178-
179-
# Pretend the forward is the real object
180-
settings = cast("EdgySettings", SettingsForward())
181-
182-
__all__ = ["settings"]
121+
{!> ../docs_src/settings/forwarder.py !}
183122
```
184123

185124
### Note:

docs/testing.md

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ For testing purposes, **Monkay** provides three context manager methods that all
1515

1616
### Available Context Managers:
1717

18-
1. **`with_settings(settings)`**: Temporarily overwrites the settings for the scope of the context.
18+
1. **`with_settings(settings, *, evaluate_settings_with=None)`**: Temporarily overwrites the settings for the scope of the context. Optionally evaluate settings with provided parameters as dict. Enable evaluation by providing a dict.
1919
2. **`with_extensions(extensions_dict, *, apply_extensions=False)`**: Temporarily overwrites the extensions for the scope. The `apply_extensions` flag controls whether extensions are applied during the overwrite.
2020
3. **`with_instance(instance, *, apply_extensions=False, use_extensions_overwrite=True)`**: Temporarily overwrites the instance for the scope. You can also apply extensions and control the overwrite behavior using the respective flags.
2121

@@ -26,10 +26,10 @@ These context managers provide a flexible way to test different configurations o
2626
```python
2727
from monkay import Monkay
2828

29-
monkay = Monkay(globals())
29+
monkay = Monkay(globals(), settings_path="", with_extensions=True, with_instance=True)
3030

31-
# Overwrite settings temporarily
32-
with monkay.with_settings({"setting_name": "new_value"}):
31+
# Overwrite settings temporarily and evaluate it
32+
with monkay.with_settings({"setting_name": "new_value"}, evaluate_settings_with={}):
3333
assert monkay.settings.setting_name == "new_value"
3434

3535
# Overwrite extensions temporarily
@@ -43,6 +43,38 @@ with monkay.with_instance(new_instance, apply_extensions=False):
4343

4444
These context managers are especially useful for writing isolated and repeatable tests, where you may want to modify the environment without affecting the global state.
4545

46+
### Full overwrite
47+
48+
If multiple attributes should be simulated, e.g. for a virtual monkay environment, you can use
49+
`with_full_overwrite`. The advantage: you have already the right order set.
50+
Note: when not enabling a feature in monkay it will cause an error to provide a parameter for it.
51+
52+
#### Parameters
53+
54+
Parameters which need a feature enabled when creating the monkay instance:
55+
56+
**extensions** - Set an extra extensions set like with `with_extensions`. You might want to set it to {} for a clean extensions set.
57+
**settings** - Overwrite the settings if specified. It types matches the `with_settings` contextmanager.
58+
**instance** - Overwrite the instance if specified. It types matches the `with_instance` contextmanager.
59+
60+
Extra parameters:
61+
62+
- **apply_extensions** - Apply extensions. Only used when `extensions` parameter is used.
63+
- **evaluate_settings_with** - Pass options to `with_settings` parameter `evaluate_settings_with`. Only used when `settings` parameter is used.
64+
To enable evaluation pass `{}`
65+
66+
#### Example:
67+
68+
```python
69+
from monkay import Monkay
70+
71+
monkay = Monkay(globals(), settings_path="", with_extensions=True, with_instance=True)
72+
73+
# Overwrite everything temporarily and use a clean extensions set
74+
with monkay.with_full_overwrite(extensions={}, settings=..., instance=..., evaluate_settings_with={}):
75+
assert monkay.instance is new_instance
76+
```
77+
4678
---
4779

4880
## Check Imports and Exports

docs/tutorial.md

Lines changed: 15 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -26,49 +26,18 @@ pip install monkay
2626
Below is an example of how to set up **Monkay** in your project. You can use **Monkay** to manage dynamic imports, lazy loading, settings, extensions, and more.
2727

2828
```python title="foo/__init__.py"
29-
from monkay import Monkay
30-
31-
monkay = Monkay(
32-
# Required for auto-hooking
33-
globals(),
34-
with_extensions=True,
35-
with_instance=True,
36-
settings_path="settings_path:Settings",
37-
preloads=["tests.targets.module_full_preloaded1:load"],
38-
# Warning: settings names have a catch
39-
settings_preloads_name="preloads",
40-
settings_extensions_name="extensions",
41-
uncached_imports=["settings"],
42-
lazy_imports={
43-
"bar": "tests.targets.fn_module:bar",
44-
"settings": lambda: monkay.settings,
45-
},
46-
deprecated_lazy_imports={
47-
"deprecated": {
48-
"path": "tests.targets.fn_module:deprecated",
49-
"reason": "old.",
50-
"new_attribute": "super_new",
51-
}
52-
},
53-
)
29+
{!>../docs_src/tutorial/full_example_init.py}
5430
```
5531

5632
This configuration sets up **Monkay** with several features:
5733
- **Lazy imports** for `bar` and `settings`.
34+
- **Lazy evaluated settings_path** for being able to update the environment variable in code.
5835
- **Deprecated lazy imports** for `deprecated`.
5936
- **Preloads** and **extensions** for dynamic configuration.
6037
- **Uncached imports** to prevent caching specific imports like settings.
6138

6239
```python title="foo/main.py"
63-
from foo import monkay
64-
65-
def get_application():
66-
# sys.path updates
67-
important_preloads = [...]
68-
monkay.evaluate_preloads(important_preloads, ignore_import_errors=False)
69-
extra_preloads = [...]
70-
monkay.evaluate_preloads(extra_preloads)
71-
monkay.evaluate_settings()
40+
{!>../docs_src/tutorial/full_example_main.py}
7241
```
7342

7443
In `main.py`, the application is initialized by evaluating preloads and settings, ensuring that all required dependencies are loaded before use.
@@ -79,7 +48,7 @@ In `main.py`, the application is initialized by evaluating preloads and settings
7948

8049
After providing **Monkay**, if you need more control over the `__all__` variable, you can disable the automatic update of `__all__` by setting `skip_all_update=True`. You can later update it manually using `Monkay.update_all_var`.
8150

82-
**Warning**: Using `settings_preloads_name` or `settings_extensions_name` can sometimes cause circular dependency issues. To avoid such issues, ensure that you call `evaluate_settings()` later in the setup process. For more information, refer to [Settings Preloads and Extensions](#settings-preloads-andextensions).
51+
**Warning**: Using `settings_preloads_name` or `settings_extensions_name` can sometimes cause circular dependency issues. To avoid such issues, ensure that you call `evaluate_settings()` later in the setup process. For more information, refer to [Settings Preloads and Extensions](#settings-extensions-and-preloads).
8352

8453
---
8554

@@ -136,15 +105,16 @@ monkay = Monkay(
136105
globals(),
137106
with_extensions=True,
138107
with_instance=True,
139-
settings_path=os.environ.get("MONKAY_SETTINGS", "example.default.path.settings:Settings"),
108+
settings_path=lambda: os.environ.get("MONKAY_SETTINGS", "example.default.path.settings:Settings"),
140109
settings_preloads_name="preloads",
141110
settings_extensions_name="extensions",
142111
uncached_imports=["settings"],
143112
lazy_imports={"settings": lambda: monkay.settings}
144113
)
145114
```
146115

147-
Here, the `settings_path` is determined by an environment variable, and **Monkay** will use `settings` as the main settings object.
116+
Here, the `settings_path` is determined by an environment variable when accessed, and **Monkay** will use `settings` as the main settings object.
117+
The object will be cached.
148118

149119
```python title="settings.py"
150120
from pydantic_settings import BaseSettings
@@ -238,7 +208,7 @@ monkay = Monkay(
238208
globals(),
239209
with_extensions=True,
240210
with_instance=True,
241-
settings_path=os.environ.get("MONKAY_SETTINGS", "example.default.path.settings:settings"),
211+
settings_path=lambda: os.environ.get("MONKAY_SETTINGS", "example.default.path.settings:settings"),
242212
settings_preloads_name="preloads",
243213
settings_extensions_name="extensions",
244214
)
@@ -338,3 +308,10 @@ monkay = Monkay(
338308
```
339309

340310
This example updates `__all__` dynamically in the debug environment and ensures that lazy imports are added to `__all__`.
311+
312+
313+
### Sub monkay environment
314+
315+
Sometimes you want to provide temporily a different environment for a code path. You can do this with:
316+
317+
[`with_full_overwrite`](testing.md#full-overwrite)

docs_src/settings/forwarder.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from typing import TYPE_CHECKING, Any, cast
2+
3+
if TYPE_CHECKING:
4+
from .global_settings import EdgySettings
5+
6+
7+
class SettingsForward:
8+
def __getattribute__(self, name: str) -> Any:
9+
import edgy
10+
11+
return getattr(edgy.monkay.settings, name)
12+
13+
14+
# Pretend the forward is the real object
15+
settings = cast("EdgySettings", SettingsForward())
16+
17+
__all__ = ["settings"]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import os
2+
3+
from monkay import Monkay
4+
5+
monkay = Monkay(
6+
globals(),
7+
settings_path=lambda: os.environ.get("MONKAY_CHILD_SETTINGS", "foo.test:example") or "",
8+
)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import os
2+
3+
import child
4+
5+
from monkay import Monkay
6+
7+
monkay = Monkay(
8+
globals(),
9+
settings_path=lambda: os.environ.get("MONKAY_MAIN_SETTINGS", "foo.test:example") or "",
10+
)
11+
# because monkay.settings is an instance it uses not a cache for the settings
12+
child.monkay.settings = lambda: monkay.settings

docs_src/settings/lazy_loader.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import os
2+
from dataclasses import dataclass
3+
4+
from monkay import Monkay
5+
6+
7+
@dataclass
8+
class Settings:
9+
env: str
10+
11+
12+
@dataclass
13+
class ProductionSettings(Settings):
14+
env: str = "production"
15+
16+
17+
@dataclass
18+
class DebugSettings(Settings):
19+
env: str = "debug"
20+
21+
22+
def lazy_loader():
23+
# Lazy setup based on environment variables
24+
if not os.environ.get("DEBUG"):
25+
return os.environ.get("MONKAY_MAIN_SETTINGS", "foo.test:example") or ""
26+
elif os.environ.get("PERFORMANCE"):
27+
# must be class to be cached
28+
return ProductionSettings
29+
else:
30+
# not a class, will evaluated always on access
31+
return DebugSettings()
32+
33+
34+
monkay = Monkay(
35+
globals(),
36+
# Required for initializing settings feature
37+
settings_path=lazy_loader,
38+
)
39+
40+
# Now the settings are applied
41+
monkay.evaluate_settings()

0 commit comments

Comments
 (0)