Skip to content

Commit fe4cd56

Browse files
committed
Avoid monkeypatching sys for frames tests
It's software bankruptcy and breaks sysmon coverage.
1 parent 0159b39 commit fe4cd56

File tree

6 files changed

+56
-65
lines changed

6 files changed

+56
-65
lines changed

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ ci:
44

55
repos:
66
- repo: https://github.com/psf/black
7-
rev: 23.11.0
7+
rev: 23.12.1
88
hooks:
99
- id: black
1010

1111
- repo: https://github.com/astral-sh/ruff-pre-commit
12-
rev: v0.1.7
12+
rev: v0.1.9
1313
hooks:
1414
- id: ruff
1515
args: [--fix, --exit-non-zero-on-fix]

src/structlog/_frames.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from io import StringIO
1212
from types import FrameType
13+
from typing import Callable
1314

1415
from .contextvars import _ASYNC_CALLING_STACK
1516
from .typing import ExcInfo
@@ -37,21 +38,25 @@ def _format_exception(exc_info: ExcInfo) -> str:
3738

3839
def _find_first_app_frame_and_name(
3940
additional_ignores: list[str] | None = None,
41+
*,
42+
_getframe: Callable[[], FrameType] = sys._getframe,
4043
) -> tuple[FrameType, str]:
4144
"""
4245
Remove all intra-structlog calls and return the relevant app frame.
4346
44-
Parameters:
45-
47+
Args:
4648
additional_ignores:
4749
Additional names with which the first frame must not start.
4850
49-
Returns:
51+
_getframe:
52+
Callable to find current frame. Only for testing to avoid
53+
monkeypatching of sys._getframe.
5054
55+
Returns:
5156
tuple of (frame, name)
5257
"""
5358
ignores = ["structlog"] + (additional_ignores or [])
54-
f = _ASYNC_CALLING_STACK.get(sys._getframe())
59+
f = _ASYNC_CALLING_STACK.get(_getframe())
5560
name = f.f_globals.get("__name__") or "?"
5661
while any(tuple(name.startswith(i) for i in ignores)):
5762
if f.f_back is None:

tests/test_frames.py

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99

1010
from pretend import stub
1111

