Repository: facebookresearch/hydra

Issue:
Extended Structured Configs tutorial with One to many and many to many examples
Add examples for:
- 1 schema, many configs
- many schemas, many configs

Update Structured Configs tutorial to describe the two use cases.

See #917.


Discussion:

Resolving patch:
diff --git a/examples/tutorials/structured_configs/5_structured_config_schema/my_app.py b/examples/tutorials/structured_configs/5_structured_config_schema/my_app.py
--- a/examples/tutorials/structured_configs/5_structured_config_schema/my_app.py
+++ b/examples/tutorials/structured_configs/5_structured_config_schema/my_app.py
@@ -1,7 +1,7 @@
 # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
 from dataclasses import dataclass
 
-from omegaconf import MISSING, DictConfig, OmegaConf
+from omegaconf import MISSING, OmegaConf
 
 import hydra
 from hydra.core.config_store import ConfigStore
@@ -44,11 +44,12 @@ class Config:
 cs.store(group="db", name="postgresql", node=PostGreSQLConfig)
 
 
-# config here is config.yaml under the conf directory.
+# The config name matches both config.yaml under the conf directory and and the stored config
+# stored in the ConfigStore.
 # config.yaml will compose in db: mysql by default (per the defaults list),
 # and it will be validated against the schema from the Config class
 @hydra.main(config_path="conf", config_name="config")
-def my_app(cfg: DictConfig) -> None:
+def my_app(cfg: Config) -> None:
     print(OmegaConf.to_yaml(cfg))
 
 
diff --git a/examples/tutorials/structured_configs/6_static_schema_many_configs/my_app.py b/examples/tutorials/structured_configs/6_static_schema_many_configs/my_app.py
new file mode 100644
--- /dev/null
+++ b/examples/tutorials/structured_configs/6_static_schema_many_configs/my_app.py
@@ -0,0 +1,33 @@
+# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
+from dataclasses import dataclass
+
+from omegaconf import MISSING, OmegaConf
+
+import hydra
+from hydra.core.config_store import ConfigStore
+
+
+@dataclass
+class DBConfig:
+    driver: str = MISSING
+    host: str = MISSING
+    user: str = MISSING
+    password: str = MISSING
+
+
+@dataclass
+class Config:
+    db: DBConfig = MISSING
+
+
+cs = ConfigStore.instance()
+cs.store(name="config", node=Config)
+
+
+@hydra.main(config_path="conf", config_name="config")
+def my_app(cfg: Config) -> None:
+    print(OmegaConf.to_yaml(cfg))
+
+
+if __name__ == "__main__":
+    my_app()
diff --git a/examples/tutorials/structured_configs/7_dynamic_schema_many_configs/my_app.py b/examples/tutorials/structured_configs/7_dynamic_schema_many_configs/my_app.py
new file mode 100644
--- /dev/null
+++ b/examples/tutorials/structured_configs/7_dynamic_schema_many_configs/my_app.py
@@ -0,0 +1,47 @@
+# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
+from dataclasses import dataclass
+
+from omegaconf import MISSING, OmegaConf
+
+import hydra
+from hydra.core.config_store import ConfigStore
+
+
+@dataclass
+class DBConfig:
+    driver: str = MISSING
+    host: str = MISSING
+
+
+@dataclass
+class MySQLConfig(DBConfig):
+    driver: str = "mysql"
+    encoding: str = MISSING
+
+
+@dataclass
+class PostGreSQLConfig(DBConfig):
+    driver: str = "postgresql"
+    timeout: int = MISSING
+
+
+@dataclass
+class Config:
+    db: DBConfig = MISSING
+
+
+cs = ConfigStore.instance()
+# we use a different group to avoid collision with the user's db group.
+# Note that we use the package db to to ensure the schema is merged into the right node
+cs.store(group="schema/db", name="mysql", node=MySQLConfig, package="db")
+cs.store(group="schema/db", name="postgresql", node=PostGreSQLConfig, package="db")
+cs.store(name="config", node=Config)
+
+
+@hydra.main(config_path="conf", config_name="config")
+def my_app(cfg: Config) -> None:
+    print(OmegaConf.to_yaml(cfg))
+
+
+if __name__ == "__main__":
+    my_app()


Test patch:
diff --git a/tests/test_examples/test_structured_configs_tutorial.py b/tests/test_examples/test_structured_configs_tutorial.py
--- a/tests/test_examples/test_structured_configs_tutorial.py
+++ b/tests/test_examples/test_structured_configs_tutorial.py
@@ -11,7 +11,7 @@
 chdir_hydra_root()
 
 
