1 2
import json
2 2
import os
3 2
import re
4 2
import warnings
5 2
from functools import wraps
6

7 2
from dynaconf.utils import extract_json_objects
8 2
from dynaconf.utils import multi_replace
9 2
from dynaconf.utils import recursively_evaluate_lazy_format
10 2
from dynaconf.utils.boxing import DynaBox
11 2
from dynaconf.vendor import toml
12

13 2
try:
14 2
    from jinja2 import Environment
15

16 2
    jinja_env = Environment()
17 2
    for p_method in ("abspath", "realpath", "relpath", "dirname", "basename"):
18 2
        jinja_env.filters[p_method] = getattr(os.path, p_method)
19
except ImportError:  # pragma: no cover
20
    jinja_env = None
21

22 2
true_values = ("t", "true", "enabled", "1", "on", "yes", "True")
23 2
false_values = ("f", "false", "disabled", "0", "off", "no", "False", "")
24

25

26 2
KV_PATTERN = re.compile(r"([a-zA-Z0-9 ]*=[a-zA-Z0-9\- :]*)")
27
"""matches `a=b, c=d, e=f` used on `VALUE='@merge foo=bar'` variables."""
28

29

30 2
class DynaconfParseError(Exception):
31
    """Error to raise when parsing @casts"""
32

33

34 2
class MetaValue:
35
    """A Marker to trigger specific actions on `set` and `object_merge`"""
36

37 2
    _meta_value = True
38

39 2
    def __init__(self, value, box_settings):
40 2
        self.box_settings = box_settings
41 2
        self.value = parse_conf_data(
42
            value, tomlfy=True, box_settings=box_settings
43
        )
44

45 2
    def __repr__(self):
46 2
        return f"{self.__class__.__name__}({self.value}) on {id(self)}"
47

48 2
    def unwrap(self):
49 2
        return self.value
50

51

52 2
class Reset(MetaValue):
53
    """Triggers an existing key to be reset to its value
54
    NOTE: DEPRECATED on v3.0.0
55
    """
56

57 2
    _dynaconf_reset = True
58

59 2
    def __init__(self, value, box_settings):
60 2
        self.box_settings = box_settings
61 2
        self.value = parse_conf_data(
62
            value, tomlfy=True, box_settings=self.box_settings
63
        )
64 2
        warnings.warn(f"{self.value} does not need `@reset` anymore.")
65

66

67 2
class Del(MetaValue):
68
    """Triggers an existing key to be deleted"""
69

70 2
    _dynaconf_del = True
71

72 2
    def unwrap(self):
73 2
        raise ValueError("Del object has no value")
74

75

76 2
class Merge(MetaValue):
77
    """Triggers an existing key to be merged"""
78

79 2
    _dynaconf_merge = True
80

81 2
    def __init__(self, value, box_settings, unique=False):
82 2
        self.box_settings = box_settings
83

84 2
        self.value = parse_conf_data(
85
            value, tomlfy=True, box_settings=box_settings
86
        )
87

88 2
        if isinstance(self.value, (int, float, bool)):
89
            # @merge 1, @merge 1.1, @merge False
90 2
            self.value = [self.value]
91 2
        elif isinstance(self.value, str):
92
            # @merge {"valid": "json"}
93 2
            json_object = list(
94
                extract_json_objects(
95
                    multi_replace(
96
                        self.value,
97
                        {
98
                            ": True": ": true",
99
                            ":True": ": true",
100
                            ": False": ": false",
101
                            ":False": ": false",
102
                            ": None": ": null",
103
                            ":None": ": null",
104
                        },
105
                    )
106
                )
107
            )
108 2
            if len(json_object) == 1:
109 2
                self.value = json_object[0]
110
            else:
111 2
                matches = KV_PATTERN.findall(self.value)
112
                # a=b, c=d
113 2
                if matches:
