Repository: dbt-labs/dbt-core

Issue:
Full-commented-out yaml file contents return ugly error
### Describe the bug

A user reports seeing this error when trying to parse their project after upgrading to v0.20.0:
```
Encountered an error:
argument of type 'NoneType' is not iterable
```

This is the stacktrace:
```
File "/usr/local/Cellar/dbt/0.20.0_1/libexec/lib/python3.8/site-packages/dbt/parser/schemas.py", line 560, in check_format_version
    if 'version' not in yaml_dct:
TypeError: argument of type 'NoneType' is not iterable
```

That's very surprising! The error cropping up in this line:
https://github.com/dbt-labs/dbt/blob/f460d275baa4df8cf8713f27e198a3577d6ea71c/core/dbt/parser/schemas.py#L560

Means that `yaml_dct` must be set to `None`, somehow. That method is called here:
https://github.com/dbt-labs/dbt/blob/f460d275baa4df8cf8713f27e198a3577d6ea71c/core/dbt/parser/read_files.py#L21-L23

It's pretty hard to imagine [`yaml_from_file`](https://github.com/dbt-labs/dbt/blob/f460d275baa4df8cf8713f27e198a3577d6ea71c/core/dbt/parser/schemas.py#L99) returning `None`. I've tried a couple different things to trip this error (an empty `.yml` file, malformed contents, simply the word `'None'`), but none (hah) of my attemps has worked. The only way I've managed to reproduce it is by stepping directly into the `validate_yaml` or `yaml_from_file` method calls and setting the yaml dictionary to `None`.

One important hint: The user reports that this project runs fine on earlier versions of dbt. It's only with the upgrade to v0.20.0 that they started seeing this error.

### Steps To Reproduce
???

### Expected behavior
Definitely not this error!

**The output of `dbt --version`:**
```
/usr/local/Cellar/dbt/0.20.0_1/
```
(i.e. dbt v0.20.0 installed via Homebrew)


Discussion:
I wanted to take a quick peek at this, and drilling all the way down to our call to the [yaml loader](https://github.com/dbt-labs/dbt/blob/f460d275baa4df8cf8713f27e198a3577d6ea71c/core/dbt/clients/yaml_helper.py#L60), we aren't passing along any `None` values in dbt. Looking at the [PyYaml documentation](https://pyyaml.org/wiki/PyYAMLDocumentation), this line stands out: "If there are no documents in the stream, it returns None." This, of course, is still very odd.
### reproduction case

Create a yaml file that's all comments:
```yml
# models/schema.yml
# only comments here
```

Try parsing the project:
```
$ dbt --no-partial-parse parse
Running with dbt=0.20.0
14:19:08 | Start parsing.
14:19:08 | Dependencies loaded
14:19:08 | ManifestLoader created
Encountered an error:
argument of type 'NoneType' is not iterable
```

I think the catch here is, `source_file.contents` is truthy, but `yaml.load()` of those contents returns `None`.
@nathaniel-may I'm assigning this to you to look into some more

Resolving patch:
diff --git a/core/dbt/clients/yaml_helper.py b/core/dbt/clients/yaml_helper.py
--- a/core/dbt/clients/yaml_helper.py
+++ b/core/dbt/clients/yaml_helper.py
@@ -1,5 +1,5 @@
 import dbt.exceptions
-
+from typing import Any, Dict, Optional
 import yaml
 import yaml.scanner
 
@@ -56,7 +56,7 @@ def contextualized_yaml_error(raw_contents, error):
                                      raw_error=error)
 
 
-def safe_load(contents):
+def safe_load(contents) -> Optional[Dict[str, Any]]:
     return yaml.load(contents, Loader=SafeLoader)
 
 
