Skip to content

Commit e2e86b7

Browse files
authored
General Deprecation Warning Improvements (#11466)
1 parent 6b747fe commit e2e86b7

18 files changed

+915
-712
lines changed

Diff for: .changes/unreleased/Features-20250403-174659.yaml

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
kind: Features
2+
body: Show summaries for deprecations and add ability to toggle seeing all deprecation
3+
violation instances
4+
time: 2025-04-03T17:46:59.684525-05:00
5+
custom:
6+
Author: QMalcolm
7+
Issue: "11429"

Diff for: core/dbt/cli/main.py

+1
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ def global_flags(func):
129129
@p.record_timing_info
130130
@p.send_anonymous_usage_stats
131131
@p.single_threaded
132+
@p.show_all_deprecations
132133
@p.state
133134
@p.static_parser
134135
@p.target

Diff for: core/dbt/cli/params.py

+8
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,14 @@
595595
hidden=True,
596596
)
597597

598+
show_all_deprecations = click.option(
599+
"--show-all-deprecations/--no-show-all-deprecations",
600+
envvar=None,
601+
help="By default, each type of a deprecation warning is only shown once. Use this flag to show all deprecation warning instances.",
602+
is_flag=True,
603+
default=False,
604+
)
605+
598606
skip_profile_setup = click.option(
599607
"--skip-profile-setup",
600608
"-s",

Diff for: core/dbt/cli/requires.py

+3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from dbt.config.runtime import UnsetProfile, load_profile, load_project
1717
from dbt.context.providers import generate_runtime_macro_context
1818
from dbt.context.query_header import generate_query_header_context
19+
from dbt.deprecations import show_all_deprecation_summaries
1920
from dbt.events.logging import setup_event_logger
2021
from dbt.events.types import (
2122
ArtifactUploadError,
@@ -176,6 +177,8 @@ def wrapper(*args, **kwargs):
176177
except Exception as e:
177178
fire_event(ArtifactUploadError(msg=str(e)))
178179

180+
show_all_deprecation_summaries()
181+
179182
if importlib.util.find_spec("resource") is not None:
180183
import resource
181184

Diff for: core/dbt/clients/registry.py

+22-22
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import functools
22
import os
3-
from typing import Any, Dict, List
3+
from typing import Any, Dict, List, Optional
44

55
import requests
66

@@ -34,9 +34,26 @@ def _get_url(name, registry_base_url=None):
3434
return "{}{}".format(registry_base_url, url)
3535

3636

37-
def _get_with_retries(package_name, registry_base_url=None):
37+
def _check_package_redirect(package_name: str, response: Dict[str, Any]) -> None:
38+
# Either redirectnamespace or redirectname in the JSON response indicate a redirect
39+
# redirectnamespace redirects based on package ownership
40+
# redirectname redirects based on package name
41+
# Both can be present at the same time, or neither. Fails gracefully to old name
42+
if ("redirectnamespace" in response) or ("redirectname" in response):
43+
use_namespace = response.get("redirectnamespace") or response["namespace"]
44+
use_name = response.get("redirectname") or response["name"]
45+
46+
new_nwo = f"{use_namespace}/{use_name}"
47+
deprecations.warn("package-redirect", old_name=package_name, new_name=new_nwo)
48+
49+
50+
def _get_package_with_retries(
51+
package_name: str, registry_base_url: Optional[str] = None
52+
) -> Dict[str, Any]:
3853
get_fn = functools.partial(_get, package_name, registry_base_url)
39-
return connection_exception_retry(get_fn, 5)
54+
response: Dict[str, Any] = connection_exception_retry(get_fn, 5)
55+
_check_package_redirect(package_name, response)
56+
return response
4057

4158

4259
def _get(package_name, registry_base_url=None):
@@ -48,7 +65,7 @@ def _get(package_name, registry_base_url=None):
4865
resp.raise_for_status()
4966

5067
# The response should always be a dictionary. Anything else is unexpected, raise error.
51-
# Raising this error will cause this function to retry (if called within _get_with_retries)
68+
# Raising this error will cause this function to retry (if called within _get_package_with_retries)
5269
# and hopefully get a valid response. This seems to happen when there's an issue with the Hub.
5370
# Since we control what we expect the HUB to return, this is safe.
5471
# See https://github.com/dbt-labs/dbt-core/issues/4577
@@ -96,29 +113,12 @@ def _get(package_name, registry_base_url=None):
96113
return response
97114

98115

99-
_get_cached = memoized(_get_with_retries)
116+
_get_cached = memoized(_get_package_with_retries)
100117

101118

102119
def package(package_name, registry_base_url=None) -> Dict[str, Any]:
103120
# returns a dictionary of metadata for all versions of a package
104121
response = _get_cached(package_name, registry_base_url)
105-
# Either redirectnamespace or redirectname in the JSON response indicate a redirect
106-
# redirectnamespace redirects based on package ownership
107-
# redirectname redirects based on package name
108-
# Both can be present at the same time, or neither. Fails gracefully to old name
109-
if ("redirectnamespace" in response) or ("redirectname" in response):
110-
if ("redirectnamespace" in response) and response["redirectnamespace"] is not None:
111-
use_namespace = response["redirectnamespace"]
112-
else:
113-
use_namespace = response["namespace"]
114-
115-
if ("redirectname" in response) and response["redirectname"] is not None:
116-
use_name = response["redirectname"]
117-
else:
118-
use_name = response["name"]
119-
120-
new_nwo = use_namespace + "/" + use_name
121-
deprecations.warn("package-redirect", old_name=package_name, new_name=new_nwo)
122122
return response["versions"]
123123

124124

Diff for: core/dbt/deprecations.py

+41-4
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import abc
2-
from typing import Callable, ClassVar, Dict, List, Optional, Set
2+
from collections import defaultdict
3+
from typing import Callable, ClassVar, DefaultDict, Dict, List, Optional
34

45
import dbt.tracking
56
from dbt.events import types as core_types
7+
from dbt.flags import get_flags
8+
from dbt_common.events.base_types import BaseEvent
69
from dbt_common.events.functions import warn_or_error
710

811

912
class DBTDeprecation:
1013
_name: ClassVar[Optional[str]] = None
1114
_event: ClassVar[Optional[str]] = None
15+
_summary_event: ClassVar[Optional[str]] = None
1216

1317
@property
1418
def name(self) -> str:
@@ -33,17 +37,45 @@ def event(self) -> abc.ABCMeta:
3337
raise NameError(msg)
3438
raise NotImplementedError("event not implemented for {}".format(self._event))
3539

40+
@property
41+
def summary_event(self) -> Optional[abc.ABCMeta]:
42+
if self._summary_event is None:
43+
return None
44+
else:
45+
module_path = core_types
46+
class_name = self._summary_event
47+
48+
try:
49+
return getattr(module_path, class_name)
50+
except AttributeError:
51+
msg = f"Event Class `{class_name}` is not defined in `{module_path}`"
52+
raise NameError(msg)
53+
3654
def show(self, *args, **kwargs) -> None:
37-
if self.name not in active_deprecations:
55+
flags = get_flags()
56+
if self.name not in active_deprecations or flags.show_all_deprecations:
3857
event = self.event(**kwargs)
3958
warn_or_error(event)
4059
self.track_deprecation_warn()
41-
active_deprecations.add(self.name)
60+
61+
active_deprecations[self.name] += 1
62+
63+
def show_summary(self) -> None:
64+
event_class = self.summary_event
65+
if self.name in active_deprecations and event_class is not None:
66+
show_all_hint = (
67+
not get_flags().show_all_deprecations and active_deprecations[self.name] > 1
68+
)
69+
event: BaseEvent = event_class(
70+
occurrences=active_deprecations[self.name], show_all_hint=show_all_hint
71+
)
72+
warn_or_error(event)
4273

4374

4475
class PackageRedirectDeprecation(DBTDeprecation):
4576
_name = "package-redirect"
4677
_event = "PackageRedirectDeprecation"
78+
_summary_event = "PackageRedirectDeprecationSummary"
4779

4880

4981
class PackageInstallPathDeprecation(DBTDeprecation):
@@ -163,10 +195,15 @@ def show_callback():
163195
buffered_deprecations.append(show_callback)
164196

165197

198+
def show_all_deprecation_summaries() -> None:
199+
for deprecation in active_deprecations:
200+
deprecations[deprecation].show_summary()
201+
202+
166203
# these are globally available
167204
# since modules are only imported once, active_deprecations is a singleton
168205

169-
active_deprecations: Set[str] = set()
206+
active_deprecations: DefaultDict[str, int] = defaultdict(int)
170207

171208
deprecations_list: List[DBTDeprecation] = [
172209
PackageRedirectDeprecation(),

Diff for: core/dbt/events/core_types.proto

+11
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,17 @@ message MicrobatchMacroOutsideOfBatchesDeprecationMsg {
470470
MicrobatchMacroOutsideOfBatchesDeprecation data = 2;
471471
}
472472

473+
// D021
474+
message PackageRedirectDeprecationSummary {
475+
int32 occurrences = 1;
476+
bool show_all_hint = 2;
477+
}
478+
479+
message PackageRedirectDeprecationSummaryMsg {
480+
CoreEventInfo info = 1;
481+
PackageRedirectDeprecationSummary data = 2;
482+
}
483+
473484
// I065
474485
message DeprecatedModel {
475486
string model_name = 1;

Diff for: core/dbt/events/core_types_pb2.py

+640-626
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: core/dbt/events/types.py

+16
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,22 @@ def message(self) -> str:
256256
return line_wrap_message(warning_tag(f"Deprecated functionality\n\n{description}"))
257257

258258

259+
class PackageRedirectDeprecationSummary(WarnLevel):
260+
def code(self) -> str:
261+
return "D021"
262+
263+
def message(self) -> str:
264+
description = (
265+
f"Found {pluralize(self.occurrences, 'package')} that {'has' if self.occurrences == 1 else 'have'} been deprecated in favor of new packages. Please "
266+
f"update your `packages.yml` configuration to use the new packages instead."
267+
)
268+
269+
if self.show_all_hint:
270+
description += " To see all deprecated packages, run command again with the `--show-all-deprecations` flag."
271+
272+
return line_wrap_message(warning_tag(f"Deprecated functionality\n\n{description}"))
273+
274+
259275
class PackageInstallPathDeprecation(WarnLevel):
260276
def code(self) -> str:
261277
return "D002"

Diff for: tests/functional/conftest.py

+14
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,18 @@
1+
import pytest
2+
13
from tests.functional.fixtures.happy_path_fixture import ( # noqa:D
24
happy_path_project,
35
happy_path_project_files,
46
)
7+
8+
9+
@pytest.fixture(scope="function", autouse=True)
10+
def clear_memoized_get_package_with_retries():
11+
# This fixture is used to clear the memoized cache for _get_package_with_retries
12+
# in dbt.clients.registry. This is necessary because the cache is shared across
13+
# tests and can cause unexpected behavior if not cleared as some tests depend on
14+
# the deprecation warning that _get_package_with_retries fires
15+
yield
16+
from dbt.clients.registry import _get_cached
17+
18+
_get_cached.cache = {}

Diff for: tests/functional/deprecations/test_config_deprecations.py

+10-11
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from collections import defaultdict
2+
13
import pytest
24

35
from dbt import deprecations
@@ -30,14 +32,13 @@ def project_config_update(self, unique_schema):
3032

3133
def test_project_tests_config(self, project):
3234
deprecations.reset_deprecations()
33-
assert deprecations.active_deprecations == set()
35+
assert deprecations.active_deprecations == defaultdict(int)
3436
run_dbt(["parse"])
35-
expected = set()
36-
assert expected == deprecations.active_deprecations
37+
assert deprecations.active_deprecations == defaultdict(int)
3738

3839
def test_project_tests_config_fail(self, project):
3940
deprecations.reset_deprecations()
40-
assert deprecations.active_deprecations == set()
41+
assert deprecations.active_deprecations == defaultdict(int)
4142
with pytest.raises(CompilationError) as exc:
4243
run_dbt(["--warn-error", "--no-partial-parse", "parse"])
4344
exc_str = " ".join(str(exc.value).split()) # flatten all whitespace
@@ -60,14 +61,13 @@ def macros(self):
6061

6162
def test_generic_tests_config(self, project):
6263
deprecations.reset_deprecations()
63-
assert deprecations.active_deprecations == set()
64+
assert deprecations.active_deprecations == defaultdict(int)
6465
run_dbt(["parse"])
65-
expected = set()
66-
assert expected == deprecations.active_deprecations
66+
assert deprecations.active_deprecations == defaultdict(int)
6767

6868
def test_generic_tests_fail(self, project):
6969
deprecations.reset_deprecations()
70-
assert deprecations.active_deprecations == set()
70+
assert deprecations.active_deprecations == defaultdict(int)
7171
run_dbt(["--warn-error", "--no-partial-parse", "parse"])
7272

7373
def test_generic_data_test_parsing(self, project):
@@ -92,10 +92,9 @@ def seeds(self):
9292

9393
def test_source_tests_config(self, project):
9494
deprecations.reset_deprecations()
95-
assert deprecations.active_deprecations == set()
95+
assert deprecations.active_deprecations == defaultdict(int)
9696
run_dbt(["parse"])
97-
expected = set()
98-
assert expected == deprecations.active_deprecations
97+
assert deprecations.active_deprecations == defaultdict(int)
9998

10099
def test_generic_data_tests(self, project):
101100
run_dbt(["seed"])

0 commit comments

Comments
 (0)