File: structured_config.rst

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

.. contents::
   :local:

.. _structured_configs:

.. testsetup:: *

    from omegaconf import *
    from enum import Enum
    from dataclasses import dataclass, field
    import os
    import pathlib
    from pytest import raises
    from typing import Dict, Any
    import sys
    os.environ['USER'] = 'omry'

Structured configs are used to create OmegaConf configuration object with runtime type safety.
In addition, they can be used with tools like mypy or your IDE for static type checking.

Two types of structures classes are supported: dataclasses and attr classes.

- `dataclasses <https://docs.python.org/3.7/library/dataclasses.html>`_ are standard as of Python 3.7 or newer and are available in Python 3.6 via the `dataclasses` pip package.
- `attrs <https://github.com/python-attrs/attrs>`_  Offset slightly cleaner syntax in some cases but depends on the attrs pip package.

This documentation will use dataclasses, but you can use the annotation ``@attr.s(auto_attribs=True)`` from attrs instead of ``@dataclass``.

Basic usage involves passing in a structured config class or instance to ``OmegaConf.structured()``, which will return an OmegaConf config that matches
the values and types specified in the input. At runtine, OmegaConf will validate modifications to the created config object against the schema specified
in the input class.


Currently, type hints supported in OmegaConf’s structured configs include:
 - primitive types (``int``, ``float``, ``bool``, ``str``, ``bytes``, ``Path``) and enum types
   (user-defined subclasses of ``enum.Enum``). See the :ref:`simple_types` section below.
 - unions of primitive/enum types, e.g. ``Union[float, bool, MyEnum]``.
   See :ref:`union_types` below.
 - structured config fields (i.e. MyConfig.x can have type hint MySubConfig).
   See the :ref:`nesting_structured_configs` section below.
 - dict and list types: ``typing.Dict[K, V]`` or ``typing.List[V]``, where K is
   primitive or enum, and where V is any of the above (including nested dicts
   or lists, e.g. ``Dict[str, List[int]]``).
   See the :ref:`lists` and :ref:`dictionaries` sections below.
 - optional types (any of the above can be wrapped in a ``typing.Optional[...]``
   annotation). See :ref:`other_special_features` below.

.. _simple_types:

Simple types
^^^^^^^^^^^^
Simple types include
 - ``int``: numeric integers
 - ``float``: numeric floating point values
 - ``bool``: boolean values (True, False, On, Off etc)
 - ``str``: any string
 - ``bytes``: an immutable sequence of numbers in [0, 255]
 - ``pathlib.Path``: filesystem paths as represented by python's standard library ``pathlib``
 - ``Enums``: User defined enums

The following class defines fields with all simple types:

.. doctest::

    >>> class Height(Enum):
    ...     SHORT = 0
    ...     TALL = 1

    >>> @dataclass
    ... class SimpleTypes:
    ...     num: int = 10
    ...     pi: float = 3.1415
    ...     is_awesome: bool = True
    ...     height: Height = Height.SHORT
    ...     description: str = "text"
    ...     data: bytes = b"bin_data"
    ...     path: pathlib.Path = pathlib.Path("hello.txt")

You can create a config based on the SimpleTypes class itself or an instance of it.
Those would be equivalent by default, but the Object variant allows you to set the values of specific
fields during construction.

.. doctest::

    >>> conf1 = OmegaConf.structured(SimpleTypes)
    >>> conf2 = OmegaConf.structured(SimpleTypes())
    >>> # The two configs are identical in this case
    >>> assert conf1 == conf2
    >>> # But the second form allow for easy customization of the values:
    >>> conf3 = OmegaConf.structured(
    ...   SimpleTypes(num=20,
    ...   height=Height.TALL))
    >>> print(OmegaConf.to_yaml(conf3))
    num: 20
    pi: 3.1415
    is_awesome: true
    height: TALL
    description: text
    data: !!binary |
      YmluX2RhdGE=
    path: !!python/object/apply:pathlib.PosixPath
    - hello.txt
    <BLANKLINE>

The resulting object is a regular OmegaConf ``DictConfig``, except that it will utilize the type information in the input class/object
and will validate the data at runtime.
The resulting object and will also rejects attempts to access or set fields that are not already defined
(similarly to configs with their to :ref:`struct-flag` set, but not recursive).

