diff options
| author | milde <milde@929543f6-e4f2-0310-98a6-ba3bd3dd1d04> | 2011-05-19 08:56:27 +0000 |
|---|---|---|
| committer | milde <milde@929543f6-e4f2-0310-98a6-ba3bd3dd1d04> | 2011-05-19 08:56:27 +0000 |
| commit | fd72f55d904bdea4e2fea83fb3b612555f8b8909 (patch) | |
| tree | b8b357be72ac2c45f9cb081dd2cc2084d7b914e6 | |
| parent | d4fe2cf3a195f507e4473e87dd120e134e7d1698 (diff) | |
| download | docutils-fd72f55d904bdea4e2fea83fb3b612555f8b8909.tar.gz | |
New sub-module and test suite for error reporting.
* Code to handle encoding/decoding errors when reporting exceptions.
* Test and fix error reporting with problematic locale settings.
(https://bugs.gentoo.org/show_bug.cgi?id=349101)
git-svn-id: http://svn.code.sf.net/p/docutils/code/trunk@7037 929543f6-e4f2-0310-98a6-ba3bd3dd1d04
| -rw-r--r-- | docutils/HISTORY.txt | 5 | ||||
| -rw-r--r-- | docutils/docutils/core.py | 10 | ||||
| -rw-r--r-- | docutils/docutils/error_reporting.py | 193 | ||||
| -rw-r--r-- | docutils/docutils/frontend.py | 13 | ||||
| -rw-r--r-- | docutils/docutils/io.py | 207 | ||||
| -rw-r--r-- | docutils/docutils/parsers/rst/directives/misc.py | 41 | ||||
| -rw-r--r-- | docutils/docutils/parsers/rst/directives/tables.py | 10 | ||||
| -rw-r--r-- | docutils/docutils/parsers/rst/states.py | 4 | ||||
| -rw-r--r-- | docutils/docutils/statemachine.py | 2 | ||||
| -rw-r--r-- | docutils/docutils/utils.py | 18 | ||||
| -rw-r--r-- | docutils/test/test_error_reporting.py | 339 | ||||
| -rwxr-xr-x | docutils/test/test_io.py | 153 | ||||
| -rwxr-xr-x | docutils/test/test_parsers/test_rst/test_directives/test_raw.py | 2 | ||||
| -rwxr-xr-x | docutils/tools/buildhtml.py | 18 |
14 files changed, 601 insertions, 414 deletions
diff --git a/docutils/HISTORY.txt b/docutils/HISTORY.txt index befebec79..49ef25555 100644 --- a/docutils/HISTORY.txt +++ b/docutils/HISTORY.txt @@ -27,11 +27,10 @@ Changes Since 0.7 - Decode command line arguments with the locale's preferred encoding (to allow, e.g., ``--title=Dornröschen``). - Orphaned "python" reader and "newlatex2e" writer moved to the sandbox. - - Work around Issue2517_ to allow unicode messages in `IOError` - instances with Python < 2.6. + - New sub-module `error_reporting`: handle encoding/decoding errors + when reporting exceptions. .. _BCP 47: http://www.rfc-editor.org/rfc/bcp/bcp47.txt - .. _Issue2517: http://bugs.python.org/issue2517 * tools/buildhtml.py: diff --git a/docutils/docutils/core.py b/docutils/docutils/core.py index 91275f4a2..620c01ac4 100644 --- a/docutils/docutils/core.py +++ b/docutils/docutils/core.py @@ -20,7 +20,7 @@ from docutils import __version__, __version_details__, SettingsSpec from docutils import frontend, io, utils, readers, writers from docutils.frontend import OptionParser from docutils.transforms import Transformer -from docutils.io import ErrorOutput +from docutils.error_reporting import ErrorOutput, ErrorString import docutils.readers.doctree class Publisher: @@ -152,6 +152,8 @@ class Publisher: option_parser = self.setup_option_parser( usage, description, settings_spec, config_section, **defaults) if argv is None: + # converting to Unicode (Python 3 does this automatically): + # TODO: make this failsafe and reversible argv_encoding = (sys.stdin.encoding or frontend.locale_encoding or 'ascii') argv = [a.decode(argv_encoding) for a in sys.argv[1:]] @@ -258,7 +260,7 @@ class Publisher: elif isinstance(error, UnicodeEncodeError): self.report_UnicodeError(error) else: - print >>self._stderr, '%s: %s' % (error.__class__.__name__, error) + print >>self._stderr, u'%s' % ErrorString(error) print >>self._stderr, ("""\ Exiting due to error. Use "--traceback" to diagnose. Please report errors to <docutils-users@lists.sf.net>. @@ -275,7 +277,7 @@ command line used.""" % (__version__, __version_details__, def report_UnicodeError(self, error): data = error.object[error.start:error.end] self._stderr.write( - '%s: %s\n' + '%s\n' '\n' 'The specified output encoding (%s) cannot\n' 'handle all of the output.\n' @@ -295,7 +297,7 @@ command line used.""" % (__version__, __version_details__, 'Include "--traceback" output, Docutils version (%s),\n' 'Python version (%s), your OS type & version, and the\n' 'command line used.\n' - % (error.__class__.__name__, error, + % (ErrorString(error), self.settings.output_encoding, data.encode('ascii', 'xmlcharrefreplace'), data.encode('ascii', 'backslashreplace'), diff --git a/docutils/docutils/error_reporting.py b/docutils/docutils/error_reporting.py new file mode 100644 index 000000000..a00685aa6 --- /dev/null +++ b/docutils/docutils/error_reporting.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python +# -*- coding: utf8 -*- +# :Copyright: © 2011 Günter Milde. +# Released without warranties or conditions of any kind +# under the terms of the Apache License, Version 2.0 +# http://www.apache.org/licenses/LICENSE-2.0 +# :Id: $Id$ + +""" +Error reporting should be safe from encoding/decoding errors. +However, implicit conversions of strings and exceptions like + +>>> u'%s world: %s' % ('H\xe4llo', Exception(u'H\xe4llo') + +fail in some Python versions: + +* In Python <= 2.6, ``unicode(<exception instance>)`` uses + `__str__` and fails with non-ASCII chars in`unicode` arguments. + (work around http://bugs.python.org/issue2517): + +* In Python 2, unicode(<exception instance>) fails, with non-ASCII + chars in arguments. (Use case: in some locales, the errstr + argument of IOError contains non-ASCII chars.) + +* In Python 2, str(<exception instance>) fails, with non-ASCII chars + in `unicode` arguments. + +The `SafeString`, `ErrorString` and `ErrorOutput` classes handle +common exceptions. +""" + +import sys, codecs + +# Guess the locale's encoding. +# If no valid guess can be made, locale_encoding is set to `None`: +try: + import locale # module missing in Jython +except ImportError: + locale_encoding = None +else: + locale_encoding = locale.getlocale()[1] or locale.getdefaultlocale()[1] + # locale.getpreferredencoding([do_setlocale=True|False]) + # has side-effects | might return a wrong guess. + # (cf. Update 1 in http://stackoverflow.com/questions/4082645/using-python-2-xs-locale-module-to-format-numbers-and-currency) + try: + codecs.lookup(locale_encoding or '') # None -> '' + except LookupError: + locale_encoding = None + + + +class SafeString(object): + """ + A wrapper providing robust conversion to `str` and `unicode`. + """ + + def __init__(self, data, encoding=None, encoding_errors='backslashreplace', + decoding_errors='replace'): + self.data = data + self.encoding = (encoding or getattr(data, 'encoding', None) or + locale_encoding or 'ascii') + self.encoding_errors = encoding_errors + self.decoding_errors = decoding_errors + + + def __str__(self): + try: + return str(self.data) + except UnicodeEncodeError, err: + if isinstance(self.data, Exception): + args = [str(SafeString(arg, self.encoding, + self.encoding_errors)) + for arg in self.data.args] + return ', '.join(args) + if isinstance(self.data, unicode): + return self.data.encode(self.encoding, self.encoding_errors) + raise + + def __unicode__(self): + """ + Return unicode representation of `self.data`. + + Try ``unicode(self.data)``, catch `UnicodeError` and + + * if `self.data` is an Exception instance, work around + http://bugs.python.org/issue2517 with an emulation of + Exception.__unicode__, + + * else decode with `self.encoding` and `self.decoding_errors`. + """ + try: + return unicode(self.data) + except UnicodeError, error: # catch ..Encode.. and ..Decode.. errors + if isinstance(self.data, EnvironmentError): + return u"[Errno %s] %s: '%s'" % (self.data.errno, + SafeString(self.data.strerror, self.encoding, + self.decoding_errors), + SafeString(self.data.filename, self.encoding, + self.decoding_errors)) + if isinstance(self.data, Exception): + args = [unicode(SafeString(arg, self.encoding, + decoding_errors=self.decoding_errors)) + for arg in self.data.args] + return u', '.join(args) + if isinstance(error, UnicodeDecodeError): + return unicode(self.data, self.encoding, self.decoding_errors) + raise + +class ErrorString(SafeString): + """ + Safely report exception type and message. + """ + def __str__(self): + return '%s: %s' % (self.data.__class__.__name__, + super(ErrorString, self).__str__()) + + def __unicode__(self): + return u'%s: %s' % (self.data.__class__.__name__, + super(ErrorString, self).__unicode__()) + + +class ErrorOutput(object): + """ + Wrapper class for file-like error streams with + failsave de- and encoding of `str`, `bytes`, `unicode` and + `Exception` instances. + """ + + def __init__(self, stream=None, encoding=None, + encoding_errors='backslashreplace', + decoding_errors='replace'): + """ + :Parameters: + - `stream`: a file-like object (which is written to), + a string (opended as a file), + `None` (bind to `sys.stderr`; default). + If evaluating to `False` (but not `None`), + write() requests are ignored. + - `encoding`: `stream` text encoding. Guessed if None. + - `encoding_errors`: how to treat encoding errors. + """ + if stream is None: + stream = sys.stderr + elif not(stream): + stream = False + # if `stream` is a file name, open it + elif isinstance(stream, str): + stream = open(stream, 'w') + elif isinstance(stream, unicode): + stream = open(stream.encode(sys.getfilesystemencoding()), 'w') + + self.stream = stream + """Where warning output is sent.""" + + self.encoding = (encoding or getattr(stream, 'encoding', None) or + locale_encoding or 'ascii') + """The output character encoding.""" + + self.encoding_errors = encoding_errors + """Encoding error handler.""" + + self.decoding_errors = decoding_errors + """Decoding error handler.""" + + def write(self, data): + """ + Write `data` to self.stream. Ignore, if self.stream is False. + + `data` can be a `string`, `unicode`, or `Exception` instance. + """ + if self.stream is False: + return + if isinstance(data, Exception): + data = unicode(SafeString(data, self.encoding, + self.encoding_errors, self.decoding_errors)) + try: + self.stream.write(data) + except UnicodeEncodeError: + self.stream.write(data.encode(self.encoding, self.encoding_errors)) + except TypeError: # in Python 3, stderr expects unicode + self.stream.write(unicode(data, self.encoding, self.decoding_errors)) + + def close(self): + """ + Close the error-output stream. + + Ignored if the stream has no close() method. + """ + try: + self.stream.close() + except AttributeError: + pass + diff --git a/docutils/docutils/frontend.py b/docutils/docutils/frontend.py index 0f78f18be..a5c1a9126 100644 --- a/docutils/docutils/frontend.py +++ b/docutils/docutils/frontend.py @@ -38,7 +38,7 @@ from optparse import SUPPRESS_HELP import docutils import docutils.utils import docutils.nodes -from docutils.io import locale_encoding, ErrorOutput +from docutils.error_reporting import locale_encoding, ErrorOutput def store_multiple(option, opt, value, parser, *args, **kwargs): @@ -276,8 +276,8 @@ class Option(optparse.Option): new_value = self.validator(setting, value, parser) except Exception, error: raise (optparse.OptionValueError( - 'Error in option "%s":\n %s: %s' - % (opt, error.__class__.__name__, error)), + 'Error in option "%s":\n %s' + % (opt, ErrorString(error))), None, sys.exc_info()[2]) setattr(values, setting, new_value) if self.overrides: @@ -753,9 +753,10 @@ Skipping "%s" configuration file. except Exception, error: raise (ValueError( 'Error in config file "%s", section "[%s]":\n' - ' %s: %s\n %s = %s' - % (filename, section, error.__class__.__name__, - error, setting, value)), None, sys.exc_info()[2]) + ' %s\n' + ' %s = %s' + % (filename, section, ErrorString(error), + setting, value)), None, sys.exc_info()[2]) self.set(section, setting, new_value) if option.overrides: self.set(section, option.overrides, None) diff --git a/docutils/docutils/io.py b/docutils/docutils/io.py index 796cb2288..e58f8e9e8 100644 --- a/docutils/docutils/io.py +++ b/docutils/docutils/io.py @@ -13,25 +13,8 @@ import sys import re import codecs from docutils import TransformSpec -from docutils._compat import b, bytes - - -# Guess the locale's encoding. -# If no valid guess can be made, locale_encoding is set to `None`: -try: - import locale # module missing in Jython -except ImportError: - locale_encoding = None -else: - locale_encoding = locale.getlocale()[1] or locale.getdefaultlocale()[1] - # locale.getpreferredencoding([do_setlocale=True|False]) - # has side-effects | might return a wrong guess. - # (cf. Update 1 in http://stackoverflow.com/questions/4082645/using-python-2-xs-locale-module-to-format-numbers-and-currency) - try: - codecs.lookup(locale_encoding or '') # None -> '' - except LookupError: - locale_encoding = None - +from docutils._compat import b +from docutils.error_reporting import locale_encoding, ErrorString, ErrorOutput class Input(TransformSpec): @@ -101,30 +84,23 @@ class Input(TransformSpec): # Apply heuristics only if no encoding is explicitly given and # no BOM found. Start with UTF-8, because that only matches # data that *IS* UTF-8: - encodings = ['utf-8', - locale_encoding, - 'latin-1', # fallback encoding - ] - error = None - error_details = '' + encodings = [enc for enc in ('utf-8', + locale_encoding, # can be None + 'latin-1') # fallback encoding + if enc] for enc in encodings: - if not enc: - continue try: decoded = unicode(data, enc, self.error_handler) self.successful_encoding = enc # Return decoded, removing BOMs. return decoded.replace(u'\ufeff', u'') - except (UnicodeError, LookupError), tmperror: - error = tmperror # working around Python 3 deleting the - # error variable after the except clause - if error is not None: - error_details = '\n(%s: %s)' % (error.__class__.__name__, error) + except (UnicodeError, LookupError), err: + error = err # in Python 3, the <exception instance> is + # local to the except clause raise UnicodeError( 'Unable to decode input data. Tried the following encodings: ' - '%s.%s' - % (', '.join([repr(enc) for enc in encodings if enc]), - error_details)) + '%s.\n(%s)' % (', '.join([repr(enc) for enc in encodings]), + ErrorString(error))) coding_slug = re.compile(b("coding[:=]\s*([-\w.]+)")) """Encoding declaration pattern.""" @@ -200,160 +176,6 @@ class Output(TransformSpec): else: return data.encode(self.encoding, self.error_handler) -# Robust error reporting -# ----------------------- -# -# Implicit conversions of strings and exceptions like -# -# >>> u'%s world' % 'H\xe4llo' -# -# fail in some Python versions: -# -# * In Python <= 2.6, ``unicode(<exception instance>)`` uses -# `__str__` and fails with non-ASCII chars in`unicode` arguments. -# (work around http://bugs.python.org/issue2517): -# -# * In Python 2, unicode(<exception instance>) fails, with non-ASCII -# chars in `str` arguments. (Use case: in some locales, the errstr -# argument of IOError contains non-ASCII chars.) -# -# * In Python 2, str(<exception instance>) fails, with non-ASCII chars -# in `unicode` arguments. -# -# However, when reporting an error we do not want to mask ist with -# encoding/decoding errors. The `ErrString` and `ErrorOutput` classes -# handle common exceptions: - -class ErrorString(object): - """ - A wrapper providing robust (bytes and unicode) string conversion. - """ - - def __init__(self, data, encoding=None, encoding_errors='backslashreplace', - decoding_errors='replace'): - self.data = data - self.encoding = (encoding or getattr(data, 'encoding', None) or - locale_encoding or 'ascii') - self.encoding_errors = encoding_errors - self.decoding_errors = decoding_errors - - - def __str__(self): - try: - return str(self.data) - except UnicodeEncodeError, err: - if isinstance(self.data, Exception): - args = [str(ErrorString(arg, self.encoding, - self.encoding_errors)) - for arg in self.data.args] - return ', '.join(args) - if isinstance(self.data, unicode): - return self.data.encode(self.encoding, self.encoding_errors) - raise - - def __unicode__(self): - """ - Return unicode representation of `self.data`. - - Try ``unicode(self.data)``, catch `UnicodeError` and - - * if `self.data` is an Exception instance, work around - http://bugs.python.org/issue2517 with an emulation of - Exception.__unicode__, - - * else decode with `self.encoding` and `self.decoding_errors`. - """ - try: - return unicode(self.data) - except UnicodeError, err: # can be ..EncodeError or ..DecodeError - if isinstance(self.data, IOError): - return u"[Errno %d] %s: '%s'" % (self.data.errno, - ErrorString(self.data.strerror, self.encoding, - self.decoding_errors), - ErrorString(self.data.filename, self.encoding, - self.decoding_errors)) - if isinstance(self.data, Exception): - args = [unicode(ErrorString(arg, self.encoding, - decoding_errors=self.decoding_errors)) - for arg in self.data.args] - return u', '.join(args) - if isinstance(err, UnicodeDecodeError): - return unicode(self.data, self.encoding, self.decoding_errors) - raise - - -class ErrorOutput(object): - """ - Wrapper class for file-like error streams with - failsave de- and encoding of `str`, `bytes`, `unicode` and - `Exception` instances. - """ - - def __init__(self, stream=None, encoding=None, - encoding_errors='backslashreplace', - decoding_errors='replace'): - """ - :Parameters: - - `stream`: a file-like object (which is written to), - a string (opended as a file), - `None` (bind to `sys.stderr`; default). - If evaluating to `False` (but not `None`), - write() requests are ignored. - - `encoding`: `stream` text encoding. Guessed if None. - - `encoding_errors`: how to treat encoding errors. - """ - if stream is None: - stream = sys.stderr - elif not(stream): - stream = False - # if `stream` is a file name, open it - elif isinstance(stream, bytes): - stream = open(stream, 'w') - elif isinstance(stream, unicode): - stream = open(stream.encode(sys.getfilesystemencoding()), 'w') - - self.stream = stream - """Where warning output is sent.""" - - self.encoding = (encoding or getattr(stream, 'encoding', None) or - locale_encoding or 'ascii') - """The output character encoding.""" - - self.encoding_errors = encoding_errors - """Encoding error handler.""" - - self.decoding_errors = decoding_errors - """Decoding error handler.""" - - def write(self, data): - """ - Write `data` to self.stream. Ignore, if self.stream is False. - - `data` can be a `string`, `unicode`, or `Exception` instance. - """ - if self.stream is False: - return - if isinstance(data, Exception): - data = unicode(ErrorString(data, self.encoding, - self.encoding_errors, self.decoding_errors)) - try: - self.stream.write(data) - except UnicodeEncodeError: - self.stream.write(data.encode(self.encoding, self.encoding_errors)) - except TypeError: # in Python 3, stderr expects unicode - self.stream.write(unicode(data, self.encoding, self.decoding_errors)) - - def close(self): - """ - Close the error-output stream. - - Ignored if the stream has no close() method. - """ - try: - self.stream.close() - except AttributeError: - pass - class FileInput(Input): @@ -396,8 +218,7 @@ class FileInput(Input): except IOError, error: if not handle_io_errors: raise - print >>self._stderr, '%s: %s' % ( - error.__class__.__name__, error) + print >>self._stderr, ErrorString(error) print >>self._stderr, (u'Unable to open source' u" file for reading ('%s'). Exiting." % source_path) sys.exit(1) @@ -489,7 +310,7 @@ class FileOutput(Output): except IOError, error: if not self.handle_io_errors: raise - print >>self._stderr, '%s: %s' % (error.__class__.__name__, error) + print >>self._stderr, ErrorString(error) print >>self._stderr, (u'Unable to open destination file' u" for writing ('%s'). Exiting." % self.destination_path) sys.exit(1) @@ -528,7 +349,7 @@ class BinaryFileOutput(FileOutput): except IOError, error: if not self.handle_io_errors: raise - print >>self._stderr, '%s: %s' % (error.__class__.__name__, error) + print >>self._stderr, ErrorString(error) print >>self._stderr, (u'Unable to open destination file' u" for writing ('%s'). Exiting." % self.destination_path) sys.exit(1) diff --git a/docutils/docutils/parsers/rst/directives/misc.py b/docutils/docutils/parsers/rst/directives/misc.py index 5cca85045..43f299bab 100644 --- a/docutils/docutils/parsers/rst/directives/misc.py +++ b/docutils/docutils/parsers/rst/directives/misc.py @@ -11,7 +11,7 @@ import os.path import re import time from docutils import io, nodes, statemachine, utils -from docutils.io import ErrorString +from docutils.error_reporting import SafeString, ErrorString from docutils.parsers.rst import Directive, convert_directive_function from docutils.parsers.rst import directives, roles, states from docutils.transforms import misc @@ -67,8 +67,8 @@ class Include(Directive): input_encoding_error_handler), handle_io_errors=None) except IOError, error: - raise self.severe(u'Problems with "%s" directive path:\n%s: %s.' % - (self.name, error.__class__.__name__, ErrorString(error))) + raise self.severe(u'Problems with "%s" directive path:\n%s.' % + (self.name, ErrorString(error))) startline = self.options.get('start-line', None) endline = self.options.get('end-line', None) try: @@ -78,9 +78,8 @@ class Include(Directive): else: rawtext = include_file.read() except UnicodeError, error: - raise self.severe( - 'Problem with "%s" directive:\n%s: %s' - % (self.name, error.__class__.__name__, error)) + raise self.severe(u'Problem with "%s" directive:\n%s' % + (self.name, ErrorString(error))) # start-after/end-before: no restrictions on newlines in match-text, # and no restrictions on matching inside lines vs. line boundaries after_text = self.options.get('start-after', None) @@ -167,14 +166,13 @@ class Raw(Directive): input_encoding_error_handler), handle_io_errors=None) except IOError, error: - raise self.severe('Problems with "%s" directive path:\n%s.' - % (self.name, error)) + raise self.severe(u'Problems with "%s" directive path:\n%s.' + % (self.name, ErrorString(error))) try: text = raw_file.read() except UnicodeError, error: - raise self.severe( - 'Problem with "%s" directive:\n%s: %s' - % (self.name, error.__class__.__name__, error)) + raise self.severe(u'Problem with "%s" directive:\n%s' + % (self.name, ErrorString(error))) attributes['source'] = path elif 'url' in self.options: source = self.options['url'] @@ -185,9 +183,8 @@ class Raw(Directive): try: raw_text = urllib2.urlopen(source).read() except (urllib2.URLError, IOError, OSError), error: - raise self.severe( - 'Problems with "%s" directive URL "%s":\n%s.' - % (self.name, self.options['url'], error)) + raise self.severe(u'Problems with "%s" directive URL "%s":\n%s.' + % (self.name, self.options['url'], ErrorString(error))) raw_file = io.StringInput( source=raw_text, source_path=source, encoding=encoding, error_handler=(self.state.document.settings.\ @@ -195,9 +192,8 @@ class Raw(Directive): try: text = raw_file.read() except UnicodeError, error: - raise self.severe( - 'Problem with "%s" directive:\n%s: %s' - % (self.name, error.__class__.__name__, error)) + raise self.severe(u'Problem with "%s" directive:\n%s' + % (self.name, ErrorString(error))) attributes['source'] = source else: # This will always fail because there is no content. @@ -275,10 +271,9 @@ class Unicode(Directive): for code in codes: try: decoded = directives.unicode_code(code) - except ValueError, err: - raise self.error( - 'Invalid character code: %s\n%s: %s' - % (code, err.__class__.__name__, err)) + except ValueError, error: + raise self.error(u'Invalid character code: %s\n%s' + % (code, ErrorString(error))) element += nodes.Text(decoded) return element.children @@ -373,8 +368,8 @@ class Role(Directive): options['class'] = directives.class_option(new_role_name) except ValueError, detail: error = self.state_machine.reporter.error( - 'Invalid argument for "%s" directive:\n%s.' - % (self.name, detail), nodes.literal_block( + u'Invalid argument for "%s" directive:\n%s.' + % (self.name, SafeString(detail)), nodes.literal_block( self.block_text, self.block_text), line=self.lineno) return messages + [error] role = roles.CustomRole(new_role_name, base_role, options, content) diff --git a/docutils/docutils/parsers/rst/directives/tables.py b/docutils/docutils/parsers/rst/directives/tables.py index 8beca5142..748557620 100644 --- a/docutils/docutils/parsers/rst/directives/tables.py +++ b/docutils/docutils/parsers/rst/directives/tables.py @@ -14,6 +14,7 @@ import os.path import csv from docutils import io, nodes, statemachine, utils +from docutils.error_reporting import SafeString from docutils.utils import SystemMessagePropagation from docutils.parsers.rst import Directive from docutils.parsers.rst import directives @@ -274,9 +275,10 @@ class CSVTable(Table): csv_data = csv_file.read().splitlines() except IOError, error: severe = self.state_machine.reporter.severe( - 'Problems with "%s" directive path:\n%s.' - % (self.name, error), nodes.literal_block( - self.block_text, self.block_text), line=self.lineno) + u'Problems with "%s" directive path:\n%s.' + % (self.name, SafeString(error)), + nodes.literal_block(self.block_text, self.block_text), + line=self.lineno) raise SystemMessagePropagation(severe) elif 'url' in self.options: # CSV data is from a URL. @@ -290,7 +292,7 @@ class CSVTable(Table): except (urllib2.URLError, IOError, OSError, ValueError), error: severe = self.state_machine.reporter.severe( 'Problems with "%s" directive URL "%s":\n%s.' - % (self.name, self.options['url'], error), + % (self.name, self.options['url'], SafeString(error)), nodes.literal_block(self.block_text, self.block_text), line=self.lineno) raise SystemMessagePropagation(severe) diff --git a/docutils/docutils/parsers/rst/states.py b/docutils/docutils/parsers/rst/states.py index ef9b9a0a4..b76cd48c3 100644 --- a/docutils/docutils/parsers/rst/states.py +++ b/docutils/docutils/parsers/rst/states.py @@ -1443,8 +1443,8 @@ class Body(RSTState): except MarkupError, error: # This shouldn't happen; pattern won't match. src, srcline = self.state_machine.get_source_and_line() - msg = self.reporter.error('Invalid option list marker: %s' % - str(error), source=src, line=srcline) + msg = self.reporter.error(u'Invalid option list marker: %s' % + error, source=src, line=srcline) self.parent += msg indented, indent, line_offset, blank_finish = \ self.state_machine.get_first_known_indented(match.end()) diff --git a/docutils/docutils/statemachine.py b/docutils/docutils/statemachine.py index decaaf17f..6c0c6fc03 100644 --- a/docutils/docutils/statemachine.py +++ b/docutils/docutils/statemachine.py @@ -110,7 +110,7 @@ import sys import re import types import unicodedata -from docutils.io import ErrorOutput +from docutils.error_reporting import ErrorOutput class StateMachine: diff --git a/docutils/docutils/utils.py b/docutils/docutils/utils.py index 950318146..fd9b0ec2e 100644 --- a/docutils/docutils/utils.py +++ b/docutils/docutils/utils.py @@ -15,8 +15,7 @@ import warnings import unicodedata from docutils import ApplicationError, DataError from docutils import nodes -from docutils.io import ErrorOutput -from docutils._compat import bytes +from docutils.error_reporting import ErrorOutput, SafeString class SystemMessage(ApplicationError): @@ -154,19 +153,8 @@ class Reporter: Raise an exception or generate a warning if appropriate. """ # `message` can be a `string`, `unicode`, or `Exception` instance. - # Convert now to detect errors: - try: - message = unicode(message) - except UnicodeError, err: - # In Python < 2.6, # unicode(<exception instance>) uses __str__ - # and fails with non-ASCII chars in arguments - if sys.version_info < (2,6): - try: - message = u', '.join(message.args) - except AttributeError: - raise err - else: - raise err + if isinstance(message, Exception): + message = SafeString(message) attributes = kwargs.copy() if 'base_node' in kwargs: diff --git a/docutils/test/test_error_reporting.py b/docutils/test/test_error_reporting.py new file mode 100644 index 000000000..61c832096 --- /dev/null +++ b/docutils/test/test_error_reporting.py @@ -0,0 +1,339 @@ +#! /usr/bin/env python +# .. coding: utf8 +# $Id$ +# Author: Günter Milde <milde@users.sourceforge.net> +# Copyright: This module has been placed in the public domain. + +""" +Test `EnvironmentError` reporting. + +In some locales, the `errstr` argument of IOError and OSError contains +non-ASCII chars. + +In Python 2, converting an exception instance to `str` or `unicode` +might fail, with non-ASCII chars in arguments and the default encoding +and errors ('ascii', 'strict'). + +Therefore, Docutils must not use string interpolation with exception +instances like, e.g., :: + + try: + something + except IOError, error: + print 'Found %s' % error + +unless the minimal required Python version has this problem fixed. +""" + +import unittest +import sys, os +import codecs +try: # from standard library module `io` + from io import StringIO, BytesIO +except ImportError: # new in Python 2.6 + from StringIO import StringIO + BytesIO = StringIO + +import DocutilsTestSupport # must be imported before docutils +from docutils import core, parsers, frontend, utils +from docutils.error_reporting import SafeString, ErrorString, ErrorOutput +from docutils._compat import b, bytes + +oldlocale = None +if sys.version_info < (3,0): # problems solved in py3k + try: + import locale # module missing in Jython + oldlocale = locale.getlocale() + # Why does getlocale return the defaultlocale in Python 3.2 ???? + # oldlocale = (None, None) # test suite runs without locale + except ImportError: + print ('cannot test error reporting with problematic locales,\n' + '`import locale` failed.') + + +# locales confirmed to use non-ASCII chars in the IOError message +# for a missing file (https://bugs.gentoo.org/show_bug.cgi?id=349101) +# TODO: add more confirmed problematic locales +problematic_locales = ['cs_CZ', 'cs_CZ.UTF8', + 'el_GR', 'el_GR.UTF-8', + # 'fr_FR.UTF-8', # only OSError + 'ja_JP.UTF-8', + 'ru_RU', 'ru_RU.KOI8-R', + 'ru_RU.UTF-8', + '', # default locale: might be non-problematic + ] + +if oldlocale is not None: + # find a supported problematic locale: + for testlocale in problematic_locales: + try: + locale.setlocale(locale.LC_ALL, testlocale) + except locale.Error: + testlocale = None + else: + break + locale.setlocale(locale.LC_ALL, oldlocale) # reset +else: + testlocale = None + +class SafeStringTests(unittest.TestCase): + # the error message in EnvironmentError instances comes from the OS + # and in some locales (e.g. ru_RU), contains high bit chars. + # -> see the test in test_error_reporting.py + + # test data: + bs = b('\xfc') # unicode(bs) fails, str(bs) in Python 3 return repr() + us = u'\xfc' # bytes(us) fails; str(us) fails in Python 2 + be = Exception(bs) # unicode(be) fails + ue = Exception(us) # bytes(ue) fails, str(ue) fails in Python 2; + # unicode(ue) fails in Python < 2.6 (issue2517_) + # .. _issue2517: http://bugs.python.org/issue2517 + # wrapped test data: + wbs = SafeString(bs) + wus = SafeString(us) + wbe = SafeString(be) + wue = SafeString(ue) + + def test_7bit(self): + # wrapping (not required with 7-bit chars) must not change the + # result of conversions: + bs7 = b('foo') + us7 = u'foo' + be7 = Exception(bs7) + ue7 = Exception(us7) + self.assertEqual(str(42), str(SafeString(42))) + self.assertEqual(str(bs7), str(SafeString(bs7))) + self.assertEqual(str(us7), str(SafeString(us7))) + self.assertEqual(str(be7), str(SafeString(be7))) + self.assertEqual(str(ue7), str(SafeString(ue7))) + self.assertEqual(unicode(7), unicode(SafeString(7))) + self.assertEqual(unicode(bs7), unicode(SafeString(bs7))) + self.assertEqual(unicode(us7), unicode(SafeString(us7))) + self.assertEqual(unicode(be7), unicode(SafeString(be7))) + self.assertEqual(unicode(ue7), unicode(SafeString(ue7))) + + def test_ustr(self): + """Test conversion to a unicode-string.""" + # unicode(self.bs) fails + self.assertEqual(unicode, type(unicode(self.wbs))) + self.assertEqual(unicode(self.us), unicode(self.wus)) + # unicode(self.be) fails + self.assertEqual(unicode, type(unicode(self.wbe))) + # unicode(ue) fails in Python < 2.6 (issue2517_) + self.assertEqual(unicode, type(unicode(self.wue))) + self.assertEqual(self.us, unicode(self.wue)) + + def test_str(self): + """Test conversion to a string (bytes in Python 2, unicode in Python 3).""" + self.assertEqual(str(self.bs), str(self.wbs)) + self.assertEqual(str(self.be), str(self.be)) + # str(us) fails in Python 2 + self.assertEqual(str, type(str(self.wus))) + # str(ue) fails in Python 2 + self.assertEqual(str, type(str(self.wue))) + + +class ErrorStringTests(unittest.TestCase): + bs = b('\xfc') # unicode(bs) fails, str(bs) in Python 3 return repr() + us = u'\xfc' # bytes(us) fails; str(us) fails in Python 2 + + def test_str(self): + self.assertEqual('Exception: spam', + str(ErrorString(Exception('spam')))) + self.assertEqual('IndexError: '+str(self.bs), + str(ErrorString(IndexError(self.bs)))) + self.assertEqual('ImportError: %s' % SafeString(self.us), + str(ErrorString(ImportError(self.us)))) + + def test_unicode(self): + self.assertEqual(u'Exception: spam', + unicode(ErrorString(Exception(u'spam')))) + self.assertEqual(u'IndexError: '+self.us, + unicode(ErrorString(IndexError(self.us)))) + self.assertEqual(u'ImportError: %s' % SafeString(self.bs), + unicode(ErrorString(ImportError(self.bs)))) + + +# ErrorOutput tests +# ----------------- + +# Stub: Buffer with 'strict' auto-conversion of input to byte string: +class BBuf(BytesIO, object): + def write(self, data): + if isinstance(data, unicode): + data.encode('ascii', 'strict') + super(BBuf, self).write(data) + +# Stub: Buffer expecting unicode string: +class UBuf(StringIO, object): + def write(self, data): + # emulate Python 3 handling of stdout, stderr + if isinstance(data, bytes): + raise TypeError('must be unicode, not bytes') + super(UBuf, self).write(data) + +class ErrorOutputTests(unittest.TestCase): + def test_defaults(self): + e = ErrorOutput() + self.assertEquals(e.stream, sys.stderr) + + def test_bbuf(self): + buf = BBuf() # buffer storing byte string + e = ErrorOutput(buf, encoding='ascii') + # write byte-string as-is + e.write(b('b\xfc')) + self.assertEquals(buf.getvalue(), b('b\xfc')) + # encode unicode data with backslashescape fallback replacement: + e.write(u' u\xfc') + self.assertEquals(buf.getvalue(), b('b\xfc u\\xfc')) + # handle Exceptions with Unicode string args + # unicode(Exception(u'e\xfc')) # fails in Python < 2.6 + e.write(AttributeError(u' e\xfc')) + self.assertEquals(buf.getvalue(), b('b\xfc u\\xfc e\\xfc')) + # encode with `encoding` attribute + e.encoding = 'utf8' + e.write(u' u\xfc') + self.assertEquals(buf.getvalue(), b('b\xfc u\\xfc e\\xfc u\xc3\xbc')) + + def test_ubuf(self): + buf = UBuf() # buffer only accepting unicode string + # decode of binary strings + e = ErrorOutput(buf, encoding='ascii') + e.write(b('b\xfc')) + self.assertEquals(buf.getvalue(), u'b\ufffd') # use REPLACEMENT CHARACTER + # write Unicode string and Exceptions with Unicode args + e.write(u' u\xfc') + self.assertEquals(buf.getvalue(), u'b\ufffd u\xfc') + e.write(AttributeError(u' e\xfc')) + self.assertEquals(buf.getvalue(), u'b\ufffd u\xfc e\xfc') + # decode with `encoding` attribute + e.encoding = 'latin1' + e.write(b(' b\xfc')) + self.assertEquals(buf.getvalue(), u'b\ufffd u\xfc e\xfc b\xfc') + + + +class SafeStringTests_locale(unittest.TestCase): + """ + Test docutils.SafeString with 'problematic' locales. + + The error message in `EnvironmentError` instances comes from the OS + and in some locales (e.g. ru_RU), contains high bit chars. + """ + if testlocale: + locale.setlocale(locale.LC_ALL, testlocale) + # test data: + bs = b('\xfc') + us = u'\xfc' + try: + open(b('\xfc')) + except IOError, e: # in Python 3 the name for the exception instance + bioe = e # is local to the except clause + try: + open(u'\xfc') + except IOError, e: + uioe = e + except UnicodeEncodeError: + try: + open(u'\xfc'.encode(sys.getfilesystemencoding(), 'replace')) + except IOError: + uioe = e + try: + os.chdir(b('\xfc')) + except OSError, e: + bose = e + try: + os.chdir(u'\xfc') + except OSError, e: + uose = e + except UnicodeEncodeError: + try: + os.chdir(u'\xfc'.encode(sys.getfilesystemencoding(), 'replace')) + except OSError: + uose = e + # wrapped test data: + wbioe = SafeString(bioe) + wuioe = SafeString(uioe) + wbose = SafeString(bose) + wuose = SafeString(uose) + # reset locale + if testlocale: + locale.setlocale(locale.LC_ALL, oldlocale) + + def test_ustr(self): + """Test conversion to a unicode-string.""" + # unicode(bioe) fails with e.g. 'ru_RU.utf8' locale + self.assertEqual(unicode, type(unicode(self.wbioe))) + self.assertEqual(unicode, type(unicode(self.wuioe))) + self.assertEqual(unicode, type(unicode(self.wbose))) + self.assertEqual(unicode, type(unicode(self.wuose))) + + def test_str(self): + """Test conversion to a string (bytes in Python 2, unicode in Python 3).""" + self.assertEqual(str(self.bioe), str(self.wbioe)) + self.assertEqual(str(self.uioe), str(self.wuioe)) + self.assertEqual(str(self.bose), str(self.wbose)) + self.assertEqual(str(self.uose), str(self.wuose)) + + + +class ErrorReportingTests(unittest.TestCase): + """ + Test cases where error reporting can go wrong. + + Do not test the exact output (as this varies with the locale), just + ensure that the correct exception is thrown. + """ + + # These tests fail with a 'problematic locale' and + # (revision < 7035) and Python-2. + + parser = parsers.rst.Parser() + """Parser shared by all ParserTestCases.""" + + option_parser = frontend.OptionParser(components=(parsers.rst.Parser,)) + settings = option_parser.get_default_values() + settings.report_level = 1 + settings.halt_level = 1 + settings.warning_stream = '' + document = utils.new_document('test data', settings) + + def setUp(self): + if testlocale: + locale.setlocale(locale.LC_ALL, testlocale) + + def tearDown(self): + if testlocale: + locale.setlocale(locale.LC_ALL, oldlocale) + + def test_include(self): + source = ('.. include:: bogus.txt') + self.assertRaises(utils.SystemMessage, + self.parser.parse, source, self.document) + + def test_raw_file(self): + source = ('.. raw:: html\n' + ' :file: bogus.html\n') + self.assertRaises(utils.SystemMessage, + self.parser.parse, source, self.document) + + def test_raw_url(self): + source = ('.. raw:: html\n' + ' :url: http://bogus.html\n') + self.assertRaises(utils.SystemMessage, + self.parser.parse, source, self.document) + + def test_csv_table(self): + source = ('.. csv-table:: external file\n' + ' :file: bogus.csv\n') + self.assertRaises(utils.SystemMessage, + self.parser.parse, source, self.document) + + def test_csv_table_url(self): + source = ('.. csv-table:: external URL\n' + ' :url: ftp://bogus.csv\n') + self.assertRaises(utils.SystemMessage, + self.parser.parse, source, self.document) + +if __name__ == '__main__': + unittest.main() diff --git a/docutils/test/test_io.py b/docutils/test/test_io.py index 3b83c1391..5cf44f439 100755 --- a/docutils/test/test_io.py +++ b/docutils/test/test_io.py @@ -8,18 +8,7 @@ Test module for io.py. """ -try: - import locale -except ImportError: - locale = None - import unittest, sys -try: # from standard library module `io` - from io import StringIO, BytesIO -except ImportError: # new in Python 2.6 - from StringIO import StringIO - BytesIO = StringIO - import DocutilsTestSupport # must be imported before docutils from docutils import io from docutils._compat import b, bytes @@ -74,147 +63,5 @@ print "hello world" self.assertEquals(input.successful_encoding, 'utf-8') -class ErrorStringTests(unittest.TestCase): - # test data: - bs = b('\xfc') # unicode(bs) fails, str(bs) in Python 3 return repr() - us = u'\xfc' # bytes(us) fails; str(us) fails in Python 2 - be = Exception(bs) # unicode(be) fails - ue = Exception(us) # bytes(ue) fails, str(ue) fails in Python 2; - # unicode(ue) fails in Python < 2.6 (issue2517_) - # the error message in IOError comes from the OS, and in some locales - # (e.g. ru_RU), contains high bit chars: - if locale: - try: - locale.setlocale(locale.LC_ALL, '') - except locale.Error: - print 'cannot test locale' - try: - open(b('\xfc')) - except IOError, e: # in Python >= 3.2, the name for the exception instance - bioe = e # is local to the except clause - try: - open(u'\xfc') - except IOError, e: - uioe = e - except UnicodeEncodeError: - try: - open(u'\xfc'.encode(sys.getfilesystemencoding(), 'replace')) - except IOError: - uioe = e - if locale: - locale.setlocale(locale.LC_ALL, 'C') # reset - # wrapped test data: - wbs = io.ErrorString(bs) - wus = io.ErrorString(us) - wbe = io.ErrorString(be) - wue = io.ErrorString(ue) - wbioe = io.ErrorString(bioe) - wuioe = io.ErrorString(uioe) - - def test_7bit(self): - # wrapping (not required with 7-bit chars) must not change the - # result of conversions: - bs = b('foo') - us = u'foo' - be = Exception(bs) - ue = Exception(us) - self.assertEqual(str(7), str(io.ErrorString(7))) - self.assertEqual(str(bs), str(io.ErrorString(bs))) - self.assertEqual(str(us), str(io.ErrorString(us))) - self.assertEqual(str(be), str(io.ErrorString(be))) - self.assertEqual(str(ue), str(io.ErrorString(ue))) - self.assertEqual(unicode(7), unicode(io.ErrorString(7))) - self.assertEqual(unicode(bs), unicode(io.ErrorString(bs))) - self.assertEqual(unicode(us), unicode(io.ErrorString(us))) - self.assertEqual(unicode(be), unicode(io.ErrorString(be))) - self.assertEqual(unicode(ue), unicode(io.ErrorString(ue))) - - def test_ustr(self): - """Test conversion to a unicode-string.""" - # unicode(self.bs) fails - self.assertEqual(unicode, type(unicode(self.wbs))) - self.assertEqual(unicode(self.us), unicode(self.wus)) - # unicode(self.be) fails - self.assertEqual(unicode, type(unicode(self.wbe))) - # unicode(ue) fails in Python < 2.6 (issue2517_) - self.assertEqual(unicode, type(unicode(self.wue))) - self.assertEqual(self.us, unicode(self.wue)) - # unicode(bioe) fails with e.g. 'ru_RU.utf8' locale - self.assertEqual(unicode, type(unicode(self.wbioe))) - self.assertEqual(unicode, type(unicode(self.wuioe))) - - def test_str(self): - """Test conversion to a string (bytes in Python 2, unicode in Python 3).""" - self.assertEqual(str(self.bs), str(self.wbs)) - self.assertEqual(str(self.be), str(self.be)) - # str(us) fails in Python 2 - self.assertEqual(str, type(str(self.wus))) - # str(ue) fails in Python 2 - self.assertEqual(str, type(str(self.wue))) - self.assertEqual(str(self.bioe), str(self.wbioe)) - self.assertEqual(str(self.uioe), str(self.wuioe)) - - -# .. _issue2517: http://bugs.python.org/issue2517 - - -# ErrorOutput tests -# ----------------- - -# Stub: Buffer with 'strict' auto-conversion of input to byte string: -class BBuf(BytesIO, object): - def write(self, data): - if isinstance(data, unicode): - data.encode('ascii', 'strict') - super(BBuf, self).write(data) - -# Stub: Buffer expecting unicode string: -class UBuf(StringIO, object): - def write(self, data): - # emulate Python 3 handling of stdout, stderr - if isinstance(data, bytes): - raise TypeError('must be unicode, not bytes') - super(UBuf, self).write(data) - -class ErrorOutputTests(unittest.TestCase): - def test_defaults(self): - e = io.ErrorOutput() - self.assertEquals(e.stream, sys.stderr) - - def test_bbuf(self): - buf = BBuf() # buffer storing byte string - e = io.ErrorOutput(buf, encoding='ascii') - # write byte-string as-is - e.write(b('b\xfc')) - self.assertEquals(buf.getvalue(), b('b\xfc')) - # encode unicode data with backslashescape fallback replacement: - e.write(u' u\xfc') - self.assertEquals(buf.getvalue(), b('b\xfc u\\xfc')) - # handle Exceptions with Unicode string args - # unicode(Exception(u'e\xfc')) # fails in Python < 2.6 - e.write(AttributeError(u' e\xfc')) - self.assertEquals(buf.getvalue(), b('b\xfc u\\xfc e\\xfc')) - # encode with `encoding` attribute - e.encoding = 'utf8' - e.write(u' u\xfc') - self.assertEquals(buf.getvalue(), b('b\xfc u\\xfc e\\xfc u\xc3\xbc')) - - def test_ubuf(self): - buf = UBuf() # buffer only accepting unicode string - # decode of binary strings - e = io.ErrorOutput(buf, encoding='ascii') - e.write(b('b\xfc')) - self.assertEquals(buf.getvalue(), u'b\ufffd') # use REPLACEMENT CHARACTER - # write Unicode string and Exceptions with Unicode args - e.write(u' u\xfc') - self.assertEquals(buf.getvalue(), u'b\ufffd u\xfc') - e.write(AttributeError(u' e\xfc')) - self.assertEquals(buf.getvalue(), u'b\ufffd u\xfc e\xfc') - # decode with `encoding` attribute - e.encoding = 'latin1' - e.write(b(' b\xfc')) - self.assertEquals(buf.getvalue(), u'b\ufffd u\xfc e\xfc b\xfc') - - if __name__ == '__main__': unittest.main() diff --git a/docutils/test/test_parsers/test_rst/test_directives/test_raw.py b/docutils/test/test_parsers/test_rst/test_directives/test_raw.py index fa7808112..680da5045 100755 --- a/docutils/test/test_parsers/test_rst/test_directives/test_raw.py +++ b/docutils/test/test_parsers/test_rst/test_directives/test_raw.py @@ -158,7 +158,7 @@ Raw input file is UTF-16-encoded, and is not valid ASCII. <system_message level="4" line="1" source="test data" type="SEVERE"> <paragraph> Problems with "raw" directive path: - [Errno 2] No such file or directory: %s'non-existent.file'. + IOError: [Errno 2] No such file or directory: %s'non-existent.file'. <literal_block xml:space="preserve"> .. raw:: html :file: non-existent.file diff --git a/docutils/tools/buildhtml.py b/docutils/tools/buildhtml.py index 52d6d2458..807ef0109 100755 --- a/docutils/tools/buildhtml.py +++ b/docutils/tools/buildhtml.py @@ -28,7 +28,8 @@ import copy from fnmatch import fnmatch import docutils from docutils import ApplicationError -from docutils import core, frontend, utils, io +from docutils import core, frontend, utils +from docutils.error_reporting import ErrorOutput, ErrorString from docutils.parsers import rst from docutils.readers import standalone, pep from docutils.writers import html4css1, pep_html @@ -194,15 +195,15 @@ class Builder: def visit(self, directory, names): # BUG prune and ignore do not work settings = self.get_settings('', directory) - stderr = io.ErrorOutput(encoding=settings.error_encoding) + errout = ErrorOutput(encoding=settings.error_encoding) if settings.prune and (os.path.abspath(directory) in settings.prune): - stderr.write('/// ...Skipping directory (pruned): %s\n' % - directory) + print >>errout, ('/// ...Skipping directory (pruned): %s' % + directory) sys.stderr.flush() names[:] = [] return if not self.initial_settings.silent: - stderr.write('/// Processing directory: %s\n' % directory) + print >>errout, '/// Processing directory: %s' % directory sys.stderr.flush() # settings.ignore grows many duplicate entries as we recurse # if we add patterns in config files or on the command line. @@ -224,14 +225,14 @@ class Builder: else: publisher = '.txt' settings = self.get_settings(publisher, directory) - stderr = io.ErrorOutput(encoding=settings.error_encoding) + errout = ErrorOutput(encoding=settings.error_encoding) pub_struct = self.publishers[publisher] if settings.prune and (directory in settings.prune): return 1 settings._source = os.path.normpath(os.path.join(directory, name)) settings._destination = settings._source[:-4]+'.html' if not self.initial_settings.silent: - stderr.write(' ::: Processing: %s\n'% name) + print >>errout, ' ::: Processing: %s' % name sys.stderr.flush() try: if not settings.dry_run: @@ -242,8 +243,7 @@ class Builder: writer_name=pub_struct.writer_name, settings=settings) except ApplicationError, error: - stderr.write(' Error (%s): %s' % - (error.__class__.__name__, error)) + print >>errout, ' %s' % ErrorString(error) if __name__ == "__main__": |
