First stab at the Warnings PEP. I'm still working on the
implementation...
This commit is contained in:
parent
9056227bb3
commit
5d702e543f
328
pep-0230.txt
328
pep-0230.txt
|
@ -19,6 +19,334 @@ Abstract
|
|||
counter-proposal posted on the same date.
|
||||
|
||||
|
||||
Motivation
|
||||
|
||||
With Python 3000 looming, it is necessary to start issuing
|
||||
warnings about the use of obsolete or deprecated features, in
|
||||
addition to errors. There are also lots of other reasons to be
|
||||
able to issue warnings, both from C and from Python code, both at
|
||||
compile time and at run time.
|
||||
|
||||
Warnings aren't fatal, and thus it's possible that a program
|
||||
triggers the same warning many times during a single execution.
|
||||
It would be annoying if a program emitted an endless stream of
|
||||
identical warnings. Therefore, a mechanism is needed that
|
||||
suppresses multiple identical warnings.
|
||||
|
||||
It is also desirable to have user control over which warnings are
|
||||
printed. While in general it is useful to see all warnings all
|
||||
the time, there may be times where it is impractical to fix the
|
||||
code right away in a production program. In this case, there
|
||||
should be a way to suppress warnings.
|
||||
|
||||
It is also useful to be able to suppress specific warnings during
|
||||
program development, e.g. when a warning is generated by a piece
|
||||
of 3rd party code that cannot be fixed right away, or when there
|
||||
is no way to fix the code (possibly a warning message is generated
|
||||
for a perfectly fine piece of code). It would be unwise to offer
|
||||
to suppress all warnings in such cases: the developer would miss
|
||||
warnings about the rest of the code.
|
||||
|
||||
On the other hand, there are also situations conceivable where
|
||||
some or all warnings are better treated as errors. For example,
|
||||
it may be a local coding standard that a particular deprecated
|
||||
feature should not be used. In order to enforce this, it is
|
||||
useful to be able to turn the warning about this particular
|
||||
feature into an error, raising an exception (without necessarily
|
||||
turning all warnings into errors).
|
||||
|
||||
Therefore, I propose to introduce a flexible "warning filter"
|
||||
which can filter out warnings or change them into exceptions,
|
||||
based on:
|
||||
|
||||
- Where in the code they are generated (per package, module, or
|
||||
function)
|
||||
|
||||
- The warning category (warning categories are discussed below)
|
||||
|
||||
- A specific warning message
|
||||
|
||||
The warning filter must be controllable both from the command line
|
||||
and from Python code.
|
||||
|
||||
|
||||
APIs For Issuing Warnings
|
||||
|
||||
- To issue a warning from Python:
|
||||
|
||||
sys.warn(message[, category])
|
||||
|
||||
The category argument, if given, must be a warning category
|
||||
class (see below); it defaults to warnings.UserWarning. This
|
||||
may raise an exception if the particular warning issued is
|
||||
changed into an error by the warnings filter.
|
||||
|
||||
- To issue a warning from C:
|
||||
|
||||
int Py_Warn(char *message, PyObject *category)
|
||||
|
||||
Return 0 normally, 1 if an exception is raised. The category
|
||||
argument must be a warning category class (see below). When
|
||||
Py_Warn() function returns 1, the caller should do normal
|
||||
exception handling. [Question: what about issuing warnings
|
||||
during lexing or parsing, which don't have the exception
|
||||
machinery available?]
|
||||
|
||||
|
||||
Warnings Categories
|
||||
|
||||
The "warnings" module defines classes representing warning
|
||||
categories. This categorization is useful to be able to filter
|
||||
out groups of warnings. The classes defined in this module have
|
||||
no semantics attached to them and are never instantiated -- only
|
||||
their names are used for filtering (see the section on the
|
||||
warnings filter below). The following warnings category classes
|
||||
are currently defined:
|
||||
|
||||
- Warning -- this is the base class of all warning category
|
||||
classes. A warning category must always be a subclass of this
|
||||
class.
|
||||
|
||||
- UserWarning -- the default category for sys.warn()
|
||||
|
||||
- DeprecationWarning -- base category for warnings about deprecated
|
||||
features
|
||||
|
||||
- SyntaxWarning -- base category for warnings about dubious
|
||||
syntactic features
|
||||
|
||||
Other categories may be proposed during the review period for this
|
||||
PEP.
|
||||
|
||||
|
||||
The Warnings Filter
|
||||
|
||||
The warnings filter control whether warnings are ignored,
|
||||
displayed, or turned into errors (raising an exception).
|
||||
|
||||
There are three sides to the warnings filter:
|
||||
|
||||
- The data structures used to efficiently determine the
|
||||
disposition of a particular Py_Warn() call.
|
||||
|
||||
- The API to control the filter from Python source code.
|
||||
|
||||
- The command line switches to control the filter.
|
||||
|
||||
The warnings filter works in several stages. It is optimized for
|
||||
the (expected to be common) case where the same warning is issued
|
||||
from the same place in the code over and over.
|
||||
|
||||
First, the warning filter collects the module and line number
|
||||
where the warning is issued; this information is readily available
|
||||
through PyEval_GetFrame().
|
||||
|
||||
Conceptually, the warnings filter maintains an ordered list of
|
||||
filter specifications; any specific warning is matched against
|
||||
each filter specification in the list in turn until a match is
|
||||
found; the match determines the disposition of the match. Each
|
||||
entry is a tuple as follows:
|
||||
|
||||
(category, message, module, lineno, action)
|
||||
|
||||
- category is a class (a subclass of warnings.Warning) of which
|
||||
the warning category must be a subclass in order to match
|
||||
|
||||
- message is a regular expression that the warning message must
|
||||
match
|
||||
|
||||
- module is a regular expression that the module name must match
|
||||
|
||||
- lineno is an integer that the line number where the warning
|
||||
occurred must match, or 0 to match all line numbers
|
||||
|
||||
- action is one of the following strings:
|
||||
|
||||
- "error" -- turn matching warnings into exceptions
|
||||
|
||||
- "ignore" -- never print matching warnings
|
||||
|
||||
- "always" -- always print matching warnings
|
||||
|
||||
- "default" -- print the first occurrence of matching warnings
|
||||
for each location where the warning is issued
|
||||
|
||||
- "module" -- print the first occurrence of matching warnings
|
||||
for each module where the warning is issued
|
||||
|
||||
- "once" -- print only the first occurrence of matching
|
||||
warnings
|
||||
|
||||
The Warning class is derived from the built-in Exception class, so
|
||||
that to turn a warning into an error we raise category(message).
|
||||
|
||||
|
||||
The Warnings Output Hook
|
||||
|
||||
When the warnings filter decides to issue a warning (but not when
|
||||
it decides to raise an exception), it passes the information about
|
||||
the function sys.showwarning(message, category, filename, lineno).
|
||||
The default implementation of this function writes the warning text
|
||||
to sys.stderr, and shows the source line of the filename.
|
||||
|
||||
|
||||
TO DO
|
||||
|
||||
- There should be a function sys.insertwarningfilter(message,
|
||||
category, filename, lineno) that inserts items into the warnings
|
||||
filter after some sanity checking.
|
||||
|
||||
- There should be command line options to specify the most common
|
||||
filtering actions, which I expect to include at least:
|
||||
|
||||
- suppress all warnings
|
||||
|
||||
- suppress a particular warning message everywhere
|
||||
|
||||
- suppress all warnings in a particular module
|
||||
|
||||
- turn all warnings into exceptions
|
||||
|
||||
|
||||
Implementation
|
||||
|
||||
Here is a mostly functional implementation:
|
||||
|
||||
"""Prototype implementation of sys.warn() and related stuff."""
|
||||
|
||||
import sys, re, linecache, getopt
|
||||
|
||||
class Warning(Exception):
|
||||
"""Base class for warning categories.
|
||||
|
||||
All warning categories must be subclasses of this.
|
||||
"""
|
||||
pass
|
||||
|
||||
class UserWarning(Warning):
|
||||
"""Base class for warnings generated by user code."""
|
||||
pass
|
||||
|
||||
class DeprecationWarning(Warning):
|
||||
"""Base class for warnings about deprecated features."""
|
||||
pass
|
||||
|
||||
class SyntaxWarning(Warning):
|
||||
"""Base class for warnings about dubious syntax."""
|
||||
pass
|
||||
|
||||
defaultaction = "default"
|
||||
filter = []
|
||||
onceregistry = {}
|
||||
|
||||
def warn(message, category=None, level=1):
|
||||
"""Issue a warning, or maybe ignore it or raise an exception."""
|
||||
# Check category argument
|
||||
if category is None:
|
||||
category = UserWarning
|
||||
assert issubclass(category, Warning)
|
||||
# Get context information
|
||||
caller = sys._getframe(level)
|
||||
globals = caller.f_globals
|
||||
lineno = caller.f_lineno
|
||||
module = globals['__name__']
|
||||
filename = globals.get('__file__')
|
||||
if not filename:
|
||||
if module == "__main__":
|
||||
filename = sys.argv[0]
|
||||
if not filename:
|
||||
filename = module
|
||||
# Quick test for common case
|
||||
registry = globals.setdefault("__warningregistry__", {})
|
||||
key = (message, category, lineno)
|
||||
if registry.get(key):
|
||||
return
|
||||
# Search the filter
|
||||
for msg, cat, mod, ln, action in filter:
|
||||
if (re.match(msg, message) and
|
||||
issubclass(category, cat) and
|
||||
re.match(mod, module) and
|
||||
(ln == 0 or lineno == ln)):
|
||||
break
|
||||
else:
|
||||
action = defaultaction
|
||||
# Early exit actions
|
||||
if action == "ignore":
|
||||
registry[key] = 1
|
||||
return
|
||||
if action == "error":
|
||||
raise category(message)
|
||||
# Other actions
|
||||
if action == "once":
|
||||
registry[key] = 1
|
||||
oncekey = (message, category)
|
||||
if onceregistry.get(oncekey):
|
||||
return
|
||||
onceregistry[oncekey] = 1
|
||||
elif action == "always":
|
||||
pass
|
||||
elif action == "module":
|
||||
registry[key] = 1
|
||||
altkey = (message, category, 0)
|
||||
if registry.get(altkey):
|
||||
return
|
||||
registry[altkey] = 1
|
||||
elif action == "default":
|
||||
registry[key] = 1
|
||||
else:
|
||||
# Unrecognized actions are errors
|
||||
raise RuntimeError(
|
||||
"Unrecognized action (%s) in warnings.filter:\n %s" %
|
||||
(`action`, str((cat, msg, mod, ln, action))))
|
||||
# Print message and context
|
||||
showwarning(message, category, filename, lineno)
|
||||
|
||||
def showwarning(message, category, filename, lineno):
|
||||
print >>sys.stderr, "%s:%s: %s: %s" % (filename, lineno,
|
||||
category.__name__, message)
|
||||
line = linecache.getline(filename, lineno).strip()
|
||||
if line:
|
||||
print >>sys.stderr, " " + line
|
||||
|
||||
def setupfilter(args):
|
||||
"""Set up the warnings filter based upon command line options.
|
||||
|
||||
Return remaining command line arguments.
|
||||
Raise getopt.error if an unrecognized option is found.
|
||||
"""
|
||||
opts, args = getopt.getop(args, "")
|
||||
return args
|
||||
|
||||
# Self-test
|
||||
|
||||
def _test():
|
||||
hello = "hello world"
|
||||
warn(hello)
|
||||
warn(hello, UserWarning)
|
||||
warn(hello, DeprecationWarning)
|
||||
for i in range(3):
|
||||
warn(hello)
|
||||
filter.append(("", Warning, "", 0, "error"))
|
||||
try:
|
||||
warn(hello)
|
||||
except Exception, msg:
|
||||
print "Caught", msg.__class__.__name__ + ":", msg
|
||||
else:
|
||||
print "No exception"
|
||||
filter[:] = []
|
||||
filter.append(("", Warning, "", 0, "booh"))
|
||||
try:
|
||||
warn(hello)
|
||||
except Exception, msg:
|
||||
print "Caught", msg.__class__.__name__ + ":", msg
|
||||
else:
|
||||
print "No exception"
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = setupfilter(sys.argv[1:])
|
||||
_test()
|
||||
|
||||
|
||||
|
||||
Local Variables:
|
||||
mode: indented-text
|
||||
|
|
Loading…
Reference in New Issue