114 2
                    self.value = {
115
                        k.strip(): parse_conf_data(
116
                            v, tomlfy=True, box_settings=box_settings
117
                        )
118
                        for k, v in (
119
                            match.strip().split("=") for match in matches
120
                        )
121
                    }
122 2
                elif "," in self.value:
123
                    # @merge foo,bar
124 2
                    self.value = self.value.split(",")
125
                else:
126
                    # @merge foo
127 2
                    self.value = [self.value]
128

129 2
        self.unique = unique
130

131

132 2
class BaseFormatter:
133 2
    def __init__(self, function, token):
134 2
        self.function = function
135 2
        self.token = token
136

137 2
    def __call__(self, value, **context):
138 2
        return self.function(value, **context)
139

140 2
    def __str__(self):
141 2
        return str(self.token)
142

143

144 2
def _jinja_formatter(value, **context):
145
    if jinja_env is None:  # pragma: no cover
146
        raise ImportError(
147
            "jinja2 must be installed to enable '@jinja' settings in dynaconf"
148
        )
149 2
    return jinja_env.from_string(value).render(**context)
150

151

152 2
class Formatters:
153
    """Dynaconf builtin formatters"""
154

155 2
    python_formatter = BaseFormatter(str.format, "format")
156 2
    jinja_formatter = BaseFormatter(_jinja_formatter, "jinja")
157

158

159 2
class Lazy:
160
    """Holds data to format lazily."""
161

162 2
    _dynaconf_lazy_format = True
163

164 2
    def __init__(self, value, formatter=Formatters.python_formatter):
165 2
        self.value = value
166 2
        self.formatter = formatter
167

168 2
    @property
169
    def context(self):
170
        """Builds a context for formatting."""
171 2
        return {"env": os.environ, "this": self.settings}
172

173 2
    def __call__(self, settings):
174
        """LazyValue triggers format lazily."""
175 2
        self.settings = settings
176 2
        return self.formatter(self.value, **self.context)
177

178 2
    def __str__(self):
179
        """Gives string representation for the object."""
180 2
        return str(self.value)
181

182 2
    def __repr__(self):
183
        """Give the quoted str representation"""
184 2
        return f"'@{self.formatter} {self.value}'"
185

186 2
    def _dynaconf_encode(self):
187
        """Encodes this object values to be serializable to json"""
188 2
        return f"@{self.formatter} {self.value}"
189

190

191 2
def try_to_encode(value, callback=str):
192
    """Tries to encode a value by verifying existence of `_dynaconf_encode`"""
193 2
    try:
194 2
        return value._dynaconf_encode()
195 2
    except (AttributeError, TypeError):
196 2
        return callback(value)
197

198

199 2
def evaluate_lazy_format(f):
200
    """Marks a method on Settings instance to
201
    lazily evaluate LazyFormat objects upon access."""
202

203 2
    @wraps(f)
204
    def evaluate(settings, *args, **kwargs):
205 2
        value = f(settings, *args, **kwargs)
206 2
        return recursively_evaluate_lazy_format(value, settings)
207

208 2
    return evaluate
209

210

211 2
converters = {
212
    "@int": int,
213
    "@float": float,
214
    "@bool": lambda value: str(value).lower() in true_values,
215
    "@json": json.loads,
216
    "@format": lambda value: Lazy(value),
217
    "@jinja": lambda value: Lazy(value, formatter=Formatters.jinja_formatter),
218
    # Meta Values to trigger pre assignment actions
219
    "@reset": lambda value, box_settings: Reset(
220
        value, box_settings
221
    ),  # @reset is DEPRECATED on v3.0.0
222
    "@del": lambda value, box_settings: Del(value, box_settings),
223
    "@merge": lambda value, box_settings: Merge(value, box_settings),
224
    "@merge_unique": lambda value, box_settings: Merge(
225
        value, box_settings, unique=True
226
    ),
227
    # Special markers to be used as placeholders e.g: in prefilled forms
228
    # will always return None when evaluated
229
    "@note": lambda value: None,
230
    "@comment": lambda value: None,
231
    "@null": lambda value: None,
232
    "@none": lambda value: None,
233
}
234

