testtools: Import new upstream snapshot.
[obnox/samba/samba-obnox.git] / lib / testtools / testtools / compat.py
1 # Copyright (c) 2008-2010 testtools developers. See LICENSE for details.
2
3 """Compatibility support for python 2 and 3."""
4
5
6 import codecs
7 import linecache
8 import locale
9 import os
10 import re
11 import sys
12 import traceback
13
14 __metaclass__ = type
15 __all__ = [
16     '_b',
17     '_u',
18     'advance_iterator',
19     'str_is_unicode',
20     'unicode_output_stream',
21     ]
22
23
24 __u_doc = """A function version of the 'u' prefix.
25
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.
28
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.
33 """
34
35 if sys.version_info > (3, 0):
36     def _u(s):
37         return s
38     _r = ascii
39     def _b(s):
40         """A byte literal."""
41         return s.encode("latin-1")
42     advance_iterator = next
43     def istext(x):
44         return isinstance(x, str)
45     def classtypes():
46         return (type,)
47     str_is_unicode = True
48 else:
49     def _u(s):
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"))
54     _r = repr
55     def _b(s):
56         return s
57     advance_iterator = lambda it: it.next()
58     def istext(x):
59         return isinstance(x, basestring)
60     def classtypes():
61         import types
62         return (type, types.ClassType)
63     str_is_unicode = sys.platform == "cli"
64
65 _u.__doc__ = __u_doc
66
67
68 def unicode_output_stream(stream):
69     """Get wrapper for given stream that writes any unicode without exception
70
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.
74
75     The wrapper only allows unicode to be written, not non-ascii bytestrings,
76     which is a good thing to ensure sanity and sanitation.
77     """
78     if sys.platform == "cli":
79         # Best to never encode before writing in IronPython
80         return stream
81     try:
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
89         return stream
90     if sys.version_info > (3, 0):
91         # Python 3 doesn't seem to make this easy, handle a common case
92         try:
93             return stream.__class__(stream.buffer, stream.encoding, "replace",
94                 stream.newlines, stream.line_buffering)
95         except AttributeError:
96             pass
97     return writer(stream, "replace")    
98
99
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"
104
105 # Pattern specified in <http://www.python.org/dev/peps/pep-0263/>
106 _cookie_search=re.compile("coding[:=]\s*([-\w.]+)").search
107
108 def _detect_encoding(lines):
109     """Get the encoding of a Python source file from a list of lines as bytes
110
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
114     determined is valid.
115     """
116     if not lines:
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
120         return "utf-8"
121     # Only the first two lines of the source file are examined
122     magic = _cookie_search("".join(lines[:2]))
123     if magic is None:
124         return _default_source_encoding
125     encoding = magic.group(1)
126     try:
127         codecs.lookup(encoding)
128     except LookupError:
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
133     return encoding
134
135
136 class _EncodingTuple(tuple):
137     """A tuple type that can have an encoding attribute smuggled on"""
138
139
140 def _get_source_encoding(filename):
141     """Detect, cache and return the encoding of Python source at filename"""
142     try:
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
150         return encoding
151
152
153 def _get_exception_encoding():
154     """Return the encoding we expect messages from the OS to be encoded in"""
155     if os.name == "nt":
156         # GZ 2010-05-24: Really want the codepage number instead, the error
157         #                handling of standard codecs is more deterministic
158         return "mbcs"
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"
163
164
165 def _exception_to_text(evalue):
166     """Try hard to get a sensible text value out of an exception instance"""
167     try:
168         return unicode(evalue)
169     except KeyboardInterrupt:
170         raise
171     except:
172         # Apparently this is what traceback._some_str does. Sigh - RBC 20100623
173         pass
174     try:
175         return str(evalue).decode(_get_exception_encoding(), "replace")
176     except KeyboardInterrupt:
177         raise
178     except:
179         # Apparently this is what traceback._some_str does. Sigh - RBC 20100623
180         pass
181     # Okay, out of ideas, let higher level handle it
182     return None
183
184
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
190
191     Compatibility function for Python 2 which ensures each component of a
192     traceback is correctly decoded according to its origins.
193
194     Based on traceback.format_exception and related functions.
195     """
196     fs_enc = sys.getfilesystemencoding()
197     if tb:
198         list = [_TB_HEADER]
199         extracted_list = []
200         for filename, lineno, name, line in traceback.extract_tb(tb, limit):
201             extracted_list.append((
202                 filename.decode(fs_enc, "replace"),
203                 lineno,
204                 name.decode("ascii", "replace"),
205                 line and line.decode(
206                     _get_source_encoding(filename), "replace")))
207         list.extend(traceback.format_list(extracted_list))
208     else:
209         list = []
210     if evalue is None:
211         # Is a (deprecated) string exception
212         list.append((eclass + "\n").decode("ascii", "replace"))
213         return list
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
218         try:
219             msg, (filename, lineno, offset, line) = evalue
220         except (TypeError, ValueError):
221             pass # Strange exception instance, fall through to generic code
222         else:
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
227             if line:
228                 bytestr = linecache.getline(filename, lineno)
229                 if bytestr:
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]
235                 else:
236                     line = line.decode("ascii", "replace")
237             if filename:
238                 filename = filename.decode(fs_enc, "replace")
239             evalue = eclass(msg, (filename, lineno, offset, line))
240             list.extend(traceback.format_exception_only(eclass, evalue))
241             return list
242     sclass = eclass.__name__
243     svalue = _exception_to_text(evalue)
244     if svalue:
245         list.append("%s: %s\n" % (sclass, svalue))
246     elif svalue is None:
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))
249     else:
250         list.append("%s\n" % sclass)
251     return list