Skip to content

Commit 519ea97

Browse files
committed
improve tests and decorator
1 parent 262744e commit 519ea97

File tree

3 files changed

+111
-43
lines changed

3 files changed

+111
-43
lines changed

superset/utils/decorators.py

+46-18
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,10 @@ def wrapped(*args: Any, **kwargs: Any) -> Any:
6262
return decorate
6363

6464

65-
def logs_context(**ctx_kwargs: int | str | UUID | None) -> Callable[..., Any]:
65+
def logs_context(
66+
context_func: Callable[..., dict[Any, Any]] | None = None,
67+
**ctx_kwargs: int | str | UUID | None,
68+
) -> Callable[..., Any]:
6669
"""
6770
Takes arguments and adds them to the global logs_context.
6871
This is for logging purposes only and values should not be relied on or mutated
@@ -72,35 +75,60 @@ def decorate(f: Callable[..., Any]) -> Callable[..., Any]:
7275
def wrapped(*args: Any, **kwargs: Any) -> Any:
7376
if not hasattr(g, "logs_context"):
7477
g.logs_context = {}
78+
79+
# limit data that can be saved to logs_context
80+
# in order to prevent antipatterns
7581
available_logs_context_keys = [
7682
"slice_id",
7783
"dashboard_id",
84+
"dataset_id",
7885
"execution_id",
7986
"report_schedule_id",
8087
]
88+
# set value from kwargs from
89+
# wrapper function if it exists
90+
# e.g. @logs_context()
91+
# def my_func(slice_id=None, **kwargs)
92+
#
93+
# my_func(slice_id=2)
8194
logs_context_data = {
8295
key: val
8396
for key, val in kwargs.items()
8497
if key in available_logs_context_keys
98+
if val is not None
8599
}
86100

87-
# if keys are passed in to decorator directly, add them to logs_context
88-
# by overriding values from kwargs
89-
# e.g. @logs_context(slice_id=1, dashboard_id=1)
90-
for key in available_logs_context_keys:
91-
if ctx_kwargs.get(key) is not None:
92-
logs_context_data[key] = ctx_kwargs[key]
93-
try:
94-
# override value from local kwargs from
95-
# function execution if it exists
96-
# e.g. @logs_context(slice_id=1, dashboard_id=1)
97-
# def my_func(slice_id=None, **kwargs)
98-
#
99-
# my_func(slice_id=2)
100-
logs_context_data[key] = locals()["kwargs"][key]
101-
except KeyError:
102-
# do nothing if the key doesn't exist
103-
pass
101+
try:
102+
# if keys are passed in to decorator directly, add them to logs_context
103+
# by overriding values from kwargs
104+
# e.g. @logs_context(slice_id=1, dashboard_id=1)
105+
logs_context_data.update(
106+
{
107+
key: ctx_kwargs.get(key)
108+
for key in available_logs_context_keys
109+
if ctx_kwargs.get(key) is not None
110+
}
111+
)
112+
113+
if context_func is not None:
114+
# if a context function is passed in, call it and add the
115+
# returned values to logs_context
116+
# context_func=lambda *args, **kwargs: {
117+
# "slice_id": 1, "dashboard_id": 1
118+
# }
119+
logs_context_data.update(
120+
{
121+
key: value
122+
for key, value in context_func(*args, **kwargs).items()
123+
if key in available_logs_context_keys
124+
if value is not None
125+
}
126+
)
127+
128+
except (TypeError, KeyError):
129+
# do nothing if the key doesn't exist
130+
# or context is not callable
131+
pass
104132

105133
g.logs_context.update(logs_context_data)
106134
return f(*args, **kwargs)

tests/unit_tests/notifications/slack_tests.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@
2121
from flask import g
2222

2323

24+
@patch("superset.reports.notifications.slack.g")
2425
@patch("superset.reports.notifications.slack.logger")
2526
def test_send_slack(
2627
logger_mock: MagicMock,
28+
flask_global_mock: MagicMock,
2729
) -> None:
2830
# `superset.models.helpers`, a dependency of following imports,
2931
# requires app context
@@ -32,7 +34,7 @@ def test_send_slack(
3234
from superset.reports.notifications.slack import SlackNotification, WebClient
3335

3436
execution_id = uuid.uuid4()
35-
g.logs_context = {"execution_id": execution_id}
37+
flask_global_mock.logs_context = {"execution_id": execution_id}
3638
content = NotificationContent(
3739
name="test alert",
3840
header_data={
@@ -82,6 +84,3 @@ def test_send_slack(
8284
```
8385
""",
8486
)
85-
86-
# reset g.logs_context
87-
g.logs_context = None

tests/unit_tests/utils/test_decorators.py

+62-21
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
from unittest.mock import call, Mock, patch
2424

2525
import pytest
26-
from flask import g
2726

2827
from superset import app
2928
from superset.utils import decorators
@@ -89,7 +88,10 @@ def my_func(response: ResponseValues, *args: Any, **kwargs: Any) -> str:
8988
mock.assert_called_once_with(expected_result, 1)
9089

9190

