chanfig 0.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
chanfig/config.py ADDED
@@ -0,0 +1,1739 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from argparse import ArgumentParser
5
+ from ast import literal_eval
6
+ from collections import OrderedDict as OrderedDict_
7
+ from collections.abc import Mapping
8
+ from contextlib import contextmanager
9
+ from copy import copy, deepcopy
10
+ from functools import wraps
11
+ from json import dumps as json_dumps
12
+ from json import loads as json_loads
13
+ from os import PathLike
14
+ from os.path import splitext
15
+ from typing import IO, Any, Callable, Iterable, List, Optional, Sequence, Union
16
+ from warnings import warn
17
+
18
+ from typing_extensions import LiteralString # type: ignore
19
+ from yaml import SafeDumper, SafeLoader
20
+ from yaml import dump as yaml_dump
21
+ from yaml import load as yaml_load
22
+
23
+ PathStr = Union[PathLike, str, bytes, LiteralString]
24
+ File = Union[PathStr, IO]
25
+
26
+ YAML = ("yml", "yaml")
27
+ JSON = ("json",)
28
+ PYTHON = ("py",)
29
+
30
+
31
+ class Dumper(SafeDumper): # pylint: disable=C0115
32
+ def increase_indent(self, flow: Optional[bool] = False, indentless: Optional[bool] = False):
33
+ return super().increase_indent(flow, indentless)
34
+
35
+
36
+ class FileError(ValueError): # pylint: disable=C0115
37
+ pass
38
+
39
+
40
+ class ConfigParser(ArgumentParser): # pylint: disable=C0115
41
+ def parse(
42
+ self,
43
+ args: Optional[Sequence[str]] = None,
44
+ config: Optional[Config] = None,
45
+ default_config: Optional[str] = None,
46
+ ) -> Config:
47
+ r"""
48
+ Parse the arguments for config.
49
+ There are three levels of config:
50
+
51
+ 1. The base config parsed into the function,
52
+ 2. The config file located at the path of default_config (if specified),
53
+ 3. The config specified in arguments.
54
+
55
+ Higher levels override lower levels (i.e. 3 > 2 > 1).
56
+
57
+ Args:
58
+ args (Optional[Sequence[str]]): The arguments to parse. Defaults to sys.argv[1:].
59
+ config (Optional[Config]): The base config. Defaults to an empty Config.
60
+ default_config (Optional[str]): The path to a config file.
61
+
62
+ Example:
63
+ ```python
64
+ >>> c = Config(a=0)
65
+ >>> c.to(dict)
66
+ {'a': 0}
67
+ >>> c.parse(['--a', '1', '--b', '2', '--c', '3']).to(dict)
68
+ {'a': 1, 'b': 2, 'c': 3}
69
+
70
+ ```
71
+ """
72
+
73
+ if args is None:
74
+ args = sys.argv[1:]
75
+ for arg in args:
76
+ if arg.startswith("--") and args != "--":
77
+ arg = arg.split("=")[0]
78
+ if arg not in self._option_string_actions:
79
+ self.add_argument(arg)
80
+ if config is None:
81
+ config = Config()
82
+ path = getattr(config, default_config, None) if default_config is not None else None
83
+ if path is not None:
84
+ warn(f"Config has 'default_config={path}' specified, its values will override values in Config")
85
+ parsed, _ = self.parse_known_args(args)
86
+ if path is not None:
87
+ config = config.update(path) # type: ignore
88
+ config = config.update(vars(parsed)) # type: ignore
89
+ return config # type: ignore
90
+
91
+ parse_config = parse
92
+
93
+ @staticmethod
94
+ def identity(string):
95
+ """
96
+ https://stackoverflow.com/questions/69896931/cant-pickle-local-object-argumentparser-init-locals-identity
97
+ """
98
+ return string
99
+
100
+
101
+ class Variable:
102
+ r"""
103
+ Mutable wrapper for immutable objects.
104
+
105
+ Example:
106
+ ```python
107
+ >>> v = Variable(1)
108
+ >>> n = v
109
+ >>> v, n
110
+ (1, 1)
111
+ >>> v += 1
112
+ >>> v, n
113
+ (2, 2)
114
+ >>> v.value = 3
115
+ >>> v, n
116
+ (3, 3)
117
+ >>> v.set(4)
118
+ >>> v, n
119
+ (4, 4)
120
+ >>> v = 5
121
+ >>> v, n
122
+ (5, 4)
123
+
124
+ ```
125
+ """
126
+
127
+ storage: List[Any]
128
+
129
+ def __init__(self, value):
130
+ self.storage = [value]
131
+
132
+ @property
133
+ def __class__(self):
134
+ return self.value.__class__
135
+
136
+ @__class__.setter
137
+ def __class__(self, value):
138
+ self.value.__class__ = value
139
+
140
+ @property
141
+ def value(self):
142
+ """
143
+ actual object stored in the Variable
144
+ """
145
+ return self.storage[0]
146
+
147
+ @value.setter
148
+ def value(self, value):
149
+ """
150
+ assign value to object stored in the Variable
151
+ """
152
+ self.storage[0] = self._get_value(value)
153
+
154
+ def get(self):
155
+ """
156
+ alias of value
157
+ """
158
+ return self.value
159
+
160
+ def set(self, value):
161
+ """
162
+ alias of value.setter
163
+ """
164
+ self.value = value
165
+
166
+ @staticmethod
167
+ def _get_value(obj):
168
+ if isinstance(obj, Variable):
169
+ return obj.value
170
+ return obj
171
+
172
+ def __getattr__(self, attr):
173
+ return getattr(self.value, attr)
174
+
175
+ def __lt__(self, other) -> bool:
176
+ return self.value < self._get_value(other)
177
+
178
+ def __le__(self, other) -> bool:
179
+ return self.value <= self._get_value(other)
180
+
181
+ def __eq__(self, other) -> bool:
182
+ return self.value == self._get_value(other)
183
+
184
+ def __ne__(self, other) -> bool:
185
+ return self.value != self._get_value(other)
186
+
187
+ def __ge__(self, other) -> bool:
188
+ return self.value >= self._get_value(other)
189
+
190
+ def __gt__(self, other) -> bool:
191
+ return self.value > self._get_value(other)
192
+
193
+ def __index__(self) -> int:
194
+ return int(self.value)
195
+
196
+ def __invert__(self):
197
+ return ~self.value
198
+
199
+ def __abs__(self):
200
+ return abs(self.value)
201
+
202
+ def __add__(self, other):
203
+ return Variable(self.value + self._get_value(other))
204
+
205
+ def __radd__(self, other):
206
+ return Variable(self._get_value(other) + self.value)
207
+
208
+ def __iadd__(self, other):
209
+ self.value += self._get_value(other)
210
+ return self
211
+
212
+ def __and__(self, other):
213
+ return Variable(self.value & self._get_value(other))
214
+
215
+ def __rand__(self, other):
216
+ return Variable(self._get_value(other) & self.value)
217
+
218
+ def __iand__(self, other):
219
+ self.value &= self._get_value(other)
220
+ return self
221
+
222
+ def __floordiv__(self, other):
223
+ return Variable(self.value // self._get_value(other))
224
+
225
+ def __rfloordiv__(self, other):
226
+ return Variable(self._get_value(other) // self.value)
227
+
228
+ def __ifloordiv__(self, other):
229
+ self.value //= self._get_value(other)
230
+ return self
231
+
232
+ def __mod__(self, other):
233
+ return Variable(self.value % self._get_value(other))
234
+
235
+ def __rmod__(self, other):
236
+ return Variable(self._get_value(other) % self.value)
237
+
238
+ def __imod__(self, other):
239
+ self.value %= self._get_value(other)
240
+ return self
241
+
242
+ def __mul__(self, other):
243
+ return Variable(self.value * self._get_value(other))
244
+
245
+ def __rmul__(self, other):
246
+ return Variable(self._get_value(other) * self.value)
247
+
248
+ def __imul__(self, other):
249
+ self.value *= self._get_value(other)
250
+ return self
251
+
252
+ def __matmul__(self, other):
253
+ return Variable(self.value @ self._get_value(other))
254
+
255
+ def __rmatmul__(self, other):
256
+ return Variable(self._get_value(other) @ self.value)
257
+
258
+ def __imatmul__(self, other):
259
+ self.value @= self._get_value(other)
260
+ return self
261
+
262
+ def __pow__(self, other):
263
+ return Variable(self.value ** self._get_value(other))
264
+
265
+ def __rpow__(self, other):
266
+ return Variable(self._get_value(other) ** self.value)
267
+
268
+ def __ipow__(self, other):
269
+ self.value **= self._get_value(other)
270
+ return self
271
+
272
+ def __truediv__(self, other):
273
+ return Variable(self.value / self._get_value(other))
274
+
275
+ def __rtruediv__(self, other):
276
+ return Variable(self._get_value(other) / self.value)
277
+
278
+ def __itruediv__(self, other):
279
+ self.value /= self._get_value(other)
280
+ return self
281
+
282
+ def __sub__(self, other):
283
+ return Variable(self.value - self._get_value(other))
284
+
285
+ def __rsub__(self, other):
286
+ return Variable(self._get_value(other) - self.value)
287
+
288
+ def __isub__(self, other):
289
+ self.value -= self._get_value(other)
290
+ return self
291
+
292
+ def __copy__(self):
293
+ return Variable(self.value)
294
+
295
+ def __deepcopy__(self, memo: Optional[Mapping] = None):
296
+ return Variable(copy(self.value))
297
+
298
+ def __repr__(self):
299
+ return repr(self.value)
300
+
301
+ def to(self, cls: Callable): # pylint: disable=C0103
302
+ r"""
303
+ Convert the value to a different type.
304
+
305
+ `convert` is an alias of this method.
306
+
307
+ Args:
308
+ cls (Callable): The type to convert to.
309
+
310
+ Example:
311
+ ```python
312
+ >>> id = Variable(1013)
313
+ >>> id.to(float)
314
+ 1013.0
315
+ >>> id.to(str)
316
+ '1013.0'
317
+
318
+ ```
319
+ """
320
+
321
+ self.value = cls(self.value)
322
+ return self
323
+
324
+ def int(self) -> int:
325
+ r"""
326
+ Convert the value to a python default int.
327
+
328
+ Example:
329
+ ```python
330
+ >>> id = Variable(1013.0)
331
+ >>> id.int()
332
+ 1013
333
+
334
+ ```
335
+ """
336
+
337
+ return self.to(int)
338
+
339
+ def float(self) -> float:
340
+ r"""
341
+ Convert the value to a python default float.
342
+
343
+ Example:
344
+ ```python
345
+ >>> id = Variable(1013)
346
+ >>> id.float()
347
+ 1013.0
348
+
349
+ ```
350
+ """
351
+
352
+ return self.to(float)
353
+
354
+ def str(self) -> str:
355
+ r"""
356
+ Convert the value to a python default float.
357
+
358
+ Example:
359
+ ```python
360
+ >>> id = Variable(1013)
361
+ >>> id.str()
362
+ '1013'
363
+
364
+ ```
365
+ """
366
+
367
+ return self.to(str)
368
+
369
+
370
+ class OrderedDict(OrderedDict_):
371
+ """
372
+ Default OrderedDict with attribute-style access.
373
+
374
+ Works best with `Variable` objects.
375
+
376
+ Example:
377
+ ```python
378
+ >>> d = OrderedDict()
379
+ >>> d.d = 1013
380
+ >>> d['d']
381
+ 1013
382
+ >>> d['i'] = 1031
383
+ >>> d.i
384
+ 1031
385
+ >>> d.a = Variable(1)
386
+ >>> d.b = d.a
387
+ >>> d.a, d.b
388
+ (1, 1)
389
+ >>> d.a += 1
390
+ >>> d.a, d.b
391
+ (2, 2)
392
+ >>> d.a = 3
393
+ >>> d.a, d.b
394
+ (3, 3)
395
+ >>> d.a = Variable('hello')
396
+ >>> d.a = d.a + ', world'
397
+ >>> d.b
398
+ 'hello, world'
399
+
400
+ ```
401
+ """
402
+
403
+ default_factory: Optional[Callable]
404
+ indent: int = 2
405
+
406
+ def __init__(self, *args, default_factory: Optional[Callable] = None, **kwargs):
407
+ super().__init__()
408
+ if default_factory is not None:
409
+ if callable(default_factory):
410
+ self.setattr("default_factory", default_factory)
411
+ else:
412
+ raise TypeError(
413
+ f"default_factory={default_factory} should be of type Callable, but got {type(default_factory)}"
414
+ )
415
+ self.setattr("indent", 2)
416
+ self._init(*args, **kwargs)
417
+ self.init()
418
+
419
+ def _init(self, *args, **kwargs) -> None:
420
+ r"""
421
+ Initialise values from arguments for OrderedDict.
422
+ This method is called in `__init__`.
423
+
424
+ Args:
425
+ *args: [(key1, value1), (key2, value2)].
426
+ **kwargs: {key1: value1, key2: value2}.
427
+
428
+ """
429
+ for key, value in args:
430
+ self.set(key, value)
431
+ for key, value in kwargs.items():
432
+ self.set(key, value)
433
+
434
+ def init(self) -> None:
435
+ r"""
436
+ Setting values for OrderedDict.
437
+ This method is called in `__init__`.
438
+ You can overwrite this method instead of `__init__` as constructor to avoid call of `super().__init__()`
439
+
440
+ Args:
441
+ *args: [(key1, value1), (key2, value2)].
442
+ **kwargs: {key1: value1, key2: value2}.
443
+
444
+ """
445
+
446
+ def get(self, name: str, default: Optional[Any] = None) -> Any:
447
+ r"""
448
+ Get value from OrderedDict.
449
+
450
+ `__getitem__` and `__getattr__` are alias of this method.
451
+
452
+ Note that default here will override the default_factory if specified.
453
+
454
+ Args:
455
+ name (str): Key name.
456
+ default (Optional[Any]): Default value if name does not present.
457
+
458
+ Example:
459
+ ```python
460
+ >>> d = OrderedDict(d=1013)
461
+ >>> d.get('d')
462
+ 1013
463
+ >>> d['d']
464
+ 1013
465
+ >>> d.d
466
+ 1013
467
+ >>> d.get('f', 2)
468
+ 2
469
+ >>> d.get('f')
470
+ Traceback (most recent call last):
471
+ KeyError: 'OrderedDict does not contain f'
472
+
473
+ ```
474
+ """
475
+
476
+ return super().__getitem__(name) if default is None else self.__missing__(name, default)
477
+
478
+ __getitem__ = get
479
+ __getattr__ = get
480
+
481
+ def getattr(self, name: str, default: Optional[Any] = None):
482
+ r"""
483
+ Get attribute of OrderedDict.
484
+
485
+ Note that if won't return value in the OrderedDict, nor will it create new one if default_factory is specified.
486
+
487
+ Args:
488
+ name (str): Key name.
489
+ default (Optional[Any]): Default value if name does not present.
490
+
491
+ Example:
492
+ ```python
493
+ >>> d = OrderedDict(a=1, default_factory=list)
494
+ >>> d.getattr('default_factory')
495
+ <class 'list'>
496
+ >>> d.getattr('b', 2)
497
+ 2
498
+ >>> d.getattr('a')
499
+ Traceback (most recent call last):
500
+ AttributeError: OrderedDict has no attribute a
501
+
502
+ ```
503
+ """
504
+
505
+ try:
506
+ try:
507
+ return self.__dict__[name]
508
+ except KeyError:
509
+ return self.__class__.__dict__[name]
510
+ except KeyError:
511
+ if default is not None:
512
+ return default
513
+ raise AttributeError(f"{self.__class__.__name__} has no attribute {name}") from None
514
+
515
+ def set(self, name: str, value: Any) -> None:
516
+ r"""
517
+ Set value of OrderedDict.
518
+
519
+ `__setitem__` and `__setattr__` are alias of this method.
520
+
521
+ Args:
522
+ name (str): Key name.
523
+ value (Any): Value to set.
524
+
525
+ Example:
526
+ ```python
527
+ >>> d = OrderedDict()
528
+ >>> d.set('d', 1013)
529
+ >>> d.get('d')
530
+ 1013
531
+ >>> d['d'] = 1031
532
+ >>> d.d
533
+ 1031
534
+ >>> d.d = 'chang'
535
+ >>> d['d']
536
+ 'chang'
537
+
538
+ ```
539
+ """
540
+
541
+ if isinstance(value, str):
542
+ try:
543
+ value = literal_eval(value)
544
+ except (TypeError, ValueError, SyntaxError):
545
+ pass
546
+ if name in self and isinstance(self[name], Variable):
547
+ self[name].set(value)
548
+ else:
549
+ super().__setitem__(name, value)
550
+
551
+ __setitem__ = set
552
+ __setattr__ = set
553
+
554
+ def setattr(self, name: str, value: Any):
555
+ r"""
556
+ Set attribute of OrderedDict.
557
+ Note that it won't alter values in the OrderedDict.
558
+
559
+ Args:
560
+ name (str): Key name.
561
+ value (Any): Value to set.
562
+
563
+ Example:
564
+ ```python
565
+ >>> d = OrderedDict()
566
+ >>> d.setattr('attr', 'value')
567
+ >>> d.getattr('attr')
568
+ 'value'
569
+
570
+ ```
571
+ """
572
+
573
+ self.__dict__[name] = value
574
+
575
+ def delete(self, name: str) -> None:
576
+ r"""
577
+ Remove value from OrderedDict.
578
+
579
+ `__delitem__`, `__delattr__` and `remove` are alias of this method.
580
+
581
+ Args:
582
+ name (str): Key name.
583
+
584
+ Example:
585
+ ```python
586
+ >>> d = OrderedDict(d=1016, n='chang')
587
+ >>> d.d
588
+ 1016
589
+ >>> d.n
590
+ 'chang'
591
+ >>> d.delete('d')
592
+ >>> d.d
593
+ Traceback (most recent call last):
594
+ KeyError: 'OrderedDict does not contain d'
595
+ >>> del d.n
596
+ >>> d.n
597
+ Traceback (most recent call last):
598
+ KeyError: 'OrderedDict does not contain n'
599
+ >>> del d.f
600
+ Traceback (most recent call last):
601
+ KeyError: 'f'
602
+
603
+ ```
604
+ """
605
+
606
+ super().__delitem__(name)
607
+
608
+ __delitem__ = delete
609
+ __delattr__ = delete
610
+ remove = delete
611
+
612
+ def delattr(self, name: str) -> None:
613
+ r"""
614
+ Remove attribute of OrderedDict.
615
+ Note that it won't remove values in the OrderedDict.
616
+
617
+ Args:
618
+ name (str): Key name.
619
+
620
+ Example:
621
+ ```python
622
+ >>> d = OrderedDict()
623
+ >>> d.setattr('name', 'chang')
624
+ >>> d.getattr('name')
625
+ 'chang'
626
+ >>> d.delattr('name')
627
+ >>> d.getattr('name')
628
+ Traceback (most recent call last):
629
+ AttributeError: OrderedDict has no attribute name
630
+
631
+ ```
632
+ """
633
+
634
+ del self.__dict__[name]
635
+
636
+ def __missing__(self, name: str, default: Optional[Any] = None) -> Any:
637
+ r"""
638
+ Allow dict to have default value if it doesn't exist.
639
+
640
+ Args:
641
+ name (str): Key name.
642
+ default (Optional[Any]): Default value if name does not present.
643
+
644
+ Example:
645
+ ```python
646
+ >>> d = OrderedDict(default_factory=list)
647
+ >>> d.n
648
+ []
649
+ >>> d.get('d', 1031)
650
+ 1031
651
+ >>> d.__missing__('d', 1031)
652
+ 1031
653
+
654
+ ```
655
+ """
656
+
657
+ if default is None:
658
+ # default_factory might not in __dict__ and cannot be replaced with if self.getattr("default_factory")
659
+ if "default_factory" not in self.__dict__:
660
+ raise KeyError(f"{self.__class__.__name__} does not contain {name}")
661
+ default_factory = self.getattr("default_factory")
662
+ default = default_factory()
663
+ if isinstance(default, OrderedDict):
664
+ default.__dict__.update(self.__dict__)
665
+ super().__setitem__(name, default)
666
+ return default
667
+
668
+ def to(self, cls: Callable = dict) -> Mapping: # pylint: disable=C0103
669
+ r"""
670
+ Convert OrderedDict to other Mapping.
671
+
672
+ `to` and `dict` are alias of this method.
673
+
674
+ Args:
675
+ cls (Callable): Target class to be converted to. Defaults to dict.
676
+
677
+ Example:
678
+ ```python
679
+ >>> d = OrderedDict(a=1, b=2, c=3)
680
+ >>> d.to(dict)
681
+ {'a': 1, 'b': 2, 'c': 3}
682
+
683
+ ```
684
+ """
685
+
686
+ return cls(**{k: v.value if isinstance(v, Variable) else v for k, v in self.items()})
687
+
688
+ convert = to
689
+ dict = to
690
+
691
+ def update(self, other: Union[Mapping, Iterable, PathStr]) -> OrderedDict: # type: ignore
692
+ r"""
693
+ Update OrderedDict values w.r.t. other.
694
+
695
+ `merge`, `merge_from_file`, and `union` are alias of this method.
696
+
697
+ Args:
698
+ other (Mapping | Iterable | PathStr): Other values to update.
699
+
700
+ Example:
701
+ ```python
702
+ >>> d = OrderedDict(a=1, b=2, c=3)
703
+ >>> n = {'b': 'b', 'c': 'c', 'd': 'd'}
704
+ >>> d.update(n).to(dict)
705
+ {'a': 1, 'b': 'b', 'c': 'c', 'd': 'd'}
706
+ >>> l = [('c', 3), ('d', 4)]
707
+ >>> d.update(l).to(dict)
708
+ {'a': 1, 'b': 'b', 'c': 3, 'd': 4}
709
+
710
+ ```
711
+ """
712
+
713
+ if isinstance(other, (PathLike, str, bytes)):
714
+ other = self.load(other)
715
+ if isinstance(other, (Mapping,)):
716
+ for key, value in other.items():
717
+ if key in self and isinstance(self[key], (Mapping,)) and isinstance(value, (Mapping,)):
718
+ self[key].update(value)
719
+ else:
720
+ self[key] = value
721
+ elif isinstance(other, Iterable):
722
+ for key, value in other: # type: ignore
723
+ self[key] = value
724
+ return self
725
+
726
+ merge = update
727
+ merge_from_file = update
728
+ union = update
729
+
730
+ def difference(self, other: Union[Mapping, Iterable, PathStr]) -> OrderedDict:
731
+ r"""
732
+ Difference between OrderedDict values and other.
733
+
734
+ `diff` is an alias of this method.
735
+
736
+ Args:
737
+ other (Mapping | Iterable | PathStr): Other values to compare.
738
+
739
+ Example:
740
+ ```python
741
+ >>> d = OrderedDict(a=1, b=2, c=3)
742
+ >>> n = {'b': 'b', 'c': 'c', 'd': 'd'}
743
+ >>> d.difference(n).to(dict)
744
+ {'b': 'b', 'c': 'c', 'd': 'd'}
745
+ >>> l = [('c', 3), ('d', 4)]
746
+ >>> d.difference(l).to(dict)
747
+ {'d': 4}
748
+ >>> d.difference(1)
749
+ Traceback (most recent call last):
750
+ TypeError: other=1 should be of type Mapping, Iterable or PathStr, but got <class 'int'>
751
+
752
+ ```
753
+ """
754
+
755
+ if isinstance(other, (PathLike, str, bytes)):
756
+ other = self.load(other)
757
+ if isinstance(other, (Mapping,)):
758
+ other = other.items()
759
+ if not isinstance(other, Iterable):
760
+ raise TypeError(f"other={other} should be of type Mapping, Iterable or PathStr, but got {type(other)}")
761
+ return self.empty(
762
+ **{key: value for key, value in other if key not in self or self[key] != value} # type: ignore
763
+ )
764
+
765
+ diff = difference
766
+
767
+ def intersection(self, other: Union[Mapping, Iterable, PathStr]) -> Mapping:
768
+ r"""
769
+ Intersection between OrderedDict values and other.
770
+
771
+ Args:
772
+ other (Mapping | Iterable | PathStr): Other values to join.
773
+
774
+ Example:
775
+ ```python
776
+ >>> d = OrderedDict(a=1, b=2, c=3)
777
+ >>> n = {'b': 'b', 'c': 'c', 'd': 'd'}
778
+ >>> d.intersection(n).to(dict)
779
+ {}
780
+ >>> l = [('c', 3), ('d', 4)]
781
+ >>> d.intersection(l).to(dict)
782
+ {'c': 3}
783
+ >>> d.intersection(1)
784
+ Traceback (most recent call last):
785
+ TypeError: other=1 should be of type Mapping, Iterable or PathStr, but got <class 'int'>
786
+
787
+ ```
788
+ """
789
+
790
+ if isinstance(other, (PathLike, str, bytes)):
791
+ other = self.load(other)
792
+ if isinstance(other, (Mapping,)):
793
+ other = other.items()
794
+ if not isinstance(other, Iterable):
795
+ raise TypeError(f"other={other} should be of type Mapping, Iterable or PathStr, but got {type(other)}")
796
+ return self.empty(**{key: value for key, value in other if key in self and self[key] == value}) # type: ignore
797
+
798
+ def copy(self) -> OrderedDict:
799
+ r"""
800
+ Create a shallow copy of OrderedDict.
801
+
802
+ Example:
803
+ ```python
804
+ >>> d = OrderedDict(a=[])
805
+ >>> d.setattr("name", "Chang")
806
+ >>> c = d.copy()
807
+ >>> c.to(dict)
808
+ {'a': []}
809
+ >>> d.a.append(1)
810
+ >>> c.to(dict)
811
+ {'a': [1]}
812
+ >>> c.getattr("name")
813
+ 'Chang'
814
+
815
+ ```
816
+ """
817
+
818
+ return copy(self)
819
+
820
+ def deepcopy(self, memo: Optional[Mapping] = None) -> OrderedDict:
821
+ r"""
822
+ Create a deep copy of OrderedDict.
823
+
824
+ `clone` and `__deepcopy__` are alias of this method.
825
+
826
+ Example:
827
+ ```python
828
+ >>> d = OrderedDict(a=[])
829
+ >>> d.setattr("name", "Chang")
830
+ >>> c = d.deepcopy()
831
+ >>> c.to(dict)
832
+ {'a': []}
833
+ >>> d.a.append(1)
834
+ >>> c.to(dict)
835
+ {'a': []}
836
+ >>> c.getattr("name")
837
+ 'Chang'
838
+
839
+ ```
840
+ """
841
+
842
+ if memo is not None and id(self) in memo:
843
+ return memo[id(self)]
844
+ ret = self.empty_like()
845
+ for k, v in self.items():
846
+ if isinstance(v, OrderedDict):
847
+ ret[k] = v.deepcopy(memo=memo)
848
+ else:
849
+ ret[k] = deepcopy(v)
850
+ return ret
851
+
852
+ __deepcopy__ = deepcopy
853
+
854
+ clone = deepcopy
855
+
856
+ @classmethod
857
+ def empty(cls, *args, **kwargs):
858
+ r"""
859
+ Initialise an empty OrderedDict.
860
+ This method is helpful when you inheriting the OrderedDict with default values.
861
+ Use type(self)() in this case would copy the redundant default values.
862
+
863
+ Example:
864
+ ```python
865
+ >>> d = OrderedDict(a=[])
866
+ >>> c = d.empty()
867
+ >>> c.to(dict)
868
+ {}
869
+
870
+ ```
871
+ """
872
+
873
+ empty = cls()
874
+ empty.clear()
875
+ empty._init(*args, **kwargs)
876
+ return empty
877
+
878
+ def empty_like(self, *args, **kwargs):
879
+ r"""
880
+ Initialise an empty copy of OrderedDict.
881
+ This method will preserve everything in __dict__.
882
+
883
+ Example:
884
+ ```python
885
+ >>> d = OrderedDict(a=[])
886
+ >>> d.setattr("name", "Chang")
887
+ >>> c = d.empty_like()
888
+ >>> c.to(dict)
889
+ {}
890
+ >>> c.getattr("name")
891
+ 'Chang'
892
+
893
+ ```
894
+ """
895
+
896
+ empty = self.empty(*args, **kwargs)
897
+ empty.__dict__.update(self.__dict__)
898
+ return empty
899
+
900
+ def json(self, file: File, *args, **kwargs) -> None:
901
+ r"""
902
+ Dump OrderedDict to json file.
903
+
904
+ This function calls `self.jsons()` to generate json string.
905
+ You may overwrite `jsons` in case something is not json serializable.
906
+
907
+ Example:
908
+ ```python
909
+ >>> d = OrderedDict(a=1, b=2, c=3)
910
+ >>> d.json("example.json")
911
+
912
+ ```
913
+ """
914
+ with self.open(file, mode="w") as fp:
915
+ fp.write(self.jsons(*args, **kwargs))
916
+
917
+ @classmethod
918
+ def from_json(cls, file: File, *args, **kwargs) -> OrderedDict:
919
+ r"""
920
+ Construct OrderedDict from json file.
921
+
922
+ This function calls `self.from_jsons()` to constract object from json string.
923
+ You may overwrite `from_jsons` in case something is not json serializable.
924
+
925
+ Example:
926
+ ```python
927
+ >>> d = OrderedDict.from_json('example.json')
928
+ >>> d.to(dict)
929
+ {'a': 1, 'b': 2, 'c': 3}
930
+
931
+ ```
932
+ """
933
+ with cls.open(file) as fp:
934
+ return cls.from_jsons(fp.read(), *args, **kwargs)
935
+
936
+ def jsons(self, *args, **kwargs) -> str:
937
+ r"""
938
+ Dump OrderedDict to json string.
939
+
940
+ Example:
941
+ ```python
942
+ >>> d = OrderedDict(a=1, b=2, c=3)
943
+ >>> d.jsons()
944
+ '{\n "a": 1,\n "b": 2,\n "c": 3\n}'
945
+
946
+ ```
947
+ """
948
+
949
+ if "indent" not in kwargs:
950
+ kwargs["indent"] = self.getattr("indent")
951
+ return json_dumps(self.to(dict), *args, **kwargs)
952
+
953
+ @classmethod
954
+ def from_jsons(cls, string: str, *args, **kwargs) -> OrderedDict:
955
+ r"""
956
+ Construct OrderedDict from json string.
957
+
958
+ Example:
959
+ ```python
960
+ >>> d = OrderedDict.from_jsons('{\n "a": 1,\n "b": 2,\n "c": 3\n}')
961
+ >>> d.to(dict)
962
+ {'a': 1, 'b': 2, 'c': 3}
963
+
964
+ ```
965
+ """
966
+
967
+ return cls(**json_loads(string, *args, **kwargs))
968
+
969
+ def yaml(self, file: File, *args, **kwargs) -> None:
970
+ r"""
971
+ Dump OrderedDict to yaml file.
972
+
973
+ This function calls `self.yamls()` to generate yaml string.
974
+ You may overwrite `yamls` in case something is not yaml serializable.
975
+
976
+ Example:
977
+ ```python
978
+ >>> d = OrderedDict(a=1, b=2, c=3)
979
+ >>> d.yaml("example.yaml")
980
+
981
+ ```
982
+ """
983
+ with self.open(file, mode="w") as fp:
984
+ self.yamls(fp, *args, **kwargs)
985
+
986
+ @classmethod
987
+ def from_yaml(cls, file: File, *args, **kwargs) -> OrderedDict:
988
+ r"""
989
+ Construct OrderedDict from yaml file.
990
+
991
+ This function calls `self.from_yamls()` to constract object from yaml string.
992
+ You may overwrite `from_yamls` in case something is not yaml serializable.
993
+
994
+ ```python
995
+ >>> d = OrderedDict.from_yaml('example.yaml')
996
+ >>> d.to(dict)
997
+ {'a': 1, 'b': 2, 'c': 3}
998
+
999
+ ```
1000
+ """
1001
+ with cls.open(file) as fp:
1002
+ return cls.from_yamls(fp.read(), *args, **kwargs)
1003
+
1004
+ def yamls(self, *args, **kwargs) -> str:
1005
+ r"""
1006
+ Dump OrderedDict to yaml string.
1007
+
1008
+ Example:
1009
+ ```python
1010
+ >>> d = OrderedDict(a=1, b=2, c=3)
1011
+ >>> d.yamls()
1012
+ 'a: 1\nb: 2\nc: 3\n'
1013
+
1014
+ ```
1015
+ """
1016
+
1017
+ if "Dumper" not in kwargs:
1018
+ kwargs["Dumper"] = Dumper
1019
+ if "indent" not in kwargs:
1020
+ kwargs["indent"] = self.getattr("indent")
1021
+ return yaml_dump(self.to(dict), *args, **kwargs) # type: ignore
1022
+
1023
+ @classmethod
1024
+ def from_yamls(cls, string: str, *args, **kwargs) -> OrderedDict:
1025
+ r"""
1026
+ Construct OrderedDict from yaml string.
1027
+
1028
+ Example:
1029
+ ```python
1030
+ >>> d = OrderedDict.from_yamls('a: 1\nb: 2\nc: 3\n')
1031
+ >>> d.to(dict)
1032
+ {'a': 1, 'b': 2, 'c': 3}
1033
+
1034
+ ```
1035
+ """
1036
+
1037
+ if "Loader" not in kwargs:
1038
+ kwargs["Loader"] = SafeLoader
1039
+ return cls(**yaml_load(string, *args, **kwargs))
1040
+
1041
+ def dump(self, file: File, method: Optional[str] = None, *args, **kwargs) -> None: # pylint: disable=W1113
1042
+ r"""
1043
+ Dump OrderedDict to file.
1044
+
1045
+ Example:
1046
+ ```python
1047
+ >>> d = OrderedDict(a=1, b=2, c=3)
1048
+ >>> d.dump("example.yaml")
1049
+
1050
+ ```
1051
+ """
1052
+ if method is None:
1053
+ if isinstance(file, IO):
1054
+ raise ValueError("method must be specified when dumping to file-like object")
1055
+ method = splitext(file)[-1][1:] # type: ignore
1056
+ extension = method.lower() # type: ignore
1057
+ if extension in YAML:
1058
+ return self.yaml(file=file, *args, **kwargs) # type: ignore
1059
+ if extension in JSON:
1060
+ return self.json(file=file, *args, **kwargs) # type: ignore
1061
+ raise FileError(f"file {file} should be in {JSON} or {YAML}, but got {extension}")
1062
+
1063
+ @classmethod
1064
+ def load(cls, file: File, method: Optional[str] = None, *args, **kwargs) -> OrderedDict: # pylint: disable=W1113
1065
+ """
1066
+ Load OrderedDict from file.
1067
+
1068
+ Example:
1069
+ ```python
1070
+ >>> d = OrderedDict.load("example.yaml")
1071
+ >>> d.to(dict)
1072
+ {'a': 1, 'b': 2, 'c': 3}
1073
+
1074
+ ```
1075
+ """
1076
+
1077
+ if method is None:
1078
+ if isinstance(file, IO):
1079
+ raise ValueError("method must be specified when loading from file-like object")
1080
+ method = splitext(file)[-1][1:] # type: ignore
1081
+ extension = method.lower() # type: ignore
1082
+ if extension in JSON:
1083
+ return cls.from_json(file, *args, **kwargs)
1084
+ if extension in YAML:
1085
+ return cls.from_yaml(file, *args, **kwargs)
1086
+ raise FileError("file {file} should be in {JSON} or {YAML}, but got {extension}")
1087
+
1088
+ @staticmethod
1089
+ @contextmanager
1090
+ def open(file: File, *args, **kwargs):
1091
+ """
1092
+ open file IO from file path or file-like object.
1093
+ """
1094
+
1095
+ if isinstance(file, (PathLike, str)):
1096
+ file = open(file, *args, **kwargs)
1097
+ try:
1098
+ yield file
1099
+ finally:
1100
+ file.close()
1101
+ elif isinstance(file, (IO,)):
1102
+ yield file
1103
+ else:
1104
+ raise TypeError(
1105
+ f"file={file} should be of type (str, os.PathLike) or (io.IOBase), but got {type(file)}" # type: ignore
1106
+ )
1107
+
1108
+ @staticmethod
1109
+ def extra_repr() -> str: # pylint: disable=C0116
1110
+ return ""
1111
+
1112
+ def __repr__(self):
1113
+ r"""
1114
+ Representation of OrderedDict.
1115
+
1116
+ Example:
1117
+ ```python
1118
+ >>> d = OrderedDict(a=1, b=2, c=3)
1119
+ >>> repr(d)
1120
+ 'OrderedDict(\n (a): 1\n (b): 2\n (c): 3\n)'
1121
+
1122
+ ```
1123
+ """
1124
+
1125
+ extra_lines = []
1126
+ extra_repr = self.extra_repr()
1127
+ # empty string will be split into list ['']
1128
+ if extra_repr:
1129
+ extra_lines = extra_repr.split("\n")
1130
+ child_lines = []
1131
+ for key, value in self.items():
1132
+ value_str = repr(value)
1133
+ value_str = self._add_indent(value_str)
1134
+ child_lines.append("(" + key + "): " + value_str)
1135
+ lines = extra_lines + child_lines
1136
+
1137
+ main_str = self.__class__.__name__ + "("
1138
+ if lines:
1139
+ # simple one-liner info, which most builtin Modules will use
1140
+ if len(extra_lines) == 1 and not child_lines:
1141
+ main_str += extra_lines[0]
1142
+ else:
1143
+ main_str += "\n " + "\n ".join(lines) + "\n"
1144
+
1145
+ main_str += ")"
1146
+ return main_str
1147
+
1148
+ def _add_indent(self, s):
1149
+ st = s.split("\n")
1150
+ # don't do anything for single-line stuff
1151
+ if len(st) == 1:
1152
+ return s
1153
+ first = st.pop(0)
1154
+ st = [(self.getattr("indent") * " ") + line for line in st]
1155
+ st = "\n".join(st)
1156
+ st = first + "\n" + st
1157
+ return st
1158
+
1159
+ def __setstate__(self, states, *args, **kwargs):
1160
+ for name, value in states.items():
1161
+ self.setattr(name, value)
1162
+
1163
+
1164
+ class NestedDict(OrderedDict):
1165
+ """
1166
+ Nested Dict
1167
+ """
1168
+
1169
+ convert_mapping: bool = False
1170
+ default_factory: Optional[Callable]
1171
+ default_mapping: Optional[Callable]
1172
+ delimiter: str = "."
1173
+ indent: int = 2
1174
+
1175
+ def __init__(self, *args, default_factory: Optional[Callable] = None, **kwargs):
1176
+ self.setattr("delimiter", ".")
1177
+ self.setattr("convert_mapping", False)
1178
+ self.setattr("default_mapping", NestedDict)
1179
+ super().__init__(*args, default_factory=default_factory, **kwargs)
1180
+
1181
+ def _init(self, *args, **kwargs) -> None:
1182
+ for key, value in args:
1183
+ self.set(key, value, convert_mapping=True)
1184
+ for key, value in kwargs.items():
1185
+ self.set(key, value, convert_mapping=True)
1186
+
1187
+ def get(self, name: str, default: Optional[Any] = None) -> Any:
1188
+ r"""
1189
+ Get value from NestedDict.
1190
+
1191
+ `__getitem__` and `__getattr__` are alias of this method.
1192
+
1193
+ Note that default here will override the default_factory if specified.
1194
+
1195
+ Args:
1196
+ name (str): Key name.
1197
+ default (Optional[Any]): Default value if name does not present.
1198
+
1199
+ Example:
1200
+ ```python
1201
+ >>> d = NestedDict(default_factory=NestedDict)
1202
+ >>> d['i.d'] = 1013
1203
+ >>> d.get('i.d')
1204
+ 1013
1205
+ >>> d['i.d']
1206
+ 1013
1207
+ >>> d.i.d
1208
+ 1013
1209
+ >>> d.get('c', 2)
1210
+ 2
1211
+ >>> d.get('c')
1212
+ NestedDict()
1213
+ >>> d = NestedDict()
1214
+ >>> d.get('c')
1215
+ Traceback (most recent call last):
1216
+ KeyError: 'NestedDict does not contain c'
1217
+
1218
+ ```
1219
+ """
1220
+
1221
+ while self.getattr("delimiter") in name:
1222
+ name, rest = name.split(self.getattr("delimiter"), 1)
1223
+ self, name = self[name], rest # pylint: disable=W0642
1224
+ return super().get(name, default)
1225
+
1226
+ __getitem__ = get
1227
+ __getattr__ = get
1228
+
1229
+ def set( # pylint: disable=W0221
1230
+ self,
1231
+ name: str,
1232
+ value: Any,
1233
+ convert_mapping: Optional[bool] = None,
1234
+ ) -> None:
1235
+ r"""
1236
+ Set value of NestedDict.
1237
+
1238
+ `__setitem__` and `__setattr__` are alias of this method.
1239
+
1240
+ Args:
1241
+ name (str): Key name.
1242
+ value (Any): Value to set.
1243
+
1244
+ Example:
1245
+ ```python
1246
+ >>> d = NestedDict(default_factory=NestedDict)
1247
+ >>> d.set('i.d', 1013)
1248
+ >>> d.i.d
1249
+ 1013
1250
+ >>> d.d.i = 1031
1251
+ >>> d['d.i']
1252
+ 1031
1253
+ >>> d['n.l'] = 'chang'
1254
+ >>> d.n.l
1255
+ 'chang'
1256
+ >>> d.f.n = 'liu'
1257
+ >>> d['f.n']
1258
+ 'liu'
1259
+ >>> d.setattr('convert_mapping', True)
1260
+ >>> d.a.b = {'c': {'d': 1}, 'e.f' : 2}
1261
+ >>> d.a.b.c.d
1262
+ 1
1263
+ >>> d.a.b.e.f
1264
+ 2
1265
+
1266
+
1267
+ ```
1268
+ """
1269
+
1270
+ default_mapping = self.getattr("default_mapping")
1271
+ if convert_mapping is None:
1272
+ convert_mapping = self.convert_mapping
1273
+ while self.getattr("delimiter") in name:
1274
+ name, rest = name.split(self.getattr("delimiter"), 1)
1275
+ if name not in self:
1276
+ if convert_mapping:
1277
+ super().__setitem__(name, default_mapping())
1278
+ else:
1279
+ self.__missing__(name)
1280
+ self, name = self[name], rest # pylint: disable=W0642
1281
+ if convert_mapping and isinstance(value, Mapping):
1282
+ value = default_mapping(**value)
1283
+ super().__setitem__(name, value)
1284
+
1285
+ __setitem__ = set
1286
+ __setattr__ = set
1287
+
1288
+ def pop(self, name: str, default: Optional[Any] = None) -> Any:
1289
+ r"""
1290
+ Pop value from NestedDict.
1291
+
1292
+ Args:
1293
+ name (str): Key name.
1294
+ default (Optional[Any]): Default value if name does not present.
1295
+
1296
+ Example:
1297
+ ```python
1298
+ >>> d = NestedDict(default_factory=NestedDict)
1299
+ >>> d['i.d'] = 1013
1300
+ >>> d.pop('i.d')
1301
+ 1013
1302
+ >>> d.pop('i.d', True)
1303
+ True
1304
+ >>> d.pop('i.d')
1305
+ Traceback (most recent call last):
1306
+ KeyError: 'd'
1307
+
1308
+ ```
1309
+ """
1310
+
1311
+ if self.getattr("delimiter") in name:
1312
+ name, rest = name.split(self.getattr("delimiter"), 1)
1313
+ if name not in self:
1314
+ raise KeyError(f"{self.__class__.__name__} does not contain {name}")
1315
+ return self[name].pop(rest, default)
1316
+ return super().pop(name, default) if default is not None else super().pop(name)
1317
+
1318
+ def all_keys(self):
1319
+ r"""
1320
+ Get all keys of NestedDict.
1321
+
1322
+ Example:
1323
+ ```python
1324
+ >>> d = NestedDict(**{'a': 1, 'b': {'c': 2, 'd': 3}})
1325
+ >>> list(d.all_keys())
1326
+ ['a', 'b.c', 'b.d']
1327
+
1328
+ ```
1329
+ """
1330
+
1331
+ @wraps(self.all_keys)
1332
+ def all_keys(self, prefix=""):
1333
+ for key, value in self.items():
1334
+ if prefix:
1335
+ key = prefix + self.getattr("delimiter") + key
1336
+ if isinstance(value, NestedDict):
1337
+ yield from all_keys(value, key)
1338
+ else:
1339
+ yield key
1340
+
1341
+ return all_keys(self)
1342
+
1343
+ def all_values(self):
1344
+ r"""
1345
+ Get all values of NestedDict.
1346
+
1347
+ Example:
1348
+ ```python
1349
+ >>> d = NestedDict(**{'a': 1, 'b': {'c': 2, 'd': 3}})
1350
+ >>> list(d.all_values())
1351
+ [1, 2, 3]
1352
+
1353
+ ```
1354
+ """
1355
+
1356
+ for value in self.values():
1357
+ if isinstance(value, NestedDict):
1358
+ yield from value.all_values()
1359
+ else:
1360
+ yield value
1361
+
1362
+ def all_items(self):
1363
+ r"""
1364
+ Get all items of NestedDict.
1365
+
1366
+ Example:
1367
+ ```python
1368
+ >>> d = NestedDict(**{'a': 1, 'b': {'c': 2, 'd': 3}})
1369
+ >>> list(d.all_items())
1370
+ [('a', 1), ('b.c', 2), ('b.d', 3)]
1371
+
1372
+ ```
1373
+ """
1374
+
1375
+ @wraps(self.all_items)
1376
+ def all_items(self, prefix=""):
1377
+ for key, value in self.items():
1378
+ if prefix:
1379
+ key = prefix + self.getattr("delimiter") + key
1380
+ if isinstance(value, NestedDict):
1381
+ yield from all_items(value, key)
1382
+ else:
1383
+ yield key, value
1384
+
1385
+ return all_items(self)
1386
+
1387
+ def apply(self, func: Callable) -> NestedDict:
1388
+ r"""
1389
+ Recursively apply a function to the object and its childrens.
1390
+
1391
+ Args:
1392
+ runc (Callable): Function to be applied to.
1393
+
1394
+ Example:
1395
+ ```python
1396
+ >>> d = NestedDict()
1397
+ >>> d.a = NestedDict()
1398
+ >>> def func(d):
1399
+ ... d.t = 1
1400
+ >>> d.apply(func).to(dict)
1401
+ {'a': {'t': 1}, 't': 1}
1402
+
1403
+ ```
1404
+ """
1405
+
1406
+ for value in self.values():
1407
+ if isinstance(value, NestedDict):
1408
+ value.apply(func)
1409
+ func(self)
1410
+ return self
1411
+
1412
+ def to(self, cls: Callable = dict) -> Mapping: # pylint: disable=C0103
1413
+ r"""
1414
+ Convert NestedDict to other Mapping.
1415
+
1416
+ `to` and `dict` are alias of this method.
1417
+
1418
+ Args:
1419
+ cls (Callable): Target class to be converted to.
1420
+
1421
+ Example:
1422
+ ```python
1423
+ >>> d = NestedDict(default_factory=NestedDict, a=1, b=2, c=3)
1424
+ >>> d['i.d'] = 1013
1425
+ >>> d.to(dict)
1426
+ {'a': 1, 'b': 2, 'c': 3, 'i': {'d': 1013}}
1427
+
1428
+ ```
1429
+ """
1430
+
1431
+ ret = cls()
1432
+ for k, v in self.items():
1433
+ if isinstance(v, OrderedDict):
1434
+ v = v.convert(cls)
1435
+ ret[k] = v
1436
+ return ret
1437
+
1438
+ convert = to
1439
+ dict = to
1440
+
1441
+
1442
+ class DefaultDict(NestedDict):
1443
+ """
1444
+ NestedDict with default_factory set to NestedDict by default.
1445
+ """
1446
+
1447
+ def __init__(self, *args, default_factory: Optional[Callable] = None, **kwargs):
1448
+ if default_factory is None:
1449
+ default_factory = NestedDict
1450
+ super().__init__(*args, default_factory=default_factory, **kwargs)
1451
+
1452
+
1453
+ def frozen_check(func: Callable):
1454
+ """
1455
+ Decorator check if the object is frozen.
1456
+
1457
+ """
1458
+
1459
+ @wraps(func)
1460
+ def decorator(self, *args, **kwargs):
1461
+ if self.getattr("frozen"):
1462
+ raise ValueError("Attempting to alter a frozen config. Run config.defrost() to defrost first")
1463
+ return func(self, *args, **kwargs)
1464
+
1465
+ return decorator
1466
+
1467
+
1468
+ class Config(NestedDict):
1469
+ """
1470
+ Basic Config
1471
+ """
1472
+
1473
+ frozen: bool = False
1474
+ convert_mapping: bool = False
1475
+ default_factory: Optional[Callable]
1476
+ delimiter: str = "."
1477
+ indent: int = 2
1478
+ parser: ConfigParser
1479
+
1480
+ def __init__(self, *args, **kwargs):
1481
+ self.setattr("frozen", False)
1482
+ self.setattr("parser", ConfigParser())
1483
+ super().__init__(*args, default_factory=Config, **kwargs)
1484
+ self.setattr("convert_mapping", True)
1485
+
1486
+ @frozen_check
1487
+ def set(
1488
+ self,
1489
+ name: str,
1490
+ value: Any,
1491
+ convert_mapping: Optional[bool] = None,
1492
+ ) -> None:
1493
+ r"""
1494
+ Set value of Config.
1495
+
1496
+ `__setitem__` and `__setattr__` are alias of this method.
1497
+
1498
+ Args:
1499
+ name (str): Key name.
1500
+ value (Any): Value to set.
1501
+
1502
+ Example:
1503
+ ```python
1504
+ >>> c = Config()
1505
+ >>> c['i.d'] = 1013
1506
+ >>> c.i.d
1507
+ 1013
1508
+ >>> c.freeze().to(dict)
1509
+ {'i': {'d': 1013}}
1510
+ >>> c['i.d'] = 1031
1511
+ Traceback (most recent call last):
1512
+ ValueError: Attempting to alter a frozen config. Run config.defrost() to defrost first
1513
+ >>> c.defrost().to(dict)
1514
+ {'i': {'d': 1013}}
1515
+ >>> c['i.d'] = 1031
1516
+ >>> c.i.d
1517
+ 1031
1518
+
1519
+ ```
1520
+ """
1521
+
1522
+ return super().set(name, value, convert_mapping)
1523
+
1524
+ __setitem__ = set
1525
+ __setattr__ = set
1526
+
1527
+ @frozen_check
1528
+ def delete(self, name: str) -> None:
1529
+ r"""
1530
+ Remove value from Config.
1531
+
1532
+ `__delitem__`, `__delattr__` and `remove` are alias of this method.
1533
+
1534
+ Args:
1535
+ name (str): Key name.
1536
+
1537
+ Example:
1538
+ ```python
1539
+ >>> d = OrderedDict(d=1016, n='chang')
1540
+ >>> d.d
1541
+ 1016
1542
+ >>> d.n
1543
+ 'chang'
1544
+ >>> d.delete('d')
1545
+ >>> d.d
1546
+ Traceback (most recent call last):
1547
+ KeyError: 'OrderedDict does not contain d'
1548
+ >>> del d.n
1549
+ >>> d.n
1550
+ Traceback (most recent call last):
1551
+ KeyError: 'OrderedDict does not contain n'
1552
+ >>> del d.f
1553
+ Traceback (most recent call last):
1554
+ KeyError: 'f'
1555
+
1556
+ ```
1557
+ """
1558
+ super().remove(name)
1559
+
1560
+ __delitem__ = delete
1561
+ __delattr__ = delete
1562
+ remove = delete
1563
+
1564
+ @frozen_check
1565
+ def pop(self, name: str, default: Optional[Any] = None) -> Any:
1566
+ r"""
1567
+ Pop value from Config.
1568
+
1569
+ Args:
1570
+ name (str): Key name.
1571
+ default (Optional[Any]): Default value if name does not present.
1572
+
1573
+ Example:
1574
+ ```python
1575
+ >>> c = Config()
1576
+ >>> c['i.d'] = 1013
1577
+ >>> c.pop('i.d')
1578
+ 1013
1579
+ >>> c.pop('i.d', True)
1580
+ True
1581
+ >>> c.freeze().to(dict)
1582
+ {'i': {}}
1583
+ >>> c['i.d'] = 1031
1584
+ Traceback (most recent call last):
1585
+ ValueError: Attempting to alter a frozen config. Run config.defrost() to defrost first
1586
+ >>> c.defrost().to(dict)
1587
+ {'i': {}}
1588
+ >>> c['i.d'] = 1031
1589
+ >>> c.pop('i.d')
1590
+ 1031
1591
+
1592
+ ```
1593
+ """
1594
+
1595
+ return super().pop(name, default)
1596
+
1597
+ def freeze(self, recursive: Optional[bool] = True) -> Config:
1598
+ r"""
1599
+ Freeze the config.
1600
+
1601
+ `lock` is an alias of this method.
1602
+
1603
+ Args:
1604
+ recursive (Optional[bool]): freeze all sub-configs recursively. Defaults to True.
1605
+
1606
+ Example:
1607
+ ```python
1608
+ >>> c = Config()
1609
+ >>> c.getattr('frozen')
1610
+ False
1611
+ >>> c.freeze().to(dict)
1612
+ {}
1613
+ >>> c.getattr('frozen')
1614
+ True
1615
+
1616
+ ```
1617
+ """
1618
+
1619
+ @wraps(self.freeze)
1620
+ def freeze(config: Config) -> None:
1621
+ config.setattr("frozen", True)
1622
+
1623
+ if recursive:
1624
+ self.apply(freeze)
1625
+ else:
1626
+ freeze(self)
1627
+ return self
1628
+
1629
+ lock = freeze
1630
+
1631
+ def defrost(self, recursive: Optional[bool] = True) -> Config:
1632
+ r"""
1633
+ Defrost the config.
1634
+
1635
+ `unlock` is an alias of this method.
1636
+
1637
+ Args:
1638
+ recursive (Optional[bool]): defrost all sub-configs recursively. Defaults to True.
1639
+
1640
+ Example:
1641
+ ```python
1642
+ >>> c = Config()
1643
+ >>> c.getattr('frozen')
1644
+ False
1645
+ >>> c.freeze().to(dict)
1646
+ {}
1647
+ >>> c.getattr('frozen')
1648
+ True
1649
+ >>> c.defrost().to(dict)
1650
+ {}
1651
+ >>> c.getattr('frozen')
1652
+ False
1653
+
1654
+ ```
1655
+ """
1656
+
1657
+ @wraps(self.defrost)
1658
+ def defrost(config: Config) -> None:
1659
+ config.setattr("frozen", False)
1660
+
1661
+ if recursive:
1662
+ self.apply(defrost)
1663
+ else:
1664
+ defrost(self)
1665
+ return self
1666
+
1667
+ unlock = defrost
1668
+
1669
+ @contextmanager
1670
+ def unlocked(self):
1671
+ """
1672
+ Context manager which temporarily unlocks the config.
1673
+
1674
+ Example:
1675
+ ```python
1676
+ >>> c = Config()
1677
+ >>> c.freeze().to(dict)
1678
+ {}
1679
+ >>> with c.unlocked():
1680
+ ... c['i.d'] = 1013
1681
+ >>> c.to(dict)
1682
+ {'i': {'d': 1013}}
1683
+
1684
+ ```
1685
+ """
1686
+ was_frozen = self.getattr("frozen")
1687
+ try:
1688
+ self.defrost()
1689
+ yield self
1690
+ finally:
1691
+ if was_frozen:
1692
+ self.freeze()
1693
+
1694
+ def parse(
1695
+ self,
1696
+ args: Optional[Iterable[str]] = None,
1697
+ default_config: Optional[str] = None,
1698
+ ) -> Config:
1699
+ r"""
1700
+ Parse the arguments for config.
1701
+ There are three levels of config:
1702
+
1703
+ 1. The base config parsed into the function,
1704
+ 2. The config file located at the path of default_config (if specified),
1705
+ 3. The config specified in arguments.
1706
+
1707
+ Higher levels override lower levels (i.e. 3 > 2 > 1).
1708
+
1709
+ Args:
1710
+ args (Optional[Sequence[str]]): The arguments to parse. Defaults to sys.argv[1:].
1711
+ default_config (Optional[str]): The path to a config file.
1712
+
1713
+ Example:
1714
+ ```python
1715
+ >>> c = Config(a=0)
1716
+ >>> c.to(dict)
1717
+ {'a': 0}
1718
+ >>> c.parse(['--a', '1', '--b', '2', '--c', '3']).to(dict)
1719
+ {'a': 1, 'b': 2, 'c': 3}
1720
+
1721
+ ```
1722
+ """
1723
+
1724
+ return self.getattr("parser").parse(args, self, default_config)
1725
+
1726
+ parse_config = parse
1727
+
1728
+ def add_argument(self, *args, **kwargs) -> None:
1729
+ r"""
1730
+ Add an argument to the parser.
1731
+ """
1732
+
1733
+ self.getattr("parser").add_argument(*args, **kwargs)
1734
+
1735
+
1736
+ if __name__ == "__main__":
1737
+ import doctest
1738
+
1739
+ doctest.testmod()