1# util/compat.py
2# Copyright (C) 2005-2021 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: https://www.opensource.org/licenses/mit-license.php
7
8"""Handle Python version/platform incompatibilities."""
9
10import collections
11import contextlib
12import inspect
13import operator
14import platform
15import sys
16
17py39 = sys.version_info >= (3, 9)
18py38 = sys.version_info >= (3, 8)
19py37 = sys.version_info >= (3, 7)
20py3k = sys.version_info >= (3, 0)
21py2k = sys.version_info < (3, 0)
22pypy = platform.python_implementation() == "PyPy"
23
24
25cpython = platform.python_implementation() == "CPython"
26win32 = sys.platform.startswith("win")
27osx = sys.platform.startswith("darwin")
28arm = "aarch" in platform.machine().lower()
29
30has_refcount_gc = bool(cpython)
31
32contextmanager = contextlib.contextmanager
33dottedgetter = operator.attrgetter
34namedtuple = collections.namedtuple
35next = next  # noqa
36
37FullArgSpec = collections.namedtuple(
38    "FullArgSpec",
39    [
40        "args",
41        "varargs",
42        "varkw",
43        "defaults",
44        "kwonlyargs",
45        "kwonlydefaults",
46        "annotations",
47    ],
48)
49
50
51class nullcontext(object):
52    """Context manager that does no additional processing.
53
54    Vendored from Python 3.7.
55
56    """
57
58    def __init__(self, enter_result=None):
59        self.enter_result = enter_result
60
61    def __enter__(self):
62        return self.enter_result
63
64    def __exit__(self, *excinfo):
65        pass
66
67
68try:
69    import threading
70except ImportError:
71    import dummy_threading as threading  # noqa
72
73
74def inspect_getfullargspec(func):
75    """Fully vendored version of getfullargspec from Python 3.3."""
76
77    if inspect.ismethod(func):
78        func = func.__func__
79    if not inspect.isfunction(func):
80        raise TypeError("{!r} is not a Python function".format(func))
81
82    co = func.__code__
83    if not inspect.iscode(co):
84        raise TypeError("{!r} is not a code object".format(co))
85
86    nargs = co.co_argcount
87    names = co.co_varnames
88    nkwargs = co.co_kwonlyargcount if py3k else 0
89    args = list(names[:nargs])
90    kwonlyargs = list(names[nargs : nargs + nkwargs])
91
92    nargs += nkwargs
93    varargs = None
94    if co.co_flags & inspect.CO_VARARGS:
95        varargs = co.co_varnames[nargs]
96        nargs = nargs + 1
97    varkw = None
98    if co.co_flags & inspect.CO_VARKEYWORDS:
99        varkw = co.co_varnames[nargs]
100
101    return FullArgSpec(
102        args,
103        varargs,
104        varkw,
105        func.__defaults__,
106        kwonlyargs,
107        func.__kwdefaults__ if py3k else None,
108        func.__annotations__ if py3k else {},
109    )
110
111
112if py38:
113    from importlib import metadata as importlib_metadata
114else:
115    import importlib_metadata  # noqa
116
117
118def importlib_metadata_get(group):
119    ep = importlib_metadata.entry_points()
120    if hasattr(ep, "select"):
121        return ep.select(group=group)
122    else:
123        return ep.get(group, ())
124
125
126if py3k:
127    import base64
128    import builtins
129    import configparser
130    import itertools
131    import pickle
132
133    from functools import reduce
134    from io import BytesIO as byte_buffer
135    from io import StringIO
136    from itertools import zip_longest
137    from time import perf_counter
138    from urllib.parse import (
139        quote_plus,
140        unquote_plus,
141        parse_qsl,
142        quote,
143        unquote,
144    )
145
146    string_types = (str,)
147    binary_types = (bytes,)
148    binary_type = bytes
149    text_type = str
150    int_types = (int,)
151    iterbytes = iter
152    long_type = int
153
154    itertools_filterfalse = itertools.filterfalse
155    itertools_filter = filter
156    itertools_imap = map
157
158    exec_ = getattr(builtins, "exec")
159    import_ = getattr(builtins, "__import__")
160    print_ = getattr(builtins, "print")
161
162    def b(s):
163        return s.encode("latin-1")
164
165    def b64decode(x):
166        return base64.b64decode(x.encode("ascii"))
167
168    def b64encode(x):
169        return base64.b64encode(x).decode("ascii")
170
171    def decode_backslashreplace(text, encoding):
172        return text.decode(encoding, errors="backslashreplace")
173
174    def cmp(a, b):
175        return (a > b) - (a < b)
176
177    def raise_(
178        exception, with_traceback=None, replace_context=None, from_=False
179    ):
180        r"""implement "raise" with cause support.
181
182        :param exception: exception to raise
183        :param with_traceback: will call exception.with_traceback()
184        :param replace_context: an as-yet-unsupported feature.  This is
185         an exception object which we are "replacing", e.g., it's our
186         "cause" but we don't want it printed.    Basically just what
187         ``__suppress_context__`` does but we don't want to suppress
188         the enclosing context, if any.  So for now we make it the
189         cause.
190        :param from\_: the cause.  this actually sets the cause and doesn't
191         hope to hide it someday.
192
193        """
194        if with_traceback is not None:
195            exception = exception.with_traceback(with_traceback)
196
197        if from_ is not False:
198            exception.__cause__ = from_
199        elif replace_context is not None:
200            # no good solution here, we would like to have the exception
201            # have only the context of replace_context.__context__ so that the
202            # intermediary exception does not change, but we can't figure
203            # that out.
204            exception.__cause__ = replace_context
205
206        try:
207            raise exception
208        finally:
209            # credit to
210            # https://cosmicpercolator.com/2016/01/13/exception-leaks-in-python-2-and-3/
211            # as the __traceback__ object creates a cycle
212            del exception, replace_context, from_, with_traceback
213
214    def u(s):
215        return s
216
217    def ue(s):
218        return s
219
220    from typing import TYPE_CHECKING
221
222    # Unused. Kept for backwards compatibility.
223    callable = callable  # noqa
224
225    from abc import ABC
226
227    def _qualname(fn):
228        return fn.__qualname__
229
230
231else:
232    import base64
233    import ConfigParser as configparser  # noqa
234    import itertools
235
236    from StringIO import StringIO  # noqa
237    from cStringIO import StringIO as byte_buffer  # noqa
238    from itertools import izip_longest as zip_longest  # noqa
239    from time import clock as perf_counter  # noqa
240    from urllib import quote  # noqa
241    from urllib import quote_plus  # noqa
242    from urllib import unquote  # noqa
243    from urllib import unquote_plus  # noqa
244    from urlparse import parse_qsl  # noqa
245
246    from abc import ABCMeta
247
248    class ABC(object):
249        __metaclass__ = ABCMeta
250
251    try:
252        import cPickle as pickle
253    except ImportError:
254        import pickle  # noqa
255
256    string_types = (basestring,)  # noqa
257    binary_types = (bytes,)
258    binary_type = str
259    text_type = unicode  # noqa
260    int_types = int, long  # noqa
261    long_type = long  # noqa
262
263    callable = callable  # noqa
264    cmp = cmp  # noqa
265    reduce = reduce  # noqa
266
267    b64encode = base64.b64encode
268    b64decode = base64.b64decode
269
270    itertools_filterfalse = itertools.ifilterfalse
271    itertools_filter = itertools.ifilter
272    itertools_imap = itertools.imap
273
274    def b(s):
275        return s
276
277    def exec_(func_text, globals_, lcl=None):
278        if lcl is None:
279            exec("exec func_text in globals_")
280        else:
281            exec("exec func_text in globals_, lcl")
282
283    def iterbytes(buf):
284        return (ord(byte) for byte in buf)
285
286    def import_(*args):
287        if len(args) == 4:
288            args = args[0:3] + ([str(arg) for arg in args[3]],)
289        return __import__(*args)
290
291    def print_(*args, **kwargs):
292        fp = kwargs.pop("file", sys.stdout)
293        if fp is None:
294            return
295        for arg in enumerate(args):
296            if not isinstance(arg, basestring):  # noqa
297                arg = str(arg)
298            fp.write(arg)
299
300    def u(s):
301        # this differs from what six does, which doesn't support non-ASCII
302        # strings - we only use u() with
303        # literal source strings, and all our source files with non-ascii
304        # in them (all are tests) are utf-8 encoded.
305        return unicode(s, "utf-8")  # noqa
306
307    def ue(s):
308        return unicode(s, "unicode_escape")  # noqa
309
310    def decode_backslashreplace(text, encoding):
311        try:
312            return text.decode(encoding)
313        except UnicodeDecodeError:
314            # regular "backslashreplace" for an incompatible encoding raises:
315            # "TypeError: don't know how to handle UnicodeDecodeError in
316            # error callback"
317            return repr(text)[1:-1].decode()
318
319    def safe_bytestring(text):
320        # py2k only
321        if not isinstance(text, string_types):
322            return unicode(text).encode(  # noqa: F821
323                "ascii", errors="backslashreplace"
324            )
325        elif isinstance(text, unicode):  # noqa: F821
326            return text.encode("ascii", errors="backslashreplace")
327        else:
328            return text
329
330    exec(
331        "def raise_(exception, with_traceback=None, replace_context=None, "
332        "from_=False):\n"
333        "    if with_traceback:\n"
334        "        raise type(exception), exception, with_traceback\n"
335        "    else:\n"
336        "        raise exception\n"
337    )
338
339    TYPE_CHECKING = False
340
341    def _qualname(meth):
342        """return __qualname__ equivalent for a method on a class"""
343
344        for cls in meth.im_class.__mro__:
345            if meth.__name__ in cls.__dict__:
346                break
347        else:
348            return meth.__name__
349
350        return "%s.%s" % (cls.__name__, meth.__name__)
351
352
353if py3k:
354
355    def _formatannotation(annotation, base_module=None):
356        """vendored from python 3.7"""
357
358        if getattr(annotation, "__module__", None) == "typing":
359            return repr(annotation).replace("typing.", "")
360        if isinstance(annotation, type):
361            if annotation.__module__ in ("builtins", base_module):
362                return annotation.__qualname__
363            return annotation.__module__ + "." + annotation.__qualname__
364        return repr(annotation)
365
366    def inspect_formatargspec(
367        args,
368        varargs=None,
369        varkw=None,
370        defaults=None,
371        kwonlyargs=(),
372        kwonlydefaults={},
373        annotations={},
374        formatarg=str,
375        formatvarargs=lambda name: "*" + name,
376        formatvarkw=lambda name: "**" + name,
377        formatvalue=lambda value: "=" + repr(value),
378        formatreturns=lambda text: " -> " + text,
379        formatannotation=_formatannotation,
380    ):
381        """Copy formatargspec from python 3.7 standard library.
382
383        Python 3 has deprecated formatargspec and requested that Signature
384        be used instead, however this requires a full reimplementation
385        of formatargspec() in terms of creating Parameter objects and such.
386        Instead of introducing all the object-creation overhead and having
387        to reinvent from scratch, just copy their compatibility routine.
388
389        Ultimately we would need to rewrite our "decorator" routine completely
390        which is not really worth it right now, until all Python 2.x support
391        is dropped.
392
393        """
394
395        kwonlydefaults = kwonlydefaults or {}
396        annotations = annotations or {}
397
398        def formatargandannotation(arg):
399            result = formatarg(arg)
400            if arg in annotations:
401                result += ": " + formatannotation(annotations[arg])
402            return result
403
404        specs = []
405        if defaults:
406            firstdefault = len(args) - len(defaults)
407        for i, arg in enumerate(args):
408            spec = formatargandannotation(arg)
409            if defaults and i >= firstdefault:
410                spec = spec + formatvalue(defaults[i - firstdefault])
411            specs.append(spec)
412
413        if varargs is not None:
414            specs.append(formatvarargs(formatargandannotation(varargs)))
415        else:
416            if kwonlyargs:
417                specs.append("*")
418
419        if kwonlyargs:
420            for kwonlyarg in kwonlyargs:
421                spec = formatargandannotation(kwonlyarg)
422                if kwonlydefaults and kwonlyarg in kwonlydefaults:
423                    spec += formatvalue(kwonlydefaults[kwonlyarg])
424                specs.append(spec)
425
426        if varkw is not None:
427            specs.append(formatvarkw(formatargandannotation(varkw)))
428
429        result = "(" + ", ".join(specs) + ")"
430        if "return" in annotations:
431            result += formatreturns(formatannotation(annotations["return"]))
432        return result
433
434
435else:
436    from inspect import formatargspec as _inspect_formatargspec
437
438    def inspect_formatargspec(*spec, **kw):
439        # convert for a potential FullArgSpec from compat.getfullargspec()
440        return _inspect_formatargspec(*spec[0:4], **kw)  # noqa
441
442
443# Fix deprecation of accessing ABCs straight from collections module
444# (which will stop working in 3.8).
445if py3k:
446    import collections.abc as collections_abc
447else:
448    import collections as collections_abc  # noqa
449
450
451if py37:
452    import dataclasses
453
454    def dataclass_fields(cls):
455        """Return a sequence of all dataclasses.Field objects associated
456        with a class."""
457
458        if dataclasses.is_dataclass(cls):
459            return dataclasses.fields(cls)
460        else:
461            return []
462
463    def local_dataclass_fields(cls):
464        """Return a sequence of all dataclasses.Field objects associated with
465        a class, excluding those that originate from a superclass."""
466
467        if dataclasses.is_dataclass(cls):
468            super_fields = set()
469            for sup in cls.__bases__:
470                super_fields.update(dataclass_fields(sup))
471            return [
472                f for f in dataclasses.fields(cls) if f not in super_fields
473            ]
474        else:
475            return []
476
477
478else:
479
480    def dataclass_fields(cls):
481        return []
482
483    def local_dataclass_fields(cls):
484        return []
485
486
487def raise_from_cause(exception, exc_info=None):
488    r"""legacy.  use raise\_()"""
489
490    if exc_info is None:
491        exc_info = sys.exc_info()
492    exc_type, exc_value, exc_tb = exc_info
493    cause = exc_value if exc_value is not exception else None
494    reraise(type(exception), exception, tb=exc_tb, cause=cause)
495
496
497def reraise(tp, value, tb=None, cause=None):
498    r"""legacy.  use raise\_()"""
499
500    raise_(value, with_traceback=tb, from_=cause)
501
502
503def with_metaclass(meta, *bases, **kw):
504    """Create a base class with a metaclass.
505
506    Drops the middle class upon creation.
507
508    Source: https://lucumr.pocoo.org/2013/5/21/porting-to-python-3-redux/
509
510    """
511
512    class metaclass(meta):
513        __call__ = type.__call__
514        __init__ = type.__init__
515
516        def __new__(cls, name, this_bases, d):
517            if this_bases is None:
518                cls = type.__new__(cls, name, (), d)
519            else:
520                cls = meta(name, bases, d)
521
522            if hasattr(cls, "__init_subclass__") and hasattr(
523                cls.__init_subclass__, "__func__"
524            ):
525                cls.__init_subclass__.__func__(cls, **kw)
526            return cls
527
528    return metaclass("temporary_class", None, {})
529
530
531if py3k:
532    from datetime import timezone
533else:
534    from datetime import datetime
535    from datetime import timedelta
536    from datetime import tzinfo
537
538    class timezone(tzinfo):
539        """Minimal port of python 3 timezone object"""
540
541        __slots__ = "_offset"
542
543        def __init__(self, offset):
544            if not isinstance(offset, timedelta):
545                raise TypeError("offset must be a timedelta")
546            if not self._minoffset <= offset <= self._maxoffset:
547                raise ValueError(
548                    "offset must be a timedelta "
549                    "strictly between -timedelta(hours=24) and "
550                    "timedelta(hours=24)."
551                )
552            self._offset = offset
553
554        def __eq__(self, other):
555            if type(other) != timezone:
556                return False
557            return self._offset == other._offset
558
559        def __hash__(self):
560            return hash(self._offset)
561
562        def __repr__(self):
563            return "sqlalchemy.util.%s(%r)" % (
564                self.__class__.__name__,
565                self._offset,
566            )
567
568        def __str__(self):
569            return self.tzname(None)
570
571        def utcoffset(self, dt):
572            return self._offset
573
574        def tzname(self, dt):
575            return self._name_from_offset(self._offset)
576
577        def dst(self, dt):
578            return None
579
580        def fromutc(self, dt):
581            if isinstance(dt, datetime):
582                if dt.tzinfo is not self:
583                    raise ValueError("fromutc: dt.tzinfo " "is not self")
584                return dt + self._offset
585            raise TypeError(
586                "fromutc() argument must be a datetime instance" " or None"
587            )
588
589        @staticmethod
590        def _timedelta_to_microseconds(timedelta):
591            """backport of timedelta._to_microseconds()"""
592            return (
593                timedelta.days * (24 * 3600) + timedelta.seconds
594            ) * 1000000 + timedelta.microseconds
595
596        @staticmethod
597        def _divmod_timedeltas(a, b):
598            """backport of timedelta.__divmod__"""
599
600            q, r = divmod(
601                timezone._timedelta_to_microseconds(a),
602                timezone._timedelta_to_microseconds(b),
603            )
604            return q, timedelta(0, 0, r)
605
606        @staticmethod
607        def _name_from_offset(delta):
608            if not delta:
609                return "UTC"
610            if delta < timedelta(0):
611                sign = "-"
612                delta = -delta
613            else:
614                sign = "+"
615            hours, rest = timezone._divmod_timedeltas(
616                delta, timedelta(hours=1)
617            )
618            minutes, rest = timezone._divmod_timedeltas(
619                rest, timedelta(minutes=1)
620            )
621            result = "UTC%s%02d:%02d" % (sign, hours, minutes)
622            if rest.seconds:
623                result += ":%02d" % (rest.seconds,)
624            if rest.microseconds:
625                result += ".%06d" % (rest.microseconds,)
626            return result
627
628        _maxoffset = timedelta(hours=23, minutes=59)
629        _minoffset = -_maxoffset
630
631    timezone.utc = timezone(timedelta(0))
632