Skip to content

Construction of OmegaConf from Structured Configs #87

@omry

Description

@omry

The associated PR (#86 ) is adding support for initializing OmegaConf from class files and objects.
Details below

  1. Config classes and objects (dataclass or attrs classes) can be converted to OmegaConf object.

  2. The following value types are supported:

  • with a default value assigned to it.
  • Optional fields (can accept None)
  • assigned omegaconf.MISSING, Equivalent for '???' in the yaml files (Mandatory missing).
    The semantics here is that this value is not yet set. it must be set prior to read access.
  • Interpolation using omegaconf.II() function :
    II("foo") is equivalent to ${foo} in the YAML file.
    Examples:
    • foo=II("bar") : Foo inherits the type and value of bar at runtime.
    • foo=II("http://${host}:${port}/") : foo is always a string. actual value is determined by host and port
    • foo=II("env:USER") : foo is evaluated on access, the type and value is determined by the function (env in this case)

Example for bool types (The same is supported for int, str, float and enum):

@dataclass
class BoolConfig:
    with_default: bool = True
    null_default: Optional[bool] = None
    mandatory_missing: bool = MISSING
    interpolation: bool = II("with_default")
  1. the typing information in them is available and acted on at runtime (composition and command line overrides are at runtime).

  2. type annotations can be used to get static type checking.

Currently the following is supported for both dataclasses and attr classes.
Examples below are @dataclasses. attr classes are similar.

@dataclass
class Database:
    name: str = "demo_app_db"
    # A an untyped list. will accept any type that can be represented by OmegaConf
    tables: List = field(default_factory=lambda: ["table1", "table2"])
    port: int = 3306

    # A list of integers, will reject anything that cannot be converted to int at runtime
    admin_ports: List[int] = field(default_factory=lambda: [8080, 1080])

    # A dict of string -> int, will reject any value that cannot be converted to int at runtime
    error_levels: Dict[str, int] = field(
        default_factory=lambda: {"info": 0, "error": 10}
    )

This class, and objects of this class can be used to construct an OmegaConf object:

# Create from a class
conf1 = OmegaConf.create(Database)

# Create from an object
conf2 = OmegaConf.create(Database(port=3307))

Python type annotation can then be used on the config object to static type checking:

def foo(cfg : Database):
   cfg.port = 10 # passes static type check
   cfg.port = "nope" # fails static type check
  1. Composition and overrides are often happening at runtime (from files, or command line flags).
    To support that, there is also a runtime validation and conversion.
    The following will succeed at runtime:
conf.merge_with_dotlist(['port=30']) # success
conf.merge_with_dotlist(['port=nope']) # runtime error
  1. OmegaConf objects created from Structured configs will be set to struct mode, this means that any access to a field not in the struct will result in a runtime error:
conf.pork = 80 # fail
  1. Typed containers are validated at runtime:
conf: Database = OmegaConf.create(Database)
conf.admin_ports[0] = 999  # okay
assert conf.admin_ports[0] == 999

conf.admin_ports[0] = "1000"  # also ok!
assert conf.admin_ports[0] == 1000

with pytest.raises(ValidationError):
    conf.admin_ports[0] = "fail"

with pytest.raises(ValidationError):
    conf.error_levels["info"] = "fail"
  1. Nested Object/Classes are supported, as well as Enums.
class Protocol(Enum):
    HTTP = 0
    HTTPS = 1


@dataclass
class ServerConfig:
    # Nested configs are by value.
    # This will be expanded to default values for Database
    db1: Database = Database()

    # This will be expanded to the values in the created Database instance
    db2: Database = Database(tables=["table3", "table4"])

    host: str = "localhost"
    port: int = 80
    website: str = MISSING
    protocol: Protocol = MISSING
    debug: bool = False
  1. Enums can be assigned by Enum, by name string or by value (the later should be used at runtime only).
    conf: ServerConfig = OmegaConf.create(ServerConfig(protocol=Protocol.HTTP))
    assert conf.protocol == Protocol.HTTP

    # The HTTPS string is converted to Protocol.HTTPS per the enum name
    conf.protocol = "HTTPS"
    assert conf.protocol == Protocol.HTTPS

    # The value 0 is converted to Protocol.HTTP per the enum value
    conf.protocol = 0
    assert conf.protocol == Protocol.HTTP

    # Enum names are care sensitive
    with pytest.raises(ValidationError):
        conf.protocol = "https"
 
    for value in (True, False, "1", "http", 1.0):
        with pytest.raises(ValidationError):
            conf.protocol = value
  1. Frozen state is preserved (and is recursive):
    @dataclass(frozen=True)
    class FrozenClass:
        x: int = 10
        list: List = field(default_factory=lambda: [1, 2, 3])

    conf = OmegaConf.create(FrozenClass)
    with pytest.raises(ReadonlyConfigError):
        conf.x = 20

    # Read-only flag is recursive
    with pytest.raises(ReadonlyConfigError):
        conf.list[0] = 20

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions