跳转至

Config

Config

Bases: NestedDict

Config is an extension of NestedDict.

The differences between Config and NestedDict lies in 3 aspects:

  1. Config has default_factory set to Config and convert_mapping set to True by default.
  2. Config has a frozen attribute, which can be toggled with freeze(lock) & defrost(unlock) or temporarily changed with locked & unlocked.
  3. Config has a ConfigParser built-in, and supports add_argument and parse.

Config also features a post method and a boot method to support lazy-initilisation. This is useful when you want to perform some post-processing on the config. For example, some values may be a combination of other values, and you may define them in post.

boot is introduced to call all post methods in the nested structure of Config object. By default, boot will be called to after Config is parsed.

You could also manually call boot if you you don’t parse command-line arguments.

Notes

Since Config has default_factory set to Config, accessing anything that does not exist will create a new empty Config sub-attribute.

A frozen Config does not have this behaviour and will raises KeyError when accessing anything that does not exist.

It is recommended to call config.freeze() or config.to(NestedDict) to avoid this behaviour.

Attributes:

Name Type Description
parser ConfigParser

Parser for command-line arguments.

frozen bool

If True, the config is frozen and cannot be altered.

Examples:

Python Console Session
>>> c = Config(**{"f.n": "chang"})
>>> c.i.d = 1013
>>> c.i.d
1013
>>> c.d.i
Config(<class 'chanfig.config.Config'>, )
>>> c.freeze().dict()
{'f': {'n': 'chang'}, 'i': {'d': 1013}, 'd': {'i': {}}}
>>> c.d.i = 1013
Traceback (most recent call last):
ValueError: Attempting to alter a frozen config. Run config.defrost() to defrost first.
>>> c.d.e
Traceback (most recent call last):
AttributeError: 'Config' object has no attribute 'e'
>>> with c.unlocked():
...     del c.d
>>> c.dict()
{'f': {'n': 'chang'}, 'i': {'d': 1013}}
Source code in chanfig/config.py
Python
 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
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
class Config(NestedDict):
    r"""
    `Config` is an extension of `NestedDict`.

    The differences between `Config` and `NestedDict` lies in 3 aspects:

    1. `Config` has `default_factory` set to `Config` and `convert_mapping` set to `True` by default.
    2. `Config` has a `frozen` attribute, which can be toggled with `freeze`(`lock`) & `defrost`(`unlock`)
        or temporarily changed with `locked` & `unlocked`.
    3. `Config` has a `ConfigParser` built-in, and supports `add_argument` and `parse`.

    Config also features a `post` method and a `boot` method to support lazy-initilisation.
    This is useful when you want to perform some post-processing on the config.
    For example, some values may be a combination of other values, and you may define them in `post`.

    `boot` is introduced to call all `post` methods in the nested structure of `Config` object.
    By default, `boot` will be called to after `Config` is parsed.

    You could also manually call `boot` if you you don't parse command-line arguments.

    Notes:
        Since `Config` has `default_factory` set to `Config`,
        accessing anything that does not exist will create a new empty Config sub-attribute.

        A **frozen** `Config` does not have this behaviour and
        will raises `KeyError` when accessing anything that does not exist.

        It is recommended to call `config.freeze()` or `config.to(NestedDict)` to avoid this behaviour.

    Attributes:
        parser (ConfigParser): Parser for command-line arguments.
        frozen (bool): If `True`, the config is frozen and cannot be altered.

    Examples:
        >>> c = Config(**{"f.n": "chang"})
        >>> c.i.d = 1013
        >>> c.i.d
        1013
        >>> c.d.i
        Config(<class 'chanfig.config.Config'>, )
        >>> c.freeze().dict()
        {'f': {'n': 'chang'}, 'i': {'d': 1013}, 'd': {'i': {}}}
        >>> c.d.i = 1013
        Traceback (most recent call last):
        ValueError: Attempting to alter a frozen config. Run config.defrost() to defrost first.
        >>> c.d.e
        Traceback (most recent call last):
        AttributeError: 'Config' object has no attribute 'e'
        >>> with c.unlocked():
        ...     del c.d
        >>> c.dict()
        {'f': {'n': 'chang'}, 'i': {'d': 1013}}
    """

    parser: None  # ConfigParser, Python 3.7 does not support forward reference
    frozen: bool

    def __init__(self, *args: Any, default_factory: Callable | None = None, **kwargs: Any):
        if default_factory is None:
            default_factory = Config
        self.setattr("frozen", False)
        super().__init__(*args, default_factory=default_factory, **kwargs)

    def copy_class_attributes(self, recursive: bool = True) -> Self:
        r"""
        Copy class attributes to instance.

        Args:
            recursive:

        Returns:
            self:

        Examples:
            >>> class Ancestor(Config):
            ...     a = 1
            >>> class Parent(Ancestor):
            ...     b = 2
            >>> class Child(Parent):
            ...     c = 3
            >>> c = Child()
            >>> c
            Child(<class 'chanfig.config.Config'>, )
            >>> c.copy_class_attributes(recursive=False)
            Child(<class 'chanfig.config.Config'>,('c'): 3)
            >>> c.copy_class_attributes()  # doctest: +SKIP
            Child(<class 'chanfig.config.Config'>,
                ('a'): 1,
                ('b'): 2,
                ('c'): 3
            )
        """

        def copy_cls_attributes(cls: type) -> Mapping:
            return {
                k: v
                for k, v in cls.__dict__.items()
                if k not in self
                and not k.startswith("__")
                and (not (isinstance(v, (property, staticmethod, classmethod)) or callable(v)))
            }

        if recursive:
            for cls in self.__class__.__mro__:
                if cls.__module__.startswith("chanfig"):
                    break
                self.merge(copy_cls_attributes(cls), overwrite=False)
        else:
            self.merge(copy_cls_attributes(self.__class__), overwrite=False)
        return self

    def post(self) -> Self | None:
        r"""
        Post process of `Config`.

        Some `Config` may need to do some post process after `Config` is initialised.
        `post` is provided for this lazy-initialisation purpose.

        By default, `post` calls `interpolate` to perform variable interpolation.

        Note that you should always call `boot` to apply `post` rather than calling `post` directly,
        as `boot` recursively call `post` on sub-configs.

        See Also:
            [`boot`][chanfig.Config.boot]

        Returns:
            self:

        Examples:
            >>> c = Config()
            >>> c.dne
            Config(<class 'chanfig.config.Config'>, )
            >>> c.post()
            Config(
              ('dne'): Config()
            )
            >>> c.dne2
            Traceback (most recent call last):
            AttributeError: 'Config' object has no attribute 'dne2'
            >>> class PostConfig(Config):
            ...     def post(self):
            ...         if isinstance(self.data, str):
            ...             self.data = Config(feature=self.data, label=self.data)
            ...         return self
            >>> c = PostConfig(data="path")
            >>> c.post()
            PostConfig(<class 'chanfig.config.Config'>,
              ('data'): Config(<class 'chanfig.config.Config'>,
                ('feature'): 'path'
                ('label'): 'path'
              )
            )
        """

        self.interpolate()
        self.validate()
        self.apply_(lambda c: c.setattr("default_factory", None) if isinstance(c, Config) else None)
        return self

    def boot(self) -> Self:
        r"""
        Apply `post` recursively.

        Sub-config may have their own `post` method.
        `boot` is provided to apply `post` recursively.

        By default, `boot` is called after `Config` is parsed.
        If you don't need to parse command-line arguments, you should call `boot` manually.

        See Also:
            [`post`][chanfig.Config.post]

        Returns:
            self:

        Examples:
            >>> class DataConfig(Config):
            ...     def post(self):
            ...         if isinstance(self.path, str):
            ...             self.path = Config(feature=self.path, label=self.path)
            ...         return self
            >>> class BootConfig(Config):
            ...     def __init__(self, *args, **kwargs):
            ...         super().__init__(*args, **kwargs)
            ...         self.dataset = DataConfig(path="path")
            ...     def post(self):
            ...         if isinstance(self.id, str):
            ...             self.id += "_id"
            ...         return self
            >>> c = BootConfig(id="boot")
            >>> c.boot()
            BootConfig(<class 'chanfig.config.Config'>,
              ('id'): 'boot_id'
              ('dataset'): DataConfig(<class 'chanfig.config.Config'>,
                ('path'): Config(<class 'chanfig.config.Config'>,
                  ('feature'): 'path'
                  ('label'): 'path'
                )
              )
            )
        """

        for value in self.values():
            if isinstance(value, Config):
                value.boot()
        self.post()
        return self

    def parse(
        self,
        args: Iterable[str] | None = None,
        default_config: str | None = None,
        no_default_config_action: str = "raise",
        boot: bool = True,
    ) -> Self:
        r"""

        Parse command-line arguments with `ConfigParser`.

        `parse` will try to parse all command-line arguments,
        you don't need to pre-define them but typos may cause trouble.

        By default, this method internally calls `Config.boot()`.
        To disable this behaviour, set `boot` to `False`.

        Args:
            args (Iterable[str] | None, optional): Command-line arguments. Defaults to `None`.
            default_config (str | None, optional): Path to default config file. Defaults to `None`.
            no_default_config_action (str, optional): Action when `default_config` is not found.
                Can be one of `["raise", "warn", "ignore"]`. Defaults to `"raise"`.
            boot (bool, optional): If `True`, call `Config.boot()` after parsing. Defaults to `True`.

        See Also:
            [`chanfig.ConfigParser.parse`][chanfig.ConfigParser.parse]: Implementation of `parse`.
            [`parse_config`][chanfig.Config.parse_config]: Only parse valid config arguments.

        Examples:
            >>> c = Config(a=0)
            >>> c.dict()
            {'a': 0}
            >>> c.parse(['--a', '1', '--b', '2', '--c', '3']).dict()
            {'a': 1, 'b': 2, 'c': 3}
        """

        if not self.hasattr("parser"):
            self.setattr("parser", ConfigParser())
        self.getattr("parser").parse(args, self, default_config, no_default_config_action)
        if boot:
            self.boot()
        return self

    def parse_config(
        self,
        args: Iterable[str] | None = None,
        default_config: str | None = None,
        no_default_config_action: str = "raise",
        boot: bool = True,
    ) -> Self:
        r"""

        Parse command-line arguments with `ConfigParser`.

        `parse_config` only parse command-line arguments that is in defined in `Config`.

        By default, this method internally calls `Config.boot()`.
        To disable this behaviour, set `boot` to `False`.

        Args:
            args (Iterable[str] | None, optional): Command-line arguments. Defaults to `None`.
            default_config (str | None, optional): Path to default config file. Defaults to `None`.
            no_default_config_action (str, optional): Action when `default_config` is not found.
                Can be one of `["raise", "warn", "ignore"]`. Defaults to `"raise"`.
            boot (bool, optional): If `True`, call `Config.boot()` after parsing. Defaults to `True`.

        See Also:
            [`chanfig.ConfigParser.parse_config`][chanfig.ConfigParser.parse_config]: Implementation of `parse_config`.
            [`parse`][chanfig.Config.parse]: Parse all command-line arguments.

        Examples:
            >>> c = Config(a=0, b=0, c=0)
            >>> c.dict()
            {'a': 0, 'b': 0, 'c': 0}
            >>> c.parse_config(['--a', '1', '--b', '2', '--c', '3']).dict()
            {'a': 1, 'b': 2, 'c': 3}
        """

        if not self.hasattr("parser"):
            self.setattr("parser", ConfigParser())
        self.getattr("parser").parse_config(args, self, default_config, no_default_config_action)
        if boot:
            self.boot()
        return self

    def add_argument(self, *args: Any, **kwargs: Any) -> None:
        r"""
        Add an argument to `ConfigParser`.

        Note that value defined in `Config` will override the default value defined in `add_argument`.

        Examples:
            >>> c = Config(a=0, c=1)
            >>> arg = c.add_argument("--a", type=int, default=1)
            >>> arg = c.add_argument("--b", type=int, default=2)
            >>> c.parse(['--c', '4']).dict()
            {'a': 1, 'c': 4, 'b': 2}
        """

        if not self.hasattr("parser"):
            self.setattr("parser", ConfigParser())
        return self.getattr("parser").add_argument(*args, **kwargs)

    def freeze(self, recursive: bool = True) -> Self:
        r"""
        Freeze `Config`.

        Args:
            recursive:

        **Alias**:

        + `lock`

        Examples:
            >>> c = Config(**{'i.d': 1013})
            >>> c.getattr('frozen')
            False
            >>> c.freeze(recursive=False).dict()
            {'i': {'d': 1013}}
            >>> c.getattr('frozen')
            True
            >>> c.i.getattr('frozen')
            False
            >>> c.lock().dict()  # alias
            {'i': {'d': 1013}}
            >>> c.i.getattr('frozen')
            True
        """

        @wraps(self.freeze)
        def freeze(config: Config) -> None:
            if isinstance(config, Config):
                config.setattr("frozen", True)

        if recursive:
            self.apply_(freeze)
        else:
            freeze(self)
        return self

    def lock(self, recursive: bool = True) -> Self:
        r"""
        Alias of [`freeze`][chanfig.Config.freeze].
        """
        return self.freeze(recursive=recursive)

    @contextmanager
    def locked(self):
        """
        Context manager which temporarily locks `Config`.

        Examples:
            >>> c = Config()
            >>> with c.locked():
            ...     c['i.d'] = 1013
            Traceback (most recent call last):
            ValueError: Attempting to alter a frozen config. Run config.defrost() to defrost first.
            >>> c.i.d = 1013
            >>> c.dict()
            {'i': {'d': 1013}}
        """

        was_frozen = self.getattr("frozen", False)
        try:
            self.freeze()
            yield self
        finally:
            if not was_frozen:
                self.defrost()

    def defrost(self, recursive: bool = True) -> Self:
        r"""
        Defrost `Config`.

        Args:
            recursive:

        **Alias**:

        + `unlock`

        Examples:
            >>> c = Config(**{'i.d': 1013})
            >>> c.getattr('frozen')
            False
            >>> c.freeze().dict()
            {'i': {'d': 1013}}
            >>> c.getattr('frozen')
            True
            >>> c.defrost(recursive=False).dict()
            {'i': {'d': 1013}}
            >>> c.getattr('frozen')
            False
            >>> c.i.getattr('frozen')
            True
            >>> c.unlock().dict()  # alias
            {'i': {'d': 1013}}
            >>> c.i.getattr('frozen')
            False
        """

        @wraps(self.defrost)
        def defrost(config: Config) -> None:
            if isinstance(config, Config):
                config.setattr("frozen", False)

        if recursive:
            self.apply_(defrost)
        else:
            defrost(self)
        return self

    def unlock(self, recursive: bool = True) -> Self:
        r"""
        Alias of [`defrost`][chanfig.Config.defrost].
        """
        return self.defrost(recursive=recursive)

    @contextmanager
    def unlocked(self):
        """
        Context manager which temporarily unlocks `Config`.

        Examples:
            >>> c = Config()
            >>> c.freeze().dict()
            {}
            >>> with c.unlocked():
            ...     c['i.d'] = 1013
            >>> c.defrost().dict()
            {'i': {'d': 1013}}
        """

        was_frozen = self.getattr("frozen", False)
        try:
            self.defrost()
            yield self
        finally:
            if was_frozen:
                self.freeze()

    def get(self, name: Any, default: Any = None, fallback: bool | None = None) -> Any:
        r"""
        Get value from `Config`.

        Note that `default` has higher priority than `default_factory`.

        Args:
            name:
            default:

        Returns:
            value:
                If `Config` does not contain `name`, return `default`.
                If `default` is not specified, return `default_factory()`.

        Raises:
            KeyError: If `Config` does not contain `name` and `default`/`default_factory` is not specified.

        Examples:
            >>> d = Config(**{"i.d": 1013})
            >>> d.get('i.d')
            1013
            >>> d['i.d']
            1013
            >>> d.i.d
            1013
            >>> d.get('f', 2)
            2
            >>> d.f
            Config(<class 'chanfig.config.Config'>, )
            >>> del d.f
            >>> d.freeze()
            Config(<class 'chanfig.config.Config'>,
              ('i'): Config(<class 'chanfig.config.Config'>,
                ('d'): 1013
              )
            )
            >>> d.f
            Traceback (most recent call last):
            AttributeError: 'Config' object has no attribute 'f'
            >>> d["f.n"]
            Traceback (most recent call last):
            KeyError: 'f.n'
        """

        if not self.hasattr("default_factory"):  # did not call super().__init__() in sub-class
            self.setattr("default_factory", Config)
        if name in self or not self.getattr("frozen", False):
            return super().get(name, default, fallback)
        raise KeyError(name)

    @frozen_check
    def set(
        self,
        name: Any,
        value: Any,
        convert_mapping: bool | None = None,
    ) -> None:
        r"""
        Set value of `Config`.

        Args:
            name:
            value:
            convert_mapping: Whether to convert `Mapping` to `NestedDict`.
                Defaults to self.convert_mapping.

        Raises:
            ValueError: If `Config` is frozen.

        Examples:
            >>> c = Config()
            >>> c['i.d'] = 1013
            >>> c.i.d
            1013
            >>> c.freeze().dict()
            {'i': {'d': 1013}}
            >>> c['i.d'] = 1013
            Traceback (most recent call last):
            ValueError: Attempting to alter a frozen config. Run config.defrost() to defrost first.
            >>> c.defrost().dict()
            {'i': {'d': 1013}}
            >>> c['i.d'] = 1013
            >>> c.i.d
            1013
        """

        return super().set(name, value, convert_mapping)

    @frozen_check
    def delete(self, name: Any) -> None:
        r"""
        Delete value from `Config`.

        Args:
            name:

        Examples:
            >>> d = Config(**{"i.d": 1013, "f.n": "chang"})
            >>> d.i.d
            1013
            >>> d.f.n
            'chang'
            >>> d.delete('i.d')
            >>> "i.d" in d
            False
            >>> d.i.d
            Config(<class 'chanfig.config.Config'>, )
            >>> "i.d" in d
            True
            >>> del d.f.n
            >>> d.f.n
            Config(<class 'chanfig.config.Config'>, )
            >>> del d.c
            Traceback (most recent call last):
            AttributeError: 'Config' object has no attribute 'c'
        """

        super().delete(name)

    @frozen_check
    def pop(self, name: Any, default: Any = Null) -> Any:
        r"""
        Pop value from `Config`.

        Args:
            name:
            default:

        Returns:
            value: If `Config` does not contain `name`, return `default`.

        Examples:
            >>> c = Config()
            >>> c['i.d'] = 1013
            >>> c.pop('i.d')
            1013
            >>> c.pop('i.d', True)
            True
            >>> c.freeze().dict()
            {'i': {}}
            >>> c['i.d'] = 1013
            Traceback (most recent call last):
            ValueError: Attempting to alter a frozen config. Run config.defrost() to defrost first.
            >>> c.defrost().dict()
            {'i': {}}
            >>> c['i.d'] = 1013
            >>> c.pop('i.d')
            1013
        """

        return super().pop(name, default)