.. doctest::

    >>> conf = OmegaConf.structured(SimpleTypes)
    >>> with raises(AttributeError):
    ...    conf.does_not_exist


Static type checker support
^^^^^^^^^^^^^^^^^^^^^^^^^^^
Python type annotation can be used by static type checkers like Mypy/Pyre or by IDEs like PyCharm.

.. doctest::

    >>> conf: SimpleTypes = OmegaConf.structured(SimpleTypes)
    >>> # Passes static type checking
    >>> conf.description = "text"
    >>> # Fails static type checking (but will also raise a Validation error)
    >>> with raises(ValidationError):
    ...     conf.num = "foo"

This is duck-typing; the actual object type of ``conf`` is ``DictConfig``. You can access the underlying
type using ``OmegaConf.get_type()``:

.. doctest::
    
    >>> type(conf).__name__
    'DictConfig'

    >>> OmegaConf.get_type(conf).__name__
    'SimpleTypes'



Runtime type validation and conversion
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
OmegaConf supports merging configs together, as well as overriding from the command line.
This means some mistakes can not be identified by static type checkers, and runtime validation is required.

.. doctest::

    >>> # This is okay, the string "100" can be converted to an int
    >>> # Note that static type checkers will not like it and you should
    >>> # avoid such explicit mistyped assignments.
    >>> conf.num = "100"
    >>> assert conf.num == 100

    >>> with raises(ValidationError):
    ...     # This will fail at runtime because num is an int
    ...     # and foo cannot be converted to an int
    ...     # Note that the static type checker can't help here.
    ...     conf.merge_with_dotlist(["num=foo"])

Runtime validation and conversion works for all supported types, including Enums:

.. doctest::

    >>> conf.height = Height.TALL
    >>> assert conf.height == Height.TALL

    >>> # The name of Height.TALL is TALL
    >>> conf.height = "TALL"
    >>> assert conf.height == Height.TALL

    >>> # This works too
    >>> conf.height = "Height.TALL"
    >>> assert conf.height == Height.TALL

    >>> # The ordinal of Height.TALL is 1
    >>> conf.height = 1
    >>> assert conf.height == Height.TALL

.. _nesting_structured_configs:

Nesting structured configs
^^^^^^^^^^^^^^^^^^^^^^^^^^

Structured configs can be nested.

.. doctest::

    >>> @dataclass
    ... class User:
    ...     # A simple user class with two missing fields
    ...     name: str = MISSING
    ...     height: Height = MISSING
    >>>
    >>> @dataclass
    ... class DuperUser(User):
    ...     duper: bool = True
    ...
    >>> # Group class contains two instances of User.
    >>> @dataclass
    ... class Group:
    ...     name: str = MISSING
    ...     # data classes can be nested
    ...     admin: User = field(default_factory=User)
    ...
    ...     # You can also specify different defaults for nested classes
    ...     manager: User = field(default_factory=lambda: User(name="manager", height=Height.TALL))

    >>> conf: Group = OmegaConf.structured(Group)
    >>> print(OmegaConf.to_yaml(conf))
    name: ???
    admin:
      name: ???
      height: ???
    manager:
      name: manager
      height: TALL
    <BLANKLINE>

OmegaConf will validate that assignment of nested objects is of the correct type:

.. doctest::

    >>> with raises(ValidationError):
    ...     conf.manager = 10

You can assign subclasses:

.. doctest::

    >>> conf.manager = DuperUser()
    >>> assert conf.manager.duper == True


.. _lists:

Lists
^^^^^
Structured Config fields annotated with ``typing.List`` or ``typing.Tuple`` can hold any type
supported by OmegaConf (``int``, ``float``. ``bool``, ``str``, ``bytes``, ``pathlib.Path``, ``Enum`` or Structured configs).

