1 # Copyright (c) 2008-2011 testtools developers. See LICENSE for details.
3 """Compatibility support for python 2 and 3."""
18 'unicode_output_stream',
29 from testtools.helpers import try_imports
31 BytesIO = try_imports(['StringIO.StringIO', 'io.BytesIO'])
32 StringIO = try_imports(['StringIO.StringIO', 'io.StringIO'])
35 from testtools import _compat2x as _compat
38 from testtools import _compat3x as _compat
40 reraise = _compat.reraise
43 __u_doc = """A function version of the 'u' prefix.
45 This is needed becayse the u prefix is not usable in Python 3 but is required
46 in Python 2 to get a unicode object.
48 To migrate code that was written as u'\u1234' in Python 2 to 2+3 change
49 it to be _u('\u1234'). The Python 3 interpreter will decode it
50 appropriately and the no-op _u for Python 3 lets it through, in Python
51 2 we then call unicode-escape in the _u function.
54 if sys.version_info > (3, 0):
60 return s.encode("latin-1")
61 advance_iterator = next
63 return isinstance(x, str)
69 # The double replace mangling going on prepares the string for
70 # unicode-escape - \foo is preserved, \u and \U are decoded.
71 return (s.replace("\\", "\\\\").replace("\\\\u", "\\u")
72 .replace("\\\\U", "\\U").decode("unicode-escape"))
76 advance_iterator = lambda it: it.next()
78 return isinstance(x, basestring)
81 return (type, types.ClassType)
82 str_is_unicode = sys.platform == "cli"
87 if sys.version_info > (2, 5):
89 _error_repr = BaseException.__repr__
90 def isbaseexception(exception):
91 """Return whether exception inherits from BaseException only"""
92 return (isinstance(exception, BaseException)
93 and not isinstance(exception, Exception))
96 """If contents of iterable all evaluate as boolean True"""
101 def _error_repr(exception):
102 """Format an exception instance as Python 2.5 and later do"""
103 return exception.__class__.__name__ + repr(exception.args)
104 def isbaseexception(exception):
105 """Return whether exception would inherit from BaseException only
107 This approximates the hierarchy in Python 2.5 and later, compare the
108 difference between the diagrams at the bottom of the pages:
109 <http://docs.python.org/release/2.4.4/lib/module-exceptions.html>
110 <http://docs.python.org/release/2.5.4/lib/module-exceptions.html>
112 return isinstance(exception, (KeyboardInterrupt, SystemExit))
115 def unicode_output_stream(stream):
116 """Get wrapper for given stream that writes any unicode without exception
118 Characters that can't be coerced to the encoding of the stream, or 'ascii'
119 if valid encoding is not found, will be replaced. The original stream may
120 be returned in situations where a wrapper is determined unneeded.
122 The wrapper only allows unicode to be written, not non-ascii bytestrings,
123 which is a good thing to ensure sanity and sanitation.
125 if sys.platform == "cli":
126 # Best to never encode before writing in IronPython
129 writer = codecs.getwriter(stream.encoding or "")
130 except (AttributeError, LookupError):
131 # GZ 2010-06-16: Python 3 StringIO ends up here, but probably needs
132 # different handling as it doesn't want bytestrings
133 return codecs.getwriter("ascii")(stream, "replace")
134 if writer.__module__.rsplit(".", 1)[1].startswith("utf"):
135 # The current stream has a unicode encoding so no error handler is needed
136 if sys.version_info > (3, 0):
138 return writer(stream)
139 if sys.version_info > (3, 0):
140 # Python 3 doesn't seem to make this easy, handle a common case
142 return stream.__class__(stream.buffer, stream.encoding, "replace",
143 stream.newlines, stream.line_buffering)
144 except AttributeError:
146 return writer(stream, "replace")
149 # The default source encoding is actually "iso-8859-1" until Python 2.5 but
150 # using non-ascii causes a deprecation warning in 2.4 and it's cleaner to
151 # treat all versions the same way
152 _default_source_encoding = "ascii"
154 # Pattern specified in <http://www.python.org/dev/peps/pep-0263/>
155 _cookie_search=re.compile("coding[:=]\s*([-\w.]+)").search
157 def _detect_encoding(lines):
158 """Get the encoding of a Python source file from a list of lines as bytes
160 This function does less than tokenize.detect_encoding added in Python 3 as
161 it does not attempt to raise a SyntaxError when the interpreter would, it
162 just wants the encoding of a source file Python has already compiled and
166 return _default_source_encoding
167 if lines[0].startswith("\xef\xbb\xbf"):
168 # Source starting with UTF-8 BOM is either UTF-8 or a SyntaxError
170 # Only the first two lines of the source file are examined
171 magic = _cookie_search("".join(lines[:2]))
173 return _default_source_encoding
174 encoding = magic.group(1)
176 codecs.lookup(encoding)
178 # Some codecs raise something other than LookupError if they don't
179 # support the given error handler, but not the text ones that could
180 # actually be used for Python source code
181 return _default_source_encoding
185 class _EncodingTuple(tuple):
186 """A tuple type that can have an encoding attribute smuggled on"""
189 def _get_source_encoding(filename):
190 """Detect, cache and return the encoding of Python source at filename"""
192 return linecache.cache[filename].encoding
193 except (AttributeError, KeyError):
194 encoding = _detect_encoding(linecache.getlines(filename))
195 if filename in linecache.cache:
196 newtuple = _EncodingTuple(linecache.cache[filename])
197 newtuple.encoding = encoding
198 linecache.cache[filename] = newtuple
202 def _get_exception_encoding():
203 """Return the encoding we expect messages from the OS to be encoded in"""
205 # GZ 2010-05-24: Really want the codepage number instead, the error
206 # handling of standard codecs is more deterministic
208 # GZ 2010-05-23: We need this call to be after initialisation, but there's
209 # no benefit in asking more than once as it's a global
210 # setting that can change after the message is formatted.
211 return locale.getlocale(locale.LC_MESSAGES)[1] or "ascii"
214 def _exception_to_text(evalue):
215 """Try hard to get a sensible text value out of an exception instance"""
217 return unicode(evalue)
218 except KeyboardInterrupt:
221 # Apparently this is what traceback._some_str does. Sigh - RBC 20100623
224 return str(evalue).decode(_get_exception_encoding(), "replace")
225 except KeyboardInterrupt:
228 # Apparently this is what traceback._some_str does. Sigh - RBC 20100623
230 # Okay, out of ideas, let higher level handle it
234 # GZ 2010-05-23: This function is huge and horrible and I welcome suggestions
235 # on the best way to break it up
236 _TB_HEADER = _u('Traceback (most recent call last):\n')
237 def _format_exc_info(eclass, evalue, tb, limit=None):
238 """Format a stack trace and the exception information as unicode
240 Compatibility function for Python 2 which ensures each component of a
241 traceback is correctly decoded according to its origins.
243 Based on traceback.format_exception and related functions.
245 fs_enc = sys.getfilesystemencoding()
249 for filename, lineno, name, line in traceback.extract_tb(tb, limit):
250 extracted_list.append((
251 filename.decode(fs_enc, "replace"),
253 name.decode("ascii", "replace"),
254 line and line.decode(
255 _get_source_encoding(filename), "replace")))
256 list.extend(traceback.format_list(extracted_list))
260 # Is a (deprecated) string exception
261 list.append((eclass + "\n").decode("ascii", "replace"))
263 if isinstance(evalue, SyntaxError):
264 # Avoid duplicating the special formatting for SyntaxError here,
265 # instead create a new instance with unicode filename and line
266 # Potentially gives duff spacing, but that's a pre-existing issue
268 msg, (filename, lineno, offset, line) = evalue
269 except (TypeError, ValueError):
270 pass # Strange exception instance, fall through to generic code
272 # Errors during parsing give the line from buffer encoded as
273 # latin-1 or utf-8 or the encoding of the file depending on the
274 # coding and whether the patch for issue #1031213 is applied, so
275 # give up on trying to decode it and just read the file again
277 bytestr = linecache.getline(filename, lineno)
279 if lineno == 1 and bytestr.startswith("\xef\xbb\xbf"):
280 bytestr = bytestr[3:]
281 line = bytestr.decode(
282 _get_source_encoding(filename), "replace")
283 del linecache.cache[filename]
285 line = line.decode("ascii", "replace")
287 filename = filename.decode(fs_enc, "replace")
288 evalue = eclass(msg, (filename, lineno, offset, line))
289 list.extend(traceback.format_exception_only(eclass, evalue))
291 sclass = eclass.__name__
292 svalue = _exception_to_text(evalue)
294 list.append("%s: %s\n" % (sclass, svalue))
296 # GZ 2010-05-24: Not a great fallback message, but keep for the moment
297 list.append("%s: <unprintable %s object>\n" % (sclass, sclass))
299 list.append("%s\n" % sclass)