diff --git a/core/dbt/parser/manifest.py b/core/dbt/parser/manifest.py
--- a/core/dbt/parser/manifest.py
+++ b/core/dbt/parser/manifest.py
@@ -379,10 +379,10 @@ def parse_project(
         if not self.partially_parsing and HookParser in parser_types:
             hook_parser = HookParser(project, self.manifest, self.root_project)
             path = hook_parser.get_path()
-            file_block = FileBlock(
-                load_source_file(path, ParseFileType.Hook, project.project_name)
-            )
-            hook_parser.parse_file(file_block)
+            file = load_source_file(path, ParseFileType.Hook, project.project_name)
+            if file:
+                file_block = FileBlock(file)
+                hook_parser.parse_file(file_block)
 
         # Store the performance info
         elapsed = time.perf_counter() - start_timer
diff --git a/core/dbt/parser/read_files.py b/core/dbt/parser/read_files.py
--- a/core/dbt/parser/read_files.py
+++ b/core/dbt/parser/read_files.py
@@ -6,12 +6,13 @@
 from dbt.parser.schemas import yaml_from_file, schema_file_keys, check_format_version
 from dbt.exceptions import CompilationException
 from dbt.parser.search import FilesystemSearcher
+from typing import Optional
 
 
 # This loads the files contents and creates the SourceFile object
 def load_source_file(
         path: FilePath, parse_file_type: ParseFileType,
-        project_name: str) -> AnySourceFile:
+        project_name: str) -> Optional[AnySourceFile]:
     file_contents = load_file_contents(path.absolute_path, strip=False)
     checksum = FileHash.from_contents(file_contents)
     sf_cls = SchemaSourceFile if parse_file_type == ParseFileType.Schema else SourceFile
@@ -20,8 +21,11 @@ def load_source_file(
     source_file.contents = file_contents.strip()
     if parse_file_type == ParseFileType.Schema and source_file.contents:
         dfy = yaml_from_file(source_file)
-        validate_yaml(source_file.path.original_file_path, dfy)
-        source_file.dfy = dfy
+        if dfy:
+            validate_yaml(source_file.path.original_file_path, dfy)
+            source_file.dfy = dfy
+        else:
+            source_file = None
     return source_file
 
 
@@ -76,8 +80,10 @@ def get_source_files(project, paths, extension, parse_file_type):
         if parse_file_type == ParseFileType.Seed:
             fb_list.append(load_seed_source_file(fp, project.project_name))
         else:
-            fb_list.append(load_source_file(
-                fp, parse_file_type, project.project_name))
+            file = load_source_file(fp, parse_file_type, project.project_name)
+            # only append the list if it has contents. added to fix #3568
+            if file:
+                fb_list.append(file)
     return fb_list
 
 


Test patch:
diff --git a/test/integration/071_commented_yaml_regression_3568_test/models/schema.yml b/test/integration/071_commented_yaml_regression_3568_test/models/schema.yml
new file mode 100644
--- /dev/null
+++ b/test/integration/071_commented_yaml_regression_3568_test/models/schema.yml
@@ -0,0 +1,3 @@
+# models/schema.yml
+# only comments here
+# https://github.com/dbt-labs/dbt/issues/3568
\ No newline at end of file
diff --git a/test/integration/071_commented_yaml_regression_3568_test/test_all_comment_yml_files.py b/test/integration/071_commented_yaml_regression_3568_test/test_all_comment_yml_files.py
new file mode 100644
--- /dev/null
+++ b/test/integration/071_commented_yaml_regression_3568_test/test_all_comment_yml_files.py
@@ -0,0 +1,20 @@
+from test.integration.base import DBTIntegrationTest, use_profile
+
+
+class TestAllCommentYMLIsOk(DBTIntegrationTest):
+    @property
+    def schema(self):
+        return "071_commented_yaml"
+
+    @property
+    def models(self):
+        return "models"
+
+    @use_profile('postgres')
+    def test_postgres_parses_with_all_comment_yml(self):
+        try:
+            self.run_dbt(['parse'])
+        except TypeError:
+            assert False, '`dbt parse` failed with a yaml file that is all comments with the same exception as 3568'
+        except:
+            assert False, '`dbt parse` failed with a yaml file that is all comments'