92-
def test_context_decorator() -> None:
91+
@patch("superset.utils.decorators.g")
92+
def test_context_decorator(flask_g_mock) -> None:
93+
flask_g_mock.logs_context = {}
94+
9395
@decorators.logs_context()
9496
def myfunc(*args, **kwargs) -> str:
9597
return "test"
@@ -99,46 +101,85 @@ def myfunc(*args, **kwargs) -> str:
99101
def myfunc_with_kwargs(*args, **kwargs) -> str:
100102
return "test"
101103

104+
@decorators.logs_context(bad_context=1)
105+
def myfunc_with_dissallowed_kwargs(*args, **kwargs) -> str:
106+
return "test"
107+
108+
@decorators.logs_context(
109+
context_func=lambda *args, **kwargs: {"slice_id": kwargs["chart_id"]}
110+
)
111+
def myfunc_with_context(*args, **kwargs) -> str:
112+
return "test"
113+
102114
# should not add any data to the global.logs_context scope
103115
myfunc(1, 1)
104-
assert g.logs_context == {}
116+
assert flask_g_mock.logs_context == {}
105117

106118
# should add dashboard_id to the global.logs_context scope
107119
myfunc(1, 1, dashboard_id=1)
108-
assert g.logs_context == {"dashboard_id": 1}
109-
g.logs_context = {}
120+
assert flask_g_mock.logs_context == {"dashboard_id": 1}
121+
# reset logs_context
122+
flask_g_mock.logs_context = {}
110123

111124
# should add slice_id to the global.logs_context scope
112125
myfunc(1, 1, slice_id=1)
113-
assert g.logs_context == {"slice_id": 1}
114-
g.logs_context = {}
126+
assert flask_g_mock.logs_context == {"slice_id": 1}
127+
# reset logs_context
128+
flask_g_mock.logs_context = {}
115129

116130
# should add execution_id to the global.logs_context scope
117131
myfunc(1, 1, execution_id=1)
118-
assert g.logs_context == {"execution_id": 1}
119-
g.logs_context = {}
132+
assert flask_g_mock.logs_context == {"execution_id": 1}
133+
# reset logs_context
134+
flask_g_mock.logs_context = {}
120135

121136
# should add all three to the global.logs_context scope
122137
myfunc(1, 1, dashboard_id=1, slice_id=1, execution_id=1)
123-
assert g.logs_context == {"dashboard_id": 1, "slice_id": 1, "execution_id": 1}
124-
g.logs_context = {}
138+
assert flask_g_mock.logs_context == {
139+
"dashboard_id": 1,
140+
"slice_id": 1,
141+
"execution_id": 1,
142+
}
143+
# reset logs_context
144+
flask_g_mock.logs_context = {}
125145

126146
# should overwrite existing values in the global.logs_context scope
127-
g.logs_context = {"dashboard_id": 2, "slice_id": 2, "execution_id": 2}
147+
flask_g_mock.logs_context = {"dashboard_id": 2, "slice_id": 2, "execution_id": 2}
128148
myfunc(1, 1, dashboard_id=3, slice_id=3, execution_id=3)
129-
assert g.logs_context == {"dashboard_id": 3, "slice_id": 3, "execution_id": 3}
130-
g.logs_context = {}
149+
assert flask_g_mock.logs_context == {
150+
"dashboard_id": 3,
151+
"slice_id": 3,
152+
"execution_id": 3,
153+
}
154+
# reset logs_context
155+
flask_g_mock.logs_context = {}
156+
157+
# should not add a value that does not exist in the global.logs_context scope
158+
myfunc_with_dissallowed_kwargs()
159+
assert flask_g_mock.logs_context == {}
160+
# reset logs_context
161+
flask_g_mock.logs_context = {}
131162

132163
# should be able to add values to the decorator function directly
133164
myfunc_with_kwargs()
134-
assert g.logs_context["dashboard_id"] == 1
135-
assert g.logs_context["slice_id"] == 1
136-
assert isinstance(g.logs_context["execution_id"], uuid.UUID)
137-
g.logs_context = {}
165+
assert flask_g_mock.logs_context["dashboard_id"] == 1
166+
assert flask_g_mock.logs_context["slice_id"] == 1
167+
assert isinstance(flask_g_mock.logs_context["execution_id"], uuid.UUID)
168+
# reset logs_context
169+
flask_g_mock.logs_context = {}
138170

139171
# should be able to add values to the decorator function directly
140-
# and overwrite with any kwargs passed in
172+
# and it will overwrite any kwargs passed into the decorated function
141173
myfunc_with_kwargs(execution_id=4)
142174

143-
assert g.logs_context == {"dashboard_id": 1, "slice_id": 1, "execution_id": 4}
144-
g.logs_context = {}
175+
assert flask_g_mock.logs_context["dashboard_id"] == 1
176+
assert flask_g_mock.logs_context["slice_id"] == 1
177+
assert isinstance(flask_g_mock.logs_context["execution_id"], uuid.UUID)
178+
# reset logs_context
179+
flask_g_mock.logs_context = {}
180+
181+
# should be able to pass a callable context to the decorator
182+
myfunc_with_context(chart_id=1)
183+
assert flask_g_mock.logs_context == {"slice_id": 1}
184+
# reset logs_context
185+
flask_g_mock.logs_context = {}

0 commit comments

Comments
 (0)