Skip to content

Commit

Permalink
Update warn_error_options in Project flags to support error and `…
Browse files Browse the repository at this point in the history
…warn` options

As part of this I refactored `exclusive_primary_alt_value_setting` into an upstream
location `/config/utils`. That is because importing it in `/config/project.py` from
`cli/option_types.py` caused some circular dependency issues.
  • Loading branch information
QMalcolm committed Apr 26, 2024
1 parent ebf9106 commit 3eb4e0c
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 28 deletions.
27 changes: 2 additions & 25 deletions core/dbt/cli/option_types.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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."""

Expand Down
14 changes: 12 additions & 2 deletions core/dbt/config/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down
24 changes: 23 additions & 1 deletion core/dbt/config/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand All @@ -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
28 changes: 28 additions & 0 deletions tests/functional/configs/test_warn_error_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

0 comments on commit 3eb4e0c

Please sign in to comment.