Repository: dbt-labs/dbt-core

Issue:
An empty profile returns an un-useful error
### Steps To Reproduce
1. Create a profile that's empty (in `profiles.yml`):
```
jaffle_shop: 
```
2. Ensure your project uses this profile
2. Run any dbt command
### Expected behavior
Some sort of helpful error message, like "your profile is empty"

### Actual behaviour
```
Running with dbt=0.16.0
Encountered an error:
'NoneType' object has no attribute 'get'
```
### Screenshots and log output
If applicable, add screenshots or log output to help explain your problem.

### System information


**The output of `dbt --version`:**
0.16.0


### Additional context
Add any other context about the problem here.



Discussion:
Ooh, good catch, thanks @clrcrl! This was actually fixed (maybe inadvertently) in https://github.com/fishtown-analytics/dbt/pull/2290

```
09:51:26 ~/fishtown/clients/debug $ head dbt_project.yml

name: 'my_project'
version: '1.0'

profile:

09:51:30 ~/fishtown/clients/debug $ dbt run
Running with dbt=0.16.1-a1
Encountered an error while reading the project:
  ERROR: Runtime Error
  dbt cannot run because no profile was specified for this dbt project.
  To specify a profile for this project, add a line like the this to
  your dbt_project.yml file:

  profile: [profile name]

  Here, [profile name] should be replaced with a profile name
  defined in your profiles.yml file. You can find profiles.yml here:

  /Users/drew/.dbt/profiles.yml

Encountered an error:
Runtime Error
  Could not run dbt
```


It's going to ship in 0.16.1 next week :)
Hey @drewbanin, I'm not sure if that's the same issue.

I'm talking about having an empty profile in your `profiles.yml`, whereas the above is talking about having an empty `profile:` key in your `dbt_project.yml`

oooh, i understand, thanks for clarifying!

Adding this to the Octavius Catto milestone, should be a quick fix

For the implementer: we should add more validation around profile parsing code, raising a sensible error indicating what went wrong. We should not assume that profiles are well-formed at all in this part of the codebase.

Resolving patch:
diff --git a/core/dbt/config/profile.py b/core/dbt/config/profile.py
--- a/core/dbt/config/profile.py
+++ b/core/dbt/config/profile.py
@@ -52,7 +52,15 @@ def read_profile(profiles_dir: str) -> Dict[str, Any]:
     if os.path.isfile(path):
         try:
             contents = load_file_contents(path, strip=False)
-            return load_yaml_text(contents)
+            yaml_content = load_yaml_text(contents)
+            if not yaml_content:
+                msg = f'The profiles.yml file at {path} is empty'
+                raise DbtProfileError(
+                    INVALID_PROFILE_MESSAGE.format(
+                        error_string=msg
+                    )
+                )
+            return yaml_content
         except ValidationException as e:
             msg = INVALID_PROFILE_MESSAGE.format(error_string=e)
             raise ValidationException(msg) from e
@@ -280,7 +288,6 @@ def from_raw_profile_info(
         # user_cfg is not rendered.
         if user_cfg is None:
             user_cfg = raw_profile.get('config')
-
         # TODO: should it be, and the values coerced to bool?
         target_name, profile_data = cls.render_profile(
             raw_profile, profile_name, target_override, renderer
@@ -335,7 +342,15 @@ def from_raw_profiles(
         # First, we've already got our final decision on profile name, and we
         # don't render keys, so we can pluck that out
         raw_profile = raw_profiles[profile_name]
-
+        if not raw_profile:
+            msg = (
+                f'Profile {profile_name} in profiles.yml is empty'
+            )
+            raise DbtProfileError(
+                INVALID_PROFILE_MESSAGE.format(
+                    error_string=msg
+                )
+            )
         user_cfg = raw_profiles.get('config')
 
         return cls.from_raw_profile_info(
@@ -373,7 +388,6 @@ def render_from_args(
         raw_profiles = read_profile(args.profiles_dir)
         profile_name = cls.pick_profile_name(getattr(args, 'profile', None),
                                              project_profile_name)
-
         return cls.from_raw_profiles(
             raw_profiles=raw_profiles,
             profile_name=profile_name,


Test patch:
diff --git a/test/unit/test_config.py b/test/unit/test_config.py
--- a/test/unit/test_config.py
+++ b/test/unit/test_config.py
@@ -157,7 +157,8 @@ def setUp(self):
                     }
                 },
                 'target': 'other-postgres',
-            }
+            },
+            'empty_profile_data': {}
         }
         self.args = Args(profiles_dir=self.profiles_dir, cli_vars='{}',
                          version_check=True, project_dir=self.project_dir)
@@ -211,6 +212,10 @@ def write_profile(self, profile_data=None):
         with open(self.profile_path('profiles.yml'), 'w') as fp:
             yaml.dump(profile_data, fp)
 
+    def write_empty_profile(self):
+        with open(self.profile_path('profiles.yml'), 'w') as fp:
+            yaml.dump('', fp)
+
 
 class TestProfile(BaseConfigTest):
     def setUp(self):
@@ -530,6 +535,23 @@ def test_no_profile(self):
             self.from_args(project_profile_name=None)
         self.assertIn('no profile was specified', str(exc.exception))
 
+    def test_empty_profile(self):
+        self.write_empty_profile()
+        with self.assertRaises(dbt.exceptions.DbtProfileError) as exc:
+            self.from_args()
+        self.assertIn('profiles.yml is empty', str(exc.exception))
+
+    def test_profile_with_empty_profile_data(self):
+        renderer = empty_renderer()
+        with self.assertRaises(dbt.exceptions.DbtProfileError) as exc:
+            dbt.config.Profile.from_raw_profiles(
+                self.default_profile_data, 'empty_profile_data', renderer
+            )
+        self.assertIn(
+            'Profile empty_profile_data in profiles.yml is empty',
+            str(exc.exception)
+        )
+
 
 class TestProject(BaseConfigTest):
     def setUp(self):