diff --git a/CHANGELOG.md b/CHANGELOG.md index ba8ac396ebe..16fa12965cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Changes + - Fix the error handling for profiles.yml validation ([#820](https://github.com/fishtown-analytics/dbt/pull/820)) - Make the `--threads` parameter actually change the number of threads used ([#819](https://github.com/fishtown-analytics/dbt/pull/819)) - Use Mapping instead of dict as the base class for APIObject ([#756](https://github.com/fishtown-analytics/dbt/pull/756)) - Write JSON manifest file to disk during compilation ([#761](https://github.com/fishtown-analytics/dbt/pull/761)) diff --git a/dbt/api/object.py b/dbt/api/object.py index 9348d2b62f1..de70348d146 100644 --- a/dbt/api/object.py +++ b/dbt/api/object.py @@ -66,18 +66,17 @@ def validate(self): """ validator = Draft4Validator(self.SCHEMA) - errors = [] + errors = set() # make errors a set to avoid duplicates for error in validator.iter_errors(self.serialize()): - errors.append('.'.join( + errors.add('.'.join( list(map(str, error.path)) + [error.message] )) if errors: - raise ValidationException( - 'Invalid arguments passed to "{}" instance: {}' - .format(type(self).__name__, - ", ".join(errors))) + msg = ('Invalid arguments passed to "{}" instance: {}'.format( + type(self).__name__, ', '.join(errors))) + raise ValidationException(msg) # implement the Mapping protocol: # https://docs.python.org/3/library/collections.abc.html diff --git a/dbt/exceptions.py b/dbt/exceptions.py index dccf2653a14..8cb41b09ce0 100644 --- a/dbt/exceptions.py +++ b/dbt/exceptions.py @@ -1,5 +1,6 @@ from dbt.compat import basestring from dbt.logger import GLOBAL_LOGGER as logger +import re class Exception(BaseException): diff --git a/dbt/project.py b/dbt/project.py index 22dbdc09efa..f3e55c36aee 100644 --- a/dbt/project.py +++ b/dbt/project.py @@ -234,15 +234,7 @@ class CredentialsValidator(APIObject): try: CredentialsValidator(**target_cfg) except dbt.exceptions.ValidationException as e: - if 'extra keys not allowed' in str(e): - raise DbtProjectError( - "Extra project configuration '{}' is not recognized" - .format('.'.join(e.path)), self) - else: - # TODO : does this fail if eg. project is missing? - raise DbtProjectError( - "Expected project configuration '{}' was not supplied" - .format('.'.join(e.path)), self) + raise DbtProjectError(str(e), self) def log_warnings(self): target_cfg = self.run_environment() diff --git a/test/unit/test_project.py b/test/unit/test_project.py new file mode 100644 index 00000000000..866044d4963 --- /dev/null +++ b/test/unit/test_project.py @@ -0,0 +1,85 @@ +import unittest + +import os +import dbt.project + + +class ProjectTest(unittest.TestCase): + def setUp(self): + self.profiles = { + 'test': { + 'outputs': { + 'test': { + 'type': 'postgres', + 'threads': 4, + 'host': 'database', + 'port': 5432, + 'user': 'root', + 'pass': 'password', + 'dbname': 'dbt', + 'schema': 'dbt_test' + } + }, + 'target': 'test' + } + } + self.cfg = { + 'name': 'X', + 'version': '0.1', + 'profile': 'test', + 'project-root': os.path.abspath('.'), + } + + def test_profile_validate_success(self): + # Make sure we can instantiate + validate a valid profile + + project = dbt.project.Project( + cfg=self.cfg, + profiles=self.profiles, + profiles_dir=None + ) + + project.validate() + + def test_profile_validate_missing(self): + del self.profiles['test']['outputs']['test']['schema'] + + project = dbt.project.Project( + cfg=self.cfg, + profiles=self.profiles, + profiles_dir=None + ) + + message = r'.*schema.* is a required property.*' + with self.assertRaisesRegexp(dbt.project.DbtProjectError, message): + project.validate() + + def test_profile_validate_extra(self): + self.profiles['test']['outputs']['test']['foo'] = 'bar' + + project = dbt.project.Project( + cfg=self.cfg, + profiles=self.profiles, + profiles_dir=None + ) + + message = r'.*not allowed.*foo.* was unexpected.*' + with self.assertRaisesRegexp(dbt.project.DbtProjectError, message): + project.validate() + + def test_profile_validate_missing_and_extra(self): + del self.profiles['test']['outputs']['test']['schema'] + self.profiles['test']['outputs']['test']['foo'] = 'bar' + + project = dbt.project.Project( + cfg=self.cfg, + profiles=self.profiles, + profiles_dir=None + ) + + unrecognized = r'not allowed.*foo.* was unexpected' + extra = r'schema.* is a required property' + # fun with regexp ordering: want both, don't care about order + message = '.*({0}.*{1}|{1}.*{0}).*'.format(unrecognized, extra) + with self.assertRaisesRegexp(dbt.project.DbtProjectError, message): + project.validate()