add_argument(*args, **kwargs)

Add an argument to ConfigParser.

Note that value defined in Config will override the default value defined in add_argument.

Examples:

Python Console Session
>>> c = Config(a=0, c=1)
>>> arg = c.add_argument("--a", type=int, default=1)
>>> arg = c.add_argument("--b", type=int, default=2)
>>> c.parse(['--c', '4']).dict()
{'a': 1, 'c': 4, 'b': 2}
Source code in chanfig/config.py
Python
def add_argument(self, *args: Any, **kwargs: Any) -> None:
    r"""
    Add an argument to `ConfigParser`.

    Note that value defined in `Config` will override the default value defined in `add_argument`.

    Examples:
        >>> c = Config(a=0, c=1)
        >>> arg = c.add_argument("--a", type=int, default=1)
        >>> arg = c.add_argument("--b", type=int, default=2)
        >>> c.parse(['--c', '4']).dict()
        {'a': 1, 'c': 4, 'b': 2}
    """

    if not self.hasattr("parser"):
        self.setattr("parser", ConfigParser())
    return self.getattr("parser").add_argument(*args, **kwargs)

boot()

Apply post recursively.

Sub-config may have their own post method. boot is provided to apply post recursively.

By default, boot is called after Config is parsed. If you don’t need to parse command-line arguments, you should call boot manually.

