Repository: dbt-labs/dbt-core

Issue:
Raise a warning for required, but uninstalled packages
### Describe the feature
The error message for an uninstalled package comes straight out of the Jinja library:
```
Compilation Error in analysis scratch (analysis/scratch.sql)
    'dbt_utils' is undefined
```

This error message _is_ strictly true, but I think it might be worth taking the time to write a more explicit warning message for uninstalled packages

```
Warning: The following packages are listed as required in `packages.yml` but have not been installed
- dbt_utils

Run `dbt deps` to install these packages
```

Since we have the expected packages in `packages.yml` and the installed packages in `dbt_modules`, this should be possible, right? And not that hard?

Note: I've only got it as a warning so that the places where the package is referenced still causes an error, but I'm open to having this as an error instead

### Describe alternatives you've considered
Keep as is

### Who will this benefit?
Confused users

### Are you interested in contributing this feature?
No ty :)



Discussion:
Love it. I'll take a look at some of the code paths involved, and see if this can be a good first issue
FYI, I finally stumbled upon this issue when googling this error message, so yes, updated error message would be very much appreciated.
The message for this is indeed coming from Jinja. The simplest version could just raise a more helpful compilation exception when Jinja returns an `UndefinedError` here:
https://github.com/fishtown-analytics/dbt/blob/749f87397ec1e0a270b2e09bd8dbeb71862fdb81/core/dbt/clients/jinja.py#L505-L506

For instance:
```python
    except jinja2.exceptions.UndefinedError as e:
        raise CompilationException(
            f'{e}. This can happen when calling a macro that is missing. ' \
            'Do you need to install a package dependency with `dbt deps`?'
        , node) from e
```
```
Encountered an error:
Runtime Error
  Compilation Error in model model_b (models/model_b.sql)
    'dbt_utils' is undefined. This can happen when calling a macro that is missing. Do you need to install a package dependency with `dbt deps`?
```

The downside of an approach this simple is that it isn't possible to distinguish between an undefined value that corresponds to a missing namespace/package, versus just a typo in the macro call (`{{ my_marco() }}` when you've defined `{% macro my_macro() %}`). So the message has to be a bit vague. The upside is that we don't need to make the Jinja context aware of the information in `packages.yml`.

I'm going to mark this a good first issue! And a nice chance to add a new [dbt exception](https://github.com/fishtown-analytics/dbt/blob/develop/core/dbt/exceptions.py) :)
@jtcohen6 I'd love to jump onto this one in the next day or so.

Just to clarify, regarding:

> I'm going to mark this a good first issue! And a nice chance to add a new dbt exception :)

Will catching and reraising the exception be enough, or do I also need to modify `exceptions.py`?
@jaypeedevlin Nice! Would love to have you contribute this :)

I think a new exception in `exceptions.py` is warranted, to wrap the error message above (rather than burying it deep in `clients/jinja.py`), and to name that we're doing something a little funky: `PossiblyUninstalledPackageError`?

After rereading this issue, the _absolute best_ version of this probably does the thing Claire mentioned in the original description, and checks to (as a very first step) to see if the contents of `dbt_modules` matches what's specified in `packages.yml`. That would involve touching more code-paths than this approach, which I think is good enough for 90% of times users see this `UndefinedError`.

Resolving patch:
diff --git a/core/dbt/clients/jinja.py b/core/dbt/clients/jinja.py
--- a/core/dbt/clients/jinja.py
+++ b/core/dbt/clients/jinja.py
@@ -29,7 +29,8 @@
 from dbt.contracts.graph.parsed import ParsedSchemaTestNode
 from dbt.exceptions import (
     InternalException, raise_compiler_error, CompilationException,
-    invalid_materialization_argument, MacroReturn, JinjaRenderingException
+    invalid_materialization_argument, MacroReturn, JinjaRenderingException,
+    UndefinedMacroException
 )
 from dbt import flags
 from dbt.logger import GLOBAL_LOGGER as logger  # noqa
@@ -518,7 +519,7 @@ def catch_jinja(node=None) -> Iterator[None]:
         e.translated = False
         raise CompilationException(str(e), node) from e
     except jinja2.exceptions.UndefinedError as e:
-        raise CompilationException(str(e), node) from e
+        raise UndefinedMacroException(str(e), node) from e
     except CompilationException as exc:
         exc.add_node(node)
         raise
diff --git a/core/dbt/config/runtime.py b/core/dbt/config/runtime.py
--- a/core/dbt/config/runtime.py
+++ b/core/dbt/config/runtime.py
@@ -328,6 +328,17 @@ def load_dependencies(self) -> Mapping[str, 'RuntimeConfig']:
         if self.dependencies is None:
             all_projects = {self.project_name: self}
             internal_packages = get_include_paths(self.credentials.type)
