Skip to content

Commit a02310a

Browse files
Philipp Loosenicoddemus
authored andcommitted
Add stacklevel tests for warnings, 'location' to pytest_warning_captured
Resolves #4445 and #5928 (thanks to allanlewis) Add CHANGELOG for location parameter
1 parent 2fa0518 commit a02310a

File tree

9 files changed

+175
-10
lines changed

9 files changed

+175
-10
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ Oscar Benjamin
207207
Patrick Hayes
208208
Paweł Adamczak
209209
Pedro Algarvio
210+
Philipp Loose
210211
Pieter Mulder
211212
Piotr Banaszkiewicz
212213
Pulkit Goyal

changelog/4445.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed some warning reports produced by pytest to point to the correct location of the warning in the user's code.

changelog/5928.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Report ``PytestUnknownMarkWarning`` at the level of the user's code, not ``pytest``'s.

changelog/5984.improvement.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The ``pytest_warning_captured`` hook now receives a ``location`` parameter with the code location that generated the warning.

src/_pytest/config/__init__.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -133,13 +133,7 @@ def directory_arg(path, optname):
133133

134134

135135
# Plugins that cannot be disabled via "-p no:X" currently.
136-
essential_plugins = (
137-
"mark",
138-
"main",
139-
"runner",
140-
"fixtures",
141-
"helpconfig", # Provides -p.
142-
)
136+
essential_plugins = ("mark", "main", "runner", "fixtures", "helpconfig") # Provides -p.
143137

144138
default_plugins = essential_plugins + (
145139
"python",
@@ -589,7 +583,7 @@ def import_plugin(self, modname, consider_entry_points=False):
589583
_issue_warning_captured(
590584
PytestConfigWarning("skipped plugin {!r}: {}".format(modname, e.msg)),
591585
self.hook,
592-
stacklevel=1,
586+
stacklevel=2,
593587
)
594588
else:
595589
mod = sys.modules[importspec]

src/_pytest/hookspec.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -562,7 +562,7 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config):
562562

563563

564564
@hookspec(historic=True)
565-
def pytest_warning_captured(warning_message, when, item):
565+
def pytest_warning_captured(warning_message, when, item, location):
566566
"""
567567
Process a warning captured by the internal pytest warnings plugin.
568568
@@ -582,6 +582,10 @@ def pytest_warning_captured(warning_message, when, item):
582582
in a future release.
583583
584584
The item being executed if ``when`` is ``"runtest"``, otherwise ``None``.
585+
586+
:param tuple location:
587+
Holds information about the execution context of the captured warning (filename, linenumber, function).
588+
``function`` evaluates to <module> when the execution context is at the module level.
585589
"""
586590

587591

src/_pytest/mark/structures.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ def __getattr__(self, name: str) -> MarkDecorator:
347347
"custom marks to avoid this warning - for details, see "
348348
"https://docs.pytest.org/en/latest/mark.html" % name,
349349
PytestUnknownMarkWarning,
350+
2,
350351
)
351352

352353
return MarkDecorator(Mark(name, (), {}))

src/_pytest/warnings.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,10 @@ def _issue_warning_captured(warning, hook, stacklevel):
149149
warnings.warn(warning, stacklevel=stacklevel)
150150
# Mypy can't infer that record=True means records is not None; help it.
151151
assert records is not None
152+
frame = sys._getframe(stacklevel - 1)
153+
location = frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name
152154
hook.pytest_warning_captured.call_historic(
153-
kwargs=dict(warning_message=records[0], when="config", item=None)
155+
kwargs=dict(
156+
warning_message=records[0], when="config", item=None, location=location
157+
)
154158
)

testing/test_warnings.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os
12
import warnings
23

