From f66e573d27faf26ca00232f07f1ff1ac5b6ffece Mon Sep 17 00:00:00 2001 From: Gerda Shank Date: Thu, 10 Sep 2020 14:51:24 -0400 Subject: [PATCH] Add better error messages for yaml selectors --- CHANGELOG.md | 1 + core/dbt/config/selectors.py | 13 +++++- core/dbt/graph/cli.py | 15 +++++-- test/unit/test_selector_errors.py | 71 +++++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 5 deletions(-) create mode 100644 test/unit/test_selector_errors.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 0401d893d53..7602de866f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Added state and defer arguments to the RPC client, matching the CLI ([#2678](https://github.com/fishtown-analytics/dbt/issues/2678), [#2736](https://github.com/fishtown-analytics/dbt/pull/2736)) - Added schema and dbt versions to JSON artifacts ([#2670](https://github.com/fishtown-analytics/dbt/issues/2670), [#2767](https://github.com/fishtown-analytics/dbt/pull/2767)) - Added ability to snapshot hard-deleted records (opt-in with `invalidate_hard_deletes` config option). ([#249](https://github.com/fishtown-analytics/dbt/issues/249), [#2749](https://github.com/fishtown-analytics/dbt/pull/2749)) +- Improved error messages for YAML selectors ([#2700](https://github.com/fishtown-analytics/dbt/issues/2700), [#2781](https://github.com/fishtown-analytics/dbt/pull/2781)) Contributors: - [@joelluijmes](https://github.com/joelluijmes) ([#2749](https://github.com/fishtown-analytics/dbt/pull/2749)) diff --git a/core/dbt/config/selectors.py b/core/dbt/config/selectors.py index 74c85aae344..6995c6a1554 100644 --- a/core/dbt/config/selectors.py +++ b/core/dbt/config/selectors.py @@ -1,5 +1,6 @@ from pathlib import Path from typing import Dict, Any +import yaml from hologram import ValidationError @@ -33,7 +34,17 @@ def from_dict(cls, data: Dict[str, Any]) -> 'SelectorConfig': try: selector_file = SelectorFile.from_dict(data) selectors = parse_from_selectors_definition(selector_file) - except (ValidationError, RuntimeException) as exc: + except ValidationError as exc: + yaml_sel_cfg = yaml.dump(exc.instance) + raise DbtSelectorsError( + f"Could not parse selector file data: \n{yaml_sel_cfg}\n" + f"Valid selector description definitions: " + f"union, intersection, string, dictionary. No lists. " + f"\nhttps://docs.getdbt.com/reference/node-selection/" + f"yaml-selectors", + result_type='invalid_selector' + ) from exc + except RuntimeException as exc: raise DbtSelectorsError( f'Could not read selector file data: {exc}', result_type='invalid_selector', diff --git a/core/dbt/graph/cli.py b/core/dbt/graph/cli.py index b743022472c..bf8954200a0 100644 --- a/core/dbt/graph/cli.py +++ b/core/dbt/graph/cli.py @@ -1,5 +1,6 @@ # special support for CLI argument parsing. import itertools +import yaml from typing import ( Dict, List, Optional, Tuple, Any, Union @@ -165,9 +166,10 @@ def _parse_include_exclude_subdefs( if isinstance(definition, dict) and 'exclude' in definition: # do not allow multiple exclude: defs at the same level if diff_arg is not None: + yaml_sel_cfg = yaml.dump(definitions) raise ValidationException( - f'Got multiple exclusion definitions in definition list ' - f'{definitions}' + f"You cannot provide multiple exclude arguments to the " + f"same selector set operator:\n{yaml_sel_cfg}" ) diff_arg = _parse_exclusions(definition) else: @@ -251,10 +253,15 @@ def parse_from_definition(definition: RawDefinition) -> SelectionSpec: return parse_intersection_definition(definition) elif isinstance(definition, dict): return parse_dict_definition(definition) + elif isinstance(definition, list): + raise ValidationException( + f'Selector definition root level list not allowed. Use Union to ' + f'contain list. {definition}' + ) else: raise ValidationException( - f'Expected to find str or dict, instead found ' - f'{type(definition)}: {definition}' + f'Expected to find union, intersection, str or dict, instead ' + f'found {type(definition)}: {definition}' ) diff --git a/test/unit/test_selector_errors.py b/test/unit/test_selector_errors.py new file mode 100644 index 00000000000..95c4dbe0344 --- /dev/null +++ b/test/unit/test_selector_errors.py @@ -0,0 +1,71 @@ +from dbt.graph import cli +import dbt.exceptions +import textwrap +import yaml +import unittest +from pprint import pprint +from dbt.config.selectors import ( + selector_config_from_data +) + +from dbt.contracts.selection import SelectorFile + + +def get_selector_dict(txt: str) -> dict: + txt = textwrap.dedent(txt) + dct = yaml.safe_load(txt) + return dct + + + +class SelectorUnitTest(unittest.TestCase): + + def test_parse_multiple_excludes(self): + dct = get_selector_dict('''\ + selectors: + - name: mult_excl + definition: + union: + - method: tag + value: nightly + - exclude: + - method: tag + value: hourly + - exclude: + - method: tag + value: daily + ''') + with self.assertRaises(dbt.exceptions.DbtSelectorsError): + selectors = selector_config_from_data(dct) + + def test_parse_set_op_plus(self): + dct = get_selector_dict('''\ + selectors: + - name: union_plus + definition: + - union: + - method: tag + value: nightly + - exclude: + - method: tag + value: hourly + - method: tag + value: foo + ''') + with self.assertRaises(dbt.exceptions.DbtSelectorsError): + selectors = selector_config_from_data(dct) + + def test_parse_multiple_methods(self): + dct = get_selector_dict('''\ + selectors: + - name: mult_methods + definition: + - tag:hourly + - tag:nightly + - fqn:start + ''') + with self.assertRaises(dbt.exceptions.DbtSelectorsError): + selectors = selector_config_from_data(dct) + + +