Skip to content

Commit

Permalink
aggregate errors and expose method to manual inject env (#6)
Browse files Browse the repository at this point in the history
* aggregate errors and expose method to manual inject env

* add test

---------

Co-authored-by: Aaron <[email protected]>
  • Loading branch information
rabbit-aaron and Aaron authored Aug 29, 2023
1 parent 8ea95e1 commit 038e698
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 9 deletions.
70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,76 @@ settings.DATABASES
# }
# }
```

### IMPORTANT: If your settings are only imported in your settings file, the `__dir__` function cannot be injected automatically.

You will need to have at least 1 instance of Env in the settings module's global variable for the injection to work.

### Bad Example: won't work
```python
# settings.py
from other_settings import *
```

```python
# other_settings
from ragdoll.django import env

MY_ENV = env.Str()
```

```python
from django.conf import settings

type(settings.MY_ENV) # ragdoll.django.env.Str :(
```

### Example 1: add a str env variable to the settings module
```python
# settings.py
from ragdoll.django import env
from other_settings import *

MY_FOO = env.Str()
```

```python
# other_settings
from ragdoll.django import env

MY_ENV = env.Str()
```

```python
from django.conf import settings

type(settings.MY_FOO) # str :D
type(settings.MY_ENV) # str :D
```


### Example 2: manually inject the `__dir__`
```python
# settings.py
from other_settings import *
from ragdoll.django import env

env.configure()
```

```python
# other_settings
from ragdoll.django import env

MY_ENV = env.Str()
```

```python
from django.conf import settings

type(settings.MY_ENV) # str :D
```

### IMPORTANT: THIS WILL NOT WORK
Since the injected `__dir__` function only look at global variables, nested settings will not work,
You must assign the settings to a global variable first, then use it in nested settings.
Expand Down
44 changes: 35 additions & 9 deletions ragdoll/django/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,55 @@

from ragdoll import env
from ragdoll.env import BaseEnvEntry
from ragdoll.errors import ImproperlyConfigured


def dir_factory(module: ModuleType) -> typing.Callable[[], list[str]]:
def _dir_factory(module: ModuleType) -> typing.Callable[[], list[str]]:
def dir_() -> typing.Generator[str, None, None]:
mock_owner = SimpleNamespace(source=os.environ, case_sensitive=True)
errors = []
for name, value in module.__dict__.items():
if isinstance(value, BaseEnvEntry):
value.__set_name__(mock_owner, name) # type: ignore[arg-type]
setattr(module, name, value.__get__(mock_owner, None)) # type: ignore[arg-type]
try:
result = value.__get__(mock_owner, None) # type: ignore[arg-type]

except ImproperlyConfigured as improperly_configured:
errors.append(improperly_configured)
else:
setattr(module, name, result)
yield name
if errors:
raise ImproperlyConfigured(errors)

return lambda: list(dir_())


class ModuleNotFound(Exception):
pass


def _configure(frame_idx: int) -> None:
frame = inspect.stack()[frame_idx].frame
module = inspect.getmodule(frame)

if not module:
raise ModuleNotFound

if not getattr(module, "_dir_patched", False):
setattr(module, "__dir__", _dir_factory(module))
setattr(module, "_dir_patched", True)

del frame


def configure() -> None:
return _configure(frame_idx=2)


class DjangoEnvEntryMixin:
def __init__(self, *args, **kwargs):
frame = inspect.stack()[1].frame
module = inspect.getmodule(frame)
if not getattr(module, "_dir_patched", False):
module.__dir__ = dir_factory(module)
module._dir_patched = True

del frame
_configure(frame_idx=2)
super().__init__(*args, **kwargs)


Expand Down
18 changes: 18 additions & 0 deletions tests/test_django/test_django.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from types import ModuleType

import pytest

from ragdoll.django import env
from ragdoll.errors import ImproperlyConfigured


def test_django_dir_patched(mocker):
Expand Down Expand Up @@ -36,3 +39,18 @@ def test_django_bool_env(monkeypatch, mocker):
module.BAO = env.Bool()
dir(module)
assert module.BAO is True


def test_error_aggregation(mocker):
module = ModuleType("settings")
mocker.patch("inspect.getmodule", return_value=module)
module.BAO = env.Bool()
module.MEOW = env.Int()

with pytest.raises(ImproperlyConfigured) as exc_info:
dir(module)

assert type(exc_info.value.args[0][0]) is ImproperlyConfigured
assert exc_info.value.args[0][0].args[0] == "BAO setting was not set"
assert type(exc_info.value.args[0][1]) is ImproperlyConfigured
assert exc_info.value.args[0][1].args[0] == "MEOW setting was not set"

0 comments on commit 038e698

Please sign in to comment.