34
import pytest
@@ -641,3 +642,160 @@ def pytest_configure():
641642
assert "INTERNALERROR" not in result.stderr.str()
642643
warning = recwarn.pop()
643644
assert str(warning.message) == "from pytest_configure"
645+
646+
647+
class TestStackLevel:
648+
@pytest.fixture
649+
def capwarn(self, testdir):
650+
class CapturedWarnings:
651+
captured = []
652+
653+
@classmethod
654+
def pytest_warning_captured(cls, warning_message, when, item, location):
655+
cls.captured.append((warning_message, location))
656+
657+
testdir.plugins = [CapturedWarnings()]
658+
659+
return CapturedWarnings
660+
661+
def test_issue4445_rewrite(self, testdir, capwarn):
662+
"""#4445: Make sure the warning points to a reasonable location
663+
See origin of _issue_warning_captured at: _pytest.assertion.rewrite.py:241
664+
"""
665+
testdir.makepyfile(some_mod="")
666+
conftest = testdir.makeconftest(
667+
"""
668+
import some_mod
669+
import pytest
670+
671+
pytest.register_assert_rewrite("some_mod")
672+
"""
673+
)
674+
testdir.parseconfig()
675+
676+
# with stacklevel=5 the warning originates from register_assert_rewrite
677+
# function in the created conftest.py
678+
assert len(capwarn.captured) == 1
679+
warning, location = capwarn.captured.pop()
680+
file, lineno, func = location
681+
682+
assert "Module already imported" in str(warning.message)
683+
assert file == str(conftest)
684+
assert func == "<module>" # the above conftest.py
685+
assert lineno == 4
686+
687+
def test_issue4445_preparse(self, testdir, capwarn):
688+
"""#4445: Make sure the warning points to a reasonable location
689+
See origin of _issue_warning_captured at: _pytest.config.__init__.py:910
690+
"""
691+
testdir.makeconftest(
692+
"""
693+
import nothing
694+
"""
695+
)
696+
testdir.parseconfig("--help")
697+
698+
# with stacklevel=2 the warning should originate from config._preparse and is
699+
# thrown by an errorneous conftest.py
700+
assert len(capwarn.captured) == 1
701+
warning, location = capwarn.captured.pop()
702+
file, _, func = location
703+
704+
assert "could not load initial conftests" in str(warning.message)
705+
assert "config{sep}__init__.py".format(sep=os.sep) in file
706+
assert func == "_preparse"
707+
708+
def test_issue4445_import_plugin(self, testdir, capwarn):
709+
"""#4445: Make sure the warning points to a reasonable location
710+
See origin of _issue_warning_captured at: _pytest.config.__init__.py:585
711+
"""
712+
testdir.makepyfile(
713+
some_plugin="""
714+
import pytest
715+
pytest.skip("thing", allow_module_level=True)
716+
"""
717+
)
718+
testdir.syspathinsert()
719+
testdir.parseconfig("-p", "some_plugin")
720+
721+
# with stacklevel=2 the warning should originate from
722+
# config.PytestPluginManager.import_plugin is thrown by a skipped plugin
723+
724+
# During config parsing the the pluginargs are checked in a while loop
725+
# that as a result of the argument count runs import_plugin twice, hence
726+
# two identical warnings are captured (is this intentional?).
727+
assert len(capwarn.captured) == 2
728+
warning, location = capwarn.captured.pop()
729+
file, _, func = location
730+
731+
assert "skipped plugin 'some_plugin': thing" in str(warning.message)
732+
assert "config{sep}__init__.py".format(sep=os.sep) in file
733+
assert func == "import_plugin"
734+
735+
def test_issue4445_resultlog(self, testdir, capwarn):
736+
"""#4445: Make sure the warning points to a reasonable location
737+
See origin of _issue_warning_captured at: _pytest.resultlog.py:35
738+
"""
739+
testdir.makepyfile(
740+
"""
741+
def test_dummy():
742+
pass
743+
"""
744+
)
745+
# Use parseconfigure() because the warning in resultlog.py is triggered in
746+
# the pytest_configure hook
747+
testdir.parseconfigure(
748+
"--result-log={dir}".format(dir=testdir.tmpdir.join("result.log"))
749+
)
750+
751+
# with stacklevel=2 the warning originates from resultlog.pytest_configure
752+
# and is thrown when --result-log is used
753+
warning, location = capwarn.captured.pop()
754+
file, _, func = location
755+
756+
assert "--result-log is deprecated" in str(warning.message)
757+
assert "resultlog.py" in file
758+
assert func == "pytest_configure"
759+
760+
def test_issue4445_cacheprovider_set(self, testdir, capwarn):
761+
"""#4445: Make sure the warning points to a reasonable location
762+
See origin of _issue_warning_captured at: _pytest.cacheprovider.py:59
763+
"""
764+
testdir.tmpdir.join(".pytest_cache").write("something wrong")
765+
testdir.runpytest(plugins=[capwarn()])
766+
767+
# with stacklevel=3 the warning originates from one stacklevel above
768+
# _issue_warning_captured in cacheprovider.Cache.set and is thrown
769+
# when there are errors during cache folder creation
770+
771+
# set is called twice (in module stepwise and in cacheprovider) so emits
772+
# two warnings when there are errors during cache folder creation. (is this intentional?)
773+
assert len(capwarn.captured) == 2
774+
warning, location = capwarn.captured.pop()
775+
file, lineno, func = location
776+
777+
assert "could not create cache path" in str(warning.message)
778+
assert "cacheprovider.py" in file
779+
assert func == "set"
780+
781+
def test_issue4445_issue5928_mark_generator(self, testdir):
782+
"""#4445 and #5928: Make sure the warning from an unknown mark points to
783+
the test file where this mark is used.
784+
"""
785+
testfile = testdir.makepyfile(
786+
"""
787+
import pytest
788+
789+
@pytest.mark.unknown
790+
def test_it():
791+
pass
792+
"""
793+
)
794+
result = testdir.runpytest_subprocess()
795+
# with stacklevel=2 the warning should originate from the above created test file
796+
result.stdout.fnmatch_lines_random(
797+
[
798+
"*{testfile}:3*".format(testfile=str(testfile)),
799+
"*Unknown pytest.mark.unknown*",
800+
]
801+
)

0 commit comments

Comments
 (0)