-def test_structured_configs_1_basic_run(tmpdir: Path) -> None:
+def test_1_basic_run(tmpdir: Path) -> None:
     result = get_run_output(
         [
             "examples/tutorials/structured_configs/1_minimal/my_app.py",
@@ -21,7 +21,7 @@ def test_structured_configs_1_basic_run(tmpdir: Path) -> None:
     assert result == "Host: localhost, port: 3306"
 
 
-def test_structured_configs_1_basic_run_with_override_error(tmpdir: Path) -> None:
+def test_1_basic_run_with_override_error(tmpdir: Path) -> None:
     expected = """Key 'pork' not in 'MySQLConfig'
 \tfull_key: pork
 \treference_type=Optional[MySQLConfig]
@@ -35,7 +35,7 @@ def test_structured_configs_1_basic_run_with_override_error(tmpdir: Path) -> Non
     assert re.search(re.escape(expected), err) is not None
 
 
-def test_structured_configs_1_basic_override(tmpdir: Path) -> None:
+def test_1_basic_override(tmpdir: Path) -> None:
     result = get_run_output(
         [
             "examples/tutorials/structured_configs/1_minimal/my_app.py",
@@ -46,7 +46,7 @@ def test_structured_configs_1_basic_override(tmpdir: Path) -> None:
     assert result == "Host: localhost, port: 9090"
 
 
-def test_structured_configs_1_basic_override_type_error(tmpdir: Path) -> None:
+def test_1_basic_override_type_error(tmpdir: Path) -> None:
     cmd = [
         "examples/tutorials/structured_configs/1_minimal/my_app.py",
         "hydra.run.dir=" + str(tmpdir),
@@ -62,7 +62,7 @@ def test_structured_configs_1_basic_override_type_error(tmpdir: Path) -> None:
     assert re.search(re.escape(expected), err) is not None
 
 
-def test_structured_configs_2_static_complex(tmpdir: Path) -> None:
+def test_2_static_complex(tmpdir: Path) -> None:
     result = get_run_output(
         [
             "examples/tutorials/structured_configs/2_static_complex/my_app.py",
@@ -82,9 +82,7 @@ def test_structured_configs_2_static_complex(tmpdir: Path) -> None:
         ),
     ],
 )
-def test_structured_configs_3_config_groups(
-    tmpdir: Path, overrides: Any, expected: Any
-) -> None:
+def test_3_config_groups(tmpdir: Path, overrides: Any, expected: Any) -> None:
     cmd = [
         "examples/tutorials/structured_configs/3_config_groups/my_app.py",
         "hydra.run.dir=" + str(tmpdir),
@@ -116,7 +114,7 @@ def test_structured_configs_3_config_groups(
         ),
     ],
 )
-def test_structured_configs_3_config_groups_with_inheritance(
+def test_3_config_groups_with_inheritance(
     tmpdir: Path, overrides: Any, expected: Any
 ) -> None:
     cmd = [
@@ -128,7 +126,7 @@ def test_structured_configs_3_config_groups_with_inheritance(
     assert res == expected
 
 
-def test_structured_configs_4_defaults(tmpdir: Path) -> None:
+def test_4_defaults(tmpdir: Path) -> None:
     cmd = [
         "examples/tutorials/structured_configs/4_defaults/my_app.py",
         "hydra.run.dir=" + str(tmpdir),
@@ -145,7 +143,7 @@ def test_structured_configs_4_defaults(tmpdir: Path) -> None:
     }
 
 
-def test_structured_configs_5_structured_config_schema(tmpdir: Path) -> None:
+def test_5_structured_config_schema(tmpdir: Path) -> None:
     cmd = [
         "examples/tutorials/structured_configs/5_structured_config_schema/my_app.py",
         "hydra.run.dir=" + str(tmpdir),
@@ -160,3 +158,31 @@ def test_structured_configs_5_structured_config_schema(tmpdir: Path) -> None:
             "user": "omry",
         }
     }
+
+
+def test_6_one_schema_many_configs(tmpdir: Path) -> None:
+    cmd = [
+        "examples/tutorials/structured_configs/6_static_schema_many_configs/my_app.py",
+        "hydra.run.dir=" + str(tmpdir),
+        "db=prod",
+    ]
+    result = get_run_output(cmd)
+    assert OmegaConf.create(result) == {
+        "db": {
+            "driver": "mysql",
+            "host": "mysql001.prod",
+            "user": "root",
+            "password": "1234",
+        }
+    }
+
+
+def test_7_multiple_schemas_multiple_configs(tmpdir: Path) -> None:
+    cmd = [
+        "examples/tutorials/structured_configs/7_dynamic_schema_many_configs/my_app.py",
+        "hydra.run.dir=" + str(tmpdir),
+    ]
+    result = get_run_output(cmd)
+    assert OmegaConf.create(result) == {
+        "db": {"driver": "mysql", "host": "mysql001.staging", "encoding": "utf-8"}
+    }