File: test_hooking.py

package info (click to toggle)
python-dynaconf 3.2.12-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,900 kB
  • sloc: python: 21,464; sh: 9; makefile: 4
file content (268 lines) | stat: -rw-r--r-- 7,678 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
from __future__ import annotations

import pytest

from dynaconf import Dynaconf
from dynaconf.hooking import Action
from dynaconf.hooking import EagerValue
from dynaconf.hooking import get_hooks
from dynaconf.hooking import Hook
from dynaconf.hooking import hookable
from dynaconf.hooking import HookableSettings
from dynaconf.hooking import HookValue
from dynaconf.utils.boxing import DynaBox


class BaseHookedSettings:
    def __init__(self, **kwargs):
        self._store = DynaBox(kwargs)
        self._loaded_by_loaders = {}

    @property
    def __dict__(self):
        return self._store

    @hookable
    def get(self, key):
        return self._store.get(key)


def test_hook_dynaconf_class_get_templated():
    settings = Dynaconf(
        INTERNAL_VALUE=42,
        TEMPLATED="@format {this[INTERNAL_VALUE]}abc",
        TEMPLATED1="@int @format {this[INTERNAL_VALUE]}",
        TEMPLATED2="@jinja {{this.INTERNAL_VALUE}}abcd",
        TEMPLATED3="@int @jinja {{this.INTERNAL_VALUE}}",
        PERSON={"NAME": "Joe"},
        _wrapper_class=HookableSettings,
    )

    def just_assert_values_after_get(s, v, key, *_, **__):
        if key == "INTERNAL_VALUE":
            assert v == 99
        elif key == "PERSON":
            assert s["PERSON"] == {"NAME": "Joe", "city": "Valhala", "AGE": 18}
        else:
            assert s["PERSON"] == {"NAME": "Joe"}
            assert s["INTERNAL_VALUE"] == 42
        return v

    def set_internal_value_to_99_before_get(s, v, key, *_, **__):
        if key == "INTERNAL_VALUE":
            return EagerValue(99)
        return v

    def contribute_to_person_after_get(s, v, key, *_, **__):
        if key == "PERSON":
            data = {"PERSON__AGE": 18, "PERSON": "@merge city=Valhala"}
            s.update(data)
            return s.get(key)
        return v

    def assert_unchanged_person_value_before_get(s, v, key, *_, **__):
        assert s["PERSON"] == {"NAME": "Joe"}
        return v

    settings["_registered_hooks"] = {
        Action.BEFORE_GET: [
            Hook(set_internal_value_to_99_before_get),
            Hook(assert_unchanged_person_value_before_get),
        ],
        Action.AFTER_GET: [
            Hook(contribute_to_person_after_get),
            Hook(just_assert_values_after_get),
        ],
    }

    assert settings.TEMPLATED == "99abc"
    settings.set("FOOVALUE", 100)
    assert settings.FOOVALUE == 100
    assert settings["FOOVALUE"] == 100
    assert settings.TEMPLATED1 == 99
    assert settings.TEMPLATED2 == "99abcd"
    assert settings.TEMPLATED3 == 99
    assert settings.INTERNAL_VALUE == 99
    assert settings.get("INTERNAL_VALUE") == 99
    assert settings.PERSON.NAME == "Joe"
    assert settings.PERSON.AGE == 18
    assert settings.PERSON.CITY == "Valhala"


def test_hook_dynaconf_class_after_get():
    settings = Dynaconf(INTERNAL_VALUE=42, _wrapper_class=HookableSettings)
    assert settings.internal_value == 42

    def adds_one(s, v, *_, **__):
        return v + 1

    settings["_registered_hooks"] = {
        Action.AFTER_GET: [Hook(adds_one)],
    }
    hooks = get_hooks(settings)
    assert len(hooks) == 1
    assert settings.INTERNAL_VALUE == 43
    assert settings.get("INTERNAL_VALUE") == 43


def test_hooked_dict():
    class HookedDict(BaseHookedSettings, dict):
        _store = {}

        @hookable
        def get(self, key, default=None):
            return "to"

    d = HookedDict()
    d["_registered_hooks"] = {
        Action.AFTER_GET: [
            Hook(lambda s, v, *_, **__: f"{v}fu"),
        ],
    }
    assert d.get("key") == "tofu"


