Repository: dbt-labs/dbt-core

Issue:
if a package name shadows internal context members, terrible things happen
### Describe the bug
If you run `dbt compile`, `dbt run`, you may get a message like this back:
```
Encountered an error:
'function' object has no attribute 'update' 
```

I think we can all agree that this is a very bad error message!

### Steps To Reproduce
1) Name your project the same as any built-in default dbt function `source`, `ref`, `log`, ...
2) `dbt compile`

Various built-in context members might result in similar messages with `function` replaced by another type (also bad) or even silently corrupt dbt internal behavior (much worse)

### Expected behavior
A clear and concise description of what you expected to happen.

I expect dbt to give me a good error message. Something like `package names may not shadow builtin context members: "log" is not a valid value for package name` is probably fine. This will be annoying when we add new things to contexts and users upgrade, but basically nothing will be as annoying as the current behavior.

### Screenshots and log output
There's a big long stack trace in the debug log that points to the culprit (see `Additional context` for more on that), but nothing else useful.

### System information
**Which database are you using dbt with?**
- [X] postgres
- [X] redshift
- [X] bigquery
- [X] snowflake
- [X] other (specify: Any!)


**The output of `dbt --version`:**
Any >= 0.13.0

**The operating system you're using:**
Any

**The output of `python --version`:**
Any

### Additional context
Some thoughts on fixing this:
 - this error comes up in `dbt.context.common._add_macro_map` via  `_add_macros`
 - Any fix for this has to handle dependencies as well as the root project
 - The current structure of `modify_generated_context`, which is what calls `_add_macros`, and `generate_model` (which calls `modify_generated_context`) make this not very straightforward, as we'll also want to explicitly blacklist the following keywords that aren't in the context yet at `_add_macros` time: `write`, `render`, `var`, `context`, `this`, `pre_hooks`, `post_hooks`, `sql`.
 - Maybe we should generate a reserved keywords list somewhere else in dbt and use that. If we did that, we could spit this information out before doing context generation, which means we could do it during `dbt debug`/`dbt ls`/similar as well, which seems like a big upside.



Discussion:

Resolving patch:
diff --git a/core/dbt/contracts/project.py b/core/dbt/contracts/project.py
--- a/core/dbt/contracts/project.py
+++ b/core/dbt/contracts/project.py
@@ -2,9 +2,8 @@
 from dbt.logger import GLOBAL_LOGGER as logger  # noqa
 from dbt import tracking
 from dbt.ui import printer
-# from dbt.utils import JSONEncoder
 
-from hologram import JsonSchemaMixin
+from hologram import JsonSchemaMixin, ValidationError
 from hologram.helpers import HyphenatedJsonSchemaMixin, register_pattern, \
     ExtensibleJsonSchemaMixin
 
@@ -92,24 +91,66 @@ class RegistryPackageMetadata(
     downloads: Downloads
 
 
+# A list of all the reserved words that packages may not have as names.
+BANNED_PROJECT_NAMES = {
+    '_sql_results',
+    'adapter',
+    'api',
+    'column',
+    'config',
+    'context',
+    'database',
+    'env',
+    'env_var',
+    'exceptions',
+    'execute',
+    'flags',
+    'fromjson',
+    'graph',
+    'invocation_id',
+    'load_agate_table',
+    'load_result',
+    'log',
+    'model',
+    'modules',
+    'post_hooks',
+    'pre_hooks',
+    'ref',
+    'render',
+    'return',
+    'run_started_at',
+    'schema',
+    'source',
+    'sql',
+    'sql_now',
+    'store_result',
+    'target',
+    'this',
+    'tojson',
+    'try_or_compiler_error',
+    'var',
+    'write',
+}
+
+
 @dataclass
 class Project(HyphenatedJsonSchemaMixin, Replaceable):
     name: Name
     version: Union[SemverString, float]
-    project_root: Optional[str]
-    source_paths: Optional[List[str]]
-    macro_paths: Optional[List[str]]
-    data_paths: Optional[List[str]]
-    test_paths: Optional[List[str]]
-    analysis_paths: Optional[List[str]]
-    docs_paths: Optional[List[str]]
-    target_path: Optional[str]
-    snapshot_paths: Optional[List[str]]
-    clean_targets: Optional[List[str]]
-    profile: Optional[str]
-    log_path: Optional[str]
-    modules_path: Optional[str]
-    quoting: Optional[Quoting]
+    project_root: Optional[str] = None
+    source_paths: Optional[List[str]] = None
+    macro_paths: Optional[List[str]] = None
+    data_paths: Optional[List[str]] = None
+    test_paths: Optional[List[str]] = None
+    analysis_paths: Optional[List[str]] = None
+    docs_paths: Optional[List[str]] = None
+    target_path: Optional[str] = None
+    snapshot_paths: Optional[List[str]] = None
+    clean_targets: Optional[List[str]] = None
+    profile: Optional[str] = None
+    log_path: Optional[str] = None
+    modules_path: Optional[str] = None
+    quoting: Optional[Quoting] = None
     on_run_start: Optional[List[str]] = field(default_factory=list)
     on_run_end: Optional[List[str]] = field(default_factory=list)
     require_dbt_version: Optional[Union[List[str], str]] = None
@@ -117,6 +158,16 @@ class Project(HyphenatedJsonSchemaMixin, Replaceable):
     seeds: Dict[str, Any] = field(default_factory=dict)
     packages: List[PackageSpec] = field(default_factory=list)
 
+    @classmethod
+    def from_dict(cls, data, validate=True):
+        result = super().from_dict(data, validate=validate)
+        if result.name in BANNED_PROJECT_NAMES:
+            raise ValidationError(
+                'Invalid project name: {} is a reserved word'
+                .format(result.name)
+            )
+        return result
+
 
 @dataclass
 class UserConfig(ExtensibleJsonSchemaMixin, Replaceable):


Test patch:
diff --git a/test/integration/001_simple_copy_test/test_simple_copy.py b/test/integration/001_simple_copy_test/test_simple_copy.py
--- a/test/integration/001_simple_copy_test/test_simple_copy.py
+++ b/test/integration/001_simple_copy_test/test_simple_copy.py
@@ -282,5 +282,3 @@ def test__snowflake__incremental_overwrite(self):
 
         results = self.run_dbt(["run"])
         self.assertEqual(len(results),  1)
-
-
diff --git a/test/unit/test_contracts_graph_manifest.py b/test/unit/test_contracts_graph_manifest.py
deleted file mode 100644
diff --git a/test/unit/test_contracts_project.py b/test/unit/test_contracts_project.py
new file mode 100644
--- /dev/null
+++ b/test/unit/test_contracts_project.py
@@ -0,0 +1,33 @@
+from .utils import ContractTestCase
+
+from hologram import ValidationError
+
+from dbt.contracts.project import Project
+
+class TestProject(ContractTestCase):
+    ContractType = Project
+
+    def test_minimal(self):
+        dct = {
+            'name': 'test',
+            'version': '1.0',
+            'profile': 'test',
+            'project-root': '/usr/src/app',
+        }
+        project = Project(
+            name='test',
+            version='1.0',
+            profile='test',
+            project_root='/usr/src/app',
+        )
+        self.assert_from_dict(project, dct)
+
+    def test_invalid_name(self):
+        dct = {
+            'name': 'log',
+            'version': '1.0',
+            'profile': 'test',
+            'project-root': '/usr/src/app',
+        }
+        with self.assertRaises(ValidationError):
+            Project.from_dict(dct)