Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 714e493

Browse files
committed
Issue #24305: Prevent import subsystem stack frames from being counted
by the warnings.warn(stacklevel=) parameter.
1 parent 62b2462 commit 714e493

7 files changed

Lines changed: 127 additions & 16 deletions

File tree

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from test import support
88
from test.support.script_helper import assert_python_ok, assert_python_failure
99

10-
from test import warning_tests
10+
from test.test_warnings.data import stacklevel as warning_tests
1111

1212
import warnings as original_warnings
1313

@@ -188,11 +188,11 @@ def test_once(self):
188188
self.module.resetwarnings()
189189
self.module.filterwarnings("once", category=UserWarning)
190190
message = UserWarning("FilterTests.test_once")
191-
self.module.warn_explicit(message, UserWarning, "test_warnings.py",
191+
self.module.warn_explicit(message, UserWarning, "__init__.py",
192192
42)
193193
self.assertEqual(w[-1].message, message)
194194
del w[:]
195-
self.module.warn_explicit(message, UserWarning, "test_warnings.py",
195+
self.module.warn_explicit(message, UserWarning, "__init__.py",
196196
13)
197197
self.assertEqual(len(w), 0)
198198
self.module.warn_explicit(message, UserWarning, "test_warnings2.py",
@@ -298,10 +298,10 @@ def test_filename(self):
298298
module=self.module) as w:
299299
warning_tests.inner("spam1")
300300
self.assertEqual(os.path.basename(w[-1].filename),
301-
"warning_tests.py")
301+
"stacklevel.py")
302302
warning_tests.outer("spam2")
303303
self.assertEqual(os.path.basename(w[-1].filename),
304-
"warning_tests.py")
304+
"stacklevel.py")
305305

306306
def test_stacklevel(self):
307307
# Test stacklevel argument
@@ -311,25 +311,36 @@ def test_stacklevel(self):
311311
module=self.module) as w:
312312
warning_tests.inner("spam3", stacklevel=1)
313313
self.assertEqual(os.path.basename(w[-1].filename),
314-
"warning_tests.py")
314+
"stacklevel.py")
315315
warning_tests.outer("spam4", stacklevel=1)
316316
self.assertEqual(os.path.basename(w[-1].filename),
317-
"warning_tests.py")
317+
"stacklevel.py")
318318

319319
warning_tests.inner("spam5", stacklevel=2)
320320
self.assertEqual(os.path.basename(w[-1].filename),
321-
"test_warnings.py")
321+
"__init__.py")
322322
warning_tests.outer("spam6", stacklevel=2)
323323
self.assertEqual(os.path.basename(w[-1].filename),
324-
"warning_tests.py")
324+
"stacklevel.py")
325325
warning_tests.outer("spam6.5", stacklevel=3)
326326
self.assertEqual(os.path.basename(w[-1].filename),
327-
"test_warnings.py")
327+
"__init__.py")
328328

329329
warning_tests.inner("spam7", stacklevel=9999)
330330
self.assertEqual(os.path.basename(w[-1].filename),
331331
"sys")
332332

333+
def test_stacklevel_import(self):
334+
# Issue #24305: With stacklevel=2, module-level warnings should work.
335+
support.unload('test.test_warnings.data.import_warning')
336+
with warnings_state(self.module):
337+
with original_warnings.catch_warnings(record=True,
338+
module=self.module) as w:
339+
self.module.simplefilter('always')
340+
import test.test_warnings.data.import_warning
341+
self.assertEqual(len(w), 1)
342+
self.assertEqual(w[0].filename, __file__)
343+
333344
def test_missing_filename_not_main(self):
334345
# If __file__ is not specified and __main__ is not the module name,
335346
# then __file__ should be set to the module name.

Lib/test/test_warnings/__main__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import unittest
2+
3+
unittest.main('test.test_warnings')
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import warnings
2+
3+
warnings.warn('module-level warning', DeprecationWarning, stacklevel=2)

Lib/warnings.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,20 @@ def _getcategory(category):
160160
return cat
161161

162162