+            # raise exception if fewer installed packages than in packages.yml
+            count_packages_specified = len(self.packages.packages)  # type: ignore
+            count_packages_installed = len(tuple(self._get_project_directories()))
+            if count_packages_specified > count_packages_installed:
+                raise_compiler_error(
+                    f'dbt found {count_packages_specified} package(s) '
+                    f'specified in packages.yml, but only '
+                    f'{count_packages_installed} package(s) installed '
+                    f'in {self.modules_path}. Run "dbt deps" to '
+                    f'install package dependencies.'
+                )
             project_paths = itertools.chain(
                 internal_packages,
                 self._get_project_directories()
diff --git a/core/dbt/exceptions.py b/core/dbt/exceptions.py
--- a/core/dbt/exceptions.py
+++ b/core/dbt/exceptions.py
@@ -289,6 +289,15 @@ class JinjaRenderingException(CompilationException):
     pass
 
 
+class UndefinedMacroException(CompilationException):
+
+    def __str__(self, prefix='! ') -> str:
+        msg = super().__str__(prefix)
+        return f'{msg}. This can happen when calling a macro that does ' \
+            'not exist. Check for typos and/or install package dependencies ' \
+            'with "dbt deps".'
+
+
 class UnknownAsyncIDException(Exception):
     CODE = 10012
     MESSAGE = 'RPC server got an unknown async ID'


Test patch:
diff --git a/test/integration/029_docs_generate_tests/test_docs_generate.py b/test/integration/029_docs_generate_tests/test_docs_generate.py
--- a/test/integration/029_docs_generate_tests/test_docs_generate.py
+++ b/test/integration/029_docs_generate_tests/test_docs_generate.py
@@ -130,17 +130,6 @@ def dir(path):
     def models(self):
         return self.dir("models")
 
-    @property
-    def packages_config(self):
-        return {
-            'packages': [
-                {
-                    'git': 'https://github.com/fishtown-analytics/dbt-integration-project',
-                    'revision': 'dbt/0.17.0',
-                },
-            ],
-        }
-
     @property
     def project_config(self):
         return {
diff --git a/test/integration/033_event_tracking_test/test_events.py b/test/integration/033_event_tracking_test/test_events.py
--- a/test/integration/033_event_tracking_test/test_events.py
+++ b/test/integration/033_event_tracking_test/test_events.py
@@ -46,6 +46,7 @@ def run_event_test(
         expect_pass=True,
         expect_raise=False
     ):
+        self.run_dbt(["deps"])
         track_fn.reset_mock()
 
         project_id = hashlib.md5(
@@ -541,6 +542,7 @@ def test__postgres_event_tracking_model_error(self):
     @use_profile("postgres")
     def test__postgres_event_tracking_tests(self):
         # TODO: dbt does not track events for tests, but it should!
+        self.run_dbt(["deps"])
         self.run_dbt(["run", "--model", "example", "example_2"])
 
         expected_calls = [
diff --git a/test/integration/059_source_overrides_test/test_source_overrides.py b/test/integration/059_source_overrides_test/test_source_overrides.py
--- a/test/integration/059_source_overrides_test/test_source_overrides.py
+++ b/test/integration/059_source_overrides_test/test_source_overrides.py
@@ -76,11 +76,6 @@ def _set_updated_at_to(self, delta):
 
     @use_profile('postgres')
     def test_postgres_source_overrides(self):
-        # without running 'deps', our source overrides are invalid
-        _, stdout = self.run_dbt_and_capture(['compile'], strict=False)
-        self.assertRegex(stdout, r'WARNING(\x1b\[0m\])?: During parsing, dbt encountered source overrides that had no target')
-        schema_path = os.path.join('models', 'schema.yml')
-        self.assertIn(f'Source localdep.my_source (in {schema_path})', stdout)
         self.run_dbt(['deps'])
         seed_results = self.run_dbt(['seed'])
         assert len(seed_results) == 5
diff --git a/test/integration/100_rpc_test/test_rpc.py b/test/integration/100_rpc_test/test_rpc.py
--- a/test/integration/100_rpc_test/test_rpc.py
+++ b/test/integration/100_rpc_test/test_rpc.py
@@ -722,8 +722,8 @@ def test_invalid_requests_postgres(self):
             task_tags={'some_tag': True, 'another_tag': 'blah blah blah'}
         ).json()
         error_data = self.assertIsErrorWith(data, 10004, 'Compilation Error', {
-            'type': 'CompilationException',
-            'message': "Compilation Error in rpc mymodel (from remote system)\n  'reff' is undefined",
+            'type': 'UndefinedMacroException',
+            'message': "Compilation Error in rpc mymodel (from remote system)\n  'reff' is undefined. This can happen when calling a macro that does not exist. Check for typos and/or install package dependencies with \"dbt deps\".",
             'compiled_sql': None,
             'raw_sql': 'select * from {{ reff("nonsource_descendant") }}',
             'tags': {'some_tag': True, 'another_tag': 'blah blah blah'}
@@ -1143,11 +1143,8 @@ def models(self):
     def _check_start_predeps(self):
         self.assertFalse(os.path.exists('./dbt_modules'))
         status = self.assertIsResult(self.query('status').json())
-        self.assertEqual(status['state'], 'ready')
-
-        self.assertIsError(self.async_query('compile').json())
-        if os.path.exists('./dbt_modules'):
-            self.assertEqual(len(os.listdir('./dbt_modules')), 0)
+        # will return an error because defined dependency is missing
+        self.assertEqual(status['state'], 'error')
         return status
 
     def _check_deps_ok(self, status):
diff --git a/test/rpc/test_deps.py b/test/rpc/test_deps.py
--- a/test/rpc/test_deps.py
+++ b/test/rpc/test_deps.py
@@ -19,13 +19,14 @@ def deps_with_packages(packages, bad_packages, project_dir, profiles_dir, schema
         profiles_dir=profiles_dir,
         schema=schema,
         test_kwargs={},
+        criteria='error',
     )
 
     with querier_ctx as querier:
         # we should be able to run sql queries at startup
-        querier.async_wait_for_result(querier.run_sql('select 1 as id'))
+        querier.is_error(querier.run_sql('select 1 as id'))
 
-        # the status should be something positive
+        # the status should be an error as deps wil not be defined
         querier.is_result(querier.status())
 
         # deps should pass