Repository: dbt-labs/dbt-core

Issue:
'+' config prefix should handle whitespace
### Describe the bug
Macros are not working in post-hooks in `dbt_project.yml` but working as a model config

### Steps To Reproduce
1. Create an empty dbt project with `dbt init`
2. Create a macro in `macros/grants_test.sql`. I'm using this for testing:
```
{% macro grants_test(rel) %}
ALTER TABLE {{ rel }} SET OPTIONS (description="testing...")
{% endmacro %}
```
3. Modify the `dbt_project.yml` to add a post-hook. Based on the sample project `dbt init` creates I changed it to this:
```
models:
  my_new_project:
      # Applies to all files under models/example/
      example:
          + post-hook: [
            '{{ grants_test(this) }}'
          ]
          materialized: view
```


### Expected behavior
I would expect the post-hook to use the `grants_test` macro correctly. It actually works if instead we add it as a model config in for example: `models/example/my_first_dbt_model.sql`

### Screenshots and log output
```
Running with dbt=0.19.0
Encountered an error:
Compilation Error
  Could not render {{ grants_test(this) }}: 'grants_test' is undefined
```

### System information
**Which database are you using dbt with?**
- [ ] postgres
- [ ] redshift
- [x] bigquery
- [ ] snowflake
- [ ] other (specify: ____________)


**The output of `dbt --version`:**
```
installed version: 0.19.0
   latest version: 0.19.1

Your version of dbt is out of date! You can find instructions for upgrading here:
https://docs.getdbt.com/docs/installation

Plugins:
  - redshift: 0.19.0
  - bigquery: 0.19.0
  - snowflake: 0.19.0
  - postgres: 0.19.0
```

**The operating system you're using:**
Ubuntu 20.04

**The output of `python --version`:**
Python 3.8.5

### Additional context




Discussion:
I think I found the cause for this:
https://github.com/fishtown-analytics/dbt/issues/2583

That fixed the eager render of macros if using `+post-hook` in `dbt_project.yml`

Notice the way I'm adding a space after the plus sign `+ post-hook`. That space is not being stripped:
https://github.com/fishtown-analytics/dbt/blob/develop/core/dbt/config/renderer.py#L150

I think this would have to be changed to `(k.lstrip('+ ') if isinstance(k, str) else k)`
Hey @jmriego, thanks for opening, and nice work figuring this one out!

We don't document support for any whitespace between the `+` prefix and config keys today. At the same time, I don't think it should cause as much grief as it does. Your proposed fix is straightforward and seems reasonable to me.

Are you interested in opening a PR to make that one-line change?
thanks @jtcohen6 ! sorry, I opened this thinking it was a bug but yes, after noticing I shouldn't have put that space after the plus sign it all made sense. Sure, I'll open a PR about this. Good as a first PR

Resolving patch:
diff --git a/core/dbt/config/renderer.py b/core/dbt/config/renderer.py
--- a/core/dbt/config/renderer.py
+++ b/core/dbt/config/renderer.py
@@ -147,7 +147,7 @@ def should_render_keypath(self, keypath: Keypath) -> bool:
 
         if first in {'seeds', 'models', 'snapshots', 'tests'}:
             keypath_parts = {
-                (k.lstrip('+') if isinstance(k, str) else k)
+                (k.lstrip('+ ') if isinstance(k, str) else k)
                 for k in keypath
             }
             # model-level hooks
diff --git a/core/dbt/context/context_config.py b/core/dbt/context/context_config.py
--- a/core/dbt/context/context_config.py
+++ b/core/dbt/context/context_config.py
@@ -97,7 +97,7 @@ def _project_configs(
             result = {}
             for key, value in level_config.items():
                 if key.startswith('+'):
-                    result[key[1:]] = deepcopy(value)
+                    result[key[1:].strip()] = deepcopy(value)
                 elif not isinstance(value, dict):
                     result[key] = deepcopy(value)
 
diff --git a/core/dbt/parser/base.py b/core/dbt/parser/base.py
--- a/core/dbt/parser/base.py
+++ b/core/dbt/parser/base.py
@@ -282,7 +282,7 @@ def update_parsed_node_config(
     ) -> None:
         # Overwrite node config
         final_config_dict = parsed_node.config.to_dict(omit_none=True)
-        final_config_dict.update(config_dict)
+        final_config_dict.update({k.strip(): v for (k, v) in config_dict.items()})
         # re-mangle hooks, in case we got new ones
         self._mangle_hooks(final_config_dict)
         parsed_node.config = parsed_node.config.from_dict(final_config_dict)


Test patch:
diff --git a/test/integration/014_hook_tests/test_model_hooks.py b/test/integration/014_hook_tests/test_model_hooks.py
--- a/test/integration/014_hook_tests/test_model_hooks.py
+++ b/test/integration/014_hook_tests/test_model_hooks.py
@@ -243,6 +243,23 @@ def project_config(self):
         }
 
 
+class TestPrePostModelHooksOnSeedsPlusPrefixedWhitespace(TestPrePostModelHooksOnSeeds):
+    @property
+    def project_config(self):
+        return {
+            'config-version': 2,
+            'data-paths': ['data'],
+            'models': {},
+            'seeds': {
+                '+ post-hook': [
+                    'alter table {{ this }} add column new_col int',
+                    'update {{ this }} set new_col = 1'
+                ],
+                'quote_columns': False,
+            },
+        }
+
+
 class TestPrePostModelHooksOnSnapshots(DBTIntegrationTest):
     @property
     def schema(self):