See Also

post

Returns:

Name Type Description
self Self

Examples:

Python Console Session
>>> class DataConfig(Config):
...     def post(self):
...         if isinstance(self.path, str):
...             self.path = Config(feature=self.path, label=self.path)
...         return self
>>> class BootConfig(Config):
...     def __init__(self, *args, **kwargs):
...         super().__init__(*args, **kwargs)
...         self.dataset = DataConfig(path="path")
...     def post(self):
...         if isinstance(self.id, str):
...             self.id += "_id"
...         return self
>>> c = BootConfig(id="boot")
>>> c.boot()
BootConfig(<class 'chanfig.config.Config'>,
  ('id'): 'boot_id'
  ('dataset'): DataConfig(<class 'chanfig.config.Config'>,
    ('path'): Config(<class 'chanfig.config.Config'>,
      ('feature'): 'path'
      ('label'): 'path'
    )
  )
)
Source code in chanfig/config.py
Python
def boot(self) -> Self:
    r"""
    Apply `post` recursively.

    Sub-config may have their own `post` method.
    `boot` is provided to apply `post` recursively.

    By default, `boot` is called after `Config` is parsed.
    If you don't need to parse command-line arguments, you should call `boot` manually.

    See Also:
        [`post`][chanfig.Config.post]

    Returns:
        self:

    Examples:
        >>> class DataConfig(Config):
        ...     def post(self):
        ...         if isinstance(self.path, str):
        ...             self.path = Config(feature=self.path, label=self.path)
        ...         return self
        >>> class BootConfig(Config):
        ...     def __init__(self, *args, **kwargs):
        ...         super().__init__(*args, **kwargs)
        ...         self.dataset = DataConfig(path="path")
        ...     def post(self):
        ...         if isinstance(self.id, str):
        ...             self.id += "_id"
        ...         return self
        >>> c = BootConfig(id="boot")
        >>> c.boot()
        BootConfig(<class 'chanfig.config.Config'>,
          ('id'): 'boot_id'
          ('dataset'): DataConfig(<class 'chanfig.config.Config'>,
            ('path'): Config(<class 'chanfig.config.Config'>,
              ('feature'): 'path'
              ('label'): 'path'
            )
          )
        )
    """

    for value in self.values():
        if isinstance(value, Config):
            value.boot()
    self.post()
    return self

