diff --git a/core/dbt/cli/option_types.py b/core/dbt/cli/option_types.py index b95fb3b9d64..ef04e348260 100644 --- a/core/dbt/cli/option_types.py +++ b/core/dbt/cli/option_types.py @@ -1,11 +1,10 @@ from click import ParamType, Choice -from dbt.config.utils import parse_cli_yaml_string +from dbt.config.utils import parse_cli_yaml_string, exclusive_primary_alt_value_setting from dbt.events import ALL_EVENT_NAMES from dbt.exceptions import ValidationError, OptionNotYamlDictError -from dbt_common.exceptions import DbtConfigError, DbtValidationError +from dbt_common.exceptions import DbtValidationError from dbt_common.helper_types import WarnErrorOptions -from typing import Any, Dict class YAML(ParamType): @@ -44,28 +43,6 @@ def convert(self, value, param, ctx): return {"name": value, "version": None} -def exclusive_primary_alt_value_setting( - dictionary: Dict[str, Any], primary: str, alt: str -) -> None: - """Munges in place under the primary the options for the primary and alt values - - Sometimes we allow setting something via TWO keys, but not at the same time. If both the primary - key and alt key have values, an error gets raised. If the alt key has values, then we update - the dictionary to ensure the primary key contains the values. If neither are set, nothing happens. - """ - - primary_options = dictionary.get(primary) - alt_options = dictionary.get(alt) - - if primary_options and alt_options: - raise DbtConfigError( - f"Only `{alt}` or `{primary}` can be specified in `warn_error_options`, not both" - ) - - if alt_options: - dictionary[primary] = alt_options - - class WarnErrorOptionsType(YAML): """The Click WarnErrorOptions type. Converts YAML strings into objects.""" diff --git a/core/dbt/config/project.py b/core/dbt/config/project.py index 7cdbebf0328..42bab656dca 100644 --- a/core/dbt/config/project.py +++ b/core/dbt/config/project.py @@ -26,6 +26,7 @@ from dbt.clients.yaml_helper import load_yaml_text from dbt.adapters.contracts.connection import QueryComment from dbt.exceptions import ( + DbtConfigError, DbtProjectError, ProjectContractBrokenError, ProjectContractError, @@ -39,6 +40,7 @@ from dbt.utils import MultiDict, md5, coerce_dict_str from dbt.node_types import NodeType from dbt.config.selectors import SelectorDict +from dbt.config.utils import exclusive_primary_alt_value_setting from dbt.contracts.project import ( Project as ProjectContract, SemverString, @@ -835,10 +837,18 @@ def read_project_flags(project_dir: str, profiles_dir: str) -> ProjectFlags: project_flags = profile_project_flags if project_flags is not None: + # if warn_error_options are set, handle collapsing `include` and `error` as well as + # collapsing `exclude` and `warn` + warn_error_options = project_flags.get("warn_error_options") + if warn_error_options: + exclusive_primary_alt_value_setting(warn_error_options, "include", "error") + exclusive_primary_alt_value_setting(warn_error_options, "exclude", "warn") + ProjectFlags.validate(project_flags) return ProjectFlags.from_dict(project_flags) - except (DbtProjectError) as exc: - # We don't want to eat the DbtProjectError for UserConfig to ProjectFlags + except (DbtProjectError, DbtConfigError) as exc: + # We don't want to eat the DbtProjectError for UserConfig to ProjectFlags or + # DbtConfigError for warn_error_options munging raise exc except (DbtRuntimeError, ValidationError): pass diff --git a/core/dbt/config/utils.py b/core/dbt/config/utils.py index bb5546cc743..daeac6bed40 100644 --- a/core/dbt/config/utils.py +++ b/core/dbt/config/utils.py @@ -5,7 +5,7 @@ from dbt_common.events.functions import fire_event from dbt.events.types import InvalidOptionYAML from dbt.exceptions import OptionNotYamlDictError -from dbt_common.exceptions import DbtValidationError +from dbt_common.exceptions import DbtConfigError, DbtValidationError def parse_cli_vars(var_string: str) -> Dict[str, Any]: @@ -23,3 +23,25 @@ def parse_cli_yaml_string(var_string: str, cli_option_name: str) -> Dict[str, An except (DbtValidationError, OptionNotYamlDictError): fire_event(InvalidOptionYAML(option_name=cli_option_name)) raise + + +def exclusive_primary_alt_value_setting( + dictionary: Dict[str, Any], primary: str, alt: str +) -> None: + """Munges in place under the primary the options for the primary and alt values + + Sometimes we allow setting something via TWO keys, but not at the same time. If both the primary + key and alt key have values, an error gets raised. If the alt key has values, then we update + the dictionary to ensure the primary key contains the values. If neither are set, nothing happens. + """ + + primary_options = dictionary.get(primary) + alt_options = dictionary.get(alt) + + if primary_options and alt_options: + raise DbtConfigError( + f"Only `{alt}` or `{primary}` can be specified in `warn_error_options`, not both" + ) + + if alt_options: + dictionary[primary] = alt_options diff --git a/tests/functional/configs/test_warn_error_options.py b/tests/functional/configs/test_warn_error_options.py index 3ab15974f72..2363fc469fd 100644 --- a/tests/functional/configs/test_warn_error_options.py +++ b/tests/functional/configs/test_warn_error_options.py @@ -175,3 +175,31 @@ def test_can_exclude_specific_event( catcher.flush() result = runner.invoke(["run"]) assert_deprecation_warning(result, catcher) + + def test_cant_set_both_include_and_error( + self, project, clear_project_flags, project_root, runner: dbtRunner + ) -> None: + exclude_options = {"flags": {"warn_error_options": {"include": "all", "error": "all"}}} + update_config_file(exclude_options, project_root, "dbt_project.yml") + result = runner.invoke(["run"]) + assert not result.success + assert result.exception is not None + assert "Only `error` or `include` can be specified" in str(result.exception) + + def test_cant_set_both_exclude_and_warn( + self, project, clear_project_flags, project_root, runner: dbtRunner + ) -> None: + exclude_options = { + "flags": { + "warn_error_options": { + "include": "all", + "exclude": ["DeprecatedModel"], + "warn": ["DeprecatedModel"], + } + } + } + update_config_file(exclude_options, project_root, "dbt_project.yml") + result = runner.invoke(["run"]) + assert not result.success + assert result.exception is not None + assert "Only `warn` or `exclude` can be specified" in str(result.exception)