Skip to content

Commit 739b5db

Browse files
Merge python#5
5: Support Py3xwarning warnings with a fix argument r=vext01 a=nanjekyejoannah This PR does the following: - Adds a `fix` argument to allow for flexibility in adding a possible fix to a warning - The new warning format is used for the already merged warnings for numbers Co-authored-by: Joannah Nanjekye <[email protected]>
2 parents 7f5a0b3 + f474852 commit 739b5db

File tree

12 files changed

+564
-32
lines changed

12 files changed

+564
-32
lines changed

Doc/c-api/exceptions.rst

+7-1
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,12 @@ is a separate error indicator for each thread.
319319
and *registry* arguments may be set to *NULL* to get the default effect
320320
described there.
321321
322+
.. c:function:: int PyErr_WarnExplicit_WithFix(PyObject *category, const char *message, const char *fix, const char *filename, int lineno, const char *module, PyObject *registry)
323+
324+
Issue a warning message and a potential fix. This warning is the
325+
same as `PyErr_WarnExplicit` but adds a *fix* argument to allow
326+
for `Py3xWarning` warnings to suggest potential fixes for Python
327+
3.x incompatible code.
322328
323329
.. c:function:: int PyErr_WarnPy3k(char *message, int stacklevel)
324330
@@ -715,7 +721,7 @@ the variables:
715721
+------------------------------------------+---------------------------------+----------+
716722
| :c:data:`PyExc_UserWarning` | :exc:`UserWarning` | |
717723
+------------------------------------------+---------------------------------+----------+
718-
| :c:data:`PyExc_3xWarning` | :exc:`Py3xWarning` | |
724+
| :c:data:`PyExc_3xWarning` | :exc:`Py3xWarning` | |
719725
+------------------------------------------+---------------------------------+----------+
720726
721727
Notes:

Doc/library/warnings.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ following warnings category classes are currently defined:
9595
| | bytes and bytearray. |
9696
+----------------------------------+-----------------------------------------------+
9797
| :exc:`Py3xWarning` | Base class for warnings about 3.x |
98-
| compatibility | |
98+
| compatibility. | |
9999
+----------------------------------+-----------------------------------------------+
100100

101101
While these are technically built-in exceptions, they are documented here,

Include/warnings.h

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ PyAPI_FUNC(void) _PyWarnings_Init(void);
99
PyAPI_FUNC(int) PyErr_WarnEx(PyObject *, const char *, Py_ssize_t);
1010
PyAPI_FUNC(int) PyErr_WarnExplicit(PyObject *, const char *, const char *, int,
1111
const char *, PyObject *);
12+
PyAPI_FUNC(int) PyErr_WarnExplicit_WithFix(PyObject *, const char *, const char *, const char *, int,
13+
const char *, PyObject *);
1214

1315
#define PyErr_WarnPy3k(msg, stacklevel) \
1416
(Py_Py3kWarningFlag ? PyErr_WarnEx(PyExc_DeprecationWarning, msg, stacklevel) : 0)

Lib/test/test_grammar.py

+13-8
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,19 @@ def test_plain_integers(self):
6666
self.fail('Weird maxint value %r' % maxint)
6767

6868
if sys.py3kwarning:
69-
with warnings.catch_warnings():
70-
warnings.filterwarnings('error', category=Py3xWarning)
71-
with self.assertRaises(Py3xWarning) as oct:
72-
compile('032', '<test string>', 'eval')
73-
self.assertIn("octal literals are not supported in 3.x;\n"
74-
"drop the leading 0",
75-
str(oct.exception))
76-
69+
with warnings.catch_warnings(record=True) as w:
70+
warnings.filterwarnings('always', category=Py3xWarning)
71+
self.assertEqual(034, 28)
72+
self.assertEqual(01, 1)
73+
for warning in w:
74+
self.assertTrue(Py3xWarning is w.category)
75+
self.assertEqual(str(w.message), "using just a '0' prefix for octal literals is not supported in 3.x: " \
76+
"use the '0o' prefix for octal integers")
77+
self.assertEqual(len(w), 2)
78+
self.assertIn("using just a '0' prefix for octal literals is not supported in 3.x:: \n"
79+
"use the '0o' prefix for octal integers",
80+
str(oct.exception))
81+
7782
def test_long_integers(self):
7883
x = 0L
7984
x = 0l

Lib/test/test_optparse.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
BadOptionError, OptionValueError, Values
2424
from optparse import _match_abbrev
2525
from optparse import _parse_num
26-
from test.test_support import run_unittest, check_py3k_warnings
26+
from test.test_support import run_unittest, check_py3k_warnings, warnings
2727

