1 # Copyright (c) 2008-2010 testtools developers. See LICENSE for details.
3 """Compatibility support for python 2 and 3."""
20 'unicode_output_stream',
24 __u_doc = """A function version of the 'u' prefix.
26 This is needed becayse the u prefix is not usable in Python 3 but is required
27 in Python 2 to get a unicode object.
29 To migrate code that was written as u'\u1234' in Python 2 to 2+3 change
30 it to be _u('\u1234'). The Python 3 interpreter will decode it
31 appropriately and the no-op _u for Python 3 lets it through, in Python
32 2 we then call unicode-escape in the _u function.
35 if sys.version_info > (3, 0):
41 return s.encode("latin-1")
42 advance_iterator = next
44 return isinstance(x, str)
50 # The double replace mangling going on prepares the string for
51 # unicode-escape - \foo is preserved, \u and \U are decoded.
52 return (s.replace("\\", "\\\\").replace("\\\\u", "\\u")
53 .replace("\\\\U", "\\U").decode("unicode-escape"))
57 advance_iterator = lambda it: it.next()
59 return isinstance(x, basestring)
62 return (type, types.ClassType)
63 str_is_unicode = sys.platform == "cli"
68 def unicode_output_stream(stream):
69 """Get wrapper for given stream that writes any unicode without exception
71 Characters that can't be coerced to the encoding of the stream, or 'ascii'
72 if valid encoding is not found, will be replaced. The original stream may
73 be returned in situations where a wrapper is determined unneeded.
75 The wrapper only allows unicode to be written, not non-ascii bytestrings,
76 which is a good thing to ensure sanity and sanitation.
78 if sys.platform == "cli":
79 # Best to never encode before writing in IronPython
82 writer = codecs.getwriter(stream.encoding or "")
83 except (AttributeError, LookupError):
84 # GZ 2010-06-16: Python 3 StringIO ends up here, but probably needs
85 # different handling as it doesn't want bytestrings
86 return codecs.getwriter("ascii")(stream, "replace")
87 if writer.__module__.rsplit(".", 1)[1].startswith("utf"):
88 # The current stream has a unicode encoding so no error handler is needed
90 if sys.version_info > (3, 0):
91 # Python 3 doesn't seem to make this easy, handle a common case
93 return stream.__class__(stream.buffer, stream.encoding, "replace",
94 stream.newlines, stream.line_buffering)
95 except AttributeError:
97 return writer(stream, "replace")
100 # The default source encoding is actually "iso-8859-1" until Python 2.5 but
101 # using non-ascii causes a deprecation warning in 2.4 and it's cleaner to
102 # treat all versions the same way
103 _default_source_encoding = "ascii"
105 # Pattern specified in <http://www.python.org/dev/peps/pep-0263/>
106 _cookie_search=re.compile("coding[:=]\s*([-\w.]+)").search
108 def _detect_encoding(lines):
109 """Get the encoding of a Python source file from a list of lines as bytes
111 This function does less than tokenize.detect_encoding added in Python 3 as
112 it does not attempt to raise a SyntaxError when the interpreter would, it
113 just wants the encoding of a source file Python has already compiled and
117 return _default_source_encoding
118 if lines[0].startswith("\xef\xbb\xbf"):
119 # Source starting with UTF-8 BOM is either UTF-8 or a SyntaxError
121 # Only the first two lines of the source file are examined
122 magic = _cookie_search("".join(lines[:2]))
124 return _default_source_encoding
125 encoding = magic.group(1)
127 codecs.lookup(encoding)
129 # Some codecs raise something other than LookupError if they don't
130 # support the given error handler, but not the text ones that could
131 # actually be used for Python source code
132 return _default_source_encoding
136 class _EncodingTuple(tuple):
137 """A tuple type that can have an encoding attribute smuggled on"""
140 def _get_source_encoding(filename):
141 """Detect, cache and return the encoding of Python source at filename"""
143 return linecache.cache[filename].encoding
144 except (AttributeError, KeyError):
145 encoding = _detect_encoding(linecache.getlines(filename))
146 if filename in linecache.cache:
147 newtuple = _EncodingTuple(linecache.cache[filename])
148 newtuple.encoding = encoding
149 linecache.cache[filename] = newtuple
153 def _get_exception_encoding():
154 """Return the encoding we expect messages from the OS to be encoded in"""
156 # GZ 2010-05-24: Really want the codepage number instead, the error
157 # handling of standard codecs is more deterministic
159 # GZ 2010-05-23: We need this call to be after initialisation, but there's
160 # no benefit in asking more than once as it's a global
161 # setting that can change after the message is formatted.
162 return locale.getlocale(locale.LC_MESSAGES)[1] or "ascii"
165 def _exception_to_text(evalue):
166 """Try hard to get a sensible text value out of an exception instance"""
168 return unicode(evalue)
169 except KeyboardInterrupt:
172 # Apparently this is what traceback._some_str does. Sigh - RBC 20100623
175 return str(evalue).decode(_get_exception_encoding(), "replace")
176 except KeyboardInterrupt:
179 # Apparently this is what traceback._some_str does. Sigh - RBC 20100623
181 # Okay, out of ideas, let higher level handle it
185 # GZ 2010-05-23: This function is huge and horrible and I welcome suggestions
186 # on the best way to break it up
187 _TB_HEADER = _u('Traceback (most recent call last):\n')
188 def _format_exc_info(eclass, evalue, tb, limit=None):
189 """Format a stack trace and the exception information as unicode
191 Compatibility function for Python 2 which ensures each component of a
192 traceback is correctly decoded according to its origins.
194 Based on traceback.format_exception and related functions.
196 fs_enc = sys.getfilesystemencoding()
200 for filename, lineno, name, line in traceback.extract_tb(tb, limit):
201 extracted_list.append((
202 filename.decode(fs_enc, "replace"),
204 name.decode("ascii", "replace"),
205 line and line.decode(
206 _get_source_encoding(filename), "replace")))
207 list.extend(traceback.format_list(extracted_list))
211 # Is a (deprecated) string exception
212 list.append((eclass + "\n").decode("ascii", "replace"))
214 if isinstance(evalue, SyntaxError):
215 # Avoid duplicating the special formatting for SyntaxError here,
216 # instead create a new instance with unicode filename and line
217 # Potentially gives duff spacing, but that's a pre-existing issue
219 msg, (filename, lineno, offset, line) = evalue
220 except (TypeError, ValueError):
221 pass # Strange exception instance, fall through to generic code
223 # Errors during parsing give the line from buffer encoded as
224 # latin-1 or utf-8 or the encoding of the file depending on the
225 # coding and whether the patch for issue #1031213 is applied, so
226 # give up on trying to decode it and just read the file again
228 bytestr = linecache.getline(filename, lineno)
230 if lineno == 1 and bytestr.startswith("\xef\xbb\xbf"):
231 bytestr = bytestr[3:]
232 line = bytestr.decode(
233 _get_source_encoding(filename), "replace")
234 del linecache.cache[filename]
236 line = line.decode("ascii", "replace")
238 filename = filename.decode(fs_enc, "replace")
239 evalue = eclass(msg, (filename, lineno, offset, line))
240 list.extend(traceback.format_exception_only(eclass, evalue))
242 sclass = eclass.__name__
243 svalue = _exception_to_text(evalue)
245 list.append("%s: %s\n" % (sclass, svalue))
247 # GZ 2010-05-24: Not a great fallback message, but keep for the moment
248 list.append("%s: <unprintable %s object>\n" % (sclass, sclass))
250 list.append("%s\n" % sclass)