From ab219e0bdb250af681372aaf637d38d8ab757b4d Mon Sep 17 00:00:00 2001 From: Jacob Beck Date: Tue, 4 Feb 2020 12:20:30 -0700 Subject: [PATCH] allow "dbt debug" from subdirectories When dbt_project.yml is missing, fail more gracefully When dbt_project.yml is missing and no profile is provided, validate all profiles: - read everything in profiles.yml - validate the default target for each profile - print no connection info - mark profiles.yml as valid --- core/dbt/adapters/factory.py | 4 +-- core/dbt/task/debug.py | 58 +++++++++++++++++++++--------------- 2 files changed, 36 insertions(+), 26 deletions(-) diff --git a/core/dbt/adapters/factory.py b/core/dbt/adapters/factory.py index b9ccf714018..439931e49e3 100644 --- a/core/dbt/adapters/factory.py +++ b/core/dbt/adapters/factory.py @@ -49,10 +49,10 @@ def load_plugin(self, name: str) -> Type[Credentials]: mod: Any = import_module('.' + name, 'dbt.adapters') except ModuleNotFoundError as exc: # if we failed to import the target module in particular, inform - # the user about it via a runtiem error - logger.info(f'Error importing adapter: {exc}') + # the user about it via a runtime error if exc.name == 'dbt.adapters.' + name: raise RuntimeException(f'Could not find adapter type {name}!') + logger.info(f'Error importing adapter: {exc}') # otherwise, the error had to have come from some underlying # library. Log the stack trace. logger.debug('', exc_info=True) diff --git a/core/dbt/task/debug.py b/core/dbt/task/debug.py index d8851a740d8..58c531c7b8f 100644 --- a/core/dbt/task/debug.py +++ b/core/dbt/task/debug.py @@ -16,7 +16,7 @@ from dbt.clients.yaml_helper import load_yaml_text from dbt.ui.printer import green, red -from dbt.task.base import BaseTask +from dbt.task.base import BaseTask, get_nearest_project_dir PROFILE_DIR_MESSAGE = """To view your profiles.yml file, run: @@ -70,7 +70,10 @@ def __init__(self, args, config): self.profiles_dir = getattr(self.args, 'profiles_dir', dbt.config.PROFILES_DIR) self.profile_path = os.path.join(self.profiles_dir, 'profiles.yml') - self.project_dir = args.project_dir or os.getcwd() + try: + self.project_dir = get_nearest_project_dir(self.args) + except dbt.exceptions.Exception: + self.project_dir = os.getcwd() self.project_path = os.path.join(self.project_dir, 'dbt_project.yml') self.cli_vars = dbt.utils.parse_cli_vars( getattr(self.args, 'vars', '{}') @@ -112,6 +115,7 @@ def run(self): print('python path: {}'.format(sys.executable)) print('os info: {}'.format(platform.platform())) print('Using profiles.yml file at {}'.format(self.profile_path)) + print('Using dbt_project.yml file at {}'.format(self.project_path)) print('') self.test_configuration() self.test_dependencies() @@ -160,7 +164,7 @@ def _target_found(self): return red('ERROR not found') return green('OK found') - def _choose_profile_name(self): + def _choose_profile_names(self) -> Optional[List[str]]: assert self.project or self.project_fail_details, \ '_load_project() required' @@ -171,7 +175,7 @@ def _choose_profile_name(self): args_profile = getattr(self.args, 'profile', None) try: - return Profile.pick_profile_name(args_profile, project_profile) + return [Profile.pick_profile_name(args_profile, project_profile)] except dbt.exceptions.DbtConfigError: pass # try to guess @@ -182,25 +186,25 @@ def _choose_profile_name(self): self.messages.append('The profiles.yml has no profiles') elif len(profiles) == 1: self.messages.append(ONLY_PROFILE_MESSAGE.format(profiles[0])) - return profiles[0] + return profiles else: self.messages.append(MULTIPLE_PROFILE_MESSAGE.format( '\n'.join(' - {}'.format(o) for o in profiles) )) + return profiles return None - def _choose_target_name(self): - has_raw_profile = (self.raw_profile_data and self.profile_name and - self.profile_name in self.raw_profile_data) - # mypy appeasement, we checked just above - assert self.raw_profile_data is not None - assert self.profile_name is not None + def _choose_target_name(self, profile_name: str): + has_raw_profile = (self.raw_profile_data and profile_name and + profile_name in self.raw_profile_data) if has_raw_profile: - raw_profile = self.raw_profile_data[self.profile_name] + # mypy appeasement, we checked just above + assert self.raw_profile_data is not None + raw_profile = self.raw_profile_data[profile_name] target_name, _ = Profile.render_profile( - raw_profile, self.profile_name, + raw_profile, profile_name, getattr(self.args, 'target', None), self.cli_vars ) return target_name @@ -224,16 +228,24 @@ def _load_profile(self): if isinstance(raw_profile_data, dict): self.raw_profile_data = raw_profile_data - self.profile_name = self._choose_profile_name() - self.target_name = self._choose_target_name() - try: - self.profile = QueryCommentedProfile.from_args( - self.args, self.profile_name - ) - except dbt.exceptions.DbtConfigError as exc: - self.profile_fail_details = str(exc) - return red('ERROR invalid') + profile_errors = [] + profile_names = self._choose_profile_names() + for profile_name in profile_names: + try: + profile: Profile = QueryCommentedProfile.from_args( + self.args, profile_name + ) + except dbt.exceptions.DbtConfigError as exc: + profile_errors.append(str(exc)) + else: + if len(profile_names) == 1: + # if a profile was specified, set it on the task + self.target_name = self._choose_target_name(profile_name) + self.profile = profile + if profile_errors: + self.profile_fail_details = '\n\n'.join(profile_errors) + return red('ERROR invalid') return green('OK found and valid') def test_git(self): @@ -279,8 +291,6 @@ def _log_profile_fail(self): return if self.profile_fail_details == FILE_NOT_FOUND: return - if self.profile_name is None: - return # we expect an error (no profile provided) print('Profile loading failed for the following reason:') print(self.profile_fail_details) print('')