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