235

236 2
def get_converter(converter_key, value, box_settings):
237 2
    converter = converters[converter_key]
238 2
    try:
239 2
        converted_value = converter(value, box_settings=box_settings)
240 2
    except TypeError:
241 2
        converted_value = converter(value)
242 2
    return converted_value
243

244

245 2
def parse_with_toml(data):
246
    """Uses TOML syntax to parse data"""
247 2
    try:
248 2
        return toml.loads(f"key={data}")["key"]
249 2
    except (toml.TomlDecodeError, KeyError):
250 2
        return data
251

252

253 2
def _parse_conf_data(data, tomlfy=False, box_settings=None):
254
    """
255
    @int @bool @float @json (for lists and dicts)
256
    strings does not need converters
257

258
    export DYNACONF_DEFAULT_THEME='material'
259
    export DYNACONF_DEBUG='@bool True'
260
    export DYNACONF_DEBUG_TOOLBAR_ENABLED='@bool False'
261
    export DYNACONF_PAGINATION_PER_PAGE='@int 20'
262
    export DYNACONF_MONGODB_SETTINGS='@json {"DB": "quokka_db"}'
263
    export DYNACONF_ALLOWED_EXTENSIONS='@json ["jpg", "png"]'
264
    """
265
    # not enforced to not break backwards compatibility with custom loaders
266 2
    box_settings = box_settings or {}
267

268 2
    cast_toggler = os.environ.get("AUTO_CAST_FOR_DYNACONF", "true").lower()
269 2
    castenabled = cast_toggler not in false_values
270

271 2
    if (
272
        castenabled
273
        and data
274
        and isinstance(data, str)
275
        and data.startswith(tuple(converters.keys()))
276
    ):
277 2
        parts = data.partition(" ")
278 2
        converter_key = parts[0]
279 2
        value = parts[-1]
280 2
        return get_converter(converter_key, value, box_settings)
281

282 2
    value = parse_with_toml(data) if tomlfy else data
283 2
    if isinstance(value, dict):
284 2
        value = DynaBox(value, box_settings=box_settings)
285 2
    return value
286

287

288 2
def parse_conf_data(data, tomlfy=False, box_settings=None):
289

290
    # not enforced to not break backwards compatibility with custom loaders
291 2
    box_settings = box_settings or {}
292

293 2
    if isinstance(data, (tuple, list)):
294
        # recursively parse each sequence item
295 2
        return [
296
            parse_conf_data(item, tomlfy=tomlfy, box_settings=box_settings)
297
            for item in data
298
        ]
299 2
    elif isinstance(data, (dict, DynaBox)):
300
        # recursively parse inner dict items
301 2
        _parsed = {}
302 2
        for k, v in data.items():
303 2
            _parsed[k] = parse_conf_data(
304
                v, tomlfy=tomlfy, box_settings=box_settings
305
            )
306 2
        return _parsed
307
    else:
308
        # return parsed string value
309 2
        return _parse_conf_data(data, tomlfy=tomlfy, box_settings=box_settings)
310

311

312 2
def unparse_conf_data(value):
313 2
    if isinstance(value, bool):
314 2
        return f"@bool {value}"
315 2
    elif isinstance(value, int):
316 2
        return f"@int {value}"
317 2
    elif isinstance(value, float):
318 2
        return f"@float {value}"
319 2
    elif isinstance(value, (list, dict)):
320 2
        return f"@json {json.dumps(value)}"
321 2
    elif isinstance(value, Lazy):
322 2
        return try_to_encode(value)
323 2
    elif value is None:
324 2
        return "@none "
325
    else:
326 2
        return value

Read our documentation on viewing source code .

Loading