copy_class_attributes(recursive=True)

Copy class attributes to instance.

Parameters:

Name Type Description Default
recursive bool
True

Returns:

Name Type Description
self Self

Examples:

Python Console Session
>>> class Ancestor(Config):
...     a = 1
>>> class Parent(Ancestor):
...     b = 2
>>> class Child(Parent):
...     c = 3
>>> c = Child()
>>> c
Child(<class 'chanfig.config.Config'>, )
>>> c.copy_class_attributes(recursive=False)
Child(<class 'chanfig.config.Config'>,('c'): 3)
>>> c.copy_class_attributes()
Child(<class 'chanfig.config.Config'>,
    ('a'): 1,
    ('b'): 2,
    ('c'): 3
)
Source code in chanfig/config.py
Python
def copy_class_attributes(self, recursive: bool = True) -> Self:
    r"""
    Copy class attributes to instance.

    Args:
        recursive:

    Returns:
        self:

    Examples:
        >>> class Ancestor(Config):
        ...     a = 1
        >>> class Parent(Ancestor):
        ...     b = 2
        >>> class Child(Parent):
        ...     c = 3
        >>> c = Child()
        >>> c
        Child(<class 'chanfig.config.Config'>, )
        >>> c.copy_class_attributes(recursive=False)
        Child(<class 'chanfig.config.Config'>,('c'): 3)
        >>> c.copy_class_attributes()  # doctest: +SKIP
        Child(<class 'chanfig.config.Config'>,
            ('a'): 1,
            ('b'): 2,
            ('c'): 3
        )
    """

    def copy_cls_attributes(cls: type) -> Mapping:
        return {
            k: v
            for k, v in cls.__dict__.items()
            if k not in self
            and not k.startswith("__")
            and (not (isinstance(v, (property, staticmethod, classmethod)) or callable(v)))
        }

    if recursive:
        for cls in self.__class__.__mro__:
            if cls.__module__.startswith("chanfig"):
                break
            self.merge(copy_cls_attributes(cls), overwrite=False)
    else:
        self.merge(copy_cls_attributes(self.__class__), overwrite=False)
    return self