12-
import structlog._frames
13-
1412
from structlog._frames import (
1513
_find_first_app_frame_and_name,
1614
_format_exception,
@@ -19,68 +17,69 @@
1917

2018

2119
class TestFindFirstAppFrameAndName:
22-
def test_ignores_structlog_by_default(self, monkeypatch):
20+
def test_ignores_structlog_by_default(self):
2321
"""
2422
No matter what you pass in, structlog frames get always ignored.
2523
"""
2624
f1 = stub(f_globals={"__name__": "test"}, f_back=None)
2725
f2 = stub(f_globals={"__name__": "structlog.blubb"}, f_back=f1)
28-
monkeypatch.setattr(structlog._frames.sys, "_getframe", lambda: f2)
29-
f, n = _find_first_app_frame_and_name()
26+
27+
f, n = _find_first_app_frame_and_name(_getframe=lambda: f2)
3028

3129
assert (f1, "test") == (f, n)
3230

33-
def test_ignoring_of_additional_frame_names_works(self, monkeypatch):
31+
def test_ignoring_of_additional_frame_names_works(self):
3432
"""
3533
Additional names are properly ignored too.
3634
"""
3735
f1 = stub(f_globals={"__name__": "test"}, f_back=None)
3836
f2 = stub(f_globals={"__name__": "ignored.bar"}, f_back=f1)
3937
f3 = stub(f_globals={"__name__": "structlog.blubb"}, f_back=f2)
40-
monkeypatch.setattr(structlog._frames.sys, "_getframe", lambda: f3)
41-
f, n = _find_first_app_frame_and_name(additional_ignores=["ignored"])
38+
39+
f, n = _find_first_app_frame_and_name(
40+
additional_ignores=["ignored"], _getframe=lambda: f3
41+
)
4242

4343
assert (f1, "test") == (f, n)
4444

45-
def test_tolerates_missing_name(self, monkeypatch):
45+
def test_tolerates_missing_name(self):
4646
"""
4747
Use ``?`` if `f_globals` lacks a `__name__` key
4848
"""
4949
f1 = stub(f_globals={}, f_back=None)
50-
monkeypatch.setattr(structlog._frames.sys, "_getframe", lambda: f1)
51-
f, n = _find_first_app_frame_and_name()
50+
51+
f, n = _find_first_app_frame_and_name(_getframe=lambda: f1)
5252

5353
assert (f1, "?") == (f, n)
5454

55-
def test_tolerates_name_explicitly_None_oneframe(self, monkeypatch):
55+
def test_tolerates_name_explicitly_None_oneframe(self):
5656
"""
5757
Use ``?`` if `f_globals` has a `None` valued `__name__` key
5858
"""
5959
f1 = stub(f_globals={"__name__": None}, f_back=None)
60-
monkeypatch.setattr(structlog._frames.sys, "_getframe", lambda: f1)
61-
f, n = _find_first_app_frame_and_name()
60+
61+
f, n = _find_first_app_frame_and_name(_getframe=lambda: f1)
6262

6363
assert (f1, "?") == (f, n)
6464

65-
def test_tolerates_name_explicitly_None_manyframe(self, monkeypatch):
65+
def test_tolerates_name_explicitly_None_manyframe(self):
6666
"""
6767
Use ``?`` if `f_globals` has a `None` valued `__name__` key,
6868
multiple frames up.
6969
"""
7070
f1 = stub(f_globals={"__name__": None}, f_back=None)
7171
f2 = stub(f_globals={"__name__": "structlog.blubb"}, f_back=f1)
72-
monkeypatch.setattr(structlog._frames.sys, "_getframe", lambda: f2)
73-
f, n = _find_first_app_frame_and_name()
72+
f, n = _find_first_app_frame_and_name(_getframe=lambda: f2)
7473

7574
assert (f1, "?") == (f, n)
7675

77-
def test_tolerates_f_back_is_None(self, monkeypatch):
76+
def test_tolerates_f_back_is_None(self):
7877
"""
7978
Use ``?`` if all frames are in ignored frames.
8079
"""
8180
f1 = stub(f_globals={"__name__": "structlog"}, f_back=None)
82-
monkeypatch.setattr(structlog._frames.sys, "_getframe", lambda: f1)
83-
f, n = _find_first_app_frame_and_name()
81+
82+
f, n = _find_first_app_frame_and_name(_getframe=lambda: f1)
8483

8584
assert (f1, "?") == (f, n)
8685

tests/test_processors.py

Lines changed: 14 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -865,23 +865,12 @@ def test_additional_ignores(self, monkeypatch: pytest.MonkeyPatch) -> None:
865865
processor = self.make_processor(None, additional_ignores)
866866
event_dict: EventDict = {"event": test_message}
867867

868-
# `functools.partial` is used instead of a lambda because a lambda will
869-
# add an additional frame in a module that should not be ignored.
870-
_sys_getframe = functools.partial(additional_frame, sys._getframe)
871-
872-
# WARNING: The below three lines are sensitive to relative line numbers
873-
# (i.e. the invocation of processor must be two lines after the
874-
# invocation of get_callsite_parameters) and is order sensitive (i.e.
875-
# monkeypatch.setattr must occur after get_callsite_parameters but
876-
# before invocation of processor).
877-
callsite_params = self.get_callsite_parameters(2)
878-
monkeypatch.setattr(sys, "_getframe", value=_sys_getframe)
868+
# Warning: the next two lines must appear exactly like this to make
869+
# line numbers match.
870+
callsite_params = self.get_callsite_parameters(1)
879871
actual = processor(None, None, event_dict)
880872

881-
expected = {
882-
"event": test_message,
883-
**callsite_params,
884-
}
873+
expected = {"event": test_message, **callsite_params}
885874

886875
assert expected == actual
887876

@@ -941,10 +930,7 @@ def test_processor(
941930
callsite_params = self.filter_parameter_dict(
942931
callsite_params, parameter_strings
943932
)
944-
expected = {
945-
"event": test_message,
946-
**callsite_params,
947-
}
933+
expected = {"event": test_message, **callsite_params}
948934

949935
assert expected == actual
950936

@@ -1029,10 +1015,7 @@ def test_e2e(
10291015
for key, value in json.loads(string_io.getvalue()).items()
10301016
if not key.startswith("_")
10311017
}
1032-
expected = {
1033-
"event": test_message,
1034-
**callsite_params,
1035-
}
1018+
expected = {"event": test_message, **callsite_params}
10361019

10371020
assert expected == actual
10381021

@@ -1044,18 +1027,16 @@ def make_processor(
10441027
) -> CallsiteParameterAdder:
10451028
"""
10461029
Creates a ``CallsiteParameterAdder`` with parameters matching the
1047-
supplied ``parameter_strings`` values and with the supplied
1048-
``additional_ignores`` values.
1049-
1050-
Parameters:
1030+
supplied *parameter_strings* values and with the supplied
1031+
*additional_ignores* values.
10511032
1033+
Args:
10521034
parameter_strings:
10531035
Strings for which corresponding ``CallsiteParameters`` should
10541036
be included in the resulting ``CallsiteParameterAdded``.
10551037
10561038
additional_ignores:
1057-
1058-
Used as ``additional_ignores`` for the resulting
1039+
Used as *additional_ignores* for the resulting
10591040
``CallsiteParameterAdded``.
10601041
"""
10611042
if parameter_strings is None:
@@ -1097,11 +1078,10 @@ def filter_parameter_dict(
10971078
cls, input: dict[str, object], parameter_strings: set[str] | None
10981079
) -> dict[str, object]:
10991080
"""
1100-
Returns a dictionary that is equivalent to ``input`` but with all keys
1101-
not in ``parameter_strings`` removed.
1102-
1103-
Parameters:
1081+
Returns a dictionary that is equivalent to *input* but with all keys
1082+
not in *parameter_strings* removed.
11041083
1084+
Args:
11051085
parameter_strings:
11061086
The keys to keep in the dictionary, if this value is ``None``
11071087
then all keys matching ``cls.parameter_strings`` are kept.
@@ -1120,8 +1100,7 @@ def get_callsite_parameters(cls, offset: int = 1) -> dict[str, object]:
11201100
This function creates dictionary of callsite parameters for the line
11211101
that is ``offset`` lines after the invocation of this function.
11221102
1123-
Parameters:
1124-
1103+
Args:
11251104
offset:
11261105
The amount of lines after the invocation of this function that
11271106
callsite parameters should be generated for.

tests/test_tracebacks.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,12 @@ def bar(n):
469469
)
470470
== frames[0]
471471
)
472+
473+
# If we run the tests under Python 3.12 with sysmon enabled, it inserts
474+
# frames at the end.
475+
if sys.version_info >= (3, 12):
476+
frames = [f for f in frames if "coverage" not in f.filename]
477+
472478
# Depending on whether we invoke pytest directly or run tox, either "foo()"
473479
# or "bar()" is at the end of the stack.
474480
assert frames[-1] in [

tox.ini

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,21 @@ commands =
2424

2525

2626
# Run oldest and latest under Coverage.
27-
[testenv:py3{8,11}-tests{,-colorama,-be,-rich}]
27+
[testenv:py3{8,12}-tests{,-colorama,-be,-rich}]
28+
set_env =
29+
py312: COVERAGE_CORE=sysmon
2830
deps =
2931
coverage[toml]
30-
py311: twisted
32+
py312: twisted
3133
colorama: colorama
3234
rich: rich
3335
be: better-exceptions
3436
commands = coverage run -m pytest {posargs}
3537

3638

3739
[testenv:coverage-report]
38-
# Keep in sync with .python-version
39-
base_python = py311
40+
# Keep in sync with .python-version-default
41+
base_python = py312
4042
deps = coverage[toml]
4143
skip_install = true
4244
parallel_show_output = true

0 commit comments

Comments
 (0)