1# util/compat.py
2# Copyright (C) 2005-2019 the SQLAlchemy authors and contributors
3# <see AUTHORS file>
4#
5# This module is part of SQLAlchemy and is released under
6# the MIT License: http://www.opensource.org/licenses/mit-license.php
7
8"""Handle Python version/platform incompatibilities."""
9
10import collections
11from collections import namedtuple  # noqa
12from contextlib import contextmanager
13from operator import attrgetter as dottedgetter  # noqa
14import sys
15import time
16
17try:
18    import threading
19except ImportError:
20    import dummy_threading as threading  # noqa
21
22py36 = sys.version_info >= (3, 6)
23py33 = sys.version_info >= (3, 3)
24py35 = sys.version_info >= (3, 5)
25py32 = sys.version_info >= (3, 2)
26py3k = sys.version_info >= (3, 0)
27py2k = sys.version_info < (3, 0)
28py265 = sys.version_info >= (2, 6, 5)
29jython = sys.platform.startswith("java")
30pypy = hasattr(sys, "pypy_version_info")
31win32 = sys.platform.startswith("win")
32cpython = not pypy and not jython  # TODO: something better for this ?
33
34next = next  # noqa
35
36if py3k:
37    import pickle
38else:
39    try:
40        import cPickle as pickle
41    except ImportError:
42        import pickle  # noqa
43
44# work around http://bugs.python.org/issue2646
45if py265:
46    safe_kwarg = lambda arg: arg  # noqa
47else:
48    safe_kwarg = str
49
50ArgSpec = collections.namedtuple(
51    "ArgSpec", ["args", "varargs", "keywords", "defaults"]
52)
53
54if py3k:
55    import builtins
56
57    from inspect import getfullargspec as inspect_getfullargspec
58    from urllib.parse import (
59        quote_plus,
60        unquote_plus,
61        parse_qsl,
62        quote,
63        unquote,
64    )
65    import configparser
66    from io import StringIO
67
68    from io import BytesIO as byte_buffer
69
70    def inspect_getargspec(func):
71        return ArgSpec(*inspect_getfullargspec(func)[0:4])
72
73    string_types = (str,)
74    binary_types = (bytes,)
75    binary_type = bytes
76    text_type = str
77    int_types = (int,)
78    iterbytes = iter
79
80    def u(s):
81        return s
82
83    def ue(s):
84        return s
85
86    def b(s):
87        return s.encode("latin-1")
88
89    if py32:
90        callable = callable  # noqa
91    else:
92
93        def callable(fn):  # noqa
94            return hasattr(fn, "__call__")
95
96    def decode_backslashreplace(text, encoding):
97        return text.decode(encoding, errors="backslashreplace")
98
99    def cmp(a, b):
100        return (a > b) - (a < b)
101
102    from functools import reduce
103
104    print_ = getattr(builtins, "print")
105
106    import_ = getattr(builtins, "__import__")
107
108    import itertools
109
110    itertools_filterfalse = itertools.filterfalse
111    itertools_filter = filter
112    itertools_imap = map
113    from itertools import zip_longest
114
115    import base64
116
117    def b64encode(x):
118        return base64.b64encode(x).decode("ascii")
119
120    def b64decode(x):
121        return base64.b64decode(x.encode("ascii"))
122
123
124else:
125    from inspect import getargspec as inspect_getfullargspec
126
127    inspect_getargspec = inspect_getfullargspec
128    from urllib import quote_plus, unquote_plus, quote, unquote  # noqa
129    from urlparse import parse_qsl  # noqa
130    import ConfigParser as configparser  # noqa
131    from StringIO import StringIO  # noqa
132    from cStringIO import StringIO as byte_buffer  # noqa
133
134    string_types = (basestring,)  # noqa
135    binary_types = (bytes,)
136    binary_type = str
137    text_type = unicode  # noqa
138    int_types = int, long  # noqa
139
140    def iterbytes(buf):
141        return (ord(byte) for byte in buf)
142
143    def u(s):
144        # this differs from what six does, which doesn't support non-ASCII
145        # strings - we only use u() with
146        # literal source strings, and all our source files with non-ascii
147        # in them (all are tests) are utf-8 encoded.
148        return unicode(s, "utf-8")  # noqa
149
150    def ue(s):
151        return unicode(s, "unicode_escape")  # noqa
152
153    def b(s):
154        return s
155
156    def import_(*args):
157        if len(args) == 4:
158            args = args[0:3] + ([str(arg) for arg in args[3]],)
159        return __import__(*args)
160
161    callable = callable  # noqa
162    cmp = cmp
163    reduce = reduce
164
165    import base64
166
167    b64encode = base64.b64encode
168    b64decode = base64.b64decode
169
170    def print_(*args, **kwargs):
171        fp = kwargs.pop("file", sys.stdout)
172        if fp is None:
173            return
174        for arg in enumerate(args):
175            if not isinstance(arg, basestring):  # noqa
176                arg = str(arg)
177            fp.write(arg)
178
179    import itertools
180
181    def decode_backslashreplace(text, encoding):
182        try:
183            return text.decode(encoding)
184        except UnicodeDecodeError:
185            # regular "backslashreplace" for an incompatible encoding raises:
186            # "TypeError: don't know how to handle UnicodeDecodeError in
187            # error callback"
188            return repr(text)[1:-1].decode()
189
190    itertools_filterfalse = itertools.ifilterfalse
191    itertools_filter = itertools.ifilter
192    itertools_imap = itertools.imap
193    from itertools import izip_longest as zip_longest  # noqa
194
195if py35:
196    from inspect import formatannotation
197
198    def inspect_formatargspec(
199        args,
200        varargs=None,
201        varkw=None,
202        defaults=None,
203        kwonlyargs=(),
204        kwonlydefaults={},
205        annotations={},
206        formatarg=str,
207        formatvarargs=lambda name: "*" + name,
208        formatvarkw=lambda name: "**" + name,
209        formatvalue=lambda value: "=" + repr(value),
210        formatreturns=lambda text: " -> " + text,
211        formatannotation=formatannotation,
212    ):
213        """Copy formatargspec from python 3.7 standard library.
214
215        Python 3 has deprecated formatargspec and requested that Signature
216        be used instead, however this requires a full reimplementation
217        of formatargspec() in terms of creating Parameter objects and such.
218        Instead of introducing all the object-creation overhead and having
219        to reinvent from scratch, just copy their compatibility routine.
220
221        Utimately we would need to rewrite our "decorator" routine completely
222        which is not really worth it right now, until all Python 2.x support
223        is dropped.
224
225        """
226
227        def formatargandannotation(arg):
228            result = formatarg(arg)
229            if arg in annotations:
230                result += ": " + formatannotation(annotations[arg])
231            return result
232
233        specs = []
234        if defaults:
235            firstdefault = len(args) - len(defaults)
236        for i, arg in enumerate(args):
237            spec = formatargandannotation(arg)
238            if defaults and i >= firstdefault:
239                spec = spec + formatvalue(defaults[i - firstdefault])
240            specs.append(spec)
241        if varargs is not None:
242            specs.append(formatvarargs(formatargandannotation(varargs)))
243        else:
244            if kwonlyargs:
245                specs.append("*")
246        if kwonlyargs:
247            for kwonlyarg in kwonlyargs:
248                spec = formatargandannotation(kwonlyarg)
249                if kwonlydefaults and kwonlyarg in kwonlydefaults:
250                    spec += formatvalue(kwonlydefaults[kwonlyarg])
251                specs.append(spec)
252        if varkw is not None:
253            specs.append(formatvarkw(formatargandannotation(varkw)))
254        result = "(" + ", ".join(specs) + ")"
255        if "return" in annotations:
256            result += formatreturns(formatannotation(annotations["return"]))
257        return result
258
259
260else:
261    from inspect import formatargspec as inspect_formatargspec  # noqa
262
263if win32 or jython:
264    time_func = time.clock
265else:
266    time_func = time.time
267
268
269if py3k:
270
271    def reraise(tp, value, tb=None, cause=None):
272        if cause is not None:
273            assert cause is not value, "Same cause emitted"
274            value.__cause__ = cause
275        if value.__traceback__ is not tb:
276            raise value.with_traceback(tb)
277        raise value
278
279
280else:
281    # not as nice as that of Py3K, but at least preserves
282    # the code line where the issue occurred
283    exec(
284        "def reraise(tp, value, tb=None, cause=None):\n"
285        "    if cause is not None:\n"
286        "        assert cause is not value, 'Same cause emitted'\n"
287        "    raise tp, value, tb\n"
288    )
289
290
291def raise_from_cause(exception, exc_info=None):
292    if exc_info is None:
293        exc_info = sys.exc_info()
294    exc_type, exc_value, exc_tb = exc_info
295    cause = exc_value if exc_value is not exception else None
296    reraise(type(exception), exception, tb=exc_tb, cause=cause)
297
298
299if py3k:
300    exec_ = getattr(builtins, "exec")
301else:
302
303    def exec_(func_text, globals_, lcl=None):
304        if lcl is None:
305            exec("exec func_text in globals_")
306        else:
307            exec("exec func_text in globals_, lcl")
308
309
310def with_metaclass(meta, *bases):
311    """Create a base class with a metaclass.
312
313    Drops the middle class upon creation.
314
315    Source: http://lucumr.pocoo.org/2013/5/21/porting-to-python-3-redux/
316
317    """
318
319    class metaclass(meta):
320        __call__ = type.__call__
321        __init__ = type.__init__
322
323        def __new__(cls, name, this_bases, d):
324            if this_bases is None:
325                return type.__new__(cls, name, (), d)
326            return meta(name, bases, d)
327
328    return metaclass("temporary_class", None, {})
329
330
331@contextmanager
332def nested(*managers):
333    """Implement contextlib.nested, mostly for unit tests.
334
335    As tests still need to run on py2.6 we can't use multiple-with yet.
336
337    Function is removed in py3k but also emits deprecation warning in 2.7
338    so just roll it here for everyone.
339
340    """
341
342    exits = []
343    vars_ = []
344    exc = (None, None, None)
345    try:
346        for mgr in managers:
347            exit_ = mgr.__exit__
348            enter = mgr.__enter__
349            vars_.append(enter())
350            exits.append(exit_)
351        yield vars_
352    except:
353        exc = sys.exc_info()
354    finally:
355        while exits:
356            exit_ = exits.pop()
357            try:
358                if exit_(*exc):
359                    exc = (None, None, None)
360            except:
361                exc = sys.exc_info()
362        if exc != (None, None, None):
363            reraise(exc[0], exc[1], exc[2])
364
365
366# Fix deprecation of accessing ABCs straight from collections module
367# (which will stop working in 3.8).
368if py33:
369    import collections.abc as collections_abc
370else:
371    import collections as collections_abc  # noqa
372