Repository: facebookresearch/hydra (Python)

Issue:
Hydra 1.1 when composing a config that is a list at the top level.
Hydra 1.1 raises an error when composing a config if a used config is a ListConfig.

1. Determine behavior in Hydra 1.0
2. If behavior makes sense, allow maintain compatibility, otherwise issue an appropriate error.
Current error:
```python
        if not isinstance(ret.config, DictConfig):
            raise ValueError(
                f"Config {config_path} must be a Dictionary, got {type(ret).__name__}"
            )
```

(Note that the error is reporting the wrong type, should be `type(ret.config)`).

The following should reproduce the issue:

`group/file.yaml`:
```yaml
- a
```

`config.yaml`
```
defaults:
 - group: file
```


Discussion:
examples/advanced/ad_hoc_composition/conf/config.yaml



Patch:
diff --git a/hydra/_internal/config_loader_impl.py b/hydra/_internal/config_loader_impl.py
index 26ed1e49793..65405d9d4b7 100644
--- a/hydra/_internal/config_loader_impl.py
+++ b/hydra/_internal/config_loader_impl.py
@@ -168,6 +168,11 @@ def _process_config_searchpath(
         else:
             primary_config = OmegaConf.create()
 
+        if not OmegaConf.is_dict(primary_config):
+            raise ConfigCompositionException(
+                f"primary config '{config_name}' must be a DictConfig, got {type(primary_config).__name__}"
+            )
+
         def is_searchpath_override(v: Override) -> bool:
             return v.get_key_element() == "hydra.searchpath"
 
@@ -395,9 +400,9 @@ def _load_single_config(
         ret = repo.load_config(config_path=config_path)
         assert ret is not None
 
-        if not isinstance(ret.config, DictConfig):
+        if not OmegaConf.is_config(ret.config):
             raise ValueError(
-                f"Config {config_path} must be a Dictionary, got {type(ret).__name__}"
+                f"Config {config_path} must be an OmegaConf config, got {type(ret.config).__name__}"
             )
 
         if not ret.is_schema_source:
@@ -445,14 +450,16 @@ def _load_single_config(
                         default.config_path is not None
                         and default.config_path.startswith("hydra/")
                     )
+                    config = ret.config
                     if (
                         default.primary
-                        and "hydra" in ret.config
+                        and isinstance(config, DictConfig)
+                        and "hydra" in config
                         and not hydra_config_group
                     ):
-                        hydra = ret.config.pop("hydra")
+                        hydra = config.pop("hydra")
 
-                    merged = OmegaConf.merge(schema.config, ret.config)
+                    merged = OmegaConf.merge(schema.config, config)
                     assert isinstance(merged, DictConfig)
 
                     if hydra is not None:
@@ -470,6 +477,7 @@ def _load_single_config(
         if (
             not default.primary
             and config_path != "hydra/config"
+            and isinstance(res.config, DictConfig)
             and OmegaConf.select(res.config, "hydra.searchpath") is not None
         ):
             raise ConfigCompositionException(
diff --git a/news/1724.yaml b/news/1724.yaml
new file mode 100644
index 00000000000..7a079bc388a
--- /dev/null
+++ b/news/1724.yaml
@@ -0,0 +1,1 @@
+Fixed composition where top level of a config composed from a config group is a list


Test patch:
diff --git a/hydra/test_utils/configs/top_level_list/file1.yaml b/hydra/test_utils/configs/top_level_list/file1.yaml
new file mode 100644
index 00000000000..46ae6f50f57
--- /dev/null
+++ b/hydra/test_utils/configs/top_level_list/file1.yaml
@@ -0,0 +1,1 @@
+- a
diff --git a/tests/test_compose.py b/tests/test_compose.py
index 780fee4ec72..407b2293c3b 100644
--- a/tests/test_compose.py
+++ b/tests/test_compose.py
@@ -71,6 +71,12 @@ def test_initialize_with_config_path(hydra_restore_singletons: Any) -> None:
         (None, ["+foo=bar"], {"foo": "bar"}),
         ("compose", [], {"foo": 10, "bar": 100}),
         ("compose", ["group1=file2"], {"foo": 20, "bar": 100}),
+        (None, ["+top_level_list=file1"], {"top_level_list": ["a"]}),
+        (
+            None,
+            ["+top_level_list=file1", "top_level_list.0=b"],
+            {"top_level_list": ["b"]},
+        ),
     ],
 )
 class TestCompose:
@@ -92,6 +98,16 @@ def test_strict_failure_global_strict(
             compose(config_file, overrides)
 
 
+@mark.usefixtures("initialize_hydra")
+@mark.parametrize("config_path", ["../hydra/test_utils/configs"])
+def test_top_level_config_is_list() -> None:
+    with raises(
+        HydraException,
+        match="primary config 'top_level_list/file1' must be a DictConfig, got ListConfig",
+    ):
+        compose("top_level_list/file1", overrides=[])
+
+
 @mark.usefixtures("hydra_restore_singletons")
 @mark.parametrize(
     "config_file, overrides, expected",