defrost(recursive=True)

Defrost Config.

Parameters:

Name Type Description Default
recursive bool
True

Alias:

  • unlock

Examples:

Python Console Session
>>> c = Config(**{'i.d': 1013})
>>> c.getattr('frozen')
False
>>> c.freeze().dict()
{'i': {'d': 1013}}
>>> c.getattr('frozen')
True
>>> c.defrost(recursive=False).dict()
{'i': {'d': 1013}}
>>> c.getattr('frozen')
False
>>> c.i.getattr('frozen')
True
>>> c.unlock().dict()  # alias
{'i': {'d': 1013}}
>>> c.i.getattr('frozen')
False
Source code in chanfig/config.py
Python
def defrost(self, recursive: bool = True) -> Self:
    r"""
    Defrost `Config`.

    Args:
        recursive:

    **Alias**:

    + `unlock`

    Examples:
        >>> c = Config(**{'i.d': 1013})
        >>> c.getattr('frozen')
        False
        >>> c.freeze().dict()
        {'i': {'d': 1013}}
        >>> c.getattr('frozen')
        True
        >>> c.defrost(recursive=False).dict()
        {'i': {'d': 1013}}
        >>> c.getattr('frozen')
        False
        >>> c.i.getattr('frozen')
        True
        >>> c.unlock().dict()  # alias
        {'i': {'d': 1013}}
        >>> c.i.getattr('frozen')
        False
    """

    @wraps(self.defrost)
    def defrost(config: Config) -> None:
        if isinstance(config, Config):
            config.setattr("frozen", False)

    if recursive:
        self.apply_(defrost)
    else:
        defrost(self)
    return self

delete(name)

Delete value from Config.

Parameters:

Name Type Description Default
name Any
required

Examples:

Python Console Session
>>> d = Config(**{"i.d": 1013, "f.n": "chang"})
>>> d.i.d
1013
>>> d.f.n
'chang'
>>> d.delete('i.d')
>>> "i.d" in d
False
>>> d.i.d
Config(<class 'chanfig.config.Config'>, )
>>> "i.d" in d
True
>>> del d.f.n
>>> d.f.n
Config(<class 'chanfig.config.Config'>, )
>>> del d.c
Traceback (most recent call last):
AttributeError: 'Config' object has no attribute 'c'
Source code in chanfig/config.py
Python
@frozen_check
def delete(self, name: Any) -> None:
    r"""
    Delete value from `Config`.

    Args:
        name:

    Examples:
        >>> d = Config(**{"i.d": 1013, "f.n": "chang"})
        >>> d.i.d
        1013
        >>> d.f.n
        'chang'
        >>> d.delete('i.d')
        >>> "i.d" in d
        False
        >>> d.i.d
        Config(<class 'chanfig.config.Config'>, )
        >>> "i.d" in d
        True
        >>> del d.f.n
        >>> d.f.n
        Config(<class 'chanfig.config.Config'>, )
        >>> del d.c
        Traceback (most recent call last):
        AttributeError: 'Config' object has no attribute 'c'
    """

    super().delete(name)

freeze(recursive=True)

Freeze Config.

Parameters:

Name Type Description Default
recursive bool
True

Alias:

  • lock

Examples:

Python Console Session
>>> c = Config(**{'i.d': 1013})
>>> c.getattr('frozen')
False
>>> c.freeze(recursive=False).dict()
{'i': {'d': 1013}}
>>> c.getattr('frozen')
True
>>> c.i.getattr('frozen')
False
>>> c.lock().dict()  # alias
{'i': {'d': 1013}}
>>> c.i.getattr('frozen')
True
Source code in chanfig/config.py
Python
def freeze(self, recursive: bool = True) -> Self:
    r"""
    Freeze `Config`.

    Args:
        recursive:

    **Alias**:

    + `lock`

    Examples:
        >>> c = Config(**{'i.d': 1013})
        >>> c.getattr('frozen')
        False
        >>> c.freeze(recursive=False).dict()
        {'i': {'d': 1013}}
        >>> c.getattr('frozen')
        True
        >>> c.i.getattr('frozen')
        False
        >>> c.lock().dict()  # alias
        {'i': {'d': 1013}}
        >>> c.i.getattr('frozen')
        True
    """

    @wraps(self.freeze)
    def freeze(config: Config) -> None:
        if isinstance(config, Config):
            config.setattr("frozen", True)

    if recursive:
        self.apply_(freeze)
    else:
        freeze(self)
    return self

get(name, default=None, fallback=None)

Get value from Config.

Note that default has higher priority than default_factory.

Parameters:

Name Type Description Default
name Any
required
default Any
None

Returns:

Name Type Description
value Any

If Config does not contain name, return default. If default is not specified, return default_factory().

Raises:

Type Description
KeyError

If Config does not contain name and default/default_factory is not specified.

Examples:

Python Console Session
>>> d = Config(**{"i.d": 1013})
>>> d.get('i.d')
1013
>>> d['i.d']
1013
>>> d.i.d
1013
>>> d.get('f', 2)
2
>>> d.f
Config(<class 'chanfig.config.Config'>, )
>>> del d.f
>>> d.freeze()
Config(<class 'chanfig.config.Config'>,
  ('i'): Config(<class 'chanfig.config.Config'>,
    ('d'): 1013
  )
)
>>> d.f
Traceback (most recent call last):
AttributeError: 'Config' object has no attribute 'f'
>>> d["f.n"]
Traceback (most recent call last):
KeyError: 'f.n'
Source code in chanfig/config.py
Python
def get(self, name: Any, default: Any = None, fallback: bool | None = None) -> Any:
    r"""
    Get value from `Config`.

    Note that `default` has higher priority than `default_factory`.

    Args:
        name:
        default:

    Returns:
        value:
            If `Config` does not contain `name`, return `default`.
            If `default` is not specified, return `default_factory()`.

    Raises:
        KeyError: If `Config` does not contain `name` and `default`/`default_factory` is not specified.

    Examples:
        >>> d = Config(**{"i.d": 1013})
        >>> d.get('i.d')
        1013
        >>> d['i.d']
        1013
        >>> d.i.d
        1013
        >>> d.get('f', 2)
        2
        >>> d.f
        Config(<class 'chanfig.config.Config'>, )
        >>> del d.f
        >>> d.freeze()
        Config(<class 'chanfig.config.Config'>,
          ('i'): Config(<class 'chanfig.config.Config'>,
            ('d'): 1013
          )
        )
        >>> d.f
        Traceback (most recent call last):
        AttributeError: 'Config' object has no attribute 'f'
        >>> d["f.n"]
        Traceback (most recent call last):
        KeyError: 'f.n'
    """

    if not self.hasattr("default_factory"):  # did not call super().__init__() in sub-class
        self.setattr("default_factory", Config)
    if name in self or not self.getattr("frozen", False):
        return super().get(name, default, fallback)
    raise KeyError(name)

lock(recursive=True)

Alias of freeze.

Source code in chanfig/config.py
Python
def lock(self, recursive: bool = True) -> Self:
    r"""
    Alias of [`freeze`][chanfig.Config.freeze].
    """
    return self.freeze(recursive=recursive)

locked()

Context manager which temporarily locks Config.

Examples:

Python Console Session
>>> c = Config()
>>> with c.locked():
...     c['i.d'] = 1013
Traceback (most recent call last):
ValueError: Attempting to alter a frozen config. Run config.defrost() to defrost first.
>>> c.i.d = 1013
>>> c.dict()
{'i': {'d': 1013}}
Source code in chanfig/config.py
Python
@contextmanager
def locked(self):
    """
    Context manager which temporarily locks `Config`.

    Examples:
        >>> c = Config()
        >>> with c.locked():
        ...     c['i.d'] = 1013
        Traceback (most recent call last):
        ValueError: Attempting to alter a frozen config. Run config.defrost() to defrost first.
        >>> c.i.d = 1013
        >>> c.dict()
        {'i': {'d': 1013}}
    """

    was_frozen = self.getattr("frozen", False)
    try:
        self.freeze()
        yield self
    finally:
        if not was_frozen:
            self.defrost()

parse(args=None, default_config=None, no_default_config_action='raise', boot=True)

Parse command-line arguments with ConfigParser.

parse will try to parse all command-line arguments, you don’t need to pre-define them but typos may cause trouble.

By default, this method internally calls Config.boot(). To disable this behaviour, set boot to False.

Parameters:

Name Type Description Default
args Iterable[str] | None

Command-line arguments. Defaults to None.

None
default_config str | None

Path to default config file. Defaults to None.

None
no_default_config_action str

Action when default_config is not found. Can be one of ["raise", "warn", "ignore"]. Defaults to "raise".

'raise'
boot bool

If True, call Config.boot() after parsing. Defaults to True.

True
See Also

chanfig.ConfigParser.parse: Implementation of parse. parse_config: Only parse valid config arguments.

Examples:

Python Console Session
>>> c = Config(a=0)
>>> c.dict()
{'a': 0}
>>> c.parse(['--a', '1', '--b', '2', '--c', '3']).dict()
{'a': 1, 'b': 2, 'c': 3}
Source code in chanfig/config.py
Python
def parse(
    self,
    args: Iterable[str] | None = None,
    default_config: str | None = None,
    no_default_config_action: str = "raise",
    boot: bool = True,
) -> Self:
    r"""

    Parse command-line arguments with `ConfigParser`.

    `parse` will try to parse all command-line arguments,
    you don't need to pre-define them but typos may cause trouble.

    By default, this method internally calls `Config.boot()`.
    To disable this behaviour, set `boot` to `False`.

    Args:
        args (Iterable[str] | None, optional): Command-line arguments. Defaults to `None`.
        default_config (str | None, optional): Path to default config file. Defaults to `None`.
        no_default_config_action (str, optional): Action when `default_config` is not found.
            Can be one of `["raise", "warn", "ignore"]`. Defaults to `"raise"`.
        boot (bool, optional): If `True`, call `Config.boot()` after parsing. Defaults to `True`.

    See Also:
        [`chanfig.ConfigParser.parse`][chanfig.ConfigParser.parse]: Implementation of `parse`.
        [`parse_config`][chanfig.Config.parse_config]: Only parse valid config arguments.

    Examples:
        >>> c = Config(a=0)
        >>> c.dict()
        {'a': 0}
        >>> c.parse(['--a', '1', '--b', '2', '--c', '3']).dict()
        {'a': 1, 'b': 2, 'c': 3}
    """

    if not self.hasattr("parser"):
        self.setattr("parser", ConfigParser())
    self.getattr("parser").parse(args, self, default_config, no_default_config_action)
    if boot:
        self.boot()
    return self

parse_config(args=None, default_config=None, no_default_config_action='raise', boot=True)

Parse command-line arguments with ConfigParser.

parse_config only parse command-line arguments that is in defined in Config.

By default, this method internally calls Config.boot(). To disable this behaviour, set boot to False.

Parameters:

Name Type Description Default
args Iterable[str] | None

Command-line arguments. Defaults to None.

None
default_config str | None

Path to default config file. Defaults to None.

None
no_default_config_action str

Action when default_config is not found. Can be one of ["raise", "warn", "ignore"]. Defaults to "raise".

'raise'
boot bool

If True, call Config.boot() after parsing. Defaults to True.

True
See Also

chanfig.ConfigParser.parse_config: Implementation of parse_config. parse: Parse all command-line arguments.

Examples:

Python Console Session
>>> c = Config(a=0, b=0, c=0)
>>> c.dict()
{'a': 0, 'b': 0, 'c': 0}
>>> c.parse_config(['--a', '1', '--b', '2', '--c', '3']).dict()
{'a': 1, 'b': 2, 'c': 3}
Source code in chanfig/config.py
Python
def parse_config(
    self,
    args: Iterable[str] | None = None,
    default_config: str | None = None,
    no_default_config_action: str = "raise",
    boot: bool = True,
) -> Self:
    r"""

    Parse command-line arguments with `ConfigParser`.

    `parse_config` only parse command-line arguments that is in defined in `Config`.

    By default, this method internally calls `Config.boot()`.
    To disable this behaviour, set `boot` to `False`.

    Args:
        args (Iterable[str] | None, optional): Command-line arguments. Defaults to `None`.
        default_config (str | None, optional): Path to default config file. Defaults to `None`.
        no_default_config_action (str, optional): Action when `default_config` is not found.
            Can be one of `["raise", "warn", "ignore"]`. Defaults to `"raise"`.
        boot (bool, optional): If `True`, call `Config.boot()` after parsing. Defaults to `True`.

    See Also:
        [`chanfig.ConfigParser.parse_config`][chanfig.ConfigParser.parse_config]: Implementation of `parse_config`.
        [`parse`][chanfig.Config.parse]: Parse all command-line arguments.

    Examples:
        >>> c = Config(a=0, b=0, c=0)
        >>> c.dict()
        {'a': 0, 'b': 0, 'c': 0}
        >>> c.parse_config(['--a', '1', '--b', '2', '--c', '3']).dict()
        {'a': 1, 'b': 2, 'c': 3}
    """

    if not self.hasattr("parser"):
        self.setattr("parser", ConfigParser())
    self.getattr("parser").parse_config(args, self, default_config, no_default_config_action)
    if boot:
        self.boot()
    return self

pop(name, default=Null)

Pop value from Config.

Parameters:

Name Type Description Default
name Any
required
default Any
Null

Returns:

Name Type Description
value Any

If Config does not contain name, return default.

Examples:

Python Console Session
>>> c = Config()
>>> c['i.d'] = 1013
>>> c.pop('i.d')
1013
>>> c.pop('i.d', True)
True
>>> c.freeze().dict()
{'i': {}}
>>> c['i.d'] = 1013
Traceback (most recent call last):
ValueError: Attempting to alter a frozen config. Run config.defrost() to defrost first.
>>> c.defrost().dict()
{'i': {}}
>>> c['i.d'] = 1013
>>> c.pop('i.d')
1013
Source code in chanfig/config.py
Python
@frozen_check
def pop(self, name: Any, default: Any = Null) -> Any:
    r"""
    Pop value from `Config`.

    Args:
        name:
        default:

    Returns:
        value: If `Config` does not contain `name`, return `default`.

    Examples:
        >>> c = Config()
        >>> c['i.d'] = 1013
        >>> c.pop('i.d')
        1013
        >>> c.pop('i.d', True)
        True
        >>> c.freeze().dict()
        {'i': {}}
        >>> c['i.d'] = 1013
        Traceback (most recent call last):
        ValueError: Attempting to alter a frozen config. Run config.defrost() to defrost first.
        >>> c.defrost().dict()
        {'i': {}}
        >>> c['i.d'] = 1013
        >>> c.pop('i.d')
        1013
    """

    return super().pop(name, default)