def test_hooked_dict_store():
    class HookedDict(BaseHookedSettings, dict): ...

    d = HookedDict(
        key="to",
        _registered_hooks={
            Action.AFTER_GET: [
                Hook(lambda s, v, *_, **__: f"{v}fu"),
            ],
        },
    )
    assert d.get("key") == "tofu"


def test_hook_before_and_after_bypass_method():
    """Method is never executed, before and after hooks are called"""

    class HookedSettings(BaseHookedSettings):
        _store = {}

        _registered_hooks = {
            # Accumulate all values
            Action.BEFORE_GET: [
                Hook(lambda s, v, *_, **__: "ba"),
                Hook(lambda s, v, *_, **__: EagerValue(f"{v.value}na")),
                # EagerValue is a special value that bypasses the method
                # and goes to the after hooks
            ],
            # After hooks makes the final value
            Action.AFTER_GET: [
                Hook(lambda s, v, *_, **__: f"{v.value}na"),
            ],
        }

        @hookable(name="get")
        def get(self, key):
            # 1st before hook will make value to be "ba"
            # 2nd before hook will make value to be "bana"
            # 1st after hook will make value to be "banana"
            return "value"  # this will never be executed

    settings = HookedSettings()
    assert settings.get("key") == "banana"


def test_hook_runs_after_method():
    """After method the after hooks transforms value."""

    DATABASE = {
        "feature_enabled": True,
    }

    def try_to_get_from_database(d, value, key, *_, **__):
        assert d.get("feature_enabled") is False
        return DATABASE.get(key, value.value)

    class HookedSettings(BaseHookedSettings): ...

    settings = HookedSettings(
        feature_enabled=False,
        something_not_in_database="default value",
        _registered_hooks={
            Action.AFTER_GET: [
                Hook(try_to_get_from_database),
            ],
        },
    )

    # On the object feature is disabled
    # but on the database it is enabled
    assert settings.get("feature_enabled") is True

    # This key is not in the database, so returns regular value
    assert settings.get("something_not_in_database") == "default value"


def test_hook_fail_with_wrong_parameters():
    """Hookable decorator fails when called with wrong parameters."""

    with pytest.raises(TypeError):

        @hookable("not a function")
        def foo():
            pass


def test_hook_values():
    value = HookValue(1)
    assert value == 1
    assert value != 2
    assert value == HookValue(1)
    assert value != HookValue(2)
    assert bool(value) is True
    assert str(value) == "1"
    assert repr(value) == repr(value.value)
    assert value + 1 == 2
    assert value - 1 == 0
    assert value * 2 == 2
    assert value / 2 == 0.5
    assert value // 2 == 0
    assert value % 2 == 1
    assert value**2 == 1
    assert divmod(value, 2) == (0, 1)

    value = HookValue([1, 2, 3])
    assert value == [1, 2, 3]
    assert value != [1, 2, 4]
    assert value == HookValue([1, 2, 3])
    assert value != HookValue([1, 2, 4])
    assert bool(value) is True
    assert str(value) == "[1, 2, 3]"
    assert repr(value) == repr(value.value)
    assert value[0] == 1
    assert value[1] == 2
    assert value[2] == 3
    assert len(value) == 3
    assert value[0:2] == [1, 2]
    assert 2 in value
    assert [x for x in value] == [1, 2, 3]

    class Dummy:
        pass

    _value = Dummy()
    value = HookValue(_value)
    assert value == value.value
    assert value != object()
    assert value == HookValue(_value)
    assert value != HookValue(object())
    assert bool(value) is True
    value.name = "dummy value"
    assert value.name == "dummy value"
    delattr(value, "name")
    assert not hasattr(value, "name")

    value = HookValue({})
    assert value == {}
    assert value != {"a": 1}
    assert value == HookValue({})
    value["a"] = 1
    assert value == {"a": 1}
    assert value["a"] == 1
    del value["a"]
    assert value == {}