diff --git a/Lib/fontTools/misc/py23.py b/Lib/fontTools/misc/py23.py index 5cddfebca..bced80077 100644 --- a/Lib/fontTools/misc/py23.py +++ b/Lib/fontTools/misc/py23.py @@ -1,525 +1,148 @@ -"""Python 2/3 compat layer.""" +"""Python 2/3 compat layer leftovers.""" -from __future__ import print_function, division, absolute_import -import sys +import decimal as _decimal +import math as _math +import warnings +from contextlib import redirect_stderr, redirect_stdout +from io import BytesIO +from io import StringIO as UnicodeIO +from types import SimpleNamespace +warnings.warn( + "The py23 module has been deprecated and will be removed in the next release. " + "Please update your code.", + DeprecationWarning, +) -__all__ = ['basestring', 'unicode', 'unichr', 'byteord', 'bytechr', 'BytesIO', - 'StringIO', 'UnicodeIO', 'strjoin', 'bytesjoin', 'tobytes', 'tostr', - 'tounicode', 'Tag', 'open', 'range', 'xrange', 'round', 'Py23Error', - 'SimpleNamespace', 'zip', 'RecursionError'] +__all__ = [ + "basestring", + "bytechr", + "byteord", + "BytesIO", + "bytesjoin", + "open", + "Py23Error", + "range", + "RecursionError", + "round", + "SimpleNamespace", + "StringIO", + "strjoin", + "Tag", + "tobytes", + "tostr", + "tounicode", + "unichr", + "unicode", + "UnicodeIO", + "xrange", + "zip", +] class Py23Error(NotImplementedError): - pass + pass -PY3 = sys.version_info[0] == 3 -PY2 = sys.version_info[0] == 2 +RecursionError = RecursionError +StringIO = UnicodeIO + +basestring = str +isclose = _math.isclose +isfinite = _math.isfinite +open = open +range = range +round = round3 = round +unichr = chr +unicode = str +zip = zip -try: - basestring = basestring -except NameError: - basestring = str - -try: - unicode = unicode -except NameError: - unicode = str - -try: - unichr = unichr - - if sys.maxunicode < 0x10FFFF: - # workarounds for Python 2 "narrow" builds with UCS2-only support. - - _narrow_unichr = unichr - - def unichr(i): - """ - Return the unicode character whose Unicode code is the integer 'i'. - The valid range is 0 to 0x10FFFF inclusive. - - >>> _narrow_unichr(0xFFFF + 1) - Traceback (most recent call last): - File "", line 1, in ? - ValueError: unichr() arg not in range(0x10000) (narrow Python build) - >>> unichr(0xFFFF + 1) == u'\U00010000' - True - >>> unichr(1114111) == u'\U0010FFFF' - True - >>> unichr(0x10FFFF + 1) - Traceback (most recent call last): - File "", line 1, in ? - ValueError: unichr() arg not in range(0x110000) - """ - try: - return _narrow_unichr(i) - except ValueError: - try: - padded_hex_str = hex(i)[2:].zfill(8) - escape_str = "\\U" + padded_hex_str - return escape_str.decode("unicode-escape") - except UnicodeDecodeError: - raise ValueError('unichr() arg not in range(0x110000)') - - import re - _unicode_escape_RE = re.compile(r'\\U[A-Fa-f0-9]{8}') - - def byteord(c): - """ - Given a 8-bit or unicode character, return an integer representing the - Unicode code point of the character. If a unicode argument is given, the - character's code point must be in the range 0 to 0x10FFFF inclusive. - - >>> ord(u'\U00010000') - Traceback (most recent call last): - File "", line 1, in ? - TypeError: ord() expected a character, but string of length 2 found - >>> byteord(u'\U00010000') == 0xFFFF + 1 - True - >>> byteord(u'\U0010FFFF') == 1114111 - True - """ - try: - return ord(c) - except TypeError as e: - try: - escape_str = c.encode('unicode-escape') - if not _unicode_escape_RE.match(escape_str): - raise - hex_str = escape_str[3:] - return int(hex_str, 16) - except: - raise TypeError(e) - - else: - byteord = ord - bytechr = chr - -except NameError: - unichr = chr - def bytechr(n): - return bytes([n]) - def byteord(c): - return c if isinstance(c, int) else ord(c) +def bytechr(n): + return bytes([n]) -# the 'io' module provides the same I/O interface on both 2 and 3. -# here we define an alias of io.StringIO to disambiguate it eternally... -from io import BytesIO -from io import StringIO as UnicodeIO -try: - # in python 2, by 'StringIO' we still mean a stream of *byte* strings - from StringIO import StringIO -except ImportError: - # in Python 3, we mean instead a stream of *unicode* strings - StringIO = UnicodeIO +def byteord(c): + return c if isinstance(c, int) else ord(c) -def strjoin(iterable, joiner=''): - return tostr(joiner).join(iterable) - -def tobytes(s, encoding='ascii', errors='strict'): - if not isinstance(s, bytes): - return s.encode(encoding, errors) - else: - return s -def tounicode(s, encoding='ascii', errors='strict'): - if not isinstance(s, unicode): - return s.decode(encoding, errors) - else: - return s - -if str == bytes: - class Tag(str): - def tobytes(self): - if isinstance(self, bytes): - return self - else: - return self.encode('latin1') - - tostr = tobytes - - bytesjoin = strjoin -else: - class Tag(str): - - @staticmethod - def transcode(blob): - if isinstance(blob, bytes): - blob = blob.decode('latin-1') - return blob - - def __new__(self, content): - return str.__new__(self, self.transcode(content)) - def __ne__(self, other): - return not self.__eq__(other) - def __eq__(self, other): - return str.__eq__(self, self.transcode(other)) - - def __hash__(self): - return str.__hash__(self) - - def tobytes(self): - return self.encode('latin-1') - - tostr = tounicode - - def bytesjoin(iterable, joiner=b''): - return tobytes(joiner).join(tobytes(item) for item in iterable) +def strjoin(iterable, joiner=""): + return tostr(joiner).join(iterable) -import os -import io as _io - -try: - from msvcrt import setmode as _setmode -except ImportError: - _setmode = None # only available on the Windows platform +def tobytes(s, encoding="ascii", errors="strict"): + if not isinstance(s, bytes): + return s.encode(encoding, errors) + else: + return s -def open(file, mode='r', buffering=-1, encoding=None, errors=None, - newline=None, closefd=True, opener=None): - """ Wrapper around `io.open` that bridges the differences between Python 2 - and Python 3's built-in `open` functions. In Python 2, `io.open` is a - backport of Python 3's `open`, whereas in Python 3, it is an alias of the - built-in `open` function. - - One difference is that the 'opener' keyword argument is only supported in - Python 3. Here we pass the value of 'opener' only when it is not None. - This causes Python 2 to raise TypeError, complaining about the number of - expected arguments, so it must be avoided in py2 or py2-3 contexts. - - Another difference between 2 and 3, this time on Windows, has to do with - opening files by name or by file descriptor. - - On the Windows C runtime, the 'O_BINARY' flag is defined which disables - the newlines translation ('\r\n' <=> '\n') when reading/writing files. - On both Python 2 and 3 this flag is always set when opening files by name. - This way, the newlines translation at the MSVCRT level doesn't interfere - with the Python io module's own newlines translation. - - However, when opening files via fd, on Python 2 the fd is simply copied, - regardless of whether it has the 'O_BINARY' flag set or not. - This becomes a problem in the case of stdout, stdin, and stderr, because on - Windows these are opened in text mode by default (ie. don't have the - O_BINARY flag set). - - On Python 3, this issue has been fixed, and all fds are now opened in - binary mode on Windows, including standard streams. Similarly here, I use - the `_setmode` function to ensure that integer file descriptors are - O_BINARY'ed before I pass them on to io.open. - - For more info, see: https://bugs.python.org/issue10841 - """ - if isinstance(file, int): - # the 'file' argument is an integer file descriptor - fd = file - if fd < 0: - raise ValueError('negative file descriptor') - if _setmode: - # `_setmode` function sets the line-end translation and returns the - # value of the previous mode. AFAIK there's no `_getmode`, so to - # check if the previous mode already had the bit set, I fist need - # to duplicate the file descriptor, set the binary flag on the copy - # and check the returned value. - fdcopy = os.dup(fd) - current_mode = _setmode(fdcopy, os.O_BINARY) - if not (current_mode & os.O_BINARY): - # the binary mode was not set: use the file descriptor's copy - file = fdcopy - if closefd: - # close the original file descriptor - os.close(fd) - else: - # ensure the copy is closed when the file object is closed - closefd = True - else: - # original file descriptor already had binary flag, close copy - os.close(fdcopy) - - if opener is not None: - # "opener" is not supported on Python 2, use it at your own risk! - return _io.open( - file, mode, buffering, encoding, errors, newline, closefd, - opener=opener) - else: - return _io.open( - file, mode, buffering, encoding, errors, newline, closefd) +def tounicode(s, encoding="ascii", errors="strict"): + if not isinstance(s, unicode): + return s.decode(encoding, errors) + else: + return s -# always use iterator for 'range' and 'zip' on both py 2 and 3 -try: - range = xrange -except NameError: - range = range +tostr = tounicode + + +class Tag(str): + @staticmethod + def transcode(blob): + if isinstance(blob, bytes): + blob = blob.decode("latin-1") + return blob + + def __new__(self, content): + return str.__new__(self, self.transcode(content)) + + def __ne__(self, other): + return not self.__eq__(other) + + def __eq__(self, other): + return str.__eq__(self, self.transcode(other)) + + def __hash__(self): + return str.__hash__(self) + + def tobytes(self): + return self.encode("latin-1") + + +def bytesjoin(iterable, joiner=b""): + return tobytes(joiner).join(tobytes(item) for item in iterable) + def xrange(*args, **kwargs): - raise Py23Error("'xrange' is not defined. Use 'range' instead.") - -try: - from itertools import izip as zip -except ImportError: - zip = zip + raise Py23Error("'xrange' is not defined. Use 'range' instead.") -import math as _math +def round2(number, ndigits=None): + """ + Implementation of Python 2 built-in round() function. + Rounds a number to a given precision in decimal digits (default + 0 digits). The result is a floating point number. Values are rounded + to the closest multiple of 10 to the power minus ndigits; if two + multiples are equally close, rounding is done away from 0. + ndigits may be negative. + See Python 2 documentation: + https://docs.python.org/2/library/functions.html?highlight=round#round + """ + if ndigits is None: + ndigits = 0 -try: - isclose = _math.isclose -except AttributeError: - # math.isclose() was only added in Python 3.5 + if ndigits < 0: + exponent = 10 ** (-ndigits) + quotient, remainder = divmod(number, exponent) + if remainder >= exponent // 2 and number >= 0: + quotient += 1 + return float(quotient * exponent) + else: + exponent = _decimal.Decimal("10") ** (-ndigits) - _isinf = _math.isinf - _fabs = _math.fabs + d = _decimal.Decimal.from_float(number).quantize( + exponent, rounding=_decimal.ROUND_HALF_UP + ) - def isclose(a, b, rel_tol=1e-09, abs_tol=0): - """ - Python 2 implementation of Python 3.5 math.isclose() - https://hg.python.org/cpython/file/v3.5.2/Modules/mathmodule.c#l1993 - """ - # sanity check on the inputs - if rel_tol < 0 or abs_tol < 0: - raise ValueError("tolerances must be non-negative") - # short circuit exact equality -- needed to catch two infinities of - # the same sign. And perhaps speeds things up a bit sometimes. - if a == b: - return True - # This catches the case of two infinities of opposite sign, or - # one infinity and one finite number. Two infinities of opposite - # sign would otherwise have an infinite relative tolerance. - # Two infinities of the same sign are caught by the equality check - # above. - if _isinf(a) or _isinf(b): - return False - # Cast to float to allow decimal.Decimal arguments - if not isinstance(a, float): - a = float(a) - if not isinstance(b, float): - b = float(b) - # now do the regular computation - # this is essentially the "weak" test from the Boost library - diff = _fabs(b - a) - result = ((diff <= _fabs(rel_tol * a)) or - (diff <= _fabs(rel_tol * b)) or - (diff <= abs_tol)) - return result - - -try: - _isfinite = _math.isfinite # Python >= 3.2 -except AttributeError: - _isfinite = None - _isnan = _math.isnan - _isinf = _math.isinf - - -def isfinite(f): - """ - >>> isfinite(0.0) - True - >>> isfinite(-0.1) - True - >>> isfinite(1e10) - True - >>> isfinite(float("nan")) - False - >>> isfinite(float("+inf")) - False - >>> isfinite(float("-inf")) - False - """ - if _isfinite is not None: - return _isfinite(f) - else: - return not (_isnan(f) or _isinf(f)) - - -import decimal as _decimal - -if PY3: - def round2(number, ndigits=None): - """ - Implementation of Python 2 built-in round() function. - - Rounds a number to a given precision in decimal digits (default - 0 digits). The result is a floating point number. Values are rounded - to the closest multiple of 10 to the power minus ndigits; if two - multiples are equally close, rounding is done away from 0. - - ndigits may be negative. - - See Python 2 documentation: - https://docs.python.org/2/library/functions.html?highlight=round#round - """ - if ndigits is None: - ndigits = 0 - - if ndigits < 0: - exponent = 10 ** (-ndigits) - quotient, remainder = divmod(number, exponent) - if remainder >= exponent//2 and number >= 0: - quotient += 1 - return float(quotient * exponent) - else: - exponent = _decimal.Decimal('10') ** (-ndigits) - - d = _decimal.Decimal.from_float(number).quantize( - exponent, rounding=_decimal.ROUND_HALF_UP) - - return float(d) - - if sys.version_info[:2] >= (3, 6): - # in Python 3.6, 'round3' is an alias to the built-in 'round' - round = round3 = round - else: - # in Python3 < 3.6 we need work around the inconsistent behavior of - # built-in round(), whereby floats accept a second None argument, - # while integers raise TypeError. See https://bugs.python.org/issue27936 - _round = round - - def round3(number, ndigits=None): - return _round(number) if ndigits is None else _round(number, ndigits) - - round = round3 - -else: - # in Python 2, 'round2' is an alias to the built-in 'round' and - # 'round' is shadowed by 'round3' - round2 = round - - def round3(number, ndigits=None): - """ - Implementation of Python 3 built-in round() function. - - Rounds a number to a given precision in decimal digits (default - 0 digits). This returns an int when ndigits is omitted or is None, - otherwise the same type as the number. - - Values are rounded to the closest multiple of 10 to the power minus - ndigits; if two multiples are equally close, rounding is done toward - the even choice (aka "Banker's Rounding"). For example, both round(0.5) - and round(-0.5) are 0, and round(1.5) is 2. - - ndigits may be negative. - - See Python 3 documentation: - https://docs.python.org/3/library/functions.html?highlight=round#round - - Derived from python-future: - https://github.com/PythonCharmers/python-future/blob/master/src/future/builtins/newround.py - """ - if ndigits is None: - ndigits = 0 - # return an int when called with one argument - totype = int - # shortcut if already an integer, or a float with no decimal digits - inumber = totype(number) - if inumber == number: - return inumber - else: - # return the same type as the number, when called with two arguments - totype = type(number) - - m = number * (10 ** ndigits) - # if number is half-way between two multiples, and the mutliple that is - # closer to zero is even, we use the (slow) pure-Python implementation - if isclose(m % 1, .5) and int(m) % 2 == 0: - if ndigits < 0: - exponent = 10 ** (-ndigits) - quotient, remainder = divmod(number, exponent) - half = exponent//2 - if remainder > half or (remainder == half and quotient % 2 != 0): - quotient += 1 - d = quotient * exponent - else: - exponent = _decimal.Decimal('10') ** (-ndigits) if ndigits != 0 else 1 - - d = _decimal.Decimal.from_float(number).quantize( - exponent, rounding=_decimal.ROUND_HALF_EVEN) - else: - # else we use the built-in round() as it produces the same results - d = round2(number, ndigits) - - return totype(d) - - round = round3 - - -try: - from types import SimpleNamespace -except ImportError: - class SimpleNamespace(object): - """ - A backport of Python 3.3's ``types.SimpleNamespace``. - """ - def __init__(self, **kwargs): - self.__dict__.update(kwargs) - - def __repr__(self): - keys = sorted(self.__dict__) - items = ("{0}={1!r}".format(k, self.__dict__[k]) for k in keys) - return "{0}({1})".format(type(self).__name__, ", ".join(items)) - - def __eq__(self, other): - return self.__dict__ == other.__dict__ - - -if sys.version_info[:2] > (3, 4): - from contextlib import redirect_stdout, redirect_stderr -else: - # `redirect_stdout` was added with python3.4, while `redirect_stderr` - # with python3.5. For simplicity, I redefine both for any versions - # less than or equal to 3.4. - # The code below is copied from: - # https://github.com/python/cpython/blob/57161aa/Lib/contextlib.py - - class _RedirectStream(object): - - _stream = None - - def __init__(self, new_target): - self._new_target = new_target - # We use a list of old targets to make this CM re-entrant - self._old_targets = [] - - def __enter__(self): - self._old_targets.append(getattr(sys, self._stream)) - setattr(sys, self._stream, self._new_target) - return self._new_target - - def __exit__(self, exctype, excinst, exctb): - setattr(sys, self._stream, self._old_targets.pop()) - - - class redirect_stdout(_RedirectStream): - """Context manager for temporarily redirecting stdout to another file. - # How to send help() to stderr - with redirect_stdout(sys.stderr): - help(dir) - # How to write help() to a file - with open('help.txt', 'w') as f: - with redirect_stdout(f): - help(pow) - """ - - _stream = "stdout" - - - class redirect_stderr(_RedirectStream): - """Context manager for temporarily redirecting stderr to another file.""" - - _stream = "stderr" - - -try: - RecursionError = RecursionError -except NameError: - RecursionError = RuntimeError - - -if __name__ == "__main__": - import doctest, sys - sys.exit(doctest.testmod().failed) + return float(d)