post()

Post process of Config.

Some Config may need to do some post process after Config is initialised. post is provided for this lazy-initialisation purpose.

By default, post calls interpolate to perform variable interpolation.

Note that you should always call boot to apply post rather than calling post directly, as boot recursively call post on sub-configs.

See Also

boot

Returns:

Name Type Description
self Self | None

Examples:

Python Console Session
>>> c = Config()
>>> c.dne
Config(<class 'chanfig.config.Config'>, )
>>> c.post()
Config(
  ('dne'): Config()
)
>>> c.dne2
Traceback (most recent call last):
AttributeError: 'Config' object has no attribute 'dne2'
>>> class PostConfig(Config):
...     def post(self):
...         if isinstance(self.data, str):
...             self.data = Config(feature=self.data, label=self.data)
...         return self
>>> c = PostConfig(data="path")
>>> c.post()
PostConfig(<class 'chanfig.config.Config'>,
  ('data'): Config(<class 'chanfig.config.Config'>,
    ('feature'): 'path'
    ('label'): 'path'
  )
)
Source code in chanfig/config.py
Python
def post(self) -> Self | None:
    r"""
    Post process of `Config`.

    Some `Config` may need to do some post process after `Config` is initialised.
    `post` is provided for this lazy-initialisation purpose.

    By default, `post` calls `interpolate` to perform variable interpolation.

    Note that you should always call `boot` to apply `post` rather than calling `post` directly,
    as `boot` recursively call `post` on sub-configs.

    See Also:
        [`boot`][chanfig.Config.boot]

    Returns:
        self:

    Examples:
        >>> c = Config()
        >>> c.dne
        Config(<class 'chanfig.config.Config'>, )
        >>> c.post()
        Config(
          ('dne'): Config()
        )
        >>> c.dne2
        Traceback (most recent call last):
        AttributeError: 'Config' object has no attribute 'dne2'
        >>> class PostConfig(Config):
        ...     def post(self):
        ...         if isinstance(self.data, str):
        ...             self.data = Config(feature=self.data, label=self.data)
        ...         return self
        >>> c = PostConfig(data="path")
        >>> c.post()
        PostConfig(<class 'chanfig.config.Config'>,
          ('data'): Config(<class 'chanfig.config.Config'>,
            ('feature'): 'path'
            ('label'): 'path'
          )
        )
    """

    self.interpolate()
    self.validate()
    self.apply_(lambda c: c.setattr("default_factory", None) if isinstance(c, Config) else None)
    return self

set(name, value, convert_mapping=None)

Set value of Config.

Parameters:

Name Type Description Default
name Any
required
value Any
required
convert_mapping bool | None

Whether to convert Mapping to NestedDict. Defaults to self.convert_mapping.

None

Raises:

Type Description
ValueError

If Config is frozen.

Examples:

Python Console Session
>>> c = Config()
>>> c['i.d'] = 1013
>>> c.i.d
1013
>>> c.freeze().dict()
{'i': {'d': 1013}}
>>> c['i.d'] = 1013
Traceback (most recent call last):
ValueError: Attempting to alter a frozen config. Run config.defrost() to defrost first.
>>> c.defrost().dict()
{'i': {'d': 1013}}
>>> c['i.d'] = 1013
>>> c.i.d
1013
Source code in chanfig/config.py
Python
@frozen_check
def set(
    self,
    name: Any,
    value: Any,
    convert_mapping: bool | None = None,
) -> None:
    r"""
    Set value of `Config`.

    Args:
        name:
        value:
        convert_mapping: Whether to convert `Mapping` to `NestedDict`.
            Defaults to self.convert_mapping.

    Raises:
        ValueError: If `Config` is frozen.

    Examples:
        >>> c = Config()
        >>> c['i.d'] = 1013
        >>> c.i.d
        1013
        >>> c.freeze().dict()
        {'i': {'d': 1013}}
        >>> c['i.d'] = 1013
        Traceback (most recent call last):
        ValueError: Attempting to alter a frozen config. Run config.defrost() to defrost first.
        >>> c.defrost().dict()
        {'i': {'d': 1013}}
        >>> c['i.d'] = 1013
        >>> c.i.d
        1013
    """

    return super().set(name, value, convert_mapping)

unlock(recursive=True)

Alias of defrost.

Source code in chanfig/config.py
Python
def unlock(self, recursive: bool = True) -> Self:
    r"""
    Alias of [`defrost`][chanfig.Config.defrost].
    """
    return self.defrost(recursive=recursive)

unlocked()

Context manager which temporarily unlocks Config.

Examples:

Python Console Session
>>> c = Config()
>>> c.freeze().dict()
{}
>>> with c.unlocked():
...     c['i.d'] = 1013
>>> c.defrost().dict()
{'i': {'d': 1013}}
Source code in chanfig/config.py
Python
@contextmanager
def unlocked(self):
    """
    Context manager which temporarily unlocks `Config`.

    Examples:
        >>> c = Config()
        >>> c.freeze().dict()
        {}
        >>> with c.unlocked():
        ...     c['i.d'] = 1013
        >>> c.defrost().dict()
        {'i': {'d': 1013}}
    """

    was_frozen = self.getattr("frozen", False)
    try:
        self.defrost()
        yield self
    finally:
        if was_frozen:
            self.freeze()

frozen_check(func)

Decorator check if the object is frozen.

Source code in chanfig/config.py
Python
def frozen_check(func: Callable):
    r"""
    Decorator check if the object is frozen.
    """

    @wraps(func)
    def decorator(self, *args: Any, **kwargs: Any):
        if self.getattr("frozen", False):
            raise ValueError("Attempting to alter a frozen config. Run config.defrost() to defrost first.")
        return func(self, *args, **kwargs)

    return decorator