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..96b40bf0b7a 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(definition) 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: @@ -182,6 +184,13 @@ def parse_union_definition(definition: Dict[str, Any]) -> SelectionSpec: union = SelectionUnion(components=include) + if 'method' in definition: + yaml_sel_cfg = yaml.dump(definition) + raise ValidationException( + f"You cannot have both method and union keys in a root level " + f"selector definition:\n{yaml_sel_cfg}" + ) + if exclude is None: union.raw = definition return union @@ -198,6 +207,14 @@ def parse_intersection_definition( intersection_def_parts = _get_list_dicts(definition, 'intersection') include, exclude = _parse_include_exclude_subdefs(intersection_def_parts) intersection = SelectionIntersection(components=include) + + if 'method' in definition: + yaml_sel_cfg = yaml.dump(definition) + raise ValidationException( + f"You cannot have both method and intersection keys in a " + f"root level selector definition:\n{yaml_sel_cfg}" + ) + if exclude is None: intersection.raw = definition return intersection @@ -210,7 +227,6 @@ def parse_intersection_definition( def parse_dict_definition(definition: Dict[str, Any]) -> SelectionSpec: diff_arg: Optional[SelectionSpec] = None - if len(definition) == 1: key = list(definition)[0] value = definition[key] @@ -251,10 +267,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..cec9fe6c2a9 --- /dev/null +++ b/test/unit/test_selector_errors.py @@ -0,0 +1,88 @@ +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) + + def test_parse_set_with_method(self): + dct = get_selector_dict('''\ + selectors: + - name: mixed_syntaxes + definition: + key: value + method: tag + value: foo + union: + - method: tag + value: m1234 + - exclude: + - method: tag + value: m5678 + ''') + with self.assertRaises(dbt.exceptions.DbtSelectorsError): + selectors = selector_config_from_data(dct) + +