• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cdgriffith / Box / 464

pending completion
464

push

travis-ci

web-flow
Version 4.2.0 (#136)

* Adding optimizations for speed ups to creation and inserts
* Adding internal record of safe attributes for faster lookups, increases memory footprint for speed (thanks to Jonas Irgens Kylling)
* Adding all additional methods specific to `Box` as protected keys
* Fixing `merge_update` from incorrectly calling `__setattr__` which was causing a huge slowdown (thanks to Jonas Irgens Kylling)
* Fixing `copy` and `__copy__` not copying box options

116 of 116 new or added lines in 6 files covered. (100.0%)

797 of 798 relevant lines covered (99.87%)

3.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.77
/box/box.py
1
#!/usr/bin/env python
2
# -*- coding: UTF-8 -*-
3
#
4
# Copyright (c) 2017-2020 - Chris Griffith - MIT License
5
"""
4✔
6
Improved dictionary access through dot notation with additional tools.
7
"""
8
import copy
4✔
9
import re
4✔
10
import string
4✔
11
import warnings
4✔
12
from collections.abc import Iterable, Mapping, Callable
4✔
13
from keyword import kwlist
4✔
14
from pathlib import Path
4✔
15
from typing import Any, Union, Tuple, List, Dict
4✔
16

17
import box
4✔
18
from box.converters import (_to_json, _from_json, _from_toml, _to_toml, _from_yaml, _to_yaml, BOX_PARAMETERS)
4✔
19
from box.exceptions import BoxError, BoxKeyError, BoxTypeError, BoxValueError, BoxWarning
4✔
20

21
__all__ = ['Box']
4✔
22

23
_first_cap_re = re.compile('(.)([A-Z][a-z]+)')
4✔
24
_all_cap_re = re.compile('([a-z0-9])([A-Z])')
4✔
25
_list_pos_re = re.compile(r'\[(\d+)\]')
4✔
26

27
# a sentinel object for indicating no default, in order to allow users
28
# to pass `None` as a valid default value
29
NO_DEFAULT = object()
4✔
30

31

32
def _camel_killer(attr):
4✔
33
    """
34
    CamelKiller, qu'est-ce que c'est?
35

36
    Taken from http://stackoverflow.com/a/1176023/3244542
37
    """
38
    attr = str(attr)
4✔
39

40
    s1 = _first_cap_re.sub(r'\1_\2', attr)
4✔
41
    s2 = _all_cap_re.sub(r'\1_\2', s1)
4✔
42
    return re.sub(' *_+', '_', s2.lower())
4✔
43

44

45
def _recursive_tuples(iterable, box_class, recreate_tuples=False, **kwargs):
4✔
46
    out_list = []
4✔
47
    for i in iterable:
4✔
48
        if isinstance(i, dict):
4✔
49
            out_list.append(box_class(i, **kwargs))
4✔
50
        elif isinstance(i, list) or (recreate_tuples and isinstance(i, tuple)):
4✔
51
            out_list.append(_recursive_tuples(i, box_class, recreate_tuples, **kwargs))
4✔
52
        else:
53
            out_list.append(i)
4✔
54
    return tuple(out_list)
4✔
55

56

57
def _parse_box_dots(item):
4✔
58
    for idx, char in enumerate(item):
4✔
59
        if char == '[':
4✔
60
            return item[:idx], item[idx:]
4✔
61
        elif char == '.':
4✔
62
            return item[:idx], item[idx + 1:]
4✔
63
    raise BoxError('Could not split box dots properly')
4✔
64

65

66
def _get_box_config():
4✔
67
    return {
4✔
68
        # Internal use only
69
        '__created': False,
70
        '__safe_keys': {}
71
    }
72

73

74
class Box(dict):
4✔
75
    """
76
    Improved dictionary access through dot notation with additional tools.
77

78
    :param default_box: Similar to defaultdict, return a default value
79
    :param default_box_attr: Specify the default replacement.
80
        WARNING: If this is not the default 'Box', it will not be recursive
81
    :param default_box_none_transform: When using default_box, treat keys with none values as absent. True by default
82
    :param frozen_box: After creation, the box cannot be modified
83
    :param camel_killer_box: Convert CamelCase to snake_case
84
    :param conversion_box: Check for near matching keys as attributes
85
    :param modify_tuples_box: Recreate incoming tuples with dicts into Boxes
86
    :param box_safe_prefix: Conversion box prefix for unsafe attributes
87
    :param box_duplicates: "ignore", "error" or "warn" when duplicates exists in a conversion_box
88
    :param box_intact_types: tuple of types to ignore converting
89
    :param box_recast: cast certain keys to a specified type
90
    :param box_dots: access nested Boxes by period separated keys in string
91
    """
92

93
    _protected_keys = dir({}) + ['to_dict', 'to_json', 'to_yaml', 'from_yaml', 'from_json', 'from_toml', 'to_toml',
4✔
94
                                 '_Box__convert_and_store', '_Box__recast', '_Box__get_default', '_protected_keys',
95
                                 '_conversion_checks', 'merge_update', '_safe_attr']
96

97
    def __new__(cls, *args: Any, default_box: bool = False, default_box_attr: Any = NO_DEFAULT,
4✔
98
                default_box_none_transform: bool = True, frozen_box: bool = False, camel_killer_box: bool = False,
99
                conversion_box: bool = True, modify_tuples_box: bool = False, box_safe_prefix: str = 'x',
100
                box_duplicates: str = 'ignore', box_intact_types: Union[Tuple, List] = (),
101
                box_recast: Dict = None, box_dots: bool = False, **kwargs: Any):
102
        """
103
        Due to the way pickling works in python 3, we need to make sure
104
        the box config is created as early as possible.
105
        """
106
        obj = super(Box, cls).__new__(cls, *args, **kwargs)
4✔
107
        obj._box_config = _get_box_config()
4✔
108
        obj._box_config.update({
4✔
109
            'default_box': default_box,
110
            'default_box_attr': cls.__class__ if default_box_attr is NO_DEFAULT else default_box_attr,
111
            'default_box_none_transform': default_box_none_transform,
112
            'conversion_box': conversion_box,
113
            'box_safe_prefix': box_safe_prefix,
114
            'frozen_box': frozen_box,
115
            'camel_killer_box': camel_killer_box,
116
            'modify_tuples_box': modify_tuples_box,
117
            'box_duplicates': box_duplicates,
118
            'box_intact_types': tuple(box_intact_types),
119
            'box_recast': box_recast,
120
            'box_dots': box_dots
121
        })
122
        return obj
4✔
123

124
    def __init__(self, *args: Any, default_box: bool = False, default_box_attr: Any = NO_DEFAULT,
4✔
125
                 default_box_none_transform: bool = True, frozen_box: bool = False, camel_killer_box: bool = False,
126
                 conversion_box: bool = True, modify_tuples_box: bool = False, box_safe_prefix: str = 'x',
127
                 box_duplicates: str = 'ignore', box_intact_types: Union[Tuple, List] = (),
128
                 box_recast: Dict = None, box_dots: bool = False, **kwargs: Any):
129
        super().__init__()
4✔
130
        self._box_config = _get_box_config()
4✔
131
        self._box_config.update({
4✔
132
            'default_box': default_box,
133
            'default_box_attr': self.__class__ if default_box_attr is NO_DEFAULT else default_box_attr,
134
            'default_box_none_transform': default_box_none_transform,
135
            'conversion_box': conversion_box,
136
            'box_safe_prefix': box_safe_prefix,
137
            'frozen_box': frozen_box,
138
            'camel_killer_box': camel_killer_box,
139
            'modify_tuples_box': modify_tuples_box,
140
            'box_duplicates': box_duplicates,
141
            'box_intact_types': tuple(box_intact_types),
142
            'box_recast': box_recast,
143
            'box_dots': box_dots
144
        })
145
        if not self._box_config['conversion_box'] and self._box_config['box_duplicates'] != 'ignore':
4✔
146
            raise BoxError('box_duplicates are only for conversion_boxes')
4✔
147
        if len(args) == 1:
4✔
148
            if isinstance(args[0], str):
4✔
149
                raise BoxValueError('Cannot extrapolate Box from string')
4✔
150
            if isinstance(args[0], Mapping):
4✔
151
                for k, v in args[0].items():
4✔
152
                    if v is args[0]:
4✔
153
                        v = self
4✔
154
                    if v is None and self._box_config['default_box'] and self._box_config['default_box_none_transform']:
4✔
155
                        continue
4✔
156
                    self.__setitem__(k, v)
4✔
157
            elif isinstance(args[0], Iterable):
4✔
158
                for k, v in args[0]:
4✔
159
                    self.__setitem__(k, v)
4✔
160
            else:
161
                raise BoxValueError('First argument must be mapping or iterable')
4✔
162
        elif args:
4✔
163
            raise BoxTypeError(f'Box expected at most 1 argument, got {len(args)}')
4✔
164

165
        for k, v in kwargs.items():
4✔
166
            if args and isinstance(args[0], Mapping) and v is args[0]:
4✔
167
                v = self
4✔
168
            self.__setitem__(k, v)
4✔
169

170
        self._box_config['__created'] = True
4✔
171

172
    def __add__(self, other: dict):
4✔
173
        new_box = self.copy()
4✔
174
        if not isinstance(other, dict):
4✔
175
            raise BoxTypeError(f'Box can only merge two boxes or a box and a dictionary.')
4✔
176
        new_box.merge_update(other)
4✔
177
        return new_box
4✔
178

179
    def __hash__(self):
4✔
180
        if self._box_config['frozen_box']:
4✔
181
            hashing = 54321
4✔
182
            for item in self.items():
4✔
183
                hashing ^= hash(item)
4✔
184
            return hashing
4✔
185
        raise BoxTypeError('unhashable type: "Box"')
4✔
186

187
    def __dir__(self):
4✔
188
        allowed = string.ascii_letters + string.digits + '_'
4✔
189
        items = set(super().__dir__())
4✔
190
        # Only show items accessible by dot notation
191
        for key in self.keys():
4✔
192
            key = str(key)
4✔
193
            if ' ' not in key and key[0] not in string.digits and key not in kwlist:
4✔
194
                for letter in key:
4✔
195
                    if letter not in allowed:
4✔
196
                        break
4✔
197
                else:
198
                    items.add(key)
4✔
199

200
        for key in self.keys():
4✔
201
            if key not in items:
4✔
202
                if self._box_config['conversion_box']:
4✔
203
                    key = self._safe_attr(key)
4✔
204
                    if key:
4✔
205
                        items.add(key)
4✔
206

207
        return list(items)
4✔
208

209
    def get(self, key, default=NO_DEFAULT):
4✔
210
        if key not in self:
4✔
211
            if default is NO_DEFAULT:
4✔
212
                if self._box_config['default_box'] and self._box_config['default_box_none_transform']:
4✔
213
                    return self.__get_default(key)
4✔
214
                else:
215
                    return None
4✔
216
            if isinstance(default, dict) and not isinstance(default, Box):
4✔
217
                return Box(default)
4✔
218
            if isinstance(default, list) and not isinstance(default, box.BoxList):
4✔
219
                return box.BoxList(default)
4✔
220
            return default
4✔
221
        return self[key]
4✔
222

223
    def copy(self):
4✔
224
        return Box(super().copy(), **self.__box_config())
4✔
225

226
    def __copy__(self):
4✔
227
        return Box(super().copy(), **self.__box_config())
4✔
228

229
    def __deepcopy__(self, memodict=None):
4✔
230
        frozen = self._box_config['frozen_box']
4✔
231
        config = self.__box_config()
4✔
232
        config['frozen_box'] = False
4✔
233
        out = self.__class__(**config)
4✔
234
        memodict = memodict or {}
4✔
235
        memodict[id(self)] = out
4✔
236
        for k, v in self.items():
4✔
237
            out[copy.deepcopy(k, memodict)] = copy.deepcopy(v, memodict)
4✔
238
        out._box_config['frozen_box'] = frozen
4✔
239
        return out
4✔
240

241
    def __setstate__(self, state):
4✔
242
        self._box_config = state['_box_config']
4✔
243
        self.__dict__.update(state)
4✔
244

245
    def keys(self):
4✔
246
        return super().keys()
4✔
247

248
    def values(self):
4✔
249
        return [self[x] for x in self.keys()]
4✔
250

251
    def items(self):
4✔
252
        return [(x, self[x]) for x in self.keys()]
4✔
253

254
    def __get_default(self, item):
4✔
255
        default_value = self._box_config['default_box_attr']
4✔
256
        if default_value in (self.__class__, dict):
4✔
257
            value = self.__class__(**self.__box_config())
4✔
258
        elif isinstance(default_value, dict):
4✔
259
            value = self.__class__(**self.__box_config(), **default_value)
4✔
260
        elif isinstance(default_value, list):
4✔
261
            value = box.BoxList(**self.__box_config())
4✔
262
        elif isinstance(default_value, Callable):
4✔
263
            value = default_value()
4✔
264
        elif hasattr(default_value, 'copy'):
4✔
265
            value = default_value.copy()
4✔
266
        else:
267
            value = default_value
4✔
268
        self.__convert_and_store(item, value)
4✔
269
        return value
4✔
270

271
    def __box_config(self):
4✔
272
        out = {}
4✔
273
        for k, v in self._box_config.copy().items():
4✔
274
            if not k.startswith('__'):
4✔
275
                out[k] = v
4✔
276
        return out
4✔
277

278
    def __recast(self, item, value):
4✔
279
        if self._box_config['box_recast'] and item in self._box_config['box_recast']:
4✔
280
            try:
4✔
281
                return self._box_config['box_recast'][item](value)
4✔
282
            except ValueError:
4✔
283
                raise BoxValueError(f'Cannot convert {value} to {self._box_config["box_recast"][item]}') from None
4✔
284
        return value
4✔
285

286
    def __convert_and_store(self, item, value):
4✔
287
        if self._box_config['conversion_box']:
4✔
288
            safe_key = self._safe_attr(item)
4✔
289
            self._box_config['__safe_keys'][safe_key] = item
4✔
290
        if isinstance(value, (int, float, str, bytes, bytearray, bool, complex, set, frozenset)):
4✔
291
            return super().__setitem__(item, value)
4✔
292
        # If the value has already been converted or should not be converted, return it as-is
293
        if self._box_config['box_intact_types'] and isinstance(value, self._box_config['box_intact_types']):
4✔
294
            return super().__setitem__(item, value)
4✔
295
        # This is the magic sauce that makes sub dictionaries into new box objects
296
        if isinstance(value, dict) and not isinstance(value, Box):
4✔
297
            value = self.__class__(value, **self.__box_config())
4✔
298
        elif isinstance(value, list) and not isinstance(value, box.BoxList):
4✔
299
            if self._box_config['frozen_box']:
4✔
300
                value = _recursive_tuples(value,
4✔
301
                                          self.__class__,
302
                                          recreate_tuples=self._box_config['modify_tuples_box'],
303
                                          **self.__box_config())
304
            else:
305
                value = box.BoxList(value, box_class=self.__class__, **self.__box_config())
4✔
306
        elif self._box_config['modify_tuples_box'] and isinstance(value, tuple):
4✔
307
            value = _recursive_tuples(value, self.__class__, recreate_tuples=True, **self.__box_config())
4✔
308
        super().__setitem__(item, value)
4✔
309

310
    def __getitem__(self, item, _ignore_default=False):
4✔
311
        try:
4✔
312
            return super().__getitem__(item)
4✔
313
        except KeyError as err:
4✔
314
            if item == '_box_config':
4✔
315
                raise BoxKeyError('_box_config should only exist as an attribute and is never defaulted') from None
4✔
316
            if self._box_config['box_dots'] and isinstance(item, str) and ('.' in item or '[' in item):
4✔
317
                first_item, children = _parse_box_dots(item)
4✔
318
                if first_item in self.keys():
4✔
319
                    if hasattr(self[first_item], '__getitem__'):
4✔
320
                        return self[first_item][children]
4✔
321
            if self._box_config['camel_killer_box'] and isinstance(item, str):
4✔
322
                converted = _camel_killer(item)
4✔
323
                if converted in self.keys():
4✔
324
                    return super().__getitem__(converted)
4✔
325
            if self._box_config['default_box'] and not _ignore_default:
4✔
326
                return self.__get_default(item)
4✔
327
            raise BoxKeyError(str(err)) from None
4✔
328

329
    def __getattr__(self, item):
4✔
330
        try:
4✔
331
            try:
4✔
332
                value = self.__getitem__(item, _ignore_default=True)
4✔
333
            except KeyError:
4✔
334
                value = object.__getattribute__(self, item)
4✔
335
        except AttributeError as err:
4✔
336
            if item == '__getstate__':
4✔
337
                raise BoxKeyError(item) from None
4✔
338
            if item == '_box_config':
4✔
339
                raise BoxError('_box_config key must exist') from None
340
            if self._box_config['default_box']:
4✔
341
                return self.__get_default(item)
4✔
342
            if self._box_config['conversion_box']:
4✔
343
                safe_key = self._safe_attr(item)
4✔
344
                print(self._box_config['__safe_keys'])
4✔
345
                if safe_key in self._box_config['__safe_keys']:
4✔
346
                    return self.__getitem__(self._box_config['__safe_keys'][safe_key])
4✔
347
            raise BoxKeyError(str(err)) from None
4✔
348
        return value
4✔
349

350
    def __setitem__(self, key, value):
4✔
351
        if key != '_box_config' and self._box_config['__created'] and self._box_config['frozen_box']:
4✔
352
            raise BoxError('Box is frozen')
4✔
353
        if self._box_config['box_dots'] and isinstance(key, str) and '.' in key:
4✔
354
            first_item, children = _parse_box_dots(key)
4✔
355
            if first_item in self.keys():
4✔
356
                if hasattr(self[first_item], '__setitem__'):
4✔
357
                    return self[first_item].__setitem__(children, value)
4✔
358
        value = self.__recast(key, value)
4✔
359
        if key not in self.keys() and self._box_config['camel_killer_box']:
4✔
360
            if self._box_config['camel_killer_box'] and isinstance(key, str):
4✔
361
                key = _camel_killer(key)
4✔
362
        if self._box_config['conversion_box'] and self._box_config['box_duplicates'] != 'ignore':
4✔
363
            self._conversion_checks(key)
4✔
364
        self.__convert_and_store(key, value)
4✔
365

366
    def __setattr__(self, key, value):
4✔
367
        if key != '_box_config' and self._box_config['frozen_box'] and self._box_config['__created']:
4✔
368
            raise BoxError('Box is frozen')
4✔
369
        if key in self._protected_keys:
4✔
370
            raise BoxKeyError(f'Key name "{key}" is protected')
4✔
371
        if key == '_box_config':
4✔
372
            return object.__setattr__(self, key, value)
4✔
373
        value = self.__recast(key, value)
4✔
374
        safe_key = self._safe_attr(key)
4✔
375
        if safe_key in self._box_config['__safe_keys']:
4✔
376
            key = self._box_config['__safe_keys'][safe_key]
4✔
377
        self.__setitem__(key, value)
4✔
378

379
    def __delitem__(self, key):
4✔
380
        if self._box_config['frozen_box']:
4✔
381
            raise BoxError('Box is frozen')
4✔
382
        if key not in self.keys() and self._box_config['box_dots'] and isinstance(key, str) and '.' in key:
4✔
383
            first_item, children = key.split('.', 1)
4✔
384
            if first_item in self.keys() and isinstance(self[first_item], dict):
4✔
385
                return self[first_item].__delitem__(children)
4✔
386
        if key not in self.keys() and self._box_config['camel_killer_box']:
4✔
387
            if self._box_config['camel_killer_box'] and isinstance(key, str):
4✔
388
                for each_key in self:
4✔
389
                    if _camel_killer(key) == each_key:
4✔
390
                        key = each_key
4✔
391
                        break
4✔
392
        super().__delitem__(key)
4✔
393

394
    def __delattr__(self, item):
4✔
395
        if self._box_config['frozen_box']:
4✔
396
            raise BoxError('Box is frozen')
4✔
397
        if item == '_box_config':
4✔
398
            raise BoxError('"_box_config" is protected')
4✔
399
        if item in self._protected_keys:
4✔
400
            raise BoxKeyError(f'Key name "{item}" is protected')
4✔
401
        try:
4✔
402
            self.__delitem__(item)
4✔
403
        except KeyError as err:
4✔
404
            if self._box_config['conversion_box']:
4✔
405
                safe_key = self._safe_attr(item)
4✔
406
                if safe_key in self._box_config['__safe_keys']:
4✔
407
                    self.__delitem__(self._box_config['__safe_keys'][safe_key])
4✔
408
                    del self._box_config['__safe_keys'][safe_key]
4✔
409
                    return
4✔
410
            raise BoxKeyError(err)
411

412
    def pop(self, key, *args):
4✔
413
        if args:
4✔
414
            if len(args) != 1:
4✔
415
                raise BoxError('pop() takes only one optional argument "default"')
4✔
416
            try:
4✔
417
                item = self[key]
4✔
418
            except KeyError:
4✔
419
                return args[0]
4✔
420
            else:
421
                del self[key]
4✔
422
                return item
4✔
423
        try:
4✔
424
            item = self[key]
4✔
425
        except KeyError:
4✔
426
            raise BoxKeyError('{0}'.format(key)) from None
4✔
427
        else:
428
            del self[key]
4✔
429
            return item
4✔
430

431
    def clear(self):
4✔
432
        super().clear()
4✔
433
        self._box_config['__safe_keys'].clear()
4✔
434

435
    def popitem(self):
4✔
436
        try:
4✔
437
            key = next(self.__iter__())
4✔
438
        except StopIteration:
4✔
439
            raise BoxKeyError('Empty box') from None
4✔
440
        return key, self.pop(key)
4✔
441

442
    def __repr__(self):
4✔
443
        return f'<Box: {self.to_dict()}>'
4✔
444

445
    def __str__(self):
4✔
446
        return str(self.to_dict())
4✔
447

448
    def __iter__(self):
4✔
449
        for key in self.keys():
4✔
450
            yield key
4✔
451

452
    def __reversed__(self):
4✔
453
        for key in reversed(list(self.keys())):
4✔
454
            yield key
4✔
455

456
    def to_dict(self):
4✔
457
        """
458
        Turn the Box and sub Boxes back into a native python dictionary.
459

460
        :return: python dictionary of this Box
461
        """
462
        out_dict = dict(self)
4✔
463
        for k, v in out_dict.items():
4✔
464
            if v is self:
4✔
465
                out_dict[k] = out_dict
4✔
466
            elif hasattr(v, 'to_dict'):
4✔
467
                out_dict[k] = v.to_dict()
4✔
468
            elif hasattr(v, 'to_list'):
4✔
469
                out_dict[k] = v.to_list()
4✔
470
        return out_dict
4✔
471

472
    def update(self, __m=None, **kwargs):
4✔
473
        if __m:
4✔
474
            if hasattr(__m, 'keys'):
4✔
475
                for k in __m:
4✔
476
                    self.__convert_and_store(k, __m[k])
4✔
477
            else:
478
                for k, v in __m:
4✔
479
                    self.__convert_and_store(k, v)
4✔
480
        for k in kwargs:
4✔
481
            self.__convert_and_store(k, kwargs[k])
4✔
482

483
    def merge_update(self, __m=None, **kwargs):
4✔
484
        def convert_and_set(k, v):
4✔
485
            intact_type = (self._box_config['box_intact_types'] and isinstance(v, self._box_config['box_intact_types']))
4✔
486
            if isinstance(v, dict) and not intact_type:
4✔
487
                # Box objects must be created in case they are already
488
                # in the `converted` box_config set
489
                v = self.__class__(v, **self.__box_config())
4✔
490
                if k in self and isinstance(self[k], dict):
4✔
491
                    if isinstance(self[k], Box):
4✔
492
                        self[k].merge_update(v)
4✔
493
                    else:
494
                        self[k].update(v)
×
495
                    return
4✔
496
            if isinstance(v, list) and not intact_type:
4✔
497
                v = box.BoxList(v, **self.__box_config())
4✔
498
            self.__setitem__(k, v)
4✔
499

500
        if __m:
4✔
501
            if hasattr(__m, 'keys'):
4✔
502
                for key in __m:
4✔
503
                    convert_and_set(key, __m[key])
4✔
504
            else:
505
                for key, value in __m:
4✔
506
                    convert_and_set(key, value)
4✔
507
        for key in kwargs:
4✔
508
            convert_and_set(key, kwargs[key])
4✔
509

510
    def setdefault(self, item, default=None):
4✔
511
        if item in self:
4✔
512
            return self[item]
4✔
513

514
        if isinstance(default, dict):
4✔
515
            default = self.__class__(default, **self.__box_config())
4✔
516
        if isinstance(default, list):
4✔
517
            default = box.BoxList(default, box_class=self.__class__, **self.__box_config())
4✔
518
        self[item] = default
4✔
519
        return default
4✔
520

521
    def _safe_attr(self, attr):
4✔
522
        """Convert a key into something that is accessible as an attribute"""
523
        allowed = string.ascii_letters + string.digits + '_'
4✔
524

525
        if isinstance(attr, tuple):
4✔
526
            attr = "_".join([str(x) for x in attr])
4✔
527

528
        attr = attr.decode('utf-8', 'ignore') if isinstance(attr, bytes) else str(attr)
4✔
529
        if self.__box_config()['camel_killer_box']:
4✔
530
            attr = _camel_killer(attr)
4✔
531

532
        out = []
4✔
533
        last_safe = 0
4✔
534
        for i, character in enumerate(attr):
4✔
535
            if character in allowed:
4✔
536
                last_safe = i
4✔
537
                out.append(character)
4✔
538
            elif not out:
4✔
539
                continue
4✔
540
            else:
541
                if last_safe == i - 1:
4✔
542
                    out.append('_')
4✔
543

544
        out = "".join(out)[:last_safe + 1]
4✔
545

546
        try:
4✔
547
            int(out[0])
4✔
548
        except (ValueError, IndexError):
4✔
549
            pass
4✔
550
        else:
551
            out = f'{self.__box_config()["box_safe_prefix"]}{out}'
4✔
552

553
        if out in kwlist:
4✔
554
            out = f'{self.__box_config()["box_safe_prefix"]}{out}'
4✔
555

556
        return out
4✔
557

558
    def _conversion_checks(self, item):
4✔
559
        """
560
        Internal use for checking if a duplicate safe attribute already exists
561

562
        :param item: Item to see if a dup exists
563
        :param keys: Keys to check against
564
        """
565
        safe_item = self._safe_attr(item)
4✔
566

567
        if safe_item in self._box_config['__safe_keys']:
4✔
568
            dups = [f'{item}({safe_item})', f'{self._box_config["__safe_keys"][safe_item]}({safe_item})']
4✔
569
            if self._box_config['box_duplicates'].startswith('warn'):
4✔
570
                warnings.warn(f'Duplicate conversion attributes exist: {dups}', BoxWarning)
4✔
571
            else:
572
                raise BoxError(f'Duplicate conversion attributes exist: {dups}')
4✔
573

574
    def to_json(self, filename: Union[str, Path] = None, encoding: str = 'utf-8', errors: str = 'strict',
4✔
575
                **json_kwargs):
576
        """
577
        Transform the Box object into a JSON string.
578

579
        :param filename: If provided will save to file
580
        :param encoding: File encoding
581
        :param errors: How to handle encoding errors
582
        :param json_kwargs: additional arguments to pass to json.dump(s)
583
        :return: string of JSON (if no filename provided)
584
        """
585
        return _to_json(self.to_dict(), filename=filename, encoding=encoding, errors=errors, **json_kwargs)
4✔
586

587
    @classmethod
4✔
588
    def from_json(cls, json_string: str = None, filename: Union[str, Path] = None, encoding: str = 'utf-8',
4✔
589
                  errors: str = 'strict', **kwargs):
590
        """
591
        Transform a json object string into a Box object. If the incoming
592
        json is a list, you must use BoxList.from_json.
593

594
        :param json_string: string to pass to `json.loads`
595
        :param filename: filename to open and pass to `json.load`
596
        :param encoding: File encoding
597
        :param errors: How to handle encoding errors
598
        :param kwargs: parameters to pass to `Box()` or `json.loads`
599
        :return: Box object from json data
600
        """
601
        box_args = {}
4✔
602
        for arg in kwargs.copy():
4✔
603
            if arg in BOX_PARAMETERS:
4✔
604
                box_args[arg] = kwargs.pop(arg)
4✔
605

606
        data = _from_json(json_string, filename=filename, encoding=encoding, errors=errors, **kwargs)
4✔
607

608
        if not isinstance(data, dict):
4✔
609
            raise BoxError(f'json data not returned as a dictionary, but rather a {type(data).__name__}')
4✔
610
        return cls(data, **box_args)
4✔
611

612
    def to_yaml(self, filename: Union[str, Path] = None, default_flow_style: bool = False, encoding: str = 'utf-8',
4✔
613
                errors: str = 'strict', **yaml_kwargs):
614
        """
615
        Transform the Box object into a YAML string.
616

617
        :param filename:  If provided will save to file
618
        :param default_flow_style: False will recursively dump dicts
619
        :param encoding: File encoding
620
        :param errors: How to handle encoding errors
621
        :param yaml_kwargs: additional arguments to pass to yaml.dump
622
        :return: string of YAML (if no filename provided)
623
        """
624
        return _to_yaml(self.to_dict(), filename=filename, default_flow_style=default_flow_style,
4✔
625
                        encoding=encoding, errors=errors, **yaml_kwargs)
626

627
    @classmethod
4✔
628
    def from_yaml(cls, yaml_string: str = None, filename: Union[str, Path] = None, encoding: str = 'utf-8',
4✔
629
                  errors: str = 'strict', **kwargs):
630
        """
631
        Transform a yaml object string into a Box object. By default will use SafeLoader.
632

633
        :param yaml_string: string to pass to `yaml.load`
634
        :param filename: filename to open and pass to `yaml.load`
635
        :param encoding: File encoding
636
        :param errors: How to handle encoding errors
637
        :param kwargs: parameters to pass to `Box()` or `yaml.load`
638
        :return: Box object from yaml data
639
        """
640
        box_args = {}
4✔
641
        for arg in kwargs.copy():
4✔
642
            if arg in BOX_PARAMETERS:
4✔
643
                box_args[arg] = kwargs.pop(arg)
4✔
644

645
        data = _from_yaml(yaml_string=yaml_string, filename=filename, encoding=encoding, errors=errors, **kwargs)
4✔
646
        if not isinstance(data, dict):
4✔
647
            raise BoxError(f'yaml data not returned as a dictionary but rather a {type(data).__name__}')
4✔
648
        return cls(data, **box_args)
4✔
649

650
    def to_toml(self, filename: Union[str, Path] = None, encoding: str = 'utf-8', errors: str = 'strict'):
4✔
651
        """
652
        Transform the Box object into a toml string.
653

654
        :param filename: File to write toml object too
655
        :param encoding: File encoding
656
        :param errors: How to handle encoding errors
657
        :return: string of TOML (if no filename provided)
658
        """
659
        return _to_toml(self.to_dict(), filename=filename, encoding=encoding, errors=errors)
4✔
660

661
    @classmethod
4✔
662
    def from_toml(cls, toml_string: str = None, filename: Union[str, Path] = None,
4✔
663
                  encoding: str = 'utf-8', errors: str = 'strict', **kwargs):
664
        """
665
        Transforms a toml string or file into a Box object
666

667
        :param toml_string: string to pass to `toml.load`
668
        :param filename: filename to open and pass to `toml.load`
669
        :param encoding: File encoding
670
        :param errors: How to handle encoding errors
671
        :param kwargs: parameters to pass to `Box()`
672
        :return:
673
        """
674
        box_args = {}
4✔
675
        for arg in kwargs.copy():
4✔
676
            if arg in BOX_PARAMETERS:
4✔
677
                box_args[arg] = kwargs.pop(arg)
4✔
678

679
        data = _from_toml(toml_string=toml_string, filename=filename, encoding=encoding, errors=errors)
4✔
680
        return cls(data, **box_args)
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc