Skip to content

Commit 660bd4d

Browse files
authored
Supporting more use cases for the log_context decorator (#5866)
1 parent 41cfd39 commit 660bd4d

File tree

3 files changed

+145
-2
lines changed

3 files changed

+145
-2
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Changes can also be flagged with a GitHub label for tracking purposes. The URL o
3939
- Moved non-prod Admin UI dependencies to devDependencies [#5832](https://github.com/ethyca/fides/pull/5832)
4040
- Prevent Admin UI and Privacy Center from starting when running `nox -s dev` with datastore params [#5843](https://github.com/ethyca/fides/pull/5843)
4141
- Remove plotly (unused package) to reduce fides image size [#5852](https://github.com/ethyca/fides/pull/5852)
42+
- Fixed issue where the log_context decorator didn't support positional arguments [#5866](https://github.com/ethyca/fides/pull/5866)
4243

4344
### Fixed
4445
- Fixed pagination bugs on some tables [#5819](https://github.com/ethyca/fides/pull/5819)

src/fides/api/util/logger_context_utils.py

+36-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import inspect
12
from abc import abstractmethod
23
from enum import Enum
34
from functools import wraps
@@ -81,12 +82,46 @@ def decorator(func: Callable) -> Callable:
8182
def wrapper(*args: Any, **kwargs: Any) -> Any:
8283
context = dict(additional_context)
8384

84-
# extract specified param values from kwargs
85+
# extract specified param values from kwargs and args
8586
if capture_args:
87+
# First, process kwargs as they're explicitly named
8688
for arg_name, context_name in capture_args.items():
8789
if arg_name in kwargs:
8890
context[context_name.value] = kwargs[arg_name]
8991

92+
# Process args using signature binding for more robust parameter mapping
93+
if args:
94+
try:
95+
# Get the signature and bind the arguments
96+
sig = inspect.signature(func)
97+
# This will map positional args to their parameter names correctly
98+
bound_args = sig.bind_partial(*args, **kwargs)
99+
100+
# Now we can iterate through the bound arguments
101+
for param_name, arg_value in bound_args.arguments.items():
102+
# Only process if this parameter is in capture_args and wasn't already found in kwargs
103+
if param_name in capture_args and param_name not in kwargs:
104+
context_name = capture_args[param_name]
105+
context[context_name.value] = arg_value
106+
except TypeError:
107+
# Handle the case where the arguments don't match the signature
108+
pass
109+
110+
# Handle default parameters that weren't provided in args or kwargs
111+
if capture_args:
112+
sig = inspect.signature(func)
113+
for param_name, param in sig.parameters.items():
114+
# Check if parameter has a default value and is in capture_args
115+
# and hasn't been processed yet (not in context)
116+
if (
117+
param.default is not param.empty
118+
and param_name in capture_args
119+
and capture_args[param_name].value not in context
120+
):
121+
context_name = capture_args[param_name]
122+
context[context_name.value] = param.default
123+
124+
# Process Contextualizable args
90125
for arg in args:
91126
if isinstance(arg, Contextualizable):
92127
arg_context = arg.get_log_context()

tests/ops/util/test_logger_context_utils.py

+108-1
Original file line numberDiff line numberDiff line change
@@ -161,12 +161,119 @@ def func(other_param: str, task_id: str):
161161
logger.info("processing")
162162
return other_param, task_id
163163

164-
func("something", task_id="abc123")
164+
func("something", "abc123")
165165

166166
assert loguru_caplog.records[0].extra == {
167167
LoggerContextKeys.task_id.value: "abc123"
168168
}
169169

170+
def test_log_context_with_multiple_positional_captured_args(self, loguru_caplog):
171+
"""Test that multiple captured args work with positional arguments"""
172+
173+
@log_context(
174+
capture_args={
175+
"task_id": LoggerContextKeys.task_id,
176+
"request_id": LoggerContextKeys.privacy_request_id,
177+
}
178+
)
179+
def func(other_param: str, task_id: str, request_id: str):
180+
logger.info("processing")
181+
return other_param, task_id, request_id
182+
183+
# Pass all arguments as positional arguments
184+
func("something", "abc123", "req456")
185+
186+
assert loguru_caplog.records[0].extra == {
187+
LoggerContextKeys.task_id.value: "abc123",
188+
LoggerContextKeys.privacy_request_id.value: "req456",
189+
}
190+
191+
def test_log_context_with_mixed_positional_and_keyword_only_args(
192+
self, loguru_caplog
193+
):
194+
"""Test that captured args work with functions that have a mix of positional and keyword-only arguments"""
195+
196+
@log_context(
197+
capture_args={
198+
"task_id": LoggerContextKeys.task_id,
199+
"request_id": LoggerContextKeys.privacy_request_id,
200+
}
201+
)
202+
def func(task_id: str, *, request_id: str):
203+
logger.info("processing")
204+
return task_id, request_id
205+
206+
# Pass task_id as positional and request_id as keyword (required)
207+
func("abc123", request_id="req456")
208+
209+
assert loguru_caplog.records[0].extra == {
210+
LoggerContextKeys.task_id.value: "abc123",
211+
LoggerContextKeys.privacy_request_id.value: "req456",
212+
}
213+
214+
def test_log_context_with_keyword_only_args(self, loguru_caplog):
215+
"""Test that captured args work with functions that have only keyword-only arguments"""
216+
217+
@log_context(
218+
capture_args={
219+
"task_id": LoggerContextKeys.task_id,
220+
"request_id": LoggerContextKeys.privacy_request_id,
221+
}
222+
)
223+
def func(*, task_id: str, request_id: str):
224+
logger.info("processing")
225+
return task_id, request_id
226+
227+
# All arguments must be passed as keywords
228+
func(task_id="abc123", request_id="req456")
229+
230+
assert loguru_caplog.records[0].extra == {
231+
LoggerContextKeys.task_id.value: "abc123",
232+
LoggerContextKeys.privacy_request_id.value: "req456",
233+
}
234+
235+
def test_log_context_with_default_parameters(self, loguru_caplog):
236+
"""Test that captured args work with functions that have default parameters"""
237+
238+
@log_context(
239+
capture_args={
240+
"task_id": LoggerContextKeys.task_id,
241+
"request_id": LoggerContextKeys.privacy_request_id,
242+
}
243+
)
244+
def func(task_id: str = "default_task", request_id: str = "default_request"):
245+
logger.info("processing")
246+
return task_id, request_id
247+
248+
# Call with no arguments - should use defaults
249+
func()
250+
251+
assert loguru_caplog.records[0].extra == {
252+
LoggerContextKeys.task_id.value: "default_task",
253+
LoggerContextKeys.privacy_request_id.value: "default_request",
254+
}
255+
256+
def test_log_context_with_overridden_default_parameters(self, loguru_caplog):
257+
"""Test that captured args work with functions where default parameters are overridden"""
258+
259+
@log_context(
260+
capture_args={
261+
"task_id": LoggerContextKeys.task_id,
262+
"request_id": LoggerContextKeys.privacy_request_id,
263+
}
264+
)
265+
def func(task_id: str = "default_task", request_id: str = "default_request"):
266+
logger.info("processing")
267+
return task_id, request_id
268+
269+
# Override only one default parameter
270+
func(task_id="abc123")
271+
272+
assert loguru_caplog.records[0].extra == {
273+
LoggerContextKeys.task_id.value: "abc123",
274+
LoggerContextKeys.privacy_request_id.value: "default_request",
275+
}
276+
170277

171278
class TestDetailFunctions:
172279
@pytest.fixture

0 commit comments

Comments
 (0)