1"""passlib.utils.compat - python 2/3 compatibility helpers"""
2#=============================================================================
3# figure out what we're running
4#=============================================================================
5
6#------------------------------------------------------------------------
7# python version
8#------------------------------------------------------------------------
9import sys
10PY2 = sys.version_info < (3,0)
11PY3 = sys.version_info >= (3,0)
12
13# make sure it's not an unsupported version, even if we somehow got this far
14if sys.version_info < (2,6) or (3,0) <= sys.version_info < (3,2):
15    raise RuntimeError("Passlib requires Python 2.6, 2.7, or >= 3.2 (as of passlib 1.7)")
16
17PY26 = sys.version_info < (2,7)
18
19#------------------------------------------------------------------------
20# python implementation
21#------------------------------------------------------------------------
22JYTHON = sys.platform.startswith('java')
23
24PYPY = hasattr(sys, "pypy_version_info")
25
26if PYPY and sys.pypy_version_info < (2,0):
27    raise RuntimeError("passlib requires pypy >= 2.0 (as of passlib 1.7)")
28
29# e.g. '2.7.7\n[Pyston 0.5.1]'
30# NOTE: deprecated support 2019-11
31PYSTON = "Pyston" in sys.version
32
33#=============================================================================
34# common imports
35#=============================================================================
36import logging; log = logging.getLogger(__name__)
37if PY3:
38    import builtins
39else:
40    import __builtin__ as builtins
41
42def add_doc(obj, doc):
43    """add docstring to an object"""
44    obj.__doc__ = doc
45
46#=============================================================================
47# the default exported vars
48#=============================================================================
49__all__ = [
50    # python versions
51    'PY2', 'PY3', 'PY26',
52
53    # io
54    'BytesIO', 'StringIO', 'NativeStringIO', 'SafeConfigParser',
55    'print_',
56
57    # type detection
58##    'is_mapping',
59    'int_types',
60    'num_types',
61    'unicode_or_bytes_types',
62    'native_string_types',
63
64    # unicode/bytes types & helpers
65    'u',
66    'unicode',
67    'uascii_to_str', 'bascii_to_str',
68    'str_to_uascii', 'str_to_bascii',
69    'join_unicode', 'join_bytes',
70    'join_byte_values', 'join_byte_elems',
71    'byte_elem_value',
72    'iter_byte_values',
73
74    # iteration helpers
75    'irange', #'lrange',
76    'imap', 'lmap',
77    'iteritems', 'itervalues',
78    'next',
79
80    # collections
81    'OrderedDict',
82
83    # context helpers
84    'nullcontext',
85
86    # introspection
87    'get_method_function', 'add_doc',
88]
89
90# begin accumulating mapping of lazy-loaded attrs,
91# 'merged' into module at bottom
92_lazy_attrs = dict()
93
94#=============================================================================
95# unicode & bytes types
96#=============================================================================
97if PY3:
98    unicode = str
99
100    # TODO: once we drop python 3.2 support, can use u'' again!
101    def u(s):
102        assert isinstance(s, str)
103        return s
104
105    unicode_or_bytes_types = (str, bytes)
106    native_string_types = (unicode,)
107
108else:
109    unicode = builtins.unicode
110
111    def u(s):
112        assert isinstance(s, str)
113        return s.decode("unicode_escape")
114
115    unicode_or_bytes_types = (basestring,)
116    native_string_types = (basestring,)
117
118# shorter preferred aliases
119unicode_or_bytes = unicode_or_bytes_types
120unicode_or_str = native_string_types
121
122# unicode -- unicode type, regardless of python version
123# bytes -- bytes type, regardless of python version
124# unicode_or_bytes_types -- types that text can occur in, whether encoded or not
125# native_string_types -- types that native python strings (dict keys etc) can occur in.
126
127#=============================================================================
128# unicode & bytes helpers
129#=============================================================================
130# function to join list of unicode strings
131join_unicode = u('').join
132
133# function to join list of byte strings
134join_bytes = b''.join
135
136if PY3:
137    def uascii_to_str(s):
138        assert isinstance(s, unicode)
139        return s
140
141    def bascii_to_str(s):
142        assert isinstance(s, bytes)
143        return s.decode("ascii")
144
145    def str_to_uascii(s):
146        assert isinstance(s, str)
147        return s
148
149    def str_to_bascii(s):
150        assert isinstance(s, str)
151        return s.encode("ascii")
152
153    join_byte_values = join_byte_elems = bytes
154
155    def byte_elem_value(elem):
156        assert isinstance(elem, int)
157        return elem
158
159    def iter_byte_values(s):
160        assert isinstance(s, bytes)
161        return s
162
163    def iter_byte_chars(s):
164        assert isinstance(s, bytes)
165        # FIXME: there has to be a better way to do this
166        return (bytes([c]) for c in s)
167
168else:
169    def uascii_to_str(s):
170        assert isinstance(s, unicode)
171        return s.encode("ascii")
172
173    def bascii_to_str(s):
174        assert isinstance(s, bytes)
175        return s
176
177    def str_to_uascii(s):
178        assert isinstance(s, str)
179        return s.decode("ascii")
180
181    def str_to_bascii(s):
182        assert isinstance(s, str)
183        return s
184
185    def join_byte_values(values):
186        return join_bytes(chr(v) for v in values)
187
188    join_byte_elems = join_bytes
189
190    byte_elem_value = ord
191
192    def iter_byte_values(s):
193        assert isinstance(s, bytes)
194        return (ord(c) for c in s)
195
196    def iter_byte_chars(s):
197        assert isinstance(s, bytes)
198        return s
199
200add_doc(uascii_to_str, "helper to convert ascii unicode -> native str")
201add_doc(bascii_to_str, "helper to convert ascii bytes -> native str")
202add_doc(str_to_uascii, "helper to convert ascii native str -> unicode")
203add_doc(str_to_bascii, "helper to convert ascii native str -> bytes")
204
205# join_byte_values -- function to convert list of ordinal integers to byte string.
206
207# join_byte_elems --  function to convert list of byte elements to byte string;
208#                 i.e. what's returned by ``b('a')[0]``...
209#                 this is b('a') under PY2, but 97 under PY3.
210
211# byte_elem_value -- function to convert byte element to integer -- a noop under PY3
212
213add_doc(iter_byte_values, "iterate over byte string as sequence of ints 0-255")
214add_doc(iter_byte_chars, "iterate over byte string as sequence of 1-byte strings")
215
216#=============================================================================
217# numeric
218#=============================================================================
219if PY3:
220    int_types = (int,)
221    num_types = (int, float)
222else:
223    int_types = (int, long)
224    num_types = (int, long, float)
225
226#=============================================================================
227# iteration helpers
228#
229# irange - range iterable / view (xrange under py2, range under py3)
230# lrange - range list (range under py2, list(range()) under py3)
231#
232# imap - map to iterator
233# lmap - map to list
234#=============================================================================
235if PY3:
236    irange = range
237    ##def lrange(*a,**k):
238    ##    return list(range(*a,**k))
239
240    def lmap(*a, **k):
241        return list(map(*a,**k))
242    imap = map
243
244    def iteritems(d):
245        return d.items()
246    def itervalues(d):
247        return d.values()
248
249    def nextgetter(obj):
250        return obj.__next__
251
252    izip = zip
253
254else:
255    irange = xrange
256    ##lrange = range
257
258    lmap = map
259    from itertools import imap, izip
260
261    def iteritems(d):
262        return d.iteritems()
263    def itervalues(d):
264        return d.itervalues()
265
266    def nextgetter(obj):
267        return obj.next
268
269add_doc(nextgetter, "return function that yields successive values from iterable")
270
271#=============================================================================
272# typing
273#=============================================================================
274##def is_mapping(obj):
275##    # non-exhaustive check, enough to distinguish from lists, etc
276##    return hasattr(obj, "items")
277
278#=============================================================================
279# introspection
280#=============================================================================
281if PY3:
282    method_function_attr = "__func__"
283else:
284    method_function_attr = "im_func"
285
286def get_method_function(func):
287    """given (potential) method, return underlying function"""
288    return getattr(func, method_function_attr, func)
289
290def get_unbound_method_function(func):
291    """given unbound method, return underlying function"""
292    return func if PY3 else func.__func__
293
294def error_from(exc,  # *,
295               cause=None):
296    """
297    backward compat hack to suppress exception cause in python3.3+
298
299    one python < 3.3 support is dropped, can replace all uses with "raise exc from None"
300    """
301    exc.__cause__ = cause
302    exc.__suppress_context__ = True
303    return exc
304
305# legacy alias
306suppress_cause = error_from
307
308#=============================================================================
309# input/output
310#=============================================================================
311if PY3:
312    _lazy_attrs = dict(
313        BytesIO="io.BytesIO",
314        UnicodeIO="io.StringIO",
315        NativeStringIO="io.StringIO",
316        SafeConfigParser="configparser.ConfigParser",
317    )
318
319    print_ = getattr(builtins, "print")
320
321else:
322    _lazy_attrs = dict(
323        BytesIO="cStringIO.StringIO",
324        UnicodeIO="StringIO.StringIO",
325        NativeStringIO="cStringIO.StringIO",
326        SafeConfigParser="ConfigParser.SafeConfigParser",
327    )
328
329    def print_(*args, **kwds):
330        """The new-style print function."""
331        # extract kwd args
332        fp = kwds.pop("file", sys.stdout)
333        sep = kwds.pop("sep", None)
334        end = kwds.pop("end", None)
335        if kwds:
336            raise TypeError("invalid keyword arguments")
337
338        # short-circuit if no target
339        if fp is None:
340            return
341
342        # use unicode or bytes ?
343        want_unicode = isinstance(sep, unicode) or isinstance(end, unicode) or \
344                       any(isinstance(arg, unicode) for arg in args)
345
346        # pick default end sequence
347        if end is None:
348            end = u("\n") if want_unicode else "\n"
349        elif not isinstance(end, unicode_or_bytes_types):
350            raise TypeError("end must be None or a string")
351
352        # pick default separator
353        if sep is None:
354            sep = u(" ") if want_unicode else " "
355        elif not isinstance(sep, unicode_or_bytes_types):
356            raise TypeError("sep must be None or a string")
357
358        # write to buffer
359        first = True
360        write = fp.write
361        for arg in args:
362            if first:
363                first = False
364            else:
365                write(sep)
366            if not isinstance(arg, basestring):
367                arg = str(arg)
368            write(arg)
369        write(end)
370
371#=============================================================================
372# collections
373#=============================================================================
374if PY26:
375    _lazy_attrs['OrderedDict'] = 'passlib.utils.compat._ordered_dict.OrderedDict'
376else:
377    _lazy_attrs['OrderedDict'] = 'collections.OrderedDict'
378
379#=============================================================================
380# context managers
381#=============================================================================
382
383try:
384    # new in py37
385    from contextlib import nullcontext
386except ImportError:
387
388    class nullcontext(object):
389        """
390        Context manager that does no additional processing.
391        """
392        def __init__(self, enter_result=None):
393            self.enter_result = enter_result
394
395        def __enter__(self):
396            return self.enter_result
397
398        def __exit__(self, *exc_info):
399            pass
400
401#=============================================================================
402# lazy overlay module
403#=============================================================================
404from types import ModuleType
405
406def _import_object(source):
407    """helper to import object from module; accept format `path.to.object`"""
408    modname, modattr = source.rsplit(".",1)
409    mod = __import__(modname, fromlist=[modattr], level=0)
410    return getattr(mod, modattr)
411
412class _LazyOverlayModule(ModuleType):
413    """proxy module which overlays original module,
414    and lazily imports specified attributes.
415
416    this is mainly used to prevent importing of resources
417    that are only needed by certain password hashes,
418    yet allow them to be imported from a single location.
419
420    used by :mod:`passlib.utils`, :mod:`passlib.crypto`,
421    and :mod:`passlib.utils.compat`.
422    """
423
424    @classmethod
425    def replace_module(cls, name, attrmap):
426        orig = sys.modules[name]
427        self = cls(name, attrmap, orig)
428        sys.modules[name] = self
429        return self
430
431    def __init__(self, name, attrmap, proxy=None):
432        ModuleType.__init__(self, name)
433        self.__attrmap = attrmap
434        self.__proxy = proxy
435        self.__log = logging.getLogger(name)
436
437    def __getattr__(self, attr):
438        proxy = self.__proxy
439        if proxy and hasattr(proxy, attr):
440            return getattr(proxy, attr)
441        attrmap = self.__attrmap
442        if attr in attrmap:
443            source = attrmap[attr]
444            if callable(source):
445                value = source()
446            else:
447                value = _import_object(source)
448            setattr(self, attr, value)
449            self.__log.debug("loaded lazy attr %r: %r", attr, value)
450            return value
451        raise AttributeError("'module' object has no attribute '%s'" % (attr,))
452
453    def __repr__(self):
454        proxy = self.__proxy
455        if proxy:
456            return repr(proxy)
457        else:
458            return ModuleType.__repr__(self)
459
460    def __dir__(self):
461        attrs = set(dir(self.__class__))
462        attrs.update(self.__dict__)
463        attrs.update(self.__attrmap)
464        proxy = self.__proxy
465        if proxy is not None:
466            attrs.update(dir(proxy))
467        return list(attrs)
468
469# replace this module with overlay that will lazily import attributes.
470_LazyOverlayModule.replace_module(__name__, _lazy_attrs)
471
472#=============================================================================
473# eof
474#=============================================================================
475