Compare commits

...

5 Commits

Author SHA1 Message Date
Quigley Malcolm
19a5a62ebb Add model specific configs to model yml description in happy path test 2025-05-02 12:30:45 -05:00
Quigley Malcolm
aa15c2d607 Begin testing that happy_path_project and project fixtures have no deprecations 2025-05-02 12:30:42 -05:00
Quigley Malcolm
739b48c786 Update handling additionalProperties violations to handle multiple keys in violation 2025-05-02 12:15:51 -05:00
Quigley Malcolm
6739349836 Refactor key parsing from jsonschema ValidationError messages to single definition 2025-05-02 12:15:51 -05:00
Quigley Malcolm
444aa9042c Fix detection of additional config property deprecation
Previously we were taking the first `key` on the `instance` property
of the jsonschema ValidationError. However, this validation error
is raised as an "anyOf" violation, which then has sub-errors in its
`context` property. To identify the key in violation, we have to
find the `additionalProperties` validation in the sub-errors. The key
that is an issue can then be parsed from that sub-error.
2025-05-02 12:15:50 -05:00
4 changed files with 110 additions and 21 deletions

View File

@@ -3,7 +3,7 @@ import os
import re
from datetime import date, datetime
from pathlib import Path
from typing import Any, Dict, Iterator
from typing import Any, Dict, Iterator, List
import jsonschema
from jsonschema import ValidationError
@@ -57,6 +57,11 @@ def error_path_to_string(error: jsonschema.ValidationError) -> str:
return path
def _additional_properties_violation_keys(error: ValidationError) -> List[str]:
found_keys = re.findall(r"'\S+'", error.message)
return [key.strip("'") for key in found_keys]
def jsonschema_validate(schema: Dict[str, Any], json: Dict[str, Any], file_path: str) -> None:
if not os.environ.get("DBT_ENV_PRIVATE_RUN_JSONSCHEMA_VALIDATIONS"):
@@ -69,20 +74,23 @@ def jsonschema_validate(schema: Dict[str, Any], json: Dict[str, Any], file_path:
# Listify the error path to make it easier to work with (it's a deque in the ValidationError object)
error_path = list(error.path)
if error.validator == "additionalProperties":
key = re.search(r"'\S+'", error.message)
keys = _additional_properties_violation_keys(error)
if len(error.path) == 0:
deprecations.warn(
"custom-top-level-key-deprecation",
msg="Unexpected top-level key" + (" " + key.group() if key else ""),
file=file_path,
)
for key in keys:
deprecations.warn(
"custom-top-level-key-deprecation",
msg="Unexpected top-level key" + (" " + key if key else ""),
file=file_path,
)
else:
deprecations.warn(
"custom-key-in-object-deprecation",
key=key.group() if key else "",
file=file_path,
key_path=error_path_to_string(error),
)
key_path = error_path_to_string(error)
for key in keys:
deprecations.warn(
"custom-key-in-object-deprecation",
key=key,
file=file_path,
key_path=key_path,
)
elif (
error.validator == "anyOf"
and len(error_path) > 0
@@ -90,12 +98,20 @@ def jsonschema_validate(schema: Dict[str, Any], json: Dict[str, Any], file_path:
and isinstance(error.instance, dict)
and len(error.instance.keys()) > 0
):
deprecations.warn(
"custom-key-in-config-deprecation",
key=(list(error.instance.keys()))[0],
file=file_path,
key_path=error_path_to_string(error),
)
for sub_error in error.context or []:
if (
isinstance(sub_error, ValidationError)
and sub_error.validator == "additionalProperties"
):
keys = _additional_properties_violation_keys(sub_error)
key_path = error_path_to_string(error)
for key in keys:
deprecations.warn(
"custom-key-in-config-deprecation",
key=key,
file=file_path,
key_path=key_path,
)
else:
deprecations.warn(
"generic-json-schema-validation-deprecation",

View File

@@ -168,6 +168,16 @@ models:
my_custom_key: "my_custom_value"
"""
multiple_custom_keys_in_config_yaml = """
models:
- name: models_trivial
description: "This is a test model"
deprecation_date: 1999-01-01 00:00:00.00+00:00
config:
my_custom_key: "my_custom_value"
my_custom_key2: "my_custom_value2"
"""
custom_key_in_object_yaml = """
models:
- name: models_trivial

View File

@@ -26,6 +26,7 @@ from tests.functional.deprecations.fixtures import (
duplicate_keys_yaml,
invalid_deprecation_date_yaml,
models_trivial__model_sql,
multiple_custom_keys_in_config_yaml,
)
from tests.utils import EventCatcher
@@ -341,7 +342,10 @@ class TestCustomKeyInConfigDeprecation:
@mock.patch.dict(os.environ, {"DBT_ENV_PRIVATE_RUN_JSONSCHEMA_VALIDATIONS": "True"})
def test_duplicate_yaml_keys_in_schema_files(self, project):
event_catcher = EventCatcher(CustomKeyInConfigDeprecation)
run_dbt(["parse", "--no-partial-parse"], callbacks=[event_catcher.catch])
run_dbt(
["parse", "--no-partial-parse", "--show-all-deprecations"],
callbacks=[event_catcher.catch],
)
assert len(event_catcher.caught_events) == 1
assert (
"Custom key `my_custom_key` found in `config` at path `models[0].config`"
@@ -349,6 +353,32 @@ class TestCustomKeyInConfigDeprecation:
)
class TestMultipleCustomKeysInConfigDeprecation:
@pytest.fixture(scope="class")
def models(self):
return {
"models_trivial.sql": models_trivial__model_sql,
"models.yml": multiple_custom_keys_in_config_yaml,
}
@mock.patch.dict(os.environ, {"DBT_ENV_PRIVATE_RUN_JSONSCHEMA_VALIDATIONS": "True"})
def test_duplicate_yaml_keys_in_schema_files(self, project):
event_catcher = EventCatcher(CustomKeyInConfigDeprecation)
run_dbt(
["parse", "--no-partial-parse", "--show-all-deprecations"],
callbacks=[event_catcher.catch],
)
assert len(event_catcher.caught_events) == 2
assert (
"Custom key `my_custom_key` found in `config` at path `models[0].config`"
in event_catcher.caught_events[0].info.msg
)
assert (
"Custom key `my_custom_key2` found in `config` at path `models[0].config`"
in event_catcher.caught_events[1].info.msg
)
class TestCustomKeyInObjectDeprecation:
@pytest.fixture(scope="class")
def models(self):
@@ -363,7 +393,7 @@ class TestCustomKeyInObjectDeprecation:
run_dbt(["parse", "--no-partial-parse"], callbacks=[event_catcher.catch])
assert len(event_catcher.caught_events) == 1
assert (
"Custom key `'my_custom_property'` found at `models[0]` in file"
"Custom key `my_custom_property` found at `models[0]` in file"
in event_catcher.caught_events[0].info.msg
)
@@ -380,3 +410,25 @@ class TestJsonschemaValidationDeprecationsArentRunWithoutEnvVar:
event_catcher = EventCatcher(CustomKeyInObjectDeprecation)
run_dbt(["parse", "--no-partial-parse"], callbacks=[event_catcher.catch])
assert len(event_catcher.caught_events) == 0
class TestHappyPathProjectHasNoDeprecations:
@mock.patch.dict(os.environ, {"DBT_ENV_PRIVATE_RUN_JSONSCHEMA_VALIDATIONS": "True"})
def test_happy_path_project_has_no_deprecations(self, happy_path_project):
event_cathcer = EventCatcher(DeprecationsSummary)
run_dbt(
["parse", "--no-partial-parse", "--show-all-deprecations"],
callbacks=[event_cathcer.catch],
)
assert len(event_cathcer.caught_events) == 0
class TestBaseProjectHasNoDeprecations:
@mock.patch.dict(os.environ, {"DBT_ENV_PRIVATE_RUN_JSONSCHEMA_VALIDATIONS": "True"})
def test_base_project_has_no_deprecations(self, project):
event_cathcer = EventCatcher(DeprecationsSummary)
run_dbt(
["parse", "--no-partial-parse", "--show-all-deprecations"],
callbacks=[event_cathcer.catch],
)
assert len(event_cathcer.caught_events) == 0

View File

@@ -8,8 +8,19 @@ models:
data_tests:
- unique
- not_null
config:
materialized: table
sql_header: "SELECT 1 as header;"
on_configuration_change: apply
unique_key: id
batch_size: day
begin: "2020-01-01"
lookback: 5
concurrent_batches: false
- name: metricflow_time_spine
description: Day time spine
time_spine:
standard_granularity_column: date_day
columns:
- name: date_day
granularity: day