.. doctest::

    >>> from dataclasses import dataclass, field
    >>> from typing import List, Tuple
    >>> @dataclass
    ... class User:
    ...     name: str = MISSING

    >>> @dataclass
    ... class ListsExample:
    ...     # Typed list can hold Any, int, float, bool, str,
    ...     # bytes, pathlib.Path and Enums as well as arbitrary Structured configs.
    ...     ints: List[int] = field(default_factory=lambda: [10, 20, 30])
    ...     bools: Tuple[bool, bool] = field(default_factory=lambda: (True, False))
    ...     users: List[User] = field(default_factory=lambda: [User(name="omry")])

OmegaConf verifies at runtime that your Lists contains only values of the correct type.
In the example below, the OmegaConf object ``conf`` (which is actually an instance of ``DictConfig``) is duck-typed as ``ListExample``.

.. doctest::

    >>> conf: ListsExample = OmegaConf.structured(ListsExample)

    >>> # Okay, 10 is an int
    >>> conf.ints.append(10)
    >>> # Okay, "20" can be converted to an int
    >>> conf.ints.append("20")

    >>> conf.bools.append(True)
    >>> conf.users.append(User(name="Joe"))
    >>> # Not okay, 10 cannot be converted to a User
    >>> with raises(ValidationError):
    ...     conf.users.append(10)

.. _dictionaries:

Dictionaries
^^^^^^^^^^^^
Dictionaries are supported via annotation of structured config fields with ``typing.Dict``.
Keys must be typed as one of ``str``, ``int``, ``Enum``, ``float``, ``bytes``, or ``bool``. Values can
be any of the types supported by OmegaConf (``Any``, ``int``, ``float``, ``bool``, ``bytes``,
``pathlib.Path``, ``str`` and ``Enum`` as well as arbitrary Structured configs)

.. doctest::

    >>> from dataclasses import dataclass, field
    >>> from typing import Dict
    >>> @dataclass
    ... class DictExample:
    ...     ints: Dict[str, int] = field(default_factory=lambda: {"a": 10, "b": 20, "c": 30})
    ...     bools: Dict[str, bool] = field(default_factory=lambda: {"Uno": True, "Zoro": False})
    ...     users: Dict[str, User] = field(default_factory=lambda: {"omry": User(name="omry")})

Like with Lists, the types of values contained in Dicts are verified at runtime.

.. doctest::

    >>> conf: DictExample = OmegaConf.structured(DictExample)

    >>> # Okay, correct type is assigned
    >>> conf.ints["d"] = 10
    >>> conf.bools["Dos"] = True
    >>> conf.users["James"] = User(name="Bond")

    >>> # Not okay, 10 cannot be assigned to a User
    >>> with raises(ValidationError):
    ...     conf.users["Joe"] = 10

.. _nested_dict_and_list_annotations:

Nested dict and list annotations
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Dict and List annotations can be nested flexibly:

.. doctest::

    >>> @dataclass
    ... class NestedContainers:
    ...     dict_of_dict: Dict[str, Dict[str, int]]
    ...     list_of_list: List[List[int]] = field(default_factory=lambda: [[123]])
    ...     dict_of_list: Dict[str, List[int]] = MISSING
    ...     list_of_dict: List[Dict[str, int]] = MISSING
    ... 
    ... 
    >>> cfg = OmegaConf.structured(NestedContainers(dict_of_dict={"foo": {"bar": 123}}))
    >>> print(OmegaConf.to_yaml(cfg))
    dict_of_dict:
      foo:
        bar: 123
    list_of_list:
    - - 123
    dict_of_list: ???
    list_of_dict: ???
    <BLANKLINE>
    >>> with raises(ValidationError):
    ...     cfg.list_of_dict = [["whoops"]]  # not a list of dicts

.. _union_types:

Unions
^^^^^^

You can use `typing.Union <https://docs.python.org/3/library/typing.html#typing.Union>`_
to annotate unions of :ref:`simple_types`. 

.. doctest::

    >>> from typing import Union
    >>>
    >>> @dataclass
    ... class HasUnion:
    ...     u: Union[float, bool] = 10.1
    ...
    >>> cfg = OmegaConf.structured(HasUnion)
    >>> assert cfg.u == 10.1
    >>> cfg.u = True  # ok
    >>> cfg.u = b"binary"  # bytes not compatible with union
    Traceback (most recent call last):
    ...
    omegaconf.errors.ValidationError: Cannot assign 'b'binary'' of type 'bytes' to Union[float, bool]
        full_key: u
        object_type=HasUnion
    >>> OmegaConf.structured(HasUnion("abc"))  # str not compatible
    Traceback (most recent call last):
    ...
    omegaconf.errors.ValidationError: Cannot assign 'abc' of type 'str' to Union[float, bool]
        full_key: u
        object_type=None

