Skip to content

Commit 005ce73

Browse files
authored
Add CallsiteParameter.QUAL_NAME (hynek#761)
* Add CallsiteParameter.QUAL_NAME Fixes hynek#386 * Get rid of gunk * Make it truly 3.11-only * Streamline * Terse
1 parent f002806 commit 005ce73

File tree

3 files changed

+75
-7
lines changed

3 files changed

+75
-7
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ You can find our backwards-compatibility policy [here](https://github.com/hynek/
5757
- Native loggers now allow the passing of a dictionary for dictionary-based interpolation `log.info("hello %(name)s!", {"name": "world"})`.
5858
[#748](https://github.com/hynek/structlog/pull/748)
5959

60+
- On Python 3.11+, `structlog.processors.CallsiteParameterAdder` now supports `CallsiteParameter.QUAL_NAME` that adds the qualified name of the callsite, including scope and class names.
61+
This is only available for *structlog*-originated events since the standard library has no equivalent.
62+
6063

6164
### Changed
6265

src/structlog/processors.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,9 @@ class CallsiteParameter(enum.Enum):
729729
the callsite parameters in the event dictionary.
730730
731731
.. versionadded:: 21.5.0
732+
733+
.. versionadded:: 25.5.0
734+
`QUAL_NAME` parameter.
732735
"""
733736

734737
#: The full path to the python source file of the callsite.
@@ -742,6 +745,9 @@ class CallsiteParameter(enum.Enum):
742745
MODULE = "module"
743746
#: The name of the function that the callsite was in.
744747
FUNC_NAME = "func_name"
748+
#: The qualified name of the callsite (includes scope and class names).
749+
#: Requires Python 3.11+.
750+
QUAL_NAME = "qual_name"
745751
#: The line number of the callsite.
746752
LINENO = "lineno"
747753
#: The ID of the thread the callsite was executed in.
@@ -770,6 +776,10 @@ def _get_callsite_func_name(module: str, frame: FrameType) -> Any:
770776
return frame.f_code.co_name
771777

772778

779+
def _get_callsite_qual_name(module: str, frame: FrameType) -> Any:
780+
return frame.f_code.co_qualname # will crash on Python <3.11
781+
782+
773783
def _get_callsite_lineno(module: str, frame: FrameType) -> Any:
774784
return frame.f_lineno
775785

@@ -837,6 +847,7 @@ class CallsiteParameterAdder:
837847
CallsiteParameter.FILENAME: _get_callsite_filename,
838848
CallsiteParameter.MODULE: _get_callsite_module,
839849
CallsiteParameter.FUNC_NAME: _get_callsite_func_name,
850+
CallsiteParameter.QUAL_NAME: _get_callsite_qual_name,
840851
CallsiteParameter.LINENO: _get_callsite_lineno,
841852
CallsiteParameter.THREAD: _get_callsite_thread,
842853
CallsiteParameter.THREAD_NAME: _get_callsite_thread_name,
@@ -882,12 +893,15 @@ def __init__(
882893
self._active_handlers.append(
883894
(parameter, self._handlers[parameter])
884895
)
885-
self._record_mappings.append(
886-
self._RecordMapping(
887-
parameter.value,
888-
self._record_attribute_map[parameter],
896+
if (
897+
record_attr := self._record_attribute_map.get(parameter)
898+
) is not None:
899+
self._record_mappings.append(
900+
self._RecordMapping(
901+
parameter.value,
902+
record_attr,
903+
)
889904
)
890-
)
891905

892906
def __call__(
893907
self, logger: logging.Logger, name: str, event_dict: EventDict

tests/processors/test_processors.py

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,13 @@ class TestCallsiteParameterAdder:
303303
"process_name",
304304
}
305305

306-
_all_parameters = set(CallsiteParameter)
306+
# Exclude QUAL_NAME from the general set to keep parity with stdlib
307+
# LogRecord-derived parameters. QUAL_NAME is tested separately.
308+
_all_parameters = {
309+
p
310+
for p in set(CallsiteParameter)
311+
if p is not CallsiteParameter.QUAL_NAME
312+
}
307313

308314
def test_all_parameters(self) -> None:
309315
"""
@@ -317,6 +323,50 @@ def test_all_parameters(self) -> None:
317323
}
318324
assert self.parameter_strings == self.get_callsite_parameters().keys()
319325

326+
@pytest.mark.skipif(
327+
sys.version_info < (3, 11), reason="QUAL_NAME requires Python 3.11+"
328+
)
329+
def test_qual_name_structlog(self) -> None:
330+
"""
331+
QUAL_NAME is added for structlog-originated events on Python 3.11+.
332+
"""
333+
processor = CallsiteParameterAdder(
334+
parameters={CallsiteParameter.QUAL_NAME}
335+
)
336+
event_dict: EventDict = {"event": "msg"}
337+
actual = processor(None, None, event_dict)
338+
339+
assert actual["qual_name"].endswith(
340+
f"{self.__class__.__name__}.test_qual_name_structlog"
341+
)
342+
343+
def test_qual_name_logging_origin_absent(self) -> None:
344+
"""
345+
QUAL_NAME is not sourced from stdlib LogRecord and remains absent
346+
(because it doesn't exist).
347+
"""
348+
processor = CallsiteParameterAdder(
349+
parameters={CallsiteParameter.QUAL_NAME}
350+
)
351+
record = logging.LogRecord(
352+
"name",
353+
logging.INFO,
354+
__file__,
355+
0,
356+
"message",
357+
None,
358+
None,
359+
"func",
360+
)
361+
event_dict: EventDict = {
362+
"event": "message",
363+
"_record": record,
364+
"_from_structlog": False,
365+
}
366+
actual = processor(None, None, event_dict)
367+
368+
assert "qual_name" not in actual
369+
320370
@pytest.mark.asyncio
321371
@pytest.mark.parametrize(
322372
("wrapper_class", "method_name"),
@@ -550,7 +600,8 @@ def make_processor(
550600
"""
551601
if parameter_strings is None:
552602
return CallsiteParameterAdder(
553-
additional_ignores=additional_ignores
603+
parameters=cls._all_parameters,
604+
additional_ignores=additional_ignores,
554605
)
555606

556607
parameters = cls.filter_parameters(parameter_strings)

0 commit comments

Comments
 (0)