163+
def _is_internal_frame(frame):
164+
"""Signal whether the frame is an internal CPython implementation detail."""
165+
filename = frame.f_code.co_filename
166+
return 'importlib' in filename and '_bootstrap' in filename
167+
168+
169+
def _next_external_frame(frame):
170+
"""Find the next frame that doesn't involve CPython internals."""
171+
frame = frame.f_back
172+
while frame is not None and _is_internal_frame(frame):
173+
frame = frame.f_back
174+
return frame
175+
176+
163177
# Code typically replaced by _warnings
164178
def warn(message, category=None, stacklevel=1):
165179
"""Issue a warning, or maybe ignore it or raise an exception."""
@@ -174,13 +188,23 @@ def warn(message, category=None, stacklevel=1):
174188
"not '{:s}'".format(type(category).__name__))
175189
# Get context information
176190
try:
177-
caller = sys._getframe(stacklevel)
191+
if stacklevel <= 1 or _is_internal_frame(sys._getframe(1)):
192+
# If frame is too small to care or if the warning originated in
193+
# internal code, then do not try to hide any frames.
194+
frame = sys._getframe(stacklevel)
195+
else:
196+
frame = sys._getframe(1)
197+
# Look for one frame less since the above line starts us off.
198+
for x in range(stacklevel-1):
199+
frame = _next_external_frame(frame)
200+
if frame is None:
201+
raise ValueError
178202
except ValueError:
179203
globals = sys.__dict__
180204
lineno = 1
181205
else:
182-
globals = caller.f_globals
183-
lineno = caller.f_lineno
206+
globals = frame.f_globals
207+
lineno = frame.f_lineno
184208
if '__name__' in globals:
185209
module = globals['__name__']
186210
else:
@@ -374,7 +398,6 @@ def __exit__(self, *exc_info):
374398
defaultaction = _defaultaction
375399
onceregistry = _onceregistry
376400
_warnings_defaults = True
377-
378401
except ImportError:
379402
filters = []
380403
defaultaction = "default"

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ Release date: 2015-09-06
1010
Core and Builtins
1111
-----------------
1212

13+
- Issue #24305: Prevent import subsystem stack frames from being counted
14+
by the warnings.warn(stacklevel=) parameter.
15+
1316
- Issue #24912: Prevent __class__ assignment to immutable built-in objects.
1417

1518
- Issue #24975: Fix AST compilation for PEP 448 syntax.

Python/_warnings.c

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,64 @@ warn_explicit(PyObject *category, PyObject *message,
513513
return result; /* Py_None or NULL. */
514514
}
515515

516+
static int
517+
is_internal_frame(PyFrameObject *frame)
518+
{
519+
static PyObject *importlib_string = NULL;
520+
static PyObject *bootstrap_string = NULL;
521+
PyObject *filename;
522+
int contains;
523+
524+
if (importlib_string == NULL) {
525+
importlib_string = PyUnicode_FromString("importlib");
526+
if (importlib_string == NULL) {
527+
return 0;
528+
}
529+
530+
bootstrap_string = PyUnicode_FromString("_bootstrap");
531+
if (bootstrap_string == NULL) {
532+
Py_DECREF(importlib_string);
533+
return 0;
534+
}
535+
Py_INCREF(importlib_string);
536+
Py_INCREF(bootstrap_string);
537+
}
538+
539+
if (frame == NULL || frame->f_code == NULL ||
540+
frame->f_code->co_filename == NULL) {
541+
return 0;
542+
}
543+
filename = frame->f_code->co_filename;
544+
if (!PyUnicode_Check(filename)) {
545+
return 0;
546+
}
547+
contains = PyUnicode_Contains(filename, importlib_string);
548+
if (contains < 0) {
549+
return 0;
550+
}
551+
else if (contains > 0) {
552+
contains = PyUnicode_Contains(filename, bootstrap_string);
553+
if (contains < 0) {
554+
return 0;
555+
}
556+
else if (contains > 0) {
557+
return 1;
558+
}
559+
}
560+
561+
return 0;
562+
}
563+
564+
static PyFrameObject *
565+
next_external_frame(PyFrameObject *frame)
566+
{
567+
do {
568+
frame = frame->f_back;
569+
} while (frame != NULL && is_internal_frame(frame));
570+
571+
return frame;
572+
}
573+
516574
/* filename, module, and registry are new refs, globals is borrowed */
517575
/* Returns 0 on error (no new refs), 1 on success */
518576
static int
@@ -523,8 +581,18 @@ setup_context(Py_ssize_t stack_level, PyObject **filename, int *lineno,
523581

524582
/* Setup globals and lineno. */
525583
PyFrameObject *f = PyThreadState_GET()->frame;
526-
while (--stack_level > 0 && f != NULL)
527-
f = f->f_back;
584+
// Stack level comparisons to Python code is off by one as there is no
585+
// warnings-related stack level to avoid.
586+
if (stack_level <= 0 || is_internal_frame(f)) {
587+
while (--stack_level > 0 && f != NULL) {
588+
f = f->f_back;
589+
}
590+
}
591+
else {
592+
while (--stack_level > 0 && f != NULL) {
593+
f = next_external_frame(f);
594+
}
595+
}
528596

529597
if (f == NULL) {
530598
globals = PyThreadState_Get()->interp->sysdict;

0 commit comments

Comments
 (0)