If any argument of a ``Union`` type hint is ``Optional``, the *whole*
union is considered optional. For example, OmegaConf treats all four of the
following type hints as equivalent:

- ``Optional[Union[int, str]]``
- ``Union[Optional[int], str]``
- ``Union[int, str, None]``
- ``Union[int, str, type(None)]``

Ordinarily, assignment to a structured config field results in coercion of the
assigned value to the field's type. For example, assigning an integer to a
field typed as ``str`` results in the integer being coverted to a string:

.. doctest::

    >>> @dataclass
    ... class HasStr:
    ...     s: str
    ...
    >>> cfg = OmegaConf.structured(HasStr)
    >>> cfg.s = 10.1
    >>> assert cfg.s == "10.1"  # The assigned value has been converted to a string

When dealing with ``Union`` types, however, conversion is disabled so as to
avoid ambiguity. Values assigned to a union-typed field of a structured config
must precisely match one of the types in the ``Union`` annotation:

.. doctest::

    >>> @dataclass
    ... class StrOrInt:
    ...     u: Union[str, float]
    ...
    >>> cfg = OmegaConf.structured(StrOrInt)
    >>> cfg.u = 10.1
    >>> assert cfg.u == 10.1  # The assigned value remains a `float`.
    >>> cfg.u = "10.1"
    >>> assert cfg.u == "10.1"  # The assigned value remains a `str`.
    >>> cfg.u = 123  # Conversion from `int` to `float` does not occur.
    Traceback (most recent call last):
    ...
    omegaconf.errors.ValidationError: Value '123' of type 'int' is incompatible with type hint 'Union[str, float]'
        full_key: u
        object_type=StrOrInt


.. _other_special_features:

Other special features
^^^^^^^^^^^^^^^^^^^^^^
OmegaConf supports field modifiers such as ``MISSING`` and ``Optional``.

.. doctest::

    >>> from typing import Optional
    >>> from omegaconf import MISSING

    >>> @dataclass
    ... class Modifiers:
    ...     num: int = 10
    ...     optional_num: Optional[int] = 10
    ...     another_num: int = MISSING
    ...     optional_dict: Optional[Dict[str, int]] = None
    ...     list_optional: List[Optional[int]] = field(default_factory=lambda: [10, MISSING, None])

    >>> conf: Modifiers = OmegaConf.structured(Modifiers)

Note for Python3.6 users: :ref:`pickling <save_and_load_pickle_file>`
structured configs with complex type annotations, such as dict-of-list or
list-of-optional, is not supported.

Mandatory missing values
++++++++++++++++++++++++

Fields assigned the constant ``MISSING`` do not have a value and the value must be set prior to accessing the field.
Otherwise a ``MissingMandatoryValue`` exception is raised.

.. doctest::

    >>> with raises(MissingMandatoryValue):
    ...     x = conf.another_num
    >>> conf.another_num = 20
    >>> assert conf.another_num == 20


Optional fields
+++++++++++++++

.. doctest::

    >>> with raises(ValidationError):
    ...     # regular fields cannot be assigned None
    ...     conf.num = None

    >>> conf.optional_num = None
    >>> assert conf.optional_num is None
    >>> assert conf.list_optional[2] is None



Interpolations
++++++++++++++

:ref:`interpolation` works normally with Structured configs, but static type checkers may object to you assigning a string to another type.
To work around this, use the special functions ``omegaconf.SI`` and ``omegaconf.II`` described below.

.. doctest::

    >>> from omegaconf import SI, II
    >>> @dataclass
    ... class Interpolation:
    ...     val: int = 100
    ...     # This will work, but static type checkers will complain
    ...     a: int = "${val}"
    ...     # This is equivalent to the above, but static type checkers
    ...     # will not complain
    ...     b: int = SI("${val}")
    ...     # This is syntactic sugar; the input string is
    ...     # wrapped with ${} automatically.
    ...     c: int = II("val")

    >>> conf: Interpolation = OmegaConf.structured(Interpolation)
    >>> assert conf.a == 100
    >>> assert conf.b == 100
    >>> assert conf.c == 100


