testtools: Merge in new upstream.
[nivanova/samba-autobuild/.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 if sys.version_info > (2, 5):
69     all = all
70     _error_repr = BaseException.__repr__
71     def isbaseexception(exception):
72         """Return whether exception inherits from BaseException only"""
73         return (isinstance(exception, BaseException)
74             and not isinstance(exception, Exception))
75 else:
76     def all(iterable):
77         """If contents of iterable all evaluate as boolean True"""
78         for obj in iterable:
79             if not obj:
80                 return False
81         return True
82     def _error_repr(exception):
83         """Format an exception instance as Python 2.5 and later do"""
84         return exception.__class__.__name__ + repr(exception.args)
85     def isbaseexception(exception):
86         """Return whether exception would inherit from BaseException only
87
88         This approximates the hierarchy in Python 2.5 and later, compare the
89         difference between the diagrams at the bottom of the pages:
90         <http://docs.python.org/release/2.4.4/lib/module-exceptions.html>
91         <http://docs.python.org/release/2.5.4/lib/module-exceptions.html>
92         """
93         return isinstance(exception, (KeyboardInterrupt, SystemExit))
94
95
96 def unicode_output_stream(stream):
97     """Get wrapper for given stream that writes any unicode without exception
98
99     Characters that can't be coerced to the encoding of the stream, or 'ascii'
100     if valid encoding is not found, will be replaced. The original stream may
101     be returned in situations where a wrapper is determined unneeded.
102
103     The wrapper only allows unicode to be written, not non-ascii bytestrings,
104     which is a good thing to ensure sanity and sanitation.
105     """
106     if sys.platform == "cli":
107         # Best to never encode before writing in IronPython
108         return stream
109     try:
110         writer = codecs.getwriter(stream.encoding or "")
111     except (AttributeError, LookupError):
112         # GZ 2010-06-16: Python 3 StringIO ends up here, but probably needs
113         #                different handling as it doesn't want bytestrings
114         return codecs.getwriter("ascii")(stream, "replace")
115     if writer.__module__.rsplit(".", 1)[1].startswith("utf"):
116         # The current stream has a unicode encoding so no error handler is needed
117         return stream
118     if sys.version_info > (3, 0):
119         # Python 3 doesn't seem to make this easy, handle a common case
120         try:
121             return stream.__class__(stream.buffer, stream.encoding, "replace",
122                 stream.newlines, stream.line_buffering)
123         except AttributeError:
124             pass
125     return writer(stream, "replace")    
126
127
128 # The default source encoding is actually "iso-8859-1" until Python 2.5 but
129 # using non-ascii causes a deprecation warning in 2.4 and it's cleaner to
130 # treat all versions the same way
131 _default_source_encoding = "ascii"
132
133 # Pattern specified in <http://www.python.org/dev/peps/pep-0263/>
134 _cookie_search=re.compile("coding[:=]\s*([-\w.]+)").search
135
136 def _detect_encoding(lines):
137     """Get the encoding of a Python source file from a list of lines as bytes
138
139     This function does less than tokenize.detect_encoding added in Python 3 as
140     it does not attempt to raise a SyntaxError when the interpreter would, it
141     just wants the encoding of a source file Python has already compiled and
142     determined is valid.
143     """
144     if not lines:
145         return _default_source_encoding
146     if lines[0].startswith("\xef\xbb\xbf"):
147         # Source starting with UTF-8 BOM is either UTF-8 or a SyntaxError
148         return "utf-8"
149     # Only the first two lines of the source file are examined
150     magic = _cookie_search("".join(lines[:2]))
151     if magic is None:
152         return _default_source_encoding
153     encoding = magic.group(1)
154     try:
155         codecs.lookup(encoding)
156     except LookupError:
157         # Some codecs raise something other than LookupError if they don't
158         # support the given error handler, but not the text ones that could
159         # actually be used for Python source code
160         return _default_source_encoding
161     return encoding
162
163
164 class _EncodingTuple(tuple):
165     """A tuple type that can have an encoding attribute smuggled on"""
166
167
168 def _get_source_encoding(filename):
169     """Detect, cache and return the encoding of Python source at filename"""
170     try:
171         return linecache.cache[filename].encoding
172     except (AttributeError, KeyError):
173         encoding = _detect_encoding(linecache.getlines(filename))
174         if filename in linecache.cache:
175             newtuple = _EncodingTuple(linecache.cache[filename])
176             newtuple.encoding = encoding
177             linecache.cache[filename] = newtuple
178         return encoding
179
180
181 def _get_exception_encoding():
182     """Return the encoding we expect messages from the OS to be encoded in"""
183     if os.name == "nt":
184         # GZ 2010-05-24: Really want the codepage number instead, the error
185         #                handling of standard codecs is more deterministic
186         return "mbcs"
187     # GZ 2010-05-23: We need this call to be after initialisation, but there's
188     #                no benefit in asking more than once as it's a global
189     #                setting that can change after the message is formatted.
190     return locale.getlocale(locale.LC_MESSAGES)[1] or "ascii"
191
192
193 def _exception_to_text(evalue):
194     """Try hard to get a sensible text value out of an exception instance"""
195     try:
196         return unicode(evalue)
197     except KeyboardInterrupt:
198         raise
199     except:
200         # Apparently this is what traceback._some_str does. Sigh - RBC 20100623
201         pass
202     try:
203         return str(evalue).decode(_get_exception_encoding(), "replace")
204     except KeyboardInterrupt:
205         raise
206     except:
207         # Apparently this is what traceback._some_str does. Sigh - RBC 20100623
208         pass
209     # Okay, out of ideas, let higher level handle it
210     return None
211
212
213 # GZ 2010-05-23: This function is huge and horrible and I welcome suggestions
214 #                on the best way to break it up
215 _TB_HEADER = _u('Traceback (most recent call last):\n')
216 def _format_exc_info(eclass, evalue, tb, limit=None):
217     """Format a stack trace and the exception information as unicode
218
219     Compatibility function for Python 2 which ensures each component of a
220     traceback is correctly decoded according to its origins.
221
222     Based on traceback.format_exception and related functions.
223     """
224     fs_enc = sys.getfilesystemencoding()
225     if tb:
226         list = [_TB_HEADER]
227         extracted_list = []
228         for filename, lineno, name, line in traceback.extract_tb(tb, limit):
229             extracted_list.append((
230                 filename.decode(fs_enc, "replace"),
231                 lineno,
232                 name.decode("ascii", "replace"),
233                 line and line.decode(
234                     _get_source_encoding(filename), "replace")))
235         list.extend(traceback.format_list(extracted_list))
236     else:
237         list = []
238     if evalue is None:
239         # Is a (deprecated) string exception
240         list.append((eclass + "\n").decode("ascii", "replace"))
241         return list
242     if isinstance(evalue, SyntaxError):
243         # Avoid duplicating the special formatting for SyntaxError here,
244         # instead create a new instance with unicode filename and line
245         # Potentially gives duff spacing, but that's a pre-existing issue
246         try:
247             msg, (filename, lineno, offset, line) = evalue
248         except (TypeError, ValueError):
249             pass # Strange exception instance, fall through to generic code
250         else:
251             # Errors during parsing give the line from buffer encoded as
252             # latin-1 or utf-8 or the encoding of the file depending on the
253             # coding and whether the patch for issue #1031213 is applied, so
254             # give up on trying to decode it and just read the file again
255             if line:
256                 bytestr = linecache.getline(filename, lineno)
257                 if bytestr:
258                     if lineno == 1 and bytestr.startswith("\xef\xbb\xbf"):
259                         bytestr = bytestr[3:]
260                     line = bytestr.decode(
261                         _get_source_encoding(filename), "replace")
262                     del linecache.cache[filename]
263                 else:
264                     line = line.decode("ascii", "replace")
265             if filename:
266                 filename = filename.decode(fs_enc, "replace")
267             evalue = eclass(msg, (filename, lineno, offset, line))
268             list.extend(traceback.format_exception_only(eclass, evalue))
269             return list
270     sclass = eclass.__name__
271     svalue = _exception_to_text(evalue)
272     if svalue:
273         list.append("%s: %s\n" % (sclass, svalue))
274     elif svalue is None:
275         # GZ 2010-05-24: Not a great fallback message, but keep for the moment
276         list.append("%s: <unprintable %s object>\n" % (sclass, sclass))
277     else:
278         list.append("%s\n" % sclass)
279     return list