2828
retype = type(re.compile(''))
2929

@@ -1656,14 +1656,20 @@ def test_numeric_options(self):
16561656
"option -l: invalid long integer value: '0x12x'")
16571657

16581658
def test_parse_num_3k_warnings(self):
1659-
expected = 'the L suffix is not supported in 3.x; simply drop the suffix, \
1660-
or accept the auto fixer modifications'
1661-
with check_py3k_warnings((expected, Py3xWarning)):
1659+
with warnings.catch_warnings(record=True) as w:
1660+
warnings.filterwarnings("always", "the L suffix is not supported in 3.x: \
1661+
drop the suffix",
1662+
Py3xWarning)
16621663
x = 10L
16631664
y = 8L
16641665
z = x + y
16651666
a = x * y
16661667
b = x - y
1668+
for warning in w:
1669+
self.assertTrue(Py3xWarning is w.category)
1670+
self.assertEqual(str(w.message), "the L suffix is not supported in 3.x: " \
1671+
"drop the suffix")
1672+
self.assertEqual(len(w), 5)
16671673

16681674

16691675
def test_main():

Lib/test/test_warnings.py

+62-2
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ class PublicAPITests(BaseTest):
7070

7171
def test_module_all_attribute(self):
7272
self.assertTrue(hasattr(self.module, '__all__'))
73-
target_api = ["warn", "warn_explicit", "showwarning",
73+
target_api = ["warn", "warn_explicit", "warn_explicit_with_fix"
74+
"showwarning", "showwarningwithfix", "formatwarningwithfix",
7475
"formatwarning", "filterwarnings", "simplefilter",
7576
"resetwarnings", "catch_warnings"]
7677
self.assertSetEqual(set(self.module.__all__),
@@ -161,6 +162,23 @@ def test_once(self):
161162
42)
162163
self.assertEqual(len(w), 0)
163164

165+
def test_once_with_fix(self):
166+
with original_warnings.catch_warnings(record=True,
167+
module=self.module) as w:
168+
self.module.resetwarnings()
169+
self.module.filterwarnings("once", category=UserWarning)
170+
message = UserWarning("FilterTests.test_once")
171+
fix = 'some fix'
172+
self.module.warn_explicit_with_fix(message, fix, UserWarning, "test_warnings.py",
173+
42)
174+
del w[:]
175+
self.module.warn_explicit_with_fix(message, fix, UserWarning, "test_warnings.py",
176+
13)
177+
self.assertEqual(len(w), 0)
178+
self.module.warn_explicit_with_fix(message, fix, UserWarning, "test_warnings2.py",
179+
42)
180+
self.assertEqual(len(w), 0)
181+
164182
def test_inheritance(self):
165183
with original_warnings.catch_warnings(module=self.module) as w:
166184
self.module.resetwarnings()
@@ -230,7 +248,8 @@ class PyFilterTests(BaseTest, FilterTests):
230248

231249
class WarnTests(unittest.TestCase):
232250

233-
"""Test warnings.warn() and warnings.warn_explicit()."""
251+
"""Test warnings.warn(), warnings.warn_explicit()
252+
and warnings.warn_explicit_with_fix()."""
234253

235254
def test_message(self):
236255
with original_warnings.catch_warnings(record=True,
@@ -644,6 +663,25 @@ def test_formatwarning(self):
644663
self.assertEqual(expect, self.module.formatwarning(message,
645664
category, file_name, line_num, file_line))
646665

666+
def test_formatwarningwithfix(self):
667+
message = "msg"
668+
fix = 'fix'
669+
category = Warning
670+
file_name = os.path.splitext(warning_tests.__file__)[0] + '.py'
671+
line_num = 3
672+
file_line = linecache.getline(file_name, line_num).strip()
673+
format = "%s:%s: %s: %s: %s\n %s\n"
674+
expect = format % (file_name, line_num, category.__name__, message,
675+
fix, file_line)
676+
self.assertEqual(expect, self.module.formatwarningwithfix(message, fix,
677+
category, file_name, line_num))
678+
# Test the 'line' argument.
679+
file_line += " for the win!"
680+
expect = format % (file_name, line_num, category.__name__, message,
681+
fix, file_line)
682+
self.assertEqual(expect, self.module.formatwarningwithfix(message, fix,
683+
category, file_name, line_num, file_line))
684+
647685
@test_support.requires_unicode
648686
def test_formatwarning_unicode_msg(self):
649687
message = u"msg"
@@ -722,6 +760,28 @@ def test_showwarning(self):
722760
file_object, expected_file_line)
723761
self.assertEqual(expect, file_object.getvalue())
724762

763+
def test_showwarningwithfix(self):
764+
file_name = os.path.splitext(warning_tests.__file__)[0] + '.py'
765+
line_num = 3
766+
expected_file_line = linecache.getline(file_name, line_num).strip()
767+
message = 'msg'
768+
fix = 'fix'
769+
category = Warning
770+
file_object = StringIO.StringIO()
771+
expect = self.module.formatwarningwithfix(message, fix, category, file_name,
772+
line_num)
773+
self.module.showwarningwithfix(message, fix, category, file_name, line_num,
774+
file_object)
775+
self.assertEqual(file_object.getvalue(), expect)
776+
# Test 'line' argument.
777+
expected_file_line += "for the win!"
778+
expect = self.module.formatwarningwithfix(message, fix, category, file_name,
779+
line_num, expected_file_line)
780+
file_object = StringIO.StringIO()
781+
self.module.showwarningwithfix(message, fix, category, file_name, line_num,
782+
file_object, expected_file_line)
783+
self.assertEqual(expect, file_object.getvalue())
784+
725785
class CWarningsDisplayTests(BaseTest, WarningsDisplayTests):
726786
module = c_warnings
727787

Lib/warnings.py

+112-3
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
import sys
88
import types
99

10-
__all__ = ["warn", "warn_explicit", "showwarning",
11-
"formatwarning", "filterwarnings", "simplefilter",
10+
__all__ = ["warn", "warn_explicit", "warn_explicit_with_fix",
11+
"showwarning", "showwarningwithfix", "formatwarning",
12+
"formatwarningwithfix", "filterwarnings", "simplefilter",
1213
"resetwarnings", "catch_warnings"]
1314

1415

@@ -37,6 +38,21 @@ def _show_warning(message, category, filename, lineno, file=None, line=None):
3738
# triggered.
3839
showwarning = _show_warning
3940

41+
def _show_warning_with_fix(message, fix, category, filename, lineno, file=None, line=None):
42+
"""Hook to write a warning to a file; replace if you like."""
43+
if file is None:
44+
file = sys.stderr
45+
if file is None:
46+
# sys.stderr is None - warnings get lost
47+
return
48+
try:
49+
file.write(formatwarningwithfix(message, fix, category, filename, lineno, line))
50+
except (IOError, UnicodeError):
51+
pass # the file (probably stderr) is invalid - this warning gets lost.
52+
# Keep a working version around in case the deprecation of the old API is
53+
# triggered.
54+
showwarningwithfix = _show_warning_with_fix
55+
4056
def formatwarning(message, category, filename, lineno, line=None):
4157
"""Function to format a warning the standard way."""
4258
try:
@@ -64,6 +80,34 @@ def formatwarning(message, category, filename, lineno, line=None):
6480
s = "%s:%s" % (filename, s)
6581
return s
6682

83+
def formatwarningwithfix(message, fix, category, filename, lineno, line=None):
84+
"""Function to format a warning the standard way with a fix."""
85+
try:
86+
unicodetype = unicode
87+
except NameError:
88+
unicodetype = ()
89+
try:
90+
message = str(message)
91+
fix = str(fix)
92+
except UnicodeEncodeError:
93+
pass
94+
s = "%s: %s: %s: %s\n" % (lineno, category.__name__, message, fix)
95+
line = linecache.getline(filename, lineno) if line is None else line
96+
if line:
97+
line = line.strip()
98+
if isinstance(s, unicodetype) and isinstance(line, str):
99+
line = unicode(line, 'latin1')
100+
s += " %s\n" % line
101+
if isinstance(s, unicodetype) and isinstance(filename, str):
102+
enc = sys.getfilesystemencoding()
103+
if enc:
104+
try:
105+
filename = unicode(filename, enc)
106+
except UnicodeDecodeError:
107+
pass
108+
s = "%s:%s" % (filename, s)
109+
return s
110+
67111
def filterwarnings(action, message="", category=Warning, module="", lineno=0,
68112
append=0):
69113
"""Insert an entry into the list of warnings filters (at the front).
@@ -299,6 +343,71 @@ def warn_explicit(message, category, filename, lineno,
299343
# Print message and context
300344
showwarning(message, category, filename, lineno)
301345

346+
def warn_explicit_with_fix(message, fix, category, filename, lineno,
347+
module=None, registry=None, module_globals=None):
348+
lineno = int(lineno)
349+
if module is None:
350+
module = filename or "<unknown>"
351+
if module[-3:].lower() == ".py":
352+
module = module[:-3] # XXX What about leading pathname?
353+
if registry is None:
354+
registry = {}
355+
if isinstance(message, Warning):
356+
text = str(message)
357+
category = message.__class__
358+
else:
359+
text = message
360+
message = category(message)
361+
key = (text, category, lineno)
362+
# Quick test for common case
363+
if registry.get(key):
364+
return
365+
# Search the filters
366+
for item in filters:
367+
action, msg, cat, mod, ln = item
368+
if ((msg is None or msg.match(text)) and
369+
issubclass(category, cat) and
370+
(mod is None or mod.match(module)) and
371+
(ln == 0 or lineno == ln)):
372+
break
373+
else:
374+
action = defaultaction
375+
# Early exit actions
376+
if action == "ignore":
377+
registry[key] = 1
378+
return
379+
380+
# Prime the linecache for formatting, in case the
381+
# "file" is actually in a zipfile or something.
382+
linecache.getlines(filename, module_globals)
383+
384+
if action == "error":
385+
raise message
386+
# Other actions
387+
if action == "once":
388+
registry[key] = 1
389+
oncekey = (text, category)
390+
if onceregistry.get(oncekey):
391+
return
392+
onceregistry[oncekey] = 1
393+
elif action == "always":
394+
pass
395+
elif action == "module":
396+
registry[key] = 1
397+
altkey = (text, category, 0)
398+
if registry.get(altkey):
399+
return
400+
registry[altkey] = 1
401+
elif action == "default":
402+
registry[key] = 1
403+
else:
404+
# Unrecognized actions are errors
405+
raise RuntimeError(
406+
"Unrecognized action (%r) in warnings.filters:\n %s" %
407+
(action, item))
408+
# Print message and context
409+
showwarningwithfix(message, fix, category, filename, lineno)
410+
302411

303412
class WarningMessage(object):
304413

@@ -395,7 +504,7 @@ def __exit__(self, *exc_info):
395504
_warnings_defaults = False
396505
try:
397506
from _warnings import (filters, default_action, once_registry,
398-
warn, warn_explicit)
507+
warn, warn_explicit, warn_explicit_with_fix)
399508
defaultaction = default_action
400509
onceregistry = once_registry
401510
_warnings_defaults = True

Objects/exceptions.c

+2-1
Original file line numberDiff line numberDiff line change
@@ -1982,7 +1982,8 @@ SimpleExtendsException(PyExc_Warning, SyntaxWarning,
19821982

19831983

19841984
/*
1985-
* 3xWarning extends Warning
1985+
* Py3xWarning extends Warning
1986+
* XXX(nanjekyejoannah): Suppress this warning for legacy tests for now
19861987
*/
19871988
SimpleExtendsException(PyExc_Warning, Py3xWarning,
19881989
"Base class for warnings about 3.x compatibility.");

PC/os2emx/python27.def

+1
Original file line numberDiff line numberDiff line change
@@ -914,6 +914,7 @@ EXPORTS
914914
"_PyErr_BadInternalCall"
915915
"PyErr_Warn"
916916
"PyErr_WarnExplicit"
917+
"PyErr_WarnExplicit_WithFix"
917918

918919
; From python27_s.lib(frozen)
919920
"PyImport_FrozenModules"

Parser/tokenizer.c

+9-8
Original file line numberDiff line numberDiff line change
@@ -1433,14 +1433,6 @@ tok_get(register struct tok_state *tok, char **p_start, char **p_end)
14331433
/* Number */
14341434
if (isdigit(c)) {
14351435
if (c == '0') {
1436-
if (Py_Py3kWarningFlag) {
1437-
if (PyErr_WarnExplicit(PyExc_Py3xWarning,
1438-
"octal literals are not supported in 3.x;\n"
1439-
"drop the leading 0",
1440-
tok->filename, tok->lineno, NULL, NULL)) {
1441-
return NULL;
1442-
}
1443-
}
14441436
/* Hex, octal or binary -- maybe. */
14451437
c = tok_nextc(tok);
14461438
if (c == '.')
@@ -1449,6 +1441,15 @@ tok_get(register struct tok_state *tok, char **p_start, char **p_end)
14491441
if (c == 'j' || c == 'J')
14501442
goto imaginary;
14511443
#endif
1444+
if (c != 'o' || c != 'O') {
1445+
char buf[100];
1446+
if (Py_Py3kWarningFlag) {
1447+
if (PyErr_WarnExplicit_WithFix(PyExc_Py3xWarning, "using just a '0' prefix for octal literals is not supported in 3.x",
1448+
"use the '0o' prefix for octal integers, if you intended the integer to be decimal", tok->filename, tok->lineno, NULL, NULL)) {
1449+
return NULL;
1450+
}
1451+
}
1452+
}
14521453
if (c == 'x' || c == 'X') {
14531454

14541455
/* Hex */

0 commit comments

Comments
 (0)