Interpolated values are validated, and converted when possible, to the annotated type when the interpolation is accessed, e.g:

.. doctest::

    >>> from omegaconf import II
    >>> @dataclass
    ... class Interpolation:
    ...     str_key: str = "string"
    ...     int_key: int = II("str_key")

    >>> cfg = OmegaConf.structured(Interpolation)
    >>> cfg.int_key  # fails due to type mismatch
    Traceback (most recent call last):
      ...
    omegaconf.errors.InterpolationValidationError: Value 'string' could not be converted to Integer
        full_key: int_key
        object_type=Interpolation
    >>> cfg.str_key = "1234"  # string value
    >>> assert cfg.int_key == 1234  # automatically convert str to int

Note however that this validation step is currently skipped for container node interpolations:

.. doctest::

    >>> @dataclass
    ... class NotValidated:
    ...     some_int: int = 0
    ...     some_dict: Dict[str, str] = II("some_int")

    >>> cfg = OmegaConf.structured(NotValidated)
    >>> assert cfg.some_dict == 0  # type mismatch, but no error


Frozen classes
++++++++++++++

Frozen dataclasses and attr classes are supported via OmegaConf :ref:`read-only-flag`, which makes the entire config node and all if it's child nodes read-only.

.. doctest::

    >>> from dataclasses import dataclass, field
    >>> from typing import List
    >>> @dataclass(frozen=True)
    ... class FrozenClass:
    ...     x: int = 10
    ...     list: List = field(default_factory=lambda: [1, 2, 3])

    >>> conf = OmegaConf.structured(FrozenClass)
    >>> with raises(ReadonlyConfigError):
    ...    conf.x = 20

The read-only flag is recursive:

.. doctest::

    >>> with raises(ReadonlyConfigError):
    ...    conf.list[0] = 20

Merging with other configs
^^^^^^^^^^^^^^^^^^^^^^^^^^

Once an OmegaConf object is created, it can be merged with others regardless of its source.
OmegaConf configs created from Structured configs contains type information that is enforced at runtime.
This can be used to validate config files based on a schema specified in a structured config class

**example.yaml** file:

.. include:: example.yaml
   :code: yaml

A Schema for the above config can be defined like this.

.. doctest::

    >>> @dataclass
    ... class Server:
    ...     port: int = MISSING

    >>> @dataclass
    ... class Log:
    ...     file: str = MISSING
    ...     rotation: int = MISSING

    >>> @dataclass
    ... class MyConfig:
    ...     server: Server = field(default_factory=Server)
    ...     log: Log = field(default_factory=Log)
    ...     users: List[int] = field(default_factory=list)


I intentionally made an error in the type of the users list (``List[int]`` should be ``List[str]``).
This will cause a validation error when merging the config from the file with that from the scheme.

.. doctest::

    >>> schema = OmegaConf.structured(MyConfig)
    >>> conf = OmegaConf.load("source/example.yaml")
    >>> with raises(ValidationError):
    ...     OmegaConf.merge(schema, conf)


Using Metadata to Ignore Fields
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

OmegaConf inspects the metadata of dataclasss / attr class fields,
ignoring any fields where ``metadata["omegaconf_ignore"]`` is ``True``.
When defining a dataclass or attr class, fields can be given metadata by passing the
``metadata`` keyword argument to the ``dataclasses.field`` function or the ``attrs.field`` function:

.. doctest::

    >>> @dataclass
    ... class HasIgnoreMetadata:
    ...     normal_field: int = 1
    ...     field_ignored: int = field(default=2, metadata={"omegaconf_ignore": True})
    ...     field_not_ignored: int = field(default=3, metadata={"omegaconf_ignore": False})
    ...
    >>> cfg = OmegaConf.create(HasIgnoreMetadata)
    >>> cfg
    {'normal_field': 1, 'field_not_ignored': 3}

In the above example, ``field_ignored`` is ignored by OmegaConf.