diff --git a/.gitattributes b/.gitattributes index f8ef6ff9a6718a5181efefa44b504efaf41410c5..bae5aac131ffc99b6ca2595c873615046c9a60c5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -71,3 +71,14 @@ hug/lib/python3.12/site-packages/cuda/bindings/nvrtc.cpython-312-x86_64-linux-gn hug/lib/python3.12/site-packages/cuda/bindings/nvvm.cpython-312-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text hug/lib/python3.12/site-packages/cuda/bindings/runtime.cpython-312-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text hug/lib/python3.12/site-packages/cv2/cv2.abi3.so filter=lfs diff=lfs merge=lfs -text +hug/lib/python3.12/site-packages/filelock/__pycache__/_api.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text +hug/lib/python3.12/site-packages/fontTools/__pycache__/agl.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text +hug/lib/python3.12/site-packages/fontTools/cffLib/__pycache__/__init__.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text +hug/lib/python3.12/site-packages/fontTools/cu2qu/cu2qu.cpython-312-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text +hug/lib/python3.12/site-packages/fontTools/designspaceLib/__pycache__/__init__.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text +hug/lib/python3.12/site-packages/fontTools/feaLib/__pycache__/ast.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text +hug/lib/python3.12/site-packages/fontTools/feaLib/__pycache__/parser.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text +hug/lib/python3.12/site-packages/fontTools/feaLib/lexer.cpython-312-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text +hug/lib/python3.12/site-packages/fontTools/misc/bezierTools.cpython-312-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text +hug/lib/python3.12/site-packages/fontTools/otlLib/__pycache__/builder.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text +hug/lib/python3.12/site-packages/fontTools/pens/momentsPen.cpython-312-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text diff --git a/hug/lib/python3.12/site-packages/dateutil/parser/__init__.py b/hug/lib/python3.12/site-packages/dateutil/parser/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d174b0e4dcc472999b75e55ebb88af320ae38081 --- /dev/null +++ b/hug/lib/python3.12/site-packages/dateutil/parser/__init__.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +from ._parser import parse, parser, parserinfo, ParserError +from ._parser import DEFAULTPARSER, DEFAULTTZPARSER +from ._parser import UnknownTimezoneWarning + +from ._parser import __doc__ + +from .isoparser import isoparser, isoparse + +__all__ = ['parse', 'parser', 'parserinfo', + 'isoparse', 'isoparser', + 'ParserError', + 'UnknownTimezoneWarning'] + + +### +# Deprecate portions of the private interface so that downstream code that +# is improperly relying on it is given *some* notice. + + +def __deprecated_private_func(f): + from functools import wraps + import warnings + + msg = ('{name} is a private function and may break without warning, ' + 'it will be moved and or renamed in future versions.') + msg = msg.format(name=f.__name__) + + @wraps(f) + def deprecated_func(*args, **kwargs): + warnings.warn(msg, DeprecationWarning) + return f(*args, **kwargs) + + return deprecated_func + +def __deprecate_private_class(c): + import warnings + + msg = ('{name} is a private class and may break without warning, ' + 'it will be moved and or renamed in future versions.') + msg = msg.format(name=c.__name__) + + class private_class(c): + __doc__ = c.__doc__ + + def __init__(self, *args, **kwargs): + warnings.warn(msg, DeprecationWarning) + super(private_class, self).__init__(*args, **kwargs) + + private_class.__name__ = c.__name__ + + return private_class + + +from ._parser import _timelex, _resultbase +from ._parser import _tzparser, _parsetz + +_timelex = __deprecate_private_class(_timelex) +_tzparser = __deprecate_private_class(_tzparser) +_resultbase = __deprecate_private_class(_resultbase) +_parsetz = __deprecated_private_func(_parsetz) diff --git a/hug/lib/python3.12/site-packages/dateutil/parser/__pycache__/__init__.cpython-312.pyc b/hug/lib/python3.12/site-packages/dateutil/parser/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cbbfd3bad8dee028abcc670cc66cddc435849363 Binary files /dev/null and b/hug/lib/python3.12/site-packages/dateutil/parser/__pycache__/__init__.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/dateutil/parser/__pycache__/_parser.cpython-312.pyc b/hug/lib/python3.12/site-packages/dateutil/parser/__pycache__/_parser.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..51ec62f9953d5551bf583dc8e6a5e09f44a68c77 Binary files /dev/null and b/hug/lib/python3.12/site-packages/dateutil/parser/__pycache__/_parser.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/dateutil/parser/__pycache__/isoparser.cpython-312.pyc b/hug/lib/python3.12/site-packages/dateutil/parser/__pycache__/isoparser.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..53006aaa5488c985d11351e71ee6e6790c4b407d Binary files /dev/null and b/hug/lib/python3.12/site-packages/dateutil/parser/__pycache__/isoparser.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/dateutil/parser/_parser.py b/hug/lib/python3.12/site-packages/dateutil/parser/_parser.py new file mode 100644 index 0000000000000000000000000000000000000000..37d1663b2f72447800d9a553929e3de932244289 --- /dev/null +++ b/hug/lib/python3.12/site-packages/dateutil/parser/_parser.py @@ -0,0 +1,1613 @@ +# -*- coding: utf-8 -*- +""" +This module offers a generic date/time string parser which is able to parse +most known formats to represent a date and/or time. + +This module attempts to be forgiving with regards to unlikely input formats, +returning a datetime object even for dates which are ambiguous. If an element +of a date/time stamp is omitted, the following rules are applied: + +- If AM or PM is left unspecified, a 24-hour clock is assumed, however, an hour + on a 12-hour clock (``0 <= hour <= 12``) *must* be specified if AM or PM is + specified. +- If a time zone is omitted, a timezone-naive datetime is returned. + +If any other elements are missing, they are taken from the +:class:`datetime.datetime` object passed to the parameter ``default``. If this +results in a day number exceeding the valid number of days per month, the +value falls back to the end of the month. + +Additional resources about date/time string formats can be found below: + +- `A summary of the international standard date and time notation + `_ +- `W3C Date and Time Formats `_ +- `Time Formats (Planetary Rings Node) `_ +- `CPAN ParseDate module + `_ +- `Java SimpleDateFormat Class + `_ +""" +from __future__ import unicode_literals + +import datetime +import re +import string +import time +import warnings + +from calendar import monthrange +from io import StringIO + +import six +from six import integer_types, text_type + +from decimal import Decimal + +from warnings import warn + +from .. import relativedelta +from .. import tz + +__all__ = ["parse", "parserinfo", "ParserError"] + + +# TODO: pandas.core.tools.datetimes imports this explicitly. Might be worth +# making public and/or figuring out if there is something we can +# take off their plate. +class _timelex(object): + # Fractional seconds are sometimes split by a comma + _split_decimal = re.compile("([.,])") + + def __init__(self, instream): + if isinstance(instream, (bytes, bytearray)): + instream = instream.decode() + + if isinstance(instream, text_type): + instream = StringIO(instream) + elif getattr(instream, 'read', None) is None: + raise TypeError('Parser must be a string or character stream, not ' + '{itype}'.format(itype=instream.__class__.__name__)) + + self.instream = instream + self.charstack = [] + self.tokenstack = [] + self.eof = False + + def get_token(self): + """ + This function breaks the time string into lexical units (tokens), which + can be parsed by the parser. Lexical units are demarcated by changes in + the character set, so any continuous string of letters is considered + one unit, any continuous string of numbers is considered one unit. + + The main complication arises from the fact that dots ('.') can be used + both as separators (e.g. "Sep.20.2009") or decimal points (e.g. + "4:30:21.447"). As such, it is necessary to read the full context of + any dot-separated strings before breaking it into tokens; as such, this + function maintains a "token stack", for when the ambiguous context + demands that multiple tokens be parsed at once. + """ + if self.tokenstack: + return self.tokenstack.pop(0) + + seenletters = False + token = None + state = None + + while not self.eof: + # We only realize that we've reached the end of a token when we + # find a character that's not part of the current token - since + # that character may be part of the next token, it's stored in the + # charstack. + if self.charstack: + nextchar = self.charstack.pop(0) + else: + nextchar = self.instream.read(1) + while nextchar == '\x00': + nextchar = self.instream.read(1) + + if not nextchar: + self.eof = True + break + elif not state: + # First character of the token - determines if we're starting + # to parse a word, a number or something else. + token = nextchar + if self.isword(nextchar): + state = 'a' + elif self.isnum(nextchar): + state = '0' + elif self.isspace(nextchar): + token = ' ' + break # emit token + else: + break # emit token + elif state == 'a': + # If we've already started reading a word, we keep reading + # letters until we find something that's not part of a word. + seenletters = True + if self.isword(nextchar): + token += nextchar + elif nextchar == '.': + token += nextchar + state = 'a.' + else: + self.charstack.append(nextchar) + break # emit token + elif state == '0': + # If we've already started reading a number, we keep reading + # numbers until we find something that doesn't fit. + if self.isnum(nextchar): + token += nextchar + elif nextchar == '.' or (nextchar == ',' and len(token) >= 2): + token += nextchar + state = '0.' + else: + self.charstack.append(nextchar) + break # emit token + elif state == 'a.': + # If we've seen some letters and a dot separator, continue + # parsing, and the tokens will be broken up later. + seenletters = True + if nextchar == '.' or self.isword(nextchar): + token += nextchar + elif self.isnum(nextchar) and token[-1] == '.': + token += nextchar + state = '0.' + else: + self.charstack.append(nextchar) + break # emit token + elif state == '0.': + # If we've seen at least one dot separator, keep going, we'll + # break up the tokens later. + if nextchar == '.' or self.isnum(nextchar): + token += nextchar + elif self.isword(nextchar) and token[-1] == '.': + token += nextchar + state = 'a.' + else: + self.charstack.append(nextchar) + break # emit token + + if (state in ('a.', '0.') and (seenletters or token.count('.') > 1 or + token[-1] in '.,')): + l = self._split_decimal.split(token) + token = l[0] + for tok in l[1:]: + if tok: + self.tokenstack.append(tok) + + if state == '0.' and token.count('.') == 0: + token = token.replace(',', '.') + + return token + + def __iter__(self): + return self + + def __next__(self): + token = self.get_token() + if token is None: + raise StopIteration + + return token + + def next(self): + return self.__next__() # Python 2.x support + + @classmethod + def split(cls, s): + return list(cls(s)) + + @classmethod + def isword(cls, nextchar): + """ Whether or not the next character is part of a word """ + return nextchar.isalpha() + + @classmethod + def isnum(cls, nextchar): + """ Whether the next character is part of a number """ + return nextchar.isdigit() + + @classmethod + def isspace(cls, nextchar): + """ Whether the next character is whitespace """ + return nextchar.isspace() + + +class _resultbase(object): + + def __init__(self): + for attr in self.__slots__: + setattr(self, attr, None) + + def _repr(self, classname): + l = [] + for attr in self.__slots__: + value = getattr(self, attr) + if value is not None: + l.append("%s=%s" % (attr, repr(value))) + return "%s(%s)" % (classname, ", ".join(l)) + + def __len__(self): + return (sum(getattr(self, attr) is not None + for attr in self.__slots__)) + + def __repr__(self): + return self._repr(self.__class__.__name__) + + +class parserinfo(object): + """ + Class which handles what inputs are accepted. Subclass this to customize + the language and acceptable values for each parameter. + + :param dayfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the day (``True``) or month (``False``). If + ``yearfirst`` is set to ``True``, this distinguishes between YDM + and YMD. Default is ``False``. + + :param yearfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the year. If ``True``, the first number is taken + to be the year, otherwise the last number is taken to be the year. + Default is ``False``. + """ + + # m from a.m/p.m, t from ISO T separator + JUMP = [" ", ".", ",", ";", "-", "/", "'", + "at", "on", "and", "ad", "m", "t", "of", + "st", "nd", "rd", "th"] + + WEEKDAYS = [("Mon", "Monday"), + ("Tue", "Tuesday"), # TODO: "Tues" + ("Wed", "Wednesday"), + ("Thu", "Thursday"), # TODO: "Thurs" + ("Fri", "Friday"), + ("Sat", "Saturday"), + ("Sun", "Sunday")] + MONTHS = [("Jan", "January"), + ("Feb", "February"), # TODO: "Febr" + ("Mar", "March"), + ("Apr", "April"), + ("May", "May"), + ("Jun", "June"), + ("Jul", "July"), + ("Aug", "August"), + ("Sep", "Sept", "September"), + ("Oct", "October"), + ("Nov", "November"), + ("Dec", "December")] + HMS = [("h", "hour", "hours"), + ("m", "minute", "minutes"), + ("s", "second", "seconds")] + AMPM = [("am", "a"), + ("pm", "p")] + UTCZONE = ["UTC", "GMT", "Z", "z"] + PERTAIN = ["of"] + TZOFFSET = {} + # TODO: ERA = ["AD", "BC", "CE", "BCE", "Stardate", + # "Anno Domini", "Year of Our Lord"] + + def __init__(self, dayfirst=False, yearfirst=False): + self._jump = self._convert(self.JUMP) + self._weekdays = self._convert(self.WEEKDAYS) + self._months = self._convert(self.MONTHS) + self._hms = self._convert(self.HMS) + self._ampm = self._convert(self.AMPM) + self._utczone = self._convert(self.UTCZONE) + self._pertain = self._convert(self.PERTAIN) + + self.dayfirst = dayfirst + self.yearfirst = yearfirst + + self._year = time.localtime().tm_year + self._century = self._year // 100 * 100 + + def _convert(self, lst): + dct = {} + for i, v in enumerate(lst): + if isinstance(v, tuple): + for v in v: + dct[v.lower()] = i + else: + dct[v.lower()] = i + return dct + + def jump(self, name): + return name.lower() in self._jump + + def weekday(self, name): + try: + return self._weekdays[name.lower()] + except KeyError: + pass + return None + + def month(self, name): + try: + return self._months[name.lower()] + 1 + except KeyError: + pass + return None + + def hms(self, name): + try: + return self._hms[name.lower()] + except KeyError: + return None + + def ampm(self, name): + try: + return self._ampm[name.lower()] + except KeyError: + return None + + def pertain(self, name): + return name.lower() in self._pertain + + def utczone(self, name): + return name.lower() in self._utczone + + def tzoffset(self, name): + if name in self._utczone: + return 0 + + return self.TZOFFSET.get(name) + + def convertyear(self, year, century_specified=False): + """ + Converts two-digit years to year within [-50, 49] + range of self._year (current local time) + """ + + # Function contract is that the year is always positive + assert year >= 0 + + if year < 100 and not century_specified: + # assume current century to start + year += self._century + + if year >= self._year + 50: # if too far in future + year -= 100 + elif year < self._year - 50: # if too far in past + year += 100 + + return year + + def validate(self, res): + # move to info + if res.year is not None: + res.year = self.convertyear(res.year, res.century_specified) + + if ((res.tzoffset == 0 and not res.tzname) or + (res.tzname == 'Z' or res.tzname == 'z')): + res.tzname = "UTC" + res.tzoffset = 0 + elif res.tzoffset != 0 and res.tzname and self.utczone(res.tzname): + res.tzoffset = 0 + return True + + +class _ymd(list): + def __init__(self, *args, **kwargs): + super(self.__class__, self).__init__(*args, **kwargs) + self.century_specified = False + self.dstridx = None + self.mstridx = None + self.ystridx = None + + @property + def has_year(self): + return self.ystridx is not None + + @property + def has_month(self): + return self.mstridx is not None + + @property + def has_day(self): + return self.dstridx is not None + + def could_be_day(self, value): + if self.has_day: + return False + elif not self.has_month: + return 1 <= value <= 31 + elif not self.has_year: + # Be permissive, assume leap year + month = self[self.mstridx] + return 1 <= value <= monthrange(2000, month)[1] + else: + month = self[self.mstridx] + year = self[self.ystridx] + return 1 <= value <= monthrange(year, month)[1] + + def append(self, val, label=None): + if hasattr(val, '__len__'): + if val.isdigit() and len(val) > 2: + self.century_specified = True + if label not in [None, 'Y']: # pragma: no cover + raise ValueError(label) + label = 'Y' + elif val > 100: + self.century_specified = True + if label not in [None, 'Y']: # pragma: no cover + raise ValueError(label) + label = 'Y' + + super(self.__class__, self).append(int(val)) + + if label == 'M': + if self.has_month: + raise ValueError('Month is already set') + self.mstridx = len(self) - 1 + elif label == 'D': + if self.has_day: + raise ValueError('Day is already set') + self.dstridx = len(self) - 1 + elif label == 'Y': + if self.has_year: + raise ValueError('Year is already set') + self.ystridx = len(self) - 1 + + def _resolve_from_stridxs(self, strids): + """ + Try to resolve the identities of year/month/day elements using + ystridx, mstridx, and dstridx, if enough of these are specified. + """ + if len(self) == 3 and len(strids) == 2: + # we can back out the remaining stridx value + missing = [x for x in range(3) if x not in strids.values()] + key = [x for x in ['y', 'm', 'd'] if x not in strids] + assert len(missing) == len(key) == 1 + key = key[0] + val = missing[0] + strids[key] = val + + assert len(self) == len(strids) # otherwise this should not be called + out = {key: self[strids[key]] for key in strids} + return (out.get('y'), out.get('m'), out.get('d')) + + def resolve_ymd(self, yearfirst, dayfirst): + len_ymd = len(self) + year, month, day = (None, None, None) + + strids = (('y', self.ystridx), + ('m', self.mstridx), + ('d', self.dstridx)) + + strids = {key: val for key, val in strids if val is not None} + if (len(self) == len(strids) > 0 or + (len(self) == 3 and len(strids) == 2)): + return self._resolve_from_stridxs(strids) + + mstridx = self.mstridx + + if len_ymd > 3: + raise ValueError("More than three YMD values") + elif len_ymd == 1 or (mstridx is not None and len_ymd == 2): + # One member, or two members with a month string + if mstridx is not None: + month = self[mstridx] + # since mstridx is 0 or 1, self[mstridx-1] always + # looks up the other element + other = self[mstridx - 1] + else: + other = self[0] + + if len_ymd > 1 or mstridx is None: + if other > 31: + year = other + else: + day = other + + elif len_ymd == 2: + # Two members with numbers + if self[0] > 31: + # 99-01 + year, month = self + elif self[1] > 31: + # 01-99 + month, year = self + elif dayfirst and self[1] <= 12: + # 13-01 + day, month = self + else: + # 01-13 + month, day = self + + elif len_ymd == 3: + # Three members + if mstridx == 0: + if self[1] > 31: + # Apr-2003-25 + month, year, day = self + else: + month, day, year = self + elif mstridx == 1: + if self[0] > 31 or (yearfirst and self[2] <= 31): + # 99-Jan-01 + year, month, day = self + else: + # 01-Jan-01 + # Give precedence to day-first, since + # two-digit years is usually hand-written. + day, month, year = self + + elif mstridx == 2: + # WTF!? + if self[1] > 31: + # 01-99-Jan + day, year, month = self + else: + # 99-01-Jan + year, day, month = self + + else: + if (self[0] > 31 or + self.ystridx == 0 or + (yearfirst and self[1] <= 12 and self[2] <= 31)): + # 99-01-01 + if dayfirst and self[2] <= 12: + year, day, month = self + else: + year, month, day = self + elif self[0] > 12 or (dayfirst and self[1] <= 12): + # 13-01-01 + day, month, year = self + else: + # 01-13-01 + month, day, year = self + + return year, month, day + + +class parser(object): + def __init__(self, info=None): + self.info = info or parserinfo() + + def parse(self, timestr, default=None, + ignoretz=False, tzinfos=None, **kwargs): + """ + Parse the date/time string into a :class:`datetime.datetime` object. + + :param timestr: + Any date/time string using the supported formats. + + :param default: + The default datetime object, if this is a datetime object and not + ``None``, elements specified in ``timestr`` replace elements in the + default object. + + :param ignoretz: + If set ``True``, time zones in parsed strings are ignored and a + naive :class:`datetime.datetime` object is returned. + + :param tzinfos: + Additional time zone names / aliases which may be present in the + string. This argument maps time zone names (and optionally offsets + from those time zones) to time zones. This parameter can be a + dictionary with timezone aliases mapping time zone names to time + zones or a function taking two parameters (``tzname`` and + ``tzoffset``) and returning a time zone. + + The timezones to which the names are mapped can be an integer + offset from UTC in seconds or a :class:`tzinfo` object. + + .. doctest:: + :options: +NORMALIZE_WHITESPACE + + >>> from dateutil.parser import parse + >>> from dateutil.tz import gettz + >>> tzinfos = {"BRST": -7200, "CST": gettz("America/Chicago")} + >>> parse("2012-01-19 17:21:00 BRST", tzinfos=tzinfos) + datetime.datetime(2012, 1, 19, 17, 21, tzinfo=tzoffset(u'BRST', -7200)) + >>> parse("2012-01-19 17:21:00 CST", tzinfos=tzinfos) + datetime.datetime(2012, 1, 19, 17, 21, + tzinfo=tzfile('/usr/share/zoneinfo/America/Chicago')) + + This parameter is ignored if ``ignoretz`` is set. + + :param \\*\\*kwargs: + Keyword arguments as passed to ``_parse()``. + + :return: + Returns a :class:`datetime.datetime` object or, if the + ``fuzzy_with_tokens`` option is ``True``, returns a tuple, the + first element being a :class:`datetime.datetime` object, the second + a tuple containing the fuzzy tokens. + + :raises ParserError: + Raised for invalid or unknown string format, if the provided + :class:`tzinfo` is not in a valid format, or if an invalid date + would be created. + + :raises TypeError: + Raised for non-string or character stream input. + + :raises OverflowError: + Raised if the parsed date exceeds the largest valid C integer on + your system. + """ + + if default is None: + default = datetime.datetime.now().replace(hour=0, minute=0, + second=0, microsecond=0) + + res, skipped_tokens = self._parse(timestr, **kwargs) + + if res is None: + raise ParserError("Unknown string format: %s", timestr) + + if len(res) == 0: + raise ParserError("String does not contain a date: %s", timestr) + + try: + ret = self._build_naive(res, default) + except ValueError as e: + six.raise_from(ParserError(str(e) + ": %s", timestr), e) + + if not ignoretz: + ret = self._build_tzaware(ret, res, tzinfos) + + if kwargs.get('fuzzy_with_tokens', False): + return ret, skipped_tokens + else: + return ret + + class _result(_resultbase): + __slots__ = ["year", "month", "day", "weekday", + "hour", "minute", "second", "microsecond", + "tzname", "tzoffset", "ampm","any_unused_tokens"] + + def _parse(self, timestr, dayfirst=None, yearfirst=None, fuzzy=False, + fuzzy_with_tokens=False): + """ + Private method which performs the heavy lifting of parsing, called from + ``parse()``, which passes on its ``kwargs`` to this function. + + :param timestr: + The string to parse. + + :param dayfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the day (``True``) or month (``False``). If + ``yearfirst`` is set to ``True``, this distinguishes between YDM + and YMD. If set to ``None``, this value is retrieved from the + current :class:`parserinfo` object (which itself defaults to + ``False``). + + :param yearfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the year. If ``True``, the first number is taken + to be the year, otherwise the last number is taken to be the year. + If this is set to ``None``, the value is retrieved from the current + :class:`parserinfo` object (which itself defaults to ``False``). + + :param fuzzy: + Whether to allow fuzzy parsing, allowing for string like "Today is + January 1, 2047 at 8:21:00AM". + + :param fuzzy_with_tokens: + If ``True``, ``fuzzy`` is automatically set to True, and the parser + will return a tuple where the first element is the parsed + :class:`datetime.datetime` datetimestamp and the second element is + a tuple containing the portions of the string which were ignored: + + .. doctest:: + + >>> from dateutil.parser import parse + >>> parse("Today is January 1, 2047 at 8:21:00AM", fuzzy_with_tokens=True) + (datetime.datetime(2047, 1, 1, 8, 21), (u'Today is ', u' ', u'at ')) + + """ + if fuzzy_with_tokens: + fuzzy = True + + info = self.info + + if dayfirst is None: + dayfirst = info.dayfirst + + if yearfirst is None: + yearfirst = info.yearfirst + + res = self._result() + l = _timelex.split(timestr) # Splits the timestr into tokens + + skipped_idxs = [] + + # year/month/day list + ymd = _ymd() + + len_l = len(l) + i = 0 + try: + while i < len_l: + + # Check if it's a number + value_repr = l[i] + try: + value = float(value_repr) + except ValueError: + value = None + + if value is not None: + # Numeric token + i = self._parse_numeric_token(l, i, info, ymd, res, fuzzy) + + # Check weekday + elif info.weekday(l[i]) is not None: + value = info.weekday(l[i]) + res.weekday = value + + # Check month name + elif info.month(l[i]) is not None: + value = info.month(l[i]) + ymd.append(value, 'M') + + if i + 1 < len_l: + if l[i + 1] in ('-', '/'): + # Jan-01[-99] + sep = l[i + 1] + ymd.append(l[i + 2]) + + if i + 3 < len_l and l[i + 3] == sep: + # Jan-01-99 + ymd.append(l[i + 4]) + i += 2 + + i += 2 + + elif (i + 4 < len_l and l[i + 1] == l[i + 3] == ' ' and + info.pertain(l[i + 2])): + # Jan of 01 + # In this case, 01 is clearly year + if l[i + 4].isdigit(): + # Convert it here to become unambiguous + value = int(l[i + 4]) + year = str(info.convertyear(value)) + ymd.append(year, 'Y') + else: + # Wrong guess + pass + # TODO: not hit in tests + i += 4 + + # Check am/pm + elif info.ampm(l[i]) is not None: + value = info.ampm(l[i]) + val_is_ampm = self._ampm_valid(res.hour, res.ampm, fuzzy) + + if val_is_ampm: + res.hour = self._adjust_ampm(res.hour, value) + res.ampm = value + + elif fuzzy: + skipped_idxs.append(i) + + # Check for a timezone name + elif self._could_be_tzname(res.hour, res.tzname, res.tzoffset, l[i]): + res.tzname = l[i] + res.tzoffset = info.tzoffset(res.tzname) + + # Check for something like GMT+3, or BRST+3. Notice + # that it doesn't mean "I am 3 hours after GMT", but + # "my time +3 is GMT". If found, we reverse the + # logic so that timezone parsing code will get it + # right. + if i + 1 < len_l and l[i + 1] in ('+', '-'): + l[i + 1] = ('+', '-')[l[i + 1] == '+'] + res.tzoffset = None + if info.utczone(res.tzname): + # With something like GMT+3, the timezone + # is *not* GMT. + res.tzname = None + + # Check for a numbered timezone + elif res.hour is not None and l[i] in ('+', '-'): + signal = (-1, 1)[l[i] == '+'] + len_li = len(l[i + 1]) + + # TODO: check that l[i + 1] is integer? + if len_li == 4: + # -0300 + hour_offset = int(l[i + 1][:2]) + min_offset = int(l[i + 1][2:]) + elif i + 2 < len_l and l[i + 2] == ':': + # -03:00 + hour_offset = int(l[i + 1]) + min_offset = int(l[i + 3]) # TODO: Check that l[i+3] is minute-like? + i += 2 + elif len_li <= 2: + # -[0]3 + hour_offset = int(l[i + 1][:2]) + min_offset = 0 + else: + raise ValueError(timestr) + + res.tzoffset = signal * (hour_offset * 3600 + min_offset * 60) + + # Look for a timezone name between parenthesis + if (i + 5 < len_l and + info.jump(l[i + 2]) and l[i + 3] == '(' and + l[i + 5] == ')' and + 3 <= len(l[i + 4]) and + self._could_be_tzname(res.hour, res.tzname, + None, l[i + 4])): + # -0300 (BRST) + res.tzname = l[i + 4] + i += 4 + + i += 1 + + # Check jumps + elif not (info.jump(l[i]) or fuzzy): + raise ValueError(timestr) + + else: + skipped_idxs.append(i) + i += 1 + + # Process year/month/day + year, month, day = ymd.resolve_ymd(yearfirst, dayfirst) + + res.century_specified = ymd.century_specified + res.year = year + res.month = month + res.day = day + + except (IndexError, ValueError): + return None, None + + if not info.validate(res): + return None, None + + if fuzzy_with_tokens: + skipped_tokens = self._recombine_skipped(l, skipped_idxs) + return res, tuple(skipped_tokens) + else: + return res, None + + def _parse_numeric_token(self, tokens, idx, info, ymd, res, fuzzy): + # Token is a number + value_repr = tokens[idx] + try: + value = self._to_decimal(value_repr) + except Exception as e: + six.raise_from(ValueError('Unknown numeric token'), e) + + len_li = len(value_repr) + + len_l = len(tokens) + + if (len(ymd) == 3 and len_li in (2, 4) and + res.hour is None and + (idx + 1 >= len_l or + (tokens[idx + 1] != ':' and + info.hms(tokens[idx + 1]) is None))): + # 19990101T23[59] + s = tokens[idx] + res.hour = int(s[:2]) + + if len_li == 4: + res.minute = int(s[2:]) + + elif len_li == 6 or (len_li > 6 and tokens[idx].find('.') == 6): + # YYMMDD or HHMMSS[.ss] + s = tokens[idx] + + if not ymd and '.' not in tokens[idx]: + ymd.append(s[:2]) + ymd.append(s[2:4]) + ymd.append(s[4:]) + else: + # 19990101T235959[.59] + + # TODO: Check if res attributes already set. + res.hour = int(s[:2]) + res.minute = int(s[2:4]) + res.second, res.microsecond = self._parsems(s[4:]) + + elif len_li in (8, 12, 14): + # YYYYMMDD + s = tokens[idx] + ymd.append(s[:4], 'Y') + ymd.append(s[4:6]) + ymd.append(s[6:8]) + + if len_li > 8: + res.hour = int(s[8:10]) + res.minute = int(s[10:12]) + + if len_li > 12: + res.second = int(s[12:]) + + elif self._find_hms_idx(idx, tokens, info, allow_jump=True) is not None: + # HH[ ]h or MM[ ]m or SS[.ss][ ]s + hms_idx = self._find_hms_idx(idx, tokens, info, allow_jump=True) + (idx, hms) = self._parse_hms(idx, tokens, info, hms_idx) + if hms is not None: + # TODO: checking that hour/minute/second are not + # already set? + self._assign_hms(res, value_repr, hms) + + elif idx + 2 < len_l and tokens[idx + 1] == ':': + # HH:MM[:SS[.ss]] + res.hour = int(value) + value = self._to_decimal(tokens[idx + 2]) # TODO: try/except for this? + (res.minute, res.second) = self._parse_min_sec(value) + + if idx + 4 < len_l and tokens[idx + 3] == ':': + res.second, res.microsecond = self._parsems(tokens[idx + 4]) + + idx += 2 + + idx += 2 + + elif idx + 1 < len_l and tokens[idx + 1] in ('-', '/', '.'): + sep = tokens[idx + 1] + ymd.append(value_repr) + + if idx + 2 < len_l and not info.jump(tokens[idx + 2]): + if tokens[idx + 2].isdigit(): + # 01-01[-01] + ymd.append(tokens[idx + 2]) + else: + # 01-Jan[-01] + value = info.month(tokens[idx + 2]) + + if value is not None: + ymd.append(value, 'M') + else: + raise ValueError() + + if idx + 3 < len_l and tokens[idx + 3] == sep: + # We have three members + value = info.month(tokens[idx + 4]) + + if value is not None: + ymd.append(value, 'M') + else: + ymd.append(tokens[idx + 4]) + idx += 2 + + idx += 1 + idx += 1 + + elif idx + 1 >= len_l or info.jump(tokens[idx + 1]): + if idx + 2 < len_l and info.ampm(tokens[idx + 2]) is not None: + # 12 am + hour = int(value) + res.hour = self._adjust_ampm(hour, info.ampm(tokens[idx + 2])) + idx += 1 + else: + # Year, month or day + ymd.append(value) + idx += 1 + + elif info.ampm(tokens[idx + 1]) is not None and (0 <= value < 24): + # 12am + hour = int(value) + res.hour = self._adjust_ampm(hour, info.ampm(tokens[idx + 1])) + idx += 1 + + elif ymd.could_be_day(value): + ymd.append(value) + + elif not fuzzy: + raise ValueError() + + return idx + + def _find_hms_idx(self, idx, tokens, info, allow_jump): + len_l = len(tokens) + + if idx+1 < len_l and info.hms(tokens[idx+1]) is not None: + # There is an "h", "m", or "s" label following this token. We take + # assign the upcoming label to the current token. + # e.g. the "12" in 12h" + hms_idx = idx + 1 + + elif (allow_jump and idx+2 < len_l and tokens[idx+1] == ' ' and + info.hms(tokens[idx+2]) is not None): + # There is a space and then an "h", "m", or "s" label. + # e.g. the "12" in "12 h" + hms_idx = idx + 2 + + elif idx > 0 and info.hms(tokens[idx-1]) is not None: + # There is a "h", "m", or "s" preceding this token. Since neither + # of the previous cases was hit, there is no label following this + # token, so we use the previous label. + # e.g. the "04" in "12h04" + hms_idx = idx-1 + + elif (1 < idx == len_l-1 and tokens[idx-1] == ' ' and + info.hms(tokens[idx-2]) is not None): + # If we are looking at the final token, we allow for a + # backward-looking check to skip over a space. + # TODO: Are we sure this is the right condition here? + hms_idx = idx - 2 + + else: + hms_idx = None + + return hms_idx + + def _assign_hms(self, res, value_repr, hms): + # See GH issue #427, fixing float rounding + value = self._to_decimal(value_repr) + + if hms == 0: + # Hour + res.hour = int(value) + if value % 1: + res.minute = int(60*(value % 1)) + + elif hms == 1: + (res.minute, res.second) = self._parse_min_sec(value) + + elif hms == 2: + (res.second, res.microsecond) = self._parsems(value_repr) + + def _could_be_tzname(self, hour, tzname, tzoffset, token): + return (hour is not None and + tzname is None and + tzoffset is None and + len(token) <= 5 and + (all(x in string.ascii_uppercase for x in token) + or token in self.info.UTCZONE)) + + def _ampm_valid(self, hour, ampm, fuzzy): + """ + For fuzzy parsing, 'a' or 'am' (both valid English words) + may erroneously trigger the AM/PM flag. Deal with that + here. + """ + val_is_ampm = True + + # If there's already an AM/PM flag, this one isn't one. + if fuzzy and ampm is not None: + val_is_ampm = False + + # If AM/PM is found and hour is not, raise a ValueError + if hour is None: + if fuzzy: + val_is_ampm = False + else: + raise ValueError('No hour specified with AM or PM flag.') + elif not 0 <= hour <= 12: + # If AM/PM is found, it's a 12 hour clock, so raise + # an error for invalid range + if fuzzy: + val_is_ampm = False + else: + raise ValueError('Invalid hour specified for 12-hour clock.') + + return val_is_ampm + + def _adjust_ampm(self, hour, ampm): + if hour < 12 and ampm == 1: + hour += 12 + elif hour == 12 and ampm == 0: + hour = 0 + return hour + + def _parse_min_sec(self, value): + # TODO: Every usage of this function sets res.second to the return + # value. Are there any cases where second will be returned as None and + # we *don't* want to set res.second = None? + minute = int(value) + second = None + + sec_remainder = value % 1 + if sec_remainder: + second = int(60 * sec_remainder) + return (minute, second) + + def _parse_hms(self, idx, tokens, info, hms_idx): + # TODO: Is this going to admit a lot of false-positives for when we + # just happen to have digits and "h", "m" or "s" characters in non-date + # text? I guess hex hashes won't have that problem, but there's plenty + # of random junk out there. + if hms_idx is None: + hms = None + new_idx = idx + elif hms_idx > idx: + hms = info.hms(tokens[hms_idx]) + new_idx = hms_idx + else: + # Looking backwards, increment one. + hms = info.hms(tokens[hms_idx]) + 1 + new_idx = idx + + return (new_idx, hms) + + # ------------------------------------------------------------------ + # Handling for individual tokens. These are kept as methods instead + # of functions for the sake of customizability via subclassing. + + def _parsems(self, value): + """Parse a I[.F] seconds value into (seconds, microseconds).""" + if "." not in value: + return int(value), 0 + else: + i, f = value.split(".") + return int(i), int(f.ljust(6, "0")[:6]) + + def _to_decimal(self, val): + try: + decimal_value = Decimal(val) + # See GH 662, edge case, infinite value should not be converted + # via `_to_decimal` + if not decimal_value.is_finite(): + raise ValueError("Converted decimal value is infinite or NaN") + except Exception as e: + msg = "Could not convert %s to decimal" % val + six.raise_from(ValueError(msg), e) + else: + return decimal_value + + # ------------------------------------------------------------------ + # Post-Parsing construction of datetime output. These are kept as + # methods instead of functions for the sake of customizability via + # subclassing. + + def _build_tzinfo(self, tzinfos, tzname, tzoffset): + if callable(tzinfos): + tzdata = tzinfos(tzname, tzoffset) + else: + tzdata = tzinfos.get(tzname) + # handle case where tzinfo is paased an options that returns None + # eg tzinfos = {'BRST' : None} + if isinstance(tzdata, datetime.tzinfo) or tzdata is None: + tzinfo = tzdata + elif isinstance(tzdata, text_type): + tzinfo = tz.tzstr(tzdata) + elif isinstance(tzdata, integer_types): + tzinfo = tz.tzoffset(tzname, tzdata) + else: + raise TypeError("Offset must be tzinfo subclass, tz string, " + "or int offset.") + return tzinfo + + def _build_tzaware(self, naive, res, tzinfos): + if (callable(tzinfos) or (tzinfos and res.tzname in tzinfos)): + tzinfo = self._build_tzinfo(tzinfos, res.tzname, res.tzoffset) + aware = naive.replace(tzinfo=tzinfo) + aware = self._assign_tzname(aware, res.tzname) + + elif res.tzname and res.tzname in time.tzname: + aware = naive.replace(tzinfo=tz.tzlocal()) + + # Handle ambiguous local datetime + aware = self._assign_tzname(aware, res.tzname) + + # This is mostly relevant for winter GMT zones parsed in the UK + if (aware.tzname() != res.tzname and + res.tzname in self.info.UTCZONE): + aware = aware.replace(tzinfo=tz.UTC) + + elif res.tzoffset == 0: + aware = naive.replace(tzinfo=tz.UTC) + + elif res.tzoffset: + aware = naive.replace(tzinfo=tz.tzoffset(res.tzname, res.tzoffset)) + + elif not res.tzname and not res.tzoffset: + # i.e. no timezone information was found. + aware = naive + + elif res.tzname: + # tz-like string was parsed but we don't know what to do + # with it + warnings.warn("tzname {tzname} identified but not understood. " + "Pass `tzinfos` argument in order to correctly " + "return a timezone-aware datetime. In a future " + "version, this will raise an " + "exception.".format(tzname=res.tzname), + category=UnknownTimezoneWarning) + aware = naive + + return aware + + def _build_naive(self, res, default): + repl = {} + for attr in ("year", "month", "day", "hour", + "minute", "second", "microsecond"): + value = getattr(res, attr) + if value is not None: + repl[attr] = value + + if 'day' not in repl: + # If the default day exceeds the last day of the month, fall back + # to the end of the month. + cyear = default.year if res.year is None else res.year + cmonth = default.month if res.month is None else res.month + cday = default.day if res.day is None else res.day + + if cday > monthrange(cyear, cmonth)[1]: + repl['day'] = monthrange(cyear, cmonth)[1] + + naive = default.replace(**repl) + + if res.weekday is not None and not res.day: + naive = naive + relativedelta.relativedelta(weekday=res.weekday) + + return naive + + def _assign_tzname(self, dt, tzname): + if dt.tzname() != tzname: + new_dt = tz.enfold(dt, fold=1) + if new_dt.tzname() == tzname: + return new_dt + + return dt + + def _recombine_skipped(self, tokens, skipped_idxs): + """ + >>> tokens = ["foo", " ", "bar", " ", "19June2000", "baz"] + >>> skipped_idxs = [0, 1, 2, 5] + >>> _recombine_skipped(tokens, skipped_idxs) + ["foo bar", "baz"] + """ + skipped_tokens = [] + for i, idx in enumerate(sorted(skipped_idxs)): + if i > 0 and idx - 1 == skipped_idxs[i - 1]: + skipped_tokens[-1] = skipped_tokens[-1] + tokens[idx] + else: + skipped_tokens.append(tokens[idx]) + + return skipped_tokens + + +DEFAULTPARSER = parser() + + +def parse(timestr, parserinfo=None, **kwargs): + """ + + Parse a string in one of the supported formats, using the + ``parserinfo`` parameters. + + :param timestr: + A string containing a date/time stamp. + + :param parserinfo: + A :class:`parserinfo` object containing parameters for the parser. + If ``None``, the default arguments to the :class:`parserinfo` + constructor are used. + + The ``**kwargs`` parameter takes the following keyword arguments: + + :param default: + The default datetime object, if this is a datetime object and not + ``None``, elements specified in ``timestr`` replace elements in the + default object. + + :param ignoretz: + If set ``True``, time zones in parsed strings are ignored and a naive + :class:`datetime` object is returned. + + :param tzinfos: + Additional time zone names / aliases which may be present in the + string. This argument maps time zone names (and optionally offsets + from those time zones) to time zones. This parameter can be a + dictionary with timezone aliases mapping time zone names to time + zones or a function taking two parameters (``tzname`` and + ``tzoffset``) and returning a time zone. + + The timezones to which the names are mapped can be an integer + offset from UTC in seconds or a :class:`tzinfo` object. + + .. doctest:: + :options: +NORMALIZE_WHITESPACE + + >>> from dateutil.parser import parse + >>> from dateutil.tz import gettz + >>> tzinfos = {"BRST": -7200, "CST": gettz("America/Chicago")} + >>> parse("2012-01-19 17:21:00 BRST", tzinfos=tzinfos) + datetime.datetime(2012, 1, 19, 17, 21, tzinfo=tzoffset(u'BRST', -7200)) + >>> parse("2012-01-19 17:21:00 CST", tzinfos=tzinfos) + datetime.datetime(2012, 1, 19, 17, 21, + tzinfo=tzfile('/usr/share/zoneinfo/America/Chicago')) + + This parameter is ignored if ``ignoretz`` is set. + + :param dayfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the day (``True``) or month (``False``). If + ``yearfirst`` is set to ``True``, this distinguishes between YDM and + YMD. If set to ``None``, this value is retrieved from the current + :class:`parserinfo` object (which itself defaults to ``False``). + + :param yearfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the year. If ``True``, the first number is taken to + be the year, otherwise the last number is taken to be the year. If + this is set to ``None``, the value is retrieved from the current + :class:`parserinfo` object (which itself defaults to ``False``). + + :param fuzzy: + Whether to allow fuzzy parsing, allowing for string like "Today is + January 1, 2047 at 8:21:00AM". + + :param fuzzy_with_tokens: + If ``True``, ``fuzzy`` is automatically set to True, and the parser + will return a tuple where the first element is the parsed + :class:`datetime.datetime` datetimestamp and the second element is + a tuple containing the portions of the string which were ignored: + + .. doctest:: + + >>> from dateutil.parser import parse + >>> parse("Today is January 1, 2047 at 8:21:00AM", fuzzy_with_tokens=True) + (datetime.datetime(2047, 1, 1, 8, 21), (u'Today is ', u' ', u'at ')) + + :return: + Returns a :class:`datetime.datetime` object or, if the + ``fuzzy_with_tokens`` option is ``True``, returns a tuple, the + first element being a :class:`datetime.datetime` object, the second + a tuple containing the fuzzy tokens. + + :raises ParserError: + Raised for invalid or unknown string formats, if the provided + :class:`tzinfo` is not in a valid format, or if an invalid date would + be created. + + :raises OverflowError: + Raised if the parsed date exceeds the largest valid C integer on + your system. + """ + if parserinfo: + return parser(parserinfo).parse(timestr, **kwargs) + else: + return DEFAULTPARSER.parse(timestr, **kwargs) + + +class _tzparser(object): + + class _result(_resultbase): + + __slots__ = ["stdabbr", "stdoffset", "dstabbr", "dstoffset", + "start", "end"] + + class _attr(_resultbase): + __slots__ = ["month", "week", "weekday", + "yday", "jyday", "day", "time"] + + def __repr__(self): + return self._repr("") + + def __init__(self): + _resultbase.__init__(self) + self.start = self._attr() + self.end = self._attr() + + def parse(self, tzstr): + res = self._result() + l = [x for x in re.split(r'([,:.]|[a-zA-Z]+|[0-9]+)',tzstr) if x] + used_idxs = list() + try: + + len_l = len(l) + + i = 0 + while i < len_l: + # BRST+3[BRDT[+2]] + j = i + while j < len_l and not [x for x in l[j] + if x in "0123456789:,-+"]: + j += 1 + if j != i: + if not res.stdabbr: + offattr = "stdoffset" + res.stdabbr = "".join(l[i:j]) + else: + offattr = "dstoffset" + res.dstabbr = "".join(l[i:j]) + + for ii in range(j): + used_idxs.append(ii) + i = j + if (i < len_l and (l[i] in ('+', '-') or l[i][0] in + "0123456789")): + if l[i] in ('+', '-'): + # Yes, that's right. See the TZ variable + # documentation. + signal = (1, -1)[l[i] == '+'] + used_idxs.append(i) + i += 1 + else: + signal = -1 + len_li = len(l[i]) + if len_li == 4: + # -0300 + setattr(res, offattr, (int(l[i][:2]) * 3600 + + int(l[i][2:]) * 60) * signal) + elif i + 1 < len_l and l[i + 1] == ':': + # -03:00 + setattr(res, offattr, + (int(l[i]) * 3600 + + int(l[i + 2]) * 60) * signal) + used_idxs.append(i) + i += 2 + elif len_li <= 2: + # -[0]3 + setattr(res, offattr, + int(l[i][:2]) * 3600 * signal) + else: + return None + used_idxs.append(i) + i += 1 + if res.dstabbr: + break + else: + break + + + if i < len_l: + for j in range(i, len_l): + if l[j] == ';': + l[j] = ',' + + assert l[i] == ',' + + i += 1 + + if i >= len_l: + pass + elif (8 <= l.count(',') <= 9 and + not [y for x in l[i:] if x != ',' + for y in x if y not in "0123456789+-"]): + # GMT0BST,3,0,30,3600,10,0,26,7200[,3600] + for x in (res.start, res.end): + x.month = int(l[i]) + used_idxs.append(i) + i += 2 + if l[i] == '-': + value = int(l[i + 1]) * -1 + used_idxs.append(i) + i += 1 + else: + value = int(l[i]) + used_idxs.append(i) + i += 2 + if value: + x.week = value + x.weekday = (int(l[i]) - 1) % 7 + else: + x.day = int(l[i]) + used_idxs.append(i) + i += 2 + x.time = int(l[i]) + used_idxs.append(i) + i += 2 + if i < len_l: + if l[i] in ('-', '+'): + signal = (-1, 1)[l[i] == "+"] + used_idxs.append(i) + i += 1 + else: + signal = 1 + used_idxs.append(i) + res.dstoffset = (res.stdoffset + int(l[i]) * signal) + + # This was a made-up format that is not in normal use + warn(('Parsed time zone "%s"' % tzstr) + + 'is in a non-standard dateutil-specific format, which ' + + 'is now deprecated; support for parsing this format ' + + 'will be removed in future versions. It is recommended ' + + 'that you switch to a standard format like the GNU ' + + 'TZ variable format.', tz.DeprecatedTzFormatWarning) + elif (l.count(',') == 2 and l[i:].count('/') <= 2 and + not [y for x in l[i:] if x not in (',', '/', 'J', 'M', + '.', '-', ':') + for y in x if y not in "0123456789"]): + for x in (res.start, res.end): + if l[i] == 'J': + # non-leap year day (1 based) + used_idxs.append(i) + i += 1 + x.jyday = int(l[i]) + elif l[i] == 'M': + # month[-.]week[-.]weekday + used_idxs.append(i) + i += 1 + x.month = int(l[i]) + used_idxs.append(i) + i += 1 + assert l[i] in ('-', '.') + used_idxs.append(i) + i += 1 + x.week = int(l[i]) + if x.week == 5: + x.week = -1 + used_idxs.append(i) + i += 1 + assert l[i] in ('-', '.') + used_idxs.append(i) + i += 1 + x.weekday = (int(l[i]) - 1) % 7 + else: + # year day (zero based) + x.yday = int(l[i]) + 1 + + used_idxs.append(i) + i += 1 + + if i < len_l and l[i] == '/': + used_idxs.append(i) + i += 1 + # start time + len_li = len(l[i]) + if len_li == 4: + # -0300 + x.time = (int(l[i][:2]) * 3600 + + int(l[i][2:]) * 60) + elif i + 1 < len_l and l[i + 1] == ':': + # -03:00 + x.time = int(l[i]) * 3600 + int(l[i + 2]) * 60 + used_idxs.append(i) + i += 2 + if i + 1 < len_l and l[i + 1] == ':': + used_idxs.append(i) + i += 2 + x.time += int(l[i]) + elif len_li <= 2: + # -[0]3 + x.time = (int(l[i][:2]) * 3600) + else: + return None + used_idxs.append(i) + i += 1 + + assert i == len_l or l[i] == ',' + + i += 1 + + assert i >= len_l + + except (IndexError, ValueError, AssertionError): + return None + + unused_idxs = set(range(len_l)).difference(used_idxs) + res.any_unused_tokens = not {l[n] for n in unused_idxs}.issubset({",",":"}) + return res + + +DEFAULTTZPARSER = _tzparser() + + +def _parsetz(tzstr): + return DEFAULTTZPARSER.parse(tzstr) + + +class ParserError(ValueError): + """Exception subclass used for any failure to parse a datetime string. + + This is a subclass of :py:exc:`ValueError`, and should be raised any time + earlier versions of ``dateutil`` would have raised ``ValueError``. + + .. versionadded:: 2.8.1 + """ + def __str__(self): + try: + return self.args[0] % self.args[1:] + except (TypeError, IndexError): + return super(ParserError, self).__str__() + + def __repr__(self): + args = ", ".join("'%s'" % arg for arg in self.args) + return "%s(%s)" % (self.__class__.__name__, args) + + +class UnknownTimezoneWarning(RuntimeWarning): + """Raised when the parser finds a timezone it cannot parse into a tzinfo. + + .. versionadded:: 2.7.0 + """ +# vim:ts=4:sw=4:et diff --git a/hug/lib/python3.12/site-packages/dateutil/parser/isoparser.py b/hug/lib/python3.12/site-packages/dateutil/parser/isoparser.py new file mode 100644 index 0000000000000000000000000000000000000000..7060087df4776a07347cbb60127a70db393e3a65 --- /dev/null +++ b/hug/lib/python3.12/site-packages/dateutil/parser/isoparser.py @@ -0,0 +1,416 @@ +# -*- coding: utf-8 -*- +""" +This module offers a parser for ISO-8601 strings + +It is intended to support all valid date, time and datetime formats per the +ISO-8601 specification. + +..versionadded:: 2.7.0 +""" +from datetime import datetime, timedelta, time, date +import calendar +from dateutil import tz + +from functools import wraps + +import re +import six + +__all__ = ["isoparse", "isoparser"] + + +def _takes_ascii(f): + @wraps(f) + def func(self, str_in, *args, **kwargs): + # If it's a stream, read the whole thing + str_in = getattr(str_in, 'read', lambda: str_in)() + + # If it's unicode, turn it into bytes, since ISO-8601 only covers ASCII + if isinstance(str_in, six.text_type): + # ASCII is the same in UTF-8 + try: + str_in = str_in.encode('ascii') + except UnicodeEncodeError as e: + msg = 'ISO-8601 strings should contain only ASCII characters' + six.raise_from(ValueError(msg), e) + + return f(self, str_in, *args, **kwargs) + + return func + + +class isoparser(object): + def __init__(self, sep=None): + """ + :param sep: + A single character that separates date and time portions. If + ``None``, the parser will accept any single character. + For strict ISO-8601 adherence, pass ``'T'``. + """ + if sep is not None: + if (len(sep) != 1 or ord(sep) >= 128 or sep in '0123456789'): + raise ValueError('Separator must be a single, non-numeric ' + + 'ASCII character') + + sep = sep.encode('ascii') + + self._sep = sep + + @_takes_ascii + def isoparse(self, dt_str): + """ + Parse an ISO-8601 datetime string into a :class:`datetime.datetime`. + + An ISO-8601 datetime string consists of a date portion, followed + optionally by a time portion - the date and time portions are separated + by a single character separator, which is ``T`` in the official + standard. Incomplete date formats (such as ``YYYY-MM``) may *not* be + combined with a time portion. + + Supported date formats are: + + Common: + + - ``YYYY`` + - ``YYYY-MM`` + - ``YYYY-MM-DD`` or ``YYYYMMDD`` + + Uncommon: + + - ``YYYY-Www`` or ``YYYYWww`` - ISO week (day defaults to 0) + - ``YYYY-Www-D`` or ``YYYYWwwD`` - ISO week and day + + The ISO week and day numbering follows the same logic as + :func:`datetime.date.isocalendar`. + + Supported time formats are: + + - ``hh`` + - ``hh:mm`` or ``hhmm`` + - ``hh:mm:ss`` or ``hhmmss`` + - ``hh:mm:ss.ssssss`` (Up to 6 sub-second digits) + + Midnight is a special case for `hh`, as the standard supports both + 00:00 and 24:00 as a representation. The decimal separator can be + either a dot or a comma. + + + .. caution:: + + Support for fractional components other than seconds is part of the + ISO-8601 standard, but is not currently implemented in this parser. + + Supported time zone offset formats are: + + - `Z` (UTC) + - `±HH:MM` + - `±HHMM` + - `±HH` + + Offsets will be represented as :class:`dateutil.tz.tzoffset` objects, + with the exception of UTC, which will be represented as + :class:`dateutil.tz.tzutc`. Time zone offsets equivalent to UTC (such + as `+00:00`) will also be represented as :class:`dateutil.tz.tzutc`. + + :param dt_str: + A string or stream containing only an ISO-8601 datetime string + + :return: + Returns a :class:`datetime.datetime` representing the string. + Unspecified components default to their lowest value. + + .. warning:: + + As of version 2.7.0, the strictness of the parser should not be + considered a stable part of the contract. Any valid ISO-8601 string + that parses correctly with the default settings will continue to + parse correctly in future versions, but invalid strings that + currently fail (e.g. ``2017-01-01T00:00+00:00:00``) are not + guaranteed to continue failing in future versions if they encode + a valid date. + + .. versionadded:: 2.7.0 + """ + components, pos = self._parse_isodate(dt_str) + + if len(dt_str) > pos: + if self._sep is None or dt_str[pos:pos + 1] == self._sep: + components += self._parse_isotime(dt_str[pos + 1:]) + else: + raise ValueError('String contains unknown ISO components') + + if len(components) > 3 and components[3] == 24: + components[3] = 0 + return datetime(*components) + timedelta(days=1) + + return datetime(*components) + + @_takes_ascii + def parse_isodate(self, datestr): + """ + Parse the date portion of an ISO string. + + :param datestr: + The string portion of an ISO string, without a separator + + :return: + Returns a :class:`datetime.date` object + """ + components, pos = self._parse_isodate(datestr) + if pos < len(datestr): + raise ValueError('String contains unknown ISO ' + + 'components: {!r}'.format(datestr.decode('ascii'))) + return date(*components) + + @_takes_ascii + def parse_isotime(self, timestr): + """ + Parse the time portion of an ISO string. + + :param timestr: + The time portion of an ISO string, without a separator + + :return: + Returns a :class:`datetime.time` object + """ + components = self._parse_isotime(timestr) + if components[0] == 24: + components[0] = 0 + return time(*components) + + @_takes_ascii + def parse_tzstr(self, tzstr, zero_as_utc=True): + """ + Parse a valid ISO time zone string. + + See :func:`isoparser.isoparse` for details on supported formats. + + :param tzstr: + A string representing an ISO time zone offset + + :param zero_as_utc: + Whether to return :class:`dateutil.tz.tzutc` for zero-offset zones + + :return: + Returns :class:`dateutil.tz.tzoffset` for offsets and + :class:`dateutil.tz.tzutc` for ``Z`` and (if ``zero_as_utc`` is + specified) offsets equivalent to UTC. + """ + return self._parse_tzstr(tzstr, zero_as_utc=zero_as_utc) + + # Constants + _DATE_SEP = b'-' + _TIME_SEP = b':' + _FRACTION_REGEX = re.compile(b'[\\.,]([0-9]+)') + + def _parse_isodate(self, dt_str): + try: + return self._parse_isodate_common(dt_str) + except ValueError: + return self._parse_isodate_uncommon(dt_str) + + def _parse_isodate_common(self, dt_str): + len_str = len(dt_str) + components = [1, 1, 1] + + if len_str < 4: + raise ValueError('ISO string too short') + + # Year + components[0] = int(dt_str[0:4]) + pos = 4 + if pos >= len_str: + return components, pos + + has_sep = dt_str[pos:pos + 1] == self._DATE_SEP + if has_sep: + pos += 1 + + # Month + if len_str - pos < 2: + raise ValueError('Invalid common month') + + components[1] = int(dt_str[pos:pos + 2]) + pos += 2 + + if pos >= len_str: + if has_sep: + return components, pos + else: + raise ValueError('Invalid ISO format') + + if has_sep: + if dt_str[pos:pos + 1] != self._DATE_SEP: + raise ValueError('Invalid separator in ISO string') + pos += 1 + + # Day + if len_str - pos < 2: + raise ValueError('Invalid common day') + components[2] = int(dt_str[pos:pos + 2]) + return components, pos + 2 + + def _parse_isodate_uncommon(self, dt_str): + if len(dt_str) < 4: + raise ValueError('ISO string too short') + + # All ISO formats start with the year + year = int(dt_str[0:4]) + + has_sep = dt_str[4:5] == self._DATE_SEP + + pos = 4 + has_sep # Skip '-' if it's there + if dt_str[pos:pos + 1] == b'W': + # YYYY-?Www-?D? + pos += 1 + weekno = int(dt_str[pos:pos + 2]) + pos += 2 + + dayno = 1 + if len(dt_str) > pos: + if (dt_str[pos:pos + 1] == self._DATE_SEP) != has_sep: + raise ValueError('Inconsistent use of dash separator') + + pos += has_sep + + dayno = int(dt_str[pos:pos + 1]) + pos += 1 + + base_date = self._calculate_weekdate(year, weekno, dayno) + else: + # YYYYDDD or YYYY-DDD + if len(dt_str) - pos < 3: + raise ValueError('Invalid ordinal day') + + ordinal_day = int(dt_str[pos:pos + 3]) + pos += 3 + + if ordinal_day < 1 or ordinal_day > (365 + calendar.isleap(year)): + raise ValueError('Invalid ordinal day' + + ' {} for year {}'.format(ordinal_day, year)) + + base_date = date(year, 1, 1) + timedelta(days=ordinal_day - 1) + + components = [base_date.year, base_date.month, base_date.day] + return components, pos + + def _calculate_weekdate(self, year, week, day): + """ + Calculate the day of corresponding to the ISO year-week-day calendar. + + This function is effectively the inverse of + :func:`datetime.date.isocalendar`. + + :param year: + The year in the ISO calendar + + :param week: + The week in the ISO calendar - range is [1, 53] + + :param day: + The day in the ISO calendar - range is [1 (MON), 7 (SUN)] + + :return: + Returns a :class:`datetime.date` + """ + if not 0 < week < 54: + raise ValueError('Invalid week: {}'.format(week)) + + if not 0 < day < 8: # Range is 1-7 + raise ValueError('Invalid weekday: {}'.format(day)) + + # Get week 1 for the specific year: + jan_4 = date(year, 1, 4) # Week 1 always has January 4th in it + week_1 = jan_4 - timedelta(days=jan_4.isocalendar()[2] - 1) + + # Now add the specific number of weeks and days to get what we want + week_offset = (week - 1) * 7 + (day - 1) + return week_1 + timedelta(days=week_offset) + + def _parse_isotime(self, timestr): + len_str = len(timestr) + components = [0, 0, 0, 0, None] + pos = 0 + comp = -1 + + if len_str < 2: + raise ValueError('ISO time too short') + + has_sep = False + + while pos < len_str and comp < 5: + comp += 1 + + if timestr[pos:pos + 1] in b'-+Zz': + # Detect time zone boundary + components[-1] = self._parse_tzstr(timestr[pos:]) + pos = len_str + break + + if comp == 1 and timestr[pos:pos+1] == self._TIME_SEP: + has_sep = True + pos += 1 + elif comp == 2 and has_sep: + if timestr[pos:pos+1] != self._TIME_SEP: + raise ValueError('Inconsistent use of colon separator') + pos += 1 + + if comp < 3: + # Hour, minute, second + components[comp] = int(timestr[pos:pos + 2]) + pos += 2 + + if comp == 3: + # Fraction of a second + frac = self._FRACTION_REGEX.match(timestr[pos:]) + if not frac: + continue + + us_str = frac.group(1)[:6] # Truncate to microseconds + components[comp] = int(us_str) * 10**(6 - len(us_str)) + pos += len(frac.group()) + + if pos < len_str: + raise ValueError('Unused components in ISO string') + + if components[0] == 24: + # Standard supports 00:00 and 24:00 as representations of midnight + if any(component != 0 for component in components[1:4]): + raise ValueError('Hour may only be 24 at 24:00:00.000') + + return components + + def _parse_tzstr(self, tzstr, zero_as_utc=True): + if tzstr == b'Z' or tzstr == b'z': + return tz.UTC + + if len(tzstr) not in {3, 5, 6}: + raise ValueError('Time zone offset must be 1, 3, 5 or 6 characters') + + if tzstr[0:1] == b'-': + mult = -1 + elif tzstr[0:1] == b'+': + mult = 1 + else: + raise ValueError('Time zone offset requires sign') + + hours = int(tzstr[1:3]) + if len(tzstr) == 3: + minutes = 0 + else: + minutes = int(tzstr[(4 if tzstr[3:4] == self._TIME_SEP else 3):]) + + if zero_as_utc and hours == 0 and minutes == 0: + return tz.UTC + else: + if minutes > 59: + raise ValueError('Invalid minutes in time zone offset') + + if hours > 23: + raise ValueError('Invalid hours in time zone offset') + + return tz.tzoffset(None, mult * (hours * 60 + minutes) * 60) + + +DEFAULT_ISOPARSER = isoparser() +isoparse = DEFAULT_ISOPARSER.isoparse diff --git a/hug/lib/python3.12/site-packages/dateutil/relativedelta.py b/hug/lib/python3.12/site-packages/dateutil/relativedelta.py new file mode 100644 index 0000000000000000000000000000000000000000..ce71562b8c84f0e8d9a45c839cfa36e9928c3beb --- /dev/null +++ b/hug/lib/python3.12/site-packages/dateutil/relativedelta.py @@ -0,0 +1,596 @@ +# -*- coding: utf-8 -*- +import datetime +import calendar + +import operator +from math import copysign + +from six import integer_types +from warnings import warn + +from ._common import weekday + +MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7)) + +__all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"] + + +class relativedelta(object): + """ + The relativedelta type is designed to be applied to an existing datetime and + can replace specific components of that datetime, or represents an interval + of time. + + It is based on the specification of the excellent work done by M.-A. Lemburg + in his + `mx.DateTime `_ extension. + However, notice that this type does *NOT* implement the same algorithm as + his work. Do *NOT* expect it to behave like mx.DateTime's counterpart. + + There are two different ways to build a relativedelta instance. The + first one is passing it two date/datetime classes:: + + relativedelta(datetime1, datetime2) + + The second one is passing it any number of the following keyword arguments:: + + relativedelta(arg1=x,arg2=y,arg3=z...) + + year, month, day, hour, minute, second, microsecond: + Absolute information (argument is singular); adding or subtracting a + relativedelta with absolute information does not perform an arithmetic + operation, but rather REPLACES the corresponding value in the + original datetime with the value(s) in relativedelta. + + years, months, weeks, days, hours, minutes, seconds, microseconds: + Relative information, may be negative (argument is plural); adding + or subtracting a relativedelta with relative information performs + the corresponding arithmetic operation on the original datetime value + with the information in the relativedelta. + + weekday: + One of the weekday instances (MO, TU, etc) available in the + relativedelta module. These instances may receive a parameter N, + specifying the Nth weekday, which could be positive or negative + (like MO(+1) or MO(-2)). Not specifying it is the same as specifying + +1. You can also use an integer, where 0=MO. This argument is always + relative e.g. if the calculated date is already Monday, using MO(1) + or MO(-1) won't change the day. To effectively make it absolute, use + it in combination with the day argument (e.g. day=1, MO(1) for first + Monday of the month). + + leapdays: + Will add given days to the date found, if year is a leap + year, and the date found is post 28 of february. + + yearday, nlyearday: + Set the yearday or the non-leap year day (jump leap days). + These are converted to day/month/leapdays information. + + There are relative and absolute forms of the keyword + arguments. The plural is relative, and the singular is + absolute. For each argument in the order below, the absolute form + is applied first (by setting each attribute to that value) and + then the relative form (by adding the value to the attribute). + + The order of attributes considered when this relativedelta is + added to a datetime is: + + 1. Year + 2. Month + 3. Day + 4. Hours + 5. Minutes + 6. Seconds + 7. Microseconds + + Finally, weekday is applied, using the rule described above. + + For example + + >>> from datetime import datetime + >>> from dateutil.relativedelta import relativedelta, MO + >>> dt = datetime(2018, 4, 9, 13, 37, 0) + >>> delta = relativedelta(hours=25, day=1, weekday=MO(1)) + >>> dt + delta + datetime.datetime(2018, 4, 2, 14, 37) + + First, the day is set to 1 (the first of the month), then 25 hours + are added, to get to the 2nd day and 14th hour, finally the + weekday is applied, but since the 2nd is already a Monday there is + no effect. + + """ + + def __init__(self, dt1=None, dt2=None, + years=0, months=0, days=0, leapdays=0, weeks=0, + hours=0, minutes=0, seconds=0, microseconds=0, + year=None, month=None, day=None, weekday=None, + yearday=None, nlyearday=None, + hour=None, minute=None, second=None, microsecond=None): + + if dt1 and dt2: + # datetime is a subclass of date. So both must be date + if not (isinstance(dt1, datetime.date) and + isinstance(dt2, datetime.date)): + raise TypeError("relativedelta only diffs datetime/date") + + # We allow two dates, or two datetimes, so we coerce them to be + # of the same type + if (isinstance(dt1, datetime.datetime) != + isinstance(dt2, datetime.datetime)): + if not isinstance(dt1, datetime.datetime): + dt1 = datetime.datetime.fromordinal(dt1.toordinal()) + elif not isinstance(dt2, datetime.datetime): + dt2 = datetime.datetime.fromordinal(dt2.toordinal()) + + self.years = 0 + self.months = 0 + self.days = 0 + self.leapdays = 0 + self.hours = 0 + self.minutes = 0 + self.seconds = 0 + self.microseconds = 0 + self.year = None + self.month = None + self.day = None + self.weekday = None + self.hour = None + self.minute = None + self.second = None + self.microsecond = None + self._has_time = 0 + + # Get year / month delta between the two + months = (dt1.year - dt2.year) * 12 + (dt1.month - dt2.month) + self._set_months(months) + + # Remove the year/month delta so the timedelta is just well-defined + # time units (seconds, days and microseconds) + dtm = self.__radd__(dt2) + + # If we've overshot our target, make an adjustment + if dt1 < dt2: + compare = operator.gt + increment = 1 + else: + compare = operator.lt + increment = -1 + + while compare(dt1, dtm): + months += increment + self._set_months(months) + dtm = self.__radd__(dt2) + + # Get the timedelta between the "months-adjusted" date and dt1 + delta = dt1 - dtm + self.seconds = delta.seconds + delta.days * 86400 + self.microseconds = delta.microseconds + else: + # Check for non-integer values in integer-only quantities + if any(x is not None and x != int(x) for x in (years, months)): + raise ValueError("Non-integer years and months are " + "ambiguous and not currently supported.") + + # Relative information + self.years = int(years) + self.months = int(months) + self.days = days + weeks * 7 + self.leapdays = leapdays + self.hours = hours + self.minutes = minutes + self.seconds = seconds + self.microseconds = microseconds + + # Absolute information + self.year = year + self.month = month + self.day = day + self.hour = hour + self.minute = minute + self.second = second + self.microsecond = microsecond + + if any(x is not None and int(x) != x + for x in (year, month, day, hour, + minute, second, microsecond)): + # For now we'll deprecate floats - later it'll be an error. + warn("Non-integer value passed as absolute information. " + + "This is not a well-defined condition and will raise " + + "errors in future versions.", DeprecationWarning) + + if isinstance(weekday, integer_types): + self.weekday = weekdays[weekday] + else: + self.weekday = weekday + + yday = 0 + if nlyearday: + yday = nlyearday + elif yearday: + yday = yearday + if yearday > 59: + self.leapdays = -1 + if yday: + ydayidx = [31, 59, 90, 120, 151, 181, 212, + 243, 273, 304, 334, 366] + for idx, ydays in enumerate(ydayidx): + if yday <= ydays: + self.month = idx+1 + if idx == 0: + self.day = yday + else: + self.day = yday-ydayidx[idx-1] + break + else: + raise ValueError("invalid year day (%d)" % yday) + + self._fix() + + def _fix(self): + if abs(self.microseconds) > 999999: + s = _sign(self.microseconds) + div, mod = divmod(self.microseconds * s, 1000000) + self.microseconds = mod * s + self.seconds += div * s + if abs(self.seconds) > 59: + s = _sign(self.seconds) + div, mod = divmod(self.seconds * s, 60) + self.seconds = mod * s + self.minutes += div * s + if abs(self.minutes) > 59: + s = _sign(self.minutes) + div, mod = divmod(self.minutes * s, 60) + self.minutes = mod * s + self.hours += div * s + if abs(self.hours) > 23: + s = _sign(self.hours) + div, mod = divmod(self.hours * s, 24) + self.hours = mod * s + self.days += div * s + if abs(self.months) > 11: + s = _sign(self.months) + div, mod = divmod(self.months * s, 12) + self.months = mod * s + self.years += div * s + if (self.hours or self.minutes or self.seconds or self.microseconds + or self.hour is not None or self.minute is not None or + self.second is not None or self.microsecond is not None): + self._has_time = 1 + else: + self._has_time = 0 + + @property + def weeks(self): + return int(self.days / 7.0) + + @weeks.setter + def weeks(self, value): + self.days = self.days - (self.weeks * 7) + value * 7 + + def _set_months(self, months): + self.months = months + if abs(self.months) > 11: + s = _sign(self.months) + div, mod = divmod(self.months * s, 12) + self.months = mod * s + self.years = div * s + else: + self.years = 0 + + def normalized(self): + """ + Return a version of this object represented entirely using integer + values for the relative attributes. + + >>> relativedelta(days=1.5, hours=2).normalized() + relativedelta(days=+1, hours=+14) + + :return: + Returns a :class:`dateutil.relativedelta.relativedelta` object. + """ + # Cascade remainders down (rounding each to roughly nearest microsecond) + days = int(self.days) + + hours_f = round(self.hours + 24 * (self.days - days), 11) + hours = int(hours_f) + + minutes_f = round(self.minutes + 60 * (hours_f - hours), 10) + minutes = int(minutes_f) + + seconds_f = round(self.seconds + 60 * (minutes_f - minutes), 8) + seconds = int(seconds_f) + + microseconds = round(self.microseconds + 1e6 * (seconds_f - seconds)) + + # Constructor carries overflow back up with call to _fix() + return self.__class__(years=self.years, months=self.months, + days=days, hours=hours, minutes=minutes, + seconds=seconds, microseconds=microseconds, + leapdays=self.leapdays, year=self.year, + month=self.month, day=self.day, + weekday=self.weekday, hour=self.hour, + minute=self.minute, second=self.second, + microsecond=self.microsecond) + + def __add__(self, other): + if isinstance(other, relativedelta): + return self.__class__(years=other.years + self.years, + months=other.months + self.months, + days=other.days + self.days, + hours=other.hours + self.hours, + minutes=other.minutes + self.minutes, + seconds=other.seconds + self.seconds, + microseconds=(other.microseconds + + self.microseconds), + leapdays=other.leapdays or self.leapdays, + year=(other.year if other.year is not None + else self.year), + month=(other.month if other.month is not None + else self.month), + day=(other.day if other.day is not None + else self.day), + weekday=(other.weekday if other.weekday is not None + else self.weekday), + hour=(other.hour if other.hour is not None + else self.hour), + minute=(other.minute if other.minute is not None + else self.minute), + second=(other.second if other.second is not None + else self.second), + microsecond=(other.microsecond if other.microsecond + is not None else + self.microsecond)) + if isinstance(other, datetime.timedelta): + return self.__class__(years=self.years, + months=self.months, + days=self.days + other.days, + hours=self.hours, + minutes=self.minutes, + seconds=self.seconds + other.seconds, + microseconds=self.microseconds + other.microseconds, + leapdays=self.leapdays, + year=self.year, + month=self.month, + day=self.day, + weekday=self.weekday, + hour=self.hour, + minute=self.minute, + second=self.second, + microsecond=self.microsecond) + if not isinstance(other, datetime.date): + return NotImplemented + elif self._has_time and not isinstance(other, datetime.datetime): + other = datetime.datetime.fromordinal(other.toordinal()) + year = (self.year or other.year)+self.years + month = self.month or other.month + if self.months: + assert 1 <= abs(self.months) <= 12 + month += self.months + if month > 12: + year += 1 + month -= 12 + elif month < 1: + year -= 1 + month += 12 + day = min(calendar.monthrange(year, month)[1], + self.day or other.day) + repl = {"year": year, "month": month, "day": day} + for attr in ["hour", "minute", "second", "microsecond"]: + value = getattr(self, attr) + if value is not None: + repl[attr] = value + days = self.days + if self.leapdays and month > 2 and calendar.isleap(year): + days += self.leapdays + ret = (other.replace(**repl) + + datetime.timedelta(days=days, + hours=self.hours, + minutes=self.minutes, + seconds=self.seconds, + microseconds=self.microseconds)) + if self.weekday: + weekday, nth = self.weekday.weekday, self.weekday.n or 1 + jumpdays = (abs(nth) - 1) * 7 + if nth > 0: + jumpdays += (7 - ret.weekday() + weekday) % 7 + else: + jumpdays += (ret.weekday() - weekday) % 7 + jumpdays *= -1 + ret += datetime.timedelta(days=jumpdays) + return ret + + def __radd__(self, other): + return self.__add__(other) + + def __rsub__(self, other): + return self.__neg__().__radd__(other) + + def __sub__(self, other): + if not isinstance(other, relativedelta): + return NotImplemented # In case the other object defines __rsub__ + return self.__class__(years=self.years - other.years, + months=self.months - other.months, + days=self.days - other.days, + hours=self.hours - other.hours, + minutes=self.minutes - other.minutes, + seconds=self.seconds - other.seconds, + microseconds=self.microseconds - other.microseconds, + leapdays=self.leapdays or other.leapdays, + year=(self.year if self.year is not None + else other.year), + month=(self.month if self.month is not None else + other.month), + day=(self.day if self.day is not None else + other.day), + weekday=(self.weekday if self.weekday is not None else + other.weekday), + hour=(self.hour if self.hour is not None else + other.hour), + minute=(self.minute if self.minute is not None else + other.minute), + second=(self.second if self.second is not None else + other.second), + microsecond=(self.microsecond if self.microsecond + is not None else + other.microsecond)) + + def __abs__(self): + return self.__class__(years=abs(self.years), + months=abs(self.months), + days=abs(self.days), + hours=abs(self.hours), + minutes=abs(self.minutes), + seconds=abs(self.seconds), + microseconds=abs(self.microseconds), + leapdays=self.leapdays, + year=self.year, + month=self.month, + day=self.day, + weekday=self.weekday, + hour=self.hour, + minute=self.minute, + second=self.second, + microsecond=self.microsecond) + + def __neg__(self): + return self.__class__(years=-self.years, + months=-self.months, + days=-self.days, + hours=-self.hours, + minutes=-self.minutes, + seconds=-self.seconds, + microseconds=-self.microseconds, + leapdays=self.leapdays, + year=self.year, + month=self.month, + day=self.day, + weekday=self.weekday, + hour=self.hour, + minute=self.minute, + second=self.second, + microsecond=self.microsecond) + + def __bool__(self): + return not (not self.years and + not self.months and + not self.days and + not self.hours and + not self.minutes and + not self.seconds and + not self.microseconds and + not self.leapdays and + self.year is None and + self.month is None and + self.day is None and + self.weekday is None and + self.hour is None and + self.minute is None and + self.second is None and + self.microsecond is None) + # Compatibility with Python 2.x + __nonzero__ = __bool__ + + def __mul__(self, other): + try: + f = float(other) + except TypeError: + return NotImplemented + + return self.__class__(years=int(self.years * f), + months=int(self.months * f), + days=int(self.days * f), + hours=int(self.hours * f), + minutes=int(self.minutes * f), + seconds=int(self.seconds * f), + microseconds=int(self.microseconds * f), + leapdays=self.leapdays, + year=self.year, + month=self.month, + day=self.day, + weekday=self.weekday, + hour=self.hour, + minute=self.minute, + second=self.second, + microsecond=self.microsecond) + + __rmul__ = __mul__ + + def __eq__(self, other): + if not isinstance(other, relativedelta): + return NotImplemented + if self.weekday or other.weekday: + if not self.weekday or not other.weekday: + return False + if self.weekday.weekday != other.weekday.weekday: + return False + n1, n2 = self.weekday.n, other.weekday.n + if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)): + return False + return (self.years == other.years and + self.months == other.months and + self.days == other.days and + self.hours == other.hours and + self.minutes == other.minutes and + self.seconds == other.seconds and + self.microseconds == other.microseconds and + self.leapdays == other.leapdays and + self.year == other.year and + self.month == other.month and + self.day == other.day and + self.hour == other.hour and + self.minute == other.minute and + self.second == other.second and + self.microsecond == other.microsecond) + + def __hash__(self): + return hash(( + self.weekday, + self.years, + self.months, + self.days, + self.hours, + self.minutes, + self.seconds, + self.microseconds, + self.leapdays, + self.year, + self.month, + self.day, + self.hour, + self.minute, + self.second, + self.microsecond, + )) + + def __div__(self, other): + try: + reciprocal = 1 / float(other) + except TypeError: + return NotImplemented + + return self.__mul__(reciprocal) + + __truediv__ = __div__ + + def __repr__(self): + l = [] + for attr in ["years", "months", "days", "leapdays", + "hours", "minutes", "seconds", "microseconds"]: + value = getattr(self, attr) + if value: + l.append("{attr}={value:+g}".format(attr=attr, value=value)) + for attr in ["year", "month", "day", "weekday", + "hour", "minute", "second", "microsecond"]: + value = getattr(self, attr) + if value is not None: + l.append("{attr}={value}".format(attr=attr, value=repr(value))) + return "{classname}({attrs})".format(classname=self.__class__.__name__, + attrs=", ".join(l)) + + +def _sign(x): + return int(copysign(1, x)) + +# vim:ts=4:sw=4:et diff --git a/hug/lib/python3.12/site-packages/dateutil/rrule.py b/hug/lib/python3.12/site-packages/dateutil/rrule.py new file mode 100644 index 0000000000000000000000000000000000000000..84c7a1d567312ff07ec7ec7019c055e0a9d7db97 --- /dev/null +++ b/hug/lib/python3.12/site-packages/dateutil/rrule.py @@ -0,0 +1,1764 @@ +# -*- coding: utf-8 -*- +""" +The rrule module offers a small, complete, and very fast, implementation of +the recurrence rules documented in the +`iCalendar RFC `_, +including support for caching of results. +""" +import calendar +import datetime +import heapq +import itertools +import re +import sys +from functools import wraps +# For warning about deprecation of until and count +from warnings import warn + +from six import advance_iterator, integer_types + +from six.moves import _thread, range + +from ._common import weekday as weekdaybase + +try: + from math import gcd +except ImportError: + from fractions import gcd + +__all__ = ["rrule", "rruleset", "rrulestr", + "YEARLY", "MONTHLY", "WEEKLY", "DAILY", + "HOURLY", "MINUTELY", "SECONDLY", + "MO", "TU", "WE", "TH", "FR", "SA", "SU"] + +# Every mask is 7 days longer to handle cross-year weekly periods. +M366MASK = tuple([1]*31+[2]*29+[3]*31+[4]*30+[5]*31+[6]*30 + + [7]*31+[8]*31+[9]*30+[10]*31+[11]*30+[12]*31+[1]*7) +M365MASK = list(M366MASK) +M29, M30, M31 = list(range(1, 30)), list(range(1, 31)), list(range(1, 32)) +MDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7]) +MDAY365MASK = list(MDAY366MASK) +M29, M30, M31 = list(range(-29, 0)), list(range(-30, 0)), list(range(-31, 0)) +NMDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7]) +NMDAY365MASK = list(NMDAY366MASK) +M366RANGE = (0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366) +M365RANGE = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365) +WDAYMASK = [0, 1, 2, 3, 4, 5, 6]*55 +del M29, M30, M31, M365MASK[59], MDAY365MASK[59], NMDAY365MASK[31] +MDAY365MASK = tuple(MDAY365MASK) +M365MASK = tuple(M365MASK) + +FREQNAMES = ['YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY', 'HOURLY', 'MINUTELY', 'SECONDLY'] + +(YEARLY, + MONTHLY, + WEEKLY, + DAILY, + HOURLY, + MINUTELY, + SECONDLY) = list(range(7)) + +# Imported on demand. +easter = None +parser = None + + +class weekday(weekdaybase): + """ + This version of weekday does not allow n = 0. + """ + def __init__(self, wkday, n=None): + if n == 0: + raise ValueError("Can't create weekday with n==0") + + super(weekday, self).__init__(wkday, n) + + +MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7)) + + +def _invalidates_cache(f): + """ + Decorator for rruleset methods which may invalidate the + cached length. + """ + @wraps(f) + def inner_func(self, *args, **kwargs): + rv = f(self, *args, **kwargs) + self._invalidate_cache() + return rv + + return inner_func + + +class rrulebase(object): + def __init__(self, cache=False): + if cache: + self._cache = [] + self._cache_lock = _thread.allocate_lock() + self._invalidate_cache() + else: + self._cache = None + self._cache_complete = False + self._len = None + + def __reduce_ex__(self, protocol): + rv = super(rrulebase, self).__reduce_ex__(protocol) + state, slots = rv[2], None + if isinstance(state, tuple): + state, slots = state + if self._cache is not None: + state = dict(state) + state.pop("_cache_lock", None) + state.pop("_cache_gen", None) + state["_cache"] = [] + if slots is not None: + state = (state, slots) + return rv[:2] + (state,) + rv[3:] + + def __setstate__(self, state): + if isinstance(state, tuple): + state, slots = state + for name, value in slots.items(): + setattr(self, name, value) + self.__dict__.update(state) + if self._cache is not None: + self._cache_lock = _thread.allocate_lock() + self._invalidate_cache() + + def __iter__(self): + if self._cache_complete: + return iter(self._cache) + elif self._cache is None: + return self._iter() + else: + return self._iter_cached() + + def _invalidate_cache(self): + if self._cache is not None: + self._cache = [] + self._cache_complete = False + self._cache_gen = self._iter() + + if self._cache_lock.locked(): + self._cache_lock.release() + + self._len = None + + def _iter_cached(self): + i = 0 + gen = self._cache_gen + cache = self._cache + acquire = self._cache_lock.acquire + release = self._cache_lock.release + while gen: + if i == len(cache): + acquire() + if self._cache_complete: + break + try: + for j in range(10): + cache.append(advance_iterator(gen)) + except StopIteration: + self._cache_gen = gen = None + self._cache_complete = True + break + release() + yield cache[i] + i += 1 + while i < self._len: + yield cache[i] + i += 1 + + def __getitem__(self, item): + if self._cache_complete: + return self._cache[item] + elif isinstance(item, slice): + if item.step and item.step < 0: + return list(iter(self))[item] + else: + return list(itertools.islice(self, + item.start or 0, + item.stop or sys.maxsize, + item.step or 1)) + elif item >= 0: + gen = iter(self) + try: + for i in range(item+1): + res = advance_iterator(gen) + except StopIteration: + raise IndexError + return res + else: + return list(iter(self))[item] + + def __contains__(self, item): + if self._cache_complete: + return item in self._cache + else: + for i in self: + if i == item: + return True + elif i > item: + return False + return False + + # __len__() introduces a large performance penalty. + def count(self): + """ Returns the number of recurrences in this set. It will have go + through the whole recurrence, if this hasn't been done before. """ + if self._len is None: + for x in self: + pass + return self._len + + def before(self, dt, inc=False): + """ Returns the last recurrence before the given datetime instance. The + inc keyword defines what happens if dt is an occurrence. With + inc=True, if dt itself is an occurrence, it will be returned. """ + if self._cache_complete: + gen = self._cache + else: + gen = self + last = None + if inc: + for i in gen: + if i > dt: + break + last = i + else: + for i in gen: + if i >= dt: + break + last = i + return last + + def after(self, dt, inc=False): + """ Returns the first recurrence after the given datetime instance. The + inc keyword defines what happens if dt is an occurrence. With + inc=True, if dt itself is an occurrence, it will be returned. """ + if self._cache_complete: + gen = self._cache + else: + gen = self + if inc: + for i in gen: + if i >= dt: + return i + else: + for i in gen: + if i > dt: + return i + return None + + def xafter(self, dt, count=None, inc=False): + """ + Generator which yields up to `count` recurrences after the given + datetime instance, equivalent to `after`. + + :param dt: + The datetime at which to start generating recurrences. + + :param count: + The maximum number of recurrences to generate. If `None` (default), + dates are generated until the recurrence rule is exhausted. + + :param inc: + If `dt` is an instance of the rule and `inc` is `True`, it is + included in the output. + + :yields: Yields a sequence of `datetime` objects. + """ + + if self._cache_complete: + gen = self._cache + else: + gen = self + + # Select the comparison function + if inc: + comp = lambda dc, dtc: dc >= dtc + else: + comp = lambda dc, dtc: dc > dtc + + # Generate dates + n = 0 + for d in gen: + if comp(d, dt): + if count is not None: + n += 1 + if n > count: + break + + yield d + + def between(self, after, before, inc=False, count=1): + """ Returns all the occurrences of the rrule between after and before. + The inc keyword defines what happens if after and/or before are + themselves occurrences. With inc=True, they will be included in the + list, if they are found in the recurrence set. """ + if self._cache_complete: + gen = self._cache + else: + gen = self + started = False + l = [] + if inc: + for i in gen: + if i > before: + break + elif not started: + if i >= after: + started = True + l.append(i) + else: + l.append(i) + else: + for i in gen: + if i >= before: + break + elif not started: + if i > after: + started = True + l.append(i) + else: + l.append(i) + return l + + +class rrule(rrulebase): + """ + That's the base of the rrule operation. It accepts all the keywords + defined in the RFC as its constructor parameters (except byday, + which was renamed to byweekday) and more. The constructor prototype is:: + + rrule(freq) + + Where freq must be one of YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, + or SECONDLY. + + .. note:: + Per RFC section 3.3.10, recurrence instances falling on invalid dates + and times are ignored rather than coerced: + + Recurrence rules may generate recurrence instances with an invalid + date (e.g., February 30) or nonexistent local time (e.g., 1:30 AM + on a day where the local time is moved forward by an hour at 1:00 + AM). Such recurrence instances MUST be ignored and MUST NOT be + counted as part of the recurrence set. + + This can lead to possibly surprising behavior when, for example, the + start date occurs at the end of the month: + + >>> from dateutil.rrule import rrule, MONTHLY + >>> from datetime import datetime + >>> start_date = datetime(2014, 12, 31) + >>> list(rrule(freq=MONTHLY, count=4, dtstart=start_date)) + ... # doctest: +NORMALIZE_WHITESPACE + [datetime.datetime(2014, 12, 31, 0, 0), + datetime.datetime(2015, 1, 31, 0, 0), + datetime.datetime(2015, 3, 31, 0, 0), + datetime.datetime(2015, 5, 31, 0, 0)] + + Additionally, it supports the following keyword arguments: + + :param dtstart: + The recurrence start. Besides being the base for the recurrence, + missing parameters in the final recurrence instances will also be + extracted from this date. If not given, datetime.now() will be used + instead. + :param interval: + The interval between each freq iteration. For example, when using + YEARLY, an interval of 2 means once every two years, but with HOURLY, + it means once every two hours. The default interval is 1. + :param wkst: + The week start day. Must be one of the MO, TU, WE constants, or an + integer, specifying the first day of the week. This will affect + recurrences based on weekly periods. The default week start is got + from calendar.firstweekday(), and may be modified by + calendar.setfirstweekday(). + :param count: + If given, this determines how many occurrences will be generated. + + .. note:: + As of version 2.5.0, the use of the keyword ``until`` in conjunction + with ``count`` is deprecated, to make sure ``dateutil`` is fully + compliant with `RFC-5545 Sec. 3.3.10 `_. Therefore, ``until`` and ``count`` + **must not** occur in the same call to ``rrule``. + :param until: + If given, this must be a datetime instance specifying the upper-bound + limit of the recurrence. The last recurrence in the rule is the greatest + datetime that is less than or equal to the value specified in the + ``until`` parameter. + + .. note:: + As of version 2.5.0, the use of the keyword ``until`` in conjunction + with ``count`` is deprecated, to make sure ``dateutil`` is fully + compliant with `RFC-5545 Sec. 3.3.10 `_. Therefore, ``until`` and ``count`` + **must not** occur in the same call to ``rrule``. + :param bysetpos: + If given, it must be either an integer, or a sequence of integers, + positive or negative. Each given integer will specify an occurrence + number, corresponding to the nth occurrence of the rule inside the + frequency period. For example, a bysetpos of -1 if combined with a + MONTHLY frequency, and a byweekday of (MO, TU, WE, TH, FR), will + result in the last work day of every month. + :param bymonth: + If given, it must be either an integer, or a sequence of integers, + meaning the months to apply the recurrence to. + :param bymonthday: + If given, it must be either an integer, or a sequence of integers, + meaning the month days to apply the recurrence to. + :param byyearday: + If given, it must be either an integer, or a sequence of integers, + meaning the year days to apply the recurrence to. + :param byeaster: + If given, it must be either an integer, or a sequence of integers, + positive or negative. Each integer will define an offset from the + Easter Sunday. Passing the offset 0 to byeaster will yield the Easter + Sunday itself. This is an extension to the RFC specification. + :param byweekno: + If given, it must be either an integer, or a sequence of integers, + meaning the week numbers to apply the recurrence to. Week numbers + have the meaning described in ISO8601, that is, the first week of + the year is that containing at least four days of the new year. + :param byweekday: + If given, it must be either an integer (0 == MO), a sequence of + integers, one of the weekday constants (MO, TU, etc), or a sequence + of these constants. When given, these variables will define the + weekdays where the recurrence will be applied. It's also possible to + use an argument n for the weekday instances, which will mean the nth + occurrence of this weekday in the period. For example, with MONTHLY, + or with YEARLY and BYMONTH, using FR(+1) in byweekday will specify the + first friday of the month where the recurrence happens. Notice that in + the RFC documentation, this is specified as BYDAY, but was renamed to + avoid the ambiguity of that keyword. + :param byhour: + If given, it must be either an integer, or a sequence of integers, + meaning the hours to apply the recurrence to. + :param byminute: + If given, it must be either an integer, or a sequence of integers, + meaning the minutes to apply the recurrence to. + :param bysecond: + If given, it must be either an integer, or a sequence of integers, + meaning the seconds to apply the recurrence to. + :param cache: + If given, it must be a boolean value specifying to enable or disable + caching of results. If you will use the same rrule instance multiple + times, enabling caching will improve the performance considerably. + """ + def __init__(self, freq, dtstart=None, + interval=1, wkst=None, count=None, until=None, bysetpos=None, + bymonth=None, bymonthday=None, byyearday=None, byeaster=None, + byweekno=None, byweekday=None, + byhour=None, byminute=None, bysecond=None, + cache=False): + super(rrule, self).__init__(cache) + global easter + if not dtstart: + if until and until.tzinfo: + dtstart = datetime.datetime.now(tz=until.tzinfo).replace(microsecond=0) + else: + dtstart = datetime.datetime.now().replace(microsecond=0) + elif not isinstance(dtstart, datetime.datetime): + dtstart = datetime.datetime.fromordinal(dtstart.toordinal()) + else: + dtstart = dtstart.replace(microsecond=0) + self._dtstart = dtstart + self._tzinfo = dtstart.tzinfo + self._freq = freq + self._interval = interval + self._count = count + + # Cache the original byxxx rules, if they are provided, as the _byxxx + # attributes do not necessarily map to the inputs, and this can be + # a problem in generating the strings. Only store things if they've + # been supplied (the string retrieval will just use .get()) + self._original_rule = {} + + if until and not isinstance(until, datetime.datetime): + until = datetime.datetime.fromordinal(until.toordinal()) + self._until = until + + if self._dtstart and self._until: + if (self._dtstart.tzinfo is not None) != (self._until.tzinfo is not None): + # According to RFC5545 Section 3.3.10: + # https://tools.ietf.org/html/rfc5545#section-3.3.10 + # + # > If the "DTSTART" property is specified as a date with UTC + # > time or a date with local time and time zone reference, + # > then the UNTIL rule part MUST be specified as a date with + # > UTC time. + raise ValueError( + 'RRULE UNTIL values must be specified in UTC when DTSTART ' + 'is timezone-aware' + ) + + if count is not None and until: + warn("Using both 'count' and 'until' is inconsistent with RFC 5545" + " and has been deprecated in dateutil. Future versions will " + "raise an error.", DeprecationWarning) + + if wkst is None: + self._wkst = calendar.firstweekday() + elif isinstance(wkst, integer_types): + self._wkst = wkst + else: + self._wkst = wkst.weekday + + if bysetpos is None: + self._bysetpos = None + elif isinstance(bysetpos, integer_types): + if bysetpos == 0 or not (-366 <= bysetpos <= 366): + raise ValueError("bysetpos must be between 1 and 366, " + "or between -366 and -1") + self._bysetpos = (bysetpos,) + else: + self._bysetpos = tuple(bysetpos) + for pos in self._bysetpos: + if pos == 0 or not (-366 <= pos <= 366): + raise ValueError("bysetpos must be between 1 and 366, " + "or between -366 and -1") + + if self._bysetpos: + self._original_rule['bysetpos'] = self._bysetpos + + if (byweekno is None and byyearday is None and bymonthday is None and + byweekday is None and byeaster is None): + if freq == YEARLY: + if bymonth is None: + bymonth = dtstart.month + self._original_rule['bymonth'] = None + bymonthday = dtstart.day + self._original_rule['bymonthday'] = None + elif freq == MONTHLY: + bymonthday = dtstart.day + self._original_rule['bymonthday'] = None + elif freq == WEEKLY: + byweekday = dtstart.weekday() + self._original_rule['byweekday'] = None + + # bymonth + if bymonth is None: + self._bymonth = None + else: + if isinstance(bymonth, integer_types): + bymonth = (bymonth,) + + self._bymonth = tuple(sorted(set(bymonth))) + + if 'bymonth' not in self._original_rule: + self._original_rule['bymonth'] = self._bymonth + + # byyearday + if byyearday is None: + self._byyearday = None + else: + if isinstance(byyearday, integer_types): + byyearday = (byyearday,) + + self._byyearday = tuple(sorted(set(byyearday))) + self._original_rule['byyearday'] = self._byyearday + + # byeaster + if byeaster is not None: + if not easter: + from dateutil import easter + if isinstance(byeaster, integer_types): + self._byeaster = (byeaster,) + else: + self._byeaster = tuple(sorted(byeaster)) + + self._original_rule['byeaster'] = self._byeaster + else: + self._byeaster = None + + # bymonthday + if bymonthday is None: + self._bymonthday = () + self._bynmonthday = () + else: + if isinstance(bymonthday, integer_types): + bymonthday = (bymonthday,) + + bymonthday = set(bymonthday) # Ensure it's unique + + self._bymonthday = tuple(sorted(x for x in bymonthday if x > 0)) + self._bynmonthday = tuple(sorted(x for x in bymonthday if x < 0)) + + # Storing positive numbers first, then negative numbers + if 'bymonthday' not in self._original_rule: + self._original_rule['bymonthday'] = tuple( + itertools.chain(self._bymonthday, self._bynmonthday)) + + # byweekno + if byweekno is None: + self._byweekno = None + else: + if isinstance(byweekno, integer_types): + byweekno = (byweekno,) + + self._byweekno = tuple(sorted(set(byweekno))) + + self._original_rule['byweekno'] = self._byweekno + + # byweekday / bynweekday + if byweekday is None: + self._byweekday = None + self._bynweekday = None + else: + # If it's one of the valid non-sequence types, convert to a + # single-element sequence before the iterator that builds the + # byweekday set. + if isinstance(byweekday, integer_types) or hasattr(byweekday, "n"): + byweekday = (byweekday,) + + self._byweekday = set() + self._bynweekday = set() + for wday in byweekday: + if isinstance(wday, integer_types): + self._byweekday.add(wday) + elif not wday.n or freq > MONTHLY: + self._byweekday.add(wday.weekday) + else: + self._bynweekday.add((wday.weekday, wday.n)) + + if not self._byweekday: + self._byweekday = None + elif not self._bynweekday: + self._bynweekday = None + + if self._byweekday is not None: + self._byweekday = tuple(sorted(self._byweekday)) + orig_byweekday = [weekday(x) for x in self._byweekday] + else: + orig_byweekday = () + + if self._bynweekday is not None: + self._bynweekday = tuple(sorted(self._bynweekday)) + orig_bynweekday = [weekday(*x) for x in self._bynweekday] + else: + orig_bynweekday = () + + if 'byweekday' not in self._original_rule: + self._original_rule['byweekday'] = tuple(itertools.chain( + orig_byweekday, orig_bynweekday)) + + # byhour + if byhour is None: + if freq < HOURLY: + self._byhour = {dtstart.hour} + else: + self._byhour = None + else: + if isinstance(byhour, integer_types): + byhour = (byhour,) + + if freq == HOURLY: + self._byhour = self.__construct_byset(start=dtstart.hour, + byxxx=byhour, + base=24) + else: + self._byhour = set(byhour) + + self._byhour = tuple(sorted(self._byhour)) + self._original_rule['byhour'] = self._byhour + + # byminute + if byminute is None: + if freq < MINUTELY: + self._byminute = {dtstart.minute} + else: + self._byminute = None + else: + if isinstance(byminute, integer_types): + byminute = (byminute,) + + if freq == MINUTELY: + self._byminute = self.__construct_byset(start=dtstart.minute, + byxxx=byminute, + base=60) + else: + self._byminute = set(byminute) + + self._byminute = tuple(sorted(self._byminute)) + self._original_rule['byminute'] = self._byminute + + # bysecond + if bysecond is None: + if freq < SECONDLY: + self._bysecond = ((dtstart.second,)) + else: + self._bysecond = None + else: + if isinstance(bysecond, integer_types): + bysecond = (bysecond,) + + self._bysecond = set(bysecond) + + if freq == SECONDLY: + self._bysecond = self.__construct_byset(start=dtstart.second, + byxxx=bysecond, + base=60) + else: + self._bysecond = set(bysecond) + + self._bysecond = tuple(sorted(self._bysecond)) + self._original_rule['bysecond'] = self._bysecond + + if self._freq >= HOURLY: + self._timeset = None + else: + self._timeset = [] + for hour in self._byhour: + for minute in self._byminute: + for second in self._bysecond: + self._timeset.append( + datetime.time(hour, minute, second, + tzinfo=self._tzinfo)) + self._timeset.sort() + self._timeset = tuple(self._timeset) + + def __str__(self): + """ + Output a string that would generate this RRULE if passed to rrulestr. + This is mostly compatible with RFC5545, except for the + dateutil-specific extension BYEASTER. + """ + + output = [] + h, m, s = [None] * 3 + if self._dtstart: + output.append(self._dtstart.strftime('DTSTART:%Y%m%dT%H%M%S')) + h, m, s = self._dtstart.timetuple()[3:6] + + parts = ['FREQ=' + FREQNAMES[self._freq]] + if self._interval != 1: + parts.append('INTERVAL=' + str(self._interval)) + + if self._wkst: + parts.append('WKST=' + repr(weekday(self._wkst))[0:2]) + + if self._count is not None: + parts.append('COUNT=' + str(self._count)) + + if self._until: + parts.append(self._until.strftime('UNTIL=%Y%m%dT%H%M%S')) + + if self._original_rule.get('byweekday') is not None: + # The str() method on weekday objects doesn't generate + # RFC5545-compliant strings, so we should modify that. + original_rule = dict(self._original_rule) + wday_strings = [] + for wday in original_rule['byweekday']: + if wday.n: + wday_strings.append('{n:+d}{wday}'.format( + n=wday.n, + wday=repr(wday)[0:2])) + else: + wday_strings.append(repr(wday)) + + original_rule['byweekday'] = wday_strings + else: + original_rule = self._original_rule + + partfmt = '{name}={vals}' + for name, key in [('BYSETPOS', 'bysetpos'), + ('BYMONTH', 'bymonth'), + ('BYMONTHDAY', 'bymonthday'), + ('BYYEARDAY', 'byyearday'), + ('BYWEEKNO', 'byweekno'), + ('BYDAY', 'byweekday'), + ('BYHOUR', 'byhour'), + ('BYMINUTE', 'byminute'), + ('BYSECOND', 'bysecond'), + ('BYEASTER', 'byeaster')]: + value = original_rule.get(key) + if value: + parts.append(partfmt.format(name=name, vals=(','.join(str(v) + for v in value)))) + + output.append('RRULE:' + ';'.join(parts)) + return '\n'.join(output) + + def replace(self, **kwargs): + """Return new rrule with same attributes except for those attributes given new + values by whichever keyword arguments are specified.""" + new_kwargs = {"interval": self._interval, + "count": self._count, + "dtstart": self._dtstart, + "freq": self._freq, + "until": self._until, + "wkst": self._wkst, + "cache": False if self._cache is None else True } + new_kwargs.update(self._original_rule) + new_kwargs.update(kwargs) + return rrule(**new_kwargs) + + def _iter(self): + year, month, day, hour, minute, second, weekday, yearday, _ = \ + self._dtstart.timetuple() + + # Some local variables to speed things up a bit + freq = self._freq + interval = self._interval + wkst = self._wkst + until = self._until + bymonth = self._bymonth + byweekno = self._byweekno + byyearday = self._byyearday + byweekday = self._byweekday + byeaster = self._byeaster + bymonthday = self._bymonthday + bynmonthday = self._bynmonthday + bysetpos = self._bysetpos + byhour = self._byhour + byminute = self._byminute + bysecond = self._bysecond + + ii = _iterinfo(self) + ii.rebuild(year, month) + + getdayset = {YEARLY: ii.ydayset, + MONTHLY: ii.mdayset, + WEEKLY: ii.wdayset, + DAILY: ii.ddayset, + HOURLY: ii.ddayset, + MINUTELY: ii.ddayset, + SECONDLY: ii.ddayset}[freq] + + if freq < HOURLY: + timeset = self._timeset + else: + gettimeset = {HOURLY: ii.htimeset, + MINUTELY: ii.mtimeset, + SECONDLY: ii.stimeset}[freq] + if ((freq >= HOURLY and + self._byhour and hour not in self._byhour) or + (freq >= MINUTELY and + self._byminute and minute not in self._byminute) or + (freq >= SECONDLY and + self._bysecond and second not in self._bysecond)): + timeset = () + else: + timeset = gettimeset(hour, minute, second) + + total = 0 + count = self._count + while True: + # Get dayset with the right frequency + dayset, start, end = getdayset(year, month, day) + + # Do the "hard" work ;-) + filtered = False + for i in dayset[start:end]: + if ((bymonth and ii.mmask[i] not in bymonth) or + (byweekno and not ii.wnomask[i]) or + (byweekday and ii.wdaymask[i] not in byweekday) or + (ii.nwdaymask and not ii.nwdaymask[i]) or + (byeaster and not ii.eastermask[i]) or + ((bymonthday or bynmonthday) and + ii.mdaymask[i] not in bymonthday and + ii.nmdaymask[i] not in bynmonthday) or + (byyearday and + ((i < ii.yearlen and i+1 not in byyearday and + -ii.yearlen+i not in byyearday) or + (i >= ii.yearlen and i+1-ii.yearlen not in byyearday and + -ii.nextyearlen+i-ii.yearlen not in byyearday)))): + dayset[i] = None + filtered = True + + # Output results + if bysetpos and timeset: + poslist = [] + for pos in bysetpos: + if pos < 0: + daypos, timepos = divmod(pos, len(timeset)) + else: + daypos, timepos = divmod(pos-1, len(timeset)) + try: + i = [x for x in dayset[start:end] + if x is not None][daypos] + time = timeset[timepos] + except IndexError: + pass + else: + date = datetime.date.fromordinal(ii.yearordinal+i) + res = datetime.datetime.combine(date, time) + if res not in poslist: + poslist.append(res) + poslist.sort() + for res in poslist: + if until and res > until: + self._len = total + return + elif res >= self._dtstart: + if count is not None: + count -= 1 + if count < 0: + self._len = total + return + total += 1 + yield res + else: + for i in dayset[start:end]: + if i is not None: + date = datetime.date.fromordinal(ii.yearordinal + i) + for time in timeset: + res = datetime.datetime.combine(date, time) + if until and res > until: + self._len = total + return + elif res >= self._dtstart: + if count is not None: + count -= 1 + if count < 0: + self._len = total + return + + total += 1 + yield res + + # Handle frequency and interval + fixday = False + if freq == YEARLY: + year += interval + if year > datetime.MAXYEAR: + self._len = total + return + ii.rebuild(year, month) + elif freq == MONTHLY: + month += interval + if month > 12: + div, mod = divmod(month, 12) + month = mod + year += div + if month == 0: + month = 12 + year -= 1 + if year > datetime.MAXYEAR: + self._len = total + return + ii.rebuild(year, month) + elif freq == WEEKLY: + if wkst > weekday: + day += -(weekday+1+(6-wkst))+self._interval*7 + else: + day += -(weekday-wkst)+self._interval*7 + weekday = wkst + fixday = True + elif freq == DAILY: + day += interval + fixday = True + elif freq == HOURLY: + if filtered: + # Jump to one iteration before next day + hour += ((23-hour)//interval)*interval + + if byhour: + ndays, hour = self.__mod_distance(value=hour, + byxxx=self._byhour, + base=24) + else: + ndays, hour = divmod(hour+interval, 24) + + if ndays: + day += ndays + fixday = True + + timeset = gettimeset(hour, minute, second) + elif freq == MINUTELY: + if filtered: + # Jump to one iteration before next day + minute += ((1439-(hour*60+minute))//interval)*interval + + valid = False + rep_rate = (24*60) + for j in range(rep_rate // gcd(interval, rep_rate)): + if byminute: + nhours, minute = \ + self.__mod_distance(value=minute, + byxxx=self._byminute, + base=60) + else: + nhours, minute = divmod(minute+interval, 60) + + div, hour = divmod(hour+nhours, 24) + if div: + day += div + fixday = True + filtered = False + + if not byhour or hour in byhour: + valid = True + break + + if not valid: + raise ValueError('Invalid combination of interval and ' + + 'byhour resulting in empty rule.') + + timeset = gettimeset(hour, minute, second) + elif freq == SECONDLY: + if filtered: + # Jump to one iteration before next day + second += (((86399 - (hour * 3600 + minute * 60 + second)) + // interval) * interval) + + rep_rate = (24 * 3600) + valid = False + for j in range(0, rep_rate // gcd(interval, rep_rate)): + if bysecond: + nminutes, second = \ + self.__mod_distance(value=second, + byxxx=self._bysecond, + base=60) + else: + nminutes, second = divmod(second+interval, 60) + + div, minute = divmod(minute+nminutes, 60) + if div: + hour += div + div, hour = divmod(hour, 24) + if div: + day += div + fixday = True + + if ((not byhour or hour in byhour) and + (not byminute or minute in byminute) and + (not bysecond or second in bysecond)): + valid = True + break + + if not valid: + raise ValueError('Invalid combination of interval, ' + + 'byhour and byminute resulting in empty' + + ' rule.') + + timeset = gettimeset(hour, minute, second) + + if fixday and day > 28: + daysinmonth = calendar.monthrange(year, month)[1] + if day > daysinmonth: + while day > daysinmonth: + day -= daysinmonth + month += 1 + if month == 13: + month = 1 + year += 1 + if year > datetime.MAXYEAR: + self._len = total + return + daysinmonth = calendar.monthrange(year, month)[1] + ii.rebuild(year, month) + + def __construct_byset(self, start, byxxx, base): + """ + If a `BYXXX` sequence is passed to the constructor at the same level as + `FREQ` (e.g. `FREQ=HOURLY,BYHOUR={2,4,7},INTERVAL=3`), there are some + specifications which cannot be reached given some starting conditions. + + This occurs whenever the interval is not coprime with the base of a + given unit and the difference between the starting position and the + ending position is not coprime with the greatest common denominator + between the interval and the base. For example, with a FREQ of hourly + starting at 17:00 and an interval of 4, the only valid values for + BYHOUR would be {21, 1, 5, 9, 13, 17}, because 4 and 24 are not + coprime. + + :param start: + Specifies the starting position. + :param byxxx: + An iterable containing the list of allowed values. + :param base: + The largest allowable value for the specified frequency (e.g. + 24 hours, 60 minutes). + + This does not preserve the type of the iterable, returning a set, since + the values should be unique and the order is irrelevant, this will + speed up later lookups. + + In the event of an empty set, raises a :exception:`ValueError`, as this + results in an empty rrule. + """ + + cset = set() + + # Support a single byxxx value. + if isinstance(byxxx, integer_types): + byxxx = (byxxx, ) + + for num in byxxx: + i_gcd = gcd(self._interval, base) + # Use divmod rather than % because we need to wrap negative nums. + if i_gcd == 1 or divmod(num - start, i_gcd)[1] == 0: + cset.add(num) + + if len(cset) == 0: + raise ValueError("Invalid rrule byxxx generates an empty set.") + + return cset + + def __mod_distance(self, value, byxxx, base): + """ + Calculates the next value in a sequence where the `FREQ` parameter is + specified along with a `BYXXX` parameter at the same "level" + (e.g. `HOURLY` specified with `BYHOUR`). + + :param value: + The old value of the component. + :param byxxx: + The `BYXXX` set, which should have been generated by + `rrule._construct_byset`, or something else which checks that a + valid rule is present. + :param base: + The largest allowable value for the specified frequency (e.g. + 24 hours, 60 minutes). + + If a valid value is not found after `base` iterations (the maximum + number before the sequence would start to repeat), this raises a + :exception:`ValueError`, as no valid values were found. + + This returns a tuple of `divmod(n*interval, base)`, where `n` is the + smallest number of `interval` repetitions until the next specified + value in `byxxx` is found. + """ + accumulator = 0 + for ii in range(1, base + 1): + # Using divmod() over % to account for negative intervals + div, value = divmod(value + self._interval, base) + accumulator += div + if value in byxxx: + return (accumulator, value) + + +class _iterinfo(object): + __slots__ = ["rrule", "lastyear", "lastmonth", + "yearlen", "nextyearlen", "yearordinal", "yearweekday", + "mmask", "mrange", "mdaymask", "nmdaymask", + "wdaymask", "wnomask", "nwdaymask", "eastermask"] + + def __init__(self, rrule): + for attr in self.__slots__: + setattr(self, attr, None) + self.rrule = rrule + + def rebuild(self, year, month): + # Every mask is 7 days longer to handle cross-year weekly periods. + rr = self.rrule + if year != self.lastyear: + self.yearlen = 365 + calendar.isleap(year) + self.nextyearlen = 365 + calendar.isleap(year + 1) + firstyday = datetime.date(year, 1, 1) + self.yearordinal = firstyday.toordinal() + self.yearweekday = firstyday.weekday() + + wday = datetime.date(year, 1, 1).weekday() + if self.yearlen == 365: + self.mmask = M365MASK + self.mdaymask = MDAY365MASK + self.nmdaymask = NMDAY365MASK + self.wdaymask = WDAYMASK[wday:] + self.mrange = M365RANGE + else: + self.mmask = M366MASK + self.mdaymask = MDAY366MASK + self.nmdaymask = NMDAY366MASK + self.wdaymask = WDAYMASK[wday:] + self.mrange = M366RANGE + + if not rr._byweekno: + self.wnomask = None + else: + self.wnomask = [0]*(self.yearlen+7) + # no1wkst = firstwkst = self.wdaymask.index(rr._wkst) + no1wkst = firstwkst = (7-self.yearweekday+rr._wkst) % 7 + if no1wkst >= 4: + no1wkst = 0 + # Number of days in the year, plus the days we got + # from last year. + wyearlen = self.yearlen+(self.yearweekday-rr._wkst) % 7 + else: + # Number of days in the year, minus the days we + # left in last year. + wyearlen = self.yearlen-no1wkst + div, mod = divmod(wyearlen, 7) + numweeks = div+mod//4 + for n in rr._byweekno: + if n < 0: + n += numweeks+1 + if not (0 < n <= numweeks): + continue + if n > 1: + i = no1wkst+(n-1)*7 + if no1wkst != firstwkst: + i -= 7-firstwkst + else: + i = no1wkst + for j in range(7): + self.wnomask[i] = 1 + i += 1 + if self.wdaymask[i] == rr._wkst: + break + if 1 in rr._byweekno: + # Check week number 1 of next year as well + # TODO: Check -numweeks for next year. + i = no1wkst+numweeks*7 + if no1wkst != firstwkst: + i -= 7-firstwkst + if i < self.yearlen: + # If week starts in next year, we + # don't care about it. + for j in range(7): + self.wnomask[i] = 1 + i += 1 + if self.wdaymask[i] == rr._wkst: + break + if no1wkst: + # Check last week number of last year as + # well. If no1wkst is 0, either the year + # started on week start, or week number 1 + # got days from last year, so there are no + # days from last year's last week number in + # this year. + if -1 not in rr._byweekno: + lyearweekday = datetime.date(year-1, 1, 1).weekday() + lno1wkst = (7-lyearweekday+rr._wkst) % 7 + lyearlen = 365+calendar.isleap(year-1) + if lno1wkst >= 4: + lno1wkst = 0 + lnumweeks = 52+(lyearlen + + (lyearweekday-rr._wkst) % 7) % 7//4 + else: + lnumweeks = 52+(self.yearlen-no1wkst) % 7//4 + else: + lnumweeks = -1 + if lnumweeks in rr._byweekno: + for i in range(no1wkst): + self.wnomask[i] = 1 + + if (rr._bynweekday and (month != self.lastmonth or + year != self.lastyear)): + ranges = [] + if rr._freq == YEARLY: + if rr._bymonth: + for month in rr._bymonth: + ranges.append(self.mrange[month-1:month+1]) + else: + ranges = [(0, self.yearlen)] + elif rr._freq == MONTHLY: + ranges = [self.mrange[month-1:month+1]] + if ranges: + # Weekly frequency won't get here, so we may not + # care about cross-year weekly periods. + self.nwdaymask = [0]*self.yearlen + for first, last in ranges: + last -= 1 + for wday, n in rr._bynweekday: + if n < 0: + i = last+(n+1)*7 + i -= (self.wdaymask[i]-wday) % 7 + else: + i = first+(n-1)*7 + i += (7-self.wdaymask[i]+wday) % 7 + if first <= i <= last: + self.nwdaymask[i] = 1 + + if rr._byeaster: + self.eastermask = [0]*(self.yearlen+7) + eyday = easter.easter(year).toordinal()-self.yearordinal + for offset in rr._byeaster: + self.eastermask[eyday+offset] = 1 + + self.lastyear = year + self.lastmonth = month + + def ydayset(self, year, month, day): + return list(range(self.yearlen)), 0, self.yearlen + + def mdayset(self, year, month, day): + dset = [None]*self.yearlen + start, end = self.mrange[month-1:month+1] + for i in range(start, end): + dset[i] = i + return dset, start, end + + def wdayset(self, year, month, day): + # We need to handle cross-year weeks here. + dset = [None]*(self.yearlen+7) + i = datetime.date(year, month, day).toordinal()-self.yearordinal + start = i + for j in range(7): + dset[i] = i + i += 1 + # if (not (0 <= i < self.yearlen) or + # self.wdaymask[i] == self.rrule._wkst): + # This will cross the year boundary, if necessary. + if self.wdaymask[i] == self.rrule._wkst: + break + return dset, start, i + + def ddayset(self, year, month, day): + dset = [None] * self.yearlen + i = datetime.date(year, month, day).toordinal() - self.yearordinal + dset[i] = i + return dset, i, i + 1 + + def htimeset(self, hour, minute, second): + tset = [] + rr = self.rrule + for minute in rr._byminute: + for second in rr._bysecond: + tset.append(datetime.time(hour, minute, second, + tzinfo=rr._tzinfo)) + tset.sort() + return tset + + def mtimeset(self, hour, minute, second): + tset = [] + rr = self.rrule + for second in rr._bysecond: + tset.append(datetime.time(hour, minute, second, tzinfo=rr._tzinfo)) + tset.sort() + return tset + + def stimeset(self, hour, minute, second): + return (datetime.time(hour, minute, second, + tzinfo=self.rrule._tzinfo),) + + +class rruleset(rrulebase): + """ The rruleset type allows more complex recurrence setups, mixing + multiple rules, dates, exclusion rules, and exclusion dates. The type + constructor takes the following keyword arguments: + + :param cache: If True, caching of results will be enabled, improving + performance of multiple queries considerably. """ + + class _genitem(object): + def __init__(self, genlist, gen): + try: + self.dt = advance_iterator(gen) + genlist.append(self) + except StopIteration: + pass + self.genlist = genlist + self.gen = gen + + def __next__(self): + try: + self.dt = advance_iterator(self.gen) + except StopIteration: + if self.genlist[0] is self: + heapq.heappop(self.genlist) + else: + self.genlist.remove(self) + heapq.heapify(self.genlist) + + next = __next__ + + def __lt__(self, other): + return self.dt < other.dt + + def __gt__(self, other): + return self.dt > other.dt + + def __eq__(self, other): + return self.dt == other.dt + + def __ne__(self, other): + return self.dt != other.dt + + def __init__(self, cache=False): + super(rruleset, self).__init__(cache) + self._rrule = [] + self._rdate = [] + self._exrule = [] + self._exdate = [] + + @_invalidates_cache + def rrule(self, rrule): + """ Include the given :py:class:`rrule` instance in the recurrence set + generation. """ + self._rrule.append(rrule) + + @_invalidates_cache + def rdate(self, rdate): + """ Include the given :py:class:`datetime` instance in the recurrence + set generation. """ + self._rdate.append(rdate) + + @_invalidates_cache + def exrule(self, exrule): + """ Include the given rrule instance in the recurrence set exclusion + list. Dates which are part of the given recurrence rules will not + be generated, even if some inclusive rrule or rdate matches them. + """ + self._exrule.append(exrule) + + @_invalidates_cache + def exdate(self, exdate): + """ Include the given datetime instance in the recurrence set + exclusion list. Dates included that way will not be generated, + even if some inclusive rrule or rdate matches them. """ + self._exdate.append(exdate) + + def _iter(self): + rlist = [] + self._rdate.sort() + self._genitem(rlist, iter(self._rdate)) + for gen in [iter(x) for x in self._rrule]: + self._genitem(rlist, gen) + exlist = [] + self._exdate.sort() + self._genitem(exlist, iter(self._exdate)) + for gen in [iter(x) for x in self._exrule]: + self._genitem(exlist, gen) + lastdt = None + total = 0 + heapq.heapify(rlist) + heapq.heapify(exlist) + while rlist: + ritem = rlist[0] + if not lastdt or lastdt != ritem.dt: + while exlist and exlist[0] < ritem: + exitem = exlist[0] + advance_iterator(exitem) + if exlist and exlist[0] is exitem: + heapq.heapreplace(exlist, exitem) + if not exlist or ritem != exlist[0]: + total += 1 + yield ritem.dt + lastdt = ritem.dt + advance_iterator(ritem) + if rlist and rlist[0] is ritem: + heapq.heapreplace(rlist, ritem) + self._len = total + + + + +class _rrulestr(object): + """ Parses a string representation of a recurrence rule or set of + recurrence rules. + + :param s: + Required, a string defining one or more recurrence rules. + + :param dtstart: + If given, used as the default recurrence start if not specified in the + rule string. + + :param cache: + If set ``True`` caching of results will be enabled, improving + performance of multiple queries considerably. + + :param unfold: + If set ``True`` indicates that a rule string is split over more + than one line and should be joined before processing. + + :param forceset: + If set ``True`` forces a :class:`dateutil.rrule.rruleset` to + be returned. + + :param compatible: + If set ``True`` forces ``unfold`` and ``forceset`` to be ``True``. + + :param ignoretz: + If set ``True``, time zones in parsed strings are ignored and a naive + :class:`datetime.datetime` object is returned. + + :param tzids: + If given, a callable or mapping used to retrieve a + :class:`datetime.tzinfo` from a string representation. + Defaults to :func:`dateutil.tz.gettz`. + + :param tzinfos: + Additional time zone names / aliases which may be present in a string + representation. See :func:`dateutil.parser.parse` for more + information. + + :return: + Returns a :class:`dateutil.rrule.rruleset` or + :class:`dateutil.rrule.rrule` + """ + + _freq_map = {"YEARLY": YEARLY, + "MONTHLY": MONTHLY, + "WEEKLY": WEEKLY, + "DAILY": DAILY, + "HOURLY": HOURLY, + "MINUTELY": MINUTELY, + "SECONDLY": SECONDLY} + + _weekday_map = {"MO": 0, "TU": 1, "WE": 2, "TH": 3, + "FR": 4, "SA": 5, "SU": 6} + + def _handle_int(self, rrkwargs, name, value, **kwargs): + rrkwargs[name.lower()] = int(value) + + def _handle_int_list(self, rrkwargs, name, value, **kwargs): + rrkwargs[name.lower()] = [int(x) for x in value.split(',')] + + _handle_INTERVAL = _handle_int + _handle_COUNT = _handle_int + _handle_BYSETPOS = _handle_int_list + _handle_BYMONTH = _handle_int_list + _handle_BYMONTHDAY = _handle_int_list + _handle_BYYEARDAY = _handle_int_list + _handle_BYEASTER = _handle_int_list + _handle_BYWEEKNO = _handle_int_list + _handle_BYHOUR = _handle_int_list + _handle_BYMINUTE = _handle_int_list + _handle_BYSECOND = _handle_int_list + + def _handle_FREQ(self, rrkwargs, name, value, **kwargs): + rrkwargs["freq"] = self._freq_map[value] + + def _handle_UNTIL(self, rrkwargs, name, value, **kwargs): + global parser + if not parser: + from dateutil import parser + try: + rrkwargs["until"] = parser.parse(value, + ignoretz=kwargs.get("ignoretz"), + tzinfos=kwargs.get("tzinfos")) + except ValueError: + raise ValueError("invalid until date") + + def _handle_WKST(self, rrkwargs, name, value, **kwargs): + rrkwargs["wkst"] = self._weekday_map[value] + + def _handle_BYWEEKDAY(self, rrkwargs, name, value, **kwargs): + """ + Two ways to specify this: +1MO or MO(+1) + """ + l = [] + for wday in value.split(','): + if '(' in wday: + # If it's of the form TH(+1), etc. + splt = wday.split('(') + w = splt[0] + n = int(splt[1][:-1]) + elif len(wday): + # If it's of the form +1MO + for i in range(len(wday)): + if wday[i] not in '+-0123456789': + break + n = wday[:i] or None + w = wday[i:] + if n: + n = int(n) + else: + raise ValueError("Invalid (empty) BYDAY specification.") + + l.append(weekdays[self._weekday_map[w]](n)) + rrkwargs["byweekday"] = l + + _handle_BYDAY = _handle_BYWEEKDAY + + def _parse_rfc_rrule(self, line, + dtstart=None, + cache=False, + ignoretz=False, + tzinfos=None): + if line.find(':') != -1: + name, value = line.split(':') + if name != "RRULE": + raise ValueError("unknown parameter name") + else: + value = line + rrkwargs = {} + for pair in value.split(';'): + name, value = pair.split('=') + name = name.upper() + value = value.upper() + try: + getattr(self, "_handle_"+name)(rrkwargs, name, value, + ignoretz=ignoretz, + tzinfos=tzinfos) + except AttributeError: + raise ValueError("unknown parameter '%s'" % name) + except (KeyError, ValueError): + raise ValueError("invalid '%s': %s" % (name, value)) + return rrule(dtstart=dtstart, cache=cache, **rrkwargs) + + def _parse_date_value(self, date_value, parms, rule_tzids, + ignoretz, tzids, tzinfos): + global parser + if not parser: + from dateutil import parser + + datevals = [] + value_found = False + TZID = None + + for parm in parms: + if parm.startswith("TZID="): + try: + tzkey = rule_tzids[parm.split('TZID=')[-1]] + except KeyError: + continue + if tzids is None: + from . import tz + tzlookup = tz.gettz + elif callable(tzids): + tzlookup = tzids + else: + tzlookup = getattr(tzids, 'get', None) + if tzlookup is None: + msg = ('tzids must be a callable, mapping, or None, ' + 'not %s' % tzids) + raise ValueError(msg) + + TZID = tzlookup(tzkey) + continue + + # RFC 5445 3.8.2.4: The VALUE parameter is optional, but may be found + # only once. + if parm not in {"VALUE=DATE-TIME", "VALUE=DATE"}: + raise ValueError("unsupported parm: " + parm) + else: + if value_found: + msg = ("Duplicate value parameter found in: " + parm) + raise ValueError(msg) + value_found = True + + for datestr in date_value.split(','): + date = parser.parse(datestr, ignoretz=ignoretz, tzinfos=tzinfos) + if TZID is not None: + if date.tzinfo is None: + date = date.replace(tzinfo=TZID) + else: + raise ValueError('DTSTART/EXDATE specifies multiple timezone') + datevals.append(date) + + return datevals + + def _parse_rfc(self, s, + dtstart=None, + cache=False, + unfold=False, + forceset=False, + compatible=False, + ignoretz=False, + tzids=None, + tzinfos=None): + global parser + if compatible: + forceset = True + unfold = True + + TZID_NAMES = dict(map( + lambda x: (x.upper(), x), + re.findall('TZID=(?P[^:]+):', s) + )) + s = s.upper() + if not s.strip(): + raise ValueError("empty string") + if unfold: + lines = s.splitlines() + i = 0 + while i < len(lines): + line = lines[i].rstrip() + if not line: + del lines[i] + elif i > 0 and line[0] == " ": + lines[i-1] += line[1:] + del lines[i] + else: + i += 1 + else: + lines = s.split() + if (not forceset and len(lines) == 1 and (s.find(':') == -1 or + s.startswith('RRULE:'))): + return self._parse_rfc_rrule(lines[0], cache=cache, + dtstart=dtstart, ignoretz=ignoretz, + tzinfos=tzinfos) + else: + rrulevals = [] + rdatevals = [] + exrulevals = [] + exdatevals = [] + for line in lines: + if not line: + continue + if line.find(':') == -1: + name = "RRULE" + value = line + else: + name, value = line.split(':', 1) + parms = name.split(';') + if not parms: + raise ValueError("empty property name") + name = parms[0] + parms = parms[1:] + if name == "RRULE": + for parm in parms: + if parm.startswith("X-"): + # Experimental random vendor features; safe to ignore + continue + raise ValueError("unsupported RRULE parm: " + parm) + rrulevals.append(value) + elif name == "RDATE": + for parm in parms: + if parm != "VALUE=DATE-TIME": + raise ValueError("unsupported RDATE parm: "+parm) + rdatevals.append(value) + elif name == "EXRULE": + for parm in parms: + raise ValueError("unsupported EXRULE parm: "+parm) + exrulevals.append(value) + elif name == "EXDATE": + exdatevals.extend( + self._parse_date_value(value, parms, + TZID_NAMES, ignoretz, + tzids, tzinfos) + ) + elif name == "DTSTART": + dtvals = self._parse_date_value(value, parms, TZID_NAMES, + ignoretz, tzids, tzinfos) + if len(dtvals) != 1: + raise ValueError("Multiple DTSTART values specified:" + + value) + dtstart = dtvals[0] + else: + raise ValueError("unsupported property: "+name) + if (forceset or len(rrulevals) > 1 or rdatevals + or exrulevals or exdatevals): + if not parser and (rdatevals or exdatevals): + from dateutil import parser + rset = rruleset(cache=cache) + for value in rrulevals: + rset.rrule(self._parse_rfc_rrule(value, dtstart=dtstart, + ignoretz=ignoretz, + tzinfos=tzinfos)) + for value in rdatevals: + for datestr in value.split(','): + rset.rdate(parser.parse(datestr, + ignoretz=ignoretz, + tzinfos=tzinfos)) + for value in exrulevals: + rset.exrule(self._parse_rfc_rrule(value, dtstart=dtstart, + ignoretz=ignoretz, + tzinfos=tzinfos)) + for value in exdatevals: + rset.exdate(value) + if compatible and dtstart: + rset.rdate(dtstart) + return rset + else: + return self._parse_rfc_rrule(rrulevals[0], + dtstart=dtstart, + cache=cache, + ignoretz=ignoretz, + tzinfos=tzinfos) + + def __call__(self, s, **kwargs): + return self._parse_rfc(s, **kwargs) + + +rrulestr = _rrulestr() + +# vim:ts=4:sw=4:et diff --git a/hug/lib/python3.12/site-packages/dateutil/tz/__init__.py b/hug/lib/python3.12/site-packages/dateutil/tz/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..80eee620efe12adea35602e1755fa17eb366146d --- /dev/null +++ b/hug/lib/python3.12/site-packages/dateutil/tz/__init__.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +import sys + +from . import _tzpath +from ._tzpath import reset_tzpath +from .tz import * +from .tz import __doc__ + +__all__ = [ + "tzutc", + "tzoffset", + "tzlocal", + "tzfile", + "tzrange", + "tzstr", + "tzical", + "tzwin", + "tzwinlocal", + "gettz", + "enfold", + "datetime_ambiguous", + "datetime_exists", + "resolve_imaginary", + "available_iana_timezones", + "UTC", + "TZPATH", + "reset_tzpath", + "DeprecatedTzFormatWarning", + "DeprecatedTzKeyWarning", +] + +if sys.version_info < (3, 7): + # Module-level __getattr__ was added in Python 3.7, so instead of lazily + # populating TZPATH on every access, we will register a callback with + # reset_tzpath to update the top-level tuple. + TZPATH = _tzpath.TZPATH + + def _tzpath_callback(new_tzpath): + global TZPATH + TZPATH = new_tzpath + + _tzpath.TZPATH_CALLBACKS.append(_tzpath_callback) + +else: + + def __getattr__(name): + if name == "TZPATH": + return _tzpath.TZPATH + else: + raise AttributeError( + "Module %s has no attribute %s" % (repr(__name__), repr(name)) + ) + + +def __dir__(): + return sorted(list(globals()) + ["TZPATH"]) + + +class DeprecatedTzFormatWarning(Warning): + """Warning raised when time zones are parsed from deprecated formats.""" diff --git a/hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/__init__.cpython-312.pyc b/hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..66634510eb1b1852e41bf27ac674743029a8f26c Binary files /dev/null and b/hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/__init__.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/_common.cpython-312.pyc b/hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/_common.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5b5aefee5bf09db5b002791ded72d3f4033a7243 Binary files /dev/null and b/hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/_common.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/_factories.cpython-312.pyc b/hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/_factories.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..87b53333839a3b79b1f50a72dfc138a63bed5a29 Binary files /dev/null and b/hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/_factories.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/_tzfile.cpython-312.pyc b/hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/_tzfile.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..384173d5874ecfdf35e6fd4d2506c5e0e9a930db Binary files /dev/null and b/hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/_tzfile.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/_tzpath.cpython-312.pyc b/hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/_tzpath.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c097f3ec1c572fa1315d6a20efc1861961c88f74 Binary files /dev/null and b/hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/_tzpath.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/tz.cpython-312.pyc b/hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/tz.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b2337a09b8cc5f74f117e4c534694bb72301691d Binary files /dev/null and b/hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/tz.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/win.cpython-312.pyc b/hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/win.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7afe2be7efc8f72035a8d5457f5f768e893fbd0d Binary files /dev/null and b/hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/win.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/dateutil/tz/_common.py b/hug/lib/python3.12/site-packages/dateutil/tz/_common.py new file mode 100644 index 0000000000000000000000000000000000000000..e6ac11831522b266114d5b68ee1da298e3aeb14a --- /dev/null +++ b/hug/lib/python3.12/site-packages/dateutil/tz/_common.py @@ -0,0 +1,419 @@ +from six import PY2 + +from functools import wraps + +from datetime import datetime, timedelta, tzinfo + + +ZERO = timedelta(0) + +__all__ = ['tzname_in_python2', 'enfold'] + + +def tzname_in_python2(namefunc): + """Change unicode output into bytestrings in Python 2 + + tzname() API changed in Python 3. It used to return bytes, but was changed + to unicode strings + """ + if PY2: + @wraps(namefunc) + def adjust_encoding(*args, **kwargs): + name = namefunc(*args, **kwargs) + if name is not None: + name = name.encode() + + return name + + return adjust_encoding + else: + return namefunc + + +# The following is adapted from Alexander Belopolsky's tz library +# https://github.com/abalkin/tz +if hasattr(datetime, 'fold'): + # This is the pre-python 3.6 fold situation + def enfold(dt, fold=1): + """ + Provides a unified interface for assigning the ``fold`` attribute to + datetimes both before and after the implementation of PEP-495. + + :param fold: + The value for the ``fold`` attribute in the returned datetime. This + should be either 0 or 1. + + :return: + Returns an object for which ``getattr(dt, 'fold', 0)`` returns + ``fold`` for all versions of Python. In versions prior to + Python 3.6, this is a ``_DatetimeWithFold`` object, which is a + subclass of :py:class:`datetime.datetime` with the ``fold`` + attribute added, if ``fold`` is 1. + + .. versionadded:: 2.6.0 + """ + return dt.replace(fold=fold) + +else: + class _DatetimeWithFold(datetime): + """ + This is a class designed to provide a PEP 495-compliant interface for + Python versions before 3.6. It is used only for dates in a fold, so + the ``fold`` attribute is fixed at ``1``. + + .. versionadded:: 2.6.0 + """ + __slots__ = () + + def replace(self, *args, **kwargs): + """ + Return a datetime with the same attributes, except for those + attributes given new values by whichever keyword arguments are + specified. Note that tzinfo=None can be specified to create a naive + datetime from an aware datetime with no conversion of date and time + data. + + This is reimplemented in ``_DatetimeWithFold`` because pypy3 will + return a ``datetime.datetime`` even if ``fold`` is unchanged. + """ + argnames = ( + 'year', 'month', 'day', 'hour', 'minute', 'second', + 'microsecond', 'tzinfo' + ) + + for arg, argname in zip(args, argnames): + if argname in kwargs: + raise TypeError('Duplicate argument: {}'.format(argname)) + + kwargs[argname] = arg + + for argname in argnames: + if argname not in kwargs: + kwargs[argname] = getattr(self, argname) + + dt_class = self.__class__ if kwargs.get('fold', 1) else datetime + + return dt_class(**kwargs) + + @property + def fold(self): + return 1 + + def enfold(dt, fold=1): + """ + Provides a unified interface for assigning the ``fold`` attribute to + datetimes both before and after the implementation of PEP-495. + + :param fold: + The value for the ``fold`` attribute in the returned datetime. This + should be either 0 or 1. + + :return: + Returns an object for which ``getattr(dt, 'fold', 0)`` returns + ``fold`` for all versions of Python. In versions prior to + Python 3.6, this is a ``_DatetimeWithFold`` object, which is a + subclass of :py:class:`datetime.datetime` with the ``fold`` + attribute added, if ``fold`` is 1. + + .. versionadded:: 2.6.0 + """ + if getattr(dt, 'fold', 0) == fold: + return dt + + args = dt.timetuple()[:6] + args += (dt.microsecond, dt.tzinfo) + + if fold: + return _DatetimeWithFold(*args) + else: + return datetime(*args) + + +def _validate_fromutc_inputs(f): + """ + The CPython version of ``fromutc`` checks that the input is a ``datetime`` + object and that ``self`` is attached as its ``tzinfo``. + """ + @wraps(f) + def fromutc(self, dt): + if not isinstance(dt, datetime): + raise TypeError("fromutc() requires a datetime argument") + if dt.tzinfo is not self: + raise ValueError("dt.tzinfo is not self") + + return f(self, dt) + + return fromutc + + +class _tzinfo(tzinfo): + """ + Base class for all ``dateutil`` ``tzinfo`` objects. + """ + + def is_ambiguous(self, dt): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + + + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + + dt = dt.replace(tzinfo=self) + + wall_0 = enfold(dt, fold=0) + wall_1 = enfold(dt, fold=1) + + same_offset = wall_0.utcoffset() == wall_1.utcoffset() + same_dt = wall_0.replace(tzinfo=None) == wall_1.replace(tzinfo=None) + + return same_dt and not same_offset + + def _fold_status(self, dt_utc, dt_wall): + """ + Determine the fold status of a "wall" datetime, given a representation + of the same datetime as a (naive) UTC datetime. This is calculated based + on the assumption that ``dt.utcoffset() - dt.dst()`` is constant for all + datetimes, and that this offset is the actual number of hours separating + ``dt_utc`` and ``dt_wall``. + + :param dt_utc: + Representation of the datetime as UTC + + :param dt_wall: + Representation of the datetime as "wall time". This parameter must + either have a `fold` attribute or have a fold-naive + :class:`datetime.tzinfo` attached, otherwise the calculation may + fail. + """ + if self.is_ambiguous(dt_wall): + delta_wall = dt_wall - dt_utc + _fold = int(delta_wall == (dt_utc.utcoffset() - dt_utc.dst())) + else: + _fold = 0 + + return _fold + + def _fold(self, dt): + return getattr(dt, 'fold', 0) + + def _fromutc(self, dt): + """ + Given a timezone-aware datetime in a given timezone, calculates a + timezone-aware datetime in a new timezone. + + Since this is the one time that we *know* we have an unambiguous + datetime object, we take this opportunity to determine whether the + datetime is ambiguous and in a "fold" state (e.g. if it's the first + occurrence, chronologically, of the ambiguous datetime). + + :param dt: + A timezone-aware :class:`datetime.datetime` object. + """ + + # Re-implement the algorithm from Python's datetime.py + dtoff = dt.utcoffset() + if dtoff is None: + raise ValueError("fromutc() requires a non-None utcoffset() " + "result") + + # The original datetime.py code assumes that `dst()` defaults to + # zero during ambiguous times. PEP 495 inverts this presumption, so + # for pre-PEP 495 versions of python, we need to tweak the algorithm. + dtdst = dt.dst() + if dtdst is None: + raise ValueError("fromutc() requires a non-None dst() result") + delta = dtoff - dtdst + + dt += delta + # Set fold=1 so we can default to being in the fold for + # ambiguous dates. + dtdst = enfold(dt, fold=1).dst() + if dtdst is None: + raise ValueError("fromutc(): dt.dst gave inconsistent " + "results; cannot convert") + return dt + dtdst + + @_validate_fromutc_inputs + def fromutc(self, dt): + """ + Given a timezone-aware datetime in a given timezone, calculates a + timezone-aware datetime in a new timezone. + + Since this is the one time that we *know* we have an unambiguous + datetime object, we take this opportunity to determine whether the + datetime is ambiguous and in a "fold" state (e.g. if it's the first + occurrence, chronologically, of the ambiguous datetime). + + :param dt: + A timezone-aware :class:`datetime.datetime` object. + """ + dt_wall = self._fromutc(dt) + + # Calculate the fold status given the two datetimes. + _fold = self._fold_status(dt, dt_wall) + + # Set the default fold value for ambiguous dates + return enfold(dt_wall, fold=_fold) + + +class tzrangebase(_tzinfo): + """ + This is an abstract base class for time zones represented by an annual + transition into and out of DST. Child classes should implement the following + methods: + + * ``__init__(self, *args, **kwargs)`` + * ``transitions(self, year)`` - this is expected to return a tuple of + datetimes representing the DST on and off transitions in standard + time. + + A fully initialized ``tzrangebase`` subclass should also provide the + following attributes: + * ``hasdst``: Boolean whether or not the zone uses DST. + * ``_dst_offset`` / ``_std_offset``: :class:`datetime.timedelta` objects + representing the respective UTC offsets. + * ``_dst_abbr`` / ``_std_abbr``: Strings representing the timezone short + abbreviations in DST and STD, respectively. + * ``_hasdst``: Whether or not the zone has DST. + + .. versionadded:: 2.6.0 + """ + def __init__(self): + raise NotImplementedError('tzrangebase is an abstract base class') + + def utcoffset(self, dt): + isdst = self._isdst(dt) + + if isdst is None: + return None + elif isdst: + return self._dst_offset + else: + return self._std_offset + + def dst(self, dt): + isdst = self._isdst(dt) + + if isdst is None: + return None + elif isdst: + return self._dst_base_offset + else: + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + if self._isdst(dt): + return self._dst_abbr + else: + return self._std_abbr + + def fromutc(self, dt): + """ Given a datetime in UTC, return local time """ + if not isinstance(dt, datetime): + raise TypeError("fromutc() requires a datetime argument") + + if dt.tzinfo is not self: + raise ValueError("dt.tzinfo is not self") + + # Get transitions - if there are none, fixed offset + transitions = self.transitions(dt.year) + if transitions is None: + return dt + self.utcoffset(dt) + + # Get the transition times in UTC + dston, dstoff = transitions + + dston -= self._std_offset + dstoff -= self._std_offset + + utc_transitions = (dston, dstoff) + dt_utc = dt.replace(tzinfo=None) + + isdst = self._naive_isdst(dt_utc, utc_transitions) + + if isdst: + dt_wall = dt + self._dst_offset + else: + dt_wall = dt + self._std_offset + + _fold = int(not isdst and self.is_ambiguous(dt_wall)) + + return enfold(dt_wall, fold=_fold) + + def is_ambiguous(self, dt): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + + + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + if not self.hasdst: + return False + + start, end = self.transitions(dt.year) + + dt = dt.replace(tzinfo=None) + return (end <= dt < end + self._dst_base_offset) + + def _isdst(self, dt): + if not self.hasdst: + return False + elif dt is None: + return None + + transitions = self.transitions(dt.year) + + if transitions is None: + return False + + dt = dt.replace(tzinfo=None) + + isdst = self._naive_isdst(dt, transitions) + + # Handle ambiguous dates + if not isdst and self.is_ambiguous(dt): + return not self._fold(dt) + else: + return isdst + + def _naive_isdst(self, dt, transitions): + dston, dstoff = transitions + + dt = dt.replace(tzinfo=None) + + if dston < dstoff: + isdst = dston <= dt < dstoff + else: + isdst = not dstoff <= dt < dston + + return isdst + + @property + def _dst_base_offset(self): + return self._dst_offset - self._std_offset + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + return "%s(...)" % self.__class__.__name__ + + __reduce__ = object.__reduce__ diff --git a/hug/lib/python3.12/site-packages/dateutil/tz/_factories.py b/hug/lib/python3.12/site-packages/dateutil/tz/_factories.py new file mode 100644 index 0000000000000000000000000000000000000000..f8a65891a023ebf9eb0c24d391ba67541b7133f1 --- /dev/null +++ b/hug/lib/python3.12/site-packages/dateutil/tz/_factories.py @@ -0,0 +1,80 @@ +from datetime import timedelta +import weakref +from collections import OrderedDict + +from six.moves import _thread + + +class _TzSingleton(type): + def __init__(cls, *args, **kwargs): + cls.__instance = None + super(_TzSingleton, cls).__init__(*args, **kwargs) + + def __call__(cls): + if cls.__instance is None: + cls.__instance = super(_TzSingleton, cls).__call__() + return cls.__instance + + +class _TzFactory(type): + def instance(cls, *args, **kwargs): + """Alternate constructor that returns a fresh instance""" + return type.__call__(cls, *args, **kwargs) + + +class _TzOffsetFactory(_TzFactory): + def __init__(cls, *args, **kwargs): + cls.__instances = weakref.WeakValueDictionary() + cls.__strong_cache = OrderedDict() + cls.__strong_cache_size = 8 + + cls._cache_lock = _thread.allocate_lock() + + def __call__(cls, name, offset): + if isinstance(offset, timedelta): + key = (name, offset.total_seconds()) + else: + key = (name, offset) + + instance = cls.__instances.get(key, None) + if instance is None: + instance = cls.__instances.setdefault(key, + cls.instance(name, offset)) + + # This lock may not be necessary in Python 3. See GH issue #901 + with cls._cache_lock: + cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance) + + # Remove an item if the strong cache is overpopulated + if len(cls.__strong_cache) > cls.__strong_cache_size: + cls.__strong_cache.popitem(last=False) + + return instance + + +class _TzStrFactory(_TzFactory): + def __init__(cls, *args, **kwargs): + cls.__instances = weakref.WeakValueDictionary() + cls.__strong_cache = OrderedDict() + cls.__strong_cache_size = 8 + + cls.__cache_lock = _thread.allocate_lock() + + def __call__(cls, s, posix_offset=False): + key = (s, posix_offset) + instance = cls.__instances.get(key, None) + + if instance is None: + instance = cls.__instances.setdefault(key, + cls.instance(s, posix_offset)) + + # This lock may not be necessary in Python 3. See GH issue #901 + with cls.__cache_lock: + cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance) + + # Remove an item if the strong cache is overpopulated + if len(cls.__strong_cache) > cls.__strong_cache_size: + cls.__strong_cache.popitem(last=False) + + return instance + diff --git a/hug/lib/python3.12/site-packages/dateutil/tz/_tzfile.py b/hug/lib/python3.12/site-packages/dateutil/tz/_tzfile.py new file mode 100644 index 0000000000000000000000000000000000000000..3b015652df82cacd23aa30290d874935514ba90f --- /dev/null +++ b/hug/lib/python3.12/site-packages/dateutil/tz/_tzfile.py @@ -0,0 +1,1428 @@ +# -*- coding: utf-8 -*- +import bisect +import calendar +import functools +import io +import os +import re +import stat +import struct +import sys +from datetime import MAXYEAR, MINYEAR, datetime, timedelta + +import six + +from ._common import _tzinfo, enfold, tzname_in_python2 + +EPOCH = datetime(1970, 1, 1) +EPOCHORDINAL = EPOCH.toordinal() + +# TODO: Move this to _common.py +try: + # Python 3.7 feature + from contextlib import nullcontext as _nullcontext +except ImportError: + import contextlib + + @contextlib.contextmanager + def _nullcontext(context): + yield context + + +# It is relatively expensive to construct new timedelta objects, and in most +# cases we're looking at the same deltas, like integer numbers of hours, etc. +# To improve speed and memory use, we'll keep a dictionary with references +# to the ones we've already used so far. +# +# Loading every time zone in the 2020a version of the time zone database +# requires 447 timedeltas, which requires approximately the amount of space +# that ZoneInfo("America/New_York") with 236 transitions takes up, so we will +# set the cache size to 512 so that in the common case we always get cache +# hits, but specifically crafted ZoneInfo objects don't leak arbitrary amounts +# of memory. +_TIMEDELTA_CACHE_SIZE = 512 +if six.PY2: + + def _cache_load_timedelta(f): + from collections import OrderedDict + + from six.moves import _thread + + CACHE_LOCK = _thread.allocate_lock() + TIMEDELTA_CACHE = OrderedDict() + + @functools.wraps(f) + def cached_load_timedelta(seconds): + # OrderedDict is implemented in Python on Python 2, so every + # access has to hold the lock to keep its linked list intact. + with CACHE_LOCK: + rv = TIMEDELTA_CACHE.pop(seconds, None) + if rv is None: + rv = f(seconds) + + TIMEDELTA_CACHE[seconds] = rv + + if len(TIMEDELTA_CACHE) > _TIMEDELTA_CACHE_SIZE: + TIMEDELTA_CACHE.popitem(last=False) + + return rv + + return cached_load_timedelta + +else: + _cache_load_timedelta = functools.lru_cache(maxsize=512) + + +@_cache_load_timedelta +def _load_timedelta(seconds): + return timedelta(seconds=seconds) + + +def _unpickle(cls, key, filename, data): + obj = cls.__new__(cls) + obj._key = key + obj._filename = filename + + obj._load_from_data(data) + + return obj + + +class tzfile(_tzinfo): + """ + This is a ``tzinfo`` subclass that allows one to use the ``tzfile(5)`` + format timezone files to extract current and historical zone information. + + :param fileobj: + This can be an opened file stream or a file name that the time zone + information can be read from. + + :param filename: + This is an optional parameter specifying the source of the time zone + information in the event that ``fileobj`` is a file object. If omitted + and ``fileobj`` is a file stream, this parameter will be set either to + ``fileobj``'s ``name`` attribute or to ``repr(fileobj)``. + + This class is intended to be mostly equivalent to the standard library + :py:class:`zoneinfo.ZoneInfo` class. The primary differences are: + + * All caching in ``tzfile`` is handled by :func:`tz.gettz` level (you + should generally not attempt to construct a ``tzfile`` directly). + * ``tzfile`` objects are pickled by value rather than by reference to + the IANA key. + * ``tzfile`` has equality-by-value semantics, whereas + :py:class:`~zoneinfo.ZoneInfo` has equality-by-identity semantics. + + See `Sources for Time Zone and Daylight Saving Time Data + `_ for more information. + Time zone files can be compiled from the `IANA Time Zone database files + `_ with the `zic time zone compiler + `_ + + .. note:: + + Only construct a ``tzfile`` directly if you have a specific timezone + file on disk that you want to read into a Python ``tzinfo`` object. + If you want to get a ``tzfile`` representing a specific IANA zone, + (e.g. ``'America/New_York'``), you should call + :func:`dateutil.tz.gettz` with the zone identifier. + + .. versionchanged:: 3.0.0 + ``tzfile`` is now a partial backport of :py:class:`zoneinfo.ZoneInfo`, + which brings with it support for Version 2 and 3 ``tzfile(5)`` binaries. + + **Examples:** + + Using the US Eastern time zone as an example, we can see that a ``tzfile`` + provides time zone information for the standard Daylight Saving offsets: + + .. testsetup:: tzfile + + from dateutil.tz import gettz + from datetime import datetime + + .. doctest:: tzfile + + >>> NYC = gettz('America/New_York') + >>> NYC + tzfile('/usr/share/zoneinfo/America/New_York') + + >>> print(datetime(2016, 1, 3, tzinfo=NYC)) # EST + 2016-01-03 00:00:00-05:00 + + >>> print(datetime(2016, 7, 7, tzinfo=NYC)) # EDT + 2016-07-07 00:00:00-04:00 + + + The ``tzfile`` structure contains a fully history of the time zone, + so historical dates will also have the right offsets. For example, before + the adoption of standard time on November 18, 1883, New York used local + mean time: + + .. doctest:: tzfile + + >>> print(datetime(1883, 11, 18, tzinfo=NYC)) # LMT + 1883-11-18 00:00:00-04:56:02 + + Python versions before 3.6 do not support sub-minute offsets, so there the + offset is rounded to ``-04:56``. + + And during World War II, New York was on "Eastern War Time", which was a + state of permanent daylight saving time: + + .. doctest:: tzfile + + >>> print(datetime(1944, 2, 7, tzinfo=NYC)) # EWT + 1944-02-07 00:00:00-04:00 + + """ + + def __init__(self, fileobj, filename=None, key=None): + super(tzfile, self).__init__() + + file_opened_here = False + if isinstance(fileobj, six.string_types): + self._filename = fileobj + fileobj = open(fileobj, "rb") + file_opened_here = True + elif filename is not None: + self._filename = filename + elif hasattr(fileobj, "name"): + self._filename = fileobj.name + elif key is not None: + self._filename = key + else: + self._filename = repr(fileobj) + + if fileobj is not None: + if not file_opened_here: + fileobj = _nullcontext(fileobj) + + with fileobj as file_stream: + self._load_file(file_stream) + + self._key = key + + @property + def key(self): + """ + Read-only attribute returning the ``key`` constructor argument. + + For zones generated from :func:`gettz`, this will be the IANA key + passed to ``gettz`` — no normalization is done, so although in most + distributions of the time zone database "US/Eastern" is a link to + "America/New_York", ``tz.gettz("US/Eastern").key`` should always return + ``"US/Eastern"``. + + For zones constructed via another mechanism without specifying ``key``, + this returns :obj:`None`. + """ + return self._key + + def is_ambiguous(self, dt): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + + + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + return self._is_ambiguous_or_imaginary(dt) == 1 + + def _is_ambiguous_or_imaginary(self, dt): + if dt is None or self._fixed_offset: + return 0 + + timestamp = self._get_local_timestamp(dt) + tti_0 = self._find_trans(dt, fold=0, timestamp=timestamp) + tti_1 = self._find_trans(dt, fold=1, timestamp=timestamp) + + # Returns 1 if ambiguous, -1 if imaginary and 0 otherwise. + return int(tti_0.utcoff > tti_1.utcoff) - int( + tti_1.utcoff > tti_0.utcoff + ) + + def utcoffset(self, dt): + return self._find_trans(dt).utcoff + + def dst(self, dt): + return self._find_trans(dt).dstoff + + @tzname_in_python2 + def tzname(self, dt): + return self._find_trans(dt).tzname + + def fromutc(self, dt): + """ + The ``tzfile`` implementation of :py:func:`datetime.tzinfo.fromutc`. + + :param dt: + A :py:class:`datetime.datetime` object. + + :raises TypeError: + Raised if ``dt`` is not a :py:class:`datetime.datetime` object. + + :raises ValueError: + Raised if this is called with a ``dt`` which does not have this + ``tzinfo`` attached. + + :return: + Returns a :py:class:`datetime.datetime` object representing the + wall time in ``self``'s time zone. + """ + + if not isinstance(dt, datetime): + raise TypeError("fromutc() requires a datetime argument") + if dt.tzinfo is not self: + raise ValueError("dt.tzinfo is not self") + + timestamp = self._get_local_timestamp(dt) + num_trans = len(self._trans_utc) + + if num_trans >= 1 and timestamp < self._trans_utc[0]: + tti = self._tti_before + fold = 0 + elif ( + num_trans == 0 or timestamp > self._trans_utc[-1] + ) and not isinstance(self._tz_after, _ttinfo): + tti, fold = self._tz_after.get_trans_info_fromutc( + timestamp, dt.year + ) + + # The TZ string only knows about its own transitions, so check + # whether we are in a fold created by the last explicit one. + if num_trans and not fold: + if num_trans > 1: + tti_prev = self._ttinfos[-2] + else: + tti_prev = self._tti_before + + shift = tti_prev.utcoff - tti.utcoff + fold = shift.total_seconds() > timestamp - self._trans_utc[-1] + elif num_trans == 0: + tti = self._tz_after + fold = 0 + else: + idx = bisect.bisect_right(self._trans_utc, timestamp) + + if num_trans > 1 and timestamp >= self._trans_utc[1]: + tti_prev, tti = self._ttinfos[idx - 2 : idx] + elif timestamp > self._trans_utc[-1]: + # Only reachable with exactly one transition, so the + # offset before it is _tti_before, not _ttinfos[-1]. + tti_prev = self._tti_before + tti = self._tz_after + else: + tti_prev = self._tti_before + tti = self._ttinfos[0] + + # Detect fold + shift = tti_prev.utcoff - tti.utcoff + fold = shift.total_seconds() > timestamp - self._trans_utc[idx - 1] + dt += tti.utcoff + if fold: + return enfold(dt, fold=1) + else: + return dt + + def _find_trans(self, dt, fold=None, timestamp=None): + if dt is None: + if self._fixed_offset: + return self._tz_after + else: + return _NO_TTINFO + + if timestamp is None: + ts = self._get_local_timestamp(dt) + else: + ts = timestamp + + if fold is None: + fold = getattr(dt, "fold", 0) + + lt = self._trans_local[fold] + + num_trans = len(lt) + + if num_trans and ts < lt[0]: + return self._tti_before + elif not num_trans or ts > lt[-1]: + if isinstance(self._tz_after, _TZStr): + tti = self._tz_after.get_trans_info(ts, dt.year, fold) + + # The TZ string only describes times after the last explicit + # transition. If its answer for this local time is a different + # offset from the last transition's, and corresponds to a time + # before that transition (e.g. when the TZ string's own DST + # period for that year ends after the transition), the offset + # from the last explicit transition applies instead. + last_tti = self._ttinfos[-1] if num_trans else None + if ( + last_tti is not None + and tti.utcoff != last_tti.utcoff + and ts - tti.utcoff.total_seconds() < self._trans_utc[-1] + ): + return last_tti + + return tti + else: + return self._tz_after + else: + # idx is the transition that occurs after this timestamp, so we + # subtract off 1 to get the current ttinfo + idx = bisect.bisect_right(lt, ts) - 1 + assert idx >= 0 + return self._ttinfos[idx] + + def _get_local_timestamp(self, dt): + return ( + (dt.toordinal() - EPOCHORDINAL) * 86400 + + dt.hour * 3600 + + dt.minute * 60 + + dt.second + ) + + # The attributes set by _unpickle and _load_from_data + _UNPICKLE_ATTRS = frozenset( + [ + "_filename", + "_fixed_offset", + "_isdst", + "_key", + "_trans_idx", + "_trans_local", + "_trans_utc", + "_tti_before", + "_ttinfo_list", + "_ttinfos", + "_tz_after", + "_tz_str", + "_utcoff_raw", + ] + ) + + def __reduce__(self): + return self.__reduce_ex__(None) + + def __reduce_ex__(self, protocol): + utcoff = [ + int(ttinfo.utcoff.total_seconds()) for ttinfo in self._ttinfo_list + ] + utcoff = getattr(self, "_utcoff_raw", utcoff) + abbr = [ttinfo.tzname for ttinfo in self._ttinfo_list] + + # dst() is inferred per transition, so the transitions don't map back + # to the local time types, and these are kept as they were in the file + trans_idx = self._trans_idx + isdst = self._isdst + + trans_utc = self._trans_utc + tz_str = self._tz_str + + data = (trans_idx, trans_utc, utcoff, isdst, abbr, tz_str) + args = (self.__class__, self._key, self._filename, data) + + # Preserve any other instance state, such as attributes added by a + # subclass, but not the attributes that _unpickle rebuilds from data + state = { + k: v for k, v in vars(self).items() if k not in self._UNPICKLE_ATTRS + } + if state: + return (_unpickle, args, state) + + return (_unpickle, args) + + def _load_file(self, fobj): + # Reading the file requires seeking past the parts we don't use and + # reading the footer a line at a time, so read anything that doesn't + # support that (e.g. a pipe) into memory first; TZif files are small. + if not _is_seekable(fobj) or not hasattr(fobj, "readline"): + fobj = io.BytesIO(fobj.read()) + + # Retrieve all the data as it exists in the zoneinfo file + data = load_data(fobj) + self._load_from_data(data) + + def _load_from_data(self, data): + trans_idx, trans_utc, utcoff, isdst, abbr, tz_str = data + + # Infer the DST offsets (needed for .dst()) from the data, for each + # transition and for each local time type + trans_dstoff = self._utcoff_to_dstoff( + trans_idx, trans_utc, utcoff, isdst + ) + dstoff = [3600 if dst else 0 for dst in isdst] + for idx, trans_off in zip(trans_idx, trans_dstoff): + dstoff[idx] = trans_off + + if sys.version_info < (3, 6): + # For python pre-3.6, round to full-minutes if that's not the case. + # Python's datetime doesn't accept sub-minute timezones. Check + # http://python.org/sf/1447945 or https://bugs.python.org/issue5288 + # for some information. + utcoff_adjusted = [60 * ((s + 30) // 60) for s in utcoff] + dstoff = [60 * ((s + 30) // 60) for s in dstoff] + trans_dstoff = [60 * ((s + 30) // 60) for s in trans_dstoff] + self._utcoff_raw = utcoff + else: + utcoff_adjusted = utcoff + + # Convert all the transition times (UTC) into "seconds since 1970-01-01 local time" + trans_local = self._ts_to_local(trans_idx, trans_utc, utcoff_adjusted) + + # Construct `_ttinfo` objects for each transition in the file + _ttinfo_list = [ + _ttinfo( + _load_timedelta(utcoffset), _load_timedelta(dstoffset), tzname + ) + for utcoffset, dstoffset, tzname in zip( + utcoff_adjusted, dstoff, abbr + ) + ] + + self._ttinfo_list = _ttinfo_list + + self._trans_idx = trans_idx + self._isdst = isdst + self._trans_utc = trans_utc + self._trans_local = trans_local + + # Transitions to the same local time type usually have the same DST + # offset, so share the _ttinfo objects between them where possible. + ttinfos = {} + self._ttinfos = [] + for idx, trans_off in zip(trans_idx, trans_dstoff): + tti = _ttinfo_list[idx] + if tti.dstoff.total_seconds() != trans_off: + key = (idx, trans_off) + if key not in ttinfos: + ttinfos[key] = _ttinfo( + tti.utcoff, _load_timedelta(trans_off), tti.tzname + ) + tti = ttinfos[key] + self._ttinfos.append(tti) + + # Find the first non-DST transition + for i in range(len(isdst)): + if not isdst[i]: + self._tti_before = _ttinfo_list[i] + break + else: + if self._ttinfos: + self._tti_before = self._ttinfos[0] + else: + self._tti_before = None + + # Set the "fallback" time zone + self._tz_str = tz_str + if tz_str is not None and tz_str != b"": + self._tz_after = _parse_tz_str(tz_str.decode()) + else: + if not self._ttinfos and not _ttinfo_list: + raise ValueError("No time zone information found.") + + if self._ttinfos: + self._tz_after = self._ttinfos[-1] + else: + # With no transitions and no TZ string, RFC 9636 Section 3.2 + # says that local time is specified by time type 0 + self._tz_after = _ttinfo_list[0] + + # Determine if this is a "fixed offset" zone, meaning that the output + # of the utcoffset, dst and tzname functions does not depend on the + # specific datetime passed. + # + # We make three simplifying assumptions here: + # + # 1. If _tz_after is not a _ttinfo, it has transitions that might + # actually occur (it is possible to construct TZ strings that + # specify STD and DST but no transitions ever occur, such as + # AAA0BBB,0/0,J365/25). + # 2. If _ttinfo_list contains more than one _ttinfo object, the objects + # represent different offsets. + # 3. _ttinfo_list contains no unused _ttinfos (in which case an + # otherwise fixed-offset zone with extra _ttinfos defined may + # appear to *not* be a fixed offset zone). + # + # Violations to these assumptions would be fairly exotic, and exotic + # zones should almost certainly not be used with datetime.time (the + # only thing that would be affected by this). + if len(_ttinfo_list) > 1 or not isinstance(self._tz_after, _ttinfo): + self._fixed_offset = False + elif not _ttinfo_list: + self._fixed_offset = True + else: + self._fixed_offset = _ttinfo_list[0] == self._tz_after + + @staticmethod + def _utcoff_to_dstoff(trans_idx, trans_utc, utcoffsets, isdsts): + """Infer the DST offset (the value of dst()) for each transition. + + TZif files give the total UTC offset of each local time type and + whether it is DST, but not how much of the offset is DST, so this has + to be worked out from the standard time on either side of each DST + period. That is done for each transition rather than for each local + time type, because zic reuses a local time type for periods with + different standard offsets (e.g. UTC+0 DST in the Azores was double + summer time in the 1940s and summer time later). + """ + n = len(trans_idx) + + # The DST offset implied by the last standard time before and the + # first standard time after each transition. Before the first + # transition, it's the first standard time type, as for _tti_before. + first_std_utcoff = next( + (utcoff for utcoff, dst in zip(utcoffsets, isdsts) if not dst), + None, + ) + before = [None] * n + after = [None] * n + for indices, implied, std_utcoff in ( + (range(n), before, first_std_utcoff), + (range(n - 1, -1, -1), after, None), + ): + for i in indices: + idx = trans_idx[i] + if not isdsts[idx]: + std_utcoff = utcoffsets[idx] + elif std_utcoff is not None: + implied[i] = utcoffsets[idx] - std_utcoff + + # DST offsets where the standard time is the same on both sides + clear_cut = [ + (trans_utc[i], before[i]) + for i in range(n) + if before[i] and before[i] == after[i] + ] + + dstoffs = [0] * n + for i in range(n): + if not isdsts[trans_idx[i]]: + continue + + candidates = {c for c in (before[i], after[i]) if c} + if len(candidates) > 1: + # The standard offset changed during the DST period. A change + # from local mean time can give something like 0:34:39, which + # is not a plausible amount of DST. + plausible = {c for c in candidates if c % 300 == 0} + candidates = plausible or candidates + + if len(candidates) > 1: + # Prefer the amount of DST the zone used around the same + # time, e.g. Tehran used 1:00 and Rarotonga 0:30, though the + # TZif data for Tehran in 1977 and for Rarotonga in 1978 look + # the same. This has to be local in time, because some zones + # used different amounts in different eras (e.g. Montevideo + # used 0:30 in the 1920s and 1:00 later). + nearby = { + dstoff + for ts, dstoff in clear_cut + if abs(ts - trans_utc[i]) <= _DST_CONSENSUS_WINDOW + } + consensus = candidates & nearby + if len(consensus) == 1: + candidates = consensus + + if candidates: + # Otherwise, prefer positive DST, then the smaller amount. In + # the 1942 occupation zones, e.g. Europe/Kyiv, the standard + # time before gives -1:00 and the one after +1:00. + dstoffs[i] = min(candidates, key=lambda c: (c < 0, abs(c))) + else: + # Without any standard time to compare to, one hour is a much + # better guess than 0 + dstoffs[i] = 3600 + + return dstoffs + + @staticmethod + def _ts_to_local(trans_idx, trans_list_utc, utcoffsets): + """Generate number of seconds since 1970 *in the local time*. + + This is necessary to easily find the transition times in local time""" + if not trans_list_utc: + return [[], []] + + # Start with the timestamps and modify in-place + trans_list_wall = [list(trans_list_utc), list(trans_list_utc)] + + if len(utcoffsets) > 1: + offset_0 = utcoffsets[0] + offset_1 = utcoffsets[trans_idx[0]] + if offset_1 > offset_0: + offset_1, offset_0 = offset_0, offset_1 + else: + offset_0 = offset_1 = utcoffsets[0] + + trans_list_wall[0][0] += offset_0 + trans_list_wall[1][0] += offset_1 + + for i in range(1, len(trans_idx)): + offset_0 = utcoffsets[trans_idx[i - 1]] + offset_1 = utcoffsets[trans_idx[i]] + + if offset_1 > offset_0: + offset_1, offset_0 = offset_0, offset_1 + + trans_list_wall[0][i] += offset_0 + trans_list_wall[1][i] += offset_1 + + return trans_list_wall + + def __eq__(self, other): + if not isinstance(other, tzfile): + return NotImplemented + + return ( + self._trans_utc == other._trans_utc + and self._trans_local == other._trans_local + and self._ttinfos == other._ttinfos + and self._tti_before == other._tti_before + and self._fallback_key() == other._fallback_key() + ) + + def _fallback_key(self): + # The behavior after the last transition is determined either by the + # TZ string or, in its absence, by a single _ttinfo; _TZStr objects + # do not define equality, so compare the string they came from. + if isinstance(self._tz_after, _ttinfo): + return self._tz_after + return self._tz_str + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, repr(self._filename)) + + +# Before Python 3.16, pipes on Windows claim to be seekable, but seeking on +# them silently does nothing. See https://github.com/python/cpython/issues/86768 +_SEEKABLE_PIPES_BUG = sys.platform == "win32" and sys.version_info < (3, 16) + + +def _is_seekable(fobj): + if not hasattr(fobj, "seek"): + return False + + seekable = getattr(fobj, "seekable", None) + # Python 2 file objects can seek but have no seekable() method + if seekable is not None and not seekable(): + return False + + if not _SEEKABLE_PIPES_BUG: + return True + + # For anything backed by a file descriptor, only trust regular files. + try: + fd = fobj.fileno() + except (AttributeError, OSError, ValueError): + # io.UnsupportedOperation (e.g. from BytesIO) is both of the latter + return True + + try: + return stat.S_ISREG(os.fstat(fd).st_mode) + except OSError: + return True + + +def load_data(fobj): + try: + return _load_data(fobj) + except struct.error as e: + # struct.unpack fails when a read comes up short + six.raise_from( + ValueError("Invalid TZif file: unexpected end of file"), e + ) + + +def _load_data(fobj): + header = _TZifHeader.from_file(fobj) + + if header.version == 1: + time_size = 4 + time_type = "l" + else: + # Version 2+ has 64-bit integer transition times + time_size = 8 + time_type = "q" + + # Version 2+ also starts with a Version 1 header and data, which + # we need to skip now + skip_bytes = ( + header.timecnt * 5 # Transition times and types + + header.typecnt * 6 # Local time type records + + header.charcnt # Time zone designations + + header.leapcnt * 8 # Leap second records + + header.isstdcnt # Standard/wall indicators + + header.isutcnt # UT/local indicators + ) + + fobj.seek(skip_bytes, 1) + + # Now we need to read the second header, which is not the same + # as the first + header = _TZifHeader.from_file(fobj) + + typecnt = header.typecnt + timecnt = header.timecnt + charcnt = header.charcnt + + # The data portion starts with timecnt transitions and indices + if timecnt: + trans_list_utc = struct.unpack( + ">%s%s" % (timecnt, time_type), fobj.read(timecnt * time_size) + ) + trans_idx = struct.unpack(">%sB" % timecnt, fobj.read(timecnt)) + + if max(trans_idx) >= typecnt: + raise ValueError( + "Invalid transition index found while reading TZif: %s" + % max(trans_idx) + ) + else: + trans_list_utc = () + trans_idx = () + + # Read the ttinfo struct, (utoff, isdst, abbrind) + if typecnt: + utcoff, isdst, abbrind = zip( + *(struct.unpack(">lbb", fobj.read(6)) for i in range(typecnt)) + ) + else: + utcoff = () + isdst = () + abbrind = () + + # Now read the abbreviations. They are null-terminated strings, indexed + # not by position in the array but by position in the unsplit + # abbreviation string. I suppose this makes more sense in C, which uses + # null to terminate the strings, but it's inconvenient here... + abbr_vals = {} + abbr_chars = fobj.read(charcnt) + + def get_abbr(idx): + # Gets a string starting at idx and running until the next \x00 + # + # We cannot pre-populate abbr_vals by splitting on \x00 because there + # are some zones that use subsets of longer abbreviations, like so: + # + # LMT\x00AHST\x00HDT\x00 + # + # Where the idx to abbr mapping should be: + # + # {0: "LMT", 4: "AHST", 5: "HST", 9: "HDT"} + if idx not in abbr_vals: + span_end = abbr_chars.find(b"\x00", idx) + abbr_vals[idx] = abbr_chars[idx:span_end].decode() + + return abbr_vals[idx] + + abbr = tuple(get_abbr(idx) for idx in abbrind) + + # The remainder of the file consists of leap seconds (currently unused) and + # the standard/wall and ut/local indicators, which are metadata we don't need. + # In version 2 files, we need to skip the unnecessary data to get at the TZ string, + # and in version 1 files we'll just hopefully consume the rest of the file. + + # Each leap second record has size (time_size + 4) + skip_bytes = ( + header.isutcnt + header.isstdcnt + header.leapcnt * (time_size + 4) + ) + fobj.seek(skip_bytes, 1) + if header.version >= 2: + c = fobj.read(1) # Should be \n + if c != b"\n": + raise ValueError("Invalid TZif file: expected newline, got %r" % c) + + line = fobj.readline() + if not line.endswith(b"\n"): + raise ValueError("Invalid TZif file: unexpected end of file") + tz_str = line.rstrip(b"\n") + else: + tz_str = None + + return trans_idx, trans_list_utc, utcoff, isdst, abbr, tz_str + + +# How close in time (in seconds) the DST periods that _utcoff_to_dstoff uses +# to choose between two possible DST offsets have to be +_DST_CONSENSUS_WINDOW = 10 * 365 * 86400 + + +class _TZifHeader(object): + __slots__ = [ + "version", + "isutcnt", + "isstdcnt", + "leapcnt", + "timecnt", + "typecnt", + "charcnt", + ] + + def __init__(self, *args): + assert len(self.__slots__) == len(args) + for attr, val in zip(self.__slots__, args): + setattr(self, attr, val) + + @classmethod + def from_file(cls, stream): + # The header starts with a 4-byte "magic" value + if stream.read(4) != b"TZif": + raise ValueError("Invalid TZif file: magic not found") + + _version = stream.read(1) + if _version == b"\x00": + version = 1 + else: + version = int(_version) + stream.read(15) + + args = (version,) + + # Slots are defined in the order that the bytes are arranged + args = args + struct.unpack(">6l", stream.read(24)) + + return cls(*args) + + +class _ttinfo(object): + __slots__ = ["utcoff", "dstoff", "tzname"] + + def __init__(self, utcoff, dstoff, tzname): + self.utcoff = utcoff + self.dstoff = dstoff + self.tzname = tzname + + def __eq__(self, other): + if not isinstance(other, _ttinfo): + return NotImplemented + + return ( + self.utcoff == other.utcoff + and self.dstoff == other.dstoff + and self.tzname == other.tzname + ) + + def __ne__(self, other): + # Python 2 does not derive __ne__ from __eq__ + eq = self.__eq__(other) + if eq is NotImplemented: + return eq + return not eq + + def __repr__(self): # pragma: nocover + return "%s(%s, %s, %s)" % ( + self.__class__.__name__, + self.utcoff, + self.dstoff, + self.tzname, + ) + + +_NO_TTINFO = _ttinfo(None, None, None) + + +def _unpack(t): + # Python 2-specific backport for x, *y = t + return t[0], t[1:] + + +# re.ASCII does not exist on Python 2, where str patterns are ASCII-only +# anyway (and unicode patterns are too unless re.UNICODE is passed). +_RE_ASCII = getattr(re, "ASCII", 0) + + +def _fullmatch(pattern, string, flags=0): + # Python 2-specific backport of re.fullmatch + return re.match(r"(?:%s)\Z" % pattern, string, flags) + + +def _round_offset(seconds): + # Python < 3.6 does not support sub-minute offsets, see the comment in + # tzfile._load_from_data. + if sys.version_info < (3, 6): + return 60 * ((seconds + 30) // 60) + return seconds + + +def _parse_tz_str(tz_str): + # The tz string has the format: + # + # std[offset[dst[offset],start[/time],end[/time]]] + # + # std and dst must be 3 or more characters long and must not contain + # a leading colon, embedded digits, commas, nor a plus or minus signs; + # The spaces between "std" and "offset" are only for display and are + # not actually present in the string. + # + # The format of the offset is ``[+|-]hh[:mm[:ss]]`` + + # TODO: When we drop Python 2, this should be offset_str, *start_end_str = ... + offset_str, start_end_str = _unpack(tz_str.split(",", 1)) + + parser_re = ( + r"(?P[a-zA-Z]+|<[a-zA-Z0-9+-]+>)" + r"(?:" + r"(?P[+-]?\d{1,3}(?::\d{2}(?::\d{2})?)?)" + r"(?:" + r"(?P[a-zA-Z]+|<[a-zA-Z0-9+-]+>)" + r"(?P[+-]?\d{1,3}(?::\d{2}(?::\d{2})?)?)?" + r")?" # dst + r")?" # stdoff + ) + + m = _fullmatch(parser_re, offset_str, _RE_ASCII) + + if m is None: + raise ValueError("%s is not a valid TZ string" % tz_str) + + std_abbr = m.group("std") + dst_abbr = m.group("dst") + dst_offset = None + + std_abbr = std_abbr.strip("<>") + + if dst_abbr: + dst_abbr = dst_abbr.strip("<>") + + std_offset = m.group("stdoff") + if std_offset: + try: + std_offset = _round_offset(_parse_tz_delta(std_offset)) + except ValueError as e: + raise ValueError("Invalid STD offset in %s" % tz_str) + else: + # The STD offset is required + raise ValueError("Invalid STD offset in %s" % tz_str) + + if dst_abbr is not None: + dst_offset = m.group("dstoff") + if dst_offset: + try: + dst_offset = _round_offset(_parse_tz_delta(dst_offset)) + except ValueError as e: + raise ValueError("Invalid DST offset in %s" % tz_str) + else: + dst_offset = std_offset + 3600 + + if not start_end_str: + raise ValueError("Missing transition rules: %s" % tz_str) + + start_end_strs = start_end_str[0].split(",", 1) + try: + start, end = (_parse_dst_start_end(x) for x in start_end_strs) + except ValueError as e: + raise ValueError("Invalid TZ string: %s" % tz_str) + + return _TZStr(std_abbr, std_offset, dst_abbr, dst_offset, start, end) + elif start_end_str: + raise ValueError("Transition rule present without DST: %s" % tz_str) + else: + # This is a static ttinfo, don't return _TZStr + return _ttinfo( + _load_timedelta(std_offset), _load_timedelta(0), std_abbr + ) + + +def _parse_dst_start_end(dststr): + date, time = _unpack(dststr.split("/", 1)) + date_type = date[:1] + if date_type == "M": + n_is_julian = False + m = _fullmatch(r"M(\d{1,2})\.(\d)\.(\d)", date, _RE_ASCII) + if m is None: + raise ValueError("Invalid dst start/end date: %s" % dststr) + date_offset = tuple(map(int, m.groups())) + offset = _CalendarOffset(*date_offset) + else: + if date_type == "J": + n_is_julian = True + date = date[1:] + else: + n_is_julian = False + + if _fullmatch(r"\d{1,3}", date, _RE_ASCII) is None: + raise ValueError("Invalid dst start/end date: %s" % dststr) + doy = int(date) + offset = _DayOffset(doy, n_is_julian) + + if time: + offset.hour, offset.minute, offset.second = _parse_transition_time( + time[0] + ) + + return offset + + +def _parse_transition_time(time_str): + match = _fullmatch( + r"(?P[+-])?(?P\d{1,3})(:(?P\d{2})(:(?P\d{2}))?)?", + time_str, + _RE_ASCII, + ) + if match is None: + raise ValueError("Invalid time: %s" % time_str) + + h, m, s = (int(v or 0) for v in map(match.group, ("h", "m", "s"))) + + if h > 167: + raise ValueError("Hour must be in [0, 167]: %s" % time_str) + + if match.group("sign") == "-": + h, m, s = -h, -m, -s + + return h, m, s + + +def _parse_tz_delta(tz_delta): + match = _fullmatch( + r"(?P[+-])?(?P\d{1,3})(:(?P\d{2})(:(?P\d{2}))?)?", + tz_delta, + _RE_ASCII, + ) + # Anything passed to this function should already have hit an equivalent + # regular expression to find the section to parse. + assert match is not None, tz_delta + + h, m, s = (int(v or 0) for v in map(match.group, ("h", "m", "s"))) + + total = h * 3600 + m * 60 + s + + if h > 24: + raise ValueError("Offset hours must be in [0, 24]: %s" % tz_delta) + + # Yes, +5 maps to an offset of -5h + if match.group("sign") != "-": + total *= -1 + + return total + + +class _TZStr(object): + __slots__ = ( + "std", + "dst", + "start", + "end", + "get_trans_info", + "get_trans_info_fromutc", + "dst_diff", + ) + + def __init__( + self, std_abbr, std_offset, dst_abbr, dst_offset, start=None, end=None + ): + self.dst_diff = dst_offset - std_offset + std_offset = _load_timedelta(std_offset) + self.std = _ttinfo( + utcoff=std_offset, dstoff=_load_timedelta(0), tzname=std_abbr + ) + + self.start = start + self.end = end + + dst_offset = _load_timedelta(dst_offset) + delta = _load_timedelta(self.dst_diff) + self.dst = _ttinfo(utcoff=dst_offset, dstoff=delta, tzname=dst_abbr) + + # These are assertions because the constructor should only be called + # by functions that would fail before passing start or end + assert start is not None, "No transition start specified" + assert end is not None, "No transition end specified" + + self.get_trans_info = self._get_trans_info + self.get_trans_info_fromutc = self._get_trans_info_fromutc + + def transitions(self, year): + start = self.start.year_to_epoch(year) + end = self.end.year_to_epoch(year) + return start, end + + def _near_new_year(self, ts, year): + """Whether ts is close enough to the new year that the rules for the + previous or next year might matter.""" + year_start = _post_epoch_days_before_year(year) * 86400 + year_end = _post_epoch_days_before_year(year + 1) * 86400 + return ( + ts < year_start + _NEW_YEAR_MARGIN + or ts >= year_end - _NEW_YEAR_MARGIN + ) + + def _get_trans_info(self, ts, year, fold): + """Get the information about the current transition - tti""" + if self._near_new_year(ts, year): + return self._get_trans_info_near_new_year(ts, fold) + + start, end = self.transitions(year) + + # With fold = 0, the period (denominated in local time) with the + # smaller offset starts at the end of the gap and ends at the end of + # the fold; with fold = 1, it runs from the start of the gap to the + # beginning of the fold. + # + # So in order to determine the DST boundaries we need to know both + # the fold and whether DST is positive or negative (rare), and it + # turns out that this boils down to fold XOR is_positive. + if fold == (self.dst_diff >= 0): + end -= self.dst_diff + else: + start += self.dst_diff + + if start < end: + isdst = start <= ts < end + else: + isdst = not (end <= ts < start) + + return self.dst if isdst else self.std + + def _get_trans_info_near_new_year(self, ts, fold): + # Answer the question in terms of UTC, where the transitions from the + # adjacent years are taken into account: a local time is valid for + # an offset if the rules give that offset at the corresponding UTC + # time. + valid = [] + for tti in (self.std, self.dst): + utc_ts = ts - tti.utcoff.total_seconds() + try: + utc_year = (EPOCH + timedelta(seconds=utc_ts)).year + except OverflowError: + # Before year 1 or after year 9999 + utc_year = MINYEAR if utc_ts < 0 else MAXYEAR + actual, _ = self._get_trans_info_fromutc_near_new_year( + utc_ts, utc_year + ) + if actual is tti: + valid.append(tti) + + if len(valid) == 1: + return valid[0] + + smaller, larger = sorted((self.std, self.dst), key=lambda t: t.utcoff) + if valid: + # Ambiguous: fold=0 is the first occurrence, with the larger offset + return larger if fold == 0 else smaller + else: + # Imaginary: fold=0 uses the offset from before the transition + return smaller if fold == 0 else larger + + def _get_trans_info_fromutc_near_new_year(self, ts, year): + # The rules are in terms of the local year, which near the new year + # is not always the UTC year, and a rule can put a transition up to a + # week into the previous or next year (e.g. "J365/25"), so look at the + # transitions for the years on either side as well and use the last + # one before ts. + std_utcoff = self.std.utcoff.total_seconds() + dst_utcoff = self.dst.utcoff.total_seconds() + + transitions = [] + for y in (year - 1, year, year + 1): + start, end = self.transitions(y) + transitions.append((start - std_utcoff, True)) + transitions.append((end - dst_utcoff, False)) + + # When one DST period ends at the same instant as the next one starts, + # as with "DST all year" rules like "0/0,J365/25", sorting puts the + # end first, so these stay in DST without a transition in between. + transitions.sort() + + isdst = not transitions[0][1] + last_change = None + i = 0 + while i < len(transitions) and transitions[i][0] <= ts: + trans_ts = transitions[i][0] + new_isdst = isdst + while i < len(transitions) and transitions[i][0] == trans_ts: + new_isdst = transitions[i][1] + i += 1 + + if new_isdst != isdst: + isdst = new_isdst + last_change = trans_ts + + if isdst: + tti, tti_prev = self.dst, self.std + else: + tti, tti_prev = self.std, self.dst + + # A transition to a smaller offset creates a fold just after it. + fold = False + if last_change is not None: + shift = (tti_prev.utcoff - tti.utcoff).total_seconds() + fold = shift > ts - last_change + + return (tti, fold) + + def _get_trans_info_fromutc(self, ts, year): + if self._near_new_year(ts, year): + return self._get_trans_info_fromutc_near_new_year(ts, year) + + start, end = self.transitions(year) + start -= self.std.utcoff.total_seconds() + end -= self.dst.utcoff.total_seconds() + + if start < end: + isdst = start <= ts < end + else: + isdst = not (end <= ts < start) + + # For positive DST, the ambiguous period is one dst_diff after the end + # of DST; for negative DST, the ambiguous period is one dst_diff before + # the start of DST. + if self.dst_diff > 0: + ambig_start = end + ambig_end = end + self.dst_diff + else: + ambig_start = start + ambig_end = start - self.dst_diff + + fold = ambig_start <= ts < ambig_end + + return (self.dst if isdst else self.std, fold) + + +# A rule can put a transition up to 167 hours (just under a week) away from +# the day it names, and local time is at most a day or so away from UTC, so +# only times within this distance of the new year can be affected by the +# rules for the previous or next year. +_NEW_YEAR_MARGIN = 10 * 86400 + + +def _post_epoch_days_before_year(year): + """Get the number of days between 1970-01-01 and YEAR-01-01""" + y = year - 1 + return y * 365 + y // 4 - y // 100 + y // 400 + 1 - EPOCHORDINAL + + +class _DayOffset(object): + __slots__ = ["d", "julian", "hour", "minute", "second"] + + def __init__(self, d, julian, hour=2, minute=0, second=0): + min_day = 0 + julian # convert bool to int + if not min_day <= d <= 365: + raise ValueError("d must be in [%s, 365], not: %s" % (min_day, d)) + + self.d = d + self.julian = julian + self.hour = hour + self.minute = minute + self.second = second + + def year_to_epoch(self, year): + days_before_year = _post_epoch_days_before_year(year) + + # "n" counts days from 0 and includes February 29; "Jn" counts days + # from 1 and never counts February 29, so in leap years, J60 (March 1) + # and every day after it is one day later than its number suggests. + d = self.d + if self.julian: + d -= 1 + if d >= 59 and calendar.isleap(year): + d += 1 + + epoch = (days_before_year + d) * 86400 + epoch += self.hour * 3600 + self.minute * 60 + self.second + + return epoch + + +class _CalendarOffset(object): + __slots__ = ["m", "w", "d", "hour", "minute", "second"] + + _DAYS_BEFORE_MONTH = ( + -1, + 0, + 31, + 59, + 90, + 120, + 151, + 181, + 212, + 243, + 273, + 304, + 334, + ) + + def __init__(self, m, w, d, hour=2, minute=0, second=0): + if not 1 <= m <= 12: + raise ValueError("m must be in [1, 12]") + + if not 1 <= w <= 5: + raise ValueError("w must be in [1, 5]") + + if not 0 <= d <= 6: + raise ValueError("d must be in [0, 6]") + + self.m = m + self.w = w + self.d = d + self.hour = hour + self.minute = minute + self.second = second + + @classmethod + def _ymd2ord(cls, year, month, day): + return ( + _post_epoch_days_before_year(year) + + cls._DAYS_BEFORE_MONTH[month] + + (month > 2 and calendar.isleap(year)) + + day + - 1 + ) + + # TODO: These are not actually epoch dates as they are expressed in local time + def year_to_epoch(self, year): + """Calculates the datetime of the occurrence from the year""" + # We know year and month, we need to convert w, d into day of month + # + # Week 1 is the first week in which day `d` (where 0 = Sunday) appears. + # Week 5 represents the last occurrence of day `d`, so we need to know + # the range of the month. + # + # This doesn't use calendar.monthrange because the rules for years 0 + # and 10000 can affect times that datetime can represent, and + # calendar.monthrange constructs a datetime.date. Instead, get the + # weekday (0 = Monday) from the ordinal: 1970-01-01 was a Thursday. + first_ordinal = self._ymd2ord(year, self.m, 1) + first_day = (first_ordinal + 3) % 7 + if self.m == 12: + next_ordinal = self._ymd2ord(year + 1, 1, 1) + else: + next_ordinal = self._ymd2ord(year, self.m + 1, 1) + days_in_month = next_ordinal - first_ordinal + + # This equation seems magical, so I'll break it down: + # 1. calendar says 0 = Monday, POSIX says 0 = Sunday + # so we need first_day + 1 to get 1 = Monday -> 7 = Sunday, + # which is still equivalent because this math is mod 7 + # 2. Get first day - desired day mod 7: -1 % 7 = 6, so we don't need + # to do anything to adjust negative numbers. + # 3. Add 1 because month days are a 1-based index. + month_day = (self.d - (first_day + 1)) % 7 + 1 + + # Now use a 0-based index version of `w` to calculate the w-th + # occurrence of `d` + month_day += (self.w - 1) * 7 + + # month_day will only be > days_in_month if w was 5, and `w` means + # "last occurrence of `d`", so now we just check if we over-shot the + # end of the month and if so knock off 1 week. + if month_day > days_in_month: + month_day -= 7 + + ordinal = self._ymd2ord(year, self.m, month_day) + epoch = ordinal * 86400 + epoch += self.hour * 3600 + self.minute * 60 + self.second + return epoch diff --git a/hug/lib/python3.12/site-packages/dateutil/tz/_tzpath.py b/hug/lib/python3.12/site-packages/dateutil/tz/_tzpath.py new file mode 100644 index 0000000000000000000000000000000000000000..cabdfbc9c787ed6ce5b7e94a9e33f3f75ad90c0b --- /dev/null +++ b/hug/lib/python3.12/site-packages/dateutil/tz/_tzpath.py @@ -0,0 +1,104 @@ +try: + import zoneinfo + + reset_tzpath = zoneinfo.reset_tzpath + InvalidTZPathWarning = zoneinfo.InvalidTZPathWarning + + def __getattr__(name): + if name == "TZPATH": + return zoneinfo.TZPATH + else: + raise AttributeError( + "Module %s has no attribute %s" % (repr(__name__), repr(name)) + ) + +except ImportError: + import os + import sys + import warnings + + import six + + class InvalidTZPathWarning(RuntimeWarning): + """Warning raised if an invalid path is specified in PYTHONTZPATH.""" + + def _parse_python_tzpath(env_var, stacklevel): + if not env_var: + return () + + raw_tzpath = env_var.split(os.pathsep) + new_tzpath = tuple(filter(os.path.isabs, raw_tzpath)) + + # If anything has been filtered out, we will warn about it + if len(new_tzpath) != len(raw_tzpath): + msg = _get_invalid_paths_message(raw_tzpath) + + warnings.warn( + "Invalid paths specified in PYTHONTZPATH environment variable. " + + msg, + InvalidTZPathWarning, + stacklevel=stacklevel, + ) + + return new_tzpath + + def _get_invalid_paths_message(tzpaths): + invalid_paths = (path for path in tzpaths if not os.path.isabs(path)) + + prefix = "\n " + indented_str = prefix + prefix.join(invalid_paths) + + return ( + "Paths should be absolute but found the following relative paths:" + + indented_str + ) + + def _reset_tzpath(to=None, stacklevel=4): + global TZPATH + tzpaths = to + if tzpaths is not None: + if isinstance(tzpaths, six.string_types + (bytes,)): + raise TypeError( + "tzpaths must be a sequence, not %s: %s" + % ( + type(tzpaths), + repr(tzpaths), + ) + ) + + # Path-like objects not supported by os.path operations before 3.6 + if sys.version_info < (3, 6): + tzpaths = tuple(map(str, tzpaths)) + + if not all(map(os.path.isabs, tzpaths)): + raise ValueError(_get_invalid_paths_message(tzpaths)) + base_tzpath = tzpaths + else: + env_var = os.environ.get("PYTHONTZPATH", None) + if env_var is not None: + base_tzpath = _parse_python_tzpath(env_var, stacklevel) + elif sys.platform != "win32": + base_tzpath = ( + "/usr/share/zoneinfo", + "/usr/lib/zoneinfo", + "/usr/share/lib/zoneinfo", + "/etc/zoneinfo", + ) + else: + base_tzpath = () + + TZPATH = tuple(base_tzpath) + if TZPATH_CALLBACKS: + for callback in TZPATH_CALLBACKS: + callback(TZPATH) + + def reset_tzpath(to=None): + # As in CPython, the warning about PYTHONTZPATH is raised from a + # helper that is used both at import time and by this function, so + # that each can pass the stacklevel that points at its caller. + _reset_tzpath(to) + + TZPATH = () + TZPATH_CALLBACKS = [] + + _reset_tzpath(stacklevel=5) diff --git a/hug/lib/python3.12/site-packages/dateutil/tz/tz.py b/hug/lib/python3.12/site-packages/dateutil/tz/tz.py new file mode 100644 index 0000000000000000000000000000000000000000..f92dc70d01e6ffaa235cc70b9fb5492d87c9d0c2 --- /dev/null +++ b/hug/lib/python3.12/site-packages/dateutil/tz/tz.py @@ -0,0 +1,1472 @@ +# -*- coding: utf-8 -*- +""" +This module offers timezone implementations subclassing the abstract +:py:class:`datetime.tzinfo` type. There are classes to handle tzfile format +files (usually are in :file:`/etc/localtime`, :file:`/usr/share/zoneinfo`, +etc), TZ environment string (in all known formats), given ranges (with help +from relative deltas), local machine timezone, fixed offset timezone, and UTC +timezone. +""" + +import bisect +import datetime +import os +import struct +import sys +import time +import warnings +import weakref +from collections import OrderedDict + +import six +from six import string_types +from six.moves import _thread + +from .. import _tzdata_impl +from . import _tzpath +from ._common import ( + _tzinfo, + _validate_fromutc_inputs, + enfold, + tzname_in_python2, + tzrangebase, +) +from ._factories import _TzOffsetFactory, _TzSingleton, _TzStrFactory +from ._tzfile import tzfile + +try: + from .win import tzwin, tzwinlocal +except ImportError: + tzwin = tzwinlocal = None + +# For warning about rounding tzinfo +from warnings import warn + +ZERO = datetime.timedelta(0) +EPOCH = datetime.datetime(1970, 1, 1, 0, 0) + + +@six.add_metaclass(_TzSingleton) +class tzutc(datetime.tzinfo): + """ + This is a tzinfo object that represents the UTC time zone. + + **Examples:** + + .. doctest:: + + >>> from datetime import * + >>> from dateutil.tz import * + + >>> datetime.now() + datetime.datetime(2003, 9, 27, 9, 40, 1, 521290) + + >>> datetime.now(tzutc()) + datetime.datetime(2003, 9, 27, 12, 40, 12, 156379, tzinfo=tzutc()) + + >>> datetime.now(tzutc()).tzname() + 'UTC' + + .. versionchanged:: 2.7.0 + ``tzutc()`` is now a singleton, so the result of ``tzutc()`` will + always return the same object. + + .. doctest:: + + >>> from dateutil.tz import tzutc, UTC + >>> tzutc() is tzutc() + True + >>> tzutc() is UTC + True + """ + def utcoffset(self, dt): + return ZERO + + def dst(self, dt): + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + return "UTC" + + def is_ambiguous(self, dt): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + + + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + return False + + @_validate_fromutc_inputs + def fromutc(self, dt): + """ + Fast track version of fromutc() returns the original ``dt`` object for + any valid :py:class:`datetime.datetime` object. + """ + return dt + + def __eq__(self, other): + if not isinstance(other, (tzutc, tzoffset)): + return NotImplemented + + return (isinstance(other, tzutc) or + (isinstance(other, tzoffset) and other._offset == ZERO)) + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + return "%s()" % self.__class__.__name__ + + __reduce__ = object.__reduce__ + + +#: Convenience constant providing a :class:`tzutc()` instance +#: +#: .. versionadded:: 2.7.0 +UTC = tzutc() + + +@six.add_metaclass(_TzOffsetFactory) +class tzoffset(datetime.tzinfo): + """ + A simple class for representing a fixed offset from UTC. + + :param name: + The timezone name, to be returned when ``tzname()`` is called. + :param offset: + The time zone offset in seconds, or (since version 2.6.0, represented + as a :py:class:`datetime.timedelta` object). + """ + def __init__(self, name, offset): + self._name = name + + try: + # Allow a timedelta + offset = offset.total_seconds() + except (TypeError, AttributeError): + pass + + self._offset = datetime.timedelta(seconds=_get_supported_offset(offset)) + + def utcoffset(self, dt): + return self._offset + + def dst(self, dt): + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + return self._name + + @_validate_fromutc_inputs + def fromutc(self, dt): + return dt + self._offset + + def is_ambiguous(self, dt): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + return False + + def __eq__(self, other): + if not isinstance(other, tzoffset): + return NotImplemented + + return self._offset == other._offset + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + return "%s(%s, %s)" % (self.__class__.__name__, + repr(self._name), + int(self._offset.total_seconds())) + + __reduce__ = object.__reduce__ + + +class tzlocal(_tzinfo): + """ + A :class:`tzinfo` subclass built around the ``time`` timezone functions. + """ + def __init__(self): + super(tzlocal, self).__init__() + + self._std_offset = datetime.timedelta(seconds=-time.timezone) + if time.daylight: + self._dst_offset = datetime.timedelta(seconds=-time.altzone) + else: + self._dst_offset = self._std_offset + + self._dst_saved = self._dst_offset - self._std_offset + self._hasdst = bool(self._dst_saved) + self._tznames = tuple(time.tzname) + + def utcoffset(self, dt): + if dt is None and self._hasdst: + return None + + if self._isdst(dt): + return self._dst_offset + else: + return self._std_offset + + def dst(self, dt): + if dt is None and self._hasdst: + return None + + if self._isdst(dt): + return self._dst_offset - self._std_offset + else: + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + return self._tznames[self._isdst(dt)] + + def is_ambiguous(self, dt): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + + + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + naive_dst = self._naive_is_dst(dt) + return (not naive_dst and + (naive_dst != self._naive_is_dst(dt - self._dst_saved))) + + def _naive_is_dst(self, dt): + timestamp = _datetime_to_timestamp(dt) + return time.localtime(timestamp + time.timezone).tm_isdst + + def _isdst(self, dt, fold_naive=True): + # We can't use mktime here. It is unstable when deciding if + # the hour near to a change is DST or not. + # + # timestamp = time.mktime((dt.year, dt.month, dt.day, dt.hour, + # dt.minute, dt.second, dt.weekday(), 0, -1)) + # return time.localtime(timestamp).tm_isdst + # + # The code above yields the following result: + # + # >>> import tz, datetime + # >>> t = tz.tzlocal() + # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() + # 'BRDT' + # >>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname() + # 'BRST' + # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() + # 'BRST' + # >>> datetime.datetime(2003,2,15,22,tzinfo=t).tzname() + # 'BRDT' + # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() + # 'BRDT' + # + # Here is a more stable implementation: + # + if not self._hasdst: + return False + + # Check for ambiguous times: + dstval = self._naive_is_dst(dt) + fold = getattr(dt, 'fold', None) + + if self.is_ambiguous(dt): + if fold is not None: + return not self._fold(dt) + else: + return True + + return dstval + + def __eq__(self, other): + if isinstance(other, tzlocal): + return (self._std_offset == other._std_offset and + self._dst_offset == other._dst_offset) + elif isinstance(other, tzutc): + return (not self._hasdst and + self._tznames[0] in {'UTC', 'GMT'} and + self._std_offset == ZERO) + elif isinstance(other, tzoffset): + return (not self._hasdst and + self._tznames[0] == other._name and + self._std_offset == other._offset) + else: + return NotImplemented + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + return "%s()" % self.__class__.__name__ + + __reduce__ = object.__reduce__ + + +class tzrange(tzrangebase): + """ + The ``tzrange`` object is a time zone specified by a set of offsets and + abbreviations, equivalent to the way the ``TZ`` variable can be specified + in POSIX-like systems, but using Python delta objects to specify DST + start, end and offsets. + + :param stdabbr: + The abbreviation for standard time (e.g. ``'EST'``). + + :param stdoffset: + An integer or :class:`datetime.timedelta` object or equivalent + specifying the base offset from UTC. + + If unspecified, +00:00 is used. + + :param dstabbr: + The abbreviation for DST / "Summer" time (e.g. ``'EDT'``). + + If specified, with no other DST information, DST is assumed to occur + and the default behavior or ``dstoffset``, ``start`` and ``end`` is + used. If unspecified and no other DST information is specified, it + is assumed that this zone has no DST. + + If this is unspecified and other DST information is *is* specified, + DST occurs in the zone but the time zone abbreviation is left + unchanged. + + :param dstoffset: + A an integer or :class:`datetime.timedelta` object or equivalent + specifying the UTC offset during DST. If unspecified and any other DST + information is specified, it is assumed to be the STD offset +1 hour. + + :param start: + A :class:`relativedelta.relativedelta` object or equivalent specifying + the time and time of year that daylight savings time starts. To + specify, for example, that DST starts at 2AM on the 2nd Sunday in + March, pass: + + ``relativedelta(hours=2, month=3, day=1, weekday=SU(+2))`` + + If unspecified and any other DST information is specified, the default + value is 2 AM on the first Sunday in April. + + :param end: + A :class:`relativedelta.relativedelta` object or equivalent + representing the time and time of year that daylight savings time + ends, with the same specification method as in ``start``. One note is + that this should point to the first time in the *standard* zone, so if + a transition occurs at 2AM in the DST zone and the clocks are set back + 1 hour to 1AM, set the ``hours`` parameter to +1. + + + **Examples:** + + .. testsetup:: tzrange + + from dateutil.tz import tzrange, tzstr + + .. doctest:: tzrange + + >>> tzstr('EST5EDT') == tzrange("EST", -18000, "EDT") + True + + >>> from dateutil.relativedelta import * + >>> range1 = tzrange("EST", -18000, "EDT") + >>> range2 = tzrange("EST", -18000, "EDT", -14400, + ... relativedelta(hours=+2, month=4, day=1, + ... weekday=SU(+1)), + ... relativedelta(hours=+1, month=10, day=31, + ... weekday=SU(-1))) + >>> tzstr('EST5EDT') == range1 == range2 + True + + """ + def __init__(self, stdabbr, stdoffset=None, + dstabbr=None, dstoffset=None, + start=None, end=None): + + global relativedelta + from dateutil import relativedelta + + self._std_abbr = stdabbr + self._dst_abbr = dstabbr + + try: + stdoffset = stdoffset.total_seconds() + except (TypeError, AttributeError): + pass + + try: + dstoffset = dstoffset.total_seconds() + except (TypeError, AttributeError): + pass + + if stdoffset is not None: + self._std_offset = datetime.timedelta(seconds=stdoffset) + else: + self._std_offset = ZERO + + if dstoffset is not None: + self._dst_offset = datetime.timedelta(seconds=dstoffset) + elif dstabbr and stdoffset is not None: + self._dst_offset = self._std_offset + datetime.timedelta(hours=+1) + else: + self._dst_offset = ZERO + + if dstabbr and start is None: + self._start_delta = relativedelta.relativedelta( + hours=+2, month=4, day=1, weekday=relativedelta.SU(+1)) + else: + self._start_delta = start + + if dstabbr and end is None: + self._end_delta = relativedelta.relativedelta( + hours=+1, month=10, day=31, weekday=relativedelta.SU(-1)) + else: + self._end_delta = end + + self._dst_base_offset_ = self._dst_offset - self._std_offset + self.hasdst = bool(self._start_delta) + + def transitions(self, year): + """ + For a given year, get the DST on and off transition times, expressed + always on the standard time side. For zones with no transitions, this + function returns ``None``. + + :param year: + The year whose transitions you would like to query. + + :return: + Returns a :class:`tuple` of :class:`datetime.datetime` objects, + ``(dston, dstoff)`` for zones with an annual DST transition, or + ``None`` for fixed offset zones. + """ + if not self.hasdst: + return None + + base_year = datetime.datetime(year, 1, 1) + + start = base_year + self._start_delta + end = base_year + self._end_delta + + return (start, end) + + def __eq__(self, other): + if not isinstance(other, tzrange): + return NotImplemented + + return (self._std_abbr == other._std_abbr and + self._dst_abbr == other._dst_abbr and + self._std_offset == other._std_offset and + self._dst_offset == other._dst_offset and + self._start_delta == other._start_delta and + self._end_delta == other._end_delta) + + @property + def _dst_base_offset(self): + return self._dst_base_offset_ + + +@six.add_metaclass(_TzStrFactory) +class tzstr(tzrange): + """ + ``tzstr`` objects are time zone objects specified by a time-zone string as + it would be passed to a ``TZ`` variable on POSIX-style systems (see + the `GNU C Library: TZ Variable`_ for more details). + + There is one notable exception, which is that POSIX-style time zones use an + inverted offset format, so normally ``GMT+3`` would be parsed as an offset + 3 hours *behind* GMT. The ``tzstr`` time zone object will parse this as an + offset 3 hours *ahead* of GMT. If you would like to maintain the POSIX + behavior, pass a ``True`` value to ``posix_offset``. + + The :class:`tzrange` object provides the same functionality, but is + specified using :class:`relativedelta.relativedelta` objects. rather than + strings. + + :param s: + A time zone string in ``TZ`` variable format. This can be a + :class:`bytes` (2.x: :class:`str`), :class:`str` (2.x: + :class:`unicode`) or a stream emitting unicode characters + (e.g. :class:`StringIO`). + + :param posix_offset: + Optional. If set to ``True``, interpret strings such as ``GMT+3`` or + ``UTC+3`` as being 3 hours *behind* UTC rather than ahead, per the + POSIX standard. + + .. caution:: + + Prior to version 2.7.0, this function also supported time zones + in the format: + + * ``EST5EDT,4,0,6,7200,10,0,26,7200,3600`` + * ``EST5EDT,4,1,0,7200,10,-1,0,7200,3600`` + + This format is non-standard and has been deprecated; this function + will raise a :class:`DeprecatedTZFormatWarning` until + support is removed in a future version. + + .. _`GNU C Library: TZ Variable`: + https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html + """ + def __init__(self, s, posix_offset=False): + global parser + from dateutil.parser import _parser as parser + + self._s = s + + res = parser._parsetz(s) + if res is None or res.any_unused_tokens: + raise ValueError("unknown string format") + + # Here we break the compatibility with the TZ variable handling. + # GMT-3 actually *means* the timezone -3. + if res.stdabbr in ("GMT", "UTC") and not posix_offset: + res.stdoffset *= -1 + + # We must initialize it first, since _delta() needs + # _std_offset and _dst_offset set. Use False in start/end + # to avoid building it two times. + tzrange.__init__(self, res.stdabbr, res.stdoffset, + res.dstabbr, res.dstoffset, + start=False, end=False) + + if not res.dstabbr: + self._start_delta = None + self._end_delta = None + else: + self._start_delta = self._delta(res.start) + if self._start_delta: + self._end_delta = self._delta(res.end, isend=1) + + self.hasdst = bool(self._start_delta) + + def _delta(self, x, isend=0): + from dateutil import relativedelta + kwargs = {} + if x.month is not None: + kwargs["month"] = x.month + if x.weekday is not None: + kwargs["weekday"] = relativedelta.weekday(x.weekday, x.week) + if x.week > 0: + kwargs["day"] = 1 + else: + kwargs["day"] = 31 + elif x.day: + kwargs["day"] = x.day + elif x.yday is not None: + kwargs["yearday"] = x.yday + elif x.jyday is not None: + kwargs["nlyearday"] = x.jyday + if not kwargs: + # Default is to start on first sunday of april, and end + # on last sunday of october. + if not isend: + kwargs["month"] = 4 + kwargs["day"] = 1 + kwargs["weekday"] = relativedelta.SU(+1) + else: + kwargs["month"] = 10 + kwargs["day"] = 31 + kwargs["weekday"] = relativedelta.SU(-1) + if x.time is not None: + kwargs["seconds"] = x.time + else: + # Default is 2AM. + kwargs["seconds"] = 7200 + if isend: + # Convert to standard time, to follow the documented way + # of working with the extra hour. See the documentation + # of the tzinfo class. + delta = self._dst_offset - self._std_offset + kwargs["seconds"] -= delta.seconds + delta.days * 86400 + return relativedelta.relativedelta(**kwargs) + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, repr(self._s)) + + +class _tzicalvtzcomp(object): + def __init__(self, tzoffsetfrom, tzoffsetto, isdst, + tzname=None, rrule=None): + self.tzoffsetfrom = datetime.timedelta(seconds=tzoffsetfrom) + self.tzoffsetto = datetime.timedelta(seconds=tzoffsetto) + self.tzoffsetdiff = self.tzoffsetto - self.tzoffsetfrom + self.isdst = isdst + self.tzname = tzname + self.rrule = rrule + + +class _tzicalvtz(_tzinfo): + def __init__(self, tzid, comps=[]): + super(_tzicalvtz, self).__init__() + + self._tzid = tzid + self._comps = comps + self._cachedate = [] + self._cachecomp = [] + self._cache_lock = _thread.allocate_lock() + + def _find_comp(self, dt): + if len(self._comps) == 1: + return self._comps[0] + + dt = dt.replace(tzinfo=None) + + try: + with self._cache_lock: + return self._cachecomp[self._cachedate.index( + (dt, self._fold(dt)))] + except ValueError: + pass + + lastcompdt = None + lastcomp = None + + for comp in self._comps: + compdt = self._find_compdt(comp, dt) + + if compdt and (not lastcompdt or lastcompdt < compdt): + lastcompdt = compdt + lastcomp = comp + + if not lastcomp: + # RFC says nothing about what to do when a given + # time is before the first onset date. We'll look for the + # first standard component, or the first component, if + # none is found. + for comp in self._comps: + if not comp.isdst: + lastcomp = comp + break + else: + lastcomp = comp[0] + + with self._cache_lock: + self._cachedate.insert(0, (dt, self._fold(dt))) + self._cachecomp.insert(0, lastcomp) + + if len(self._cachedate) > 10: + self._cachedate.pop() + self._cachecomp.pop() + + return lastcomp + + def _find_compdt(self, comp, dt): + if comp.tzoffsetdiff < ZERO and self._fold(dt): + dt -= comp.tzoffsetdiff + + compdt = comp.rrule.before(dt, inc=True) + + return compdt + + def utcoffset(self, dt): + if dt is None: + return None + + return self._find_comp(dt).tzoffsetto + + def dst(self, dt): + comp = self._find_comp(dt) + if comp.isdst: + return comp.tzoffsetdiff + else: + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + return self._find_comp(dt).tzname + + def __repr__(self): + return "" % repr(self._tzid) + + __reduce__ = object.__reduce__ + + +class tzical(object): + """ + This object is designed to parse an iCalendar-style ``VTIMEZONE`` structure + as set out in `RFC 5545`_ Section 4.6.5 into one or more `tzinfo` objects. + + :param `fileobj`: + A file or stream in iCalendar format, which should be UTF-8 encoded + with CRLF endings. + + .. _`RFC 5545`: https://tools.ietf.org/html/rfc5545 + """ + def __init__(self, fileobj): + global rrule + from dateutil import rrule + + if isinstance(fileobj, string_types): + self._s = fileobj + # ical should be encoded in UTF-8 with CRLF + fileobj = open(fileobj, 'r') + else: + self._s = getattr(fileobj, 'name', repr(fileobj)) + fileobj = _nullcontext(fileobj) + + self._vtz = {} + + with fileobj as fobj: + self._parse_rfc(fobj.read()) + + def keys(self): + """ + Retrieves the available time zones as a list. + """ + return list(self._vtz.keys()) + + def get(self, tzid=None): + """ + Retrieve a :py:class:`datetime.tzinfo` object by its ``tzid``. + + :param tzid: + If there is exactly one time zone available, omitting ``tzid`` + or passing :py:const:`None` value returns it. Otherwise a valid + key (which can be retrieved from :func:`keys`) is required. + + :raises ValueError: + Raised if ``tzid`` is not specified but there are either more + or fewer than 1 zone defined. + + :returns: + Returns either a :py:class:`datetime.tzinfo` object representing + the relevant time zone or :py:const:`None` if the ``tzid`` was + not found. + """ + if tzid is None: + if len(self._vtz) == 0: + raise ValueError("no timezones defined") + elif len(self._vtz) > 1: + raise ValueError("more than one timezone available") + tzid = next(iter(self._vtz)) + + return self._vtz.get(tzid) + + def _parse_offset(self, s): + s = s.strip() + if not s: + raise ValueError("empty offset") + if s[0] in ('+', '-'): + signal = (-1, +1)[s[0] == '+'] + s = s[1:] + else: + signal = +1 + if len(s) == 4: + return (int(s[:2]) * 3600 + int(s[2:]) * 60) * signal + elif len(s) == 6: + return (int(s[:2]) * 3600 + int(s[2:4]) * 60 + int(s[4:])) * signal + else: + raise ValueError("invalid offset: " + s) + + def _parse_rfc(self, s): + lines = s.splitlines() + if not lines: + raise ValueError("empty string") + + # Unfold + i = 0 + while i < len(lines): + line = lines[i].rstrip() + if not line: + del lines[i] + elif i > 0 and line[0] == " ": + lines[i-1] += line[1:] + del lines[i] + else: + i += 1 + + tzid = None + comps = [] + invtz = False + comptype = None + for line in lines: + if not line: + continue + name, value = line.split(':', 1) + parms = name.split(';') + if not parms: + raise ValueError("empty property name") + name = parms[0].upper() + parms = parms[1:] + if invtz: + if name == "BEGIN": + if value in ("STANDARD", "DAYLIGHT"): + # Process component + pass + else: + raise ValueError("unknown component: "+value) + comptype = value + founddtstart = False + tzoffsetfrom = None + tzoffsetto = None + rrulelines = [] + tzname = None + elif name == "END": + if value == "VTIMEZONE": + if comptype: + raise ValueError("component not closed: "+comptype) + if not tzid: + raise ValueError("mandatory TZID not found") + if not comps: + raise ValueError( + "at least one component is needed") + # Process vtimezone + self._vtz[tzid] = _tzicalvtz(tzid, comps) + invtz = False + elif value == comptype: + if not founddtstart: + raise ValueError("mandatory DTSTART not found") + if tzoffsetfrom is None: + raise ValueError( + "mandatory TZOFFSETFROM not found") + if tzoffsetto is None: + raise ValueError( + "mandatory TZOFFSETFROM not found") + # Process component + rr = None + if rrulelines: + rr = rrule.rrulestr("\n".join(rrulelines), + compatible=True, + ignoretz=True, + cache=True) + comp = _tzicalvtzcomp(tzoffsetfrom, tzoffsetto, + (comptype == "DAYLIGHT"), + tzname, rr) + comps.append(comp) + comptype = None + else: + raise ValueError("invalid component end: "+value) + elif comptype: + if name == "DTSTART": + # DTSTART in VTIMEZONE takes a subset of valid RRULE + # values under RFC 5545. + for parm in parms: + if parm != 'VALUE=DATE-TIME': + msg = ('Unsupported DTSTART param in ' + + 'VTIMEZONE: ' + parm) + raise ValueError(msg) + rrulelines.append(line) + founddtstart = True + elif name in ("RRULE", "RDATE", "EXRULE", "EXDATE"): + rrulelines.append(line) + elif name == "TZOFFSETFROM": + if parms: + raise ValueError( + "unsupported %s parm: %s " % (name, parms[0])) + tzoffsetfrom = self._parse_offset(value) + elif name == "TZOFFSETTO": + if parms: + raise ValueError( + "unsupported TZOFFSETTO parm: "+parms[0]) + tzoffsetto = self._parse_offset(value) + elif name == "TZNAME": + if parms: + raise ValueError( + "unsupported TZNAME parm: "+parms[0]) + tzname = value + elif name == "COMMENT": + pass + else: + raise ValueError("unsupported property: "+name) + else: + if name == "TZID": + if parms: + raise ValueError( + "unsupported TZID parm: "+parms[0]) + tzid = value + elif name in ("TZURL", "LAST-MODIFIED", "COMMENT"): + pass + else: + raise ValueError("unsupported property: "+name) + elif name == "BEGIN" and value == "VTIMEZONE": + tzid = None + comps = [] + invtz = True + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, repr(self._s)) + + +TZFILES = ["/etc/localtime", "localtime"] + +def __get_gettz(): + tzlocal_classes = (tzlocal,) + if tzwinlocal is not None: + tzlocal_classes += (tzwinlocal,) + + class GettzFunc(object): + """ + Retrieve a time zone object from a string representation + + This function is intended to retrieve the :py:class:`tzinfo` subclass + that best represents the time zone that would be used if a POSIX + `TZ variable`_ were set to the same value. + + If no argument or an empty string is passed to ``gettz``, local time + is returned: + + .. code-block:: python3 + + >>> gettz() + tzfile('/etc/localtime') + + This function is also the preferred way to map IANA tz database keys + to :class:`tzfile` objects. It searches for these keys in the + following order: + + 1. On the search path defined in :data:`TZPATH`. + 2. In the `tzdata `_ Python package. + 3. Using the system-specific mechanisms (e.g. the Windows registry). + + .. code-block:: python3 + + >>> gettz('Pacific/Kiritimati') + tzfile('/usr/share/zoneinfo/Pacific/Kiritimati') + + On Windows, the standard is extended to include the Windows-specific + zone names provided by the operating system: + + .. code-block:: python3 + + >>> gettz('Egypt Standard Time') + tzwin('Egypt Standard Time') + + Passing a GNU ``TZ`` style string time zone specification returns a + :class:`tzstr` object: + + .. code-block:: python3 + + >>> gettz('AEST-10AEDT-11,M10.1.0/2,M4.1.0/3') + tzstr('AEST-10AEDT-11,M10.1.0/2,M4.1.0/3') + + :param name: + A time zone name (IANA, or, on Windows, Windows keys), location of + a ``tzfile(5)`` zoneinfo file or ``TZ`` variable style time zone + specifier. An empty string, no argument or ``None`` is interpreted + as local time. + + :return: + Returns an instance of one of ``dateutil``'s :py:class:`tzinfo` + subclasses. + + .. versionchanged:: 2.7.0 + + After version 2.7.0, any two calls to ``gettz`` using the same + input strings will return the same object: + + .. code-block:: python3 + + >>> tz.gettz('America/Chicago') is tz.gettz('America/Chicago') + True + + In addition to improving performance, this ensures that + `"same zone" semantics`_ are used for datetimes in the same zone. + + + .. _`TZ variable`: + https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html + + .. _`"same zone" semantics`: + https://blog.ganssle.io/articles/2018/02/aware-datetime-arithmetic.html + """ + def __init__(self): + + self.__instances = weakref.WeakValueDictionary() + self.__strong_cache_size = 8 + self.__strong_cache = OrderedDict() + self._cache_lock = _thread.allocate_lock() + + def __call__(self, name=None): + with self._cache_lock: + rv = self.__instances.get(name, None) + + if rv is None: + # TODO: This holds the cache lock while nocache() reads + # the file, so a slow lookup (e.g. walking TZPATH on a + # network file system) blocks every other gettz call, + # including cache hits for unrelated keys. + rv = self.nocache(name=name) + if not (name is None + or isinstance(rv, tzlocal_classes) + or rv is None): + # tzlocal is slightly more complicated than the other + # time zone providers because it depends on environment + # at construction time, so don't cache that. + # + # We also cannot store weak references to None, so we + # will also not store that. + self.__instances[name] = rv + else: + # No need for strong caching, return immediately + return rv + + self.__strong_cache[name] = self.__strong_cache.pop(name, rv) + + if len(self.__strong_cache) > self.__strong_cache_size: + self.__strong_cache.popitem(last=False) + + return rv + + def set_cache_size(self, size): + with self._cache_lock: + self.__strong_cache_size = size + while len(self.__strong_cache) > size: + self.__strong_cache.popitem(last=False) + + def cache_clear(self): + with self._cache_lock: + self.__instances = weakref.WeakValueDictionary() + self.__strong_cache.clear() + + @staticmethod + def nocache(name=None): + """A non-cached version of gettz""" + tz = None + if not name: + try: + name = os.environ["TZ"] + except KeyError: + pass + if name is None or name in ("", ":"): + for filepath in TZFILES: + if not os.path.isabs(filepath): + filename = filepath + for path in _tzpath.TZPATH: + filepath = os.path.join(path, filename) + if _isfile(filepath): + break + else: + continue + if _isfile(filepath): + try: + tz = tzfile(filepath) + break + except (IOError, OSError, ValueError): + pass + else: + tz = tzlocal() + else: + try: + if name.startswith(":"): + name = name[1:] + except TypeError as e: + if isinstance(name, bytes): + new_msg = "gettz argument should be str, not bytes" + six.raise_from(TypeError(new_msg), e) + else: + raise + + # A NUL byte can never be part of a key or a path, and + # depending on the platform and Python version it raises + # ValueError from os.path, winreg or open(), so reject it + # up front rather than catching it in every branch. + if "\x00" in name: + return None + + if os.path.isabs(name): + if _isfile(name): + tz = tzfile(name) + else: + tz = None + else: + if _escapes_tzpath(name): + _warn_tzpath_escape(name) + + for path in _tzpath.TZPATH: + filepath = os.path.join(path, name) + if not _isfile(filepath): + filepath = filepath.replace(' ', '_') + if not _isfile(filepath): + continue + try: + tz = tzfile(filepath, key=name) + break + except (IOError, OSError, ValueError): + # TODO: A file that exists but can't be read or + # isn't a valid TZif file is silently skipped, + # and the key is looked up in the next TZPATH + # entry and then in tzdata. Consider warning + # here, since this usually means the system time + # zone data is broken. + pass + else: + tz = None + # As with the search path lookup above, also try + # the name with spaces replaced by underscores. + candidates = [name] + if " " in name: + candidates.append(name.replace(" ", "_")) + + for candidate in candidates: + try: + with _tzdata_impl._load_tzdata(candidate) as f: + return tzfile(f, key=name) + except ValueError: + # TZFileNotFound is a ValueError, and so is + # the error raised for files in tzdata that + # are not TZif files (e.g. "zone.tab"), + # which are skipped just as on TZPATH. + pass + + if tzwin is not None: + try: + tz = tzwin(name) + except (WindowsError, UnicodeEncodeError): + # UnicodeEncodeError is for Python 2.7 compat + tz = None + + if not tz: + for c in name: + # name is not a tzstr unless it has at least + # one offset. For short values of "name", an + # explicit for loop seems to be the fastest way + # To determine if a string contains a digit + if c in "0123456789": + try: + tz = tzstr(name) + except ValueError: + pass + break + else: + if name in ("GMT", "UTC"): + tz = UTC + elif name in time.tzname: + tz = tzlocal() + return tz + + return GettzFunc() + + +gettz = __get_gettz() +del __get_gettz + + +class DeprecatedTzKeyWarning(FutureWarning): + """Warning raised when :func:`gettz` is given a key outside of TZPATH. + + Relative keys like ``"../../etc/localtime"`` resolve to files outside of + every directory on :data:`TZPATH`. Support for these is deprecated, and a + warnings filter turns this warning into an error by default. To keep the + old behavior for now, filter it out, e.g.:: + + warnings.filterwarnings( + "ignore", category=dateutil.tz.DeprecatedTzKeyWarning + ) + + Absolute paths to TZif files are not affected. + """ + + +def _escapes_tzpath(name): + # A relative key stays inside every TZPATH entry unless, once normalized + # (which also turns os.altsep into os.sep), it starts by going up a + # directory. + return os.path.normpath(name).split(os.sep, 1)[0] == os.pardir + + +def _warn_tzpath_escape(name): + # As in dateutil.zoneinfo.rebuild, the filter is appended at call time, + # so any filter the user has installed for this category wins. + warnings.filterwarnings( + "error", category=DeprecatedTzKeyWarning, append=True + ) + warnings.warn( + "gettz(%r) refers to a file outside of TZPATH; support for keys " + "like this is deprecated and will be removed in a future version. " + "Use an absolute path instead." % (name,), + DeprecatedTzKeyWarning, + stacklevel=4, + ) + + +def available_iana_timezones(): + """Returns a set containing all available IANA time zones keys. + + These are suitable for use with :func:`gettz`. This is essentially a direct + port of :py:func:`zoneinfo.available_timezones`. + + The search includes time zones found on :data:`TZPATH` as well as any + time zones provided by the `tzdata `_ + package if it is installed. + + :return: + Returns a mutable set containing strings of time zones. Each call will + return a fresh set. + + .. caution:: + + This may attempt to open a large number of files, since the best way to + determine if a given file on the time zone search path is to open it + and check for the "magic string" at the beginning. + + .. note:: + + These values are not designed to be exposed to end-users; for + user-facing elements, applications should use something like CLDR (the + Unicode Common Locale Data Repository) to get more user-friendly + strings. + """ + # Start with loading from the tzdata package if it exists: this has a + # pre-assembled list of zones that only requires opening one file. + try: + valid_zones = set(_tzdata_impl._load_tzdata_keys()) + except ImportError: + valid_zones = set() + + def valid_key(fpath): + try: + with open(fpath, "rb") as f: + return f.read(4) == b"TZif" + except Exception: # pragma: nocover + return False + + for tz_root in _tzpath.TZPATH: + if not os.path.exists(tz_root): + continue + + for root, dirnames, files in os.walk(tz_root): + if root == tz_root: + # right/ and posix/ are special directories and shouldn't be + # included in the output of available zones + if "right" in dirnames: + dirnames.remove("right") + if "posix" in dirnames: + dirnames.remove("posix") + + for file in files: + fpath = os.path.join(root, file) + + key = os.path.relpath(fpath, start=tz_root) + if os.sep != "/": # pragma: nocover + key = key.replace(os.sep, "/") + + if not key or key in valid_zones: + continue + + if valid_key(fpath): + valid_zones.add(key) + + if "posixrules" in valid_zones: + # posixrules is a special symlink-only time zone where it exists, it + # should not be included in the output + valid_zones.remove("posixrules") + if "localtime" in valid_zones: + # localtime is a special symlink-only time zone where it exists, it + # should not be included in the output + valid_zones.remove("localtime") + + return valid_zones + + +def datetime_exists(dt, tz=None): + """ + Given a datetime and a time zone, determine whether or not a given datetime + would fall in a gap. + + :param dt: + A :class:`datetime.datetime` (whose time zone will be ignored if ``tz`` + is provided.) + + :param tz: + A :class:`datetime.tzinfo` with support for the ``fold`` attribute. If + ``None`` or not provided, the datetime's own time zone will be used. + + :return: + Returns a boolean value whether or not the "wall time" exists in + ``tz``. + + .. versionadded:: 2.7.0 + """ + if tz is None: + if dt.tzinfo is None: + raise ValueError('Datetime is naive and no time zone provided.') + tz = dt.tzinfo + + dt = dt.replace(tzinfo=None) + + # This is essentially a test of whether or not the datetime can survive + # a round trip to UTC. + dt_rt = dt.replace(tzinfo=tz).astimezone(UTC).astimezone(tz) + dt_rt = dt_rt.replace(tzinfo=None) + + return dt == dt_rt + + +def datetime_ambiguous(dt, tz=None): + """ + Given a datetime and a time zone, determine whether or not a given datetime + is ambiguous (i.e if there are two times differentiated only by their DST + status). + + :param dt: + A :class:`datetime.datetime` (whose time zone will be ignored if ``tz`` + is provided.) + + :param tz: + A :class:`datetime.tzinfo` with support for the ``fold`` attribute. If + ``None`` or not provided, the datetime's own time zone will be used. + + :return: + Returns a boolean value whether or not the "wall time" is ambiguous in + ``tz``. + + .. versionadded:: 2.6.0 + """ + if tz is None: + if dt.tzinfo is None: + raise ValueError('Datetime is naive and no time zone provided.') + + tz = dt.tzinfo + + # If a time zone defines its own "is_ambiguous" function, we'll use that. + is_ambiguous_fn = getattr(tz, 'is_ambiguous', None) + if is_ambiguous_fn is not None: + try: + return tz.is_ambiguous(dt) + except Exception: + pass + + # If it doesn't come out and tell us it's ambiguous, we'll just check if + # the fold attribute has any effect on this particular date and time. + dt = dt.replace(tzinfo=tz) + wall_0 = enfold(dt, fold=0) + wall_1 = enfold(dt, fold=1) + + same_offset = wall_0.utcoffset() == wall_1.utcoffset() + same_dst = wall_0.dst() == wall_1.dst() + + return not (same_offset and same_dst) + + +def resolve_imaginary(dt): + """ + Given a datetime that may be imaginary, return an existing datetime. + + This function assumes that an imaginary datetime represents what the + wall time would be in a zone had the offset transition not occurred, so + it will always fall forward by the transition's change in offset. + + .. doctest:: + + >>> from dateutil import tz + >>> from datetime import datetime + >>> NYC = tz.gettz('America/New_York') + >>> print(tz.resolve_imaginary(datetime(2017, 3, 12, 2, 30, tzinfo=NYC))) + 2017-03-12 03:30:00-04:00 + + >>> KIR = tz.gettz('Pacific/Kiritimati') + >>> print(tz.resolve_imaginary(datetime(1995, 1, 1, 12, 30, tzinfo=KIR))) + 1995-01-02 12:30:00+14:00 + + As a note, :func:`datetime.astimezone` is guaranteed to produce a valid, + existing datetime, so a round-trip to and from UTC is sufficient to get + an extant datetime, however, this generally "falls back" to an earlier time + rather than falling forward to the STD side (though no guarantees are made + about this behavior). + + :param dt: + A :class:`datetime.datetime` which may or may not exist. + + :return: + Returns an existing :class:`datetime.datetime`. If ``dt`` was not + imaginary, the datetime returned is guaranteed to be the same object + passed to the function. + + .. versionadded:: 2.7.0 + """ + if dt.tzinfo is not None and not datetime_exists(dt): + + curr_offset = (dt + datetime.timedelta(hours=24)).utcoffset() + old_offset = (dt - datetime.timedelta(hours=24)).utcoffset() + + dt += curr_offset - old_offset + + return dt + + +def _datetime_to_timestamp(dt): + """ + Convert a :class:`datetime.datetime` object to an epoch timestamp in + seconds since January 1, 1970, ignoring the time zone. + """ + return (dt.replace(tzinfo=None) - EPOCH).total_seconds() + + +if sys.version_info >= (3, 6): + def _get_supported_offset(second_offset): + return second_offset +else: + def _get_supported_offset(second_offset): + # For python pre-3.6, round to full-minutes if that's not the case. + # Python's datetime doesn't accept sub-minute timezones. Check + # http://python.org/sf/1447945 or https://bugs.python.org/issue5288 + # for some information. + old_offset = second_offset + calculated_offset = 60 * ((second_offset + 30) // 60) + return calculated_offset + + +if sys.version_info < (3, 8): + + def _isfile(path): + # bpo-33721: In Python 3.8 non-UTF8 paths return False rather than + # raising an error. See https://bugs.python.org/issue33721 + try: + return os.path.isfile(path) + except ValueError: + return False + +else: + _isfile = os.path.isfile + + +try: + # Python 3.7 feature + from contextlib import nullcontext as _nullcontext +except ImportError: + class _nullcontext(object): + """ + Class for wrapping contexts so that they are passed through in a + with statement. + """ + def __init__(self, context): + self.context = context + + def __enter__(self): + return self.context + + def __exit__(*args, **kwargs): + pass + +# vim:ts=4:sw=4:et diff --git a/hug/lib/python3.12/site-packages/dateutil/tz/win.py b/hug/lib/python3.12/site-packages/dateutil/tz/win.py new file mode 100644 index 0000000000000000000000000000000000000000..cde07ba792c40903f0c334839140173b39fd8124 --- /dev/null +++ b/hug/lib/python3.12/site-packages/dateutil/tz/win.py @@ -0,0 +1,370 @@ +# -*- coding: utf-8 -*- +""" +This module provides an interface to the native time zone data on Windows, +including :py:class:`datetime.tzinfo` implementations. + +Attempting to import this module on a non-Windows platform will raise an +:py:obj:`ImportError`. +""" +# This code was originally contributed by Jeffrey Harris. +import datetime +import struct + +from six.moves import winreg +from six import text_type + +try: + import ctypes + from ctypes import wintypes +except ValueError: + # ValueError is raised on non-Windows systems for some horrible reason. + raise ImportError("Running tzwin on non-Windows system") + +from ._common import tzrangebase + +__all__ = ["tzwin", "tzwinlocal", "tzres"] + +ONEWEEK = datetime.timedelta(7) + +TZKEYNAMENT = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones" +TZKEYNAME9X = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Time Zones" +TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation" + + +def _settzkeyname(): + handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) + try: + winreg.OpenKey(handle, TZKEYNAMENT).Close() + TZKEYNAME = TZKEYNAMENT + except WindowsError: + TZKEYNAME = TZKEYNAME9X + handle.Close() + return TZKEYNAME + + +TZKEYNAME = _settzkeyname() + + +class tzres(object): + """ + Class for accessing ``tzres.dll``, which contains timezone name related + resources. + + .. versionadded:: 2.5.0 + """ + p_wchar = ctypes.POINTER(wintypes.WCHAR) # Pointer to a wide char + + def __init__(self, tzres_loc='tzres.dll'): + # Load the user32 DLL so we can load strings from tzres + user32 = ctypes.WinDLL('user32') + + # Specify the LoadStringW function + user32.LoadStringW.argtypes = (wintypes.HINSTANCE, + wintypes.UINT, + wintypes.LPWSTR, + ctypes.c_int) + + self.LoadStringW = user32.LoadStringW + self._tzres = ctypes.WinDLL(tzres_loc) + self.tzres_loc = tzres_loc + + def load_name(self, offset): + """ + Load a timezone name from a DLL offset (integer). + + >>> from dateutil.tzwin import tzres + >>> tzr = tzres() + >>> print(tzr.load_name(112)) + 'Eastern Standard Time' + + :param offset: + A positive integer value referring to a string from the tzres dll. + + .. note:: + + Offsets found in the registry are generally of the form + ``@tzres.dll,-114``. The offset in this case is 114, not -114. + + """ + resource = self.p_wchar() + lpBuffer = ctypes.cast(ctypes.byref(resource), wintypes.LPWSTR) + nchar = self.LoadStringW(self._tzres._handle, offset, lpBuffer, 0) + return resource[:nchar] + + def name_from_string(self, tzname_str): + """ + Parse strings as returned from the Windows registry into the time zone + name as defined in the registry. + + >>> from dateutil.tzwin import tzres + >>> tzr = tzres() + >>> print(tzr.name_from_string('@tzres.dll,-251')) + 'Dateline Daylight Time' + >>> print(tzr.name_from_string('Eastern Standard Time')) + 'Eastern Standard Time' + + :param tzname_str: + A timezone name string as returned from a Windows registry key. + + :return: + Returns the localized timezone string from tzres.dll if the string + is of the form `@tzres.dll,-offset`, else returns the input string. + """ + if not tzname_str.startswith('@'): + return tzname_str + + name_splt = tzname_str.split(',-') + try: + offset = int(name_splt[1]) + except: + raise ValueError("Malformed timezone string.") + + return self.load_name(offset) + + +class tzwinbase(tzrangebase): + """tzinfo class based on win32's timezones available in the registry.""" + def __init__(self): + raise NotImplementedError('tzwinbase is an abstract base class') + + def __eq__(self, other): + # Compare on all relevant dimensions, including name. + if not isinstance(other, tzwinbase): + return NotImplemented + + return (self._std_offset == other._std_offset and + self._dst_offset == other._dst_offset and + self._stddayofweek == other._stddayofweek and + self._dstdayofweek == other._dstdayofweek and + self._stdweeknumber == other._stdweeknumber and + self._dstweeknumber == other._dstweeknumber and + self._stdhour == other._stdhour and + self._dsthour == other._dsthour and + self._stdminute == other._stdminute and + self._dstminute == other._dstminute and + self._std_abbr == other._std_abbr and + self._dst_abbr == other._dst_abbr) + + @staticmethod + def list(): + """Return a list of all time zones known to the system.""" + with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle: + with winreg.OpenKey(handle, TZKEYNAME) as tzkey: + result = [winreg.EnumKey(tzkey, i) + for i in range(winreg.QueryInfoKey(tzkey)[0])] + return result + + def display(self): + """ + Return the display name of the time zone. + """ + return self._display + + def transitions(self, year): + """ + For a given year, get the DST on and off transition times, expressed + always on the standard time side. For zones with no transitions, this + function returns ``None``. + + :param year: + The year whose transitions you would like to query. + + :return: + Returns a :class:`tuple` of :class:`datetime.datetime` objects, + ``(dston, dstoff)`` for zones with an annual DST transition, or + ``None`` for fixed offset zones. + """ + + if not self.hasdst: + return None + + dston = picknthweekday(year, self._dstmonth, self._dstdayofweek, + self._dsthour, self._dstminute, + self._dstweeknumber) + + dstoff = picknthweekday(year, self._stdmonth, self._stddayofweek, + self._stdhour, self._stdminute, + self._stdweeknumber) + + # Ambiguous dates default to the STD side + dstoff -= self._dst_base_offset + + return dston, dstoff + + def _get_hasdst(self): + return self._dstmonth != 0 + + @property + def _dst_base_offset(self): + return self._dst_base_offset_ + + +class tzwin(tzwinbase): + """ + Time zone object created from the zone info in the Windows registry + + These are similar to :py:class:`dateutil.tz.tzrange` objects in that + the time zone data is provided in the format of a single offset rule + for either 0 or 2 time zone transitions per year. + + :param: name + The name of a Windows time zone key, e.g. "Eastern Standard Time". + The full list of keys can be retrieved with :func:`tzwin.list`. + """ + + def __init__(self, name): + self._name = name + + with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle: + tzkeyname = text_type("{kn}\\{name}").format(kn=TZKEYNAME, name=name) + with winreg.OpenKey(handle, tzkeyname) as tzkey: + keydict = valuestodict(tzkey) + + self._std_abbr = keydict["Std"] + self._dst_abbr = keydict["Dlt"] + + self._display = keydict["Display"] + + # See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm + tup = struct.unpack("=3l16h", keydict["TZI"]) + stdoffset = -tup[0]-tup[1] # Bias + StandardBias * -1 + dstoffset = stdoffset-tup[2] # + DaylightBias * -1 + self._std_offset = datetime.timedelta(minutes=stdoffset) + self._dst_offset = datetime.timedelta(minutes=dstoffset) + + # for the meaning see the win32 TIME_ZONE_INFORMATION structure docs + # http://msdn.microsoft.com/en-us/library/windows/desktop/ms725481(v=vs.85).aspx + (self._stdmonth, + self._stddayofweek, # Sunday = 0 + self._stdweeknumber, # Last = 5 + self._stdhour, + self._stdminute) = tup[4:9] + + (self._dstmonth, + self._dstdayofweek, # Sunday = 0 + self._dstweeknumber, # Last = 5 + self._dsthour, + self._dstminute) = tup[12:17] + + self._dst_base_offset_ = self._dst_offset - self._std_offset + self.hasdst = self._get_hasdst() + + def __repr__(self): + return "tzwin(%s)" % repr(self._name) + + def __reduce__(self): + return (self.__class__, (self._name,)) + + +class tzwinlocal(tzwinbase): + """ + Class representing the local time zone information in the Windows registry + + While :class:`dateutil.tz.tzlocal` makes system calls (via the :mod:`time` + module) to retrieve time zone information, ``tzwinlocal`` retrieves the + rules directly from the Windows registry and creates an object like + :class:`dateutil.tz.tzwin`. + + Because Windows does not have an equivalent of :func:`time.tzset`, on + Windows, :class:`dateutil.tz.tzlocal` instances will always reflect the + time zone settings *at the time that the process was started*, meaning + changes to the machine's time zone settings during the run of a program + on Windows will **not** be reflected by :class:`dateutil.tz.tzlocal`. + Because ``tzwinlocal`` reads the registry directly, it is unaffected by + this issue. + """ + def __init__(self): + with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle: + with winreg.OpenKey(handle, TZLOCALKEYNAME) as tzlocalkey: + keydict = valuestodict(tzlocalkey) + + self._std_abbr = keydict["StandardName"] + self._dst_abbr = keydict["DaylightName"] + + try: + tzkeyname = text_type('{kn}\\{sn}').format(kn=TZKEYNAME, + sn=self._std_abbr) + with winreg.OpenKey(handle, tzkeyname) as tzkey: + _keydict = valuestodict(tzkey) + self._display = _keydict["Display"] + except OSError: + self._display = None + + stdoffset = -keydict["Bias"]-keydict["StandardBias"] + dstoffset = stdoffset-keydict["DaylightBias"] + + self._std_offset = datetime.timedelta(minutes=stdoffset) + self._dst_offset = datetime.timedelta(minutes=dstoffset) + + # For reasons unclear, in this particular key, the day of week has been + # moved to the END of the SYSTEMTIME structure. + tup = struct.unpack("=8h", keydict["StandardStart"]) + + (self._stdmonth, + self._stdweeknumber, # Last = 5 + self._stdhour, + self._stdminute) = tup[1:5] + + self._stddayofweek = tup[7] + + tup = struct.unpack("=8h", keydict["DaylightStart"]) + + (self._dstmonth, + self._dstweeknumber, # Last = 5 + self._dsthour, + self._dstminute) = tup[1:5] + + self._dstdayofweek = tup[7] + + self._dst_base_offset_ = self._dst_offset - self._std_offset + self.hasdst = self._get_hasdst() + + def __repr__(self): + return "tzwinlocal()" + + def __str__(self): + # str will return the standard name, not the daylight name. + return "tzwinlocal(%s)" % repr(self._std_abbr) + + def __reduce__(self): + return (self.__class__, ()) + + +def picknthweekday(year, month, dayofweek, hour, minute, whichweek): + """ dayofweek == 0 means Sunday, whichweek 5 means last instance """ + first = datetime.datetime(year, month, 1, hour, minute) + + # This will work if dayofweek is ISO weekday (1-7) or Microsoft-style (0-6), + # Because 7 % 7 = 0 + weekdayone = first.replace(day=((dayofweek - first.isoweekday()) % 7) + 1) + wd = weekdayone + ((whichweek - 1) * ONEWEEK) + if (wd.month != month): + wd -= ONEWEEK + + return wd + + +def valuestodict(key): + """Convert a registry key's values to a dictionary.""" + dout = {} + size = winreg.QueryInfoKey(key)[1] + tz_res = None + + for i in range(size): + key_name, value, dtype = winreg.EnumValue(key, i) + if dtype == winreg.REG_DWORD or dtype == winreg.REG_DWORD_LITTLE_ENDIAN: + # If it's a DWORD (32-bit integer), it's stored as unsigned - convert + # that to a proper signed integer + if value & (1 << 31): + value = value - (1 << 32) + elif dtype == winreg.REG_SZ: + # If it's a reference to the tzres DLL, load the actual string + if value.startswith('@tzres'): + tz_res = tz_res or tzres() + value = tz_res.name_from_string(value) + + value = value.rstrip('\x00') # Remove trailing nulls + + dout[key_name] = value + + return dout diff --git a/hug/lib/python3.12/site-packages/dateutil/tzwin.py b/hug/lib/python3.12/site-packages/dateutil/tzwin.py new file mode 100644 index 0000000000000000000000000000000000000000..cebc673e40fc376653ebf037e96f0a6d0b33e906 --- /dev/null +++ b/hug/lib/python3.12/site-packages/dateutil/tzwin.py @@ -0,0 +1,2 @@ +# tzwin has moved to dateutil.tz.win +from .tz.win import * diff --git a/hug/lib/python3.12/site-packages/dateutil/utils.py b/hug/lib/python3.12/site-packages/dateutil/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..dd2d245a0bebcd5fc37ac20526aabbd5358dab0e --- /dev/null +++ b/hug/lib/python3.12/site-packages/dateutil/utils.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +""" +This module offers general convenience and utility functions for dealing with +datetimes. + +.. versionadded:: 2.7.0 +""" +from __future__ import unicode_literals + +from datetime import datetime, time + + +def today(tzinfo=None): + """ + Returns a :py:class:`datetime` representing the current day at midnight + + :param tzinfo: + The time zone to attach (also used to determine the current day). + + :return: + A :py:class:`datetime.datetime` object representing the current day + at midnight. + """ + + dt = datetime.now(tzinfo) + return datetime.combine(dt.date(), time(0, tzinfo=tzinfo)) + + +def default_tzinfo(dt, tzinfo): + """ + Sets the ``tzinfo`` parameter on naive datetimes only + + This is useful for example when you are provided a datetime that may have + either an implicit or explicit time zone, such as when parsing a time zone + string. + + .. doctest:: + + >>> from dateutil.tz import tzoffset + >>> from dateutil.parser import parse + >>> from dateutil.utils import default_tzinfo + >>> dflt_tz = tzoffset("EST", -18000) + >>> print(default_tzinfo(parse('2014-01-01 12:30 UTC'), dflt_tz)) + 2014-01-01 12:30:00+00:00 + >>> print(default_tzinfo(parse('2014-01-01 12:30'), dflt_tz)) + 2014-01-01 12:30:00-05:00 + + :param dt: + The datetime on which to replace the time zone + + :param tzinfo: + The :py:class:`datetime.tzinfo` subclass instance to assign to + ``dt`` if (and only if) it is naive. + + :return: + Returns an aware :py:class:`datetime.datetime`. + """ + if dt.tzinfo is not None: + return dt + else: + return dt.replace(tzinfo=tzinfo) + + +def within_delta(dt1, dt2, delta): + """ + Useful for comparing two datetimes that may have a negligible difference + to be considered equal. + """ + delta = abs(delta) + difference = dt1 - dt2 + return -delta <= difference <= delta diff --git a/hug/lib/python3.12/site-packages/dateutil/zoneinfo/__init__.py b/hug/lib/python3.12/site-packages/dateutil/zoneinfo/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3419862f82dd05aef561b6f53e73f6115f8ccb45 --- /dev/null +++ b/hug/lib/python3.12/site-packages/dateutil/zoneinfo/__init__.py @@ -0,0 +1,233 @@ +# -*- coding: utf-8 -*- +""" +The ``dateutil.zoneinfo`` module was originally designed to work with a tarball +shipped with ``dateutil``. Since ``dateutil`` now uses data from the same +sources as :py:mod:`zoneinfo`, this module is no longer necessary. + +.. caution:: + This module is deprecated, and its functionality has been dramatically + curtailed. It is recommended that you adopt the standard library module + :py:mod:`zoneinfo` or :func:`dateutil.tz.gettz` instead. + +""" + +import json +import sys +import warnings +from tarfile import TarFile + +import six + +from dateutil import _tzdata_impl +from dateutil.tz import tzfile as _tzfile + +__all__ = ["get_zonefile_instance", "gettz", "gettz_db_metadata"] + +ZONEFILENAME = None +METADATA_FN = 'METADATA' + + +def _importer_stacklevel(): + """The ``stacklevel`` that attributes a warning to the importing code. + + This module can be imported through ``dateutil.__getattr__`` (e.g. by + ``from dateutil import zoneinfo``), which calls + ``importlib.import_module``; the warnings machinery skips the import + system's own frames, but not those two, so skip them here. + """ + level = 1 + frame = sys._getframe(1) # The body of this module + while frame.f_back is not None: + frame = frame.f_back + filename = frame.f_code.co_filename + if "importlib" in filename and "_bootstrap" in filename: + # Since Python 3.5, the warnings machinery skips these frames + # without counting them; before that they count like any other. + if sys.version_info < (3, 5): + level += 1 + continue + + level += 1 + if frame.f_globals.get("__name__") not in ("importlib", "dateutil"): + return level + + return 2 + + +warnings.warn( + "The `dateutil.zoneinfo` module has been replaced with a wrapper around " + "the tzdata package, and its use is deprecated, to be removed in a future " + "version. Use the standard library module `zoneinfo` or `dateutil.tz` " + "instead.", + DeprecationWarning, + stacklevel=_importer_stacklevel(), +) + + +tzfile = _tzfile + +class ZoneInfoFile(object): + def __init__(self, zonefile_stream=None): + if zonefile_stream is not None: + self._load_legacy_zonefile_stream(zonefile_stream) + else: + self._eager_load_tzdata() + + def _eager_load_tzdata(self): + try: + import tzdata + except ImportError as e: + six.raise_from( + ImportError( + "The tzdata module is required to use ZoneInfoFile; either add " + "a dependency on tzdata or migrate away from dateutil.zoneinfo." + ), + e, + ) + + zone_dict = {} + + for key in _tzdata_impl._load_tzdata_keys(): + with _tzdata_impl._load_tzdata(key) as f: + zone_dict[key] = tzfile(f, key=key) + + self.zones = zone_dict + self.metadata = { + "metadata_version": "3.0", + "tzversion": tzdata.IANA_VERSION, + } + + def _load_legacy_zonefile_stream(self, zonefile_stream): + with TarFile.open(fileobj=zonefile_stream) as tf: + self.zones = { + zf.name: tzfile(tf.extractfile(zf), filename=zf.name) + for zf in tf.getmembers() + if zf.isfile() and zf.name != METADATA_FN + } + # deal with links: They'll point to their parent object. Less + # waste of memory + links = { + zl.name: self.zones[zl.linkname] + for zl in tf.getmembers() + if zl.islnk() or zl.issym() + } + self.zones.update(links) + try: + metadata_json = tf.extractfile(tf.getmember(METADATA_FN)) + metadata_str = metadata_json.read().decode("UTF-8") + self.metadata = json.loads(metadata_str) + except KeyError: + # no metadata in tar file + self.metadata = None + + def get(self, name, default=None): + """ + Wrapper for :func:`ZoneInfoFile.zones.get`. This is a convenience method + for retrieving zones from the zone dictionary. + + :param name: + The name of the zone to retrieve. (Generally IANA zone names) + + :param default: + The value to return in the event of a missing key. + + .. versionadded:: 2.6.0 + + """ + return self.zones.get(name, default) + + +# The current API has gettz as a module function, although in fact it taps into +# a stateful class. So as a workaround for now, without changing the API, we +# will create a new "global" class instance the first time a user requests a +# timezone. Ugly, but adheres to the api. +# +# TODO: Remove after deprecation period. +_CLASS_ZONE_INSTANCE = [] + + +def get_zonefile_instance(new_instance=False): + """ + This is a convenience function which provides a :class:`ZoneInfoFile` + instance using the data provided by the ``dateutil`` package. By default, it + caches a single instance of the ZoneInfoFile object and returns that. + + :param new_instance: + If ``True``, a new instance of :class:`ZoneInfoFile` is instantiated and + used as the cached instance for the next call. Otherwise, new instances + are created only as necessary. + + :return: + Returns a :class:`ZoneInfoFile` object. + + .. versionadded:: 2.6 + """ + if new_instance: + zif = None + else: + zif = getattr(get_zonefile_instance, '_cached_instance', None) + + if zif is None: + zif = ZoneInfoFile() + + get_zonefile_instance._cached_instance = zif + + return zif + + +def gettz(name): + """ + This retrieves a time zone from the ``tzdata`` package. + + :param name: + An IANA-style time zone name, as found in the zoneinfo file. + + :return: + Returns a :class:`dateutil.tz.tzfile` time zone object. + + .. warning:: + It is generally inadvisable to use this function, and it is only + provided for API compatibility with earlier versions. This is *not* + equivalent to ``dateutil.tz.gettz()``, which selects an appropriate + time zone based on the inputs, favoring system zoneinfo. This is ONLY + for accessing the dateutil-specific zoneinfo (which may be out of + date compared to the system zoneinfo). + + .. deprecated:: 2.6 + If you need to use a specific zoneinfofile over the system zoneinfo, + instantiate a :class:`dateutil.zoneinfo.ZoneInfoFile` object and call + :func:`dateutil.zoneinfo.ZoneInfoFile.get(name)` instead. + + Use :func:`get_zonefile_instance` to retrieve an instance of the + dateutil-provided zoneinfo. + """ + warnings.warn("zoneinfo.gettz() will be removed in future versions, " + "to use the dateutil-provided zoneinfo files, instantiate a " + "ZoneInfoFile object and use ZoneInfoFile.zones.get() " + "instead. See the documentation for details.", + DeprecationWarning) + + if len(_CLASS_ZONE_INSTANCE) == 0: + _CLASS_ZONE_INSTANCE.append(ZoneInfoFile()) + return _CLASS_ZONE_INSTANCE[0].zones.get(name) + + +def gettz_db_metadata(): + """Get the zonefile metadata + + :returns: + A dictionary with the database metadata + + .. deprecated:: 2.6 + See deprecation warning in :func:`zoneinfo.gettz`. To get metadata, + query the attribute ``zoneinfo.ZoneInfoFile.metadata``. + """ + warnings.warn("zoneinfo.gettz_db_metadata() will be removed in future " + "versions, to use the dateutil-provided zoneinfo files, " + "ZoneInfoFile object and query the 'metadata' attribute " + "instead. See the documentation for details.", + DeprecationWarning) + + if len(_CLASS_ZONE_INSTANCE) == 0: + _CLASS_ZONE_INSTANCE.append(ZoneInfoFile()) + return _CLASS_ZONE_INSTANCE[0].metadata diff --git a/hug/lib/python3.12/site-packages/dateutil/zoneinfo/__pycache__/__init__.cpython-312.pyc b/hug/lib/python3.12/site-packages/dateutil/zoneinfo/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..93a7cb11e7600f784708a525a337001f9a24d5dd Binary files /dev/null and b/hug/lib/python3.12/site-packages/dateutil/zoneinfo/__pycache__/__init__.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/dateutil/zoneinfo/__pycache__/rebuild.cpython-312.pyc b/hug/lib/python3.12/site-packages/dateutil/zoneinfo/__pycache__/rebuild.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6468d5e8531d4f48c24606c67cd66ec32dbcc9a8 Binary files /dev/null and b/hug/lib/python3.12/site-packages/dateutil/zoneinfo/__pycache__/rebuild.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/dateutil/zoneinfo/rebuild.py b/hug/lib/python3.12/site-packages/dateutil/zoneinfo/rebuild.py new file mode 100644 index 0000000000000000000000000000000000000000..433de58f50c34c6cb26be56677f976b95a600ea2 --- /dev/null +++ b/hug/lib/python3.12/site-packages/dateutil/zoneinfo/rebuild.py @@ -0,0 +1,42 @@ +import warnings + +__all__ = ["rebuild", "RebuildDeprecationWarning"] + + +class RebuildDeprecationWarning(FutureWarning): + """Warning raised by :func:`rebuild`, which no longer does anything. + + A warnings filter turns this into an error by default. To keep calling + :func:`rebuild` as a no-op, filter it out, e.g.:: + + warnings.filterwarnings( + "ignore", category=dateutil.zoneinfo.rebuild.RebuildDeprecationWarning + ) + """ + + +def rebuild(filename, tag=None, format="gz", zonegroups=[], metadata=None): + """Formerly rebuilt the zoneinfo tarball bundled with ``dateutil``. + + ``dateutil`` no longer ships a zoneinfo tarball; it reads time zone data + from the system and from the `tzdata `_ + package instead. This function now does nothing except raise + :class:`RebuildDeprecationWarning`, which is an error unless a warnings + filter says otherwise. + + .. deprecated:: 3.0.0 + """ + # The filter is added at call time rather than at import time, so that it + # is still in place if the module was first imported inside a + # warnings.catch_warnings() block. It is appended, so any filter the user + # has installed for this category takes precedence. + warnings.filterwarnings( + "error", category=RebuildDeprecationWarning, append=True + ) + warnings.warn( + "dateutil.zoneinfo.rebuild() no longer does anything, because dateutil " + "no longer ships a zoneinfo tarball. Use the tzdata package or the " + "system time zone data instead.", + RebuildDeprecationWarning, + stacklevel=2, + ) diff --git a/hug/lib/python3.12/site-packages/distutils-precedence.pth b/hug/lib/python3.12/site-packages/distutils-precedence.pth new file mode 100644 index 0000000000000000000000000000000000000000..c659194195f07bd6f19b5522515551309af14a3d --- /dev/null +++ b/hug/lib/python3.12/site-packages/distutils-precedence.pth @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2638ce9e2500e572a5e0de7faed6661eb569d1b696fcba07b0dd223da5f5d224 +size 151 diff --git a/hug/lib/python3.12/site-packages/example.py b/hug/lib/python3.12/site-packages/example.py new file mode 100644 index 0000000000000000000000000000000000000000..504ce1f3a16250485d1a354b39cdb85a5842a7e4 --- /dev/null +++ b/hug/lib/python3.12/site-packages/example.py @@ -0,0 +1,170 @@ +################################################################################# +# Copyright (c) 2020, NVIDIA Corporation. All rights reserved. # +# # +# Redistribution and use in source and binary forms, with or without # +# modification, are permitted provided that the following conditions are met: # +# # +# * Redistributions of source code must retain the above copyright notice, # +# this list of conditions and the following disclaimer. # +# * Redistributions in binary form must reproduce the above copyright # +# notice, this list of conditions and the following disclaimer in the # +# documentation and/or other materials provided with the distribution. # +# * Neither the name of the NVIDIA Corporation nor the names of its # +# contributors may be used to endorse or promote products derived from # +# this software without specific prior written permission. # +# # +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE # +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF # +# THE POSSIBILITY OF SUCH DAMAGE. # +################################################################################# + +# +# Sample script to demonstrate the usage of NVML API python bindings +# + +# To Run: +# $ python ./example.py + +from pynvml import * + +# +# Helper function +# +def StrVirt(mode): + if mode == NVML_GPU_VIRTUALIZATION_MODE_NONE: + return "None"; + elif mode == NVML_GPU_VIRTUALIZATION_MODE_PASSTHROUGH: + return "Pass-Through"; + elif mode == NVML_GPU_VIRTUALIZATION_MODE_VGPU: + return "VGPU"; + elif mode == NVML_GPU_VIRTUALIZATION_MODE_HOST_VGPU: + return "Host VGPU"; + elif mode == NVML_GPU_VIRTUALIZATION_MODE_HOST_VSGA: + return "Host VSGA"; + else: + return "Unknown"; + +# +# Converts errors into string messages +# +def handleError(err): + if (err.value == NVML_ERROR_NOT_SUPPORTED): + return "N/A" + else: + return err.__str__() + +####### +def deviceQuery(): + + strResult = '' + try: + # + # Initialize NVML + # + nvmlInit() + + strResult += ' ' + str(nvmlSystemGetDriverVersion()) + '\n' + + deviceCount = nvmlDeviceGetCount() + strResult += ' ' + str(deviceCount) + '\n' + + for i in range(0, deviceCount): + handle = nvmlDeviceGetHandleByIndex(i) + + pciInfo = nvmlDeviceGetPciInfo(handle) + + strResult += ' \n' % pciInfo.busId + + strResult += ' ' + nvmlDeviceGetName(handle) + '\n' + + brandNames = {NVML_BRAND_UNKNOWN : "Unknown", + NVML_BRAND_QUADRO : "Quadro", + NVML_BRAND_TESLA : "Tesla", + NVML_BRAND_NVS : "NVS", + NVML_BRAND_GRID : "Grid", + NVML_BRAND_TITAN : "Titan", + NVML_BRAND_GEFORCE : "GeForce", + NVML_BRAND_NVIDIA_VAPPS : "NVIDIA Virtual Applications", + NVML_BRAND_NVIDIA_VPC : "NVIDIA Virtual PC", + NVML_BRAND_NVIDIA_VCS : "NVIDIA vGPU for Compute", + NVML_BRAND_NVIDIA_VWS : "NVIDIA RTX Virtual Workstation", + NVML_BRAND_NVIDIA_CLOUD_GAMING : "NVIDIA Cloud Gaming", + NVML_BRAND_NVIDIA_VGAMEDEV : "NVIDIA RTX Virtual Game Dev", + NVML_BRAND_QUADRO_RTX : "Quadro RTX", + NVML_BRAND_NVIDIA_RTX : "NVIDIA RTX", + NVML_BRAND_NVIDIA : "NVIDIA", + NVML_BRAND_GEFORCE_RTX : "GeForce RTX", + NVML_BRAND_TITAN_RTX : "TITAN RTX", + + } + + try: + # If nvmlDeviceGetBrand() succeeds it is guaranteed to be in the dictionary + brandName = brandNames[nvmlDeviceGetBrand(handle)] + except NVMLError as err: + brandName = handleError(err) + + strResult += ' ' + brandName + '\n' + + try: + serial = nvmlDeviceGetSerial(handle) + except NVMLError as err: + serial = handleError(err) + + strResult += ' ' + serial + '\n' + + try: + uuid = nvmlDeviceGetUUID(handle) + except NVMLError as err: + uuid = handleError(err) + + strResult += ' ' + uuid + '\n' + + strResult += ' \n' + try: + mode = StrVirt(nvmlDeviceGetVirtualizationMode(handle)) + except NVMLError as err: + mode = handleError(err) + strResult += ' ' + mode + '\n' + strResult += ' \n' + + try: + gridLicensableFeatures = nvmlDeviceGetGridLicensableFeatures(handle) + if gridLicensableFeatures.isGridLicenseSupported == 1: + strResult += ' \n' + for i in range(gridLicensableFeatures.licensableFeaturesCount): + if gridLicensableFeatures.gridLicensableFeatures[i].featureState == 0: + if nvmlDeviceGetVirtualizationMode(handle) == NVML_GPU_VIRTUALIZATION_MODE_PASSTHROUGH: + strResult += ' ' + 'NVIDIA Virtual Applications' + '\n' + strResult += ' ' + 'Licensed' + '\n' + else: + strResult += ' ' + gridLicensableFeatures.gridLicensableFeatures[i].productName + '\n' + strResult += ' ' + 'Unlicensed' + '\n' + else: + strResult += ' ' + gridLicensableFeatures.gridLicensableFeatures[i].productName + '\n' + strResult += ' ' + 'Licensed' + '\n' + strResult += ' \n' + except NVMLError as err: + gridLicensableFeatures = handleError(err) + + strResult += ' \n' + + except NVMLError as err: + strResult += 'example.py: ' + err.__str__() + '\n' + + nvmlShutdown() + + return strResult + +# If this is not exectued when module is imported +if __name__ == "__main__": + print(deviceQuery()) + diff --git a/hug/lib/python3.12/site-packages/filelock-4.2.4.dist-info/INSTALLER b/hug/lib/python3.12/site-packages/filelock-4.2.4.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..a1b589e38a32041e49332e5e81c2d363dc418d68 --- /dev/null +++ b/hug/lib/python3.12/site-packages/filelock-4.2.4.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/hug/lib/python3.12/site-packages/filelock-4.2.4.dist-info/METADATA b/hug/lib/python3.12/site-packages/filelock-4.2.4.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..826b5a93e0ebc4d46d1d2db52de9e2cce6cc2374 --- /dev/null +++ b/hug/lib/python3.12/site-packages/filelock-4.2.4.dist-info/METADATA @@ -0,0 +1,38 @@ +Metadata-Version: 2.5 +Name: filelock +Version: 4.2.4 +Summary: A platform independent file lock. +Project-URL: Documentation, https://py-filelock.readthedocs.io +Project-URL: Homepage, https://github.com/tox-dev/py-filelock +Project-URL: Source, https://github.com/tox-dev/py-filelock +Project-URL: Tracker, https://github.com/tox-dev/py-filelock/issues +Maintainer-email: Bernát Gábor +License-Expression: MIT +License-File: LICENSE +Keywords: application,cache,directory,log,user +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Programming Language :: Python :: 3.15 +Classifier: Topic :: Internet +Classifier: Topic :: Software Development :: Libraries +Classifier: Topic :: System +Requires-Python: >=3.11 +Description-Content-Type: text/markdown + +# filelock + +[![PyPI](https://img.shields.io/pypi/v/filelock)](https://pypi.org/project/filelock/) +[![Supported Python versions](https://img.shields.io/pypi/pyversions/filelock.svg)](https://pypi.org/project/filelock/) +[![Documentation status](https://readthedocs.org/projects/py-filelock/badge/?version=latest)](https://py-filelock.readthedocs.io/en/latest/?badge=latest) +[![Downloads](https://static.pepy.tech/badge/filelock/month)](https://pepy.tech/project/filelock) +[![check](https://github.com/tox-dev/py-filelock/actions/workflows/check.yaml/badge.svg)](https://github.com/tox-dev/py-filelock/actions/workflows/check.yaml) + +For more information checkout the [official documentation](https://py-filelock.readthedocs.io/en/latest/index.html). diff --git a/hug/lib/python3.12/site-packages/filelock-4.2.4.dist-info/RECORD b/hug/lib/python3.12/site-packages/filelock-4.2.4.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..2e66e8ebc3f286d3797bfa4fd5180f15ed0c552d --- /dev/null +++ b/hug/lib/python3.12/site-packages/filelock-4.2.4.dist-info/RECORD @@ -0,0 +1,52 @@ +filelock-4.2.4.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +filelock-4.2.4.dist-info/METADATA,sha256=2ZwPcqerhMkktzQBiiilABUF_g-WKAxEYrhkMT7d21A,1976 +filelock-4.2.4.dist-info/RECORD,, +filelock-4.2.4.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87 +filelock-4.2.4.dist-info/licenses/LICENSE,sha256=YIyJ1QYK6ZIa3M8yNmlbxlSplG4SMj72wCHfoE4pTUg,1088 +filelock/__init__.py,sha256=LaXgCbdErp-ugbr3o5bzpYJMBwi1hg0_hO4_vrwP1lk,5693 +filelock/__pycache__/__init__.cpython-312.pyc,, +filelock/__pycache__/_api.cpython-312.pyc,, +filelock/__pycache__/_async.cpython-312.pyc,, +filelock/__pycache__/_async_read_write.cpython-312.pyc,, +filelock/__pycache__/_descriptor.cpython-312.pyc,, +filelock/__pycache__/_error.cpython-312.pyc,, +filelock/__pycache__/_identity.cpython-312.pyc,, +filelock/__pycache__/_lease.cpython-312.pyc,, +filelock/__pycache__/_marker.cpython-312.pyc,, +filelock/__pycache__/_read_write.cpython-312.pyc,, +filelock/__pycache__/_soft.cpython-312.pyc,, +filelock/__pycache__/_soft_protocol.cpython-312.pyc,, +filelock/__pycache__/_strict.cpython-312.pyc,, +filelock/__pycache__/_unix.cpython-312.pyc,, +filelock/__pycache__/_util.cpython-312.pyc,, +filelock/__pycache__/_windows.cpython-312.pyc,, +filelock/__pycache__/asyncio.cpython-312.pyc,, +filelock/__pycache__/version.cpython-312.pyc,, +filelock/_api.py,sha256=OUugdO27gcc_gdfVOJYo0C6Bv6lvXL8Fv84vT7le378,106170 +filelock/_async.py,sha256=jSqb-YUze8cjKaNrlJ2qlMdfN9s1DZbgRMnozp4kDZs,20487 +filelock/_async_read_write.py,sha256=Ts6N_p5-UQLDoYHJKE67NfFV6ZpIcpX0cuu3S55dXzQ,17947 +filelock/_descriptor.py,sha256=Z81x98R7RbN5aHgaRzu48i7c0Ktsheze6Yg5uvWrU0Y,3277 +filelock/_error.py,sha256=sZNfFV3wacnrGCrnOipucPbnHN78NPt1IDnlg1nON1k,3235 +filelock/_identity.py,sha256=s2Kk4SQI0C5en9lHsPkWOVvGzuyY_wgZl8JRSOn63_8,17183 +filelock/_lease.py,sha256=eK0xXpuccVNM8o1BXSQByBE4TaIVYQSbSvfbbS6dReY,19173 +filelock/_marker.py,sha256=dVjalDpcpwqIRF7VUvFK2tkVZ4OCthjmmIinKy9jhFE,7896 +filelock/_read_write.py,sha256=5e0gtCHfLArSNKYH0jGdBXuD60m7stQV8KhuYgUvqQU,68071 +filelock/_soft.py,sha256=fXLlCQKHzDHaXCuwU8bXZw6qgmF1LEKRWVifxnRhRyk,33064 +filelock/_soft_protocol.py,sha256=D64pnXMlM7ysKpdx0fNXhJJgiMcS9T2Qwsu2GpPs2i0,218 +filelock/_soft_rw/__init__.py,sha256=JAMF4NA010_Jq-nEgHHZgWHZ7Of9wI3PKHg24mDdX50,452 +filelock/_soft_rw/__pycache__/__init__.cpython-312.pyc,, +filelock/_soft_rw/__pycache__/_async.cpython-312.pyc,, +filelock/_soft_rw/__pycache__/_protocol.cpython-312.pyc,, +filelock/_soft_rw/__pycache__/_storage.cpython-312.pyc,, +filelock/_soft_rw/__pycache__/_sync.cpython-312.pyc,, +filelock/_soft_rw/_async.py,sha256=v3q2adfqb9vQRwqhlctYdKZYlltJ4XutZcJ7IXuDy4M,16939 +filelock/_soft_rw/_protocol.py,sha256=O-ENv_XstRpIIDh4GnFoPNfkIg-xsDc8tVpsfBhrklA,30996 +filelock/_soft_rw/_storage.py,sha256=SbhhE8HQp7RcPo7BPnUNSEi8di2b6M7JwXHBh4EkuSc,13691 +filelock/_soft_rw/_sync.py,sha256=qVgup1GUig8fDSWLkl16z8_9UXojAcPJ_nZWl5BzIaI,37638 +filelock/_strict.py,sha256=iaEnG4QmSESjubZIhz0aq4Fq5cWmBy3STeim-8F6AZs,58570 +filelock/_unix.py,sha256=BLotMhNLFKTrW6zQ_qoI9qZGo3YqYfhGO_qP5W_p6Lw,10985 +filelock/_util.py,sha256=XW3K2JNfJapekS7YgUYBGiOFYcSf8A1cBqnKfU6jxzY,19280 +filelock/_windows.py,sha256=cDFvJN8dJtdDYjHYPt_kplvJCklD_yI7_UiaJkV-V80,38880 +filelock/asyncio.py,sha256=x3-lsFBKzEnPQR_2bQDoSD9ErdmV_3lw5rPyRq7AZc4,42865 +filelock/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +filelock/version.py,sha256=v35Uwrw-IApU2AA-2B77yCvOvNQ1Vh7gXvxfaJ9Vd14,520 diff --git a/hug/lib/python3.12/site-packages/filelock-4.2.4.dist-info/WHEEL b/hug/lib/python3.12/site-packages/filelock-4.2.4.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..39dc805011177e5b2a77e7afb5ec7187aeb3e34d --- /dev/null +++ b/hug/lib/python3.12/site-packages/filelock-4.2.4.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.32.4 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/hug/lib/python3.12/site-packages/filelock-4.2.4.dist-info/licenses/LICENSE b/hug/lib/python3.12/site-packages/filelock-4.2.4.dist-info/licenses/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..291919c0b6f41d014767f6c877af9f7595fcff99 --- /dev/null +++ b/hug/lib/python3.12/site-packages/filelock-4.2.4.dist-info/licenses/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Bernát Gábor and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/hug/lib/python3.12/site-packages/filelock/__init__.py b/hug/lib/python3.12/site-packages/filelock/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..15a6a55e2102657d1d2778790fbfd0d592cce3e3 --- /dev/null +++ b/hug/lib/python3.12/site-packages/filelock/__init__.py @@ -0,0 +1,174 @@ +""" +A platform independent file lock that supports the with-statement. + +.. autodata:: filelock.__version__ + :no-value: + +.. autodata:: filelock.OwnerMode + +.. autodata:: filelock.CompromiseReason + +""" + +from __future__ import annotations + +__lazy_modules__ = ["filelock._descriptor", "filelock._error", "importlib"] + +import sys +import warnings +from importlib import import_module +from typing import TYPE_CHECKING, Final + +from filelock._api import AcquireReturnProxy, BaseFileLock, CloseErrorPolicy, ContextErrorPolicy, LockOptions +from filelock._descriptor import lock_descriptor, unlock_descriptor +from filelock._error import LeaseSettingsMismatch, SoftFileLockLifetimeWarning, SoftFileLockProtocolError, Timeout +from filelock._unix import UnixFileLock +from filelock._unix import has_fcntl as _has_fcntl +from filelock._windows import WindowsFileLock + +# Keeps ``filelock.version`` the string 4.0.12 exposed rather than the submodule. +from filelock.version import version + +if TYPE_CHECKING: + from filelock import asyncio + from filelock._async_read_write import AsyncAcquireReadWriteReturnProxy, AsyncReadWriteLock + from filelock._lease import CompromiseReason, LeaseCompromise, SoftFileLease + from filelock._marker import MarkerSoftFileLock, OwnerMode, OwnerRecord + from filelock._read_write import ReadWriteLock + from filelock._soft import SoftFileLock + from filelock._soft_rw import AsyncAcquireSoftReadWriteReturnProxy, AsyncSoftReadWriteLock, SoftReadWriteLock + from filelock._strict import StrictSoftFileClaim, StrictSoftFileClaimState, StrictSoftFileLock + from filelock.asyncio import ( + AsyncAcquireReturnProxy, + AsyncSoftFileLease, + AsyncSoftFileLock, + AsyncStrictSoftFileLock, + AsyncUnixFileLock, + AsyncWindowsFileLock, + BaseAsyncFileLock, + ) + +#: version of the project as a string +__version__: Final[str] = version + +#: ``True`` when :func:`fcntl.flock` is available. Off Windows, :data:`FileLock` then resolves to +#: :class:`UnixFileLock`, otherwise to :class:`SoftFileLock`. Always ``False`` on Windows. +#: +#: :meta hide-value: +has_fcntl: Final[bool] = _has_fcntl + + +if sys.platform == "win32": # pragma: win32 cover + _FileLock: type[BaseFileLock] = WindowsFileLock +else: # pragma: win32 no cover # ruff:ignore[collapsible-else-if] # the else carries the win32 no-cover pragma + if has_fcntl: + _FileLock: type[BaseFileLock] = UnixFileLock + else: + from filelock._soft import SoftFileLock + + _FileLock = SoftFileLock + warnings.warn("only soft file lock is available", stacklevel=2) + +if TYPE_CHECKING: + FileLock = SoftFileLock + AsyncFileLock = AsyncSoftFileLock +else: + #: Alias for the lock, which should be used for the current platform. + FileLock = _FileLock + +# Importing asyncio and sqlite3 costs more than the rest of the package, so names that need them load on first access. +_LAZY_ATTRIBUTES: Final[dict[str, str]] = { + "AsyncAcquireReadWriteReturnProxy": "._async_read_write", + "AsyncAcquireReturnProxy": ".asyncio", + "AsyncAcquireSoftReadWriteReturnProxy": "._soft_rw", + "AsyncFileLock": ".asyncio", + "AsyncReadWriteLock": "._async_read_write", + "AsyncSoftFileLease": ".asyncio", + "AsyncSoftFileLock": ".asyncio", + "AsyncSoftReadWriteLock": "._soft_rw", + "AsyncStrictSoftFileLock": ".asyncio", + "AsyncUnixFileLock": ".asyncio", + "AsyncWindowsFileLock": ".asyncio", + "BaseAsyncFileLock": ".asyncio", + "CompromiseReason": "._lease", + "LeaseCompromise": "._lease", + "MarkerSoftFileLock": "._marker", + "OwnerMode": "._marker", + "OwnerRecord": "._marker", + "ReadWriteLock": "._read_write", + "SoftFileLease": "._lease", + "SoftFileLock": "._soft", + "SoftReadWriteLock": "._soft_rw", + "StrictSoftFileClaim": "._strict", + "StrictSoftFileClaimState": "._strict", + "StrictSoftFileLock": "._strict", + "asyncio": ".asyncio", +} + + +def __getattr__(name: str) -> object: + if (module_name := _LAZY_ATTRIBUTES.get(name)) is None: + msg = f"module {__name__!r} has no attribute {name!r}" + raise AttributeError(msg) + try: + module = import_module(module_name, __name__) + except ImportError: # pragma: lacks sqlite3 + if module_name not in {"._read_write", "._async_read_write"}: + raise + value = None + else: + if name == "asyncio": + value = module + else: + value = getattr(module, f"Async{_FileLock.__name__}" if name == "AsyncFileLock" else name) + globals()[name] = value + return value + + +def __dir__() -> list[str]: + return sorted({*globals(), *_LAZY_ATTRIBUTES}) + + +__all__ = [ + "AcquireReturnProxy", + "AsyncAcquireReadWriteReturnProxy", + "AsyncAcquireReturnProxy", + "AsyncAcquireSoftReadWriteReturnProxy", + "AsyncFileLock", + "AsyncReadWriteLock", + "AsyncSoftFileLease", + "AsyncSoftFileLock", + "AsyncSoftReadWriteLock", + "AsyncStrictSoftFileLock", + "AsyncUnixFileLock", + "AsyncWindowsFileLock", + "BaseAsyncFileLock", + "BaseFileLock", + "CloseErrorPolicy", + "CompromiseReason", + "ContextErrorPolicy", + "FileLock", + "LeaseCompromise", + "LeaseSettingsMismatch", + "LockOptions", + "MarkerSoftFileLock", + "OwnerMode", + "OwnerRecord", + "ReadWriteLock", + "SoftFileLease", + "SoftFileLock", + "SoftFileLockLifetimeWarning", + "SoftFileLockProtocolError", + "SoftReadWriteLock", + "StrictSoftFileClaim", + "StrictSoftFileClaimState", + "StrictSoftFileLock", + "Timeout", + "UnixFileLock", + "WindowsFileLock", + "__version__", + "asyncio", + "has_fcntl", + "lock_descriptor", + "unlock_descriptor", +] diff --git a/hug/lib/python3.12/site-packages/filelock/__pycache__/__init__.cpython-312.pyc b/hug/lib/python3.12/site-packages/filelock/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..76c700c42bee03f2aa372f125a5033d726213f38 Binary files /dev/null and b/hug/lib/python3.12/site-packages/filelock/__pycache__/__init__.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/filelock/__pycache__/_api.cpython-312.pyc b/hug/lib/python3.12/site-packages/filelock/__pycache__/_api.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..74c66eb7fd039fd9b174786570397a3223c7d2c0 --- /dev/null +++ b/hug/lib/python3.12/site-packages/filelock/__pycache__/_api.cpython-312.pyc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:228cffaa0359c96ef77da374611ffc1fc2167ce70ec382681b1242a5ef040863 +size 111999 diff --git a/hug/lib/python3.12/site-packages/filelock/__pycache__/_async.cpython-312.pyc b/hug/lib/python3.12/site-packages/filelock/__pycache__/_async.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5b88a9f9b54607c0c6251500c373c76ab32eae67 Binary files /dev/null and b/hug/lib/python3.12/site-packages/filelock/__pycache__/_async.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/filelock/__pycache__/_async_read_write.cpython-312.pyc b/hug/lib/python3.12/site-packages/filelock/__pycache__/_async_read_write.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a481b1614ff3cb62a865b25c57ae53a58dcc392b Binary files /dev/null and b/hug/lib/python3.12/site-packages/filelock/__pycache__/_async_read_write.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/filelock/__pycache__/_descriptor.cpython-312.pyc b/hug/lib/python3.12/site-packages/filelock/__pycache__/_descriptor.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9f1a933be9c4d08d63af6bc0cc5a142a8f0ebbaa Binary files /dev/null and b/hug/lib/python3.12/site-packages/filelock/__pycache__/_descriptor.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/filelock/__pycache__/_error.cpython-312.pyc b/hug/lib/python3.12/site-packages/filelock/__pycache__/_error.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2662cd8376dd8485efb448f146bfa03d804f864c Binary files /dev/null and b/hug/lib/python3.12/site-packages/filelock/__pycache__/_error.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/filelock/__pycache__/_identity.cpython-312.pyc b/hug/lib/python3.12/site-packages/filelock/__pycache__/_identity.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f789a3484a3e36d90cdda87e168b3fc92b4a56d1 Binary files /dev/null and b/hug/lib/python3.12/site-packages/filelock/__pycache__/_identity.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/filelock/__pycache__/_lease.cpython-312.pyc b/hug/lib/python3.12/site-packages/filelock/__pycache__/_lease.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a8bd8ac869a56a7203623bf964aa0b8b80d04efc Binary files /dev/null and b/hug/lib/python3.12/site-packages/filelock/__pycache__/_lease.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/filelock/__pycache__/_marker.cpython-312.pyc b/hug/lib/python3.12/site-packages/filelock/__pycache__/_marker.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..26262e4c3e4d58dc8d881a89fd6138b254866f70 Binary files /dev/null and b/hug/lib/python3.12/site-packages/filelock/__pycache__/_marker.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/filelock/__pycache__/_read_write.cpython-312.pyc b/hug/lib/python3.12/site-packages/filelock/__pycache__/_read_write.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..38efd5d656c9019a9f276c7be662b8b10ec28340 Binary files /dev/null and b/hug/lib/python3.12/site-packages/filelock/__pycache__/_read_write.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/filelock/__pycache__/_soft.cpython-312.pyc b/hug/lib/python3.12/site-packages/filelock/__pycache__/_soft.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e8eb7255349bf7eb92e77159f09d5782341b4b38 Binary files /dev/null and b/hug/lib/python3.12/site-packages/filelock/__pycache__/_soft.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/filelock/__pycache__/_soft_protocol.cpython-312.pyc b/hug/lib/python3.12/site-packages/filelock/__pycache__/_soft_protocol.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a53d924db49844d5db880f11220b8a92174e2c0c Binary files /dev/null and b/hug/lib/python3.12/site-packages/filelock/__pycache__/_soft_protocol.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/filelock/__pycache__/_strict.cpython-312.pyc b/hug/lib/python3.12/site-packages/filelock/__pycache__/_strict.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f64d26c6c94068f3f03094b3610afef40e00fcfa Binary files /dev/null and b/hug/lib/python3.12/site-packages/filelock/__pycache__/_strict.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/filelock/__pycache__/_unix.cpython-312.pyc b/hug/lib/python3.12/site-packages/filelock/__pycache__/_unix.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5c89e2f44b8e0a60de697cbf5437981f4658f3d5 Binary files /dev/null and b/hug/lib/python3.12/site-packages/filelock/__pycache__/_unix.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/filelock/__pycache__/_util.cpython-312.pyc b/hug/lib/python3.12/site-packages/filelock/__pycache__/_util.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1961ba9b8277637e33118b7fe30da94e09d8c1da Binary files /dev/null and b/hug/lib/python3.12/site-packages/filelock/__pycache__/_util.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/filelock/__pycache__/_windows.cpython-312.pyc b/hug/lib/python3.12/site-packages/filelock/__pycache__/_windows.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f154498bfbe589b56f37ceeec57aef14c0eb7e2b Binary files /dev/null and b/hug/lib/python3.12/site-packages/filelock/__pycache__/_windows.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/filelock/__pycache__/asyncio.cpython-312.pyc b/hug/lib/python3.12/site-packages/filelock/__pycache__/asyncio.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f4d31b9824b19721ac2acc4fc0178a0d0a25e7ae Binary files /dev/null and b/hug/lib/python3.12/site-packages/filelock/__pycache__/asyncio.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/filelock/__pycache__/version.cpython-312.pyc b/hug/lib/python3.12/site-packages/filelock/__pycache__/version.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..03f169448664093347117a8b05edb389de611530 Binary files /dev/null and b/hug/lib/python3.12/site-packages/filelock/__pycache__/version.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/filelock/_api.py b/hug/lib/python3.12/site-packages/filelock/_api.py new file mode 100644 index 0000000000000000000000000000000000000000..3dbf52ccbeb70abc99e4f43b1e0f1cf039560bfc --- /dev/null +++ b/hug/lib/python3.12/site-packages/filelock/_api.py @@ -0,0 +1,2266 @@ +from __future__ import annotations + +__lazy_modules__ = [ + "atexit", + "decimal", + "filelock._error", + "filelock._util", + "inspect", + "math", + "numbers", + "secrets", + "types", + "warnings", +] + +import atexit +import contextlib +import inspect +import logging +import math +import os +import secrets +import stat +import sys +import time +import types +import warnings +from abc import ABCMeta, abstractmethod +from collections.abc import Callable, Hashable +from contextlib import contextmanager +from dataclasses import dataclass, fields +from decimal import Decimal +from functools import cache, partial +from itertools import count, starmap +from numbers import Real +from threading import TIMEOUT_MAX, Condition, RLock, Thread, current_thread, get_ident, local +from typing import TYPE_CHECKING, Final, Generic, Literal, NoReturn, TypedDict, TypeVar, cast +from weakref import WeakKeyDictionary, WeakValueDictionary, ref + +from filelock._error import SoftFileLockLifetimeWarning, Timeout +from filelock._util import ensure_directory_exists + +#: No explicit file permission mode was passed. Lock files then open with 0o666 so umask and default ACLs pick +#: the final permissions, and fchmod is skipped to preserve POSIX default ACL inheritance. +_UNSET_FILE_MODE: Final[int] = -1 + +_PERMISSION_BITS: Final[int] = stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO + +#: Ceiling on the retry counter used as a power of two, so a long contended wait cannot overflow the backoff multiply. +_MAX_BACKOFF_EXPONENT: Final[int] = 20 + +#: How a context manager reconciles a body failure with a release failure on exit (see the property of this name). +ContextErrorPolicy = Literal["chain", "group"] +_CONTEXT_ERROR_POLICIES: Final[frozenset[str]] = frozenset({"chain", "group"}) + +#: What a descriptor-owning backend does with an ``os.close`` failure after relinquishing ownership (see the property). +CloseErrorPolicy = Literal["default", "raise", "suppress"] +_CLOSE_ERROR_POLICIES: Final[frozenset[str]] = frozenset({"default", "raise", "suppress"}) + +if TYPE_CHECKING: + from collections.abc import Generator, Mapping + from threading import Lock + from types import FrameType, TracebackType + from typing import Protocol + + from _typeshed import Unused + + from filelock._lease import _LeaseClaim + from filelock._read_write import ReadWriteLock + from filelock._soft_rw import SoftReadWriteLock + + class _ForkResettable(Protocol): + def _reset_after_fork_in_child(self) -> None: ... + + class _ForkDescriptorOwner(Protocol): + def _descriptors_for_fork(self) -> tuple[tuple[int, tuple[int, int] | None], ...]: ... + + # Matched against the class object itself rather than `type[...]` of it. A metaclass supplies this method to the + # class while leaving instances without it, so a `type[_ForkResettableClass]` bound rejects `ReadWriteLock`. + class _ForkResettableClass(Protocol): + def _reset_class_after_fork(self) -> None: ... + + class _RegisterAtFork(Protocol): + def __call__( + self, + *, + before: Callable[[], None] | None = None, + after_in_parent: Callable[[], None] | None = None, + after_in_child: Callable[[], None] | None = None, + ) -> None: ... + + from typing import Self + +_LOGGER: Final[logging.Logger] = logging.getLogger("filelock") +_REGISTER_AT_FORK: Final[_RegisterAtFork | None] = cast("_RegisterAtFork | None", getattr(os, "register_at_fork", None)) +_HAS_REGISTER_AT_FORK: Final[bool] = _REGISTER_AT_FORK is not None + +_ExtraValue = TypeVar("_ExtraValue") +_MarkerValue = TypeVar("_MarkerValue") +_SubclassValue = TypeVar("_SubclassValue") +_LockInitValue = float | int | bool | str | None | Callable[[int], None] + + +class LockOptions(TypedDict, total=False): + """Every option the metaclass forwards, so a subclass adding its own can still type what it passes through.""" + + timeout: float + mode: int + thread_local: bool + blocking: bool + is_singleton: bool + poll_interval: float + lifetime: float | None + context_error_policy: ContextErrorPolicy + close_error_policy: CloseErrorPolicy + fallback_to_soft: bool + preserve_lock_file: bool + on_acquired: Callable[[int], None] | None + + +def _is_exception_group(error: BaseException) -> bool: + return isinstance(error, BaseExceptionGroup) + + +def _raise_grouped_errors( + message: str, + first_error: BaseException, + second_error: BaseException, + *additional_errors: BaseException, + marker: tuple[str, _MarkerValue] | None = None, +) -> NoReturn: + errors = (first_error, second_error, *additional_errors) + _detach_grouped_contexts(errors) + group: Final = BaseExceptionGroup(message, errors) + if marker is not None: + setattr(group, marker[0], marker[1]) + raise group from None + + +def _detach_grouped_contexts(errors: tuple[BaseException, ...]) -> None: + seen: set[int] = set() + pending = list(errors) + while pending: + error = pending.pop() + if id(error) in seen: + continue + seen.add(id(error)) + if (context := error.__context__) is not None and ( + context is error + or _same_exception_tree(error, context) + or any(context is root or _contains_exception(root, context) for root in errors) + ): + error.__context__ = None + elif context is not None: + pending.append(context) + if error.__cause__ is not None: + pending.append(error.__cause__) + if _is_exception_group(error): + pending.extend(cast("_ExceptionGroupProtocol", error).exceptions) + + +def _same_exception_tree(first: BaseException, second: BaseException) -> bool: + pending = [(first, second)] + seen: set[tuple[int, int]] = set() + while pending: + first_error, second_error = pending.pop() + if first_error is second_error: + continue + if (pair := (id(first_error), id(second_error))) in seen: + continue + seen.add(pair) + if ( + type(first_error) is not type(second_error) + or not _is_exception_group(first_error) + or not _is_exception_group(second_error) + ): + return False + first_group = cast("_ExceptionGroupProtocol", first_error) + second_group = cast("_ExceptionGroupProtocol", second_error) + if first_group.message != second_group.message or len(first_group.exceptions) != len(second_group.exceptions): + return False + pending.extend(zip(first_group.exceptions, second_group.exceptions, strict=True)) + return True + + +def _contains_exception(error: BaseException, target: BaseException | None) -> bool: + if target is None or not _is_exception_group(error): + return False + pending = list(cast("_ExceptionGroupProtocol", error).exceptions) + seen: set[int] = set() + while pending: + child = pending.pop() + if child is target: + return True + if id(child) in seen: + continue + seen.add(id(child)) + if _is_exception_group(child): + pending.extend(cast("_ExceptionGroupProtocol", child).exceptions) + return False + + +def _append_exception_context(error: BaseException, context: BaseException) -> None: + if _exception_graph_contains(error, context) or _exception_graph_contains(context, error): + return + if error.__context__ is None: + error.__context__ = context + return + tail = error + seen: set[int] = set() + while id(tail) not in seen: + seen.add(id(tail)) + if (next_error := tail.__cause__ if tail.__cause__ is not None else tail.__context__) is None: + tail.__context__ = context + return + tail = next_error + + +def _exception_graph_contains(error: BaseException, target: BaseException) -> bool: + pending = [error] + seen: set[int] = set() + while pending: + current = pending.pop() + if current is target: + return True + if id(current) in seen: # pragma: no cover - arbitrary caller exceptions can contain cycles + continue + seen.add(id(current)) + if current.__cause__ is not None: + pending.append(current.__cause__) + if current.__context__ is not None: + pending.append(current.__context__) + if _is_exception_group(current): + pending.extend(cast("_ExceptionGroupProtocol", current).exceptions) + return False + + +def _grouped_errors( + error: BaseException, message: str, marker: tuple[str, _MarkerValue] +) -> tuple[BaseException, ...] | None: + if not _is_exception_group(error): + return None + group = cast("_ExceptionGroupProtocol", error) + return group.exceptions if group.message == message and getattr(group, marker[0], None) is marker[1] else None + + +if TYPE_CHECKING: + + class _ExceptionGroupProtocol(Protocol): + @property + def message(self) -> str: ... + + @property + def exceptions(self) -> tuple[BaseException, ...]: ... + + +def _raise_chained_errors(first_error: BaseException, second_error: BaseException | None = None) -> NoReturn: + if second_error is None: + first_context = first_error.__context__ + try: + raise first_error # ruff:ignore[raise-within-try] # the handler restores caller-supplied context before propagation + except BaseException: + first_error.__context__ = first_context + raise + if (second_context := second_error.__context__) is not None and second_context is not first_error: + _detach_exception_context(second_context, first_error) + _append_exception_context(first_error, second_context) + first_context = first_error.__context__ + try: + raise first_error # ruff:ignore[raise-within-try] # the second raise needs this error as implicit context + except BaseException: # ruff:ignore[blind-except] # first_error may be a control-flow exception + first_error.__context__ = first_context + try: + raise second_error # ruff:ignore[raise-within-try] # the handler makes the chain interpreter-independent + except BaseException: + second_error.__context__ = first_error + first_error.__context__ = first_context + raise + + +def _detach_exception_context(error: BaseException, target: BaseException) -> None: + pending = [error] + seen: set[int] = set() + while pending: + current = pending.pop() + if id(current) in seen: + continue + seen.add(id(current)) + if current.__context__ is target: + current.__context__ = None + elif current.__context__ is not None: + pending.append(current.__context__) + if current.__cause__ is not None: + pending.append(current.__cause__) + if _is_exception_group(current): + pending.extend(cast("_ExceptionGroupProtocol", current).exceptions) + + +def _raise_body_and_release(body_error: BaseException, release_error: BaseException) -> NoReturn: + # Group mode: surface the body failure and the release failure as sibling leaves instead of letting one hide in the + # other's __context__. BaseExceptionGroup returns a plain ExceptionGroup when both leaves subclass Exception, so + # ``except*`` and ``except Exception`` still catch them; a BaseException leaf (KeyboardInterrupt, CancelledError) + # keeps the group outside ordinary handlers. ``from None`` stops the group itself gaining a redundant __context__. + _raise_grouped_errors("lock body and release both failed", body_error, release_error) + + +def _raise_cleanup_errors( + message: str, + primary_error: BaseException, + *cleanup_errors: BaseException | None, +) -> NoReturn: + _raise_grouped_errors( + message, + primary_error, + *(error for error in cleanup_errors if error is not None), + ) + + +# On Windows os.path.realpath calls CreateFileW with share_mode=0, which blocks concurrent DeleteFileW and causes +# livelocks under threaded contention with SoftFileLock. os.path.abspath is purely string-based and avoids this. +_resolve_dir: Final[Callable[[str], str]] = os.path.abspath if sys.platform == "win32" else os.path.realpath + + +def _canonical(path: str | os.PathLike[str]) -> str: + """ + Return one stable key for *path*, collapsing equivalent spellings without following a final symlink. + + Relative, absolute, and ``./`` spellings of one lock file must map to a single singleton instance, deadlock-registry + entry, and removal key. Resolving the whole path with ``realpath`` would follow a final symlink and alias a lock + target the backend deliberately rejects, so the registry identity would differ from the backend's. Resolving only + the parent directory and re-appending the literal final component collapses the equivalent spellings while keeping a + final symlink a distinct key. On Windows the parent is resolved with ``abspath`` so junctions and reparse points are + not followed either. + """ + parent, name = os.path.split(os.fspath(path)) + return os.path.join(_resolve_dir(parent or os.curdir), name) # ruff:ignore[os-path-join] # string join matches abspath/realpath + + +class _ThreadLocalRegistry(local): + def __init__(self) -> None: + super().__init__() + self.held: dict[Hashable, int] = {} + + +_registry: Final[_ThreadLocalRegistry] = _ThreadLocalRegistry() + + +_T = TypeVar("_T", bound="BaseFileLock") +_LockT = TypeVar("_LockT", bound="BaseFileLock | ReadWriteLock | SoftReadWriteLock") + + +class FileLockMeta(ABCMeta): + _instances: WeakValueDictionary[str, BaseFileLock] + _instances_lock: RLock + _instances_under_construction: set[str] + + def __call__( # ruff:ignore[too-many-arguments] # forwards the public constructor's documented parameters + cls: type[_T], + lock_file: str | os.PathLike[str], + timeout: float = -1, + mode: int = _UNSET_FILE_MODE, + thread_local: bool = True, # ruff:ignore[boolean-type-hint-positional-argument, boolean-default-value-positional-argument] # public API: positional bool kept for backwards compatibility + *, + blocking: bool = True, + is_singleton: bool = False, + poll_interval: float = 0.05, + lifetime: float | None = None, + context_error_policy: ContextErrorPolicy = "chain", + close_error_policy: CloseErrorPolicy = "default", + fallback_to_soft: bool = True, + preserve_lock_file: bool = False, + on_acquired: Callable[[int], None] | None = None, + **kwargs: _ExtraValue, + ) -> _T: + _ensure_current_process() + _resolve_lazy_imports_before_shutdown() + lifetime = _resolve_lifetime(lifetime, cls, stacklevel=cls._constructor_lifetime_warning_stacklevel) + poll_interval = _resolve_poll_interval(poll_interval) + # Validate before building the instance: a raise inside __init__ would leave a half-constructed object whose + # __del__ then trips over the missing context. + _check_blocking(blocking) + # nan is left to acquire, which knows whether the call blocks; a non-blocking call ignores the timeout. + timeout = _seconds("timeout", timeout) + _check_mode(mode, cls.__name__) + context_error_policy = _resolve_context_error_policy(context_error_policy) + close_error_policy = _resolve_close_error_policy(close_error_policy) + preserve_lock_file = _resolve_preserve_lock_file( + preserve=preserve_lock_file, supported=cls._preserve_lock_file_supported, cls_name=cls.__name__ + ) + on_acquired = _resolve_on_acquired(on_acquired, supported=cls._on_acquired_supported, cls_name=cls.__name__) + # Checked before the singleton lookup so a cache hit rejects a misspelled option just like a fresh build does. + params: Final = _supported_init_params( + cls, + { + "timeout": timeout, + "mode": mode, + "thread_local": thread_local, + "blocking": blocking, + "is_singleton": is_singleton, + "poll_interval": poll_interval, + "lifetime": lifetime, + "context_error_policy": context_error_policy, + "close_error_policy": close_error_policy, + "fallback_to_soft": fallback_to_soft, + "preserve_lock_file": preserve_lock_file, + "on_acquired": on_acquired, + **kwargs, + }, + ) + if not is_singleton: + return super().__call__(lock_file, **params) + + # Look up, build and store under one lock. Without it two threads racing the first construction for a + # path both miss the cache and each build their own instance, so callers relying on is_singleton for + # reentrant locking across instances end up with two "singletons" and acquire()'s deadlock check then + # rejects a legitimate reentrant acquire; the unguarded writes to the WeakValueDictionary are a data + # race besides. ReadWriteLock and SoftReadWriteLock already guard their singleton caches this way. + # Key the cache on the canonical form so equivalent spellings of one path share a singleton, and it matches the + # deadlock-registry key acquire() uses. + singleton_key = _canonical(lock_file) + with cls._instances_lock: + # A cached lock resolves its relative path at each acquisition and pins it for the hold. After a chdir, or + # once a parent symlink points elsewhere, an unheld lock may name another file than this key; a held one + # names the file it pinned. + if (instance := cls._instances.get(singleton_key)) is None or _canonical( + instance._lock_path # ruff:ignore[private-member-access] # the metaclass owns the instance it returns + ) != singleton_key: + if singleton_key in cls._instances_under_construction: # pragma: needs fork + msg = f"Singleton lock construction is already active for {lock_file!s}" + raise RuntimeError(msg) + construction_registry = cls._instances_under_construction + construction_pid = os.getpid() + construction_registry.add(singleton_key) + try: + instance = super().__call__(lock_file, **params) + finally: + construction_registry.discard(singleton_key) + if os.getpid() != construction_pid: # pragma: needs fork + msg = "Lock construction cannot continue after fork; construct a new lock in the child" + raise RuntimeError(msg) + cls._instances[singleton_key] = instance + instance._singleton_key = singleton_key # ruff:ignore[private-member-access] # the cache entry the finalizer looks up + return instance + + non_matching_params = { + name: (str(passed_param), str(set_param)) + for name, (passed_param, set_param) in { + "thread_local": (thread_local, instance.is_thread_local()), + # Compare the wait, not the value: nan never equals itself, and -1, -5 and inf all mean no limit. + "timeout": ( + _timeout_behavior(timeout, any_negative=True), + _timeout_behavior(instance.timeout, any_negative=True), + ), + "mode": (mode, instance._context.mode), # ruff:ignore[private-member-access] # compares against the managed instance's own context + "blocking": (blocking, instance.blocking), + "poll_interval": (poll_interval, instance.poll_interval), + "lifetime": (lifetime, instance.lifetime), + "context_error_policy": (context_error_policy, instance.context_error_policy), + "close_error_policy": (close_error_policy, instance.close_error_policy), + "fallback_to_soft": (fallback_to_soft, instance.fallback_to_soft), + "preserve_lock_file": (preserve_lock_file, instance.preserve_lock_file), + }.items() + if passed_param != set_param + } + # The metaclass owns the instance it returns, so reading the subclass hook stays inside this module's contract. + non_matching_params.update(instance._singleton_extra_mismatches(kwargs)) # ruff:ignore[private-member-access] + # Callables compare by identity, not equality: two equal callables can close over different state, so a + # singleton must reject a different hook object even if it compares equal. Keep it out of the scalar dict above. + hook_mismatch = on_acquired is not instance.on_acquired + if not non_matching_params and not hook_mismatch: + return instance # ty: ignore[invalid-return-type] # https://github.com/astral-sh/ty/issues/3231 + + msg = "Singleton lock instances cannot be initialized with differing arguments" + msg += "\nNon-matching arguments: " + for param_name, (passed_param, set_param) in non_matching_params.items(): + msg += f"\n\t{param_name} (existing lock has {set_param} but {passed_param} was passed)" + if hook_mismatch: + msg += f"\n\ton_acquired (existing lock has {instance.on_acquired} but {on_acquired} was passed)" + raise ValueError(msg) + + +def _resolve_lifetime(lifetime: float | None, cls: type[BaseFileLock], *, stacklevel: int) -> float | None: + """ + Validate ``lifetime`` and drop a value the backend cannot honor. + + ``lifetime`` is a deliberate age-based lease: a lock file older than ``lifetime`` is broken even while its holder is + still alive. Existence locks (:class:`SoftFileLock`) implement that behavior by unlinking a reclaimable pathname, + which can overlap a live holder. A native OS lock lives on the inode, so unlinking the pathname by age cannot revoke + the kernel lock; a contender would lock a fresh inode and overlap the live holder (#590). Ignore the request with a + warning rather than accept a setting that breaks mutual exclusion. + """ + if lifetime is None: + return None + # An int stays exact: no float holds every int 4.0.12 accepted here. + if isinstance(lifetime, bool) or not isinstance(lifetime, int): + lifetime = _seconds("lifetime", lifetime) + if lifetime < 0 or (isinstance(lifetime, float) and not math.isfinite(lifetime)): + msg = f"lifetime must be finite and non-negative, not {lifetime!r}" + raise ValueError(msg) + if not cls._lifetime_supported: + warnings.warn( + f"lifetime is ignored for {cls.__name__}: {cls._lifetime_unsupported_reason}; " + f"only SoftFileLock supports lifetime-based expiry", + stacklevel=stacklevel, + ) + return None + if cls._lifetime_replacements is not None: + strict_lock, lease = cls._lifetime_replacements + warnings.warn( + f"{cls.__name__}(lifetime=...) uses age-based expiry and can overlap a live holder; " + f"use {lease} for expiry or {strict_lock} for fail-closed locking", + SoftFileLockLifetimeWarning, + stacklevel=stacklevel, + ) + return lifetime + + +def _resolve_poll_interval(poll_interval: float) -> float: + """Validate ``poll_interval`` used for ``time.sleep`` / ``asyncio.sleep`` retries.""" + if (poll_interval := _seconds("poll_interval", poll_interval)) < 0 or not math.isfinite(poll_interval): + msg = f"poll_interval must be finite and non-negative, not {poll_interval!r}" + raise ValueError(msg) + _check_timeout_max("poll_interval", poll_interval) + return poll_interval + + +def _check_blocking(blocking: bool) -> None: # ruff:ignore[boolean-type-hint-positional-argument] # validates the option's own value + if not isinstance(blocking, bool): + msg = f"blocking must be a bool, not {type(blocking).__name__}" + raise TypeError(msg) + + +def _seconds(name: str, value: float) -> float: + # 4.0.12 accepted Decimal and Fraction; a bool is a typo, and a string would only fail later. + if isinstance(value, bool) or not isinstance(value, (Real, Decimal)): + msg = f"{name} must be a number of seconds, not {type(value).__name__}" + raise TypeError(msg) + # float() refuses a signaling NaN, which means no number of seconds just as a quiet one does. + if isinstance(value, Decimal) and value.is_snan(): + return math.nan + try: + seconds: Final = float(value) + except OverflowError: + msg = f"{name} is too large to be a number of seconds: {value!r}" + raise ValueError(msg) from None + # float() rounds a tiny negative to -0.0, which compares as zero, yet timeout and lifetime checks read the sign. + return -math.ulp(0.0) if seconds == 0 and value < 0 else seconds + + +def _check_mode(mode: int, cls_name: str) -> None: + if mode == _UNSET_FILE_MODE: + return + if isinstance(mode, bool) or not isinstance(mode, int): + msg = f"mode must be an int, not {type(mode).__name__}" + raise TypeError(msg) + # A lock file is never executed, so setuid, setgid and sticky have no meaning on it and only widen its permissions. + if not 0 <= mode <= _PERMISSION_BITS: + msg = f"mode={mode:#o} must be within 0o000-0o777 for {cls_name}" + raise ValueError(msg) + # A lock reopens, reads, or deletes the files it creates, so a mode that denies the owner read or write fails later + # and for good. + if ~mode & (stat.S_IRUSR | stat.S_IWUSR): + msg = f"mode={mode:#o} must grant the owner read and write for {cls_name}" + raise ValueError(msg) + + +def _resolve_context_error_policy(policy: str) -> ContextErrorPolicy: + if policy not in _CONTEXT_ERROR_POLICIES: + msg = f"context_error_policy must be 'chain' or 'group', got {policy!r}" + raise ValueError(msg) + return cast("ContextErrorPolicy", policy) + + +def _resolve_close_error_policy(policy: str) -> CloseErrorPolicy: + if policy not in _CLOSE_ERROR_POLICIES: + msg = f"close_error_policy must be 'default', 'raise', or 'suppress', got {policy!r}" + raise ValueError(msg) + return cast("CloseErrorPolicy", policy) + + +def _resolve_preserve_lock_file(*, preserve: bool, supported: bool, cls_name: str) -> bool: + # An existence lock unlinks its marker to release, so preserving the pathname would defeat unlocking. Reject the + # request rather than silently ignore it, since a caller asking for a stable identity must know it cannot be kept. + if preserve and not supported: + msg = f"preserve_lock_file=True is not supported by {cls_name}: unlinking its marker is how it releases" + raise ValueError(msg) + return preserve + + +def _resolve_on_acquired( + on_acquired: Callable[[int], None] | None, *, supported: bool, cls_name: str +) -> Callable[[int], None] | None: + if on_acquired is None: + return None + # An existence lock stores protocol state in its marker, so a caller writing through the descriptor would corrupt + # stale detection and ownership metadata; only native locks lend out the descriptor. + if not supported: + msg = f"on_acquired is not supported by {cls_name}: only native locks expose the lock descriptor" + raise ValueError(msg) + return on_acquired + + +def _supported_init_params( + cls: type[BaseFileLock], params: dict[str, _LockInitValue | _ExtraValue] +) -> dict[str, _LockInitValue | _ExtraValue]: + if (model := _init_parameter_model(cls)).accepts_kwargs: + return params + if unsupported := sorted( + name + for name, value in params.items() + if name not in model.accepted_params + and ((parameter := model.default_params.get(name)) is None or value != parameter.default) + ): + msg = f"{cls.__name__} does not support non-default lock options: {', '.join(unsupported)}" + raise TypeError(msg) + # virtualenv narrows a BaseFileLock descendant's signature; omit base defaults it does not accept (#340). + return {name: value for name, value in params.items() if name in model.accepted_params} + + +def _timeout_behavior(timeout: float, *, any_negative: bool) -> float | str: + # inf, and for a BaseFileLock any negative value, wait without a limit like -1. + if math.isnan(timeout): + return "nan" + return -1.0 if timeout == math.inf or (any_negative and timeout < 0) else timeout + + +def _check_timeout_max(name: str, value: float) -> None: + # Lock.acquire, Event.wait and time.sleep raise OverflowError past this bound, which is only ~49.7 days on Windows. + if value > TIMEOUT_MAX: + msg = f"{name} must not exceed threading.TIMEOUT_MAX ({TIMEOUT_MAX}), got {value!r}" + raise ValueError(msg) + + +_INIT_PARAMETER_MODELS: Final[WeakKeyDictionary[type[BaseFileLock], _InitParameterModel]] = WeakKeyDictionary() + + +def _init_parameter_model(cls: type[BaseFileLock]) -> _InitParameterModel: + # A strong cache would keep dynamically created subclasses alive for the process lifetime. + with _fork_transition(), _FORK_STATE.parameter_models_lock: + if (model := _INIT_PARAMETER_MODELS.get(cls)) is None: + parameters = inspect.signature(cls.__init__).parameters.values() + model = _InitParameterModel( + accepted_params=frozenset( + parameter.name + for parameter in parameters + if parameter.kind in {inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY} + ), + accepts_kwargs=any(parameter.kind is inspect.Parameter.VAR_KEYWORD for parameter in parameters), + default_params={ + name: parameter + for name, parameter in inspect.signature(type(cls).__call__).parameters.items() + if parameter.default is not inspect.Parameter.empty + }, + ) + _INIT_PARAMETER_MODELS[cls] = model + return model + + +@dataclass(frozen=True) +class _InitParameterModel: + accepted_params: frozenset[str] + accepts_kwargs: bool + default_params: dict[str, inspect.Parameter] + + +class BaseFileLock(contextlib.ContextDecorator, metaclass=FileLockMeta): # ruff:ignore[too-many-public-methods] # public config properties + """ + Abstract base class for a file lock object. + + Provides the common reentrant API and state management. Subclasses implement the locking mechanism + (:class:`UnixFileLock `, :class:`WindowsFileLock `, + :class:`SoftFileLock `). + + """ + + _instances: WeakValueDictionary[str, BaseFileLock] + _instances_lock: RLock + _instances_under_construction: set[str] + + #: How the cross-instance deadlock message names the conflicting holder; the async subclass says "task". + _deadlock_holder_desc: str = "FileLock instance in this thread" + + #: Whether an age-based :attr:`lifetime` lease may break this lock. Only existence locks set it (they reclaim by + #: unlinking a pathname); native OS locks leave it ``False`` since a kernel lock cannot be revoked by file age. + _lifetime_supported: bool = False + + #: Strict-lock and lease replacements for a backend with legacy age-based expiry. + _lifetime_replacements: tuple[str, str] | None = None + + #: Why a backend that refuses ``lifetime`` cannot honor it, named in the warning that drops the value. + _lifetime_unsupported_reason: str = "a native OS lock cannot be broken safely by file age" + + #: Async construction adds one metaclass frame before lifetime validation. + _constructor_lifetime_warning_stacklevel: int = 3 + + #: Whether :attr:`preserve_lock_file` may be ``True``. Native locks keep the pathname on release, so they support + #: it; existence locks unlink their marker to release and reject it. + _preserve_lock_file_supported: bool = True + + #: Whether an :attr:`on_acquired` hook may be set. Native locks lend the descriptor out; existence locks keep + #: protocol state in the marker and reject it. + _on_acquired_supported: bool = True + + #: Ceiling in seconds on the jittered backoff between contended acquisition retries. ``0`` keeps the fixed + #: poll cadence; a multi-file backend sets it so contending processes desynchronize instead of livelocking. + _poll_backoff_cap: float = 0.0 + + #: The singleton cache key of this instance, or ``None`` outside the cache. + _singleton_key: str | None = None + + def __init_subclass__(cls, **kwargs: _SubclassValue) -> None: + """Give each lock subclass its own singleton registry and lock.""" + super().__init_subclass__(**kwargs) + cls._instances = WeakValueDictionary() + cls._instances_lock = RLock() + cls._instances_under_construction = set() + _register_fork_class(cls) + + @classmethod + def _reset_class_after_fork(cls) -> None: # pragma: forked child + cls._instances = WeakValueDictionary() + cls._instances_lock = RLock() + cls._instances_under_construction = set() + + def __init__( # ruff:ignore[too-many-arguments] # public constructor: one parameter per documented lock option + self, + lock_file: str | os.PathLike[str], + timeout: float = -1, + mode: int = _UNSET_FILE_MODE, + thread_local: bool = True, # ruff:ignore[boolean-type-hint-positional-argument, boolean-default-value-positional-argument] # public API: positional bool kept for backwards compatibility + *, + blocking: bool = True, + is_singleton: bool = False, + poll_interval: float = 0.05, + lifetime: float | None = None, + context_error_policy: ContextErrorPolicy = "chain", + close_error_policy: CloseErrorPolicy = "default", + fallback_to_soft: bool = True, + preserve_lock_file: bool = False, + on_acquired: Callable[[int], None] | None = None, + ) -> None: + """ + Create a new lock object. + + :param lock_file: path to the file + :param timeout: default timeout when acquiring the lock, in seconds. It will be used as fallback value in the + acquire method, if no timeout value (``None``) is given. If you want to disable the timeout, set it to a + negative value or ``inf``. A timeout of 0 means that there is exactly one attempt to acquire the file lock. + :param mode: file permissions for the lockfile. When not specified, the OS controls permissions via umask and + default ACLs, preserving POSIX default ACL inheritance in shared directories. Construction raises + :class:`ValueError` unless it grants the owner read and write, which the lock needs for its own files. On + Windows a native lock creates its file with an owner-only ACL when the mode has no group and other bits; any + other mode, and a soft lock, leave the file the ACL its directory hands down. A Unix lock file that already + exists keeps its permissions. + :param thread_local: Whether this object's internal context should be thread local or not. If this is set to + ``False`` then the lock will be reentrant across threads. When ``True`` (the default), **all fields of the + lock's internal context are per-thread**, including the configuration values ``poll_interval``, ``timeout``, + ``blocking``, ``mode``, and ``lifetime``. Setting one of these properties from one thread does not change + the value seen by another thread; threads that did not perform the write continue to see the value supplied + at construction time. ``mode`` has no setter, so construction is the only place it is ever set. If you need + configuration values to be visible across threads, construct the lock with ``thread_local=False``. + :param blocking: whether the lock should be blocking or not + :param is_singleton: If this is set to ``True`` then only one instance of this class will be created per lock + file. This is useful if you want to use the lock object for reentrant locking without needing to pass the + same object around. + :param poll_interval: default interval for polling the lock file, in seconds. It will be used as fallback value + in the acquire method, if no poll_interval value (``None``) is given. + :param lifetime: for :class:`SoftFileLock`, the age in seconds after which a waiting process may delete the + marker, even while its holder remains alive. This legacy expiry mode does not provide strict mutual + exclusion. ``None`` (the default) disables age-based expiry. Native OS locks (:class:`FileLock`) cannot be + revoked by file age and ignore a non-``None`` ``lifetime`` with a warning. + :param context_error_policy: how a context manager reconciles a failure in its body with a failure while + releasing on exit. ``"chain"`` (the default) keeps Python's behavior: the release error propagates with the + body error in its ``__context__``. ``"group"`` raises a :class:`BaseExceptionGroup` holding the body error + first and the release error second, so neither hides the other. + :param close_error_policy: what to do with an ``os.close`` failure after relinquishing descriptor ownership. + ``"default"`` keeps each backend's historical behavior (Unix native locks drop a FUSE/Docker ``EIO``; + Windows native locks and :class:`SoftFileLock` propagate); ``"raise"`` always propagates the ``OSError``; + ``"suppress"`` always ignores it. Held state is released either way. It does not affect unlock failures or + lock-file deletion. + :param fallback_to_soft: for :class:`UnixFileLock`, whether to switch to :class:`SoftFileLock` when the + filesystem's ``flock`` returns ``ENOSYS``. ``True`` (the default) keeps the historical fallback; + ``False`` fails closed, letting the ``ENOSYS`` propagate so a caller that needs kernel-enforced + locking is never silently downgraded. It has no effect on Windows or :class:`SoftFileLock`. + :param preserve_lock_file: for native locks (:class:`FileLock`), whether filelock promises not to unlink the + lock pathname on release. ``False`` (the default) keeps each backend's cleanup: Windows removes the lock + file, Unix already leaves it. ``True`` keeps a stable file identity for ACLs, auditing, and holder metadata: + Windows skips its post-release unlink and Unix refuses to enter the ``ENOSYS`` soft fallback (which releases + by unlinking). :class:`SoftFileLock` rejects ``True``. The promise covers filelock's own release path only; + it cannot stop another process or the filesystem from removing the pathname. + :param on_acquired: for native locks (:class:`FileLock`), a callable invoked with the borrowed lock descriptor + once per physical acquisition, after filelock holds the native lock and finished backend initialization but + before :meth:`~BaseFileLock.acquire` returns. Recursive acquisitions do not call it again. The callback may + read, write, seek, truncate, or set metadata through ``os`` on the descriptor, but must not close, unlock, + or take ownership of it, and filelock does not fsync its writes. If it raises, filelock releases the lock + and re-raises. :class:`SoftFileLock` rejects the hook. + + :raises TypeError: if *timeout* or *poll_interval* is a :class:`bool` or not a real number, *blocking* is not a + :class:`bool`, or *mode* is not an :class:`int` + :raises ValueError: if *poll_interval* is negative, not finite, or above :data:`threading.TIMEOUT_MAX`, if + *timeout* is too large to convert to :class:`float`, or if *mode* falls outside ``0o000``-``0o777`` or + denies the owner read and write + + """ + self._creator_pid = os.getpid() + #: Holder id in the deadlock registry; a stand-in that finishes the finalizer has another id(self). + self._holder_id = id(self) + self._transition_lock = RLock() + self._is_thread_local = thread_local + self._is_singleton = is_singleton + self._context_error_policy = context_error_policy # already validated by the metaclass + self._close_error_policy = close_error_policy # already validated by the metaclass + self._fallback_to_soft = fallback_to_soft + self._preserve_lock_file = preserve_lock_file # already validated by the metaclass + self._on_acquired = on_acquired # already validated by the metaclass + + self._context: FileLockContext = (ThreadLocalFileContext if thread_local else FileLockContext)( + lock_file=os.fspath(lock_file), + timeout=timeout, + mode=mode, + blocking=blocking, + poll_interval=poll_interval, + lifetime=lifetime, + ) + #: Each thread's committed hold on a thread-local lock, so another thread can release it. + self._thread_holds: dict[int, tuple[FileLockContext, Thread]] = {} + #: Each thread's uncommitted acquisition, so a backend can finish it for an exited thread. + self._thread_pending: dict[int, tuple[FileLockContext, Thread]] = {} + _register_fork_object(self) + + def _singleton_extra_mismatches( # ruff:ignore[no-self-use] # the base class adds no options of its own + self, _kwargs: Mapping[str, _ExtraValue], / + ) -> dict[str, tuple[str, str]]: + # A subclass's own options reach the metaclass only as kwargs, and only the subclass knows their defaults and + # how __init__ resolves them, so each override compares its options and merges what super() reports. + return {} + + def is_thread_local(self) -> bool: + """:returns: a flag indicating if this lock is thread local or not""" + return self._is_thread_local + + @property + def is_singleton(self) -> bool: + """ + A flag indicating if this lock is singleton or not. + + .. versionadded:: 3.13.0 + + """ + return self._is_singleton + + @property + def context_error_policy(self) -> ContextErrorPolicy: + """ + How a context manager reconciles a body failure with a release failure on exit. + + .. versionadded:: 3.30.0 + + """ + return self._context_error_policy + + @property + def close_error_policy(self) -> CloseErrorPolicy: + """ + What a lock does with an ``os.close`` failure after relinquishing descriptor ownership. + + .. versionadded:: 3.30.0 + + """ + return self._close_error_policy + + def _close_released_fd(self, fd: int, *, default_suppresses: bool) -> None: + # CPython never retries close() after EINTR because the descriptor number may already be reused, so neither does + # this. close_error_policy decides the error's fate after the backend relinquishes descriptor ownership. + try: + os.close(fd) + except OSError: + if not self._suppresses_close_error(default_suppresses=default_suppresses): + raise + + def _suppresses_close_error(self, *, default_suppresses: bool) -> bool: + return self._close_error_policy == "suppress" or (self._close_error_policy == "default" and default_suppresses) + + @property + def fallback_to_soft(self) -> bool: + """ + Whether a :class:`FileLock` falls back to :class:`SoftFileLock` when the filesystem lacks ``flock``. + + Only :class:`UnixFileLock` acts on it: when ``False`` an ``ENOSYS`` from ``flock`` propagates instead of + switching to existence-lock semantics. + + .. versionadded:: 3.30.0 + + """ + return self._fallback_to_soft + + @property + def preserve_lock_file(self) -> bool: + """ + Whether filelock promises not to unlink the lock pathname on release. + + When ``True``, Windows skips its post-release unlink and Unix refuses the ``ENOSYS`` soft fallback. + :class:`SoftFileLock` rejects ``True`` because unlinking its marker is how it releases. + + .. versionadded:: 3.30.0 + + """ + return self._preserve_lock_file + + @property + def on_acquired(self) -> Callable[[int], None] | None: + """ + The callback run with the borrowed lock descriptor once per physical acquisition, or ``None``. + + Native locks only. It runs after the native lock is held and backend initialization finished, before + :meth:`~BaseFileLock.acquire` returns; a raise rolls back the acquisition. :class:`SoftFileLock` rejects it. + + .. versionadded:: 3.30.0 + + """ + return self._on_acquired + + @property + def lock_file(self) -> str: + """Path to the lock file.""" + return self._context.lock_file + + @property + def _lock_path(self) -> str: + # The OS resolves a relative lock_file against the working directory at each call. A chdir during an + # acquisition or a hold would otherwise make the backend wait on, lock or remove a different file. + return self._context.claim_root or self._context.lock_file + + @property + def timeout(self) -> float: + """ + The default timeout value, in seconds. + + .. versionadded:: 2.0.0 + + """ + return self._context.timeout + + @timeout.setter + def timeout(self, value: float | str | bytes | bytearray) -> None: + """ + Change the default timeout value. + + :param value: the new value, in seconds; a negative value or ``inf`` waits without a limit + + :raises TypeError: if *value* is a :class:`bool` or not a real number + :raises ValueError: if *value* is too large to convert to :class:`float` + + .. deprecated:: 4.0.13 + + Passing a numeric string; pass the number instead. + + """ + if isinstance(value, (str, bytes, bytearray)): + warnings.warn( + "setting timeout from a string is deprecated and will be removed; pass a number", + DeprecationWarning, + stacklevel=2, + ) + value = float(value) + self._context.timeout = _seconds("timeout", value) + + @property + def blocking(self) -> bool: + """ + Whether the locking is blocking or not. + + .. versionadded:: 3.14.0 + + """ + return self._context.blocking + + @blocking.setter + def blocking(self, value: bool) -> None: + """ + Change the default blocking value. + + :param value: the new value as bool + + :raises TypeError: if *value* is not a bool + + """ + _check_blocking(value) + self._context.blocking = value + + @property + def poll_interval(self) -> float: + """ + The default polling interval, in seconds. + + .. versionadded:: 3.24.0 + + """ + return self._context.poll_interval + + @poll_interval.setter + def poll_interval(self, value: float) -> None: + """ + Change the default polling interval. + + :param value: the new value, in seconds + + :raises ValueError: if *value* is negative, not finite, or above :data:`threading.TIMEOUT_MAX` + :raises TypeError: if *value* is a :class:`bool` or not a real number + + """ + self._context.poll_interval = _resolve_poll_interval(value) + + @property + def lifetime(self) -> float | None: + """ + The soft marker age in seconds that permits expiry, or ``None`` to disable age-based expiry. + + A non-``None`` value permits a waiter to enter while the previous holder remains active, so it does not provide + strict mutual exclusion. Native locks ignore the value with a warning. + + .. versionadded:: 3.24.0 + + """ + return self._context.lifetime + + @lifetime.setter + def lifetime(self, value: float | None) -> None: + """ + Change the legacy age-based expiry threshold. + + :param value: the new value in seconds, or ``None`` to disable expiration + + :raises ValueError: if *value* is negative or not finite + :raises TypeError: if *value* is not ``None`` and not a real number + + """ + self._context.lifetime = _resolve_lifetime(value, type(self), stacklevel=3) + + @property + def mode(self) -> int: + """ + The file permissions requested for the lock file. + + Without an explicit ``mode`` this reports ``0o644`` for backwards compatibility, but the file is created with + ``0o666`` filtered through the umask and default ACLs, so with a ``002`` umask it ends up ``0o664``. Check + :attr:`has_explicit_mode` before relying on the value, or ``os.stat`` the file for its actual permissions. + + """ + return 0o644 if self._context.mode == _UNSET_FILE_MODE else self._context.mode + + @property + def has_explicit_mode(self) -> bool: + """Whether the file permissions were explicitly set.""" + return self._context.mode != _UNSET_FILE_MODE + + def _open_mode(self) -> int: + """Mode for ``os.open``: 0o666 when unset so umask and ACLs decide, otherwise the explicit mode.""" + return 0o666 if self._context.mode == _UNSET_FILE_MODE else self._context.mode + + @property + def is_locked(self) -> bool: + """ + A boolean indicating if the lock file is holding the lock currently. + + .. versionchanged:: 2.0.0 + + This was previously a method and is now a property. + + """ + _ensure_current_process() + return self._context.lock_file_fd is not None + + @property + def lock_counter(self) -> int: + """The number of times this lock has been acquired (but not yet released).""" + _ensure_current_process() + return self._context.lock_counter + + def __enter__(self) -> Self: + """ + Acquire the lock. + + :returns: the lock object + + """ + self.acquire() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """Release the lock, reconciling a release failure with any body failure per :attr:`context_error_policy`.""" + self._release_in_context(exc_value) + + def _release_in_context(self, body_error: BaseException | None) -> None: + # Release from a context-manager exit. "chain" lets a release failure propagate with the body error already in + # its __context__ (Python's default); "group" raises both as sibling leaves so neither one hides the other. + try: + self.release() + except BaseException as release_error: + if body_error is None or self._context_error_policy == "chain": + raise + _raise_body_and_release(body_error, release_error) + + def __del__(self) -> None: + """Force-release so a dropped reference never leaks a held lock, including every thread's hold.""" + if vars(self).get("_creator_pid") != os.getpid(): + return # pragma: forked child + # CPython runs __del__ before it clears weak references. Until then the singleton cache still hands this lock + # out, and a thread that takes a hold through it would lose that hold to a release here. The cycle collector + # clears the references first, and at exit no other thread runs. + if (key := self._singleton_key) is not None and not sys.is_finalizing() and self._instances.get(key) is self: + self._release_once_unreachable() + else: + self._release_abandoned_holds() + + def _release_abandoned_holds(self) -> None: + # A finalizer must not raise. A release error during garbage collection would otherwise surface as an + # unraisable-exception warning, attributed to whichever code triggered collection. The dropped lock still gets + # best-effort cleanup; an explicit release() reports the same error to a caller who can act on it. + with contextlib.suppress(Exception): + self.release(force=True) + # Nothing references the lock any more, so no thread can still be inside it through this instance. + with contextlib.suppress(Exception): + self._release_thread_holds(exited_only=False) + + def _release_once_unreachable(self) -> None: + # The callback runs once the lock is gone, so it reads the state through a holder. The state alone keeps the + # holder alive: a reference from the callback would keep the state, and through it the lock, alive. + self._finalizer_state = _FinalizerState(vars(self)) + _UNREACHABLE_LOCKS.add(ref(self, partial(_finalize_unreachable, ref(self._finalizer_state), type(self)))) + + def acquire( + self, + timeout: float | None = None, + poll_interval: float | None = None, + *, + poll_intervall: float | None = None, + blocking: bool | None = None, + cancel_check: Callable[[], bool] | None = None, + ) -> AcquireReturnProxy[Self]: + """ + Try to acquire the file lock. + + :param timeout: maximum wait time for acquiring the lock, ``None`` means use the default :attr:`~timeout` is and + if ``timeout < 0``, there is no timeout and this method will block until the lock could be acquired + :param poll_interval: interval of trying to acquire the lock file, ``None`` means use the default + :attr:`~poll_interval` + :param poll_intervall: deprecated, kept for backwards compatibility, use ``poll_interval`` instead + :param blocking: whether to retry until this call holds the lock or the timeout expires, ``None`` means use + the default :attr:`~blocking`. ``False`` makes one attempt and raises :class:`~Timeout` on failure. + :param cancel_check: a callable returning ``True`` when the acquisition should be canceled. Checked on each poll + iteration. When triggered, raises :class:`~Timeout` just like an expired timeout. + + :returns: a context object that will unlock the file when the context is exited + + :raises Timeout: if fails to acquire lock within the timeout period + :raises ValueError: if a blocking call gets a ``nan`` timeout, or *poll_interval* exceeds + :data:`threading.TIMEOUT_MAX` + + .. code-block:: python + + # You can use this method in the context manager (recommended) + with lock.acquire(): + pass + + # Or use an equivalent try-finally construct: + lock.acquire() + try: + pass + finally: + lock.release() + + .. versionchanged:: 2.0.0 + + This method returns now a *proxy* object instead of *self*, so that it can be used in a with statement + without side effects. + + """ + self._raise_if_inherited() + if timeout is None: + timeout = self._context.timeout + + if blocking is None: + blocking = self._context.blocking + + if poll_intervall is not None: + msg = "use poll_interval instead of poll_intervall" + warnings.warn(msg, DeprecationWarning, stacklevel=2) + poll_interval = poll_intervall + + poll_interval = poll_interval if poll_interval is not None else self._context.poll_interval + poll_interval = _resolve_poll_interval(poll_interval) + timeout = _resolve_timeout(timeout, blocking=blocking) + + start_time = time.perf_counter() + # Wait for admission before touching any state: a caller refused entry must leave the counter, the registry and + # the descriptor exactly as it found them. + with self._transition_admission( + blocking=blocking, + cancel_check=cancel_check, + timeout=timeout, + poll_interval=poll_interval, + start_time=start_time, + ): + # The interrupted acquire cannot run until the handler returns, so a second one here would wait on it + # forever or take the lock out from under it. Once the lock is held, as in an on_acquired hook, it nests. + if not self.is_locked and _on_this_stack(self._context.acquiring): + msg = f"Deadlock: lock '{self.lock_file}' is acquired again while this thread's acquire of it waits" + raise RuntimeError(msg) + # An interrupted release commits only the hold it started with and ignores a release above it on the + # stack. A hold taken here would nest into one the OS dropped, or stay held for good. + if _on_this_stack(self._context.releasing): + msg = f"lock '{self.lock_file}' is acquired again while this thread's release of it runs" + raise RuntimeError(msg) + canonical = _canonical(self.lock_file) + self._context.lock_counter += 1 + + self._raise_if_would_deadlock(canonical, timeout=timeout, blocking=blocking) + if not self.is_locked: + self._pin_lock_path(canonical) + + try: + self._poll_until_acquired( + blocking=blocking, + cancel_check=cancel_check, + timeout=timeout, + poll_interval=poll_interval, + start_time=start_time, + ) + except BaseException: + self._reconcile_failed_acquire(canonical) + raise + self._commit_acquire(canonical) + return AcquireReturnProxy(lock=self) + + @contextlib.contextmanager + def _transition_admission( + self, + *, + blocking: bool, + cancel_check: Callable[[], bool] | None, + timeout: float, + poll_interval: float, + start_time: float, + ) -> Generator[None]: + # One thread at a time drives the physical transition of a shared instance: its counter, descriptor and claim + # state are plain attributes every thread reads and writes, so an unserialized loser clears the winner's + # descriptor or double-closes it. Waiting in slices keeps cancel_check and the timeout live. A thread-local + # context is never shared, so it skips the gate. + if self._is_thread_local: + yield + return + deadline: Final = None if timeout < 0 else start_time + timeout + wait: Final = poll_interval if blocking else 0.0 + # wait leads the min() so a non-blocking call, whose nan timeout goes unchecked, waits 0 rather than nan. + while not self._transition_lock.acquire( + timeout=wait if deadline is None else min(wait, max(deadline - time.perf_counter(), 0.0)) + ): + if not blocking or (cancel_check is not None and cancel_check()): + raise Timeout(self.lock_file) + if deadline is not None and time.perf_counter() >= deadline: + raise Timeout(self.lock_file) + try: + yield + finally: + self._transition_lock.release() + + def release(self, force: bool = False) -> None: # ruff:ignore[boolean-type-hint-positional-argument, boolean-default-value-positional-argument] # public API: positional bool kept for backwards compatibility + """ + Release the file lock. The lock is only completely released when the lock counter reaches 0. The lock file + itself may be deleted automatically, the behavior is platform-specific. + + :param force: If true, the lock counter is ignored and the lock is released in every case. On a thread-local + lock it also releases the holds of threads that exited without releasing; a hold of a thread still running + stays with that thread. Dropping the last reference to the lock releases every thread's hold. + + """ + counter_before: int | None = None + try: + counter_before = self._context.lock_counter + self._release_current(force=force) + except BaseException as release_error: + # A handler that raised after release() began and before the backend changed anything leaves the counter as + # it was; only then does a second attempt release the hold the caller gave up. A failed system call keeps it + # for an explicit retry, and a changed counter means a nested level or the backend's own retry already ran. + if ( + counter_before is not None + and counter_before == self._context.lock_counter + and self._context.lock_file_fd is not None + and not _is_system_call_failure(release_error) + ): + with contextlib.suppress(Exception): + self._release_current(force=force) + raise + if force: + self._release_thread_holds(exited_only=True) + + def _release_current(self, *, force: bool) -> None: + # A shared instance releases under the same gate its acquisition ran through, so a thread entering the lock + # never observes a partially torn-down owner. Only an acquirer that found the lock free holds the gate for + # long, so an unheld lock returns without waiting behind it. + if self._context.lock_file_fd is None: + # Only a committed hold registers, so an entry without a descriptor is a release a signal handler stopped + # after the unlock; left alone it refuses every other instance on this path in this thread. + if self._context.lock_file_registry is not None: + self._finish_interrupted_release() + return + with contextlib.nullcontext() if self._is_thread_local else self._transition_lock: + state: Final = self._hold_state() + # A signal handler can land between the backend's unlock and its clearing of the descriptor, where a second + # release would close the number twice, maybe hitting a file that reused it. The flag holds the releasing + # frame, so only a release still on this stack stops it, and a flag a cut-short frame left set does not. + if self._creator_pid != os.getpid() or state.lock_file_fd is None or _on_this_stack(state.releasing): + return + if not force and state.lock_counter > 1: + state.lock_counter -= 1 + return + state.releasing = inspect.currentframe() + try: + _LOGGER.debug("Attempting to release lock %s on %s", id(self), self.lock_file) + self._release_with_fork_tracking() + finally: + # A failure after the OS unlock (during close or unlink) still released the lock: the backend cleared + # its descriptor, so commit the counter and registry to released even as the cleanup error propagates. + # A failure that left the lock held keeps the counter so a later release can retry the OS unlock. + if state.lock_file_fd is None: # pragma: no branch # a release that returns cleared the descriptor + self._commit_release() + state.releasing = None + _LOGGER.debug("Lock %s released on %s", id(self), self.lock_file) + + def _finish_interrupted_release(self) -> None: + # An unheld lock never waits behind an acquirer, whose own commit takes the entry over anyway. + if not self._transition_lock.acquire(blocking=False): + return # pragma: no cover # only a concurrent acquire of a shared instance holds the gate + try: + if self._context.lock_file_fd is None: # pragma: no branch # only a racing acquire fills it + self._commit_release() + finally: + self._transition_lock.release() + + def _hold_state(self) -> FileLockContext: + return self._context.current if isinstance(self._context, ThreadLocalFileContext) else self._context + + def _release_thread_holds(self, *, exited_only: bool) -> None: + if not isinstance(context := self._context, ThreadLocalFileContext): + return + # Serialized, or two threads would both close an exited thread's descriptor; copy() is atomic free-threaded. + with self._transition_lock: + own: Final = context.current + for held, thread in self._thread_holds.copy().values(): + if exited_only and thread.is_alive(): + continue + context.current = held + try: + self._release_current(force=True) + finally: + context.current = own + + def _raise_if_inherited(self) -> None: + if self._creator_pid != os.getpid(): # pragma: forked child + msg = f"{type(self).__name__} on {self.lock_file} was inherited across fork; construct a new instance" + raise RuntimeError(msg) + + def _mark_descriptor_owned(self, fd: int, identity: tuple[int, int] | None = None) -> None: + state: Final = self._hold_state() + # Held first, pending cleared second: an interrupt between the two then leaves the descriptor recorded. + state.lock_file_fd = fd + state.lock_file_fd_identity = identity + state.pending_lock_file_fd = None + state.pending_lock_file_fd_identity = None + if isinstance(self._context, ThreadLocalFileContext): + self._thread_pending.pop(id(state), None) + self._thread_holds[id(state)] = state, current_thread() + + def _mark_descriptor_pending(self, fd: int, identity: tuple[int, int] | None = None) -> None: + state: Final = self._hold_state() + state.pending_lock_file_fd = fd + state.pending_lock_file_fd_identity = identity + if isinstance(self._context, ThreadLocalFileContext): + self._thread_pending[id(state)] = state, current_thread() + + def _mark_descriptor_released(self) -> None: + state: Final = self._hold_state() + state.pending_lock_file_fd = None + state.pending_lock_file_fd_identity = None + state.lock_file_fd = None + state.lock_file_fd_identity = None + if isinstance(self._context, ThreadLocalFileContext): + self._thread_pending.pop(id(state), None) + self._thread_holds.pop(id(state), None) + + def _reset_after_fork_in_child(self) -> None: # pragma: forked child + # fork copies the lock in whatever state the parent's threads left it, so give the child an unheld one. + self._transition_lock = RLock() + if isinstance(self._context, ThreadLocalFileContext): + self._thread_holds.clear() + self._thread_pending.clear() + self._context.owner_claim_paths = () + self._context.claim_root = None + self._context.lock_file_fd = None + self._context.lock_file_fd_token = None + self._context.lock_file_fd_identity = None + self._context.pending_lock_file_fd = None + self._context.pending_lock_file_fd_identity = None + self._context.lock_counter = 0 + self._context.releasing = None + self._context.acquiring = None + self._context.lock_file_key = None + self._context.lock_file_registry = None + + def _descriptors_for_fork(self) -> tuple[tuple[int, tuple[int, int] | None], ...]: # pragma: needs fork + descriptors: list[tuple[int, tuple[int, int] | None]] = [] + if self._context.lock_file_fd is not None and self._context.lock_file_fd_token is None: + descriptors.append((self._context.lock_file_fd, self._context.lock_file_fd_identity)) + if self._context.pending_lock_file_fd is not None: + descriptors.append((self._context.pending_lock_file_fd, self._context.pending_lock_file_fd_identity)) + return tuple(descriptors) + + def _raise_if_would_deadlock(self, canonical: str, *, timeout: float, blocking: bool) -> None: + """ + Fail fast when a *different* live instance already holds this path in the current deadlock scope. + + Only the first, indefinitely-blocking acquire can self-deadlock this way: waiting in the OS primitive would + block on a lock this flow already owns. A finite timeout or ``blocking=False`` keeps the normal Timeout path. + """ + would_block = self._context.lock_counter == 1 and not self.is_locked and timeout < 0 and blocking + if would_block and _registry.held.get(self._registry_key(canonical)) not in {None, self._holder_id}: + self._context.lock_counter -= 1 + msg = ( + f"Deadlock: lock '{self.lock_file}' is already held by a different {self._deadlock_holder_desc}. " + f"Use is_singleton=True to enable reentrant locking across instances." + ) + raise RuntimeError(msg) + + def _registry_key(self, canonical: str) -> Hashable: + return canonical if (scope := self._deadlock_scope()) is None else (scope, canonical) + + @staticmethod + def _deadlock_scope() -> Hashable | None: + """ + Execution unit whose own hold would deadlock a new acquire. + + ``None`` scopes holders to the thread, which the thread-local registry already separates. Async locks + override it with the running task, since one event loop thread runs many tasks and only a reacquire from + the *same* task can self-deadlock. + """ + return None + + def _poll_until_acquired( + self, + *, + blocking: bool, + cancel_check: Callable[[], bool] | None, + timeout: float, + poll_interval: float, + start_time: float, + ) -> None: + lock_id = id(self) + lock_filename = self.lock_file + attempt = 0 + self._context.acquiring = inspect.currentframe() + try: + # Created once, so a directory that disappears while the acquisition waits fails it, not builds a second + # lock file beside the holder's. + ensure_directory_exists(self._lock_path) + while True: + self._raise_if_inherited() + if not self.is_locked: + self._try_break_expired_lock() + _LOGGER.debug("Attempting to acquire lock %s on %s", lock_id, lock_filename) + self._acquire_with_fork_tracking() + self._raise_if_inherited() + if self.is_locked: + _LOGGER.debug("Lock %s acquired on %s", lock_id, lock_filename) + return + if self._check_give_up( + blocking=blocking, + cancel_check=cancel_check, + timeout=timeout, + start_time=start_time, + ): + raise Timeout(lock_filename, self._unprobeable_holder()) + attempt += 1 + delay = self._poll_delay(poll_interval, attempt) + if timeout >= 0: + delay = min(delay, max(start_time + timeout - time.perf_counter(), 0.0)) + msg = "Lock %s not acquired on %s, waiting %s seconds ..." + _LOGGER.debug(msg, lock_id, lock_filename, delay) + time.sleep(delay) + finally: + self._context.acquiring = None + + def _poll_delay(self, poll_interval: float, attempt: int) -> float: + # A single-file lock retries on a fixed cadence. A backend that publishes several files per acquisition sets a + # cap, and then contending processes back off across a jittered, exponentially widening window instead of + # colliding on every poll; poll_interval stays the floor so a lone waiter is still responsive. + if not self._poll_backoff_cap: + return poll_interval + # Cap the exponent before doubling: under heavy contention attempt reaches the thousands, and 2**attempt would + # overflow the float multiply long before the window itself stops growing past the cap. + window = min( + self._poll_backoff_cap, poll_interval * 2 ** min(attempt, _MAX_BACKOFF_EXPONENT) + ) # pragma: needs hard-link + return max(poll_interval, secrets.randbelow(int(window * 1_000_000) + 1) / 1_000_000) # pragma: needs hard-link + + def _reconcile_failed_acquire(self, canonical: str) -> None: + # An acquire that raised while still holding the native lock (a hook that failed and whose rollback could not + # release) must keep the registry entry so a later release can retry the OS unlock; otherwise roll the counter + # back. is_locked was already reconciled by whichever release ran. + if self.is_locked: + self._commit_acquire(canonical) + else: + self._undo_acquire() + + def _invoke_on_acquired(self) -> None: + # The wrapper runs in the backend executor for async locks, preserving the callback's documented thread. + if self._on_acquired is None or self._context.lock_counter != 1: + return + try: + self._on_acquired(cast("int", self._context.lock_file_fd)) + except BaseException as callback_error: # arbitrary caller code; roll back on any failure + callback_context = callback_error.__context__ + try: + self._release_with_fork_tracking() + except BaseException as release_error: # ruff:ignore[blind-except] # both errors surface via the group below + _raise_body_and_release(callback_error, release_error) + callback_error.__context__ = callback_context + raise + + def _acquire_with_fork_tracking(self) -> None: + with _fork_transition(self): + try: + self._acquire() + except BaseException as acquisition_error: + self._rollback_failed_acquire(acquisition_error) + raise + try: + self._register_context_descriptor() + except BaseException as registration_error: # pragma: needs fork + self._rollback_failed_registration(registration_error) + raise + if self.is_locked: + self._invoke_on_acquired() + + def _rollback_failed_acquire(self, acquisition_error: BaseException) -> None: + if not self.is_locked: + return + registration_error: BaseException | None = None + tracking_error: BaseException | None = None + try: + self._register_context_descriptor() + except BaseException as error: # ruff:ignore[blind-except] # preserve registration and acquisition failures + registration_error = error + try: + # Rollback may fail too; retain the fd so a child can close it without another identity probe. + self._register_unverified_context_descriptor() + except BaseException as error: # ruff:ignore[blind-except] # pragma: no cover - allocation/control-flow during fallback + tracking_error = error + try: + self._release_with_fork_tracking() + except BaseException as rollback_error: # ruff:ignore[blind-except] # preserve rollback and acquisition failures + _raise_cleanup_errors( + "lock acquisition cleanup failed", + acquisition_error, + registration_error, + tracking_error, + rollback_error, + ) + if registration_error is not None: # pragma: needs fork + _raise_cleanup_errors( + "lock acquisition cleanup failed", acquisition_error, registration_error, tracking_error + ) + + def _rollback_failed_registration(self, registration_error: BaseException) -> None: # pragma: needs fork + tracking_error: BaseException | None = None + try: + # Rollback may fail too; retain the fd so a child can close it without another identity probe. + self._register_unverified_context_descriptor() + except BaseException as error: # ruff:ignore[blind-except] # pragma: no cover - allocation/control-flow during fallback + tracking_error = error + try: + self._release_with_fork_tracking() + except BaseException as rollback_error: # ruff:ignore[blind-except] # preserve rollback and registration failures + _raise_cleanup_errors( + "descriptor registration cleanup failed", registration_error, tracking_error, rollback_error + ) + if tracking_error is not None: # pragma: no cover - requires failed in-memory fallback + _raise_cleanup_errors("descriptor registration cleanup failed", registration_error, tracking_error) + + def _release_with_fork_tracking(self) -> None: + with _fork_transition(self): + try: + self._release() + except BaseException as release_error: + # A backend unlocks first and clears the descriptor second. A signal handler that raises in between + # leaves a hold the OS dropped. A failed system call alone shows the lock is still held, so it keeps it. + if self._context.lock_file_fd is not None and not _is_system_call_failure(release_error): + with contextlib.suppress(Exception): + self._release() + raise + finally: + self._unregister_released_descriptor() + + def _register_context_descriptor(self) -> None: + if self._context.lock_file_fd is not None and self._context.lock_file_fd_token is None: + self._context.lock_file_fd_token = _register_owned_descriptor( + self._context.lock_file_fd, + self._context.lock_file_fd_identity, + ) + + def _register_unverified_context_descriptor(self) -> None: + # The rollback only reaches here still holding a descriptor it never managed to register. + if self._context.lock_file_fd is not None and self._context.lock_file_fd_token is None: # pragma: no branch + self._context.lock_file_fd_token = _register_unverified_owned_descriptor(self._context.lock_file_fd) + + def _unregister_released_descriptor(self) -> None: + if self._context.lock_file_fd is None: + if (token := self._context.lock_file_fd_token) is not None: # pragma: needs fork + _unregister_owned_descriptor(token) + self._context.lock_file_fd_token = None + self._context.lock_file_fd_identity = None + + def _undo_acquire(self) -> None: + """Roll back the counter after a failed acquire, dropping the registry entry once nothing holds the path.""" + self._context.lock_counter = max(0, self._context.lock_counter - 1) + if self._context.lock_counter == 0: + self._drop_registry_entry() + self._context.claim_root = None + + def _pin_lock_path(self, canonical: str) -> None: + # A path without a final component names no file. Its canonical form is a directory, and pinning that would + # change the error the backend raises. + self._context.claim_root = None if canonical.endswith(os.sep) else canonical + + def _commit_acquire(self, canonical: str) -> None: + """Record this instance as the holder once the first acquire succeeds, so peers can detect the deadlock.""" + # Keyed on the registry rather than the counter: an on_acquired hook that acquires again bumps the counter + # past 1 before this first commit runs, and must not leave the hold unregistered. + if self._context.lock_file_registry is None: + key = self._registry_key(canonical) + # The holder scope is resolved once at commit so a later release from another flow drops the right entry. + self._context.lock_file_key = key + # A shared instance may be released from another thread, which must clear the entry in the acquirer's + # registry: the releaser's own thread-local registry never held it. + self._context.lock_file_registry = _registry.held + _registry.held[key] = self._holder_id + + def _drop_registry_entry(self) -> None: + """Forget the key owned by this hold without resolving a mutable path again.""" + # Ordered so a run an interrupt cuts short repeats safely: the entry goes only while it is still ours, and the + # key outlives the registry it indexes, so a later commit sees no registry and records the hold afresh. + key, registry = self._context.lock_file_key, self._context.lock_file_registry + if registry is not None and registry.get(key) == self._holder_id: + del registry[key] + self._context.lock_file_registry = None + self._context.lock_file_key = None + + def _commit_release(self) -> None: + """Record the lock as fully released: reset the recursion counter and drop the deadlock-registry entry.""" + self._context.lock_counter = 0 + self._drop_registry_entry() + self._context.claim_root = None + + def _try_break_expired_lock(self) -> None: + """Remove the lock file once it is older than :attr:`lifetime`; only an existence lock accepts one.""" + + def _check_give_up( + self, + *, + blocking: bool, + cancel_check: Callable[[], bool] | None, + timeout: float, + start_time: float, + ) -> bool: + lock_id, lock_filename = id(self), self.lock_file + if blocking is False: + _LOGGER.debug("Failed to immediately acquire lock %s on %s", lock_id, lock_filename) + return True + if cancel_check is not None and cancel_check(): + _LOGGER.debug("Cancellation requested for lock %s on %s", lock_id, lock_filename) + return True + if 0 <= timeout < time.perf_counter() - start_time: + _LOGGER.debug("Timeout on acquiring lock %s on %s", lock_id, lock_filename) + return True + return False + + def _unprobeable_holder(self) -> str | None: # ruff:ignore[no-self-use] # existence locks override it + return None + + @abstractmethod + def _acquire(self) -> None: + """If the file lock could be acquired, self._context.lock_file_fd holds the file descriptor of the lock file.""" + raise NotImplementedError + + @abstractmethod + def _release(self) -> None: + """Releases the lock and sets self._context.lock_file_fd to None.""" + raise NotImplementedError + + +def _resolve_read_write_timeout(timeout: float, *, blocking: bool) -> float: + """Validate a read-write lock's acquire ``timeout``: ``-1`` or a non-negative number, ``inf`` meaning ``-1``.""" + if (timeout := _resolve_timeout(timeout, blocking=blocking)) < 0 and timeout != -1 and blocking: + msg = f"timeout must be a non-negative number or -1, not {timeout!r}" + raise ValueError(msg) + return timeout + + +def _acquire_thread_lock( + lock: Lock, *, blocking: bool, timeout: float, poll_interval: float, cancel_check: Callable[[], bool] | None +) -> bool: + """Take a read-write lock's in-process lock; with *cancel_check*, ask it every *poll_interval* while waiting.""" + if not blocking: + return lock.acquire(blocking=False) + if cancel_check is None: + # Lock.acquire raises OverflowError past TIMEOUT_MAX, which is 49.7 days on Windows. + return lock.acquire() if timeout == -1 else lock.acquire(timeout=min(timeout, TIMEOUT_MAX)) + deadline = None if timeout == -1 else time.perf_counter() + timeout + while not lock.acquire( + timeout=poll_interval if deadline is None else min(poll_interval, max(deadline - time.perf_counter(), 0)) + ): + if cancel_check() or (deadline is not None and time.perf_counter() >= deadline): + return False + return True + + +def _resolve_timeout(timeout: float, *, blocking: bool) -> float: + """Validate an acquire ``timeout``; every negative value waits without a limit.""" + if math.isnan(timeout := _seconds("timeout", timeout)) and blocking: + msg = "timeout must be a number of seconds, not nan" + raise ValueError(msg) + # The fail-fast deadlock check only sees a negative timeout as unlimited, so spell inf that way. + return -1 if timeout == math.inf else timeout + + +@dataclass(slots=True, weakref_slot=True) +class _FinalizerState: + """A finalized lock's state, held for the release that runs once the lock is unreachable.""" + + state: dict[str, object] | None + + +#: Weak references whose callback finishes a singleton's finalizer once the cache no longer finds it. +_UNREACHABLE_LOCKS: Final[set[ref[BaseFileLock]]] = set() + + +def _finalize_unreachable(holder: ref[_FinalizerState], cls: type[BaseFileLock], reference: ref[BaseFileLock]) -> None: + _UNREACHABLE_LOCKS.discard(reference) + if (finalizer_state := holder()) is None or (state := finalizer_state.state) is None: + return # pragma: no cover # only a collector freeing a resurrected lock as part of a cycle drops the holder + # The lock's dictionary points back at the holder; that cycle ends here. + finalizer_state.state = None + # The release methods only read and write this state, so a fresh instance takes it over. Dropping that instance + # runs __del__ again, and the cache no longer finds the lock. + stand_in = object.__new__(cls) + stand_in.__dict__ = state + del stand_in + + +def _is_system_call_failure(error: BaseException) -> bool: + # A failed system call sets errno. A bare TimeoutError raised by a signal handler is an OSError without one. + if _is_exception_group(error): + return all(map(_is_system_call_failure, cast("_ExceptionGroupProtocol", error).exceptions)) + return isinstance(error, OSError) and error.errno is not None + + +def _on_this_stack(frame: FrameType | None) -> bool: + if frame is None: + return False + caller = inspect.currentframe() + while caller is not None and caller is not frame: + caller = caller.f_back + return caller is not None + + +# acquire() returns this wrapper instead of self so entering the with-statement does not call __enter__ a second +# time; returning self would re-acquire the lock in BaseFileLock.__enter__ without a matching release (issue #37). +class AcquireReturnProxy(Generic[_LockT]): + """A context-aware object that will release the lock file when exiting.""" + + def __init__(self, lock: _LockT) -> None: + self.lock: _LockT = lock + + def __enter__(self) -> _LockT: + return self.lock + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + # Widen to the bound: ty narrows the else branch to the type variable minus BaseFileLock, not to the union rest. + lock: Final = cast("BaseFileLock | ReadWriteLock | SoftReadWriteLock", self.lock) + if isinstance(lock, BaseFileLock): + lock._release_in_context(exc_value) # ruff:ignore[private-member-access] # forwards __exit__ to the owned lock's context release + else: # a reader/writer lock does not carry a context_error_policy + lock.release() + + +@dataclass +class FileLockContext: + """Holds the context for a ``BaseFileLock`` object.""" + + # A separate class so ThreadLocalFileContext can make the whole context thread-local. + + lock_file: str + timeout: float + mode: int + blocking: bool + poll_interval: float + + #: The lock lifetime in seconds; ``None`` means the lock never expires. + lifetime: float | None = None + + #: File descriptor from os.open for the lock file; not None while the lock is held. + lock_file_fd: int | None = None + + #: Registry token for the descriptor owned by this thread's context. + lock_file_fd_token: int | None = None + + #: Identity captured by a backend that already inspected the descriptor. + lock_file_fd_identity: tuple[int, int] | None = None + + #: Descriptor opened by a backend but not yet committed as the held lock. + pending_lock_file_fd: int | None = None + + #: Identity captured for a descriptor whose acquisition has not committed. + pending_lock_file_fd_identity: tuple[int, int] | None = None + + #: Depth of nested acquisitions; the lock is released only when it returns to 0. + lock_counter: int = 0 + + #: Canonical registry key captured when the first physical acquisition commits. + lock_file_key: Hashable | None = None + + #: The acquiring thread's deadlock registry holding :attr:`lock_file_key`. + lock_file_registry: dict[Hashable, int] | None = None + + #: Claim pathnames this owner published, removed by name on release so no holder ever unlinks a peer's claim. + owner_claim_paths: tuple[str, ...] = () + + #: The frame of the :meth:`BaseFileLock.release` tearing this hold down, so a re-entrant call from a signal handler + #: on the same thread returns instead of closing the descriptor a second time. + releasing: FrameType | None = None + + #: The frame of an acquire still polling for this hold, so one a signal handler starts on this thread fails fast. + acquiring: FrameType | None = None + + #: Canonical lock path, resolved when an acquisition starts and kept until its hold ends. A waiter on a relative + #: path stays in the directory it started in, and the release finds the file there, after a chdir in between. + claim_root: str | None = None + + #: The lease claim of this hold, kept with it so a cross-thread release stops the right heartbeat. + lease_claim: _LeaseClaim | None = None + + +class ThreadLocalFileContext(FileLockContext, local): + """ + A thread local version of the ``FileLockContext`` class. + + Each thread's state lives in an ordinary :class:`FileLockContext` at :attr:`current`, which the lock also keeps, so + ``release(force=True)`` and the finalizer can unlock a hold whose thread exited. + """ + + current: FileLockContext + + # threading.local runs __init__ again with these arguments the first time each thread touches the context. + def __init__( # ruff:ignore[too-many-arguments] # mirrors the FileLockContext fields set at construction + self, + *, + lock_file: str, + timeout: float, + mode: int, + blocking: bool, + poll_interval: float, + lifetime: float | None = None, + ) -> None: + self.current = FileLockContext( + lock_file=lock_file, + timeout=timeout, + mode=mode, + blocking=blocking, + poll_interval=poll_interval, + lifetime=lifetime, + ) + + +def _delegate_to_current(name: str) -> property: + return property( + lambda context: getattr(context.current, name), + lambda context, value: setattr(context.current, name, value), + ) + + +for _field in fields(FileLockContext): + setattr(ThreadLocalFileContext, _field.name, _delegate_to_current(_field.name)) + + +@dataclass(frozen=True) +class _OwnedDescriptor: + fd: int + creator_pid: int + device: int | None + inode: int | None + + +class _ForkState: + def __init__(self) -> None: + self.gate = Condition(RLock()) + self.registry_lock = RLock() + self.parameter_models_lock = RLock() + self.transitions: dict[int, dict[int, _ForkDescriptorOwner | None]] = {} + self.active_transitions = 0 + self.admission_closed = False + self.fork_owner_depths: dict[int, int] = {} + self.pinned_objects: dict[int, list[tuple[_ForkResettable, ...]]] = {} + self.pinned_classes: dict[int, list[tuple[_ForkResettableClass, ...]]] = {} + self.provisional_descriptor_tokens: dict[int, list[tuple[int, ...]]] = {} + self.pid = os.getpid() + + def reset_synchronization(self) -> None: # pragma: forked child + self.gate = Condition(RLock()) + self.registry_lock = RLock() + self.parameter_models_lock = RLock() + self.transitions = {} + self.active_transitions = 0 + self.admission_closed = False + self.fork_owner_depths = {} + self.pinned_objects = {} + self.pinned_classes = {} + self.provisional_descriptor_tokens = {} + + +_FORK_OBJECTS: Final[WeakValueDictionary[int, _ForkResettable]] = WeakValueDictionary() +_FORK_CLASSES: Final[WeakValueDictionary[int, _ForkResettableClass]] = WeakValueDictionary() +_OWNED_DESCRIPTORS: Final[dict[int, _OwnedDescriptor]] = {} +_DESCRIPTOR_TOKENS: Final[count[int]] = count() +_TRANSITION_TOKENS: Final[count[int]] = count() +_FORK_STATE: Final = _ForkState() +_FORK_AUDIT_EVENTS: Final[frozenset[str]] = frozenset({"os.fork", "os.forkpty"}) +# CPython before 3.12 guards sys.settrace with one process-wide flag that stays set while audit hooks run, so a thread +# switch inside a Python hook makes a concurrent settrace raise. Coverage and debuggers call it on every thread start. +_SETTRACE_SURVIVES_AUDIT_HOOKS: Final[bool] = sys.implementation.name != "cpython" or sys.version_info >= (3, 12) + + +@cache +def _resolve_lazy_imports_before_shutdown() -> None: + if hasattr(types, "LazyImportType"): # pragma: >=3.15 cover + atexit.register(_resolve_lazy_imports) + + +def _resolve_lazy_imports() -> None: # pragma: >=3.15 cover + # Interpreter shutdown clears sys.modules and sys.meta_path before it finalizes a lock still alive at exit, so a + # lazy import (PEP 810) that the release path in __del__ reaches first can no longer load. Load them while it can. + for name, module in list(sys.modules.items()): + if name == "filelock" or name.startswith("filelock."): + for attribute, value in list(vars(module).items()): + if type(value) is types.LazyImportType: + getattr(module, attribute) + + +def _register_fork_hooks() -> None: + if _REGISTER_AT_FORK is None: + return # pragma: lacks fork + if _SETTRACE_SURVIVES_AUDIT_HOOKS: # pragma: needs fork # pragma: needs settrace-safe-audit-hooks + sys.addaudithook(_audit_fork_safety) + _REGISTER_AT_FORK( # pragma: needs fork + before=_pin_fork_objects, + after_in_parent=_resume_parent_after_fork, + after_in_child=_reset_child_after_fork, + ) + + +@contextmanager +def _fork_transition(descriptor_owner: _ForkDescriptorOwner | None = None) -> Generator[None]: + # No fork follows exit, and a daemon thread stopped inside this bookkeeping holds its lock forever. + if not _HAS_REGISTER_AT_FORK or sys.is_finalizing(): + yield # pragma: lacks fork + return # pragma: lacks fork + _ensure_current_process() # pragma: needs fork + creator_pid = os.getpid() # pragma: needs fork + # A generator holding this transition can be finalized on another thread, which must close the entering thread's. + thread_id: Final = get_ident() # pragma: needs fork + token: Final = _enter_fork_transition(descriptor_owner, thread_id) # pragma: needs fork + try: # pragma: needs fork + yield + finally: + if os.getpid() == creator_pid: # pragma: needs fork + _leave_fork_transition(token, thread_id) + + +def _register_fork_object(instance: _ForkResettable) -> None: + if not _HAS_REGISTER_AT_FORK: + return # pragma: lacks fork + with _fork_transition(), _FORK_STATE.registry_lock: # pragma: needs fork + _FORK_OBJECTS[id(instance)] = instance + _refresh_owner_pins() + + +def _register_fork_class(cls: _ForkResettableClass) -> None: + if not _HAS_REGISTER_AT_FORK: + return # pragma: lacks fork + with _fork_transition(), _FORK_STATE.registry_lock: # pragma: needs fork + _FORK_CLASSES[id(cls)] = cls + _refresh_owner_pins() + + +def _register_owned_descriptor(fd: int, identity: tuple[int, int] | None = None) -> int | None: + if not _HAS_REGISTER_AT_FORK: + return None # pragma: lacks fork + with _fork_transition(): # pragma: needs fork + if identity is None: + stat_result = os.fstat(fd) + identity = stat_result.st_dev, stat_result.st_ino + return _record_owned_descriptor(fd, identity) + + +def _register_unverified_owned_descriptor(fd: int) -> int | None: + if not _HAS_REGISTER_AT_FORK: + return None # pragma: lacks fork + with _fork_transition(): # pragma: needs fork + return _record_owned_descriptor(fd, None) + + +def _record_owned_descriptor(fd: int, identity: tuple[int, int] | None) -> int: # pragma: needs fork + with _FORK_STATE.registry_lock: + token = next(_DESCRIPTOR_TOKENS) + _OWNED_DESCRIPTORS[token] = _OwnedDescriptor( + fd=fd, + creator_pid=os.getpid(), + device=None if identity is None else identity[0], + inode=None if identity is None else identity[1], + ) + return token + + +def _unregister_owned_descriptor(token: int) -> None: # pragma: needs fork + # No fork follows exit, and a daemon thread stopped inside this bookkeeping holds its lock forever. + if sys.is_finalizing(): # pragma: no cover # only exit reaches it, after coverage stops measuring + return + with _fork_transition(), _FORK_STATE.registry_lock: + _OWNED_DESCRIPTORS.pop(token, None) + + +def _pin_fork_objects() -> None: # pragma: needs fork + _ensure_current_process() + thread_id = get_ident() + provisional_descriptor_tokens: list[int] = [] + # A signal handler that raises during the wait does not stop the fork. The after hooks pop one snapshot per + # admission closed here, so the finally records the snapshot on failure too. + try: + with _FORK_STATE.gate: + _FORK_STATE.fork_owner_depths[thread_id] = _FORK_STATE.fork_owner_depths.get(thread_id, 0) + 1 + _FORK_STATE.admission_closed = True + while _FORK_STATE.active_transitions > len(_FORK_STATE.transitions.get(thread_id, ())): + _FORK_STATE.gate.wait() + transition_owners = tuple(_FORK_STATE.transitions.get(thread_id, {}).values()) + _verify_unverified_descriptors() + owners = {id(owner): owner for owner in transition_owners if owner is not None} + provisional_descriptor_tokens.extend( + starmap( + _snapshot_descriptor_for_fork, + (descriptor for owner in owners.values() for descriptor in owner._descriptors_for_fork()), # ruff:ignore[private-member-access] # snapshots each owner's own fork descriptors + ) + ) + finally: + with _FORK_STATE.registry_lock: + _FORK_STATE.provisional_descriptor_tokens.setdefault(thread_id, []).append( + tuple(provisional_descriptor_tokens) + ) + _FORK_STATE.pinned_objects.setdefault(thread_id, []).append(tuple(_FORK_OBJECTS.values())) + _FORK_STATE.pinned_classes.setdefault(thread_id, []).append(tuple(_FORK_CLASSES.values())) + + +def _resume_parent_after_fork() -> None: # pragma: needs fork + thread_id = get_ident() + try: + with _FORK_STATE.registry_lock: + for token in _FORK_STATE.provisional_descriptor_tokens[thread_id].pop(): + _OWNED_DESCRIPTORS.pop(token, None) + _FORK_STATE.pinned_objects[thread_id].pop() + _FORK_STATE.pinned_classes[thread_id].pop() + if not _FORK_STATE.provisional_descriptor_tokens[thread_id]: # pragma: needs fork + del _FORK_STATE.provisional_descriptor_tokens[thread_id] + del _FORK_STATE.pinned_objects[thread_id] + del _FORK_STATE.pinned_classes[thread_id] + finally: + # No other code reopens admission. A failure above would leave the other threads waiting for good. + with _FORK_STATE.gate: + # A handler that raised before the depth was stored left no entry to decrement. + if (depth := _FORK_STATE.fork_owner_depths.get(thread_id, 1)) <= 1: + _FORK_STATE.fork_owner_depths.pop(thread_id, None) + else: # pragma: no cover - earlier at-fork callbacks may deadlock first + _FORK_STATE.fork_owner_depths[thread_id] = depth - 1 + _FORK_STATE.admission_closed = bool(_FORK_STATE.fork_owner_depths) + _FORK_STATE.gate.notify_all() + + +def _ensure_current_process() -> None: + _reset_child_after_fork() + + +def _reset_child_after_fork() -> None: # pragma: forked child + if (pid := os.getpid()) == _FORK_STATE.pid: + return + thread_id = get_ident() + pinned_objects = _FORK_STATE.pinned_objects.get(thread_id, [()])[-1] + pinned_classes = _FORK_STATE.pinned_classes.get(thread_id, [()])[-1] + descriptors: list[_OwnedDescriptor] = [] + for token, descriptor in tuple(_OWNED_DESCRIPTORS.items()): + if descriptor.creator_pid != pid: + descriptors.append(_OWNED_DESCRIPTORS.pop(token)) + _FORK_STATE.reset_synchronization() + _FORK_STATE.pid = pid + _INIT_PARAMETER_MODELS.clear() + _detach_child_state(descriptors, pinned_objects, pinned_classes) + + +def _enter_fork_transition(descriptor_owner: _ForkDescriptorOwner | None, thread_id: int) -> int: # pragma: needs fork + with _FORK_STATE.gate: + while ( + _FORK_STATE.admission_closed + and thread_id not in _FORK_STATE.fork_owner_depths + and thread_id not in _FORK_STATE.transitions + ): + _FORK_STATE.gate.wait() # pragma: no cover - exercised in isolated interpreter + token = next(_TRANSITION_TOKENS) + _FORK_STATE.transitions.setdefault(thread_id, {})[token] = descriptor_owner + _FORK_STATE.active_transitions += 1 + return token + + +def _leave_fork_transition(token: int, thread_id: int) -> None: # pragma: needs fork + with _FORK_STATE.gate: + owned: Final = _FORK_STATE.transitions[thread_id] + del owned[token] + if not owned: + del _FORK_STATE.transitions[thread_id] + _FORK_STATE.active_transitions -= 1 + _FORK_STATE.gate.notify_all() + + +def _verify_unverified_descriptors() -> None: # pragma: needs fork + with _FORK_STATE.registry_lock: + for token, descriptor in tuple(_OWNED_DESCRIPTORS.items()): + if descriptor.creator_pid != os.getpid() or descriptor.device is not None: + continue + try: + stat_result = os.fstat(descriptor.fd) + except OSError: + continue + _OWNED_DESCRIPTORS[token] = _OwnedDescriptor( + fd=descriptor.fd, + creator_pid=descriptor.creator_pid, + device=stat_result.st_dev, + inode=stat_result.st_ino, + ) + + +def _snapshot_descriptor_for_fork(fd: int, identity: tuple[int, int] | None) -> int: # pragma: needs fork + if identity is None: # pragma: needs fork + try: + stat_result = os.fstat(fd) + except OSError: + pass + else: + identity = stat_result.st_dev, stat_result.st_ino + return _record_owned_descriptor(fd, identity) + + +def _detach_child_state( # pragma: forked child + descriptors: list[_OwnedDescriptor], + pinned_objects: tuple[_ForkResettable, ...], + pinned_classes: tuple[_ForkResettableClass, ...], +) -> None: + for descriptor in descriptors: + if descriptor.device is None: + continue + try: + stat_result = os.fstat(descriptor.fd) + except OSError: + continue + if (stat_result.st_dev, stat_result.st_ino) != (descriptor.device, descriptor.inode): + continue + with contextlib.suppress(OSError): + os.close(descriptor.fd) + for instance in pinned_objects: + instance._reset_after_fork_in_child() # ruff:ignore[private-member-access] # resets each pinned instance in the fork child + for cls in pinned_classes: + cls._reset_class_after_fork() + _registry.held.clear() + + +def _refresh_owner_pins() -> None: # pragma: needs fork + with _FORK_STATE.gate: + fork_owner_thread_ids = tuple(_FORK_STATE.fork_owner_depths) + if get_ident() not in fork_owner_thread_ids: # pragma: no cover - at-fork callbacks disable tracing + return + objects = tuple(_FORK_OBJECTS.values()) + classes = tuple(_FORK_CLASSES.values()) + for thread_id in fork_owner_thread_ids: + if object_snapshots := _FORK_STATE.pinned_objects.get(thread_id): # pragma: needs fork + object_snapshots[:] = [objects] * len(object_snapshots) + _FORK_STATE.pinned_classes[thread_id][:] = [classes] * len(object_snapshots) + + +# The defaults capture the module globals: the hook outlives them at interpreter shutdown, where CPython wipes the +# module dict to None before the final audit events fire. +def _audit_fork_safety( # pragma: no cover - CPython disables tracing while Python audit hooks run + event: str, + _args: Unused, + *, + _fork_events: frozenset[str] = _FORK_AUDIT_EVENTS, + _state: _ForkState = _FORK_STATE, + _get_ident: Callable[[], int] = get_ident, +) -> None: + if event in _fork_events: + if _get_ident() in _state.transitions or _state.fork_owner_depths: + msg = f"{event} is unsafe while filelock is changing descriptor ownership" + raise RuntimeError(msg) + elif event == "_posixsubprocess.fork_exec" and _get_ident() in _state.transitions: + msg = "fork_exec is unsafe while filelock is changing descriptor ownership" + raise RuntimeError(msg) + + +_register_fork_hooks() + + +__all__ = [ + "_UNSET_FILE_MODE", + "AcquireReturnProxy", + "BaseFileLock", + "CloseErrorPolicy", + "ContextErrorPolicy", + "FileLockContext", + "FileLockMeta", + "LockOptions", + "ThreadLocalFileContext", + "_acquire_thread_lock", + "_append_exception_context", + "_canonical", + "_check_timeout_max", + "_delegate_to_current", + "_ensure_current_process", + "_fork_transition", + "_grouped_errors", + "_raise_body_and_release", + "_raise_chained_errors", + "_raise_cleanup_errors", + "_raise_grouped_errors", + "_register_fork_class", + "_register_fork_object", + "_register_owned_descriptor", + "_resolve_poll_interval", + "_resolve_read_write_timeout", + "_resolve_timeout", + "_seconds", + "_timeout_behavior", + "_unregister_owned_descriptor", +] diff --git a/hug/lib/python3.12/site-packages/filelock/_async.py b/hug/lib/python3.12/site-packages/filelock/_async.py new file mode 100644 index 0000000000000000000000000000000000000000..a7a8fc2aacd8989132e919189d1362617de63308 --- /dev/null +++ b/hug/lib/python3.12/site-packages/filelock/_async.py @@ -0,0 +1,507 @@ +"""Separate caller cancellation from backend task and executor-future results.""" + +from __future__ import annotations + +__lazy_modules__ = ["asyncio", "collections", "concurrent.futures", "filelock._error"] + +import asyncio +import contextlib +import time +from collections import deque +from concurrent.futures import Future as ConcurrentFuture +from dataclasses import dataclass +from threading import Lock +from typing import TYPE_CHECKING, Final, Generic, Literal, NoReturn, TypeVar, cast +from weakref import WeakKeyDictionary + +from filelock._api import ( + _append_exception_context, + _fork_transition, + _raise_chained_errors, + _resolve_read_write_timeout, +) +from filelock._error import Timeout + +if TYPE_CHECKING: + from collections.abc import AsyncGenerator, Awaitable, Callable + from concurrent.futures import Executor + + from filelock._read_write import ReadWriteLock + from filelock._soft_rw import SoftReadWriteLock + +_T = TypeVar("_T") + +#: The holder an async wrapper records its backend level under. No thread has this identifier, so sync code on the +#: executor thread that took the level cannot re-enter or release it. +_TASKS_THREAD_ID: Final[int] = 0 + + +class _AsyncTransitionUnavailableError(Exception): + pass + + +@dataclass(frozen=True) +class _BackendOutcome(Generic[_T]): + value: _T | None = None + error: BaseException | None = None + + +class _AsyncTransitionGate: + """ + First-in, first-out admission for the calls that transition one lock context. + + Each call queues a ticket, and the head of the queue holds the gate. A call that gives up removes its own ticket, + so abandoned calls do not pile up behind a long holder. + """ + + def __init__(self) -> None: + self._queue_lock: Final[Lock] = Lock() + self._queue: Final[deque[ConcurrentFuture[None]]] = deque() + + @contextlib.asynccontextmanager + async def hold(self) -> AsyncGenerator[None]: + ticket: ConcurrentFuture[None] = ConcurrentFuture() + try: + self._join(ticket) + if not ticket.done(): + await self._wait_for_turn(ticket, blocking=True, cancel_check=None, deadline=None, poll_interval=0.05) + yield + finally: + self._leave(ticket) + + @contextlib.asynccontextmanager + async def hold_for_acquire( + self, + *, + blocking: bool, + cancel_check: Callable[[], bool] | None, + deadline: float | None, + poll_interval: float, + ) -> AsyncGenerator[None]: + ticket: ConcurrentFuture[None] = ConcurrentFuture() + try: + self._join(ticket) + if not ticket.done(): + await self._wait_for_turn( + ticket, + blocking=blocking, + cancel_check=cancel_check, + deadline=deadline, + poll_interval=poll_interval, + ) + yield + finally: + self._leave(ticket) + + async def _wait_for_turn( + self, + ticket: ConcurrentFuture[None], + *, + blocking: bool, + cancel_check: Callable[[], bool] | None, + deadline: float | None, + poll_interval: float, + ) -> None: + if not blocking: + raise _AsyncTransitionUnavailableError + waiter = asyncio.wrap_future(ticket) + while not ticket.done(): + if cancel_check is not None and cancel_check(): + raise _AsyncTransitionUnavailableError + wait_interval = poll_interval + if deadline is not None: + if (remaining := deadline - time.perf_counter()) <= 0: + raise _AsyncTransitionUnavailableError + wait_interval = min(wait_interval, remaining) + await asyncio.wait((waiter,), timeout=wait_interval) + # A hand-off that a signal handler's exception cut short leaves the head undone, so the head settles itself. + self._settle_head() + + def _join(self, ticket: ConcurrentFuture[None]) -> None: + with self._queue_lock: + self._queue.append(ticket) + if len(self._queue) == 1: + ticket.set_result(None) + + def _leave(self, ticket: ConcurrentFuture[None]) -> None: + with self._queue_lock: + try: + self._queue.remove(ticket) + except ValueError: + # The join never completed, or this ticket already left. + return + self._settle_head_locked() + + def _settle_head(self) -> None: + with self._queue_lock: + self._settle_head_locked() + + def _settle_head_locked(self) -> None: + if self._queue and not (head := self._queue[0]).done(): + head.set_result(None) + + +@dataclass +class _TaskHold: + """ + One task's hold, as the nesting count per wrapper it entered through. + + A task's hold ends when its last wrapper level is released or closed. Closing one wrapper ends only the levels + taken through it. + """ + + depths: dict[object, int] + + +class _TaskOwners: + """ + Per-task ownership in front of a sync reader/writer lock that tracks threads. + + An async wrapper runs each call on an executor thread, and that thread does not identify the calling task. Waiting + writers block new readers so a stream of readers cannot starve them. A thread lock guards the table because tasks on + different event loops share it. The table stands for one backend level, taken once for all tasks. Sync code can + end that level directly, with ``close()`` or a forced release on a shared singleton. The next admission then + forgets the holds instead of letting more tasks join them. + """ + + def __init__(self) -> None: + self._lock: Final[Lock] = Lock() + self._mode: Literal["read", "write"] | None = None + self._holds: dict[asyncio.Task[object], _TaskHold] = {} + self._transitioning = False + self._waiting_writers = 0 + #: One ticket per waiting call, settled on the next state change. A call that stops waiting takes its own out, + #: so one that times out or is canceled leaves nothing behind. + self._waiters: dict[ConcurrentFuture[None], None] = {} + + async def acquire( # ruff:ignore[too-many-arguments] # one keyword per piece of wrapper state the table consults + self, + mode: Literal["read", "write"], + *, + timeout: float, + blocking: bool, + lock_file: str, + via: object, + enter: Callable[[float], Awaitable[None]], + joinable: Callable[[], bool], + ) -> None: + task = _current_task() + timeout = _resolve_read_write_timeout(timeout, blocking=blocking) + deadline = None if not blocking or timeout < 0 else time.perf_counter() + timeout + waiting_writer = False + try: + while isinstance( + admission := self._admit( + task, mode, lock_file, via=via, joinable=joinable, waiting_writer=waiting_writer + ), + ConcurrentFuture, + ): + try: + if not blocking or (deadline is not None and time.perf_counter() >= deadline): + raise Timeout(lock_file) + if mode == "write" and not waiting_writer: + waiting_writer = True + with self._lock: + self._waiting_writers += 1 + wait = None if deadline is None else deadline - time.perf_counter() + await asyncio.wait((asyncio.wrap_future(admission),), timeout=wait) + finally: + self._forget(admission) + finally: + if waiting_writer: + with self._lock: + self._waiting_writers -= 1 + self._notify() + if admission == "held": + return + try: + await enter(-1 if deadline is None else max(0.0, deadline - time.perf_counter())) + except BaseException: + self._finish_transition(mode=None) + raise + self._finish_transition(mode=mode, holds={task: _TaskHold(depths={via: 1})}) + + def _admit( # ruff:ignore[too-many-arguments] # the acquire's state, passed through under the table lock + self, + task: asyncio.Task[object], + mode: Literal["read", "write"], + lock_file: str, + *, + via: object, + joinable: Callable[[], bool], + waiting_writer: bool, + ) -> Literal["held", "enter"] | ConcurrentFuture[None]: + with self._lock: + # A "held" answer trusts the backend level. Once the backend lost it, the holds are gone and the next task + # has to take the lock again. + if self._mode is not None and not self._transitioning and not joinable(): + self._holds.clear() + self._mode = None + if (hold := self._holds.get(task)) is not None: + if self._mode != mode: + msg = ( + f"Cannot acquire {mode} lock on {lock_file}: already holding a " + f"{'write' if mode == 'read' else 'read'} lock " + f"({'downgrade' if mode == 'read' else 'upgrade'} not allowed)" + ) + raise RuntimeError(msg) + hold.depths[via] = hold.depths.get(via, 0) + 1 + return "held" + writers_ahead = self._waiting_writers - waiting_writer + if not self._transitioning: + if mode == "read" and self._mode == "read" and not writers_ahead: + self._holds[task] = _TaskHold(depths={via: 1}) + return "held" + if not self._holds and (mode == "write" or not writers_ahead): + self._transitioning, self._mode = True, mode + return "enter" + return self._ticket() + + async def release( + self, + *, + force: bool, + lock_file: str, + via: object, + leave: Callable[[], asyncio.Future[_BackendOutcome[None]]], + still_held: Callable[[], bool], + ) -> None: + task = _current_task() + with self._lock: + if (hold := self._holds.get(task)) is None or via not in hold.depths: + if force: + return + msg = f"Cannot release a lock on {lock_file} that is not held by this task through this wrapper" + raise RuntimeError(msg) + if not force and hold.depths[via] > 1: + hold.depths[via] -= 1 + return + removed = hold.depths.pop(via) + if hold.depths: + return + del self._holds[task] + if self._holds: + return + self._transitioning = True + mode = self._mode + if not still_held(): + self._finish_transition(mode=None) + msg = f"Cannot release a lock on {lock_file} that is not held by this task" + raise RuntimeError(msg) + try: + cancellation = await _drain_cancellation(leave()) + except BaseException: + hold.depths[via] = removed + # Keep the hold for a retried release only while the sync lock still has the level. Restored after the sync + # lock let go, it would stand for a lock nobody holds. + self._finish_transition(mode=mode if still_held() else None, holds={task: hold}) + raise + self._finish_transition(mode=None) + if cancellation is not None: + # The release succeeded, so a restored hold would be a phantom no backend call can clear. + raise cancellation + + async def retire( + self, + via: object | None, + *, + leave: Callable[[], asyncio.Future[_BackendOutcome[None]]], + still_held: Callable[[], bool], + ) -> None: + """ + End the holds taken through *via*, or every hold when it is ``None``, as one transition. + + Leaves the backend through *leave* when that ends the last hold, and always when *via* is ``None``. Arrivals + wait while it runs, so none joins a hold the backend is letting go of. A transition already under way is waited + out first: a task entering through *via* must not get a hold after the wrapper is gone. A cancellation is raised + once the holds are settled. + """ + cancellation: asyncio.CancelledError | None = None + while isinstance(settled := self._drop_holds_via(via), ConcurrentFuture): + try: + await _wait_until_done(asyncio.wrap_future(settled)) + except asyncio.CancelledError as error: + cancellation = error + finally: + self._forget(settled) + mode, dropped = settled + if via is None or (mode is not None and still_held()): + try: + cancellation = await _drain_cancellation(leave()) or cancellation + except BaseException: + # A close that failed with the backend still holding stays retryable, as a failed release does. + self._finish_transition(mode=mode if still_held() else None, holds=dropped) + raise + self._finish_transition(mode=None) + elif mode is not None: + # Sync code already ended the level taken for tasks, so there is nothing to give back. + self._finish_transition(mode=None) + if cancellation is not None: + raise cancellation + + def _drop_holds_via( + self, via: object | None + ) -> tuple[Literal["read", "write"] | None, dict[asyncio.Task[object], _TaskHold]] | ConcurrentFuture[None]: + # Returns the future a running transition settles, or else the mode to leave (None when other holds keep the + # backend level) and the dropped holds. The table stays transitioning while that mode is left. + with self._lock: + if self._transitioning: + return self._ticket() + if via is None: + dropped = dict(self._holds) + self._holds.clear() + else: + dropped = {} + for task, hold in list(self._holds.items()): + if (count := hold.depths.pop(via, None)) is not None: + dropped[task] = _TaskHold(depths={via: count}) + if not hold.depths: + del self._holds[task] + if self._holds and via is not None: + return None, dropped + mode = self._mode + self._transitioning = mode is not None or via is None + return mode, dropped + + def _finish_transition( + self, *, mode: Literal["read", "write"] | None, holds: dict[asyncio.Task[object], _TaskHold] | None = None + ) -> None: + with self._lock: + self._transitioning, self._mode = False, mode + if mode is not None and holds: + for task, hold in holds.items(): + self._holds.setdefault(task, _TaskHold(depths={})).depths.update(hold.depths) + self._notify() + + def _ticket(self) -> ConcurrentFuture[None]: + # Called under the table lock, in the section that decided to wait, so no state change slips in between. + ticket: ConcurrentFuture[None] = ConcurrentFuture() + self._waiters[ticket] = None + return ticket + + def _forget(self, ticket: ConcurrentFuture[None]) -> None: + with self._lock: + self._waiters.pop(ticket, None) + + def _notify(self) -> None: + waiters, self._waiters = self._waiters, {} + for ticket in waiters: + ticket.set_result(None) + + +_TASK_OWNERS: Final[WeakKeyDictionary[ReadWriteLock | SoftReadWriteLock, _TaskOwners]] = WeakKeyDictionary() +_TASK_OWNERS_LOCK: Final[Lock] = Lock() + + +def _task_owners_for(sync_lock: ReadWriteLock | SoftReadWriteLock) -> _TaskOwners: + """Singleton wrappers share one sync lock, so they must share its task holds too.""" + with _fork_transition(), _TASK_OWNERS_LOCK: + if (owners := _TASK_OWNERS.get(sync_lock)) is None: + owners = _TASK_OWNERS[sync_lock] = _TaskOwners() + return owners + + +def _current_task() -> asyncio.Task[object]: + if (task := asyncio.current_task()) is None: + msg = "an async reader/writer lock must be used from inside an asyncio task" + raise RuntimeError(msg) + return task + + +async def _drain_cancellation(future: asyncio.Future[_BackendOutcome[None]]) -> asyncio.CancelledError | None: + # Returned, not raised, so the caller can tell a call that completed under cancellation from one that failed. + try: + await _wait_until_done(future) + except asyncio.CancelledError as cancellation: + try: + await _drain_future(future) + except BaseException as error: # ruff:ignore[blind-except] # reported with the cancellation below + _raise_cancelled_error(cancellation, error) + return cancellation + _future_result(future) + return None + + +async def _drain_future(future: asyncio.Future[_BackendOutcome[_T]]) -> _T: + while not future.done(): + with contextlib.suppress(asyncio.CancelledError): + await _wait_until_done(future) + return _future_result(future) + + +async def _wait_until_done(future: asyncio.Future[_T]) -> None: + if not future.done(): + await asyncio.wait((future,)) + + +def _future_result(future: asyncio.Future[_BackendOutcome[_T]]) -> _T: + outcome = future.result() + if (error := outcome.error) is None: + return cast("_T", outcome.value) + context = error.__context__ + try: + raise error # ruff:ignore[raise-within-try] # the handler restores context changed across the async boundary + except BaseException: + error.__context__ = context + raise + + +def _run_in_executor( + loop: asyncio.AbstractEventLoop, executor: Executor | None, func: Callable[[], _T] +) -> asyncio.Future[_BackendOutcome[_T]]: + outcome: asyncio.Future[_BackendOutcome[_T]] = loop.create_future() + + def settle(call: asyncio.Future[_BackendOutcome[_T]]) -> None: + try: + result = call.result() + except asyncio.CancelledError: + # shutdown(cancel_futures=True) dropped the call unrun: a CancelledError would cancel a task nobody canceled + # and hide that the lock state never changed. + msg = "the executor shut down before running the lock backend call" + result = _BackendOutcome(error=RuntimeError(msg)) + outcome.set_result(result) + + loop.run_in_executor(executor, _capture_call, func).add_done_callback(settle) + return outcome + + +def _capture_call(func: Callable[[], _T]) -> _BackendOutcome[_T]: + try: + return _BackendOutcome(value=func()) + except BaseException as error: # ruff:ignore[blind-except] # backend control-flow exceptions are operation results + return _BackendOutcome(error=error) + + +def _raise_cancelled_error(cancellation: asyncio.CancelledError, error: BaseException) -> NoReturn: + # A reconciliation step failed while unwinding a cancellation, so keep both exception chains. Splice the error's + # existing context onto the cancellation, then make the cancellation the error's context, so both the failure and + # the cancellation that triggered it survive. Shared by the async wrappers so cancellations report the same way. + if (context := error.__context__) is not None and context is not cancellation: + if (cancellation_context := cancellation.__context__) is not None: + _append_exception_context(context, cancellation_context) + cancellation.__context__ = context + error.__context__ = cancellation + _raise_chained_errors(error) + + +async def _capture_awaitable(awaitable: Awaitable[_T]) -> _BackendOutcome[_T]: + try: + return _BackendOutcome(value=await awaitable) + except BaseException as error: # ruff:ignore[blind-except] # backend cancellation must remain distinct from caller cancellation + return _BackendOutcome(error=error) + + +__all__ = [ + "_TASKS_THREAD_ID", + "_AsyncTransitionGate", + "_AsyncTransitionUnavailableError", + "_BackendOutcome", + "_capture_awaitable", + "_capture_call", + "_drain_future", + "_future_result", + "_raise_cancelled_error", + "_run_in_executor", + "_task_owners_for", + "_wait_until_done", +] diff --git a/hug/lib/python3.12/site-packages/filelock/_async_read_write.py b/hug/lib/python3.12/site-packages/filelock/_async_read_write.py new file mode 100644 index 0000000000000000000000000000000000000000..5920457397ac02dabb9a5a184c55cbd3a1f31488 --- /dev/null +++ b/hug/lib/python3.12/site-packages/filelock/_async_read_write.py @@ -0,0 +1,413 @@ +"""Async wrapper around :class:`ReadWriteLock` for use with ``asyncio``.""" + +from __future__ import annotations + +__lazy_modules__ = ["asyncio", "concurrent.futures", "filelock._async", "filelock._error", "functools", "threading"] + +import asyncio +import functools +import os +import sqlite3 +import threading +from concurrent.futures import ThreadPoolExecutor +from concurrent.futures import thread as executor_threads +from contextlib import asynccontextmanager +from typing import TYPE_CHECKING, Final, Literal, ParamSpec, TypeVar, cast + +from filelock._api import ( + _append_exception_context, + _ensure_current_process, + _fork_transition, + _register_fork_object, +) +from filelock._async import ( + _TASKS_THREAD_ID, + _BackendOutcome, + _capture_call, + _drain_future, + _future_result, + _raise_cancelled_error, + _task_owners_for, + _wait_until_done, +) +from filelock._error import Timeout +from filelock._read_write import ReadWriteLock + +if TYPE_CHECKING: + from collections.abc import AsyncGenerator, Callable, MutableMapping + from concurrent import futures + from types import TracebackType + + from filelock._api import AcquireReturnProxy + + +_P = ParamSpec("_P") +_R = TypeVar("_R") + + +class AsyncReadWriteLock: + """ + Async wrapper around :class:`ReadWriteLock` for use in ``asyncio`` applications. + + This wrapper dispatches every blocking SQLite operation to a thread pool via ``loop.run_in_executor()`` because + Python's :mod:`sqlite3` module has no async API. Each ``asyncio`` task owns its own hold. Tasks share a read lock, + and a task asking for the write lock waits until the other holders release, with waiting writers ahead of new + readers. As with :class:`ReadWriteLock`, upgrading or downgrading a held lock raises :class:`RuntimeError`. + Singleton wrappers over one path share their task holds, and closing one ends only the holds taken through it. + + :param lock_file: path to the SQLite database file used as the lock + :param timeout: maximum wait time in seconds; ``-1`` waits up to SQLite's busy-timeout cap of about 23 days + :param blocking: if ``False``, raise :class:`~filelock.Timeout` immediately when the lock is unavailable + :param is_singleton: if ``True``, reuse existing :class:`ReadWriteLock` instances for the same resolved path + :param loop: event loop for ``run_in_executor``; ``None`` uses the running loop + :param executor: executor for ``run_in_executor``. When ``None`` this lock creates and owns a dedicated + single-thread executor so every operation runs on the same thread (SQLite affinity requires this) and shuts it + down in :meth:`close`. This lock uses a caller-supplied executor as-is and never shuts it down, so after passing + no executor call :meth:`close` to release the owned one. + + .. versionadded:: 3.21.0 + + """ + + def __init__( # ruff:ignore[too-many-arguments] # public constructor: one parameter per documented lock option + self, + lock_file: str | os.PathLike[str], + timeout: float = -1, + *, + blocking: bool = True, + is_singleton: bool = True, + loop: asyncio.AbstractEventLoop | None = None, + executor: futures.Executor | None = None, + ) -> None: + creator_pid = os.getpid() + self._creator_pid = creator_pid + self._fork_invalidated = False + self._closed = False + self._owns_backend = not is_singleton + #: Marks the task holds taken through this wrapper, which its close() ends. + self._via: Final = object() + _register_fork_object(self) + with _fork_transition(): + self._lock = ReadWriteLock(lock_file, timeout, blocking=blocking, is_singleton=is_singleton) + self._owners = _task_owners_for(self._lock) + self._loop = loop + self._owns_executor = executor is None + self._executor = executor or ThreadPoolExecutor(max_workers=1) + if os.getpid() != creator_pid: # pragma: forked child + msg = "AsyncReadWriteLock construction cannot continue after fork" + raise RuntimeError(msg) + + @property + def lock_file(self) -> str: + """The path to the lock file.""" + return self._lock.lock_file + + @property + def timeout(self) -> float: + """The default timeout.""" + return self._lock.timeout + + @property + def blocking(self) -> bool: + """Whether blocking is enabled by default.""" + return self._lock.blocking + + @property + def loop(self) -> asyncio.AbstractEventLoop | None: + """The event loop (or ``None`` for the running loop).""" + return self._loop + + @property + def executor(self) -> futures.Executor: + """The executor used for ``run_in_executor`` (a dedicated single-thread one if none was supplied).""" + return self._executor + + @asynccontextmanager + async def read_lock(self, timeout: float | None = None, *, blocking: bool | None = None) -> AsyncGenerator[None]: + """ + Async context manager that acquires and releases a shared read lock. + + Falls back to instance defaults for *timeout* and *blocking* when ``None``. + + :param timeout: maximum wait time in seconds, or ``None`` to use the instance default + :param blocking: if ``False``, raise :class:`~filelock.Timeout` immediately; ``None`` uses the instance default + + """ + await self.acquire_read(timeout, blocking=blocking) + body_error: BaseException | None = None + try: + yield + except BaseException as error: + body_error = error + raise + finally: + await self._release_in_context(body_error) + + @asynccontextmanager + async def write_lock(self, timeout: float | None = None, *, blocking: bool | None = None) -> AsyncGenerator[None]: + """ + Async context manager that acquires and releases an exclusive write lock. + + Falls back to instance defaults for *timeout* and *blocking* when ``None``. + + :param timeout: maximum wait time in seconds, or ``None`` to use the instance default + :param blocking: if ``False``, raise :class:`~filelock.Timeout` immediately; ``None`` uses the instance default + + """ + await self.acquire_write(timeout, blocking=blocking) + body_error: BaseException | None = None + try: + yield + except BaseException as error: + body_error = error + raise + finally: + await self._release_in_context(body_error) + + async def _release_in_context(self, body_error: BaseException | None) -> None: + try: + await self.release() + except BaseException as release_error: + if body_error is not None: + _append_exception_context(release_error, body_error) + raise + + async def acquire_read( + self, timeout: float | None = None, *, blocking: bool | None = None + ) -> AsyncAcquireReadWriteReturnProxy: + """ + Acquire a shared read lock. + + See :meth:`ReadWriteLock.acquire_read` for full semantics. + + :param timeout: seconds to wait; ``None`` uses the instance setting; ``-1`` waits up to SQLite's + busy-timeout cap of about 23 days + :param blocking: if ``False``, raise :class:`~filelock.Timeout` on contention; + ``None`` uses the instance setting + + :returns: a proxy that can be used as an async context manager to release the lock + + :raises RuntimeError: if the calling task already holds the write lock + :raises Timeout: if the lock cannot be acquired within *timeout* seconds + :raises TypeError: if *timeout* is a :class:`bool` or not a real number + :raises ValueError: if a blocking call gets a ``nan`` *timeout* or a negative one other than ``-1`` + + """ + self._raise_if_unusable() + await self._acquire("read", timeout, blocking=blocking) + return AsyncAcquireReadWriteReturnProxy(lock=self) + + async def acquire_write( + self, timeout: float | None = None, *, blocking: bool | None = None + ) -> AsyncAcquireReadWriteReturnProxy: + """ + Acquire an exclusive write lock. + + See :meth:`ReadWriteLock.acquire_write` for full semantics. + + :param timeout: seconds to wait; ``None`` uses the instance setting; ``-1`` waits up to SQLite's + busy-timeout cap of about 23 days + :param blocking: if ``False``, raise :class:`~filelock.Timeout` on contention; + ``None`` uses the instance setting + + :returns: a proxy that can be used as an async context manager to release the lock + + :raises RuntimeError: if the calling task already holds the read lock + :raises Timeout: if the lock cannot be acquired within *timeout* seconds + :raises TypeError: if *timeout* is a :class:`bool` or not a real number + :raises ValueError: if a blocking call gets a ``nan`` *timeout* or a negative one other than ``-1`` + + """ + self._raise_if_unusable() + await self._acquire("write", timeout, blocking=blocking) + return AsyncAcquireReadWriteReturnProxy(lock=self) + + async def _acquire(self, mode: Literal["read", "write"], timeout: float | None, *, blocking: bool | None) -> None: + timeout = self.timeout if timeout is None else timeout + blocking = self.blocking if blocking is None else blocking + + async def enter(remaining: float) -> None: + # A close() may have landed while this task waited for the table. + self._raise_if_unusable() + await self._run_acquire( + functools.partial( + self._lock._acquire_level_of, # ruff:ignore[private-member-access] # the wrapper owns this backend hold + _TASKS_THREAD_ID, + mode, + remaining, + blocking=blocking, + ) + ) + + def joinable() -> bool: + self._raise_if_unusable() + return self._backend_holds() + + await self._owners.acquire( + mode, + timeout=timeout, + blocking=blocking, + lock_file=self.lock_file, + via=self._via, + enter=enter, + joinable=joinable, + ) + + async def release(self, *, force: bool = False) -> None: + """ + Release one level of the calling task's hold. + + The wrapper releases the database lock once no task holds it. + + :param force: if ``True``, end every level the calling task took through this wrapper. Levels taken through + other wrappers on the same path stay. A wrapper that holds no level for the task releases nothing. + + :raises RuntimeError: if the calling task holds no level through this wrapper and *force* is ``False`` + + """ + _ensure_current_process() + if self._inherited: # pragma: needs fork + return + await self._owners.release( + force=force, + lock_file=self.lock_file, + via=self._via, + leave=functools.partial(self._submit, self._leave_backend), + still_held=self._backend_holds, + ) + + async def close(self) -> None: + """ + End the holds of the tasks that acquired through this instance and release the lock once no task holds it. + + After calling this method, the lock instance is no longer usable. With ``is_singleton=False`` it also closes the + underlying SQLite connection. A singleton wrapper leaves the shared :class:`ReadWriteLock` open, so tasks that + acquired through another wrapper and synchronous users of the same path keep their holds. The next acquire + notices a synchronous ``close()`` or forced release of that shared lock. One that lands in the few bytecodes + between that check and the admission still admits the task into the ended hold. + + """ + _ensure_current_process() + if self._inherited: # pragma: needs fork + return + if self._closed: + return + # Refuse acquires from here on, so none is admitted into a hold the backend is letting go of. + self._closed = True + try: + await self._owners.retire( + None if self._owns_backend else self._via, + leave=functools.partial(self._submit, self._lock.close if self._owns_backend else self._leave_backend), + still_held=self._backend_holds, + ) + except BaseException as error: + # Only a failed backend call can leave the level held, and then a retried close() has to find it. + if not isinstance(error, asyncio.CancelledError) and self._backend_holds(): + self._closed = False + else: + self._shutdown_owned_executor() + raise + # Wait for the worker to exit rather than letting it drain in the background: a caller that forks right + # after closing deserves a single-threaded process, and os.fork warns about any surviving thread. + if self._owns_executor: + await asyncio.to_thread(functools.partial(self._executor.shutdown, wait=True)) + + async def _run_acquire(self, acquire: Callable[..., AcquireReturnProxy]) -> None: + abandoned = threading.Event() + acquire_future = self._submit(acquire, cancel_check=abandoned.is_set) + try: + await _wait_until_done(acquire_future) + except asyncio.CancelledError as cancellation: + # Stops a backend wait on another process's hold, which would otherwise outlast the caller by as long. + abandoned.set() + try: + await _drain_future(acquire_future) + except Timeout: + # Nothing was taken, so the caller's cancellation is the whole outcome. + raise cancellation from None + except BaseException as error: # ruff:ignore[blind-except] # reported with the cancellation below + _raise_cancelled_error(cancellation, error) + try: + await _drain_future(self._submit(self._leave_backend)) + except BaseException as error: # ruff:ignore[blind-except] # reported with the cancellation below + _raise_cancelled_error(cancellation, error) + raise + _future_result(acquire_future) + + def _backend_holds(self) -> bool: + # A sync close() or forced release on a shared singleton ends the tasks' level without the table taking part. + return self._lock._holds_level_of(_TASKS_THREAD_ID) # ruff:ignore[private-member-access] # the wrapper owns this backend hold + + def _leave_backend(self) -> None: + # Only the tasks' level: a sync user of the same singleton keeps its own. + self._lock._release_level_of(_TASKS_THREAD_ID) # ruff:ignore[private-member-access] # the wrapper owns this backend hold + + def _submit( + self, func: Callable[_P, _R], *args: _P.args, **kwargs: _P.kwargs + ) -> asyncio.Future[_BackendOutcome[_R]]: + return (self._loop or asyncio.get_running_loop()).run_in_executor( + self._executor, + _capture_call, + functools.partial(func, *args, **kwargs), + ) + + def _shutdown_owned_executor(self) -> None: + if self._owns_executor: + self._executor.shutdown(wait=False) + + @property + def _inherited(self) -> bool: + return self._fork_invalidated or os.getpid() != self._creator_pid + + def _raise_if_unusable(self) -> None: + _ensure_current_process() + if self._inherited: # pragma: needs fork + msg = f"AsyncReadWriteLock on {self.lock_file} was invalidated by fork(); construct a new instance" + raise RuntimeError(msg) + if self._closed: + msg = "Cannot operate on a closed database." + raise sqlite3.ProgrammingError(msg) + + def _reset_after_fork_in_child(self) -> None: # pragma: forked child + self._fork_invalidated = True + if self._owns_executor: + # The worker did not survive the fork, yet before 3.12 the child's exit still wakes it through its queue. On + # PyPy that queue is pure Python, and its lock may have been held by the worker at fork time, so exit hangs. + # CPython 3.12+ forgets every inherited worker in the child; do the same for the one this lock owns. + # typeshed declares the registry read-only; it is a WeakKeyDictionary the exit hook iterates. + registry = cast( + "MutableMapping[threading.Thread, object]", + executor_threads._threads_queues, # ruff:ignore[private-member-access] # the exit hook's registry + ) + for worker in cast("ThreadPoolExecutor", self._executor)._threads: # ruff:ignore[private-member-access] # only the executor lists its workers + registry.pop(worker, None) + + def __del__(self) -> None: + # Safety net when close() was never called: shut down the executor we own so its worker thread does not + # outlive the lock. shutdown(wait=False) never blocks. + if os.getpid() == getattr(self, "_creator_pid", None) and getattr(self, "_owns_executor", False): + self._executor.shutdown(wait=False) + + +class AsyncAcquireReadWriteReturnProxy: + """Context-aware object that releases the async read/write lock on exit.""" + + def __init__(self, lock: AsyncReadWriteLock) -> None: + self.lock = lock + + async def __aenter__(self) -> AsyncReadWriteLock: + return self.lock + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + await self.lock.release() + + +__all__ = [ + "AsyncAcquireReadWriteReturnProxy", + "AsyncReadWriteLock", +] diff --git a/hug/lib/python3.12/site-packages/filelock/_descriptor.py b/hug/lib/python3.12/site-packages/filelock/_descriptor.py new file mode 100644 index 0000000000000000000000000000000000000000..61450ff9fae19481ddd7213ce8e5bf5f2d3c32f2 --- /dev/null +++ b/hug/lib/python3.12/site-packages/filelock/_descriptor.py @@ -0,0 +1,76 @@ +"""A minimal native lock over a caller-owned file descriptor, contending with :class:`FileLock` on the same file.""" + +from __future__ import annotations + +__lazy_modules__ = ["filelock._unix", "threading", "typing"] + +import sys +import time +from threading import TIMEOUT_MAX +from typing import Final + +from filelock._api import _seconds + +if sys.platform == "win32": # pragma: win32 cover + from filelock._windows import _lock_fd_nonblocking, _unlock_fd +else: # pragma: win32 no cover + from filelock._unix import _lock_fd_nonblocking, _unlock_fd + + +def lock_descriptor(fd: int, *, blocking: bool = True, poll_interval: float = 0.05) -> bool: + """ + Take the native OS lock on *fd*, a file descriptor the caller opened and owns. + + This is the same one-byte exclusive lock :class:`FileLock` uses, so a descriptor lock and a path lock on the same + file contend with each other. Unlike :class:`FileLock` it adds no path handling: it never opens, truncates, closes, + unlinks, chmods, canonicalizes, or falls back. The caller owns *fd* before, during, and after the call, and must + close it. On Windows *fd* must be a synchronous descriptor (its handle not opened with ``FILE_FLAG_OVERLAPPED``). + + For timeout, reentrancy, singleton, lifetime, or stale-break behavior, use :class:`FileLock`. There is no async + wrapper. Run this in an executor, or drive ``blocking=False`` from your own polling loop. + + :param fd: an open file descriptor the caller owns. + :param blocking: when ``True`` (default), retry the nonblocking attempt every *poll_interval* seconds until it + succeeds; when ``False``, make one attempt. + :param poll_interval: positive seconds between attempts while blocking, at most :data:`threading.TIMEOUT_MAX`; + ignored when *blocking* is ``False``. + + :returns: ``True`` once the lock is held, or ``False`` on contention when ``blocking`` is ``False``. + + :raises OSError: for a permanent native failure, such as an invalid descriptor, or with ``errno.ENOSYS`` when the + Python build lacks the native locking primitive. The descriptor is left open. + :raises ValueError: if a blocking call receives a *poll_interval* outside that range, including ``nan``. + + .. versionadded:: 3.30.0 + + """ + if not blocking: + return _lock_fd_nonblocking(fd) + # time.sleep raises OverflowError past TIMEOUT_MAX; nan fails both comparisons. + if not 0 < (poll_interval := _seconds("poll_interval", poll_interval)) <= TIMEOUT_MAX: + msg: Final[str] = f"poll_interval must be greater than 0 and at most {TIMEOUT_MAX}, got {poll_interval}" + raise ValueError(msg) + while not _lock_fd_nonblocking(fd): + time.sleep(poll_interval) + return True + + +def unlock_descriptor(fd: int) -> None: + """ + Release the native OS lock on *fd* without touching the descriptor. + + :param fd: the descriptor a prior :func:`lock_descriptor` locked; the caller still owns and must close it. + + :raises OSError: if the native unlock fails, including ``errno.ENOSYS`` when the Python build lacks the native + locking primitive; the caller may retry on the same descriptor. + + .. versionadded:: 3.30.0 + + """ + _unlock_fd(fd) + + +__all__ = [ + "lock_descriptor", + "unlock_descriptor", +] diff --git a/hug/lib/python3.12/site-packages/filelock/_error.py b/hug/lib/python3.12/site-packages/filelock/_error.py new file mode 100644 index 0000000000000000000000000000000000000000..fb5831306a90d2e1369c6242c7a16f52fa26fddc --- /dev/null +++ b/hug/lib/python3.12/site-packages/filelock/_error.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +__lazy_modules__ = ["typing"] + +from typing import Final + + +class Timeout(TimeoutError): # ruff:ignore[error-suffix-on-exception-name] # public exception name; renaming breaks the API + """Raised when the lock could not be acquired in *timeout* seconds.""" + + def __init__(self, lock_file: str, holder: str | None = None) -> None: + super().__init__() + self._lock_file = lock_file + self._holder = holder + + def __reduce__(self) -> tuple[type[Timeout], tuple[str], dict[str, object]]: + # holder rides in the state so a subclass taking only lock_file, and a 4.0.12 reader, can still unpickle it. + return self.__class__, (self._lock_file,), self.__dict__ + + def __str__(self) -> str: # pragma: needs hard-link + message: Final = f"The file lock '{self._lock_file}' could not be acquired." + if self._holder is None: + return message + return f"{message} It is held by {self._holder}, which this host cannot probe." + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.lock_file!r})" + + @property + def lock_file(self) -> str: + """The path of the file lock.""" + return self._lock_file + + +class SoftFileLockLifetimeWarning(FutureWarning): + """ + The configured soft-lock lifetime permits overlapping live holders after expiry. + + A :class:`FutureWarning` shows by default wherever the lock is configured; a :class:`DeprecationWarning` shows only + in ``__main__``. + + """ + + +class LeaseSettingsMismatch(ValueError): # ruff:ignore[error-suffix-on-exception-name] # public exception name; renaming breaks the API + """A lease contender disagrees with the published claim about how long the lease lasts.""" + + +class SoftFileLockProtocolError(OSError): + """Raised when strict soft-lock state cannot be interpreted without risking overlap.""" + + def __init__(self, lock_file: str, claim_name: str | None, reason: str) -> None: + self._lock_file = lock_file + self._claim_name = claim_name + self._reason = reason + super().__init__(self.__str__()) + + def __reduce__( + self, + ) -> tuple[ + type[SoftFileLockProtocolError], tuple[str, str | None, str], dict[str, object] + ]: # pragma: needs hard-link + return self.__class__, (self._lock_file, self._claim_name, self._reason), self.__dict__ + + def __str__(self) -> str: + location = self._lock_file if self._claim_name is None else f"{self._lock_file}: claim {self._claim_name!r}" + return f"Invalid strict soft-lock state at {location}: {self._reason}" + + @property + def lock_file(self) -> str: # pragma: needs hard-link + """The requested lock path.""" + return self._lock_file + + @property + def claim_name(self) -> str | None: # pragma: needs hard-link + """The claim that caused the error, if scanning identified one.""" + return self._claim_name + + @property + def reason(self) -> str: # pragma: needs hard-link + """The protocol validation failure.""" + return self._reason + + +__all__ = [ + "LeaseSettingsMismatch", + "SoftFileLockLifetimeWarning", + "SoftFileLockProtocolError", + "Timeout", +] diff --git a/hug/lib/python3.12/site-packages/filelock/_identity.py b/hug/lib/python3.12/site-packages/filelock/_identity.py new file mode 100644 index 0000000000000000000000000000000000000000..4abc60d0406be2be484b5f2a918952c6d3cd14e7 --- /dev/null +++ b/hug/lib/python3.12/site-packages/filelock/_identity.py @@ -0,0 +1,328 @@ +from __future__ import annotations + +__lazy_modules__ = ["hashlib", "pathlib", "socket", "typing"] + +import hashlib +import os +import socket +import sys +from errno import EPERM, ESRCH +from pathlib import Path +from typing import Final + +#: Every marker format in this package caps the hostname at the RFC 1123 limit and reads a longer one as malformed. +_HOST_NAME_LIMIT: Final[int] = 253 +#: The bytes those formats carry verbatim: printable non-space ASCII, less the ``?`` reserved for the escape. +_HOST_NAME_VERBATIM: Final[frozenset[int]] = frozenset(range(0x21, 0x7F)) - {ord("?")} +#: The characters at the end of an over-long name that carry a digest of the part cut off. +_HOST_NAME_DIGEST_LEN: Final[int] = 8 +#: The kernel ABI fixes the initial PID namespace's inode (``PID_NS_INIT_INO`` in ``linux/nsfs.h``); every namespace +#: created later draws one from ``0xF0000000`` up, so this value names the host's own namespace. +_INITIAL_PID_NAMESPACE: Final[int] = 0xEFFFFFFC +_NAMESPACE_MARK: Final[str] = "?pidns-" + + +def host_name() -> str: + """ + The hostname recorded alongside an owner, so a marker written on another machine is never probed here. + + Marker formats here hold the field to printable non-space ASCII, while ``socket.gethostname()`` reports whatever + the kernel stores: a space, a newline that forges an extra marker line, or a byte no codec encodes, which Python + hands back as a surrogate. A holder that publishes one raw writes a marker it cannot read back, and loses the lock + to the first peer that ages it out as malformed. ``?`` is illegal in a hostname, so escaping it along with every + out-of-grammar byte as ``?`` leaves a real name untouched and still keeps two hosts apart, which + :func:`owner_is_stale` relies on to refuse to probe a foreign PID. + + Linux appends the PID namespace, as Mercurial's lock prefix does. Two containers in one pod share the hostname and + the boot id but not the PID namespace, so a PID recorded in one names another process, or none, in the other. The + suffix starts with a ``?`` no escape can produce, so ``a`` in a namespace never reads as a host literally named + ``a?pidns-...``. The initial namespace adds nothing, which keeps a host's name what older releases recorded. + + An over-long name ends in a digest of the part cut to fit the limit. A bare cut gives every name that shares the + kept prefix one identity, and :func:`owner_is_stale` would then probe a local PID for another host's holder. + """ + name = ( + "".join( + chr(byte) if byte in _HOST_NAME_VERBATIM else f"?{byte:02x}" + for byte in socket.gethostname().encode("utf-8", "surrogateescape") + ) + + _pid_namespace_suffix() + ) + if len(name) > _HOST_NAME_LIMIT: + keep = _HOST_NAME_LIMIT - _HOST_NAME_DIGEST_LEN - 1 + name = f"{name[:keep]}-{hashlib.sha256(name[keep:].encode()).hexdigest()[:_HOST_NAME_DIGEST_LEN]}" + # An escape is three characters and a kept byte is never '?', so a bare '?' can only mean an empty hostname. + return name or "?" + + +def _pid_namespace_suffix() -> str: + try: + namespace: Final = Path("/proc/self/ns/pid").stat().st_ino + except OSError: # only Linux 3.8+ has the file, and a sandbox may hide it; nothing else tells namespaces apart + return "" + return "" if namespace == _INITIAL_PID_NAMESPACE else f"{_NAMESPACE_MARK}{namespace:x}" + + +def owner_is_stale(pid: int, hostname: str, start_token: int | None) -> bool: + """ + Whether the recorded owner is provably gone, so reclaiming its marker cannot detach a live holder. + + Fail closed: return ``True`` only when this process can prove the exact recorded owner is dead. A marker from + another host cannot be probed; a live PID whose start token still matches, or whose token cannot be read, is the + holder or is indistinguishable from it; a live PID whose start token differs is a recycled PID, so the process that + wrote the marker is gone. PostgreSQL, Qt ``QLockFile`` and Mercurial all break a stale lock only on proof of death + and treat an unreadable or foreign owner as still holding. + """ + if hostname != host_name(): + return False + if process_alive(pid) and ( + start_token is None or (current := process_start_token(pid)) is None or _same_start(current, start_token) + ): + return False + # Only a bare hostname can be an older release's, written from a container that shares this host's name. + return _NAMESPACE_MARK in hostname or not _owner_runs_elsewhere(pid, start_token) + + +def owner_is_current_process(pid: int, hostname: str, start_token: int | None) -> bool: + """ + Whether the recorded owner is this process, so a marker a recycled PID left behind never reads as ours. + + Fail closed: a recorded start token must match this process's. One that differs was written by an earlier process + that reused this PID, and when this process cannot read its own token nothing proves the marker is ours. A marker + without a token falls back to the PID and hostname. + """ + if pid != os.getpid() or hostname != host_name(): + return False + return start_token is None or start_token == process_start_token(pid) + + +def _same_start(current: int, recorded: int) -> bool: + # A Linux token carries the boot id in its high 64 bits, or 0 there when the process could not read the boot id. + # systemd's ProcSubset=pid hides /proc/sys from one unit, so one process may lack the boot id while its peer has + # it. A missing boot id then proves nothing, and only starttime is compared. Other platforms' tokens fit in the + # low bits. + return current == recorded or (0 in {current >> 64, recorded >> 64} and current % 2**64 == recorded % 2**64) + + +if sys.platform == "win32": # pragma: win32 cover + import ctypes + from ctypes import wintypes + + _KERNEL32: Final[ctypes.WinDLL] = ctypes.WinDLL("kernel32", use_last_error=True) + _KERNEL32.CloseHandle.argtypes = [wintypes.HANDLE] + _KERNEL32.CloseHandle.restype = wintypes.BOOL + _KERNEL32.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD] + _KERNEL32.OpenProcess.restype = wintypes.HANDLE + _KERNEL32.GetProcessTimes.argtypes = [ + wintypes.HANDLE, + ctypes.POINTER(wintypes.FILETIME), + ctypes.POINTER(wintypes.FILETIME), + ctypes.POINTER(wintypes.FILETIME), + ctypes.POINTER(wintypes.FILETIME), + ] + _KERNEL32.GetProcessTimes.restype = wintypes.BOOL + + _WIN_SYNCHRONIZE: Final[int] = 0x100000 + _WIN_PROCESS_QUERY_LIMITED_INFORMATION: Final[int] = 0x1000 + _WIN_ERROR_INVALID_PARAMETER: Final[int] = 87 + _WIN_INHERIT_HANDLE: Final[bool] = False + + def process_alive(pid: int) -> bool: + """Whether a process with this PID exists, treating an access denial as proof it does.""" + handle = _KERNEL32.OpenProcess(_WIN_SYNCHRONIZE, _WIN_INHERIT_HANDLE, pid) + if handle: + _KERNEL32.CloseHandle(handle) + return True + return ctypes.get_last_error() != _WIN_ERROR_INVALID_PARAMETER + + def process_start_token(pid: int) -> int | None: + """The process creation FILETIME as a 100ns tick count, or ``None`` when it cannot be read.""" + handle = _KERNEL32.OpenProcess(_WIN_PROCESS_QUERY_LIMITED_INFORMATION, _WIN_INHERIT_HANDLE, pid) + if not handle: + return None + creation, exit_time, kernel_time, user_time = (wintypes.FILETIME() for _ in range(4)) + try: + if not _KERNEL32.GetProcessTimes( + handle, + ctypes.byref(creation), + ctypes.byref(exit_time), + ctypes.byref(kernel_time), + ctypes.byref(user_time), + ): + return None # pragma: no cover # win32 GetProcessTimes failure path; not reproducible on a live handle + finally: + _KERNEL32.CloseHandle(handle) + return (creation.dwHighDateTime << 32) | creation.dwLowDateTime + +else: # pragma: win32 no cover + + def process_alive(pid: int) -> bool: + """Whether a process with this PID exists, treating an access denial (``EPERM``) as proof it does.""" + try: + os.kill(pid, 0) + except OSError as error: + if error.errno == ESRCH: + return False + if error.errno == EPERM: + return True + raise + return True + + if sys.platform in {"linux", "android"}: # pragma: linux cover + # Termux/Android reports sys.platform == "android" but runs the Linux kernel, so /proc//stat and the boot + # id are the same reliable start-time source; treat it exactly like Linux rather than the tokenless fallback. + # comm (field 2) is wrapped in parentheses and may itself contain spaces or a ')', so the fixed fields start + # after the final ')'. starttime is field 22 overall, the twentieth of those trailing fields (index 19). + _STARTTIME_INDEX: Final[int] = 19 + + def _read_boot_id() -> int: + # starttime is measured in clock ticks since boot, so on its own it repeats across a reboot. Folding the + # boot id into the high bits makes the Linux token reboot-safe like the absolute clocks macOS and Windows + # expose, while staying a single integer so a 3.29 reader still parses the third marker line. 0 when this + # process cannot read the boot id leaves the bare starttime, which _same_start compares alone. + try: + boot_id = Path("/proc/sys/kernel/random/boot_id").read_text(encoding="ascii") + return int(boot_id.strip().replace("-", ""), 16) + except (OSError, ValueError): + return 0 + + _BOOT_ID: Final[int] = _read_boot_id() + #: ``TIME_NS_INIT_INO`` in ``linux/proc_ns.h``, fixed by the kernel ABI like the initial PID namespace's inode. + _INITIAL_TIME_NAMESPACE: Final[int] = 0xEFFFFFFA + + def process_start_token(pid: int) -> int | None: + """ + The ``/proc//stat`` ``starttime`` folded with the boot id. + + ``None`` when the process is gone, or when this process runs in a time namespace with a boottime offset. + Since Linux 5.6 the kernel shifts ``starttime`` by the reader's offset, so a value read there differs + from what a peer outside reads. + """ + return _read_start_token(pid) if _starttime_unshifted() else None + + def _starttime_unshifted() -> bool: + # setns can move this process into another time namespace, so ask on every read. + try: + namespace: Final = Path("/proc/self/ns/time").stat().st_ino + except FileNotFoundError: # no entry means no time namespaces on this kernel + return True + except OSError: # any other failure leaves the namespace unknown, so starttime is not comparable + return False + if namespace == _INITIAL_TIME_NAMESPACE: + return True + # A container runtime may give each container a time namespace without offsets. timens_offsets describes + # the namespace this process's children get, its own unless it unshared one since. + try: + if Path("/proc/self/ns/time_for_children").stat().st_ino != namespace: + return False + offsets: Final = Path("/proc/self/timens_offsets").read_text(encoding="ascii") + except OSError: + return False + # The clock goes by name since Linux 5.8 and by clock id before. + return any(line.split() in (["boottime", "0", "0"], ["7", "0", "0"]) for line in offsets.splitlines()) + + def _read_start_token(pid: int) -> int | None: + try: + data = Path(f"/proc/{pid}/stat").read_bytes() + except OSError: + return None + # psutil identifies a process by the same (pid, starttime) pair; the boot id extends that across reboots. + fields = data[data.rfind(b")") + 1 :].split() + if len(fields) <= _STARTTIME_INDEX: # pragma: no cover # a truncated /proc read, never seen in practice + return None + try: + starttime = int(fields[_STARTTIME_INDEX]) + except ValueError: # pragma: no cover # /proc always renders starttime as an integer + return None + return (_BOOT_ID << 64) | starttime + + def _owner_runs_elsewhere(pid: int, start_token: int | None) -> bool: + """ + Whether the owner runs in another PID namespace as *pid*. + + An older release in a container sharing this hostname recorded its in-container PID, which means nothing + here. Readers without a boottime offset read the same starttime and boot id. A task with the recorded + start token whose innermost PID is *pid* is therefore the owner; one that started in the same tick is not. + A reader with an offset cannot compare start tokens, so the innermost PID alone keeps the marker. + """ + if start_token is None or (_BOOT_ID and start_token >> 64 not in {0, _BOOT_ID}): + return False + unshifted: Final = _starttime_unshifted() + try: + with os.scandir("/proc") as entries: + return any( + entry.name.isdigit() + and (task := int(entry.name)) != pid + and ( + not unshifted + or ((token := _read_start_token(task)) is not None and _same_start(token, start_token)) + ) + and _innermost_pid(task) == pid + for entry in entries + ) + except OSError: + # A /proc this process cannot list proves nothing either way, which leaves 4.0.12's verdict. + return False + + def _innermost_pid(task: int) -> int | None: + try: + status: Final = Path(f"/proc/{task}/status").read_bytes() + except OSError: # pragma: no cover # the task exited between the stat read and this one + return None + # The kernel ends a line only with "\n". A task names itself, and its Name line keeps "\r", "\v" and the + # other separators that a text-mode read and str.splitlines() split at. A name could then forge a line. + return next( + (int(line.split()[-1]) for line in status.split(b"\n") if line.startswith(b"NSpid:")), + None, # kernels before 4.1 record no namespace PIDs + ) + + elif sys.platform == "darwin": # pragma: darwin cover + import ctypes + import struct + + _LIBC: Final[ctypes.CDLL] = ctypes.CDLL(None, use_errno=True) + _CTL_KERN: Final[int] = 1 + _KERN_PROC: Final[int] = 14 + _KERN_PROC_PID: Final[int] = 1 + # kinfo_proc opens with kp_proc.p_starttime (a struct timeval) at offset 0: int64 seconds, int32 microseconds. + # The offset is fixed by the struct chain kinfo_proc -> extern_proc -> p_un, so a read at 0 is not a guess. + _TIMEVAL_AT_ZERO: Final[str] = " int | None: + """The process start time in microseconds from ``sysctl(KERN_PROC_PID)``, or ``None`` when it is gone.""" + mib = (ctypes.c_int * 4)(_CTL_KERN, _KERN_PROC, _KERN_PROC_PID, pid) + length = ctypes.c_size_t(0) + # The size probe reports the kinfo_proc size for any PID, so the read below, not the probe, tells a live + # process from a gone one: a gone PID leaves the fetch a zero-length success, so re-check the length after. + if _LIBC.sysctl(mib, 4, None, ctypes.byref(length), None, 0) != 0: + return None + buffer = (ctypes.c_char * length.value)() + if _LIBC.sysctl(mib, 4, buffer, ctypes.byref(length), None, 0) != 0 or length.value < _TIMEVAL_SIZE: + return None + seconds, microseconds = struct.unpack_from(_TIMEVAL_AT_ZERO, buffer.raw, 0) + return seconds * 1_000_000 + microseconds + + else: # pragma: no cover # a POSIX platform without a proven start-time source falls back to fail-closed liveness + + def process_start_token(pid: int) -> int | None: + """No proven start-time source, so the owner carries no token and liveness rests on the PID alone.""" + del pid + return None + + +if sys.platform not in {"linux", "android"}: # pragma: linux no cover # pragma: android no cover + + def _owner_runs_elsewhere(pid: int, start_token: int | None) -> bool: + """Without Linux PID namespaces, a PID that is gone or recycled leaves its owner nowhere else to run.""" + del pid, start_token + return False + + +__all__ = [ + "host_name", + "owner_is_current_process", + "owner_is_stale", + "process_alive", + "process_start_token", +] diff --git a/hug/lib/python3.12/site-packages/filelock/_lease.py b/hug/lib/python3.12/site-packages/filelock/_lease.py new file mode 100644 index 0000000000000000000000000000000000000000..d328eaf4be6f35b93d81a64cc95a640ba23a63c3 --- /dev/null +++ b/hug/lib/python3.12/site-packages/filelock/_lease.py @@ -0,0 +1,402 @@ +from __future__ import annotations + +__lazy_modules__ = [ + "contextlib", + "filelock._error", + "filelock._identity", + "filelock._soft", + "filelock._util", + "math", + "secrets", + "threading", + "weakref", +] + +import os +import secrets +import sys +import time +from contextlib import suppress +from dataclasses import dataclass +from math import isfinite +from threading import Event, Thread, current_thread +from typing import TYPE_CHECKING, Final, Literal, cast +from weakref import WeakMethod + +from filelock._api import _check_timeout_max, _seconds +from filelock._error import LeaseSettingsMismatch +from filelock._identity import owner_is_stale +from filelock._marker import MarkerSoftFileLock, OwnerMode, OwnerRecord, parse_marker +from filelock._soft import _read_lock_file +from filelock._util import pin_inode, touch + +if TYPE_CHECKING: + from collections.abc import Callable, Mapping + from typing import Unpack + + from filelock._api import LockOptions, _ExtraValue + +#: ``evicted`` is a :class:`~filelock.SoftReadWriteLock` loss: a peer waited out the stale threshold and committed a +#: generation without this holder. +CompromiseReason = Literal["marker-missing", "owner-changed", "refresh-failed", "evicted"] + +_RefreshOutcome = Literal["ok", "transient", "marker-missing", "owner-changed"] +_DEFAULT_LEASE_DURATION: Final[float] = 30.0 + + +@dataclass(frozen=True) +class LeaseCompromise: + """Why a held lease stopped being this process's to hold, and the error that ended its refresh, if any.""" + + lock_file: str + token: str + reason: CompromiseReason + error: Exception | None = None + + +@dataclass(frozen=True) +class _Heartbeat: + """A running heartbeat and the event that stops it, which only ever exist together.""" + + thread: Thread + stop: Event + + +@dataclass +class _LeaseClaim: + """The state of one claim: its token, its heartbeat, and how that claim was lost.""" + + token: str | None = None + compromise: LeaseCompromise | None = None + heartbeat: _Heartbeat | None = None + + +class SoftFileLease(MarkerSoftFileLock): + """ + Existence lock whose claim expires, so a peer may take it while the previous holder still runs. + + A lease trades mutual exclusion for progress. The holder publishes a claim and refreshes it every + ``heartbeat_interval`` seconds; a contender takes the marker once it has seen no refresh for ``lease_duration`` + seconds of its own monotonic clock, so clock skew between hosts cannot expire a live claim. Nothing stops the + expired holder: it keeps running, and it keeps using whatever the lock protects. Treat the lease as a hint about + who *should* be working, not as a guarantee that only one worker is. + + To make a protected resource reject a superseded holder, that resource must be linearizable and must fence on a + monotonic generation it controls. :attr:`token` names a claim; it does not fence one. Where overlap is unacceptable, + use :class:`StrictSoftFileLock ` instead. + + Every contender for a path must agree on ``lease_duration``. A contender that finds a claim published under a + different duration raises :class:`LeaseSettingsMismatch ` rather than apply its own + expiry to a peer that never agreed to it. + + Expiry reclaims less on Windows, which refuses to rename or delete a file another process holds open. A peer there + takes an expired claim only once the previous holder's process exits and its handle closes; a holder that lives on + but stops refreshing keeps the marker. Unix reclaims the marker either way. + + ``on_compromise`` fires from the heartbeat thread when a refresh fails, or when the marker vanishes or names another + owner. The holder should stop touching the protected resource when it runs. Because it runs on that thread, a + ``release()`` inside it only takes effect when the lease was built with ``thread_local=False``; the default + thread-local context hides the claim from every thread but the one that acquired it, so the release does nothing. + Signal the acquiring thread instead when the context stays thread-local. + + .. versionadded:: 3.30.0 + + """ + + _owner_mode: OwnerMode = "lease" + + #: lease_duration replaces the legacy age-based lifetime, so accepting both would give one lock two expiry clocks. + _lifetime_supported: bool = False + _lifetime_unsupported_reason: str = "lease_duration sets when a lease expires" + + def __init__( + self, + lock_file: str | os.PathLike[str], + *, + lease_duration: float = _DEFAULT_LEASE_DURATION, + heartbeat_interval: float | None = None, + on_compromise: Callable[[LeaseCompromise], None] | None = None, + **kwargs: Unpack[LockOptions], + ) -> None: + """ + Create a lease. + + :param lease_duration: seconds a contender must see the marker go unrefreshed before it may take the claim. + Every contender for the path must pass the same value. + :param heartbeat_interval: seconds between refreshes. Defaults to a third of ``lease_duration``, leaving room + for two missed refreshes before a peer may take the claim. Must be shorter than ``lease_duration`` and at + most :data:`threading.TIMEOUT_MAX`. + :param on_compromise: called from the heartbeat thread with a :class:`LeaseCompromise` when the claim is lost. + :param kwargs: every other :class:`BaseFileLock ` option, ``timeout`` and ``mode`` among + them. The metaclass passes them all by keyword, and taking them here lets + :class:`AsyncSoftFileLease ` add the async plumbing a fixed signature would + hide. + + """ + # A plain float: the marker records its repr, which NumPy's float64 spells in a form no parser reads. + if not isfinite(lease_duration := _seconds("lease_duration", lease_duration)) or lease_duration <= 0: + msg = f"lease_duration must be positive and finite, got {lease_duration!r}" + raise ValueError(msg) + heartbeat_interval = ( + lease_duration / 3 if heartbeat_interval is None else _seconds("heartbeat_interval", heartbeat_interval) + ) + if not 0 < heartbeat_interval < lease_duration: + msg = f"heartbeat_interval must be positive and below lease_duration, got {heartbeat_interval!r}" + raise ValueError(msg) + _check_timeout_max("heartbeat_interval", heartbeat_interval) + super().__init__(lock_file, **kwargs) + self._lease_duration = lease_duration + self._heartbeat_interval = heartbeat_interval + self._on_compromise = on_compromise + # Two threads lazily creating a shared claim could each get one, and release() would miss the heartbeat's. + if not self.is_thread_local(): + self._context.lease_claim = _LeaseClaim() + + def _singleton_extra_mismatches(self, kwargs: Mapping[str, _ExtraValue], /) -> dict[str, tuple[str, str]]: + mismatches = super()._singleton_extra_mismatches(kwargs) + lease_duration: Final = _seconds( + "lease_duration", cast("float", kwargs.get("lease_duration", _DEFAULT_LEASE_DURATION)) + ) + if lease_duration != self._lease_duration: + mismatches["lease_duration"] = (str(lease_duration), str(self._lease_duration)) + # An omitted heartbeat_interval resolves as in __init__; a differing duration is already reported above. + heartbeat_interval: Final = ( + self._lease_duration / 3 + if (passed := kwargs.get("heartbeat_interval")) is None + else _seconds("heartbeat_interval", cast("float", passed)) + ) + if heartbeat_interval != self._heartbeat_interval: + mismatches["heartbeat_interval"] = (str(passed), str(self._heartbeat_interval)) + # A callback compares by identity, as on_acquired does: two equal callables can close over different state. + if (on_compromise := kwargs.get("on_compromise")) is not self._on_compromise: + mismatches["on_compromise"] = (str(on_compromise), str(self._on_compromise)) + return mismatches + + @property + def _claim(self) -> _LeaseClaim: + # Per hold, so another thread's failed acquire cannot stop the holder's heartbeat. + state: Final = self._hold_state() + if (claim := state.lease_claim) is None: + claim = state.lease_claim = _LeaseClaim() + return claim + + @property + def lease_duration(self) -> float: + """The staleness in seconds after which a contender may take this claim.""" + return self._lease_duration + + @property + def token(self) -> str | None: + """ + The token naming the claim this process published. + + :returns: the token while the lease is held, ``None`` otherwise. It identifies a claim; it does not fence one. + + """ + return self._claim.token + + @property + def compromise(self) -> LeaseCompromise | None: + """ + The loss of claim the heartbeat observed. + + The value outlives ``release()``, so a holder can check after leaving the ``with`` block whether its work may + have overlapped a successor; the next acquisition resets it, as :attr:`SoftReadWriteLock.compromise + ` does. + + :returns: the :class:`LeaseCompromise` of the latest claim, or ``None`` while that claim holds or held to its + release + + """ + return self._claim.compromise + + def _acquire(self) -> None: + claim = self._claim + self._stop_heartbeat() # no earlier claim's heartbeat outlives the acquisition of the next one + # The published record reads the token, so it exists before the marker is written and goes back on failure. + claim.token = token = secrets.token_hex(16) + claim.compromise = None + try: + super()._acquire() + except BaseException: + claim.token = None + raise + # The context is thread-local by default, so the heartbeat thread cannot read the descriptor this one just + # published, nor the claim this one owns. Hand it the fd, the inode it verified and the claim instead. + if (fd := self._context.lock_file_fd) is not None and ( + identity := self._context.lock_file_fd_identity + ) is not None: + self._start_heartbeat(claim, fd, identity, token) + else: + claim.token = None + + def _release(self) -> None: + self._stop_heartbeat() + self._claim.token = None + super()._release() + + def _published_record(self) -> OwnerRecord: + return super()._published_record()._replace(token=self._claim.token, lease_duration=self._lease_duration) + + def _try_break_stale_lock(self) -> None: + # A break can fail for reasons a contender must ride out rather than raise on: a peer broke the marker first, + # or Windows refuses to rename a file whose holder still has it open. Poll again instead. + with suppress(OSError), pin_inode(self._lock_path) as pinned: + self._try_break_pinned_lock(pinned) + + def _try_break_pinned_lock(self, pinned: int | None) -> None: + if (peer := self._read_peer()) is None: + # Not a readable protocol 2 lease record: a partial write, a foreign or legacy protocol 1 marker, or the + # strict sentinel. The base self-heal evicts a genuinely malformed marker once it ages past the grace + # window and leaves a legitimate legacy or strict holder in place, so a corrupt marker no longer wedges + # every lease contender until its own timeout. + super()._try_break_stale_lock() + return + owner, marker = peer + if pinned not in {None, marker.st_ino}: # pragma: win32 no cover # Windows pins nothing + return # replaced before the read, so nothing keeps the number read from reuse + # Only a peer that published a lease agreed to be superseded by age; an exclusive owner is reclaimed only once + # it is provably dead, and a contract this version does not know never is. Raise the mismatch outside the read + # so the suppression cannot swallow it. + if owner.mode == "lease" and owner.lease_duration != self._lease_duration: + msg = ( + f"{self.lock_file} holds a lease of {owner.lease_duration!r}s but this contender configured " + f"{self._lease_duration!r}s; every contender for a path must agree on lease_duration" + ) + raise LeaseSettingsMismatch(msg) + # A dead or recycled owner is reclaimed at once, whether it held a lease or an exclusive marker; a live lease + # owner whose marker stops being refreshed is superseded on the schedule every contender agreed to. + if owner.mode != "unknown" and owner_is_stale(owner.pid, owner.hostname, owner.start): + self._take_over(marker, owner.pid) + return + if owner.mode == "lease" and self._marker_unchanged_for(marker) >= self._lease_duration: + self._take_over(marker, None) + + def _read_peer(self) -> tuple[OwnerRecord, os.stat_result] | None: + with suppress(OSError, ValueError): + content, marker = _read_lock_file(self._lock_path) + if (owner := parse_marker(content)) is not None: + return owner, marker + return None + + def _start_heartbeat(self, claim: _LeaseClaim, fd: int, identity: tuple[int, int], token: str) -> None: + # The thread watches the event it was handed rather than whatever the claim names later: a heartbeat that + # outlives its join timeout would otherwise adopt the next acquisition's event and never stop. + stop: Final = Event() + loop: Final = _RefreshLoop( + # A strong reference from a running thread would keep a dropped lease alive, so __del__ would never release + # it and its marker would outlive the process. The callback stops a heartbeat that __del__ could not reach. + report=WeakMethod(self._report_compromise, lambda _: stop.set()), + lock_file=self._lock_path, + claim=claim, + fd=fd, + identity=identity, + token=token, + stop=stop, + interval=self._heartbeat_interval, + duration=self._lease_duration, + ) + thread: Final = Thread(target=loop.run, name=f"filelock-lease-{os.getpid()}", daemon=True) + # Record the heartbeat before starting the thread so a release racing this acquire on a shared, + # non-thread-local claim always sees it and sets the stop event; the thread then exits at its first wait + # instead of outliving the release. A start that raises leaves the unstarted thread for _stop_heartbeat. + claim.heartbeat = _Heartbeat(thread, stop) + thread.start() + + def _stop_heartbeat(self) -> None: + claim = self._claim + if (heartbeat := claim.heartbeat) is None: + return + heartbeat.stop.set() + claim.heartbeat = None + # An unstarted thread has nothing to join, on_compromise may release from the heartbeat thread itself, and + # during finalization a daemon thread is frozen, so joining it raises PythonFinalizationError (3.14+). + if heartbeat.thread.ident is not None and heartbeat.thread is not current_thread() and not sys.is_finalizing(): + heartbeat.thread.join(timeout=self._heartbeat_interval) + + def _report_compromise( + self, + claim: _LeaseClaim, + reason: CompromiseReason, + error: OSError | None, + token: str, + ) -> None: + # Record it on the claim this heartbeat serves, not on self._claim: a thread-local claim read from the + # heartbeat thread is a different, empty one, so the holder would never see the loss it is being told about. + # The token is the one this thread published, not claim.token, which a release may already have cleared. + claim.compromise = LeaseCompromise(lock_file=self.lock_file, token=token, reason=reason, error=error) + if self._on_compromise is not None: + self._on_compromise(claim.compromise) + + +@dataclass(frozen=True) +class _RefreshLoop: + """What a heartbeat thread needs to refresh one claim, holding the lease itself only weakly.""" + + report: WeakMethod[Callable[[_LeaseClaim, CompromiseReason, OSError | None, str], None]] + lock_file: str + claim: _LeaseClaim + fd: int + identity: tuple[int, int] + token: str + stop: Event + interval: float + duration: float + + def run(self) -> None: + # A transient error (ESTALE / EIO on the NFS-style filesystems a lease targets) is not a loss. Like restic, call + # the claim unrefreshable once failures run long enough that a contender could take it before the next success. + last_success, missing = time.monotonic(), False + while not self.stop.wait(self.interval): + outcome, error = self._refresh() + if outcome == "ok": + last_success, missing = time.monotonic(), False + continue + # A peer's stale break exchanges a live marker aside for the length of its check, so only a second miss or + # change in a row is a loss. + if outcome in {"marker-missing", "owner-changed"} and not missing: + missing = True + continue + if outcome == "transient" and time.monotonic() - last_success < self.duration - self.interval: + continue + if (report := self.report()) is not None: + report(self.claim, "refresh-failed" if outcome == "transient" else outcome, error, self.token) + return + + def _refresh(self) -> tuple[_RefreshOutcome, OSError | None]: + try: + st = os.lstat(self.lock_file) + except FileNotFoundError as error: + return self._refresh_unnamed(error) + except OSError as error: + return "transient", error + # A peer that took the expired claim replaced the marker, so the pathname now names its inode, not ours. + if (st.st_dev, st.st_ino) != self.identity: + return "owner-changed", None + return self._touch() + + def _refresh_unnamed(self, name_error: FileNotFoundError) -> tuple[_RefreshOutcome, OSError | None]: + # A renamed directory takes the marker with it; the name is gone but the inode behind the descriptor keeps its + # link. + try: + linked = os.fstat(self.fd).st_nlink > 0 + except OSError as error: + return "transient", error + if not linked: + return "marker-missing", name_error + return self._touch() + + def _touch(self) -> tuple[_RefreshOutcome, OSError | None]: + try: + touch(self.lock_file, fd=self.fd) + except OSError as error: + return "transient", error + return "ok", None + + +__all__ = [ + "CompromiseReason", + "LeaseCompromise", + "SoftFileLease", + "_LeaseClaim", +] diff --git a/hug/lib/python3.12/site-packages/filelock/_marker.py b/hug/lib/python3.12/site-packages/filelock/_marker.py new file mode 100644 index 0000000000000000000000000000000000000000..2a7c694c5382de3b2235c62feb24bac25fd9c497 --- /dev/null +++ b/hug/lib/python3.12/site-packages/filelock/_marker.py @@ -0,0 +1,189 @@ +from __future__ import annotations + +__lazy_modules__ = ["contextlib", "filelock._identity", "filelock._util", "math"] + +import math +import os +import re +from contextlib import suppress +from typing import Final, Literal, NamedTuple + +from filelock._identity import host_name, owner_is_current_process, owner_is_stale, process_start_token +from filelock._soft import SoftFileLock, _read_lock_file, parse_decimal +from filelock._util import pin_inode, write_all + +#: Protocol 1 is the legacy ``\n\n[\n]`` marker that :class:`SoftFileLock` still writes. +#: Protocol 2 carries the owner mode and the lease claim. A protocol 1 reader treats a protocol 2 marker as malformed +#: and evicts it after its grace period, so the two never guarantee mutual exclusion against each other. +_PROTOCOL: Final[str] = "filelock/2" + +_MAX_PID: Final[int] = 2**31 - 1 + +#: The forms ``repr`` gives a non-negative finite float or int; ``float()`` would also accept whitespace, underscores +#: and non-ASCII digits. +_DURATION: Final[re.Pattern[str]] = re.compile(r"[0-9]+(?:\.[0-9]+)?(?:e[+-][0-9]+)?", re.ASCII) + +#: Preserve unknown contracts so contenders cannot mistake them for malformed, reclaimable markers. +OwnerMode = Literal["lease", "exclusive", "unknown"] + + +class MarkerSoftFileLock(SoftFileLock): + """An existence lock whose marker carries a protocol 2 owner record.""" + + #: Age-based expiry requires a lease contract; an exclusive holder grants none. + _owner_mode: OwnerMode = "exclusive" + + @property + def owner(self) -> OwnerRecord | None: + """ + The owner named by the marker on disk. + + :returns: the published record, or ``None`` when no marker exists or its record is malformed or protocol 1 + + """ + return self._read_owner() + + @property + def pid(self) -> int | None: + """ + The PID of the process holding this lock, read from the marker. + + :returns: the PID, or ``None`` when no marker exists or its record is unreadable + + """ + return None if (owner := self._read_owner()) is None else owner.pid + + @property + def is_lock_held_by_us(self) -> bool: + """ + Whether the marker on disk names this process. + + :returns: ``True`` when the marker names this process's PID, hostname and, when recorded, start token + + """ + owner = self._read_owner() + return owner is not None and owner_is_current_process(owner.pid, owner.hostname, owner.start) + + def force_break(self) -> None: + """ + Remove the marker whoever holds it, so a later contender can acquire. + + Forced breaking voids mutual exclusion: the previous holder keeps running and keeps using whatever the lock + protects. Reserve it for an operator clearing a marker whose holder is known to be gone. + """ + self.break_lock() + + def _try_break_stale_lock(self) -> None: + with suppress(OSError, ValueError), pin_inode(self._lock_path) as pinned: + content, marker = _read_lock_file(self._lock_path) + owner: Final[OwnerRecord | None] + if pinned not in {None, marker.st_ino}: # pragma: win32 no cover # Windows pins nothing + return # replaced before the read, so nothing keeps the number read from reuse + if (owner := parse_marker(content)) is None: + super()._try_break_stale_lock() + elif owner.mode != "unknown" and owner_is_stale(owner.pid, owner.hostname, owner.start): + self._take_over(marker, owner.pid) + + @classmethod + def _recorded_holder(cls, content: str | None) -> tuple[int, str] | None: + if (owner := parse_marker(content)) is not None: + return owner.pid, owner.hostname + return super()._recorded_holder(content) + + @classmethod + def _marker_is_live(cls, content: str | None, breaker_pid: int) -> bool: # pragma: win32 no cover + if (owner := parse_marker(content)) is None: + return super()._marker_is_live(content, breaker_pid) + return owner.mode == "unknown" or ( + (owner.pid != breaker_pid or owner.hostname != host_name()) + and not owner_is_stale(owner.pid, owner.hostname, owner.start) + ) + + def _read_owner(self) -> OwnerRecord | None: + with suppress(OSError, ValueError): + return parse_marker(_read_lock_file(self._lock_path)[0]) + return None + + def _write_lock_info(self, fd: int) -> None: + write_all(fd, encode_marker(self._published_record())) + + def _published_record(self) -> OwnerRecord: + return OwnerRecord( + pid=os.getpid(), + hostname=host_name(), + mode=self._owner_mode, + start=process_start_token(os.getpid()), + ) + + +class OwnerRecord(NamedTuple): + """Keep optional metadata when reading unknown lock modes.""" + + pid: int + hostname: str + mode: OwnerMode + token: str | None = None + lease_duration: float | None = None + start: int | None = None + + +def encode_marker(record: OwnerRecord) -> bytes: + """Render an owner record as the bytes a protocol 2 marker holds.""" + lines = [_PROTOCOL, f"pid={record.pid}", f"host={record.hostname}", f"mode={record.mode}"] + if record.token is not None: + lines.append(f"token={record.token}") + if record.lease_duration is not None: + lines.append(f"duration={record.lease_duration!r}") + if record.start is not None: + lines.append(f"start={record.start}") + return "".join(f"{line}\n" for line in lines).encode() + + +def parse_marker(content: str | None) -> OwnerRecord | None: + """Return the owner a protocol 2 marker names, or ``None`` when the record is malformed or protocol 1.""" + # A torn write may end in a prefix like "mode=exclu", an unknown contract nobody reclaims; malformed, it ages out. + if not content or content[-1] != "\n" or not (lines := content.strip().splitlines()) or lines[0] != _PROTOCOL: + return None + fields: dict[str, str] = {} + for line in lines[1:]: + key, separator, value = line.partition("=") + if not separator: + return None + fields[key] = value + return _build_record(fields) + + +def _build_record(fields: dict[str, str]) -> OwnerRecord | None: + # An unknown key is a field a newer filelock published, so ignore it rather than read the record as malformed. An + # unrecognized mode is the same story one level up: a contract this version does not implement. Reading it as + # malformed would age the marker out of a live owner's hands, so keep it and let the caller refuse to reclaim it. + # A record naming no mode at all states no contract and stays malformed. + if (published := fields.get("mode")) is None: + return None + mode: Final[OwnerMode] = published if published in {"lease", "exclusive"} else "unknown" + hostname = fields.get("host") + if not hostname or "pid" not in fields or ("duration" in fields and not _DURATION.fullmatch(fields["duration"])): + return None + try: + pid = parse_decimal(fields["pid"]) + duration = float(fields["duration"]) if "duration" in fields else None + start = parse_decimal(fields["start"]) if "start" in fields else None + except ValueError: + return None + if not 1 <= pid <= _MAX_PID: + return None + token = fields.get("token") + # "1e400" passes the grammar but overflows to inf, which mismatches every configured duration and so wedges reclaim, + # where a malformed marker ages out. + if mode == "lease" and (not token or duration is None or not (math.isfinite(duration) and duration > 0)): + return None + return OwnerRecord(pid=pid, hostname=hostname, mode=mode, token=token, lease_duration=duration, start=start) + + +__all__ = [ + "MarkerSoftFileLock", + "OwnerMode", + "OwnerRecord", + "encode_marker", + "parse_marker", +] diff --git a/hug/lib/python3.12/site-packages/filelock/_read_write.py b/hug/lib/python3.12/site-packages/filelock/_read_write.py new file mode 100644 index 0000000000000000000000000000000000000000..bc377d558d59fdd9115aff6ef1cee8ee239b997f --- /dev/null +++ b/hug/lib/python3.12/site-packages/filelock/_read_write.py @@ -0,0 +1,1504 @@ +from __future__ import annotations + +__lazy_modules__ = [ + "collections", + "filelock._error", + "filelock._identity", + "hashlib", + "itertools", + "pathlib", + "secrets", + "tempfile", +] + +import hashlib +import logging +import os +import pathlib +import secrets +import sqlite3 +import stat +import sys +import tempfile +import threading +import time +from collections import deque +from contextlib import contextmanager, suppress +from dataclasses import dataclass, field +from itertools import count +from typing import TYPE_CHECKING, ClassVar, Final, Literal, NamedTuple, TypeAlias, cast +from weakref import WeakValueDictionary + +from filelock._api import ( + AcquireReturnProxy, + _acquire_thread_lock, + _canonical, + _ensure_current_process, + _fork_transition, + _raise_chained_errors, + _register_fork_class, + _register_fork_object, + _register_owned_descriptor, + _resolve_lazy_imports_before_shutdown, + _resolve_read_write_timeout, + _seconds, + _timeout_behavior, + _unregister_owned_descriptor, +) +from filelock._error import Timeout +from filelock._identity import host_name, owner_is_stale, process_start_token + +if sys.platform == "win32": # pragma: win32 cover + from filelock._windows import ( + _directory_is_private, + _identity_at, + _identity_of_fd, + _make_private_directory, + _open_non_reparse_fd, + _pinned, + ) + +if TYPE_CHECKING: + from collections.abc import Callable, Generator + from typing import Self + + from _typeshed import Unused + +_LOGGER: Final[logging.Logger] = logging.getLogger("filelock") +_GETPID: Final[Callable[[], int]] = os.getpid +_IS_PYPY: Final[bool] = sys.implementation.name == "pypy" +_NEEDS_CONNECTION_ESCROW: Final[bool] = ( + hasattr(os, "register_at_fork") and sys.implementation.name == "cpython" and sys.version_info < (3, 12) +) +_ConnectionParameter: TypeAlias = ( + str | bytes | os.PathLike[str] | os.PathLike[bytes] | float | int | type[sqlite3.Connection] | None +) +_DatabaseIdentity: TypeAlias = tuple[int, int] + +# sqlite3_busy_timeout() accepts a C int, max 2_147_483_647 on 32-bit. Use a lower value to be safe (~23 days). +_MAX_SQLITE_TIMEOUT_MS: Final[int] = 2_000_000_000 - 1 +# How long a wait that its caller may abandon runs before it checks whether it should stop. +_CANCEL_POLL_INTERVAL: Final[float] = 0.1 +_UNSAFE_FORK_EXIT_STATUS: Final[int] = 70 +#: The longest a contended acquisition waits for the database inside one fork transition, in seconds. +_MAX_TRANSITION_WAIT: Final[float] = 0.05 +# O_NONBLOCK keeps an open from blocking on a FIFO planted at the path; the regular-file check then rejects it. +_DB_OPEN_FLAGS: Final[int] = os.O_RDWR | os.O_CREAT | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0) +_DIRECTORY_FLAGS: Final[int] = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0) +# Header bytes 18 and 19 hold the file format versions, 2 in WAL mode: https://www.sqlite.org/fileformat.html +_SQLITE_MAGIC: Final[bytes] = b"SQLite format 3\x00" +_FORMAT_VERSIONS: Final[slice] = slice(18, 20) +_HEADER_SIZE: Final[int] = 20 +_WAL_FORMAT_VERSION: Final[int] = 2 + + +class _SQLiteTransitionContext(threading.local): + depth: int = 0 + + +_SQLITE_TRANSITION_CONTEXT: Final = _SQLiteTransitionContext() + + +class _ConnectionEscrow: + def __init__(self) -> None: + self._lock = threading.RLock() + self._functions: tuple[Callable[[sqlite3.Connection], None], Callable[[sqlite3.Connection], None]] | None = None + + def functions( + self, + ) -> tuple[Callable[[sqlite3.Connection], None], Callable[[sqlite3.Connection], None]] | None: + if not _NEEDS_CONNECTION_ESCROW: + return None # pragma: >=3.12 cover + with self._lock: # pragma: <3.12 cover # pragma: needs fork + if self._functions is None: + import ctypes # ruff:ignore[import-outside-top-level] # keep optional ctypes and its audited dlsym out of ordinary imports + + function_type = ctypes.PYFUNCTYPE(None, ctypes.py_object) + increment_address = ctypes.cast(ctypes.pythonapi.Py_IncRef, ctypes.c_void_p).value + decrement_address = ctypes.cast(ctypes.pythonapi.Py_DecRef, ctypes.c_void_p).value + if increment_address is None or decrement_address is None: # pragma: no cover - resolved CPython API + msg = "CPython reference functions have no address" + raise RuntimeError(msg) + self._functions = ( + cast("Callable[[sqlite3.Connection], None]", function_type(increment_address)), + cast("Callable[[sqlite3.Connection], None]", function_type(decrement_address)), + ) + return self._functions + + def _reset_after_fork_in_child(self) -> None: # pragma: forked child + self._lock = threading.RLock() + + +_CONNECTION_ESCROW: Final = _ConnectionEscrow() + + +class _ForkedDatabaseRegistry: + def __init__(self) -> None: + self._lock = threading.RLock() + self._paths: set[pathlib.Path] = set() + self._identities: set[_DatabaseIdentity] = set() + self._sqlite_used = False + self._all_paths_poisoned = False + + def raise_if_poisoned(self, path: pathlib.Path) -> None: + identity = self.identity(path) + with self._lock: + all_paths_poisoned = self._all_paths_poisoned + poisoned = ( + all_paths_poisoned or path in self._paths or (identity is not None and identity in self._identities) + ) + if poisoned: # pragma: needs fork + msg = ( + "ReadWriteLock is unavailable in a PyPy fork child; exec or exit before using it" + if all_paths_poisoned + else f"SQLite database {path!s} was active across fork(); exec or exit before using it in the child" + ) + raise RuntimeError(msg) + + def poison_after_fork(self, path: pathlib.Path, identity: _DatabaseIdentity | None) -> None: + self._paths.add(path) + if identity is not None: + self._identities.add(identity) + + def note_sqlite_use(self) -> None: + if _IS_PYPY: + with self._lock: + self._sqlite_used = True + + def _reset_after_fork_in_child(self) -> None: # pragma: forked child + self._lock = threading.RLock() + self._all_paths_poisoned = self._all_paths_poisoned or (_IS_PYPY and self._sqlite_used) + self._sqlite_used = False + + @staticmethod + def identity(path: pathlib.Path) -> _DatabaseIdentity | None: + try: + stat_result = path.stat() + except OSError: + return None + return stat_result.st_dev, stat_result.st_ino + + +_FORKED_DATABASES: Final = _ForkedDatabaseRegistry() + +_LINK_PREFIX: Final[str] = ".filelock-" +_LINK_NAME: Final[str] = "lock.db" +_SWEPT_DATABASES: Final[set[_DatabaseIdentity]] = set() + + +# POSIX locks belong to the process and the inode, so closing any descriptor on the inode drops the locks every other +# connection of this process holds there. SQLite defers its own closes for this reason (section 2.2 of +# https://www.sqlite.org/howtocorrupt.html); the probes stay open until the last connection on their inode closes. +class _ProbeDescriptors: + def __init__(self) -> None: + self._lock = threading.Lock() + self._inodes: dict[_DatabaseIdentity, _InodeProbes] = {} + self._releases: deque[_Probe] = deque() + self._leases = count() + + def adopt(self, database: str, claimed: list[_Probe]) -> _Probe | None: # pragma: win32 no cover + # Reuse a probe no live connection holds: each connect would otherwise keep another descriptor open until the + # inode's last close. + try: + stat_result: Final = os.lstat(database) + except OSError: + return None + identity: Final = stat_result.st_dev, stat_result.st_ino + with self._lock: + if not stat.S_ISREG(stat_result.st_mode) or not (probes := self._inodes.get(identity)) or not probes.idle: + return None + claimed.append(adopted := _Probe(identity, probes.idle.pop(), next(self._leases))) + probes.leases.add(adopted.lease) + return adopted + + def retain(self, fd: int, claimed: list[_Probe]) -> _Probe: # pragma: win32 no cover + stat_result: Final = os.fstat(fd) + identity: Final = stat_result.st_dev, stat_result.st_ino + token: Final = _register_owned_descriptor(fd, identity) + with self._lock: + (probes := self._inodes.setdefault(identity, _InodeProbes())).descriptors.append((fd, token)) + claimed.append(retained := _Probe(identity, fd, next(self._leases))) + probes.leases.add(retained.lease) + return retained + + def release(self, probe: _Probe) -> None: # pragma: win32 no cover + # A finalizer may run this on a thread already inside the lock, so it never waits; the lock holder drains. + self._releases.append(probe) + self.drain() + + def drain(self) -> None: # pragma: win32 no cover + while self._releases and self._lock.acquire(blocking=False): + try: + # Dequeued only once applied, so an exception leaves the rest of the release for the next drain. + while self._releases: + self._release_locked(self._releases[0]) + self._releases.popleft() + finally: + self._lock.release() + + def _release_locked(self, probe: _Probe) -> None: # pragma: win32 no cover + if (probes := self._inodes.get(probe.identity)) is None: + return + if probe.lease in probes.leases: + probes.leases.remove(probe.lease) + if probes.leases: + probes.idle.append(probe.fd) + if probes.leases: + return + # No connect may adopt a probe about to close, even one an interrupt leaves listed. + probes.idle.clear() + # Closing under the lock keeps a concurrent connect from taking a lock on this inode before the close. + while probes.descriptors: + fd, token = probes.descriptors[-1] + if token is not None: # pragma: needs fork + _unregister_owned_descriptor(token) + # Forgotten before the close: after an interrupt, a leaked descriptor beats closing a reused number. + probes.descriptors.pop() + # The descriptor is gone even when close reports an error, and no lock of this process depends on it. + with suppress(OSError): + os.close(fd) + del self._inodes[probe.identity] + + def _reset_after_fork_in_child(self) -> None: # pragma: forked child + # The child holds no POSIX locks, and the owned-descriptor registry closes the inherited probes. + self._lock = threading.Lock() + self._inodes = {} + self._releases = deque() + + +class _Operation(NamedTuple): + pid: int + start_time: float + #: Asked between bounded waits, so an abandoned acquire stops waiting; ``None`` waits in one call. + cancel_check: Callable[[], bool] | None + + +class _Probe(NamedTuple): + identity: _DatabaseIdentity + fd: int + # One per connection, so a release applied again after an interrupt kept it queued counts once. + lease: int + + +@dataclass(slots=True) +class _InodeProbes: + leases: set[int] = field(default_factory=set) + descriptors: list[tuple[int, int | None]] = field(default_factory=list) + idle: list[int] = field(default_factory=list) + + +_PROBE_DESCRIPTORS: Final = _ProbeDescriptors() + + +class _ForkSafeConnection(sqlite3.Connection): + _creator_pid: int + _decrement_escrow: Callable[[sqlite3.Connection], None] | None + _database_directory: _LinkDirectory | None + database_identity: _DatabaseIdentity | None = None + _probe: _Probe | None = None + + def __new__( + cls, + *_args: _ConnectionParameter, + **_kwargs: _ConnectionParameter, + ) -> Self: + connection = super().__new__(cls) + connection._creator_pid = _GETPID() + connection._decrement_escrow = None + connection._database_directory = None + return connection + + def close(self) -> None: + with _sqlite_transition(): + if _GETPID() != self._creator_pid: # pragma: needs fork + return + with _fork_transition(): + sqlite3.Connection.close(self) + try: + if (directory := self._database_directory) is not None: # pragma: needs posix-hard-link + self._database_directory = None + _remove_link_directory(directory) + finally: + if (probe := self._probe) is not None: # pragma: win32 no cover + self._probe = self.database_identity = None + _PROBE_DESCRIPTORS.release(probe) + if (decrement := self._decrement_escrow) is not None: # pragma: <3.12 cover # pragma: needs fork + self._decrement_escrow = None + decrement(self) + + def retain_link(self, directory: _LinkDirectory | None) -> None: # pragma: win32 cover + """Keep the private link directory, if there is one, until SQLite no longer uses it.""" + self._database_directory = directory + + def retain_until_close(self, directory: _LinkDirectory | None, probe: _Probe) -> None: # pragma: win32 no cover + """Keep the private link and this connection's probe descriptor until SQLite no longer uses them.""" + self._database_directory = directory + self._probe = probe + self.database_identity = probe.identity + + def acquire_escrow( # pragma: <3.12 cover # pragma: needs fork + self, + functions: tuple[Callable[[sqlite3.Connection], None], Callable[[sqlite3.Connection], None]] | None, + ) -> None: + # The caller only reaches here holding the escrow functions; it skips the call entirely without them. + if functions is not None: # pragma: no branch + increment, decrement = functions + increment(self) + self._decrement_escrow = decrement + + def __del__(self) -> None: + with suppress(sqlite3.Error, RuntimeError, OSError): + self.close() + + +@dataclass(slots=True) +class _OwnedConnection: + """ + The connection of one lock, closed when the lock is collected. + + CPython runs ``__del__`` before it clears weak references. A finalizer on the lock itself would close the + connection while the singleton cache still hands the lock out, and a thread given the lock would nest into a hold + with no database lock behind it. The lock alone refers to this holder, so the holder is collected once the cache no + longer finds the lock. + """ + + connection: _ForkSafeConnection | None = None + + def __del__(self) -> None: + if (connection := self.connection) is not None: + with suppress(sqlite3.Error, RuntimeError): + connection.close() + + +class _ReadWriteLockMeta(type): + """ + Resolve singleton instances for ``is_singleton=True`` construction. + + This logic lives here rather than in ReadWriteLock.get_lock so ``ReadWriteLock(path)`` returns cached instances + without a 2-arg ``super()`` call that type checkers cannot verify. + + """ + + _instances: WeakValueDictionary[pathlib.Path, ReadWriteLock] + _instances_lock: threading.RLock + _instances_pid: int + _instances_under_construction: set[pathlib.Path] + + def __call__( + cls, + lock_file: str | os.PathLike[str], + timeout: float = -1, + *, + blocking: bool = True, + is_singleton: bool = True, + ) -> ReadWriteLock: + _ensure_current_process() + _resolve_lazy_imports_before_shutdown() + timeout = _seconds("timeout", timeout) + if cls._instances_pid != _GETPID(): + cls._reset_class_after_fork() + construction_pid = _GETPID() + if not is_singleton: + instance = super().__call__(lock_file, timeout, blocking=blocking, is_singleton=is_singleton) + if _GETPID() != construction_pid: # pragma: forked child + msg = "ReadWriteLock construction cannot continue after fork" + raise RuntimeError(msg) + return instance + + # Resolve only the parent, so a symlink at the lock path stays its own key instead of aliasing its target. + normalized = pathlib.Path(_canonical(lock_file)) + with cls._instances_lock: + # One lookup, since another thread can drop the last reference between a membership check and a read. + if (instance := cls._instances.get(normalized)) is None: + if normalized in cls._instances_under_construction: # pragma: no cover - exercised in an audit callback + msg = f"Singleton lock construction is already active for {lock_file!s}" + raise RuntimeError(msg) + construction_registry = cls._instances_under_construction + construction_registry.add(normalized) + try: + instance = super().__call__(lock_file, timeout, blocking=blocking, is_singleton=is_singleton) + finally: + if _GETPID() == construction_pid: + construction_registry.discard(normalized) + if _GETPID() != construction_pid: + msg = "ReadWriteLock construction cannot continue after fork" + raise RuntimeError(msg) + cls._instances[normalized] = instance + + if ( + _timeout_behavior(instance.timeout, any_negative=False) + != _timeout_behavior(timeout, any_negative=False) + or instance.blocking != blocking + ): + msg = ( + f"Singleton lock created with timeout={instance.timeout}, blocking={instance.blocking}," + f" cannot be changed to timeout={timeout}, blocking={blocking}" + ) + raise ValueError(msg) + return instance + + def _reset_class_after_fork(cls) -> None: # pragma: forked child + cls._instances = WeakValueDictionary() + cls._instances_lock = threading.RLock() + cls._instances_pid = _GETPID() + cls._instances_under_construction = set() + + +class ReadWriteLock(metaclass=_ReadWriteLockMeta): + """ + Cross-process read-write lock backed by SQLite. + + Allows concurrent shared readers or a single exclusive writer. The lock is reentrant within the same mode (multiple + ``acquire_read`` calls nest, as do multiple ``acquire_write`` calls from the same thread), but upgrading from read + to write or downgrading from write to read raises :class:`RuntimeError`. Write locks are pinned to the thread that + acquired them, and each thread sharing a read lock can release only the levels it acquired. + + An instance coordinates processes, not threads: a thread asking for the opposite mode of another thread's hold + raises :class:`RuntimeError` instead of waiting. Threads that must wait for each other each need their own + instance (``is_singleton=False``). + + By default, ``is_singleton=True``: calling ``ReadWriteLock(path)`` with the same resolved path returns the same + instance. A ``.db`` extension is a convention rather than a requirement; the filesystem must be one the active + SQLite VFS supports. Give the lock a file of its own: a WAL-mode database, or one with a schema larger than a lock + needs, raises :class:`ValueError`. A database directory that other accounts can write must also be writable by + this one and support hard links; the protection against other accounts ends once one of them can replace the + database directory or a directory above it. + + :param lock_file: path to the SQLite database file used as the lock + :param timeout: maximum wait time in seconds; ``-1`` waits up to SQLite's busy-timeout cap of about 23 days + :param blocking: if ``False``, raise :class:`~filelock.Timeout` immediately when the lock is unavailable + :param is_singleton: if ``True``, reuse existing instances for the same resolved path + + .. versionadded:: 3.21.0 + + """ + + _instances: WeakValueDictionary[pathlib.Path, ReadWriteLock] = WeakValueDictionary() + _instances_lock = threading.RLock() + _instances_pid = _GETPID() + _instances_under_construction: ClassVar[set[pathlib.Path]] = set() + + def __init_subclass__(cls) -> None: + super().__init_subclass__() + cls._instances = WeakValueDictionary() + cls._instances_lock = threading.RLock() + cls._instances_pid = _GETPID() + cls._instances_under_construction = set() + _register_fork_class(cls) + + @classmethod + def get_lock( + cls, lock_file: str | os.PathLike[str], timeout: float = -1, *, blocking: bool = True + ) -> ReadWriteLock: + """ + Return the singleton :class:`ReadWriteLock` for *lock_file*. + + :param lock_file: path to the SQLite database file used as the lock + :param timeout: maximum wait time in seconds; ``-1`` waits up to SQLite's busy-timeout cap of about 23 days + :param blocking: if ``False``, raise :class:`~filelock.Timeout` immediately when the lock is unavailable + + :returns: the singleton lock instance + + :raises ValueError: if an instance already exists for this path with different *timeout* or *blocking* values + + """ + return cls(lock_file, timeout, blocking=blocking) + + def __init__( + self, + lock_file: str | os.PathLike[str], + timeout: float = -1, + *, + blocking: bool = True, + is_singleton: bool = True, # ruff:ignore[unused-method-argument] # consumed by _ReadWriteLockMeta.__call__ + ) -> None: + self.lock_file = os.fspath(lock_file) + self._canonical_path = pathlib.Path(_canonical(lock_file)) + _FORKED_DATABASES.raise_if_poisoned(self._canonical_path) + self.timeout = timeout + self.blocking = blocking + self._transaction_lock = threading.Lock() # serializes the (possibly blocking) SQLite transaction work + self._internal_lock = threading.Lock() # protects _lock_level / _current_mode updates and rollback + self._lock_level = 0 + self._current_mode: Literal["read", "write"] | None = None + self._write_thread_id: int | None = None + self._read_depths: dict[int, int] = {} + self._acquisition_thread_ids: set[int] = set() + self._owned_connection = _OwnedConnection() + self._connection_transaction_released = True + self._connection_identity: _DatabaseIdentity | None = None + self._closed = False + self._creator_pid = _GETPID() + self._fork_invalidated = False + _register_fork_object(self) + with _fork_transition(), _sqlite_transition(): + validation_connection = self._open_connection(sqlite_timeout=5.0) + validation_connection.close() + + def acquire_read(self, timeout: float | None = None, *, blocking: bool | None = None) -> AcquireReturnProxy: + """ + Acquire a shared read lock. + + If this instance already holds a read lock, the lock level is incremented (reentrant). Attempting to acquire a + read lock while holding a write lock raises :class:`RuntimeError` (downgrade not allowed). + + :param timeout: seconds to wait; ``None`` uses the instance setting; ``-1`` waits up to SQLite's + busy-timeout cap of about 23 days + :param blocking: if ``False``, raise :class:`~filelock.Timeout` on contention; + ``None`` uses the instance setting + + :returns: a proxy that can be used as a context manager to release the lock + + :raises RuntimeError: if a write lock is already held on this instance + :raises Timeout: if the lock cannot be acquired within *timeout* seconds + :raises TypeError: if *timeout* is a :class:`bool` or not a real number + :raises ValueError: if a blocking call gets a ``nan`` *timeout* or a negative one other than ``-1`` + + """ + return self._acquire( + "read", + self.timeout if timeout is None else timeout, + blocking=self.blocking if blocking is None else blocking, + ) + + def acquire_write(self, timeout: float | None = None, *, blocking: bool | None = None) -> AcquireReturnProxy: + """ + Acquire an exclusive write lock. + + If this instance already holds a write lock from the same thread, the lock level is incremented (reentrant). + Attempting to acquire a write lock while holding a read lock raises :class:`RuntimeError` (upgrade not allowed). + Write locks are pinned to the acquiring thread: a different thread trying to re-enter also raises + :class:`RuntimeError`. + + :param timeout: seconds to wait; ``None`` uses the instance setting; ``-1`` waits up to SQLite's + busy-timeout cap of about 23 days + :param blocking: if ``False``, raise :class:`~filelock.Timeout` on contention; + ``None`` uses the instance setting + + :returns: a proxy that can be used as a context manager to release the lock + + :raises RuntimeError: if a read lock is already held, or a write lock is held by a different thread + :raises Timeout: if the lock cannot be acquired within *timeout* seconds + :raises TypeError: if *timeout* is a :class:`bool` or not a real number + :raises ValueError: if a blocking call gets a ``nan`` *timeout* or a negative one other than ``-1`` + + """ + return self._acquire( + "write", + self.timeout if timeout is None else timeout, + blocking=self.blocking if blocking is None else blocking, + ) + + @contextmanager + def read_lock(self, timeout: float | None = None, *, blocking: bool | None = None) -> Generator[None]: + """ + Context manager that acquires and releases a shared read lock. + + Falls back to instance defaults for *timeout* and *blocking* when ``None``. + + :param timeout: maximum wait time in seconds, or ``None`` to use the instance default + :param blocking: if ``False``, raise :class:`~filelock.Timeout` immediately; ``None`` uses the instance default + + """ + self.acquire_read(timeout, blocking=blocking) + try: + yield + finally: + self.release() + + @contextmanager + def write_lock(self, timeout: float | None = None, *, blocking: bool | None = None) -> Generator[None]: + """ + Context manager that acquires and releases an exclusive write lock. + + Falls back to instance defaults for *timeout* and *blocking* when ``None``. + + :param timeout: maximum wait time in seconds, or ``None`` to use the instance default + :param blocking: if ``False``, raise :class:`~filelock.Timeout` immediately; ``None`` uses the instance default + + """ + self.acquire_write(timeout, blocking=blocking) + try: + yield + finally: + self.release() + + def release(self, *, force: bool = False) -> None: + """ + Release one level of the current lock. + + When the lock level reaches zero the underlying SQLite transaction is rolled back, releasing the database lock. + + :param force: if ``True``, release the lock completely regardless of the current lock level + + :raises RuntimeError: if no lock is currently held and *force* is ``False``, or if the calling thread holds + no level of the current lock and *force* is ``False`` + + """ + with _fork_transition(): + _ensure_current_process() + if self._inherited: # pragma: needs fork + return + self._raise_if_acquiring("release") + self._release(force=force, close=False) + + def close(self) -> None: + """ + Release the lock (if held) and close the underlying SQLite connection. + + After calling this method, the lock instance is no longer usable. Unlike :meth:`release`, it ends every + thread's hold on this instance, whichever thread calls it. + + """ + with _fork_transition(): + _ensure_current_process() + if self._inherited: # pragma: needs fork + return + self._raise_if_acquiring("close") + self._release(force=True, close=True) + + def _acquire_level_of( + self, + holder: int, + mode: Literal["read", "write"], + timeout: float, + *, + blocking: bool, + cancel_check: Callable[[], bool], + ) -> AcquireReturnProxy: + # Records the level under *holder* instead of the calling thread: an async wrapper's executor thread does not + # identify the tasks it holds for. Gives up with Timeout once *cancel_check* is true. + return self._acquire(mode, timeout, blocking=blocking, holder=holder, cancel_check=cancel_check) + + def _release_level_of(self, thread_id: int) -> None: + with _fork_transition(): + _ensure_current_process() + self._raise_if_acquiring("release") + self._release(force=False, close=False, thread_id=thread_id) + + def _holds_level_of(self, thread_id: int) -> bool: + # Read without the internal lock. The async wrapper asks from its event loop, which must not wait out a release + # on another thread. A stale answer is what a caller asking a moment earlier would have had. A level whose + # transaction ended is one an interrupted release left behind; it counts as gone, as _holds_level() treats it. + if thread_id != self._write_thread_id and thread_id not in self._read_depths: + return False + try: + return not self._transaction_ended() + except sqlite3.ProgrammingError: + # The connection closed between the read of _con and the in_transaction call, so the transaction is over. + return False + + def _release(self, *, force: bool, close: bool, thread_id: int | None = None) -> None: + thread_id = threading.get_ident() if thread_id is None else thread_id + with self._transaction_lock, self._internal_lock: + if not force: + self._raise_if_not_holder(thread_id) + if self._lock_level == 0: + if force and self._con is None: + if close: + self._closed = True + return + if not force: + msg = f"Cannot release a lock on {self.lock_file} (lock id: {id(self)}) that is not held" + raise RuntimeError(msg) + if not force and self._lock_level > 1: + self._leave_one_level(thread_id) + return + try: + self._finish_connection() + except BaseException: + # Once the rollback ran, the level is gone whatever stopped the cleanup, a signal handler's exception + # included. Kept, it would let the next acquire nest into a transaction that no longer exists. + if self._transaction_ended(): + self._clear_lock_state() + raise + self._clear_lock_state() + if close: + self._closed = True + + def _transaction_ended(self) -> bool: + if self._connection_transaction_released or (connection := self._con) is None: + return True + # An interrupt can land between the rollback and the flag that records it. + return not connection.in_transaction + + def _raise_if_not_holder(self, thread_id: int) -> None: + # The connection runs with check_same_thread=False, so a release from another thread would end the owner's + # transaction under it and let a second writer in. + if self._current_mode == "write" and thread_id != self._write_thread_id: + msg = ( + f"Cannot release write lock on {self.lock_file} (lock id: {id(self)}) " + f"from thread {thread_id} while it is held by thread {self._write_thread_id}" + ) + raise RuntimeError(msg) + # Readers share that one transaction too, so a thread that holds no read level would end another's hold. + if self._current_mode == "read" and thread_id not in self._read_depths: + msg = ( + f"Cannot release read lock on {self.lock_file} (lock id: {id(self)}) " + f"from thread {thread_id}, which does not hold it" + ) + raise RuntimeError(msg) + + def _leave_one_level(self, thread_id: int) -> None: + self._lock_level -= 1 + if self._current_mode == "read" and (depth := self._read_depths.pop(thread_id) - 1): + self._read_depths[thread_id] = depth + + def _clear_lock_state(self) -> None: + self._lock_level = 0 + self._current_mode = None + self._write_thread_id = None + self._read_depths = {} + + @property + def _con(self) -> _ForkSafeConnection | None: + return self._owned_connection.connection + + @_con.setter + def _con(self, connection: _ForkSafeConnection | None) -> None: + self._owned_connection.connection = connection + + def _reset_after_fork_in_child(self) -> None: # pragma: forked child + if self._con is not None: + _FORKED_DATABASES.poison_after_fork(self._canonical_path, self._connection_identity) + self._con = None + self._connection_transaction_released = True + self._connection_identity = None + self._transaction_lock = threading.Lock() + self._internal_lock = threading.Lock() + self._clear_lock_state() + self._acquisition_thread_ids = set() + self._fork_invalidated = True + + @property + def _inherited(self) -> bool: + return self._fork_invalidated or _GETPID() != self._creator_pid + + def _raise_if_unusable(self) -> None: + _ensure_current_process() + if self._inherited: # pragma: needs fork + msg = f"ReadWriteLock on {self.lock_file} was invalidated by fork(); construct a new instance" + raise RuntimeError(msg) + if self._closed: + msg = "Cannot operate on a closed database." + raise sqlite3.ProgrammingError(msg) + + def _acquire( + self, + mode: Literal["read", "write"], + timeout: float, + *, + blocking: bool, + holder: int | None = None, + cancel_check: Callable[[], bool] | None = None, + ) -> AcquireReturnProxy: + timeout = _resolve_read_write_timeout(timeout, blocking=blocking) + start_time: Final = time.monotonic() + remaining = timeout # -1 waits up to SQLite's busy-timeout cap + while True: + # A fork waits for the open transitions and admits no new one meanwhile. A wait for the database inside one + # stalls the forking thread and, behind it, each lock call in the process. With no other thread to fork, + # the wait runs in one piece, which keeps SQLite's writer preference. threading does not count a + # thread started through _thread, so a fork from one of those still waits. + final = ( + not blocking + or 0 <= remaining <= _MAX_TRANSITION_WAIT + or not hasattr(os, "register_at_fork") + or threading.active_count() == 1 + ) + try: + return self._attempt_acquire( + mode, + remaining if final else _MAX_TRANSITION_WAIT, + blocking=blocking, + holder=holder, + cancel_check=cancel_check, + ) + except Timeout: + if final or (cancel_check is not None and cancel_check()): + raise + # Only a runtime with fork handlers waits in more than one attempt. + if timeout != -1: # pragma: needs register-at-fork + remaining = max(timeout - (time.monotonic() - start_time), 0) + + def _attempt_acquire( + self, + mode: Literal["read", "write"], + timeout: float, + *, + blocking: bool, + holder: int | None = None, + cancel_check: Callable[[], bool] | None = None, + ) -> AcquireReturnProxy: + with _fork_transition(): + self._raise_if_unusable() + operation_pid = _GETPID() + thread_id = threading.get_ident() + holder = thread_id if holder is None else holder + with self._internal_lock: + if self._holds_level(): + return self._validate_reentrant(mode, holder) + if thread_id in self._acquisition_thread_ids: # pragma: no cover - exercised in an audit callback + msg = f"Cannot acquire ReadWriteLock on {self.lock_file} while acquisition is active in this thread" + raise RuntimeError(msg) + self._acquisition_thread_ids.add(thread_id) + try: + start_time = time.perf_counter() + self._acquire_transaction_lock(blocking=blocking, timeout=timeout, cancel_check=cancel_check) + try: + self._raise_if_unusable() + return self._do_acquire_inner( + mode, + timeout, + holder, + blocking=blocking, + operation=_Operation(operation_pid, start_time, cancel_check), + ) + finally: + self._transaction_lock.release() + finally: + with self._internal_lock: + self._acquisition_thread_ids.discard(thread_id) + + def _do_acquire_inner( + self, + mode: Literal["read", "write"], + timeout: float, + holder: int, + *, + blocking: bool, + operation: _Operation, + ) -> AcquireReturnProxy: + operation_pid, start_time, _ = operation + # Double-check: another thread may have acquired the lock while we waited on _transaction_lock. + with self._internal_lock: + if self._holds_level(): + return self._validate_reentrant(mode, holder) + if self._con is not None: + self._finish_connection() + try: + self._open_for_acquisition( + timeout, + blocking=blocking, + operation_pid=operation_pid, + start_time=start_time, + ) + self._configure_and_begin(mode, timeout, blocking=blocking, operation=operation) + self._raise_if_process_changed(operation_pid) + except BaseException as error: + acquisition_error: BaseException + if isinstance(error, sqlite3.OperationalError) and "database is locked" in str(error): + acquisition_error = Timeout(self.lock_file) + else: + acquisition_error = error + try: + self._finish_connection() + except sqlite3.Error as cleanup_error: + _raise_chained_errors(acquisition_error, cleanup_error) + if acquisition_error is not error: + raise acquisition_error from None + raise + with self._internal_lock: + self._raise_if_process_changed(operation_pid) + self._current_mode = mode + self._lock_level = 1 + if mode == "write": + self._write_thread_id = holder + else: + self._read_depths = {holder: 1} + return AcquireReturnProxy(lock=self) + + def _open_for_acquisition(self, timeout: float, *, blocking: bool, operation_pid: int, start_time: float) -> None: + with _sqlite_transition(): + sqlite_timeout = ( + timeout_for_sqlite( + timeout, + blocking=blocking, + already_waited=time.perf_counter() - start_time, + ) + / 1000 + ) + connection = self._open_connection(sqlite_timeout=sqlite_timeout) + self._con, self._connection_transaction_released, self._connection_identity = ( + connection, + False, + _FORKED_DATABASES.identity(self._canonical_path), + ) + self._raise_if_process_changed(operation_pid) + + def _configure_and_begin( + self, + mode: Literal["read", "write"], + timeout: float, + *, + blocking: bool, + operation: _Operation, + ) -> None: + with _sqlite_transition(): + connection = cast("_ForkSafeConnection", self._con) + # SQLite opened the file the name held at connect time, and its first statement would replay a journal + # beside that file, so the name is checked before any statement runs. + self._raise_if_database_replaced(connection) + with _bounded_schema(connection, self.lock_file): + self._raise_if_process_changed(operation.pid) + # Use legacy journal mode (not WAL) because WAL does not block readers while a concurrent EXCLUSIVE + # write transaction is active, which makes read-write locking impossible without modifying table data. + # MEMORY is safe here since no writes happen, so a crash cannot corrupt the DB. + # See https://sqlite.org/lang_transaction.html#deferred_immediate_and_exclusive_transactions + self._execute_waiting( + connection, "PRAGMA journal_mode=MEMORY;", timeout, blocking=blocking, operation=operation + ) + self._raise_if_process_changed(operation.pid) + statements = "BEGIN EXCLUSIVE TRANSACTION;" if mode == "write" else "BEGIN TRANSACTION;" + if mode == "read": + # SQLite takes the SHARED lock only when a statement reads; BEGIN alone stays deferred. + # https://www.sqlite.org/lockingv3.html#transaction_control + # sqlite_schema is an alias added in SQLite 3.33.0; sqlite_master works on every version. + statements += " SELECT name FROM sqlite_master LIMIT 1;" + self._execute_waiting(connection, statements, timeout, blocking=blocking, operation=operation) + self._raise_if_database_replaced(connection) + + @staticmethod + def _execute_waiting( + connection: _ForkSafeConnection, statements: str, timeout: float, *, blocking: bool, operation: _Operation + ) -> None: + # SQLite's busy wait cannot be interrupted. An acquire its caller may abandon waits in bounded slices and asks + # between them whether to stop. + slice_ms = int(_CANCEL_POLL_INTERVAL * 1000) + while True: + busy_ms = timeout_for_sqlite( + timeout, blocking=blocking, already_waited=time.perf_counter() - operation.start_time + ) + cancel_check = operation.cancel_check if busy_ms > slice_ms else None + try: + connection.executescript( + f"PRAGMA busy_timeout={busy_ms if cancel_check is None else slice_ms}; {statements}" + ).close() + except sqlite3.OperationalError as error: + if cancel_check is None or "database is locked" not in str(error) or cancel_check(): + raise + # A read's BEGIN succeeds before its SELECT waits, so the retry has to start a fresh transaction. + if connection.in_transaction: + connection.rollback() + else: + return + + def _raise_if_database_replaced(self, connection: _ForkSafeConnection) -> None: + # Windows pins the name: SQLite opens the database without delete sharing, so it cannot be unlinked or renamed. + if (identity := connection.database_identity) is None: # pragma: win32 cover + return + try: # pragma: win32 no cover + current = self._canonical_path.stat(follow_symlinks=False) + except FileNotFoundError: # pragma: win32 no cover + current = None + # The lock lives on the inode, so one taken after the path moved on excludes no later opener. + if current is None or (current.st_dev, current.st_ino) != identity: # pragma: win32 no cover + msg = f"lock database {self.lock_file!r} was unlinked or replaced while acquiring; leave it in place" + raise OSError(msg) + + def _open_connection(self, *, sqlite_timeout: float) -> _ForkSafeConnection: + with _sqlite_transition(): + creator_pid = _GETPID() + functions = _CONNECTION_ESCROW.functions() + if _GETPID() != creator_pid: # pragma: forked child + msg = "SQLite connection construction cannot continue after fork" + raise RuntimeError(msg) + try: + connection = _connect( + os.fspath(self._canonical_path), + factory=_ForkSafeConnection, + timeout=sqlite_timeout, + ) + except BlockingIOError: + raise Timeout(self.lock_file) from None + if functions is not None: # pragma: <3.12 cover # pragma: needs fork + connection.acquire_escrow(functions) + if _GETPID() != creator_pid: # pragma: forked child + _FORKED_DATABASES.poison_after_fork( + self._canonical_path, + _FORKED_DATABASES.identity(self._canonical_path), + ) + msg = "SQLite connection construction cannot continue after fork" + raise RuntimeError(msg) + return connection + + def _finish_connection(self) -> None: + with _sqlite_transition(): + if (connection := self._con) is None: + return + rollback_error: sqlite3.Error | None = None + if not self._connection_transaction_released: + if connection.in_transaction: + try: + connection.rollback() + except sqlite3.Error as error: + if connection.in_transaction: + raise + self._connection_transaction_released = True + rollback_error = error + else: + self._connection_transaction_released = True + else: + self._connection_transaction_released = True + try: + connection.close() + except sqlite3.Error as close_error: + if rollback_error is not None: + _raise_chained_errors(rollback_error, close_error) + raise + self._con = None + self._connection_transaction_released = True + self._connection_identity = None + if rollback_error is not None: + raise rollback_error + + def _holds_level(self) -> bool: + # A release interrupted after its rollback, but before it recorded that, leaves a level with no transaction. + if self._lock_level > 0 and self._transaction_ended(): + self._clear_lock_state() + return self._lock_level > 0 + + def _validate_reentrant(self, mode: Literal["read", "write"], cur: int) -> AcquireReturnProxy: + if self._current_mode != mode: + opposite = "write" if mode == "read" else "read" + if cur == self._write_thread_id or cur in self._read_depths: + direction = "downgrade" if mode == "read" else "upgrade" + reason = f"already holding a {opposite} lock ({direction} not allowed)" + else: + reason = ( + f"another thread holds a {opposite} lock on this instance, which coordinates processes rather " + "than threads; give each thread its own instance with is_singleton=False" + ) + msg = f"Cannot acquire {mode} lock on {self.lock_file} (lock id: {id(self)}): {reason}" + raise RuntimeError(msg) + if mode == "write" and cur != self._write_thread_id: + msg = ( + f"Cannot acquire write lock on {self.lock_file} (lock id: {id(self)}) " + f"from thread {cur} while it is held by thread {self._write_thread_id}" + ) + raise RuntimeError(msg) + if mode == "read": + self._read_depths[cur] = self._read_depths.get(cur, 0) + 1 + self._lock_level += 1 + return AcquireReturnProxy(lock=self) + + def _acquire_transaction_lock( + self, *, blocking: bool, timeout: float, cancel_check: Callable[[], bool] | None + ) -> None: + if not _acquire_thread_lock( + self._transaction_lock, + blocking=blocking, + timeout=timeout, + poll_interval=_CANCEL_POLL_INTERVAL, + cancel_check=cancel_check, + ): + raise Timeout(self.lock_file) from None + + def _raise_if_acquiring(self, operation: Literal["acquire", "close", "release"]) -> None: + with self._internal_lock: + active_in_current_thread = threading.get_ident() in self._acquisition_thread_ids + if active_in_current_thread: # pragma: no cover - exercised in an audit callback + msg = f"Cannot {operation} ReadWriteLock on {self.lock_file} while acquisition is active in this thread" + raise RuntimeError(msg) + + def _raise_if_process_changed(self, operation_pid: int) -> None: + if _GETPID() != operation_pid or self._inherited: # pragma: forked child + msg = f"ReadWriteLock on {self.lock_file} was invalidated by fork(); construct a new instance" + raise RuntimeError(msg) + + +def _connect(database: str, *, factory: type[_ForkSafeConnection], timeout: float) -> _ForkSafeConnection: + _FORKED_DATABASES.note_sqlite_use() + # A symlink at the path would make SQLite open, lock, or create its target, so connect through a descriptor that + # refuses one. + if sys.platform == "win32": # pragma: win32 cover + fd: Final = _open_lock_database(database, oplock_wait=timeout) + link: _LinkDirectory | None = None + # A Windows lock belongs to the handle that took it, so closing this one leaves SQLite's locks in place. + try: + _raise_if_wal(fd, database) + target = database + if not _directory_is_private(os.fspath(pathlib.Path(database).parent)): + link = _link_beside_on_windows(database, _identity_of_fd(fd)) + target = os.fspath(link.database) + try: + connection = sqlite3.connect( + target, check_same_thread=False, factory=factory, cached_statements=0, timeout=timeout + ) + except BaseException: + if link is not None: + _remove_link_directory(link) + raise + connection.retain_link(link) + return connection + finally: + os.close(fd) + return _connect_through_descriptor( # pragma: win32 no cover + database, factory=factory, timeout=timeout, owner=os.geteuid() + ) + + +def _connect_through_descriptor( # pragma: win32 no cover + database: str, *, factory: type[_ForkSafeConnection], timeout: float, owner: int +) -> _ForkSafeConnection: + # Listed before its lease is taken and cleared once a connection owns it, so any exception between, an interrupt + # included, releases the lease instead of pinning the inode's probes. + claimed: Final[list[_Probe]] = [] + link: _LinkDirectory | None = None + try: + if (probe := _PROBE_DESCRIPTORS.adopt(database, claimed)) is None: + probe = _PROBE_DESCRIPTORS.retain(_open_lock_database(database), claimed) + identity, fd, _ = probe + # Releases a finalizer queued while the table was locked. + _PROBE_DESCRIPTORS.drain() + _raise_if_wal(fd, database) + target = database + # SQLite opens the name it is given, following symlinks, and uses the -journal, -wal and -shm names beside it, + # so the database name is safe only where no process of another account can create names. A descriptor path + # such as /proc/self/fd/ is no better: SQLite reads it back into the name the kernel reports. + if not _names_are_private(pathlib.Path(database).parent, owner): # pragma: needs posix-hard-link + link = _link_beside(database, identity, owner) + target = os.fspath(link.database) + connection = sqlite3.connect( + target, check_same_thread=False, factory=factory, cached_statements=0, timeout=timeout + ) + connection.retain_until_close(link, probe) + claimed.clear() + return connection + finally: + for unowned in claimed: + _PROBE_DESCRIPTORS.release(unowned) + if claimed and link is not None: # pragma: needs posix-hard-link + # A connection that already took the link removes it too when it closes. + _remove_link_directory(link) + + +def _names_are_private(directory: pathlib.Path, owner: int) -> bool: # pragma: win32 no cover + status: Final = os.lstat(directory) + # The sticky bit still lets others create names, so a directory anyone else may write to never qualifies. + return status.st_uid in {owner, 0} and not status.st_mode & (stat.S_IWGRP | stat.S_IWOTH) + + +class _LinkDirectory(NamedTuple): # pragma: needs posix-hard-link + path: pathlib.Path + identity: _DatabaseIdentity + # Made through a descriptor of path, which nobody else can list, so a directory another process swaps in under + # path cannot hold this name and SQLite fails to open instead of following the swap. + name: str + + @property + def database(self) -> pathlib.Path: + return self.path / self.name / _LINK_NAME + + +def _link_beside( # pragma: needs posix-hard-link + database: str, identity: _DatabaseIdentity, owner: int +) -> _LinkDirectory: + # Beside the database because a link cannot cross filesystems, and TMPDIR is often on another one. + parent: Final = pathlib.Path(database).parent + _sweep_links_of_the_dead(parent, identity, owner) + path: Final = pathlib.Path(tempfile.mkdtemp(prefix=_link_prefix(), dir=parent)) + fd: Final = os.open(path, _DIRECTORY_FLAGS) + try: + made: Final = os.fstat(fd) + # Whoever can rename in the parent can put another directory under the new name before it opens. + if made.st_uid != owner or made.st_mode & 0o077: + msg = f"link directory {os.fspath(path)!r} was replaced while opening" + raise OSError(msg) + link: Final = _LinkDirectory(path, (made.st_dev, made.st_ino), secrets.token_hex(8)) + try: + os.mkdir(link.name, 0o700, dir_fd=fd) + os.link(database, link.database, follow_symlinks=False) + linked: Final = link.database.stat(follow_symlinks=False) + except BaseException: + _remove_link_directory(link) + raise + finally: + os.close(fd) + if (linked.st_dev, linked.st_ino) != identity: + _remove_link_directory(link) + msg = f"lock database changed while opening: {database!r}" + raise OSError(msg) + return link + + +def _sweep_links_of_the_dead( + parent: pathlib.Path, identity: _DatabaseIdentity, owner: int +) -> None: # pragma: needs posix-hard-link + # A process killed while it held a link leaves the directory, and an extra link to the database, behind for good. + # Two threads sweeping at once only both try the same removals, which a lock would cost a fork-safety reset for. + if identity in _SWEPT_DATABASES: + return + _SWEPT_DATABASES.add(identity) + try: + with os.scandir(parent) as entries: + names: Final = [ + entry.name + for entry in entries + if entry.name.startswith(_LINK_PREFIX) and entry.is_dir(follow_symlinks=False) + ] + except OSError: + return + for name in names: + # A peer sweeping alongside, or a directory this user cannot remove, must not fail the connect. + with suppress(OSError): + _remove_dead_links(parent / name, identity, owner) + + +def _remove_dead_links( + path: pathlib.Path, identity: _DatabaseIdentity, owner: int +) -> None: # pragma: needs posix-hard-link + if sys.platform == "win32": # pragma: win32 cover + _remove_dead_links_on_windows(path, identity) + return + # Any directory can carry a dead holder's name after a rename in the parent, so only hard links to this database + # go, and the directory only once that leaves it empty. + fd: Final = os.open(path, _DIRECTORY_FLAGS) + try: + # Only a directory this account made carries a holder of its own; a name another account chose is never + # parsed, so no PID is probed on its behalf. + if os.fstat(fd).st_uid != owner or not _owner_is_gone(path.name): + return + with os.scandir(fd) as entries: + names: Final = [entry.name for entry in entries] + for entry in names: + # Releases up to 4.2.0 linked the database right inside the directory. + if entry == _LINK_NAME: + _unlink_link_to(identity, fd) + continue + try: + nested = os.open(entry, _DIRECTORY_FLAGS, dir_fd=fd) + except OSError: + continue + try: + _unlink_link_to(identity, nested) + finally: + os.close(nested) + with suppress(OSError): + os.rmdir(entry, dir_fd=fd) + finally: + os.close(fd) + with suppress(OSError): + path.rmdir() + + +def _unlink_link_to(identity: _DatabaseIdentity, dir_fd: int) -> None: # pragma: needs posix-hard-link + try: + linked: Final = os.stat(_LINK_NAME, dir_fd=dir_fd, follow_symlinks=False) + except OSError: + return + if (linked.st_dev, linked.st_ino) == identity: + os.unlink(_LINK_NAME, dir_fd=dir_fd) + + +def _owner_is_gone(name: str) -> bool: # pragma: needs posix-hard-link + match name.removeprefix(_LINK_PREFIX).split("-"): + case [pid, start, digest, _] if ( + pid.isascii() and pid.isdigit() and (not start or (start.isascii() and start.isdigit())) + ): + if not 1 <= (owner_pid := int(pid)) <= 2**31 - 1: + return False + host: Final = host_name() + return digest == _host_digest(host) and owner_is_stale(owner_pid, host, int(start) if start else None) + case _: + return False + + +def _link_prefix() -> str: # pragma: needs posix-hard-link + # The owner rides in the name, which mkdtemp creates in one step, so no directory ever exists without it. + start: Final = process_start_token(pid := os.getpid()) + return f"{_LINK_PREFIX}{pid}-{'' if start is None else start}-{_host_digest(host_name())}-" + + +def _host_digest(host: str) -> str: # pragma: needs posix-hard-link + # SHA-256 as host_name does: some runtimes, GraalPy's among them, have no BLAKE2 with a custom digest size. + return hashlib.sha256(host.encode()).hexdigest()[:16] + + +def _remove_link_directory(link: _LinkDirectory) -> None: # pragma: needs posix-hard-link + if sys.platform == "win32": # pragma: win32 cover + _remove_link_directory_on_windows(link) + return + # A rename in the parent may have put any directory under this name, so entries go through a descriptor checked + # to be the directory made, and the name goes only through rmdir, which refuses a non-empty directory. + try: + fd: Final = os.open(link.path, _DIRECTORY_FLAGS) + except OSError: + return + try: + opened: Final = os.fstat(fd) + if (opened.st_dev, opened.st_ino) != link.identity: + return + for suffix in ("", "-journal", "-wal", "-shm"): + with suppress(FileNotFoundError): + os.unlink(f"{link.name}/{_LINK_NAME}{suffix}", dir_fd=fd) + with suppress(FileNotFoundError): + os.rmdir(link.name, dir_fd=fd) + finally: + os.close(fd) + # The link is gone, so a failure here leaves one stray empty directory and is not worth failing a release for. + with suppress(OSError): + link.path.rmdir() + + +if sys.platform == "win32": # pragma: win32 cover + + def _link_beside_on_windows(database: str, identity: _DatabaseIdentity) -> _LinkDirectory: + parent: Final = pathlib.Path(database).parent + # No uid exists here: a directory is a dead holder's only if its ACL admits nobody but the trusted accounts. + _sweep_links_of_the_dead(parent, identity, 0) + path: Final = parent / f"{_link_prefix()}{secrets.token_hex(8)}" + _make_private_directory(os.fspath(path)) + link: _LinkDirectory | None = None + try: + # The handle keeps a rename in the parent from swapping the directory while the link is made inside it. + with _pinned(os.fspath(path)) as directory_identity: + link = _LinkDirectory(path, directory_identity, secrets.token_hex(8)) + _fill_link_directory(link, database, identity) + except BaseException: + if link is not None: # pragma: no branch # a pinned directory always gets its link + _remove_link_directory(link) + raise + return link + + def _fill_link_directory(link: _LinkDirectory, database: str, identity: _DatabaseIdentity) -> None: + if not _directory_is_private(os.fspath(link.path)): + msg = f"link directory {os.fspath(link.path)!r} was replaced while opening" + raise OSError(msg) + _make_private_directory(os.fspath(link.path / link.name)) + os.link(database, link.database) + # The open database handle pins its path against a rename, so only a file index that is not unique can differ. + if _identity_at(os.fspath(link.database)) != identity: # pragma: no cover + msg = f"lock database changed while opening: {database!r}" + raise OSError(msg) + + def _remove_dead_links_on_windows(path: pathlib.Path, identity: _DatabaseIdentity) -> None: + with _pinned(os.fspath(path)): + if not _directory_is_private(os.fspath(path)) or not _owner_is_gone(path.name): + return + with os.scandir(path) as entries: + nested: Final = [pathlib.Path(entry.path) for entry in entries] + for directory in nested: + _unlink_link_to_on_windows(identity, directory / _LINK_NAME) + with suppress(OSError): + directory.rmdir() + with suppress(OSError): + path.rmdir() + + def _unlink_link_to_on_windows(identity: _DatabaseIdentity, path: pathlib.Path) -> None: + with suppress(OSError): + if _identity_at(os.fspath(path)) == identity: + path.unlink() + + def _remove_link_directory_on_windows(link: _LinkDirectory) -> None: + # A removal never fails a release: SQLite or a scanner may still hold a name, and the next sweep retries. + with suppress(OSError): + with _pinned(os.fspath(link.path)) as directory_identity: + if directory_identity != link.identity: + return + for suffix in ("", "-journal", "-wal", "-shm"): + with suppress(FileNotFoundError): + pathlib.Path(f"{link.database}{suffix}").unlink() + with suppress(FileNotFoundError): + pathlib.Path(link.database.parent).rmdir() + link.path.rmdir() + + +def _open_lock_database(database: str, *, oplock_wait: float = 0) -> int: + if sys.platform == "win32": # pragma: win32 cover + # The handle refuses a reparse point and omits delete sharing, so the name cannot be swapped while it is open. + if (fd := _open_non_reparse_fd(database, 0o600, oplock_wait=oplock_wait)) is None: + msg = f"lock database {database!r} is held without sharing or pending deletion" + raise PermissionError(msg) + return fd + fd = os.open(database, _DB_OPEN_FLAGS, 0o600) # pragma: win32 no cover + if not stat.S_ISREG(os.fstat(fd).st_mode): # pragma: win32 no cover + os.close(fd) + msg = f"refusing a non-regular lock database: {database!r}" + raise OSError(msg) + return fd # pragma: win32 no cover + + +def _raise_if_wal(fd: int, database: str) -> None: + # SQLite derives the -wal name from the path it connects through, a descriptor name on POSIX, and WAL never blocks + # readers behind a writer, so the lock cannot work on such a database. + # pread: an adopted probe is shared, so its file offset is not this connect's to move. + header: Final = os.pread(fd, _HEADER_SIZE, 0) if hasattr(os, "pread") else os.read(fd, _HEADER_SIZE) + if header.startswith(_SQLITE_MAGIC) and _WAL_FORMAT_VERSION in header[_FORMAT_VERSIONS]: + msg = f"refusing a WAL-mode lock database: {database!r}; give ReadWriteLock a file of its own" + raise ValueError(msg) + + +@contextmanager +def _bounded_schema(connection: sqlite3.Connection, database: str) -> Generator[None]: + # The first statement loads the file's complete schema, which no timeout bounds, and a lock database has none. + # Waiting for a lock runs no virtual machine steps, so a step budget bounds only that load. + calls: Final = count() + max_calls: Final = 10 + connection.setlimit(sqlite3.SQLITE_LIMIT_LENGTH, 4096) + connection.set_progress_handler(lambda: next(calls) >= max_calls, 1000) + try: + yield + except sqlite3.DatabaseError as error: + # An interrupted schema load reports a malformed schema rather than the interrupt. + if isinstance(error, sqlite3.DataError) or next(calls) > max_calls: + msg = ( + f"refusing a lock database whose schema is too large to load: {database!r}; " + "give ReadWriteLock a file of its own" + ) + raise ValueError(msg) from error + raise + finally: + connection.set_progress_handler(None, 0) + + +@contextmanager +def _sqlite_transition() -> Generator[None]: + _SQLITE_TRANSITION_CONTEXT.depth += 1 + try: + yield + finally: + _SQLITE_TRANSITION_CONTEXT.depth -= 1 + + +def _abort_forked_sqlite_transition() -> None: # pragma: forked child + if _SQLITE_TRANSITION_CONTEXT.depth: + os._exit(_UNSAFE_FORK_EXIT_STATUS) # inherited SQLite handles cannot be used or closed safely + + +def _track_sqlite_use(event: str, _args: Unused) -> None: + if event == "sqlite3.connect": + _FORKED_DATABASES.note_sqlite_use() + + +def timeout_for_sqlite(timeout: float, *, blocking: bool, already_waited: float) -> int: + if blocking is False: + return 0 + + if timeout == -1: + return _MAX_SQLITE_TIMEOUT_MS + + if timeout < 0: + msg = "timeout must be a non-negative number or -1" + raise ValueError(msg) + + timeout_ms = int((max(timeout - already_waited, 0) if timeout > 0 else timeout) * 1000) + if timeout_ms > _MAX_SQLITE_TIMEOUT_MS or timeout_ms < 0: + _LOGGER.warning("timeout %s is too large for SQLite, using %s ms instead", timeout, _MAX_SQLITE_TIMEOUT_MS) + return _MAX_SQLITE_TIMEOUT_MS + return timeout_ms + + +_register_fork_object(_CONNECTION_ESCROW) +_register_fork_object(_FORKED_DATABASES) +_register_fork_object(_PROBE_DESCRIPTORS) +_register_fork_class(ReadWriteLock) +if _IS_PYPY: + sys.addaudithook(_track_sqlite_use) # pragma: pypy cover +if hasattr(os, "register_at_fork"): # pragma: needs fork + os.register_at_fork(after_in_child=_abort_forked_sqlite_transition) + +__all__ = [ + "ReadWriteLock", + "timeout_for_sqlite", +] diff --git a/hug/lib/python3.12/site-packages/filelock/_soft.py b/hug/lib/python3.12/site-packages/filelock/_soft.py new file mode 100644 index 0000000000000000000000000000000000000000..d54fffa70d700a341ed84d6c551c4f59c585da6e --- /dev/null +++ b/hug/lib/python3.12/site-packages/filelock/_soft.py @@ -0,0 +1,643 @@ +from __future__ import annotations + +__lazy_modules__ = [ + "contextlib", + "filelock._identity", + "filelock._soft_protocol", + "filelock._util", + "itertools", + "pathlib", + "re", + "secrets", + "typing", +] + +import os +import re +import secrets +import stat +import sys +import threading +import time +from contextlib import suppress +from errno import EACCES, EEXIST, EPERM +from itertools import islice +from pathlib import Path +from typing import Final + +from filelock._api import BaseFileLock, _raise_grouped_errors +from filelock._identity import host_name, owner_is_current_process, owner_is_stale, process_start_token +from filelock._soft_protocol import STRICT_SOFT_SENTINEL_RECORD +from filelock._util import ( + break_lock_file, + exchange_names, + exchange_supported, + pin_inode, + raise_on_not_writable_file, + remove_own_file, + write_all, +) + +if sys.platform == "win32": # pragma: win32 cover + from filelock._windows import open_read_without_oplock_wait + from filelock._windows import unlink_without_oplock_wait as unlink_path +else: # pragma: win32 no cover + from filelock._util import unlink_path + +MALFORMED_LOCK_AGE_THRESHOLD: Final[float] = 2.0 +_MAX_LOCK_FILE_SIZE: Final[int] = 1024 +_UNLINK_MAX_RETRIES: Final[int] = 10 +_MARKER_WITH_START_TOKEN_LINE_COUNT: Final[int] = 3 +#: Contenders act on the record in the marker, so no other account may write it, whatever the mode or umask. +_OTHERS_WRITE: Final[int] = stat.S_IWGRP | stat.S_IWOTH +#: All a SoftFileLock before 3.22 writes, and what a current one shows between creating its marker and writing it. +_RECORDLESS_MARKER: Final[str] = "" +_CLAIMS_IN_PROGRESS: Final[set[str]] = set() +_CLAIMS_LOCK: Final[threading.Lock] = threading.Lock() + + +class SoftFileLock(BaseFileLock): + """ + Cooperative file lock based on a shared existence marker. + + Unlike :class:`UnixFileLock ` and :class:`WindowsFileLock `, this + lock does not use OS-level locking primitives. Instead, it creates the lock file with ``O_CREAT | O_EXCL`` and + treats its existence as the lock indicator. The filesystem must provide coherent exclusive creation and directory + updates to each participating process. A crash can leave the marker behind. + + The marker contains the holder's PID and hostname. A contender may remove it when it can no longer find a same-host + process with that PID. A configured :attr:`~filelock.BaseFileLock.lifetime` also permits removal based on marker + age, including while the holder remains alive. Age-based expiry can overlap protected operations and does not + provide strict mutual exclusion. + + """ + + #: Existence locks reclaim by unlinking a pathname, so an age-based lease may break one; a native inode lock cannot. + _lifetime_supported: bool = True + + #: Age-based expiry preserves historical behavior but does not provide strict mutual exclusion. + _lifetime_replacements: tuple[str, str] | None = ("StrictSoftFileLock", "SoftFileLease") + + #: An existence lock unlinks its marker to release, so it cannot promise to keep the pathname. + _preserve_lock_file_supported: bool = False + + #: An existence lock keeps protocol state in its marker, so it cannot lend the descriptor to an on_acquired hook. + _on_acquired_supported: bool = False + + #: The marker's (mtime, inode, ctime) when this instance last saw it change, and when, on its monotonic clock. + _marker_seen: tuple[tuple[float, int, int], float] | None = None + + #: The identity of a marker a Windows release relinquished but has not unlinked yet. + _marker_cleanup: tuple[int, int] | None = None + + #: The descriptor of that marker while its close has not run yet, as a list so one C call can take it. + _marker_fds: list[int] + + def _acquire(self) -> None: + self._finish_marker_cleanup() + raise_on_not_writable_file(self._lock_path) + # O_CREAT | O_EXCL makes the create fail with EEXIST when the file already exists, so a successful open + # means this process now holds the lock. + flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_TRUNC + if (o_nofollow := getattr(os, "O_NOFOLLOW", None)) is not None: # pragma: needs o-nofollow + flags |= o_nofollow + try: + fd = os.open(self._lock_path, flags, self._open_mode() & ~_OTHERS_WRITE) + except OSError as exception: + if not ( + exception.errno == EEXIST or (exception.errno == EACCES and sys.platform == "win32") + ): # pragma: win32 no cover + raise + self._try_break_stale_lock() + return + # Publish held state only once the record is on disk. On failure, cancellation included, close the descriptor + # and unlink the path only while it still names the file we opened: after an expired lease a successor's + # marker may sit at the same path. The guard starts at the open, so a signal handler that raises before + # _mark_descriptor_owned leaves no marker behind. + identity: tuple[int, int] | None = None + try: + identity = _file_identity(os.fstat(fd)) + self._mark_descriptor_pending(fd) + self._apply_explicit_mode(fd) + self._write_lock_info(fd) + self._mark_descriptor_owned(fd, identity) + except BaseException: + self._mark_descriptor_released() + try: + if sys.platform != "win32": # pragma: win32 no cover + # Before the close: our open descriptor keeps the inode allocated, so a successor's marker cannot + # carry its number. + with suppress(OSError): + remove_own_file(self._lock_path, fd) + finally: + try: + os.close(fd) + finally: + if sys.platform == "win32": # pragma: win32 cover + self._unlink_held_marker(identity) + raise + + def _apply_explicit_mode(self, fd: int) -> None: + # os.open filters the mode through the umask, so an explicit mode lands as given only through fchmod, the way + # UnixFileLock applies it. Windows maps the mode to a read-only bit that os.open already set. + if self.has_explicit_mode and sys.platform != "win32": # pragma: win32 no cover + with suppress(PermissionError): + os.fchmod(fd, self._context.mode & ~_OTHERS_WRITE) + + def _try_break_stale_lock(self) -> None: + with suppress(OSError, ValueError), pin_inode(self._lock_path) as pinned: + content, marker = _read_lock_file(self._lock_path) + if pinned not in {None, marker.st_ino}: # pragma: win32 no cover # Windows pins nothing + return # another node replaced the pinned one before the read, and nothing keeps its number from reuse + if content == STRICT_SOFT_SENTINEL_RECORD: # pragma: needs hard-link + return + # Such a marker is held for as long as its owner runs, so being unchanged proves nobody gone. + if content == _RECORDLESS_MARKER: + return + holder = _parse_lock_holder(content) + + if holder is None: + # Unparsable: wrong line count, a non-integer PID or start token, empty, oversized or not UTF-8. + # Self-heal only once the file is clearly not a half-written fresh lock (a peer between O_EXCL and + # _write_lock_info), so the brief create-then-write window is never mistaken for a stale lock. + if self._marker_unchanged_for(marker) >= MALFORMED_LOCK_AGE_THRESHOLD: + self._take_over(marker, _placeholder_breaker(content)) + return + + if owner_is_stale(*holder): + self._take_over(marker, holder[0]) + + def _try_break_expired_lock(self) -> None: + if (lifetime := self._context.lifetime) is None: + return + with suppress(OSError), pin_inode(self._lock_path) as pinned: + # _read_lock_file takes the lstat, not stat: a symlink swapped in at the path must report its own mtime. + content, marker = _read_lock_file(self._lock_path) + if pinned not in {None, marker.st_ino} or time.time() - marker.st_mtime < lifetime: + return + holder = self._recorded_holder(content) + self._take_over(marker, None if holder is None else holder[0]) + + def _take_over(self, marker: os.stat_result, breaker_pid: int | None) -> None: + # Swap the judged marker for a complete marker of ours in one exchange, so the lock path never stands empty. + # Once the file swapped out proves to be the one judged, this lock holds the path; the caller's pin keeps that + # file's inode number from naming anything else. Only a peer's exchange can move our marker after that, and + # the peer puts it back because what it took is not the file it judged. Every failure before the handoff takes + # our marker off the path and puts back what the exchange took. Without an exchange, the portable break decides. + if exchange_supported(): # pragma: win32 no cover + self._take_over_by_exchange(marker, breaker_pid) + else: + break_lock_file(self._lock_path, marker.st_mtime, marker.st_ino) + + def _take_over_by_exchange(self, marker: os.stat_result, breaker_pid: int | None) -> None: # pragma: win32 no cover + if (current := os.lstat(self._lock_path)).st_mtime != marker.st_mtime or current.st_ino != marker.st_ino: + return + if breaker_pid is not None and self._restore_stranded_marker(breaker_pid, marker): + return + aside: Final = f"{self._lock_path}.break.{os.getpid()}.{secrets.token_hex(16)}" + flags: Final = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0) + fd: Final = os.open(aside, flags, self._open_mode() & ~_OTHERS_WRITE) + self._mark_descriptor_pending(fd) + exchanged = held = False + try: + identity: Final = _file_identity(os.fstat(fd)) + self._apply_explicit_mode(fd) + self._write_lock_info(fd) + if not exchange_names(aside, self._lock_path): + return # NFS, SMB and the like refuse it; renaming the marker aside would empty the path + exchanged = True + if (taken := os.lstat(aside)).st_mtime != marker.st_mtime or taken.st_ino != marker.st_ino: + return # a peer recreated the marker, which the cleanup below swaps back + Path(aside).unlink() + exchanged = False + self._mark_descriptor_owned(fd, identity) + held = True + finally: + if not held: + self._abandon_take_over(fd, aside, put_back=exchanged) + + def _restore_stranded_marker(self, breaker_pid: int, marker: os.stat_result) -> bool: # pragma: win32 no cover + # A breaker killed between its exchange and the put-back leaves its own marker at the path and the live + # holder's marker under its break name. A recoverer first renames that record to a name of its own. Only one + # contender wins the rename, so two of them never exchange the same record and reverse each other. A record + # a dead recoverer holds is claimable again. A claim of this process that no thread is working on was + # abandoned by a failed put-back, and the next poll takes it up again. Where the marker cannot be put back + # the lock counts as held, as it does wherever no exchange is possible. + if (candidates := self._stranded_candidates(breaker_pid)) is None: + return True + return any(self._recover_claimed(path, breaker_pid, marker, adopt=abandoned) for path, abandoned in candidates) + + def _stranded_candidates(self, breaker_pid: int) -> list[tuple[str, bool]] | None: # pragma: win32 no cover + directory, name = os.path.split(self._lock_path) + stranded: Final = re.compile(rf"{re.escape(name)}\.break\.{breaker_pid}\.[0-9a-f]{{32}}") + claimed: Final = re.compile(rf"{re.escape(name)}\.restore\.(\d+)\.[0-9a-f]{{32}}") + candidates: list[tuple[str, bool]] = [] + with os.scandir(directory or os.curdir) as entries: + for entry in entries: + if not entry.is_file(follow_symlinks=False): + continue + path = str(Path(directory) / entry.name) + if stranded.fullmatch(entry.name): + candidates.append((path, False)) + elif found := claimed.fullmatch(entry.name): + ours = int(found[1]) == os.getpid() + if not (ours or owner_is_stale(int(found[1]), host_name(), None)): + return None + candidates.append((path, ours)) + return sorted(candidates) + + def _recover_claimed( + self, path: str, breaker_pid: int, marker: os.stat_result, *, adopt: bool + ) -> bool: # pragma: win32 no cover + # One try/finally owns the registration from the first step to the last, so no interruption can leave it set. + # Registered before the rename publishes the name, so a thread that scans meanwhile sees a claim in progress. + own: Final = path if adopt else f"{self._lock_path}.restore.{os.getpid()}.{secrets.token_hex(16)}" + if not _begin_claim(own): + return True + try: + if not adopt: + try: + Path(path).rename(own) + except OSError: # a peer that won the rename now holds the record, so the lock counts as held + return True + return self._restore_claimed(own, breaker_pid, marker) + finally: + _end_claim(own) + + def _restore_claimed(self, own: str, breaker_pid: int, marker: os.stat_result) -> bool: # pragma: win32 no cover + try: + content, _ = _read_lock_file(own) + except OSError: + self._return_claim(own, breaker_pid) + return True + if not self._marker_is_live(content, breaker_pid): + with suppress(OSError): + Path(own).unlink() + return False + # The inode of the marker judged dead must still be at the path, or a peer has acted since. + with suppress(OSError): + current = os.lstat(self._lock_path) + if (current.st_ino, current.st_mtime) == (marker.st_ino, marker.st_mtime) and exchange_names( + own, self._lock_path + ): + if os.lstat(own).st_ino == marker.st_ino: + Path(own).unlink() + return True + # A peer replaced the path after the check, so the exchange took its marker: swap it straight back. + exchange_names(own, self._lock_path) + self._return_claim(own, breaker_pid) + return True + + def _return_claim(self, own: str, breaker_pid: int) -> None: # pragma: win32 no cover + # A refused rename leaves the claim under this process's PID, and the next poll retries it. + with suppress(OSError): + Path(own).rename(f"{self._lock_path}.break.{breaker_pid}.{secrets.token_hex(16)}") + + @classmethod + def _marker_is_live(cls, content: str | None, breaker_pid: int) -> bool: # pragma: win32 no cover + if content == STRICT_SOFT_SENTINEL_RECORD: # pragma: needs hard-link + return True + if content == _RECORDLESS_MARKER: + # An empty marker is a pre-3.22 holder for as long as it exists, as the stale check treats it. + return True + # The breaker's own marker names its PID on this host; the same number on another host is a holder. + return ( + (holder := _parse_lock_holder(content)) is not None + and (holder[0] != breaker_pid or holder[1] != host_name()) + and not owner_is_stale(*holder) + ) + + def _abandon_take_over(self, fd: int, aside: str, *, put_back: bool) -> None: # pragma: win32 no cover + self._mark_descriptor_released() + try: + if put_back: + exchange_names(aside, self._lock_path) + finally: + try: + # Whichever name still holds our marker loses it: the descriptor keeps its inode number ours. + own: Final = os.fstat(fd) + for name in (self._lock_path, aside): + with suppress(FileNotFoundError): + if os.path.samestat(os.lstat(name), own): + Path(name).unlink() + finally: + os.close(fd) + + def _marker_unchanged_for(self, marker: os.stat_result) -> float: + # The mtime is the writer's or file server's clock, so against time.time() a contender whose clock runs ahead + # would expire a fresh marker. Only watch it for change, timed on this process's monotonic clock. The owner of a + # file sets its mtime, and ext4 reuses a freed inode number, so a new marker can repeat both. The change time + # cannot be set and moves with every create, which keeps a new marker from inheriting an old one's age. + now: Final = time.monotonic() + key: Final = marker.st_mtime, marker.st_ino, marker.st_ctime_ns + if (seen := self._marker_seen) is None or seen[0] != key: + self._marker_seen = key, now + return 0.0 + return now - seen[1] + + def _unprobeable_holder(self) -> str | None: + # This host cannot prove such a holder dead, so whoever decides whether it is gone needs its name. + with suppress(OSError, ValueError): + if (content := _read_lock_file(self._lock_path)[0]) == _RECORDLESS_MARKER: + return "an owner that recorded no PID (a filelock release before 3.22, or one still writing its record)" + if (holder := self._recorded_holder(content)) and holder[1] != host_name(): + return f"pid {holder[0]} on {holder[1]}" + return None + + @classmethod + def _recorded_holder(cls, content: str | None) -> tuple[int, str] | None: + return None if (holder := _parse_lock_holder(content)) is None else holder[:2] + + @staticmethod + def _write_lock_info(fd: int) -> None: + # No suppression: a write failure must reach the acquisition rollback so it never publishes a half-written + # marker as held state. The optional third line is this process's start token, absent when the platform + # exposes no proven start time, in which case a reader falls back to PID-only liveness. + info = f"{os.getpid()}\n{host_name()}\n" + if (token := process_start_token(os.getpid())) is not None: + info += f"{token}\n" + write_all(fd, info.encode()) + + @property + def pid(self) -> int | None: + """ + The PID of the process holding this lock, read from the lock file. + + :returns: the PID as an integer, or ``None`` if the lock file does not exist or cannot be parsed + + """ + with suppress(OSError, ValueError): + holder = _parse_lock_holder(_read_lock_file(self._lock_path)[0]) + if holder is not None: + return holder[0] + return None + + @property + def is_lock_held_by_us(self) -> bool: + """ + Whether this lock is held by the current process. + + :returns: ``True`` if the lock file exists and names the current process's PID, hostname and, when recorded, + start token + + """ + with suppress(OSError, ValueError): + if (holder := _parse_lock_holder(_read_lock_file(self._lock_path)[0])) is not None: + return owner_is_current_process(*holder) + return False + + def break_lock(self) -> None: + """Forcibly break the lock by removing the lock file, regardless of who holds it.""" + with suppress(OSError): + Path(self._lock_path).unlink() + + def _release_current(self, *, force: bool) -> None: + try: + super()._release_current(force=force) + finally: + # A release a handler cut short after the descriptor closed still owes the unlink on Windows. + self._finish_marker_cleanup() + + def _release(self) -> None: + fd = self._context.lock_file_fd + assert fd is not None # ruff:ignore[assert] # _release runs only while held, so the descriptor is set + # Capture the held file's identity before closing so cleanup can refuse to unlink a successor's marker. A + # supported lifetime lease lets a peer break our expired marker and create its own at this path before we + # release; unlinking by path alone would then delete the successor's lock. + identity: tuple[int, int] | None = None + with suppress(OSError): + identity = _file_identity(os.fstat(fd)) + if sys.platform == "win32": # pragma: win32 cover + self._release_then_unlink(fd, identity) + else: # pragma: win32 no cover + # Our open descriptor keeps the inode allocated, so its number cannot name a successor's marker yet, and an + # interrupt before the unlink lands leaves the lock held for the retried release to finish. + unlinked: Final = self._unlink_open_marker(fd, identity) + # A failed close may already have released and recycled the descriptor number. Relinquish it before the + # one close attempt so no later release can close an unrelated descriptor that reused the same integer. + self._mark_descriptor_released() + try: + self._close_released_fd(fd, default_suppresses=False) + finally: + # A share that refuses to delete an open file gets the unlink after the close; the marker still exists + # until then, so its inode cannot be reused either. + if not unlinked: + self._unlink_held_marker(identity) + + def _release_then_unlink(self, fd: int, identity: tuple[int, int] | None) -> None: # pragma: win32 cover + # Windows cannot unlink an open file, so the unlink outlives the hold until it lands, here or on a later acquire + # or release. Only the gate's holder touches the pending record, so two threads never overwrite each other's. + with self._transition_lock: + self._marker_fds = [fd] + self._marker_cleanup = identity + self._mark_descriptor_released() + try: + self._close_marker_fd() + # Marker cleanup must also run for control-flow exceptions, and both failures must remain observable. + except BaseException as close_error: + try: + self._finish_marker_cleanup() + except BaseException as cleanup_error: # ruff:ignore[blind-except] # keep control-flow failures + _raise_grouped_errors( + "lock descriptor close and marker cleanup both failed", + close_error, + cleanup_error, + ) + raise + self._finish_marker_cleanup() + + def _marker_names(self) -> tuple[str, ...]: + # After a rename of the lock's directory only the caller's spelling, resolved through the working directory, + # still reaches the marker this hold opened. + return tuple(dict.fromkeys((self._lock_path, self.lock_file))) + + def _unlink_open_marker(self, fd: int, identity: tuple[int, int] | None) -> bool: # pragma: win32 no cover + if identity is None: + return True + replaced = False + for name in self._marker_names(): + try: + if _file_identity(os.lstat(name)) == identity: + Path(name).unlink() + return True + replaced = True + except FileNotFoundError: + continue + except OSError: + return False + if replaced: + try: + _leave_released_record(fd) + except OSError: + return False + return True + + def _finish_marker_cleanup(self) -> None: + # Never waits: whoever holds the gate is acquiring or releasing this lock and finishes the cleanup itself. + if self._marker_cleanup is not None and self._transition_lock.acquire(blocking=False): # pragma: win32 cover + try: + # A close error here belongs to the release an interrupt cut short, not to this caller. + with suppress(OSError): + self._close_marker_fd() + if (identity := self._marker_cleanup) is not None: # pragma: no branch # the gate's holder clears it + self._windows_unlink_if_ours(identity) + finally: + self._transition_lock.release() + + def _close_marker_fd(self) -> None: # pragma: win32 cover + # Popping and closing is one chain of C calls that no signal handler can split, so a retried release closes the + # descriptor at most once; a second close could hit an unrelated file that reused the number. + try: + list(map(os.close, islice(iter(vars(self).setdefault("_marker_fds", []).pop, None), 1))) + except IndexError: + return + except OSError: + if not self._suppresses_close_error(default_suppresses=False): + raise + + def _unlink_held_marker(self, identity: tuple[int, int] | None) -> None: + if sys.platform == "win32": # pragma: win32 cover + if identity is not None: + self._windows_unlink_if_ours(identity) + else: # pragma: win32 no cover + for name in self._marker_names(): + with suppress(OSError): + if _file_identity(os.lstat(name)) == identity: + Path(name).unlink() + return + + def _windows_unlink_if_ours(self, identity: tuple[int, int]) -> None: # pragma: win32 cover + retry_delay = 0.001 + for attempt in range(_UNLINK_MAX_RETRIES): + # Windows doesn't immediately release file handles after close, causing EACCES/EPERM on unlink. Recheck + # identity each attempt: a failed unlink leaves a window for a successor to replace the marker at this path. + try: + for name in self._marker_names(): + with suppress(FileNotFoundError): + if _file_identity(os.lstat(name)) == identity: + # Forgotten before the unlink: Windows does not promise unique file IDs, so no retry may + # reach a successor that reused ours. + self._marker_cleanup = None + unlink_path(name) + break + except OSError as exc: + if exc.errno in {EACCES, EPERM}: + # The file is still ours, so the next acquire or release tries again if these attempts run out. + self._marker_cleanup = identity + if attempt < _UNLINK_MAX_RETRIES - 1: + time.sleep(retry_delay) + retry_delay *= 2 + continue + self._marker_cleanup = None + return + + +def _file_identity(st: os.stat_result) -> tuple[int, int]: + # (st_dev, st_ino) names the concrete inode behind a path, so a marker recreated at the same pathname after an + # expired lease reads as a different file. CPython populates both on Windows from the volume serial and file index. + return st.st_dev, st.st_ino + + +def _begin_claim(name: str | Path) -> bool: # pragma: win32 no cover + # Keyed by the file name alone: its random token identifies the claim, and the spelling of the directory varies. + with _CLAIMS_LOCK: + if (key := Path(name).name) in _CLAIMS_IN_PROGRESS: + return False + _CLAIMS_IN_PROGRESS.add(key) + return True + + +def _end_claim(name: str | Path) -> None: # pragma: win32 no cover + with _CLAIMS_LOCK: + _CLAIMS_IN_PROGRESS.discard(Path(name).name) + + +def _placeholder_breaker(content: str | None) -> int | None: # pragma: win32 no cover + # The placeholder a break leaves at the path while it works names the breaker that may have died there. + if content is None or not content.startswith(placeholder := "filelock/break\n") or not content.endswith("\n"): + return None + with suppress(ValueError): + return parse_decimal(content.removeprefix(placeholder)[:-1]) + return None + + +def _leave_released_record(fd: int) -> None: + # A peer's exchange holds our marker under its break name and will put it back. Overwriting the record lets + # contenders age it out instead of waiting on this live PID. The write comes before the truncate so the file is + # never empty. + if sys.platform != "win32": # pragma: win32 no cover + os.pwrite(fd, released := b"filelock/released\n", 0) + os.ftruncate(fd, len(released)) + + +def _read_lock_file(path: str) -> tuple[str | None, os.stat_result]: + # A legitimate lock file is always a regular file. Classify the path with lstat first, so any other node (symlink, + # FIFO, socket, device) is reported as a malformed lock the caller can evict, without an os.open that would follow + # a symlink, stall on a FIFO, or fail on a socket and leave acquisition wedged. The stat result still flows back + # for the identity-checked stale break. lstat, not stat, so a hostile symlink is never followed onto its target. + st = os.lstat(path) + if not stat.S_ISREG(st.st_mode): # pragma: needs fifo + return None, st + # Re-check on the opened handle: O_NOFOLLOW refuses a symlink swapped in after the lstat, O_NONBLOCK stops a FIFO + # swapped in from stalling the open, and the fstat catches any other non-regular replacement race before we read. + # The capped read stops a huge regular file (e.g. one filled from /dev/zero) from exhausting memory. + if sys.platform == "win32": # pragma: win32 cover + # os.open would wait for an oplock holder to answer its break, past the caller's deadline. + fd = open_read_without_oplock_wait(path) + else: # pragma: win32 no cover + fd = os.open(path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0)) + try: + st = os.fstat(fd) + if not stat.S_ISREG(st.st_mode): # pragma: no cover # only a non-regular node swapped in after the lstat + return None, st + data = os.read(fd, _MAX_LOCK_FILE_SIZE + 1) + finally: + os.close(fd) + if len(data) <= _MAX_LOCK_FILE_SIZE: + with suppress(UnicodeDecodeError): + return data.decode("utf-8"), st + return None, st + + +def _parse_lock_holder(content: str | None) -> tuple[int, str, int | None] | None: + # A well-formed lock file is "\n\n" with an optional "\n" third line naming the + # holder's process start instant (a filelock 3.29 marker wrote this only on Windows; every platform writes it now). + # Anything else (wrong line count, a non-integer PID or start token, empty or unreadable content) is unparsable; + # returning None lets the caller treat it as a malformed lock to self-heal rather than a holder. + # Every writer since 3.22 ends each line with "\n", so a record without one was cut by a crash or read mid-write. As + # a holder it would name a foreign host no one reclaims, or a start token that lets a contender break a live lock. + if not content or not content.endswith("\n") or len(lines := content.strip().splitlines()) not in {2, 3}: + return None + try: + pid = parse_decimal(lines[0]) + start_token = parse_decimal(lines[2]) if len(lines) == _MARKER_WITH_START_TOKEN_LINE_COUNT else None + except ValueError: + return None + # A pid outside the valid range is a malformed lock, not a holder. Without this, a non-positive pid + # reaches os.kill() where 0 / -1 mean "the caller's own process group / every process" so a dead + # holder reads as alive and the lock is never reclaimed, while an oversized pid raises OverflowError + # (not OSError/ValueError) out of the self-heal path. _parse_marker_bytes already enforces this range. + if not 1 <= pid <= 2**31 - 1: + return None + return pid, lines[1], start_token + + +def parse_decimal(text: str) -> int: + """ + Read a PID or start token as a writer formats it: ASCII digits only. + + :raises ValueError: for anything else ``int()`` would accept, such as ``1_000``, surrounding whitespace, a sign or + non-ASCII digits; no filelock writes those + + """ + if not (text.isascii() and text.isdigit()): + msg = f"not a decimal integer: {text!r}" + raise ValueError(msg) + return int(text) + + +__all__ = [ + "MALFORMED_LOCK_AGE_THRESHOLD", + "SoftFileLock", + "parse_decimal", +] diff --git a/hug/lib/python3.12/site-packages/filelock/_soft_protocol.py b/hug/lib/python3.12/site-packages/filelock/_soft_protocol.py new file mode 100644 index 0000000000000000000000000000000000000000..43f048a6d49b4a35449f36aa10094a309c3f43bf --- /dev/null +++ b/hug/lib/python3.12/site-packages/filelock/_soft_protocol.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +__lazy_modules__ = ["typing"] + +from typing import Final + +STRICT_SOFT_SENTINEL_RECORD: Final[str] = "1\nfilelock-strict-v1\x00\n0\n" + +__all__ = [ + "STRICT_SOFT_SENTINEL_RECORD", +] diff --git a/hug/lib/python3.12/site-packages/filelock/_soft_rw/__init__.py b/hug/lib/python3.12/site-packages/filelock/_soft_rw/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..9401ecc9ba9b637ab8ea8131273fd22fee474dbd --- /dev/null +++ b/hug/lib/python3.12/site-packages/filelock/_soft_rw/__init__.py @@ -0,0 +1,14 @@ +"""Cross-process and cross-host reader/writer lock over a generation log of immutable snapshots.""" + +from __future__ import annotations + +__lazy_modules__ = ["filelock._soft_rw._async"] + +from filelock._soft_rw._async import AsyncAcquireSoftReadWriteReturnProxy, AsyncSoftReadWriteLock +from filelock._soft_rw._sync import SoftReadWriteLock + +__all__ = [ + "AsyncAcquireSoftReadWriteReturnProxy", + "AsyncSoftReadWriteLock", + "SoftReadWriteLock", +] diff --git a/hug/lib/python3.12/site-packages/filelock/_soft_rw/__pycache__/__init__.cpython-312.pyc b/hug/lib/python3.12/site-packages/filelock/_soft_rw/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..04c1e06b88878c3b86acf0008e09c21c17fe295d Binary files /dev/null and b/hug/lib/python3.12/site-packages/filelock/_soft_rw/__pycache__/__init__.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/filelock/_soft_rw/__pycache__/_async.cpython-312.pyc b/hug/lib/python3.12/site-packages/filelock/_soft_rw/__pycache__/_async.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..058ce1e83239d2f90fc62ab03611a504ffb1d89b Binary files /dev/null and b/hug/lib/python3.12/site-packages/filelock/_soft_rw/__pycache__/_async.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/filelock/_soft_rw/__pycache__/_protocol.cpython-312.pyc b/hug/lib/python3.12/site-packages/filelock/_soft_rw/__pycache__/_protocol.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aa28129ac7f5694d0f977325d9f3204d994efb6d Binary files /dev/null and b/hug/lib/python3.12/site-packages/filelock/_soft_rw/__pycache__/_protocol.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/filelock/_soft_rw/__pycache__/_storage.cpython-312.pyc b/hug/lib/python3.12/site-packages/filelock/_soft_rw/__pycache__/_storage.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..58c4be610b812de74dbb28945ad795699176c134 Binary files /dev/null and b/hug/lib/python3.12/site-packages/filelock/_soft_rw/__pycache__/_storage.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/filelock/_soft_rw/__pycache__/_sync.cpython-312.pyc b/hug/lib/python3.12/site-packages/filelock/_soft_rw/__pycache__/_sync.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9d91299f6ca31dfd792bcb8a227f25cf5a3890c1 Binary files /dev/null and b/hug/lib/python3.12/site-packages/filelock/_soft_rw/__pycache__/_sync.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/filelock/_soft_rw/_async.py b/hug/lib/python3.12/site-packages/filelock/_soft_rw/_async.py new file mode 100644 index 0000000000000000000000000000000000000000..f751d6b95b73f910dd6e3ad12b9729bca820c063 --- /dev/null +++ b/hug/lib/python3.12/site-packages/filelock/_soft_rw/_async.py @@ -0,0 +1,373 @@ +"""Async wrapper around :class:`SoftReadWriteLock` for use with ``asyncio``.""" + +from __future__ import annotations + +__lazy_modules__ = ["asyncio", "filelock._async", "filelock._error", "functools", "threading"] + +import asyncio +import functools +import os +import threading +from contextlib import asynccontextmanager +from typing import TYPE_CHECKING, Final, Literal, ParamSpec, TypeVar + +from filelock._async import ( + _TASKS_THREAD_ID, + _BackendOutcome, + _capture_call, + _drain_future, + _future_result, + _raise_cancelled_error, + _task_owners_for, + _wait_until_done, +) +from filelock._error import Timeout +from filelock._soft_rw._sync import SoftReadWriteLock, _lost_hold_error + +if TYPE_CHECKING: + from collections.abc import AsyncGenerator, Callable + from concurrent import futures + from types import TracebackType + + from filelock._api import AcquireReturnProxy + from filelock._lease import LeaseCompromise + +_P = ParamSpec("_P") +_R = TypeVar("_R") + + +class AsyncSoftReadWriteLock: + """ + Async wrapper around :class:`SoftReadWriteLock` for ``asyncio`` applications. + + The sync class's blocking filesystem operations run on a thread pool via ``loop.run_in_executor()``. Each + ``asyncio`` task owns its own hold. Tasks share a read lock, and a task asking for the write lock waits until the + other holders release, with waiting writers ahead of new readers. Upgrading or downgrading a held lock raises + :class:`RuntimeError`. The underlying :class:`SoftReadWriteLock` handles forks, the heartbeat, and stale eviction. + Singleton wrappers share one sync lock and its task holds, and closing one ends only the holds taken through it. + + :param lock_file: path to the lock file; the protocol directory lives next to it as ``.rw`` + :param timeout: maximum wait time in seconds; ``-1`` means block indefinitely + :param blocking: if ``False``, raise :class:`~filelock.Timeout` immediately on contention + :param is_singleton: if ``True``, reuse existing :class:`SoftReadWriteLock` instances per resolved path + :param heartbeat_interval: seconds between heartbeat refreshes; default 30 s + :param stale_threshold: seconds a holder record may stay unchanged before a contender evicts it; defaults to + ``3 * heartbeat_interval`` + :param poll_interval: seconds between acquire retries under contention; default 0.25 s + :param on_compromise: called from the heartbeat thread with a :class:`~filelock.LeaseCompromise` when the hold is + lost + :param loop: event loop for ``run_in_executor``; ``None`` uses the running loop + :param executor: executor for ``run_in_executor``; ``None`` uses the default executor + + .. versionadded:: 3.27.0 + + """ + + def __init__( # ruff:ignore[too-many-arguments] # public constructor: one parameter per documented lock option + self, + lock_file: str | os.PathLike[str], + timeout: float = -1, + *, + blocking: bool = True, + is_singleton: bool = True, + heartbeat_interval: float = 30.0, + stale_threshold: float | None = None, + poll_interval: float = 0.25, + on_compromise: Callable[[LeaseCompromise], None] | None = None, + loop: asyncio.AbstractEventLoop | None = None, + executor: futures.Executor | None = None, + ) -> None: + self._creator_pid = os.getpid() + self._closed = False + self._owns_backend = not is_singleton + #: Marks the task holds taken through this wrapper, which its close() ends. + self._via: Final = object() + self._lock = SoftReadWriteLock( + lock_file, + timeout, + blocking=blocking, + is_singleton=is_singleton, + heartbeat_interval=heartbeat_interval, + stale_threshold=stale_threshold, + poll_interval=poll_interval, + on_compromise=on_compromise, + ) + self._owners = _task_owners_for(self._lock) + self._loop = loop + self._executor = executor + + @property + def lock_file(self) -> str: + """The path to the lock file passed to the constructor.""" + return self._lock.lock_file + + @property + def timeout(self) -> float: + """The default timeout applied when ``acquire_read`` / ``acquire_write`` is called without one.""" + return self._lock.timeout + + @property + def blocking(self) -> bool: + """Whether ``acquire_*`` defaults to blocking; ``False`` makes contention raise immediately.""" + return self._lock.blocking + + @property + def generation(self) -> int | None: + """The generation the current hold was granted at, a fencing token; ``None`` when no lock is held.""" + return self._lock.generation + + @property + def compromise(self) -> LeaseCompromise | None: + """How the current hold was lost, or ``None`` while it stands; acquiring raises until the hold is released.""" + return self._lock.compromise + + @property + def loop(self) -> asyncio.AbstractEventLoop | None: + """The event loop used for ``run_in_executor``, or ``None`` for the running loop.""" + return self._loop + + @property + def executor(self) -> futures.Executor | None: + """The executor used for ``run_in_executor``, or ``None`` for the default executor.""" + return self._executor + + @asynccontextmanager + async def read_lock(self, timeout: float | None = None, *, blocking: bool | None = None) -> AsyncGenerator[None]: + """ + Async context manager that acquires and releases a shared read lock. + + :param timeout: maximum wait time in seconds, or ``None`` to use the instance default + :param blocking: if ``False``, raise :class:`~filelock.Timeout` immediately; ``None`` uses the instance default + + :raises RuntimeError: if the calling task already holds the write lock + :raises Timeout: if the lock cannot be acquired within *timeout* seconds + :raises TypeError: if *timeout* is a :class:`bool` or not a real number + :raises ValueError: if a blocking call gets a ``nan`` *timeout* or a negative one other than ``-1`` + + """ + await self.acquire_read(timeout, blocking=blocking) + try: + yield + finally: + await self.release() + + @asynccontextmanager + async def write_lock(self, timeout: float | None = None, *, blocking: bool | None = None) -> AsyncGenerator[None]: + """ + Async context manager that acquires and releases an exclusive write lock. + + :param timeout: maximum wait time in seconds, or ``None`` to use the instance default + :param blocking: if ``False``, raise :class:`~filelock.Timeout` immediately; ``None`` uses the instance default + + :raises RuntimeError: if the calling task already holds the read lock + :raises Timeout: if the lock cannot be acquired within *timeout* seconds + :raises TypeError: if *timeout* is a :class:`bool` or not a real number + :raises ValueError: if a blocking call gets a ``nan`` *timeout* or a negative one other than ``-1`` + + """ + await self.acquire_write(timeout, blocking=blocking) + try: + yield + finally: + await self.release() + + async def acquire_read( + self, timeout: float | None = None, *, blocking: bool | None = None + ) -> AsyncAcquireSoftReadWriteReturnProxy: + """ + Acquire a shared read lock. + + See :meth:`SoftReadWriteLock.acquire_read` for reentrancy / upgrade / fork semantics. The blocking work runs + inside ``run_in_executor`` so other coroutines on the same loop keep progressing while this call waits. + + :param timeout: maximum wait time in seconds, or ``None`` to use the instance default + :param blocking: if ``False``, raise :class:`~filelock.Timeout` immediately; ``None`` uses the instance default + + :returns: a proxy usable as an async context manager to release the lock + + :raises RuntimeError: if the calling task already holds the write lock, if this instance was invalidated by + :func:`os.fork`, or if :meth:`close` was called + :raises Timeout: if the lock cannot be acquired within *timeout* seconds + :raises TypeError: if *timeout* is a :class:`bool` or not a real number + :raises ValueError: if a blocking call gets a ``nan`` *timeout* or a negative one other than ``-1`` + + """ + self._raise_if_unusable() + await self._acquire("read", timeout, blocking=blocking) + return AsyncAcquireSoftReadWriteReturnProxy(lock=self) + + async def acquire_write( + self, timeout: float | None = None, *, blocking: bool | None = None + ) -> AsyncAcquireSoftReadWriteReturnProxy: + """ + Acquire an exclusive write lock. + + See :meth:`SoftReadWriteLock.acquire_write` for the writer-preferring semantics. The blocking work runs inside + ``run_in_executor``. + + :param timeout: maximum wait time in seconds, or ``None`` to use the instance default + :param blocking: if ``False``, raise :class:`~filelock.Timeout` immediately; ``None`` uses the instance default + + :returns: a proxy usable as an async context manager to release the lock + + :raises RuntimeError: if the calling task already holds the read lock, if this instance was invalidated by + :func:`os.fork`, or if :meth:`close` was called + :raises Timeout: if the lock cannot be acquired within *timeout* seconds + :raises TypeError: if *timeout* is a :class:`bool` or not a real number + :raises ValueError: if a blocking call gets a ``nan`` *timeout* or a negative one other than ``-1`` + + """ + self._raise_if_unusable() + await self._acquire("write", timeout, blocking=blocking) + return AsyncAcquireSoftReadWriteReturnProxy(lock=self) + + async def _acquire(self, mode: Literal["read", "write"], timeout: float | None, *, blocking: bool | None) -> None: + blocking = self._lock.blocking if blocking is None else blocking + + async def enter(remaining: float) -> None: + # A close() may have landed while this task waited for the table. + self._raise_if_unusable() + await self._run_acquire( + functools.partial( + self._lock._acquire_level_of, # ruff:ignore[private-member-access] # the wrapper owns this backend hold + _TASKS_THREAD_ID, + mode, + remaining, + blocking=blocking, + ) + ) + + def joinable() -> bool: + self._raise_if_unusable() + if (compromise := self._lock.compromise) is not None: + raise _lost_hold_error(self.lock_file, mode, compromise) + return self._backend_holds() + + await self._owners.acquire( + mode, + timeout=self._lock.timeout if timeout is None else timeout, + blocking=blocking, + lock_file=self.lock_file, + via=self._via, + enter=enter, + joinable=joinable, + ) + + async def release(self, *, force: bool = False) -> None: + """ + Release one level of the calling task's hold. + + The wrapper releases the lock once no task holds it. + + :param force: if ``True``, end every level the calling task took through this wrapper. Levels taken through + other wrappers on the same path stay. A wrapper that holds no level for the task releases nothing. + + :raises RuntimeError: if the calling task holds no level through this wrapper and *force* is ``False`` + + """ + if self._creator_pid == os.getpid(): + await self._owners.release( + force=force, + lock_file=self.lock_file, + via=self._via, + leave=functools.partial(self._submit, self._leave_backend), + still_held=self._backend_holds, + ) + + async def close(self) -> None: + """ + End the holds of the tasks that acquired through this instance and release the lock once no task holds it. + + Idempotent. After calling this method the instance can no longer acquire locks. With ``is_singleton=False`` it + also closes the underlying :class:`SoftReadWriteLock`. A singleton wrapper leaves the shared one open, so tasks + that acquired through another wrapper and synchronous users of the same path keep their holds. The next acquire + notices a synchronous ``close()`` or forced release of that shared lock. One that lands in the few bytecodes + between that check and the admission still admits the task into the ended hold. + + """ + if self._creator_pid != os.getpid() or self._closed: + return + # Refuse acquires from here on, so none is admitted into a hold the backend is letting go of. + self._closed = True + try: + await self._owners.retire( + None if self._owns_backend else self._via, + leave=functools.partial(self._submit, self._lock.close if self._owns_backend else self._leave_backend), + still_held=self._backend_holds, + ) + except BaseException as error: + # Only a failed backend call can leave the level held, and then a retried close() has to find it. + if not isinstance(error, asyncio.CancelledError) and self._backend_holds(): + self._closed = False + raise + + def _raise_if_unusable(self) -> None: + if self._creator_pid != os.getpid(): # pragma: forked child + msg = f"AsyncSoftReadWriteLock on {self.lock_file} was inherited across fork; construct a new instance" + raise RuntimeError(msg) + if self._closed: + msg = f"AsyncSoftReadWriteLock on {self.lock_file} has been closed" + raise RuntimeError(msg) + + def _backend_holds(self) -> bool: + # A sync close() or forced release on a shared singleton ends the tasks' level without the table taking part. + return self._lock._holds_level_of(_TASKS_THREAD_ID) # ruff:ignore[private-member-access] # the wrapper owns this backend hold + + def _leave_backend(self) -> None: + # Only the tasks' level: a sync user of the same singleton keeps its own. + self._lock._release_level_of(_TASKS_THREAD_ID) # ruff:ignore[private-member-access] # the wrapper owns this backend hold + + async def _run_acquire(self, acquire: Callable[..., AcquireReturnProxy]) -> None: + # run_in_executor cannot recall work the pool already started. A sync acquire that won the lock before it saw + # the cancellation has published its claim and started the heartbeat, which keeps the record fresh so no peer + # evicts it. Wait the call out and hand the claim back, as AsyncReadWriteLock does. + abandoned = threading.Event() + acquire_future = self._submit(acquire, cancel_check=abandoned.is_set) + try: + await _wait_until_done(acquire_future) + except asyncio.CancelledError as cancellation: + # Stops a backend wait on another process's hold, which would otherwise outlast the caller by as long. + abandoned.set() + try: + await _drain_future(acquire_future) + except Timeout: + # Nothing was taken, so the caller's cancellation is the whole outcome. + raise cancellation from None + except BaseException as error: # ruff:ignore[blind-except] # reported with the cancellation below + _raise_cancelled_error(cancellation, error) + try: + await _drain_future(self._submit(self._leave_backend)) + except BaseException as error: # ruff:ignore[blind-except] # reported with the cancellation below + _raise_cancelled_error(cancellation, error) + raise + _future_result(acquire_future) + + def _submit( + self, func: Callable[_P, _R], *args: _P.args, **kwargs: _P.kwargs + ) -> asyncio.Future[_BackendOutcome[_R]]: + loop = self._loop or asyncio.get_running_loop() + return loop.run_in_executor(self._executor, _capture_call, functools.partial(func, *args, **kwargs)) + + +class AsyncAcquireSoftReadWriteReturnProxy: + """Async context-aware object that releases an :class:`AsyncSoftReadWriteLock` on exit.""" + + def __init__(self, lock: AsyncSoftReadWriteLock) -> None: + self.lock = lock + + async def __aenter__(self) -> AsyncSoftReadWriteLock: + return self.lock + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + await self.lock.release() + + +__all__ = [ + "AsyncAcquireSoftReadWriteReturnProxy", + "AsyncSoftReadWriteLock", +] diff --git a/hug/lib/python3.12/site-packages/filelock/_soft_rw/_protocol.py b/hug/lib/python3.12/site-packages/filelock/_soft_rw/_protocol.py new file mode 100644 index 0000000000000000000000000000000000000000..565f9bcf7994c0808aa2d6d02a239cd96055b50d --- /dev/null +++ b/hug/lib/python3.12/site-packages/filelock/_soft_rw/_protocol.py @@ -0,0 +1,638 @@ +""" +The generation-log protocol behind :class:`~filelock.SoftReadWriteLock`. + +The lock's whole state is a sequence of immutable snapshots, ``gen/``, each naming the writer and the readers that +hold the lock at generation ``N``. Every transition is one atomic publication of the next snapshot: a participant reads +the latest generation, derives the successor it wants, and links a fully written temporary file to ``gen/``. The +link fails if a peer got there first, so the participant re-reads and tries again. No step is ever half done: a crash +leaves at most an unlinked temporary file, never a state that another participant must repair before it can proceed. + +Liveness rests on a nonce, not a clock. Each participant keeps ``holders/`` and rewrites its nonce every +heartbeat. A contender records the bytes it read there and the moment it read them on its own monotonic clock; a +member whose bytes have not changed for ``stale_threshold`` is evicted by committing a snapshot without it. Two hosts +never compare clocks, so a skewed file server or a suspended virtual machine cannot make a live holder look dead. + +The generation number is monotonic and unique to one transition within one incarnation of the log, so the generation at +which a hold was granted fences it: a resource that rejects anything carrying a lower generation than the highest it has +accepted refuses a holder that paused past its expiry and resumed. A random epoch file names the incarnation, so a +participant that outlives a removal of the log reads the new one instead of continuing the old numbering into it. +""" + +from __future__ import annotations + +__lazy_modules__ = ["contextlib", "filelock._error", "filelock._identity", "math", "pathlib", "secrets"] + +import os +import secrets +from contextlib import suppress +from dataclasses import dataclass +from math import isfinite +from pathlib import Path +from typing import TYPE_CHECKING, Final, Literal, Protocol + +from filelock._error import SoftFileLockProtocolError +from filelock._identity import host_name + +if TYPE_CHECKING: + from collections.abc import Callable + +Mode = Literal["read", "write"] +HeartbeatOutcome = Literal["ok", "lost", "transient"] + +_PROTOCOL: Final[str] = "filelock-rw/1" +_GENERATION_DIGITS: Final[int] = 20 +_TOKEN_HEX_LENGTH: Final[int] = 32 +#: A snapshot line is ``reader=`` plus a token, so this bounds a hostile record without capping real reader counts. +_MAX_RECORD_SIZE: Final[int] = 1 << 20 +#: Snapshots older than this many generations behind the latest are removed by whoever commits. The window is also how +#: far ``latest`` probes past a gap before it trusts that nothing newer exists; ``gen/HEAD``, not the window, is what +#: keeps a listing served further behind from passing an old generation off as the head. +_RETAINED_GENERATIONS: Final[int] = 64 +_GENERATIONS_DIRECTORY: Final[str] = "gen" +#: A random token created once per log: removing ``gen/`` removes it, so a participant can tell a log that was wiped and +#: restarted from the one it remembers. +_EPOCH_NAME: Final[str] = "epoch" +#: The epoch plus a copy of the newest snapshot, renamed into place after each commit. NFS revalidates a file opened by +#: name but may serve a listing from cache, so this is the head a stale client cannot miss; it may lag, never lead. +_HEAD_NAME: Final[str] = "HEAD" +_HOLDERS_DIRECTORY: Final[str] = "holders" +_TEMPORARY_PREFIX: Final[str] = ".commit-" + + +class TreeReplacedError(RuntimeError): + """ + Raised by a ``Files`` implementation when a protocol directory is not a real directory (a symlink, a junction, a + file) or belongs to another account. + """ + + +class Files(Protocol): + """The filesystem operations the protocol runs on; :mod:`._storage` implements them over :mod:`os`.""" + + def read(self, path: str) -> bytes | None: + """The file's bytes, ``None`` when it does not exist, or ``b""`` when it is not a regular file.""" + + def create(self, path: str, data: bytes, *, durable: bool = False) -> tuple[int, int]: + """ + Create the file exclusively with the whole record written; raise ``FileExistsError`` when it exists. + + The returned device and inode (the kernel's identity for a file, independent of its name) let ``overwrite`` + check that the name still leads to this file. + + *durable* flushes it to stable storage first, for a record that outlives the host that wrote it: a generation a + crash left empty would wedge every other participant. A holder record dies with its writer, so it skips that. + """ + + def link(self, source: str, target: str) -> bool: + """Hard-link *source* to *target* without replacing; ``True`` when *target* names *source*'s file afterwards.""" + + def replace(self, source: str, target: str) -> bool: + """Rename *source* over *target* atomically; ``False`` when the platform refused because *target* is in use.""" + + def overwrite(self, path: str, data: bytes, identity: tuple[int, int]) -> bool: + """ + Rewrite from offset zero the file ``create`` returned *identity* for. + + ``False`` when *path* no longer leads to that file. + """ + + def unlink(self, path: str) -> None: + """Remove the file, tolerating one that is already gone.""" + + def listdir(self, path: str) -> list[str]: + """The names in the directory, or an empty list when it does not exist.""" + + def prepare(self, root: str, *, force: bool = False) -> None: + """ + Create the protocol directories under *root*, refusing anything at those paths that is not a directory. + + Done once per lock; *force* repeats it after a write found a directory missing. + """ + + +@dataclass(frozen=True) +class Snapshot: + """The lock's state at one generation: which token writes, which tokens read.""" + + generation: int + writer: str | None + readers: frozenset[str] + + @property + def members(self) -> frozenset[str]: + """Every token this generation names.""" + return self.readers if self.writer is None else self.readers | {self.writer} + + def encode(self) -> bytes: + lines = [_PROTOCOL, f"generation={self.generation}"] + if self.writer is not None: + lines.append(f"writer={self.writer}") + lines.extend(f"reader={token}" for token in sorted(self.readers)) + return "".join(f"{line}\n" for line in lines).encode("ascii") + + +def parse_snapshot(data: bytes) -> Snapshot | None: + """The snapshot a ``gen/`` record encodes, or ``None`` when the record is malformed.""" + if not data or len(data) > _MAX_RECORD_SIZE: + return None + try: + lines = data.decode("ascii").split("\n") + except UnicodeDecodeError: + return None + if lines[0] != _PROTOCOL or lines[-1]: + return None + generation: int | None = None + writer: str | None = None + readers: set[str] = set() + for line in lines[1:-1]: + key, _, value = line.partition("=") + if ( + key == "generation" + and generation is None + and len(value) <= _GENERATION_DIGITS + and value.isdigit() + and str(int(value)) == value + ): + generation = int(value) + elif key == "writer" and writer is None and is_token(value): + writer = value + elif key == "reader" and is_token(value) and value not in readers: + readers.add(value) + else: + return None + return None if generation is None else Snapshot(generation=generation, writer=writer, readers=frozenset(readers)) + + +def is_token(value: str) -> bool: + """Whether *value* is a claim token: exactly 32 lowercase hex digits.""" + return len(value) == _TOKEN_HEX_LENGTH and all(character in "0123456789abcdef" for character in value) + + +def new_token() -> str: + """A fresh claim token.""" + return secrets.token_hex(_TOKEN_HEX_LENGTH // 2) + + +def encode_holder(token: str, nonce: str, lease: float) -> bytes: + """ + The ``holders/`` record; only the nonce changes between heartbeats, so the length stays constant. + + *lease* is the writer's own ``stale_threshold``: a peer configured tighter must not read this holder's longer + heartbeat interval as death, so evictors wait the longer of their threshold and this one. + """ + return ( + f"{_PROTOCOL}\ntoken={token}\npid={os.getpid()}\nhost={host_name()}\nlease={float(lease)!r}\nnonce={nonce}\n" + ).encode("ascii") + + +def holder_lease(record: bytes | None) -> float | None: + """The ``lease`` a holder record carries, or ``None`` when it has none (an older release) or it is malformed.""" + try: + text = next( + line for line in (record or b"").decode("ascii").split("\n") if line.startswith("lease=") + ).removeprefix("lease=") + lease = float(text) + except (StopIteration, ValueError): # UnicodeDecodeError is a ValueError + return None + # Only the exact spelling repr() writes: float() alone also takes "1_000", " 1", and non-ASCII digits. + return lease if isfinite(lease) and lease > 0 and repr(lease) == text else None + + +class Ledger: + """ + How long each observed value has stayed the same, measured on this observer's monotonic clock. + + A value that has not changed for ``stale_threshold`` belongs to a participant that stopped refreshing it. The clock + is the observer's own and only ever measures the gap between two of its own observations, which is what lets two + hosts agree on liveness without agreeing on the time. + """ + + def __init__(self, clock: Callable[[], float]) -> None: + self._clock = clock + self._seen: dict[str, tuple[bytes | None, float]] = {} + + def observe(self, key: str, value: bytes | None, *, asked: float | None = None) -> float: + """ + Record *value* for *key* and return for how long it has stayed the same. + + *asked* is the clock reading taken before the caller fetched *value*. An unchanged value counts as unchanged up + to *asked*, so a pause between the fetch and this call adds no time. + """ + now = self._clock() + if (seen := self._seen.get(key)) is None or seen[0] != value: + self._seen[key] = (value, now) + return 0.0 + return (now if asked is None else asked) - seen[1] + + def forget(self, key: str) -> None: + self._seen.pop(key, None) + + def retain(self, keys: set[str]) -> None: + """Drop every key not in *keys*: peers that left would otherwise stay in the ledger for the lock's lifetime.""" + self._seen = {key: seen for key, seen in self._seen.items() if key in keys} + + +class GenerationLog: + """The ordered snapshots under ``gen/``, found by listing and probing, advanced by linking the next one.""" + + def __init__(self, files: Files, lock_file: str, root: str) -> None: + self._files = files + self._lock_file = lock_file + self._directory = Path(root, _GENERATIONS_DIRECTORY) + self._epoch_path = str(self._directory / _EPOCH_NAME) + self._head_path = str(self._directory / _HEAD_NAME) + self._epoch: bytes | None = None + self._latest: Snapshot | None = None + + def latest(self) -> Snapshot: + """ + The newest snapshot. + + Starts from the newest readable generation among those ``gen/HEAD``, the listing, or memory name, then probes + forward by name across the retained window. ``HEAD`` only names a generation, and that generation must still + read back: a committer stalled before its rename can leave ``HEAD`` on a generation compacted since. A + generation compacted between the listing and the read, and a hole left by a committer that died before it + compacted, are covered by the probe. Memory and ``HEAD`` are bound to the log's epoch, so a log wiped and + restarted is never continued from a generation it no longer holds. Only a log that names generations, now or as + remembered, of which none can be read is refused, because that is a client so far behind the log that nothing + it reads can be trusted. + """ + # When everything the first pass named is gone, peers compacted past it while this client looked, or the log was + # replaced; one more pass settles whether this client cannot see the head at all. + if (latest := self._find()) is None and (latest := self._find()) is None: + raise SoftFileLockProtocolError(self._lock_file, None, "generation log names no readable snapshot") + self._latest = latest + return latest + + def _find(self) -> Snapshot | None: + if (epoch := self._incarnation()) != self._epoch: + self._epoch, self._latest = epoch, None + named = set(self._list()) + # Generation 0 is the empty log, which has no record to read back. + if self._latest is not None and self._latest.generation: + named.add(self._latest.generation) + if head := self._head_generation(): + named.add(head) + for generation in sorted(named, reverse=True): + if (start := self._read(generation)) is not None: + # Compaction keeps the surviving generations contiguous, so the first missing name past a readable HEAD + # generation is the end; probing the whole window on every poll cost 64 failed opens. + return self._probe(start, patience=1 if generation == head else _RETAINED_GENERATIONS) + newest = max(named, default=0) + found = self._probe(Snapshot(generation=newest, writer=None, readers=frozenset())) + return found if found.generation > newest or not named else None + + def _head_generation(self) -> int | None: + # HEAD is never flushed, so a crash can leave it unreadable; readers verify what it names anyway, so a torn one + # only sends them to the window probe. + if (record := self._head_record()) is None or (snapshot := parse_snapshot(record)) is None: + return None + return snapshot.generation + + def _head_record(self) -> bytes | None: + if (data := self._files.read(self._head_path)) is None: + return None + # A HEAD from another epoch landed after its log was removed; a log whose epoch was never written runs without. + epoch, newline, record = data.partition(b"\n") + return record if self._valid_epoch() == epoch + newline else None + + def _valid_epoch(self) -> bytes | None: + epoch = self._epoch + return epoch if epoch is not None and is_token(epoch.decode("ascii", "replace").removesuffix("\n")) else None + + def _incarnation(self) -> bytes | None: + if (epoch := self._files.read(self._epoch_path)) is None: + # Exclusive creation, not a link: reading the log must keep working where os.link does not exist. A peer + # that reads the file before its token is written takes the empty value for the epoch, and its next commit + # sees the change and re-reads, so a partial read costs one retry. + try: + self._files.create(self._epoch_path, f"{new_token()}\n".encode("ascii"), durable=True) + except FileExistsError: + pass + except FileNotFoundError: + # No gen/ yet: there is no log to tell apart, and the first participant's prepare() creates it. + return None + epoch = self._files.read(self._epoch_path) + return epoch + + def _list(self) -> list[int]: + listed = self._listed() + for generation in listed[:-_RETAINED_GENERATIONS]: + self._files.unlink(self._path(generation)) + return listed[-_RETAINED_GENERATIONS:] + + def _listed(self) -> list[int]: + # isdigit() alone admits Unicode digits: superscripts int() rejects, fullwidth ones it parses to another number. + return sorted( + int(name) + for name in self._files.listdir(str(self._directory)) + if len(name) == _GENERATION_DIGITS and name.isascii() and name.isdigit() + ) + + def _probe(self, start: Snapshot, patience: int = _RETAINED_GENERATIONS) -> Snapshot: + latest = start + generation = start.generation + gap = 0 + while gap < patience: + generation += 1 + if (snapshot := self._read(generation)) is None: + gap += 1 + continue + latest = snapshot + gap = 0 + return latest + + def _read(self, generation: int) -> Snapshot | None: + if (data := self._files.read(self._path(generation))) is None: + return None + if (snapshot := parse_snapshot(data)) is None or snapshot.generation != generation: + name = self._name(generation) + raise SoftFileLockProtocolError(self._lock_file, name, "malformed generation record") + return snapshot + + def commit(self, successor: Snapshot) -> bool: + """ + Publish *successor* as the next generation; ``False`` when a peer published that generation first. + + The record is complete before it gets its public name, and a no-replace hard link is the compare-and-swap: only + one link to ``gen/`` can ever succeed. The link's return code is not trusted, because an NFS retry can + report a link that did land as failed; the file's identity after the call is what decides. A log whose epoch + changed since the snapshot was read is a different log, so the commit is refused as lost and the caller + re-reads; that narrows a wipe racing this commit to the moment between the epoch check and the link. A slot + compaction freed is refused the same way, before the link and again after it, since a committer can stall in + between. After the link a surviving predecessor proves little: it may be a record another stalled committer + linked into its own freed slot and has yet to remove. A slot comes free once the log is a whole window past it, + and the head stays, so a record the listing or ``HEAD`` leads by a window is refused as well. A record refused + after it landed is removed when nothing was built on it. One that peers lead by less than a window stays, or its + slot would come free early. + """ + if self._files.read(self._epoch_path) != self._epoch or not self._extends_the_log(successor.generation): + return False + if not self._publish( + self._path(successor.generation), data := successor.encode(), self._files.link, durable=True + ): + return False + lead = max([*self._listed(), self._head_generation() or 0]) - successor.generation + if lead >= _RETAINED_GENERATIONS or not self._extends_the_log(successor.generation): + if not 0 < lead < _RETAINED_GENERATIONS: + self._files.unlink(self._path(successor.generation)) + return False + self._latest = successor + # Readers verify the generation HEAD names, so a failed rename costs a probe, not the commit. A late one would + # move HEAD back past later commits, so HEAD only ever moves forward. + with suppress(OSError): + if ( + (epoch := self._valid_epoch()) is not None + and self._files.read(self._epoch_path) == epoch + and (self._head_generation() or 0) < successor.generation + ): + self._publish(self._head_path, epoch + data, self._files.replace, durable=False) + if (expired := successor.generation - _RETAINED_GENERATIONS - 1) > 0: + self._compact_through(expired) + return True + + def _extends_the_log(self, generation: int) -> bool: + # Compaction removes oldest first, so a missing predecessor means compaction freed this slot. The first + # generation has none; a HEAD of this epoch means the log already moved past it. + if generation > 1: + return self._files.read(self._path(generation - 1)) is not None + return self._head_record() is None + + def _publish(self, target: str, data: bytes, place: Callable[[str, str], bool], *, durable: bool) -> bool: + temporary = str(self._directory / f"{_TEMPORARY_PREFIX}{new_token()}") + try: + self._files.create(temporary, data, durable=durable) + except FileNotFoundError: + # No gen/: the log was removed, or nobody has prepared it yet. Either way there is nothing to publish into + # until a participant's prepare() creates it, and the caller re-reads whatever log exists by then. + return False + try: + return place(temporary, target) + finally: + self._files.unlink(temporary) + + def _compact_through(self, expired: int) -> None: + # Oldest first, so survivors stay contiguous: a stalled committer's leftover would otherwise sit below a hole. + oldest = expired + while oldest > 1 and self._files.read(self._path(oldest - 1)) is not None: + oldest -= 1 + for generation in range(oldest, expired + 1): + self._files.unlink(self._path(generation)) + + def _path(self, generation: int) -> str: + return str(self._directory / self._name(generation)) + + @staticmethod + def _name(generation: int) -> str: + return f"{generation:0{_GENERATION_DIGITS}d}" + + +class Participant: + """ + One acquisition, from publishing a claim through holding the lock to leaving. + + ``advance`` is polled until it reports the lock granted, ``heartbeat`` runs on the holder's schedule, and ``leave`` + commits the participant out. None of them sleep: the caller owns the deadline and the poll interval. The ledger and + the log outlive one acquisition: how long a peer's record has stayed unchanged is knowledge every attempt against + the same lock shares, or a caller whose attempts are each shorter than the stale threshold could never evict. + """ + + def __init__( # ruff:ignore[too-many-arguments] # one value per protocol input; the lock builds this once per acquisition + self, + files: Files, + root: str, + mode: Mode, + *, + stale_threshold: float, + clock: Callable[[], float], + ledger: Ledger, + log: GenerationLog, + ) -> None: + self.token: Final[str] = new_token() + self.mode: Final[Mode] = mode + self.generation: int | None = None + self._files = files + self._root = root + self._stale_threshold = stale_threshold + self._clock = clock + self._ledger = ledger + self._log = log + self._holder = self._holder_path(self.token) + self._holder_identity: tuple[int, int] | None = None + self._entered = False + self._next_sweep = clock() + + def publish(self) -> None: + """Create the holder record peers watch for liveness; it exists before the token appears in any snapshot.""" + self._files.prepare(self._root) + self._create_holder() + + def _create_holder(self) -> None: + try: + self._holder_identity = self._files.create(self._holder, self._record()) + except FileNotFoundError: + # Removing the whole log takes the directories with it. + self._files.prepare(self._root, force=True) + self._holder_identity = self._files.create(self._holder, self._record()) + + def _record(self) -> bytes: + return encode_holder(self.token, new_token(), self._stale_threshold) + + def advance(self) -> bool: + """ + Take the next step toward holding the lock; ``True`` once the lock is granted. + + A reader enters as soon as no live writer is named. A writer enters as soon as no live writer is named, which + blocks every new reader, then waits for the named readers to leave. Stale members are evicted in the same commit + that admits this participant, and a writer's final admission is itself a commit, so no admission can rest on a + snapshot a peer is about to supersede. + """ + # A lost race is not a reason to wait: re-read and try again before handing the poll interval back to the + # caller. + for _ in range(4): + latest = self._log.latest() + self._keep_claim_fresh(latest) + self._sweep(latest) + stale = frozenset(token for token in latest.members - {self.token} if self._is_stale(token)) + writer = None if latest.writer in stale else latest.writer + readers = latest.readers - stale + if not self._entered: + if writer is not None: + # A live writer blocks entry in both modes; only an eviction is worth committing. + if stale: + self._commit(latest, writer, readers, stale) + return False + writer, readers = (self.token, readers) if self.mode == "write" else (None, readers | {self.token}) + granted = self.mode == "read" or not readers + if self._entered and not stale and not granted: + return False + if (generation := self._commit(latest, writer, readers, stale)) is None: + continue + self._entered = True + if granted: + self.generation = generation + return granted + return False + + def _keep_claim_fresh(self, latest: Snapshot) -> None: + # The snapshot is the truth about membership: a commit the filesystem reported lost may have landed, and a peer + # may have evicted this participant while it waited to drain, so both directions are taken from it. A wait can + # outlast the stale threshold, so the nonce changes on every poll; a peer then never reads a patient contender + # as a corpse. A record that has gone missing was taken by an evictor or a sweeper that did, or by a removal of + # the whole log. + self._entered = self.token in latest.members + if not self._refresh_nonce(): + with suppress(FileExistsError): + self._create_holder() + + def _sweep(self, latest: Snapshot) -> None: + # A participant that crashed before its first commit leaves a record no snapshot names, and a commit that + # crashed between creating and unlinking its temporary file leaves that too. Neither blocks anyone, so they are + # collected on the same unchanged-for-a-threshold rule as a member, and only every half threshold at most. + if (now := self._clock()) < self._next_sweep: + return + self._next_sweep = now + self._stale_threshold / 2 + watched = set(latest.members) + holders = Path(self._root, _HOLDERS_DIRECTORY) + for name in self._files.listdir(str(holders)): + if name != self.token and name not in latest.members and is_token(name): + watched.add(key := f"holder:{name}") + self._collect(key, str(holders / name)) + generations = Path(self._root, _GENERATIONS_DIRECTORY) + for name in self._files.listdir(str(generations)): + if name.startswith(_TEMPORARY_PREFIX): + watched.add(key := f"temporary:{name}") + self._collect(key, str(generations / name)) + self._ledger.retain(watched) + + def _collect(self, key: str, path: str) -> None: + if self._unchanged_for_its_lease(key, path): + self._files.unlink(path) + self._ledger.forget(key) + + def _commit( + self, latest: Snapshot, writer: str | None, readers: frozenset[str], stale: frozenset[str] + ) -> int | None: + successor = Snapshot(generation=latest.generation + 1, writer=writer, readers=readers) + if not self._log.commit(successor): + return None + for token in stale: + self._files.unlink(self._holder_path(token)) + self._ledger.forget(token) + return successor.generation + + def _is_stale(self, token: str) -> bool: + return self._unchanged_for_its_lease(token, self._holder_path(token)) + + def _unchanged_for_its_lease(self, key: str, path: str) -> bool: + asked = self._clock() + record = self._files.read(path) + return self._ledger.observe(key, record, asked=asked) >= max(self._stale_threshold, holder_lease(record) or 0.0) + + def _holder_path(self, token: str) -> str: + return str(Path(self._root, _HOLDERS_DIRECTORY, token)) + + def _refresh_nonce(self) -> bool: + if (identity := self._holder_identity) is None: + return False + return self._files.overwrite(self._holder, self._record(), identity) + + def heartbeat(self) -> tuple[HeartbeatOutcome, OSError | None]: + """ + Refresh the nonce and confirm the latest snapshot still names this participant. + + ``lost`` means a peer evicted this participant or removed its holder record: the lock is no longer held, and the + holder must stop using what it protects. ``transient`` is a filesystem error refreshing the nonce, worth + retrying on the next tick. A failure to read the log or to sweep decides nothing about this holder's liveness, + so it is not counted against it. + """ + try: + if not self._refresh_nonce(): + return "lost", None + except TreeReplacedError: + return "lost", None + except OSError as error: + return "transient", error + try: + latest = self._log.latest() + self._sweep(latest) + except (OSError, TreeReplacedError): + return "ok", None + return ("ok" if self.token in latest.members else "lost"), None + + def leave(self) -> None: + """Commit this participant out of the latest snapshot, then remove its holder record.""" + # The record goes even when the log cannot be read: a contender that never entered then leaves nothing behind, + # and a member still named ages out the same way with or without one. + # A tree that was swapped out leaves no record this participant can reach, so there is nothing to commit out of. + with suppress(TreeReplacedError): + try: + while self.token in (latest := self._log.latest()).members: + successor = Snapshot( + generation=latest.generation + 1, + writer=None if latest.writer == self.token else latest.writer, + readers=latest.readers - {self.token}, + ) + if self._log.commit(successor): + break + finally: + self._entered = False + self.generation = None + self._files.unlink(self._holder) + + +__all__ = [ + "Files", + "GenerationLog", + "HeartbeatOutcome", + "Ledger", + "Mode", + "Participant", + "Snapshot", + "TreeReplacedError", + "encode_holder", + "holder_lease", + "is_token", + "new_token", + "parse_snapshot", +] diff --git a/hug/lib/python3.12/site-packages/filelock/_soft_rw/_storage.py b/hug/lib/python3.12/site-packages/filelock/_soft_rw/_storage.py new file mode 100644 index 0000000000000000000000000000000000000000..a6ff6c64efab66b9879c35b714ce22a97e1c4cb5 --- /dev/null +++ b/hug/lib/python3.12/site-packages/filelock/_soft_rw/_storage.py @@ -0,0 +1,278 @@ +"""The :class:`~._protocol.Files` implementation over :mod:`os`.""" + +from __future__ import annotations + +__lazy_modules__ = ["filelock._soft_rw._protocol", "filelock._strict", "filelock._util", "pathlib"] + +import errno +import os +import stat +import sys +import time +from contextlib import contextmanager, suppress +from errno import ELOOP, ENOENT, ENOTDIR, ENXIO, ESTALE +from pathlib import Path +from typing import TYPE_CHECKING, Final + +from filelock._soft_rw._protocol import ( + _GENERATIONS_DIRECTORY, + _HOLDERS_DIRECTORY, + _MAX_RECORD_SIZE, + TreeReplacedError, +) +from filelock._strict import _link_no_follow, _pin_directories, _raise_if_hard_links_unsupported +from filelock._util import ensure_directory_exists, foreign_directory_owner, write_all + +if TYPE_CHECKING: + from collections.abc import Generator + +_O_NOFOLLOW: Final[int] = getattr(os, "O_NOFOLLOW", 0) +#: Probed once at import, as :mod:`filelock._strict` does, because a per-call check flips when a test mocks the +#: function. The set lists os.rename, whose implementation os.replace shares. +_SUPPORTS_DIR_FD: Final[bool] = ( + hasattr(os, "link") + and {os.link, os.open, os.stat, os.mkdir, os.rename, os.unlink} <= os.supports_dir_fd + and os.scandir in os.supports_fd +) +#: Windows opens descriptors in text mode by default, which rewrites newlines; every record here is exact bytes. +_O_BINARY: Final[int] = getattr(os, "O_BINARY", 0) +#: ESTALE is what an NFS client reports for a file a peer unlinked out from under its cached handle: gone, not broken. +_MISSING_ERRNOS: Final[frozenset[int]] = frozenset({ENOENT, ESTALE}) +#: Windows refuses an open with EACCES while another process is deleting the file or holds it open without sharing; +#: both clear within moments, so an open is retried this long before the refusal is taken as real. +_WINDOWS_OPEN_GRACE: Final[float] = 0.5 + + +class OsFiles: + """Record files under a lock's protocol root, opened without following symlinks and owned by this UID alone.""" + + def __init__(self, lock_file: str) -> None: + self._lock_file = lock_file + self._prepared = False + self._identities: dict[str, tuple[int, int]] = {} + + def read(self, path: str) -> bytes | None: + # O_NONBLOCK keeps an open of a FIFO planted at the path from blocking; the regular-file check below rejects it. + if (fd := self._open(path, os.O_RDONLY | _O_NOFOLLOW | getattr(os, "O_NONBLOCK", 0) | _O_BINARY)) is None: + return None + try: + if not stat.S_ISREG(os.fstat(fd).st_mode): + return b"" + chunks: list[bytes] = [] + remaining = _MAX_RECORD_SIZE + 1 + while remaining > 0 and (chunk := os.read(fd, remaining)): + chunks.append(chunk) + remaining -= len(chunk) + return b"".join(chunks) + finally: + os.close(fd) + + def create(self, path: str, data: bytes, *, durable: bool = False) -> tuple[int, int]: + with self._relative(path) as (name, dir_fd): + fd = os.open(name, os.O_WRONLY | os.O_CREAT | os.O_EXCL | _O_NOFOLLOW | _O_BINARY, 0o600, dir_fd=dir_fd) + try: + write_all(fd, data) + if durable: + os.fsync(fd) + created = os.fstat(fd) + except BaseException: + os.close(fd) + # The name is unique to this call, so removing it cannot take a record from anyone else. + self.unlink(path) + raise + os.close(fd) + return created.st_dev, created.st_ino + + def link(self, source: str, target: str) -> bool: + failure: NotImplementedError | OSError | None = None + with self._relative(source) as (source_name, source_fd), self._relative(target) as (target_name, target_fd): + try: + _link_no_follow(source_name, target_name, src_dir_fd=source_fd, dst_dir_fd=target_fd) + except FileExistsError: + pass + except (NotImplementedError, OSError) as error: + _raise_if_hard_links_unsupported(self._lock_file, error) + failure = error + # The file's identity decides, not the call's result: an NFS retransmit can report a link that landed as + # failed, and an EEXIST can be this very link answered twice. Any other error on a link that did not land is + # a fault, not a peer winning the race, and a retry would spin. + if (identity := _identity(source_name, source_fd)) is not None and identity == _identity( + target_name, target_fd + ): + return True + if failure is not None: + raise failure + return False + + def replace(self, source: str, target: str) -> bool: + try: + with self._relative(source) as (source_name, source_fd), self._relative(target) as (target_name, target_fd): + os.replace(source_name, target_name, src_dir_fd=source_fd, dst_dir_fd=target_fd) + except PermissionError: + # Windows refuses to rename over a file a reader holds open; callers tolerate a target one rename behind. + return False + return True + + def overwrite(self, path: str, data: bytes, identity: tuple[int, int]) -> bool: + # O_NONBLOCK keeps an open of a FIFO (a named pipe) at the path from blocking, as in read. With no reader the + # open fails with ENXIO. + try: + fd = self._open(path, os.O_WRONLY | _O_NOFOLLOW | getattr(os, "O_NONBLOCK", 0) | _O_BINARY) + except OSError as error: + if error.errno != ENXIO: + raise + return False # pragma: needs fifo + if fd is None: + return False + try: + # O_NOFOLLOW refuses a symlink but not a hard link (a second name for the same file) or another file renamed + # over the name, and a write through either lands in a file that is not the record. The protocol does not + # link holder records, so a second name marks a file that inherited a removed record's inode number. + opened = os.fstat(fd) + if not stat.S_ISREG(opened.st_mode) or (opened.st_dev, opened.st_ino) != identity or opened.st_nlink != 1: + return False + write_all(fd, data) + finally: + os.close(fd) + return True + + def unlink(self, path: str) -> None: + try: + with self._relative(path) as (name, dir_fd): + os.unlink(name, dir_fd=dir_fd) + except OSError as error: + # Windows refuses to remove a file another process holds open; the record then outlives its owner until + # that process exits, which only delays the cleanup a later participant repeats. + if error.errno not in _MISSING_ERRNOS and not isinstance(error, PermissionError): + raise + + def listdir(self, path: str) -> list[str]: + try: + with self._directory(path) as dir_fd, os.scandir(path if dir_fd is None else dir_fd) as entries: + return [entry.name for entry in entries] + except FileNotFoundError: + return [] + + def prepare(self, root: str, *, force: bool = False) -> None: + if self._prepared and not force: + return + if force: + self._identities.clear() + ensure_directory_exists(root) + for directory, depth in ( + (Path(root), 0), + (Path(root, _GENERATIONS_DIRECTORY), 1), + (Path(root, _HOLDERS_DIRECTORY), 1), + ): + with self._relative(str(directory), depth) as (name, dir_fd): + with suppress(FileExistsError): + os.mkdir(name, 0o700, dir_fd=dir_fd) + # mkdir has no O_NOFOLLOW, so check what the name resolves to before the lock creates anything in it. + found = os.stat(name, dir_fd=dir_fd, follow_symlinks=False) + parent = directory.parent.stat() if dir_fd is None else os.fstat(dir_fd) + if ( + not stat.S_ISDIR(found.st_mode) + or getattr(found, "st_file_attributes", 0) & stat.FILE_ATTRIBUTE_REPARSE_POINT + ): + msg = f"{directory} exists but is not a directory or is a symlink; refusing to use it" + raise TreeReplacedError(msg) + owner = foreign_directory_owner(found, parent) + if owner is not None: # pragma: needs foreign-sticky-tmp + msg = f"{directory} belongs to uid {owner}, not to this process's account or the parent's owner" + raise TreeReplacedError(msg) + self._identities[str(directory)] = found.st_dev, found.st_ino + self._prepared = True + + def _open(self, path: str, flags: int) -> int | None: + # Missing is the one answer a caller acts on without raising. A refused open is real on POSIX, where nothing + # this lock does makes its own records unreadable; on Windows it is usually another process mid-delete or + # holding the file without sharing, so it is retried through a short grace before it counts, and a file still + # refusing after that has been in delete-pending for longer than any deletion takes, so it is on its way out: + # missing. + deadline: float | None = None + while True: + if isinstance(opened := self._attempt_open(path, flags), int): + return opened + if opened.errno in _MISSING_ERRNOS: + return None + if not isinstance(opened, PermissionError) or sys.platform != "win32": + raise opened + if deadline is None: # pragma: win32 cover + deadline = time.monotonic() + _WINDOWS_OPEN_GRACE + elif time.monotonic() >= deadline: # pragma: win32 cover + return None + time.sleep(0.002) # pragma: win32 cover + + def _attempt_open(self, path: str, flags: int) -> int | OSError: + try: + with self._relative(path) as (name, dir_fd): + return os.open(name, flags, dir_fd=dir_fd) + except OSError as error: + return error + + @contextmanager + def _relative(self, path: str, depth: int = 2) -> Generator[tuple[str, int | None]]: + directory, name = os.path.split(path) + with self._directory(directory, depth) as dir_fd: + yield (path if dir_fd is None else name), dir_fd + + @contextmanager + def _directory(self, path: str, depth: int = 2) -> Generator[int | None]: + # O_NOFOLLOW applies to the last path component. The check prepare() made on and /gen holds until a + # process that can write the lock's directory renames the tree aside and puts one with a symlinked gen in its + # place. Each call therefore opens the root and then the record's directory without following symlinks and + # works inside what it opened. No descriptor outlives the call, so a later call finds a log removed and started + # again by name, as before. None means descriptors cannot anchor a call and the call uses the whole path. + if depth and _SUPPORTS_DIR_FD: # pragma: needs link-dir-fd + with self._relative(path, depth - 1) as (name, parent_fd): + try: + fd = os.open(name, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | _O_NOFOLLOW, dir_fd=parent_fd) + except OSError as error: + # NetBSD reports EFTYPE rather than ELOOP when O_NOFOLLOW refuses a symlink. + if error.errno not in {ELOOP, ENOTDIR, getattr(errno, "EFTYPE", ELOOP)}: + raise + msg = f"{path} is not a directory or is a symlink; refusing to use it" + raise TreeReplacedError(msg) from error + try: + self._admit(fd, path, parent_fd) + except BaseException: + os.close(fd) + raise + try: + yield fd + finally: + os.close(fd) + else: + # Without dir_fd, hold each of those directories open instead, so no process can rename or replace one. + try: + pins = _pin_directories(*[Path(path), *Path(path).parents][:depth][::-1]) + except NotADirectoryError as error: # pragma: win32 cover + msg = f"{error.filename} is not a directory or is a symlink; refusing to use it" + raise TreeReplacedError(msg) from error + with pins: + yield None + + def _admit(self, fd: int, path: str, parent_fd: int | None) -> None: # pragma: needs link-dir-fd + # A directory other than the one prepare() checked may belong to another account: the tree can be renamed aside + # and recreated while this object lives, and the ownership check has to hold for what is opened now. + found = os.fstat(fd) + if (found.st_dev, found.st_ino) == self._identities.get(path): + return + parent = os.fstat(parent_fd) if parent_fd is not None else Path(path).parent.stat() + if (owner := foreign_directory_owner(found, parent)) is not None: # pragma: needs foreign-sticky-tmp + msg = f"{path} belongs to uid {owner}, not to this process's account or the parent's owner" + raise TreeReplacedError(msg) + self._identities[path] = found.st_dev, found.st_ino + + +def _identity(path: str, dir_fd: int | None) -> tuple[int, int] | None: + try: + st = os.stat(path, dir_fd=dir_fd, follow_symlinks=False) + except OSError: + return None + return st.st_dev, st.st_ino + + +__all__ = [ + "OsFiles", +] diff --git a/hug/lib/python3.12/site-packages/filelock/_soft_rw/_sync.py b/hug/lib/python3.12/site-packages/filelock/_soft_rw/_sync.py new file mode 100644 index 0000000000000000000000000000000000000000..aa2e891516cb38f9027cfd840a4db758abde3390 --- /dev/null +++ b/hug/lib/python3.12/site-packages/filelock/_soft_rw/_sync.py @@ -0,0 +1,793 @@ +"""Cross-process and cross-host reader/writer lock over a generation log of immutable snapshots.""" + +from __future__ import annotations + +__lazy_modules__ = [ + "filelock._error", + "filelock._lease", + "filelock._soft_rw._protocol", + "filelock._soft_rw._storage", + "math", +] + +import atexit +import os +import threading +import time +from contextlib import contextmanager, suppress +from dataclasses import dataclass +from math import isfinite +from typing import TYPE_CHECKING, ClassVar, Final +from weakref import WeakValueDictionary + +from filelock._api import ( + AcquireReturnProxy, + _acquire_thread_lock, + _canonical, + _check_timeout_max, + _ensure_current_process, + _register_fork_class, + _register_fork_object, + _resolve_read_write_timeout, + _seconds, + _timeout_behavior, +) +from filelock._error import Timeout +from filelock._lease import LeaseCompromise +from filelock._soft_rw._protocol import GenerationLog, Ledger, Mode, Participant +from filelock._soft_rw._storage import OsFiles + +if TYPE_CHECKING: + from collections.abc import Callable, Generator + + from filelock._lease import CompromiseReason + +_ALL_INSTANCES: Final[WeakValueDictionary[int, SoftReadWriteLock]] = WeakValueDictionary() +_ALL_INSTANCES_LOCK: threading.Lock = threading.Lock() + + +class _SoftRWMeta(type): + _instances: WeakValueDictionary[str, SoftReadWriteLock] + _instances_lock: threading.RLock + _instances_under_construction: set[str] + + def __call__( # ruff:ignore[too-many-arguments] # forwards the public constructor's documented parameters + cls, + lock_file: str | os.PathLike[str], + timeout: float = -1, + *, + blocking: bool = True, + is_singleton: bool = True, + heartbeat_interval: float = 30.0, + stale_threshold: float | None = None, + poll_interval: float = 0.25, + on_compromise: Callable[[LeaseCompromise], None] | None = None, + ) -> SoftReadWriteLock: + _ensure_current_process() + timeout = _seconds("timeout", timeout) + # Passed through only when set, so a subclass that declares its own constructor without it keeps working. + extra = {} if on_compromise is None else {"on_compromise": on_compromise} + if not is_singleton: + return super().__call__( + lock_file, + timeout, + blocking=blocking, + is_singleton=is_singleton, + heartbeat_interval=heartbeat_interval, + stale_threshold=stale_threshold, + poll_interval=poll_interval, + **extra, + ) + + # Keyed like the log root, so one instance never spans two logs. + normalized = _canonical(lock_file) + with cls._instances_lock: + instance = cls._instances.get(normalized) + if instance is None: + if normalized in cls._instances_under_construction: + msg = f"Singleton lock construction is already active for {lock_file!s}" + raise RuntimeError(msg) + construction_pid = os.getpid() + construction_registry = cls._instances_under_construction + construction_registry.add(normalized) + try: + instance = super().__call__( + lock_file, + timeout, + blocking=blocking, + is_singleton=is_singleton, + heartbeat_interval=heartbeat_interval, + stale_threshold=stale_threshold, + poll_interval=poll_interval, + **extra, + ) + finally: + construction_registry.discard(normalized) + if os.getpid() != construction_pid: # pragma: needs fork + msg = "Lock construction cannot continue after fork; construct a new lock in the child" + raise RuntimeError(msg) + cls._instances[normalized] = instance + elif ( + _timeout_behavior(instance.timeout, any_negative=False) + != _timeout_behavior(timeout, any_negative=False) + or instance.blocking != blocking + ): + msg = ( + f"Singleton lock created with timeout={instance.timeout}, blocking={instance.blocking}," + f" cannot be changed to timeout={timeout}, blocking={blocking}" + ) + raise ValueError(msg) + elif on_compromise is not None and instance._on_compromise != on_compromise: # ruff: ignore[private-member-access] # the metaclass owns the singleton it compares + # A tuning difference keeps the first caller's values; a different loss callback is a different safety + # contract, and returning the cached instance would silently drop it. + msg = f"Singleton lock created with a different on_compromise callback for {lock_file!s}" + raise ValueError(msg) + else: + _validate_intervals(heartbeat_interval, stale_threshold, poll_interval) + return instance + + +class SoftReadWriteLock(metaclass=_SoftRWMeta): + """ + Cross-process and cross-host reader/writer lock for shared filesystems. + + Use this class instead of :class:`~filelock.ReadWriteLock` when the lock file lives on a network filesystem (NFS, + Lustre, HPC cluster shared storage). ``ReadWriteLock`` is backed by SQLite and cannot run on NFS because SQLite's + ``fcntl`` locking is unreliable there. + + The lock's state is a log of immutable snapshots under ``foo.lock.rw/gen/``, each naming the writer and the + readers holding the lock at generation ``N``. Every acquire, release, and eviction publishes the next snapshot with + one atomic no-replace hard link, so no transition can be left half done by a crash on any host. Each participant + also keeps ``foo.lock.rw/holders/``, a record whose nonce a daemon heartbeat thread rewrites every + ``heartbeat_interval`` seconds. A contender evicts a member whose record has not changed for ``stale_threshold`` + seconds of the contender's own monotonic clock; no clock is ever compared across hosts, so skew between hosts or + against the file server cannot make a live holder look dead. + + Writer acquire is writer-preferring: a writer enters the snapshot as soon as no live writer is named, which blocks + any new reader, then waits for the named readers to leave. Writer starvation is impossible. + + An expired holder is not fenced by the lock: a process paused past ``stale_threshold`` resumes believing it holds + the lock until its next heartbeat reports the loss through ``on_compromise``. :attr:`generation` is a monotonic + fencing token for the protected resource: a store that rejects writes carrying a lower generation than the highest + it has accepted refuses such a holder. + + Reentrancy, upgrade/downgrade rules, thread pinning, and singleton caching by canonical path match + :class:`~filelock.ReadWriteLock`. + + Forking invalidates the inherited instance in the child so the child cannot double-own the lock with its parent; + ``release()`` on that instance is a no-op, and the child must construct a new instance if it needs a lock. + + Trust boundary: protects against same-UID non-cooperating processes (one host or cross-host) and same-host + different-UID users via ``0o600`` / ``0o700`` permissions. Does not protect against root compromise or multi-tenant + mounts where hostile co-tenants share the UID. + + :param lock_file: path to the lock file; the protocol directory lives next to it as ``.rw`` + :param timeout: maximum wait time in seconds; ``-1`` means block indefinitely + :param blocking: if ``False``, raise :class:`~filelock.Timeout` immediately on contention + :param is_singleton: if ``True``, reuse existing instances for the same canonical path + :param heartbeat_interval: seconds between heartbeat refreshes; default 30 s + :param stale_threshold: seconds a holder record may stay unchanged before a contender evicts it; defaults to + ``3 * heartbeat_interval``, matching etcd's ``LeaseKeepAlive`` convention + :param poll_interval: seconds between acquire retries under contention; default 0.25 s + :param on_compromise: called from the heartbeat thread with a :class:`~filelock.LeaseCompromise` when the hold is + lost: a peer evicted it, or refreshes failed for long enough that a peer could have + + .. versionadded:: 3.27.0 + + .. versionchanged:: 3.33.0 + + The on-disk protocol is a generation log; earlier releases cannot share a lock path with this one. + + """ + + _instances: WeakValueDictionary[str, SoftReadWriteLock] = WeakValueDictionary() + _instances_lock = threading.RLock() + _instances_under_construction: ClassVar[set[str]] = set() + + def __init_subclass__(cls, **kwargs: object) -> None: + """Give each lock subclass its own singleton registry and lock.""" + super().__init_subclass__(**kwargs) + cls._instances = WeakValueDictionary() + cls._instances_lock = threading.RLock() + cls._instances_under_construction = set() + _register_fork_class(cls) + + def __init__( # ruff:ignore[too-many-arguments] # public constructor: one parameter per documented lock option + self, + lock_file: str | os.PathLike[str], + timeout: float = -1, + *, + blocking: bool = True, + is_singleton: bool = True, # ruff:ignore[unused-method-argument] # consumed by _SoftRWMeta.__call__ + heartbeat_interval: float = 30.0, + stale_threshold: float | None = None, + poll_interval: float = 0.25, + on_compromise: Callable[[LeaseCompromise], None] | None = None, + ) -> None: + self._creator_pid = os.getpid() + heartbeat_interval, stale_threshold, poll_interval = _validate_intervals( + heartbeat_interval, stale_threshold, poll_interval + ) + + self.lock_file: str = os.fspath(lock_file) + self.timeout: float = timeout + self.blocking: bool = blocking + self.heartbeat_interval: float = heartbeat_interval + self.stale_threshold: float = stale_threshold + self.poll_interval: float = poll_interval + + # Resolved once: a relative lock path must keep naming the same log after the process changes directory. The + # final component stays unresolved, as in 4.0.12, so every release shares one log on a symlinked lock path. + self._root = f"{_canonical(self.lock_file)}.rw" + self._files = OsFiles(self.lock_file) + self._ledger = Ledger(time.monotonic) + self._log = GenerationLog(self._files, self.lock_file, self._root) + self._on_compromise = on_compromise + self._locks = _Locks(internal=threading.Lock(), transaction=threading.Lock()) + self._hold: _Hold | None = None + self._compromise: LeaseCompromise | None = None + self._closed: bool = False + + with _ALL_INSTANCES_LOCK: + _ALL_INSTANCES[id(self)] = self + _register_fork_object(self) + + @classmethod + def _reset_class_after_fork(cls) -> None: # pragma: forked child + global _ALL_INSTANCES_LOCK # ruff:ignore[global-statement] # rebinds the module lock to a fresh one in the fork child + _ALL_INSTANCES_LOCK = threading.Lock() + cls._instances = WeakValueDictionary() + cls._instances_lock = threading.RLock() + cls._instances_under_construction = set() + + @property + def generation(self) -> int | None: + """ + The generation at which the current hold was granted, or ``None`` when no lock is held. + + Generations are monotonic across all participants and hosts, so this is a fencing token: pass it to the + protected resource and have the resource reject any operation carrying a lower generation than the highest it + has accepted. That refuses a holder that paused past ``stale_threshold``, was evicted, and resumed. Removing the + ``.rw`` directory starts a new log whose generations begin again at 1. + + .. versionadded:: 3.33.0 + + """ + with self._locks.internal: + return None if self._hold is None else self._hold.participant.generation + + @property + def compromise(self) -> LeaseCompromise | None: + """ + How the current hold was lost, or ``None`` while it stands. + + Set by the heartbeat thread once a peer has evicted this holder or refreshes have failed for long enough that a + peer could have. The holder should stop using the protected resource when it is set. Until the lost hold is + released, acquiring on this instance raises :class:`RuntimeError` instead of joining it. + + .. versionadded:: 3.33.0 + + """ + with self._locks.internal: + return self._compromise + + @contextmanager + def read_lock(self, timeout: float | None = None, *, blocking: bool | None = None) -> Generator[None]: + """ + Context manager that acquires and releases a shared read lock. + + Falls back to instance defaults for *timeout* and *blocking* when ``None``. + + :param timeout: maximum wait time in seconds, or ``None`` to use the instance default + :param blocking: if ``False``, raise :class:`~filelock.Timeout` immediately; ``None`` uses the instance default + + :raises RuntimeError: if a write lock is already held on this instance + :raises Timeout: if the lock cannot be acquired within *timeout* seconds + :raises TypeError: if *timeout* is a :class:`bool` or not a real number + :raises ValueError: if a blocking call gets a ``nan`` *timeout* or a negative one other than ``-1`` + + """ + self.acquire_read(timeout, blocking=blocking) + try: + yield + finally: + self.release() + + @contextmanager + def write_lock(self, timeout: float | None = None, *, blocking: bool | None = None) -> Generator[None]: + """ + Context manager that acquires and releases an exclusive write lock. + + Falls back to instance defaults for *timeout* and *blocking* when ``None``. + + :param timeout: maximum wait time in seconds, or ``None`` to use the instance default + :param blocking: if ``False``, raise :class:`~filelock.Timeout` immediately; ``None`` uses the instance default + + :raises RuntimeError: if a read lock is already held, or a write lock is held by a different thread + :raises Timeout: if the lock cannot be acquired within *timeout* seconds + :raises TypeError: if *timeout* is a :class:`bool` or not a real number + :raises ValueError: if a blocking call gets a ``nan`` *timeout* or a negative one other than ``-1`` + + """ + self.acquire_write(timeout, blocking=blocking) + try: + yield + finally: + self.release() + + def acquire_read(self, timeout: float | None = None, *, blocking: bool | None = None) -> AcquireReturnProxy: + """ + Acquire a shared read lock. + + If this instance already holds a read lock, the lock level is incremented (reentrant). Attempting to acquire a + read lock while holding a write lock raises :class:`RuntimeError` (downgrade not allowed). On the 0→1 + transition the reader publishes its holder record, enters the next snapshot once no live writer is named, and + starts a daemon heartbeat thread that rewrites the record's nonce every ``heartbeat_interval`` seconds so peers + on other hosts do not evict it. + + :param timeout: maximum wait time in seconds, or ``None`` to use the instance default; ``-1`` means block + indefinitely + :param blocking: if ``False``, raise :class:`~filelock.Timeout` immediately when the lock is unavailable; + ``None`` uses the instance default + + :returns: a proxy that can be used as a context manager to release the lock + + :raises RuntimeError: if a write lock is already held on this instance, if this instance was invalidated by + :func:`os.fork`, or if :meth:`close` was called + :raises Timeout: if the lock cannot be acquired within *timeout* seconds + :raises TypeError: if *timeout* is a :class:`bool` or not a real number + :raises ValueError: if a blocking call gets a ``nan`` *timeout* or a negative one other than ``-1`` + :raises SoftFileLockProtocolError: if a snapshot cannot be read without risking overlap, or the filesystem + refuses the no-replace hard links the protocol commits with + + """ + return self._acquire("read", timeout, blocking=blocking) + + def acquire_write(self, timeout: float | None = None, *, blocking: bool | None = None) -> AcquireReturnProxy: + """ + Acquire an exclusive write lock. + + If this instance already holds a write lock from the same thread, the lock level is incremented (reentrant). + Attempting to acquire a write lock while holding a read lock raises :class:`RuntimeError` (upgrade not + allowed). Write locks are pinned to the acquiring thread: a different thread trying to re-enter also raises + :class:`RuntimeError`. + + A writer enters the next snapshot as its writer as soon as no live writer is named, which blocks every new + reader on every host, then waits for the readers that snapshot names to leave. Writer starvation is impossible: + new readers see the named writer and wait behind it. + + :param timeout: maximum wait time in seconds, or ``None`` to use the instance default; ``-1`` means block + indefinitely + :param blocking: if ``False``, raise :class:`~filelock.Timeout` immediately when the lock is unavailable; + ``None`` uses the instance default + + :returns: a proxy that can be used as a context manager to release the lock + + :raises RuntimeError: if a read lock is already held, if a write lock is held by a different thread, if this + instance was invalidated by :func:`os.fork`, or if :meth:`close` was called + :raises Timeout: if the lock cannot be acquired within *timeout* seconds + :raises TypeError: if *timeout* is a :class:`bool` or not a real number + :raises ValueError: if a blocking call gets a ``nan`` *timeout* or a negative one other than ``-1`` + :raises SoftFileLockProtocolError: if a snapshot cannot be read without risking overlap, or the filesystem + refuses the no-replace hard links the protocol commits with + + """ + return self._acquire("write", timeout, blocking=blocking) + + @classmethod + def get_lock( + cls, + lock_file: str | os.PathLike[str], + timeout: float = -1, + *, + blocking: bool = True, + ) -> SoftReadWriteLock: + """ + Return the singleton :class:`SoftReadWriteLock` for *lock_file*. + + :param lock_file: path to the lock file; the protocol directory lives next to it as ``.rw`` + :param timeout: maximum wait time in seconds; ``-1`` means block indefinitely + :param blocking: if ``False``, raise :class:`~filelock.Timeout` immediately when the lock is unavailable + + :returns: the singleton lock instance + + :raises ValueError: if an instance already exists for this path with different *timeout* or *blocking* values + + """ + return cls(lock_file, timeout, blocking=blocking) + + def close(self) -> None: + """ + Release any held lock and mark the instance closed. + + Idempotent. After calling this method the instance can no longer acquire locks — subsequent acquires raise + :class:`RuntimeError`, and so does an acquire another thread is still waiting in, which gives up its claim + instead of taking the lock. A fork-invalidated instance is closed without raising. + """ + if self._creator_pid != os.getpid(): # pragma: forked child + return + # Closed first, under the lock an acquire publishes its hold under: an acquire still waiting then sees the flag + # and leaves instead of publishing, and one that published before it is released below. + with self._locks.internal: + self._closed = True + self.release(force=True) + + def release(self, *, force: bool = False) -> None: + """ + Release one level of the current lock. + + When the lock level reaches zero the heartbeat thread is stopped, the participant is committed out of the next + snapshot, and its holder record is removed. On a fork-invalidated instance (that is, the child of a + :func:`os.fork` call made while the parent held a lock) this method is a no-op so inherited ``with`` blocks can + unwind cleanly in the child. + + :param force: if ``True``, release the lock completely regardless of the current lock level + + :raises RuntimeError: if no lock is currently held and *force* is ``False``, or if the calling thread holds no + level of the current lock and *force* is ``False`` + + """ + if self._creator_pid != os.getpid(): # pragma: forked child + return + self._release(threading.get_ident(), force=force) + + def _acquire_level_of( + self, holder: int, mode: Mode, timeout: float, *, blocking: bool, cancel_check: Callable[[], bool] + ) -> AcquireReturnProxy: + # Records the level under *holder* instead of the calling thread: an async wrapper's executor thread does not + # identify the tasks it holds for. Gives up with Timeout once *cancel_check* is true. + return self._acquire(mode, timeout, blocking=blocking, holder=holder, cancel_check=cancel_check) + + def _release_level_of(self, holder: int) -> None: + self._release(holder, force=False) + + def _holds_level_of(self, holder: int) -> bool: + # Read without the internal lock: an async wrapper asks from its event loop, which must not wait out a release + # running on another thread. + return (hold := self._hold) is not None and (holder == hold.write_thread_id or holder in hold.read_depths) + + def _release(self, thread_id: int, *, force: bool) -> None: + with self._locks.internal: + hold = self._hold + if hold is None: + if force: + return + msg = f"Cannot release a lock on {self.lock_file} (lock id: {id(self)}) that is not held" + raise RuntimeError(msg) + if force: + hold.level = 0 + else: + self._raise_if_not_holder(hold, thread_id) + hold.leave_one_level(thread_id) + if hold.level > 0: + return + self._hold = None + + # Stop the heartbeat before leaving, so a late tick cannot re-read a snapshot this participant just left and + # report an eviction that was its own release. on_compromise runs on that thread and may be what called here, + # in which case there is nothing to join. + hold.heartbeat_stop.set() + try: + if hold.heartbeat_thread is not threading.current_thread(): + hold.heartbeat_thread.join(timeout=min(self.heartbeat_interval + 1.0, threading.TIMEOUT_MAX)) + finally: + # The hold is already gone from this instance, so a join that raises must not strand its record too. + hold.participant.leave() + + def _raise_if_not_holder(self, hold: _Hold, thread_id: int) -> None: + # The hold's own heartbeat thread may release it, since on_compromise runs there. + if thread_id == hold.heartbeat_thread.ident: + return + # Another thread's release would leave the generation log under the holder and let a peer in. + if hold.mode == "write" and thread_id != hold.write_thread_id: + msg = ( + f"Cannot release write lock on {self.lock_file} (lock id: {id(self)}) " + f"from thread {thread_id} while it is held by thread {hold.write_thread_id}" + ) + raise RuntimeError(msg) + # Readers share one participant, so a thread that holds no read level would end another's hold. + if hold.mode == "read" and thread_id not in hold.read_depths: + msg = ( + f"Cannot release read lock on {self.lock_file} (lock id: {id(self)}) " + f"from thread {thread_id}, which does not hold it" + ) + raise RuntimeError(msg) + + def _acquire( + self, + mode: Mode, + timeout: float | None, + *, + blocking: bool | None, + holder: int | None = None, + cancel_check: Callable[[], bool] | None = None, + ) -> AcquireReturnProxy: + if self._creator_pid != os.getpid(): # pragma: forked child + msg = f"SoftReadWriteLock on {self.lock_file} was invalidated by fork(); construct a new instance" + raise RuntimeError(msg) + timeout = self.timeout if timeout is None else timeout + blocking = self.blocking if blocking is None else blocking + holder = threading.get_ident() if holder is None else holder + + with self._locks.internal: + if self._closed: + raise self._closed_error() + timeout = _resolve_read_write_timeout(timeout, blocking=blocking) + if self._hold is not None: + return self._validate_reentrant(mode, holder) + + deadline = None if timeout == -1 else time.perf_counter() + timeout + if not _acquire_thread_lock( + self._locks.transaction, + blocking=blocking, + timeout=timeout, + poll_interval=self.poll_interval, + cancel_check=cancel_check, + ): + raise Timeout(self.lock_file) from None + try: + return self._do_acquire_inner(mode, deadline, holder, blocking=blocking, cancel_check=cancel_check) + finally: + self._locks.transaction.release() + + def _closed_error(self) -> RuntimeError: + return RuntimeError(f"SoftReadWriteLock on {self.lock_file} has been closed") + + def _do_acquire_inner( + self, + mode: Mode, + deadline: float | None, + holder: int, + *, + blocking: bool, + cancel_check: Callable[[], bool] | None, + ) -> AcquireReturnProxy: + with self._locks.internal: + if self._hold is not None: + return self._validate_reentrant(mode, holder) + participant = Participant( + self._files, + self._root, + mode, + stale_threshold=self.stale_threshold, + clock=time.monotonic, + ledger=self._ledger, + log=self._log, + ) + participant.publish() + try: + self._wait_for(participant.advance, deadline=deadline, blocking=blocking, cancel_check=cancel_check) + except BaseException: + # A contender that gives up must not stay named in the snapshot: a writer left there would block every + # reader until a peer waits out the stale threshold. Its own token is all leave() touches, so it cannot + # undo a peer's claim. + with suppress(OSError): + participant.leave() + raise + stop_event = threading.Event() + hold = _Hold( + level=1, + mode=mode, + write_thread_id=holder if mode == "write" else None, + read_depths={holder: 1} if mode == "read" else {}, + participant=participant, + heartbeat_thread=_HeartbeatThread( + refresh=lambda: self._refresh(participant, hold), + interval=self.heartbeat_interval, + stop_event=stop_event, + name=f"filelock-heartbeat-{id(self):x}", + ), + heartbeat_stop=stop_event, + last_refresh=time.monotonic(), + ) + # Publish the hold and start its heartbeat under one internal-lock section, so a concurrent release() never + # observes a hold whose thread has not started and joins it. If the OS refuses the thread, clear the hold and + # leave: left in place, a peer evicts the unrefreshed record and acquires while this instance still believes it + # holds the lock. A close() that landed while this acquire waited gets the same treatment: published, the hold + # would keep its heartbeat and its slot on an instance nobody can release through. + start_error: BaseException | None = None + with self._locks.internal: + if self._closed: + start_error = self._closed_error() + else: + self._hold = hold + self._compromise = None + try: + hold.heartbeat_thread.start() + except BaseException as error: # ruff:ignore[blind-except] # clear the slot below and re-raise + self._hold = None + start_error = error + if start_error is not None: + participant.leave() + raise start_error + return AcquireReturnProxy(lock=self) + + def _validate_reentrant(self, mode: Mode, holder: int) -> AcquireReturnProxy: + hold = self._hold + assert hold is not None # ruff:ignore[assert] # callers dispatch here only inside the self._hold is not None branch + if hold.mode != mode: + opposite = "write" if mode == "read" else "read" + direction = "downgrade" if mode == "read" else "upgrade" + msg = ( + f"Cannot acquire {mode} lock on {self.lock_file} (lock id: {id(self)}): " + f"already holding a {opposite} lock ({direction} not allowed)" + ) + raise RuntimeError(msg) + if mode == "write" and holder != hold.write_thread_id: + msg = ( + f"Cannot acquire write lock on {self.lock_file} (lock id: {id(self)}) " + f"from thread {holder} while it is held by thread {hold.write_thread_id}" + ) + raise RuntimeError(msg) + # Publishing a hold clears the loss recorded for the one before, so a loss seen here is this hold's. Its + # heartbeat has stopped and a peer may hold the lock, so a new level must not bypass the log. + if (compromise := self._compromise) is not None: + raise _lost_hold_error(self.lock_file, mode, compromise) + if mode == "read": + hold.read_depths[holder] = hold.read_depths.get(holder, 0) + 1 + hold.level += 1 + return AcquireReturnProxy(lock=self) + + def _wait_for( + self, + predicate: Callable[[], bool], + *, + deadline: float | None, + blocking: bool, + cancel_check: Callable[[], bool] | None, + ) -> None: + while not predicate(): + # The flag is read without the lock here only to stop waiting early; publishing the hold re-checks it. + if self._closed: + raise self._closed_error() + now = time.perf_counter() + if not blocking or (deadline is not None and now >= deadline): + raise Timeout(self.lock_file) + # An abandoned acquire gives up like a timed-out one, so the caller's rollback withdraws its claim. + if cancel_check is not None and cancel_check(): + raise Timeout(self.lock_file) + sleep_for = self.poll_interval + if deadline is not None: + sleep_for = min(sleep_for, max(deadline - now, 0.0)) + time.sleep(sleep_for) + + def _refresh(self, participant: Participant, hold: _Hold) -> bool: + # The loop ends at the first loss, so the holder hears about it once. A transient filesystem error (ESTALE / + # EIO on the NFS-style filesystems this lock targets) is not a loss: retry rather than report a false + # compromise. Report the record unrefreshable only once failures have run long enough that a peer could evict + # it before the next success would land, a margin before the record actually ages out. + try: + outcome, error = participant.heartbeat() + except Exception as failure: # ruff:ignore[blind-except] # the thread would end with the hold still published + self._report_compromise(hold, "refresh-failed", failure) + return False + if outcome == "ok": + hold.last_refresh = time.monotonic() + return True + if outcome == "lost": + self._report_compromise(hold, "evicted", None) + return False + if time.monotonic() - hold.last_refresh >= self.stale_threshold - self.heartbeat_interval: + self._report_compromise(hold, "refresh-failed", error) + return False + return True + + def _report_compromise(self, hold: _Hold, reason: CompromiseReason, error: Exception | None) -> None: + # A tick that outlived its release's join must not stamp the old hold's loss onto whatever was acquired since. + with self._locks.internal: + if self._hold is not hold: + return + compromise = LeaseCompromise( + lock_file=self.lock_file, token=hold.participant.token, reason=reason, error=error + ) + self._compromise = compromise + if self._on_compromise is not None: + self._on_compromise(compromise) + + def _reset_after_fork_in_child(self) -> None: # pragma: forked child + self._locks = _Locks(internal=threading.Lock(), transaction=threading.Lock()) + self._hold = None + + +class _HeartbeatThread(threading.Thread): + def __init__( + self, + refresh: Callable[[], bool], + interval: float, + stop_event: threading.Event, + name: str, + ) -> None: + super().__init__(name=name, daemon=True) + self._refresh = refresh + self._interval = interval + self._stop_event = stop_event + + def run(self) -> None: + # Set on every exit, an on_compromise that raises included, so nothing waits on a thread that is gone. + try: + while not self._stop_event.wait(self._interval): + if not self._refresh(): + return + finally: + self._stop_event.set() + + +def _lost_hold_error(lock_file: str, mode: Mode, compromise: LeaseCompromise) -> RuntimeError: + return RuntimeError( + f"Cannot acquire {mode} lock on {lock_file}: the current hold was lost ({compromise.reason}), " + "release it before acquiring again" + ) + + +def _validate_intervals( + heartbeat_interval: float, stale_threshold: float | None, poll_interval: float +) -> tuple[float, float, float]: + heartbeat_interval = _seconds("heartbeat_interval", heartbeat_interval) + if not isfinite(heartbeat_interval) or heartbeat_interval <= 0: + msg = f"heartbeat_interval must be positive and finite, got {heartbeat_interval}" + raise ValueError(msg) + # The heartbeat thread waits this long between refreshes, and Event.wait overflows past TIMEOUT_MAX. + _check_timeout_max("heartbeat_interval", heartbeat_interval) + stale_threshold = ( + heartbeat_interval * 3 if stale_threshold is None else _seconds("stale_threshold", stale_threshold) + ) + if not isfinite(stale_threshold) or stale_threshold <= heartbeat_interval: + msg = ( + f"stale_threshold must exceed heartbeat_interval ({heartbeat_interval}) " + f"and be finite, got {stale_threshold}" + ) + raise ValueError(msg) + if not isfinite(poll_interval := _seconds("poll_interval", poll_interval)) or poll_interval <= 0: + msg = f"poll_interval must be positive and finite, got {poll_interval}" + raise ValueError(msg) + _check_timeout_max("poll_interval", poll_interval) + # A waiting writer refreshes its record once per poll, so a poll slower than the threshold reads as dead. + if poll_interval >= stale_threshold: + msg = f"poll_interval must be below stale_threshold ({stale_threshold}), got {poll_interval}" + raise ValueError(msg) + return heartbeat_interval, stale_threshold, poll_interval + + +@dataclass +class _Locks: + internal: threading.Lock + transaction: threading.Lock + + +@dataclass +class _Hold: + """Everything that exists only while a lock is held; ``None`` when the instance has no lock.""" + + level: int + mode: Mode + write_thread_id: int | None + #: Read levels per thread, so a thread can give back only the levels it took. + read_depths: dict[int, int] + participant: Participant + heartbeat_thread: _HeartbeatThread + heartbeat_stop: threading.Event + last_refresh: float + + def leave_one_level(self, thread_id: int) -> None: + self.level -= 1 + if self.mode == "read": + # The heartbeat thread holds no level of its own, so its release gives back one of a reader's. + owner = thread_id if thread_id in self.read_depths else next(iter(self.read_depths)) + if depth := self.read_depths.pop(owner) - 1: + self.read_depths[owner] = depth + + +def _cleanup_all_instances() -> None: # pragma: no cover - runs from atexit at interpreter shutdown + for instance in list(_ALL_INSTANCES.values()): + with suppress(Exception): + instance.release(force=True) + + +atexit.register(_cleanup_all_instances) +_register_fork_class(SoftReadWriteLock) + + +__all__ = [ + "SoftReadWriteLock", + "_lost_hold_error", +] diff --git a/hug/lib/python3.12/site-packages/filelock/_strict.py b/hug/lib/python3.12/site-packages/filelock/_strict.py new file mode 100644 index 0000000000000000000000000000000000000000..14b4a4831a54b8ff1d8efacb7e6954faefdf55d8 --- /dev/null +++ b/hug/lib/python3.12/site-packages/filelock/_strict.py @@ -0,0 +1,1243 @@ +from __future__ import annotations + +__lazy_modules__ = [ + "contextlib", + "filelock._error", + "filelock._identity", + "filelock._marker", + "filelock._soft", + "filelock._util", + "functools", + "pathlib", + "secrets", + "tempfile", +] + +import contextlib +import errno +import functools +import os +import secrets +import stat +import sys +import tempfile +import time +from dataclasses import dataclass +from errno import EACCES, EEXIST, ENOENT, ENOSYS, EPERM, ESTALE, EXDEV +from pathlib import Path +from typing import TYPE_CHECKING, Final, Literal, cast + +from filelock._api import BaseFileLock, ThreadLocalFileContext, _canonical, _raise_cleanup_errors +from filelock._error import SoftFileLockProtocolError +from filelock._identity import host_name, owner_is_stale, process_start_token +from filelock._marker import parse_marker +from filelock._soft import MALFORMED_LOCK_AGE_THRESHOLD, _parse_lock_holder +from filelock._soft_protocol import STRICT_SOFT_SENTINEL_RECORD +from filelock._util import foreign_directory_owner, write_all + +if sys.platform == "win32": # pragma: win32 cover + from filelock._windows import _is_unlinked, _open_deletable_for_reading, _pin_directory, _unpin_directory + +if TYPE_CHECKING: + from collections.abc import Callable, Iterator + +StrictSoftFileClaimState = Literal["intent", "held"] +_OccupantState = Literal["sentinel", "held", "exited", "damaged", "unparsable"] + +_CLAIM_STATES: Final[frozenset[str]] = frozenset({"intent", "held"}) +_COORDINATION_SUFFIX: Final[str] = ".filelock" +_CLAIM_DIRECTORY_NAME: Final[str] = "claims" +_CLAIM_MAGIC: Final[str] = "filelock-strict-v1" +_CLAIM_RECORD_LIMIT: Final[int] = 1024 +_CLAIM_NAME_PART_COUNT: Final[int] = 3 +_TOKEN_HEX_LENGTH: Final[int] = 32 +_PRIVATE_RECORD_MARKER: Final[str] = ".private-v1-" +_PRIVATE_RECORD_SUFFIX: Final[str] = ".tmp" +_PRIVATE_RECORD_RANDOM_HEX_LENGTH: Final[int] = 32 +_PRIVATE_RECORD_GRACE: Final[float] = 2.0 +_UNLINK_MAX_RETRIES: Final[int] = 10 +#: How long a scan waits out a claim held in Windows' delete-pending state before treating it as unreadable. +_CLAIM_READ_GRACE: Final[float] = 0.5 +_CLAIM_READ_RETRY: Final[float] = 0.002 +#: Rides out a peer replacing the lock path mid-inspection; a foreign node or unreadable record outlasting it is damage. +_SENTINEL_READ_GRACE: Final[float] = 0.5 +_NODE_DESCRIPTIONS: Final[dict[int, str]] = {stat.S_IFDIR: "a directory", stat.S_IFLNK: "a symlink"} +#: Windows opens descriptors in text mode by default, which rewrites newlines and truncates a record at a control byte. +#: The claim and sentinel records are exact binary, so every record descriptor must be binary; POSIX ignores the flag. +_O_BINARY: Final[int] = getattr(os, "O_BINARY", 0) +_LEGACY_SENTINEL: Final[bytes] = STRICT_SOFT_SENTINEL_RECORD.encode() +_WINDOWS_HARD_LINK_UNSUPPORTED: Final[frozenset[int]] = frozenset({1, 17, 50}) + +# Termux/Android CPython ships without os.link (bionic long had only linkat), so the strict backend's whole hard-link +# mechanism is absent there. Probe once and gate every os.link reference on it, so importing filelock still works and +# only an actual StrictSoftFileLock acquire reports the missing capability. +_HAS_LINK: Final[bool] = hasattr(os, "link") + +# Probe dir_fd capability once at import. A per-call ``os.unlink in os.supports_dir_fd`` check flips to False the moment +# a test mocks os.unlink, silently diverting the code to a different branch than the one under test. +_OPEN_SUPPORTS_DIR_FD: Final[bool] = os.open in os.supports_dir_fd +_UNLINK_SUPPORTS_DIR_FD: Final[bool] = os.unlink in os.supports_dir_fd +_STAT_SUPPORTS_DIR_FD: Final[bool] = os.stat in os.supports_dir_fd +_LINK_SUPPORTS_DIR_FD: Final[bool] = _HAS_LINK and os.link in os.supports_dir_fd +_MKDIR_SUPPORTS_DIR_FD: Final[bool] = os.mkdir in os.supports_dir_fd +_SCANDIR_SUPPORTS_FD: Final[bool] = os.scandir in os.supports_fd + + +def _probe_link_follow_symlinks() -> bool: + # os.supports_follow_symlinks lists os.link on PyPy, but its linkat then rejects follow_symlinks=False with EINVAL, + # and Windows raises NotImplementedError for the option outright. Link a throwaway file for real so the answer + # reflects the runtime rather than its advertisement, and treat any failure as "not honored": the option only + # hardens a source this process created with O_EXCL, so skipping it is safe, and a real environment fault surfaces + # when the actual link runs. + if not _HAS_LINK: + return False + try: + with tempfile.TemporaryDirectory() as directory: + source = Path(directory, "probe-source") + source.touch() + os.link(source, Path(directory, "probe-link"), follow_symlinks=False) + except (OSError, NotImplementedError, ValueError): + return False + return True + + +_LINK_HONORS_FOLLOW_SYMLINKS: Final[bool] = _probe_link_follow_symlinks() + + +def _probe_hard_link_unsupported_errnos() -> frozenset[int]: + # GraalPy's errno omits ENOTSUP, so importing the name outright breaks every runtime that ships without it. ENOTSUP + # wins wherever it exists, leaving every runtime that names it unchanged. EOPNOTSUPP only stands in for the ones + # that do not, and it approximates rather than matches: the two codes agree on Linux but differ on macOS/BSD and on + # Windows. Where neither exists a runtime that cannot name "operation not supported" cannot raise it either, and + # ENOSYS/EXDEV still classify the link failures it can raise. + not_supported = getattr(errno, "ENOTSUP", getattr(errno, "EOPNOTSUPP", None)) + return frozenset({ENOSYS, EXDEV} if not_supported is None else {ENOSYS, EXDEV, not_supported}) + + +_HARD_LINK_UNSUPPORTED_ERRNOS: Final[frozenset[int]] = _probe_hard_link_unsupported_errnos() + + +class StrictSoftFileLock(BaseFileLock): + """Portable fail-closed lock based on immutable owner claims.""" + + _preserve_lock_file_supported: bool = True + _on_acquired_supported: bool = False + #: Age cannot clear a strict claim: expiring one on a clock is the overlap the fail-closed contract exists to rule + #: out, so only force_break() removes it. + _lifetime_supported: bool = False + _lifetime_unsupported_reason: str = "a strict claim is never broken by age, only by force_break()" + #: Contending processes each publish and rescan several files, so jitter their retries. Seconds. Only a + #: poll_interval below this cap jitters; the default equals it and keeps a fixed cadence. + _poll_backoff_cap: float = 0.05 + _occupant_seen: tuple[tuple[float, int, int], float] | None = None + #: Set by the finalizer, after which no retry of a failed cleanup can run. + _finalizing: bool = False + + def _acquire(self) -> None: + # Finish a doorway whose claim cleanup failed before publishing, so this owner never contends with leftovers. + self._discard_pending_doorways(exited_only=True) + lock_path = Path(self._lock_path) + coordination_directory = Path(f"{lock_path}{_COORDINATION_SUFFIX}") + claim_directory = coordination_directory / _CLAIM_DIRECTORY_NAME + # A process that can write these directories can remove claims from them, so they take the lock's mode plus + # search permission where the mode grants read. + directory_mode: Final = (mode := self._open_mode()) | (mode & 0o444) >> 2 + _ensure_protocol_directory(self.lock_file, coordination_directory, directory_mode) + _ensure_claim_directory(self.lock_file, claim_directory, directory_mode) + if ( + sentinel_fd := _open_or_create_sentinel( + self.lock_file, lock_path, self._open_mode(), self._occupant_unchanged_for + ) + ) is None: + return + try: + sentinel_identity = _file_identity(os.fstat(sentinel_fd)) + except BaseException as inspection_error: # preserve inspection and descriptor cleanup errors + try: + os.close(sentinel_fd) + except BaseException as close_error: # ruff:ignore[blind-except] # preserve inspection and descriptor cleanup errors + _raise_cleanup_errors("strict sentinel inspection cleanup failed", inspection_error, close_error) + raise + self._mark_descriptor_pending(sentinel_fd, sentinel_identity) + try: + if won := self._attempt_doorway(claim_directory): + self._mark_descriptor_owned(sentinel_fd, sentinel_identity) + except BaseException: + # Once recorded as held, the acquire's rollback releases the claim instead. + if not self.is_locked: + self._discard_doorway() + raise + if not won: + self._discard_doorway() + + def _discard_pending_doorways(self, *, exited_only: bool) -> None: + if self._context.owner_claim_paths and not self.is_locked: + self._discard_doorway() + if not isinstance(context := self._context, ThreadLocalFileContext): + return + # Only this registry reaches an exited thread's thread-local doorway; a live thread finishes its own. + own: Final = context.current + with self._transition_lock: + for pending, thread in self._thread_pending.copy().values(): + if exited_only and thread.is_alive(): + continue + context.current = pending + try: + self._discard_doorway() + finally: + context.current = own + + def _occupant_unchanged_for(self, occupant: os.stat_result) -> float: + # SoftFileLock's marker rule: the mtime is only tested for change, timed on this process's monotonic clock. + key, now = (occupant.st_mtime, occupant.st_ino, occupant.st_size), time.monotonic() + if (seen := self._occupant_seen) is None or seen[0] != key: + self._occupant_seen = key, now + return 0.0 + return now - seen[1] + + def _attempt_doorway(self, claim_directory: Path) -> bool: + if _read_existing_claims(self.lock_file, claim_directory): + return False + + token = secrets.token_hex(_TOKEN_HEX_LENGTH // 2) + intent_name = _claim_name("intent", token) + intent_path = str(claim_directory / intent_name) + # Record each claim before its link: once the link may have committed, any later failure must leave the name for + # _discard_doorway. The token makes the name unique, so removing it when the link never happened is a no-op. + self._context.owner_claim_paths = (intent_path,) + try: + with _pin_claim_directory(claim_directory): + publication_cleanup_error = _publish_record( + intent_path, _claim_record(token), self._open_mode(), _open_claim_directory + ) + except _PrivateRecordReclaimedError: + return False + except (NotImplementedError, OSError) as error: + _raise_if_hard_links_unsupported(self.lock_file, error) + if isinstance(error, OSError) and error.errno == EEXIST: + # Another file holds the name, so it is not this owner's to remove. + self._context.owner_claim_paths = () + return False + raise + if publication_cleanup_error is not None: # pragma: needs dir-fd + raise publication_cleanup_error + + claims = _read_existing_claims(self.lock_file, claim_directory) + if ( + not claims + or any(claim.state == "held" for claim in claims) + or min(claim.name for claim in claims) != intent_name + ): + return False + + held_name = _claim_name("held", token) + held_path = str(claim_directory / held_name) + self._context.owner_claim_paths = (held_path, intent_path) + try: + link_cleanup_error = _link_no_replace(claim_directory, intent_name, held_name) + except (NotImplementedError, OSError) as error: + _raise_if_hard_links_unsupported(self.lock_file, error) + if isinstance(error, OSError) and error.errno == EEXIST: + self._context.owner_claim_paths = (intent_path,) + raise + if link_cleanup_error is not None: # pragma: needs dir-fd + raise link_cleanup_error + + claims = _read_existing_claims(self.lock_file, claim_directory) + # Keep the intent claim for the whole hold rather than unlinking it now. The intent has existed, unchanged, + # since this owner published it, so a contender's os.scandir is guaranteed to return it (POSIX only leaves the + # visibility of entries created or removed *during* a scan unspecified). The freshly linked held claim carries + # no such guarantee: a scan that races its creation can miss it. Were the intent removed here, that scan could + # observe neither claim and let a larger-token contender win over this owner. The stable intent is the witness + # that keeps the phase-five min-token decision computed over the true set. Release unlinks both. + return {intent_name, held_name}.issubset(claim.name for claim in claims) and min( + _claim_token_key(claim.name) for claim in claims + ) == f"v1-{token}.claim" + + @property + def claims(self) -> tuple[StrictSoftFileClaim, ...]: + """Published claims that block acquisition.""" + # An inspection writes nothing, so it trusts no record's age rather than probe the filesystem clock. + return _read_existing_claims(self.lock_file, self._claim_directory, probe_clock=False) + + @property + def lock_path_occupant(self) -> str | None: + """ + What sits at the lock path in place of the strict sentinel, or ``None`` when the sentinel or nothing is there. + + A soft-lock owner's marker blocks acquisition without appearing in :attr:`~filelock.StrictSoftFileLock.claims`; + this describes it, naming the PID and host when the marker records them. A directory, a symlink, a marker whose + owner on this host has exited, or a file it cannot parse that stays unchanged for two seconds makes acquisition + raise :class:`~filelock.SoftFileLockProtocolError` instead and needs an operator to remove it. + """ + try: + fd, _, occupant = _inspect_lock_path(Path(_canonical(self.lock_file))) + except FileNotFoundError: + return None + if fd is None: + return occupant + os.close(fd) + return None + + def force_break(self, claim_name: str) -> None: + """Remove one named claim, allowing overlap if its owner still holds the protected resource.""" + _validate_force_break_name(claim_name) + with _pin_claim_directory(directory := self._claim_directory): + _require_exact_name(directory, claim_name) + cleanup_error = _unlink_in_directory(directory, claim_name) + if cleanup_error is not None: # pragma: needs dir-fd + raise cleanup_error + + def _release_abandoned_holds(self) -> None: + self._finalizing = True + super()._release_abandoned_holds() + # release() skips a doorway whose cleanup failed, as it is no hold, so a dropped instance would strand claims. + with contextlib.suppress(Exception): + # GraalPy defers finalizers to the host collector, so its tests cannot drive this cleanup. + self._discard_pending_doorways(exited_only=False) # pragma: needs collected-finalization + + def _reconcile_failed_acquire(self, canonical: str) -> None: + # The acquisition is over, so the next one resolves the working directory again rather than reuse this one's. + self._context.claim_root = None + super()._reconcile_failed_acquire(canonical) + + def _release(self) -> None: + fd = cast("int", self._context.lock_file_fd) + self._context.claim_root = None + remaining, errors = self._unlink_owner_claims() + if remaining: + _raise_recorded_errors("strict claim release failed", errors) + self._mark_descriptor_released() + try: + self._close_released_fd(fd, default_suppresses=False) + except BaseException as close_error: # ruff:ignore[blind-except] # preserve claim and sentinel cleanup errors + errors.append(close_error) + if errors: + _raise_recorded_errors("strict release cleanup failed", errors) + + def _discard_doorway(self) -> None: + remaining, errors = self._unlink_owner_claims() + # The pending descriptor holds no lock and a retry needs the claim paths alone. A dropped instance does not + # retry, so close it now. + if (fd := self._context.pending_lock_file_fd) is not None: + self._context.pending_lock_file_fd = self._context.pending_lock_file_fd_identity = None + try: + self._close_released_fd(fd, default_suppresses=False) + except BaseException as close_error: # ruff:ignore[blind-except] # preserve claim and sentinel cleanup errors + errors.append(close_error) + if remaining: + # Stay pending, not owned: is_locked reports only a held claim that passed the final rescan. The next + # acquire retries this cleanup. + _raise_recorded_errors("strict doorway claim cleanup failed", errors) + self._mark_descriptor_released() + if errors: + _raise_recorded_errors("strict doorway cleanup failed", errors) + + def _unlink_owner_claims(self) -> tuple[list[str], list[BaseException]]: + remaining, errors = _unlink_owner_paths(self._context.owner_claim_paths) + self._context.owner_claim_paths = tuple(remaining) + if self._finalizing: + # No retry runs after the finalizer and no caller sees its errors, so report success and let it close the + # descriptor even when claims remain. + return [], [] # pragma: needs collected-finalization + return remaining, errors + + @property + def _claim_directory(self) -> Path: + if self._context.owner_claim_paths: + return Path(self._context.owner_claim_paths[0]).parent + return Path(f"{_canonical(self.lock_file)}{_COORDINATION_SUFFIX}") / _CLAIM_DIRECTORY_NAME + + +@dataclass(frozen=True) +class StrictSoftFileClaim: + """One parsed strict soft-lock claim.""" + + name: str + state: StrictSoftFileClaimState + token: str + pid: int + hostname: str + #: The owner's process start token, or ``None`` when the platform exposes no proven start time. A strict lock never + #: reclaims a claim on its own, so this identifies the owner for tooling rather than driving any automatic break. + start: int | None = None + + +class _PrivateRecordReclaimedError(Exception): + pass + + +def _open_or_create_sentinel( + lock_file: str, path: Path, mode: int, unchanged_for: Callable[[os.stat_result], float] +) -> int | None: + try: + return _open_sentinel(lock_file, path, unchanged_for) + except FileNotFoundError: + pass + + _reclaim_sentinel_private_records(lock_file, path) + try: + publication_cleanup_error = _publish_record(os.fspath(path), _LEGACY_SENTINEL, mode) + except _PrivateRecordReclaimedError: + return None + except (NotImplementedError, OSError) as error: + _raise_if_hard_links_unsupported(lock_file, error) + if not isinstance(error, OSError) or error.errno != EEXIST: + raise + else: + if publication_cleanup_error is not None: # pragma: needs dir-fd + raise publication_cleanup_error + try: + return _open_sentinel(lock_file, path, unchanged_for) + except FileNotFoundError: + return None + + +def _open_sentinel(lock_file: str, path: Path, unchanged_for: Callable[[os.stat_result], float]) -> int | None: + # A live soft-lock owner blocks like any holder. Anything else raises: claims cannot show it and force_break() + # cannot remove it, so waiting would wedge every strict contender with no diagnostic. A file no lock parses may be a + # soft lock between its create and its write, so it raises only once unchanged as long as a soft contender waits. + deadline: float | None = None + while True: + fd, state, occupant = _inspect_lock_path(path) + if fd is not None or state == "held": + return fd + reason = f"expected the strict sentinel at the lock path, found {occupant}" + if state == "unparsable": + if unchanged_for(path.lstat()) < MALFORMED_LOCK_AGE_THRESHOLD: + return None + raise SoftFileLockProtocolError( + lock_file, None, f"{reason}, unchanged for {MALFORMED_LOCK_AGE_THRESHOLD:g} seconds" + ) + if deadline is None: + deadline = time.monotonic() + _SENTINEL_READ_GRACE + if state == "exited" or time.monotonic() >= deadline: + raise SoftFileLockProtocolError(lock_file, None, reason) + time.sleep(_CLAIM_READ_RETRY) + + +def _inspect_lock_path(path: Path) -> tuple[int | None, _OccupantState, str]: + # "held" may be a soft-lock owner that still runs, "exited" is one this host proves gone, "unparsable" is content no + # lock parses, and "damaged" is a node no lock writes. + if not stat.S_ISREG(mode := path.lstat().st_mode): + return None, "damaged", _NODE_DESCRIPTIONS.get(stat.S_IFMT(mode), "a special file") + try: + fd, record = _open_record((os.fspath(path.parent), None), path.name, _CLAIM_RECORD_LIMIT) + except FileNotFoundError: + raise + except PermissionError as error: + # Windows denies the read while a legacy holder keeps its marker open, and a holder running as another user + # may leave it unreadable here, so a marker that cannot be read may still be held. + return None, "held", f"a file it cannot read ({error.strerror or error})" + except OSError as error: + return None, "damaged", f"a file it cannot read ({error.strerror or error})" + if record == _LEGACY_SENTINEL: + return fd, "sentinel", "the strict sentinel" + os.close(fd) + return None, *_describe_soft_marker(record.decode("utf-8", errors="replace")) + + +def _describe_soft_marker(content: str) -> tuple[_OccupantState, str]: + # A SoftFileLock, MarkerSoftFileLock or SoftFileLease owner holds the path legitimately until it releases, so its + # marker is contention. One this host proves gone is named, never removed: strict never acts on an inferred death. + if (record := parse_marker(content)) is not None: + pid, hostname, start = record.pid, record.hostname, record.start + elif (holder := _parse_lock_holder(content)) is not None: + pid, hostname, start = holder + else: + return "unparsable", "a file that is neither the sentinel nor a soft-lock marker it can parse" + if owner_is_stale(pid, hostname, start): + return "exited", f"a soft-lock marker left by pid {pid} on {hostname}, which has exited" + return "held", f"a soft-lock marker held by pid {pid} on {hostname}" + + +def _read_claims( + lock_file: str, directory_ref: tuple[str, int | None], *, probe_clock: bool +) -> tuple[StrictSoftFileClaim, ...]: + names: Final = _list_claim_names(lock_file, directory_ref, probe_clock=probe_clock) + name_parts: dict[str, tuple[StrictSoftFileClaimState, str]] = {} + for name in names: + if (parts := _parse_claim_name(name)) is None: + raise SoftFileLockProtocolError(lock_file, name, "unknown claim name or protocol version") + name_parts[name] = parts + records: Final = _read_claim_records(lock_file, directory_ref, names, probe_clock=probe_clock) + return tuple(_parse_claim(lock_file, name, name_parts[name], records[name]) for name in names if name in records) + + +def _list_claim_names(lock_file: str, directory_ref: tuple[str, int | None], *, probe_clock: bool) -> list[str]: + try: + with _scandir_relative(directory_ref) as entries: + return _public_claim_names(directory_ref, entries, probe_clock=probe_clock) + except OSError as error: + reason = f"cannot list claim directory: {error.strerror or type(error).__name__}" + raise SoftFileLockProtocolError(lock_file, None, reason) from error + + +def _read_claim_records( + lock_file: str, directory_ref: tuple[str, int | None], names: list[str], *, probe_clock: bool +) -> dict[str, bytes]: + # A contended scan can list a claim that is not yet cleanly readable, in two ways that both resolve on a brief + # retry. Windows fails an open with EACCES while a peer's unlink holds the claim; once the deletion is pending, + # _attempt_claim_read skips the claim outright, as another process's handle can keep the name listed for seconds. + # On NFS, a peer that unlinks its own claim leaves this client's cached filehandle stale, so the next open returns + # ESTALE rather than a clean ENOENT until the lookup revalidates against the server. Retrying re-runs the path + # lookup, which turns the vanished claim into ENOENT (skip) or reads it if it still exists. A genuinely unreadable + # record (a locked-down file, an EIO fault) still fails closed. The grace starts at the first pending result rather + # than before the first attempt, so a slow first read cannot use up the window and leave no retry. One deadline + # covers the scan: a deadline per claim let claims that clear one after another stretch a scan, and the caller's + # timeout with it, by a grace each. + records: dict[str, bytes] = {} + deadline: float | None = None + while True: + pending: dict[str, OSError] = {} + for name in names: + record, error = _attempt_claim_read(lock_file, directory_ref, name) + if error is not None: + pending[name] = error + elif record is not None: + records[name] = record + if not pending: + return records + if deadline is None: + deadline = time.monotonic() + _CLAIM_READ_GRACE + elif time.monotonic() >= deadline: + break + names = list(pending) + time.sleep(_CLAIM_READ_RETRY) + # A stale handle that outlives revalidation is a claim the server no longer has (RFC 1813 NFS3ERR_STALE): skip it + # like ENOENT. Skipping a vanished claim overcounts contention at worst; it cannot free a held lock. + if denied := [name for name, error in pending.items() if error.errno != ESTALE]: + # Windows can deny opens after a peer removes the claim from the directory. + listed: Final = set(_list_claim_names(lock_file, directory_ref, probe_clock=probe_clock)) + if still_listed := [name for name in denied if name in listed]: + error = pending[still_listed[0]] + reason = f"cannot read claim: {error.strerror or str(error) or type(error).__name__}" + raise SoftFileLockProtocolError(lock_file, still_listed[0], reason) from error + return records + + +def _attempt_claim_read( + lock_file: str, directory_ref: tuple[str, int | None], name: str +) -> tuple[bytes | None, OSError | None]: + # Return the record, or (None, None) when the claim has already gone, or (None, error) for an open the caller may + # retry: a Windows EACCES while a peer's unlink holds the claim, or an NFS ESTALE from a peer unlinking its own + # claim out from under this client's cached filehandle. Any other OSError is a real fault and fails closed. + try: + return _read_record(directory_ref, name, _CLAIM_RECORD_LIMIT), None + except FileNotFoundError: + return None, None + except PermissionError as error: + if sys.platform == "win32" and _is_unlinked(str(Path(directory_ref[0], name))): # pragma: win32 cover + return None, None + return None, error + except OSError as error: + if error.errno == ESTALE: + return None, error + reason = f"cannot read claim: {error.strerror or str(error) or type(error).__name__}" + raise SoftFileLockProtocolError(lock_file, name, reason) from error + + +def _public_claim_names( + directory_ref: tuple[str, int | None], entries: Iterator[os.DirEntry[str]], *, probe_clock: bool +) -> list[str]: + names: list[str] = [] + now: Callable[[], float | None] = ( + functools.cache( + functools.partial( + _filesystem_now, directory_ref, _claim_name("held", secrets.token_hex(_TOKEN_HEX_LENGTH // 2)) + ) + ) + if probe_clock + else lambda: None + ) + for entry in entries: + if not entry.name.startswith("."): + names.append(entry.name) + elif (public_name := _private_public_name(entry.name)) is not None and _parse_claim_name( + public_name + ) is not None: + _reclaim_private_record(directory_ref, entry.name, now) + return sorted(names) + + +def _read_existing_claims( + lock_file: str, directory: Path, *, probe_clock: bool = True +) -> tuple[StrictSoftFileClaim, ...]: + if not _OPEN_SUPPORTS_DIR_FD: # pragma: lacks dir-fd + try: + pinned = _pin_claim_directory(directory) + except FileNotFoundError: + return () + except OSError as error: + reason = f"cannot open claim directory: {error.strerror or type(error).__name__}" + raise SoftFileLockProtocolError(lock_file, None, reason) from error + with pinned: + return _read_claims(lock_file, (os.fspath(directory), None), probe_clock=probe_clock) + return _read_claims_through_descriptor(lock_file, directory, probe_clock=probe_clock) # pragma: needs dir-fd + + +def _read_claims_through_descriptor( # pragma: needs dir-fd + lock_file: str, directory: Path, *, probe_clock: bool +) -> tuple[StrictSoftFileClaim, ...]: + try: + directory_fd = _open_claim_directory(os.fspath(directory)) + except FileNotFoundError: + return () + except OSError as error: + reason = f"cannot open claim directory: {error.strerror or type(error).__name__}" + raise SoftFileLockProtocolError(lock_file, None, reason) from error + try: + return _read_claims(lock_file, (os.fspath(directory), directory_fd), probe_clock=probe_clock) + finally: + # The scan wrote nothing through the descriptor, so a failed close has no result to report. + with contextlib.suppress(OSError): + os.close(directory_fd) + + +def _parse_claim( + lock_file: str, + name: str, + name_parts: tuple[StrictSoftFileClaimState, str], + record: bytes, +) -> StrictSoftFileClaim: + try: + magic, token, pid_text, hostname_hex, start_text, trailing = record.decode("ascii").split("\n") + pid = int(pid_text) + start = int(start_text) if start_text else None + hostname = bytes.fromhex(hostname_hex).decode("utf-8") + except (UnicodeDecodeError, ValueError) as error: + raise SoftFileLockProtocolError(lock_file, name, "malformed claim record") from error + if not all(( + not trailing, + magic == _CLAIM_MAGIC, + token == name_parts[1], + 1 <= pid <= 2**31 - 1, + str(pid) == pid_text, + start is None or (start >= 0 and str(start) == start_text), + hostname.encode().hex() == hostname_hex + and hostname.isprintable() + and not any(character.isspace() for character in hostname), + )): + raise SoftFileLockProtocolError(lock_file, name, "malformed claim record") + return StrictSoftFileClaim(name=name, state=name_parts[0], token=token, pid=pid, hostname=hostname, start=start) + + +def _parse_claim_name(name: str) -> tuple[StrictSoftFileClaimState, str] | None: + if not name.endswith(".claim"): + return None + parts = name.removesuffix(".claim").split("-") + if len(parts) != _CLAIM_NAME_PART_COUNT or parts[0] not in _CLAIM_STATES or parts[1] != "v1": + return None + token = parts[2] + if len(token) != _TOKEN_HEX_LENGTH or any(character not in "0123456789abcdef" for character in token): + return None + return cast("StrictSoftFileClaimState", parts[0]), token + + +def _claim_name(state: StrictSoftFileClaimState, token: str) -> str: + return f"{state}-v1-{token}.claim" + + +def _claim_token_key(name: str) -> str: + return name.removeprefix("held-").removeprefix("intent-") + + +def _claim_record(token: str) -> bytes: + hostname_hex = host_name().encode().hex() + start = process_start_token(os.getpid()) + start_text = "" if start is None else str(start) + return f"{_CLAIM_MAGIC}\n{token}\n{os.getpid()}\n{hostname_hex}\n{start_text}\n".encode("ascii") + + +def _publish_record( + public_path: str, + record: bytes, + mode: int, + open_directory: Callable[[str], int] | None = None, +) -> BaseException | None: + directory, public_name = os.path.split(public_path) + directory = directory or os.curdir + private_name = _private_record_name(public_name) + directory_fd = (open_directory or _open_directory)(directory) if _OPEN_SUPPORTS_DIR_FD else None + directory_ref = directory, directory_fd + try: + _publish_record_in_directory(directory_ref, (private_name, public_name), mode, record) + except BaseException as publication_error: # preserve publication and directory cleanup errors + try: + if directory_fd is not None: # pragma: needs dir-fd + os.close(directory_fd) + except BaseException as close_error: # ruff:ignore[blind-except] # pragma: needs dir-fd # preserve publication and directory cleanup errors + _raise_cleanup_errors("strict publication directory cleanup failed", publication_error, close_error) + raise + if directory_fd is not None: # pragma: needs dir-fd + try: + os.close(directory_fd) + except BaseException as close_error: # ruff:ignore[blind-except] # a claim's caller recorded its path before publishing + return close_error + return None + + +def _publish_record_in_directory( + directory_ref: tuple[str, int | None], + names: tuple[str, str], + mode: int, + record: bytes, +) -> None: + flags = os.O_RDWR | os.O_CREAT | os.O_EXCL | _O_BINARY + if (o_nofollow := getattr(os, "O_NOFOLLOW", None)) is not None: # pragma: needs o-nofollow + flags |= o_nofollow + private_fd = _open_relative(directory_ref, names[0], flags, mode) + private_identity: tuple[int, int] | None = None + try: + private_identity = _file_identity(os.fstat(private_fd)) + write_all(private_fd, record) + _link_private_record(directory_ref, names, private_identity) + except BaseException as publication_error: # preserve publication and cleanup errors + close_error, unlink_error = _close_and_unlink_private_record( + directory_ref, + names[0], + private_fd, + private_identity, + ) + if close_error is not None or unlink_error is not None: + _raise_cleanup_errors( + "strict record publication cleanup failed", + publication_error, + close_error, + unlink_error, + ) + raise + close_error, unlink_error = _close_and_unlink_private_record( + directory_ref, + names[0], + private_fd, + private_identity, + ) + if close_error is not None or unlink_error is not None: + _raise_record_finalization_errors(close_error, unlink_error) + + +def _close_and_unlink_private_record( + directory_ref: tuple[str, int | None], + private_name: str, + private_fd: int, + private_identity: tuple[int, int] | None, +) -> tuple[BaseException | None, BaseException | None]: + close_error: BaseException | None = None + try: + os.close(private_fd) + except BaseException as error: # ruff:ignore[blind-except] # returned for grouping with unlink failures + close_error = error + unlink_error: BaseException | None = None + try: + if private_identity is None: + _unlink_relative(directory_ref, private_name) + else: + _unlink_relative_if_identity(directory_ref, private_name, private_identity) + except FileNotFoundError: + pass + except BaseException as error: # ruff:ignore[blind-except] # returned for grouping with close failures + unlink_error = error + return close_error, unlink_error + + +def _link_private_record( + directory_ref: tuple[str, int | None], + names: tuple[str, str], + private_identity: tuple[int, int], +) -> None: + try: + _link_or_find_committed(directory_ref, *names, private_identity) + except FileNotFoundError as error: + if _relative_identity(directory_ref, names[0]) is not None: + raise + msg = "private publication record was reclaimed" + raise _PrivateRecordReclaimedError(msg) from error + if _relative_identity(directory_ref, names[1]) == private_identity: + return + msg_0 = "private publication record was replaced" + raise _PrivateRecordReclaimedError(msg_0) + + +def _raise_record_finalization_errors( + close_error: BaseException | None, + unlink_error: BaseException | None, +) -> None: + errors = [error for error in (close_error, unlink_error) if error is not None] + if len(errors) > 1: + _raise_cleanup_errors("strict record finalization failed", errors[0], *errors[1:]) + raise errors[0] + + +def _private_record_name(public_name: str) -> str: + return f".{public_name}{_PRIVATE_RECORD_MARKER}{secrets.token_hex(_PRIVATE_RECORD_RANDOM_HEX_LENGTH // 2)}.tmp" + + +def _private_public_name(private_name: str) -> str | None: + if not private_name.startswith(".") or not private_name.endswith(_PRIVATE_RECORD_SUFFIX): + return None + public_name, marker, random_hex = private_name[1 : -len(_PRIVATE_RECORD_SUFFIX)].rpartition(_PRIVATE_RECORD_MARKER) + if ( + marker != _PRIVATE_RECORD_MARKER + or len(random_hex) != _PRIVATE_RECORD_RANDOM_HEX_LENGTH + or any(character not in "0123456789abcdef" for character in random_hex) + ): + return None + return public_name + + +def _reclaim_sentinel_private_records(lock_file: str, path: Path) -> None: + directory_ref = os.fspath(path.parent), None + now = functools.cache(functools.partial(_filesystem_now, directory_ref, path.name)) + try: + with os.scandir(path.parent) as entries: + for entry in entries: + if _private_public_name(entry.name) == path.name: + _reclaim_private_record(directory_ref, entry.name, now) + except OSError as error: + reason = f"cannot reclaim sentinel private records: {error.strerror or error}" + raise SoftFileLockProtocolError(lock_file, None, reason) from error + + +def _reclaim_private_record( + directory_ref: tuple[str, int | None], private_name: str, now: Callable[[], float | None] +) -> None: + directory, directory_fd = directory_ref + try: + private_stat = ( + os.stat(private_name, dir_fd=directory_fd, follow_symlinks=False) + if directory_fd is not None and _STAT_SUPPORTS_DIR_FD + else Path(directory, private_name).lstat() + ) + except FileNotFoundError: + return + if not stat.S_ISREG(private_stat.st_mode): + msg = f"{Path(directory, private_name)} is not a regular private record" + raise OSError(msg) + if private_stat.st_nlink == 1 and ( + (current := now()) is None or current - private_stat.st_mtime < _PRIVATE_RECORD_GRACE + ): + return + try: + _unlink_private_record_once(directory_ref, private_name) + except FileNotFoundError: + pass + except OSError as error: + if error.errno not in {EACCES, EPERM}: + raise + + +def _filesystem_now(directory_ref: tuple[str, int | None], public_name: str) -> float | None: + # A network filesystem stamps mtimes with the server's clock, which can run seconds off this client's, so read "now" + # as the mtime of a probe written like a record (liblockfile uses a read's atime). The probe has a private-record + # name, so a scan reaps one left behind. Without a stamp no age is trusted. + probe = _private_record_name(public_name) + flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | _O_BINARY | getattr(os, "O_NOFOLLOW", 0) + try: + fd = _open_relative(directory_ref, probe, flags, 0o600) + except OSError: + return None + try: + write_all(fd, b"\n") + return os.fstat(fd).st_mtime + except OSError: + return None + finally: + with contextlib.suppress(OSError): + os.close(fd) + with contextlib.suppress(OSError): + _unlink_private_record_once(directory_ref, probe) + + +def _unlink_private_record_once(directory_ref: tuple[str, int | None], private_name: str) -> None: + directory, directory_fd = directory_ref + if directory_fd is not None and _UNLINK_SUPPORTS_DIR_FD: # pragma: needs dir-fd + os.unlink(private_name, dir_fd=directory_fd) + else: + Path(directory, private_name).unlink() + + +def _relative_identity(directory_ref: tuple[str, int | None], name: str) -> tuple[int, int] | None: + directory, directory_fd = directory_ref + try: + if directory_fd is not None and _STAT_SUPPORTS_DIR_FD: # pragma: needs dir-fd + path_stat = os.stat(name, dir_fd=directory_fd, follow_symlinks=False) + else: # pragma: win32 cover + path_stat = Path(directory, name).lstat() + except FileNotFoundError: + return None + return _file_identity(path_stat) + + +def _unlink_relative_if_identity( + directory_ref: tuple[str, int | None], + name: str, + identity: tuple[int, int], +) -> None: + if _relative_identity(directory_ref, name) == identity: + with contextlib.suppress(FileNotFoundError): + _unlink_relative(directory_ref, name) + + +def _open_relative(directory_ref: tuple[str, int | None], name: str, flags: int, mode: int) -> int: + directory, directory_fd = directory_ref + return ( + os.open(name, flags, mode, dir_fd=directory_fd) + if directory_fd is not None + else os.open(Path(directory, name), flags, mode) + ) + + +def _link_relative(directory_ref: tuple[str, int | None], source_name: str, destination_name: str) -> None: + directory, directory_fd = directory_ref + if directory_fd is not None and _LINK_SUPPORTS_DIR_FD: # pragma: needs dir-fd + _link_no_follow(source_name, destination_name, src_dir_fd=directory_fd, dst_dir_fd=directory_fd) + return + _link_no_follow(Path(directory, source_name), Path(directory, destination_name)) # pragma: win32 cover + + +def _link_or_find_committed( + directory_ref: tuple[str, int | None], + source_name: str, + destination_name: str, + source_identity: tuple[int, int] | None = None, +) -> None: + try: + _link_relative(directory_ref, source_name, destination_name) + except FileExistsError: + # NFS retransmits a LINK whose reply was lost and reports EEXIST for the committed link; as in open(2) NOTES + # and liblockfile, it succeeded when the destination names the source. Prefer the fstat-ed identity: a peer's + # scan reaps a private record once it has two links, so its name may already resolve to nothing. + if (identity := _relative_identity(directory_ref, destination_name)) is None or identity != ( + source_identity or _relative_identity(directory_ref, source_name) + ): + raise + + +def _link_no_follow( + source: str | Path, + destination: str | Path, + *, + src_dir_fd: int | None = None, + dst_dir_fd: int | None = None, +) -> None: + if not _HAS_LINK: + # No os.link at all (Termux/Android): report it as unsupported like a filesystem that refuses hard links, so + # the acquire path raises SoftFileLockProtocolError instead of a bare AttributeError. + msg = "os.link is unavailable on this platform" + raise NotImplementedError(msg) + # The source is a private record this process created with O_CREAT | O_EXCL, so follow_symlinks guards nothing an + # attacker can reach. Pass the option only when the runtime honors it: PyPy advertises it through + # os.supports_follow_symlinks yet its linkat rejects it with EINVAL, so probe once rather than trust the set. + if _LINK_HONORS_FOLLOW_SYMLINKS: + os.link(source, destination, src_dir_fd=src_dir_fd, dst_dir_fd=dst_dir_fd, follow_symlinks=False) + else: # pragma: lacks link-follow-symlinks + os.link(source, destination, src_dir_fd=src_dir_fd, dst_dir_fd=dst_dir_fd) + + +def _unlink_relative(directory_ref: tuple[str, int | None], name: str) -> None: + directory, directory_fd = directory_ref + if directory_fd is not None and _UNLINK_SUPPORTS_DIR_FD: # pragma: needs dir-fd + os.unlink(name, dir_fd=directory_fd) + elif sys.platform == "win32": # pragma: win32 cover + _unlink_in_directory(Path(directory), name) + else: + Path(directory, name).unlink() # pragma: win32 no cover + + +def _link_no_replace(directory: Path, source_name: str, destination_name: str) -> BaseException | None: + if _LINK_SUPPORTS_DIR_FD: # pragma: needs dir-fd + directory_fd = _open_claim_directory(str(directory)) + try: + _link_or_find_committed((str(directory), directory_fd), source_name, destination_name) + except BaseException as link_error: # preserve link and directory cleanup errors + try: + os.close(directory_fd) + except BaseException as close_error: # ruff:ignore[blind-except] # preserve link and directory cleanup errors + _raise_cleanup_errors("strict link directory cleanup failed", link_error, close_error) + raise + try: + os.close(directory_fd) + except BaseException as close_error: # ruff:ignore[blind-except] # the caller recorded the held path before linking + return close_error + return None + with _pin_claim_directory(directory): # pragma: win32 cover + _link_or_find_committed((str(directory), None), source_name, destination_name) + return None # pragma: win32 cover + + +def _unlink_owner_path(path: str) -> BaseException | None: + directory, name = os.path.split(path) + with _pin_claim_directory(claim_directory := Path(directory or os.curdir)): + return _unlink_in_directory(claim_directory, name) + + +def _unlink_owner_paths(paths: tuple[str, ...]) -> tuple[list[str], list[BaseException]]: + results = [(path, _unlink_owner_path_result(path)) for path in paths] + return [path for path, (removed, _) in results if not removed], [ + error for _, (_, error) in results if error is not None + ] + + +def _unlink_owner_path_result(path: str) -> tuple[bool, BaseException | None]: + try: + cleanup_error = _unlink_owner_path(path) + except FileNotFoundError: + return True, None + except BaseException as error: # ruff:ignore[blind-except] # keep ownership when unlink did not commit + return False, error + return True, cleanup_error + + +def _raise_recorded_errors(message: str, errors: list[BaseException]) -> None: + if len(errors) > 1: + _raise_cleanup_errors(message, errors[0], *errors[1:]) + raise errors[0] + + +def _unlink_in_directory(directory: Path, name: str) -> BaseException | None: + if _UNLINK_SUPPORTS_DIR_FD: # pragma: needs dir-fd + directory_fd = _open_claim_directory(str(directory)) + try: + os.unlink(name, dir_fd=directory_fd) + except BaseException as unlink_error: # preserve unlink and directory cleanup errors + try: + os.close(directory_fd) + except BaseException as close_error: # ruff:ignore[blind-except] # preserve unlink and directory cleanup errors + _raise_cleanup_errors("strict unlink directory cleanup failed", unlink_error, close_error) + raise + try: + os.close(directory_fd) + except BaseException as close_error: # ruff:ignore[blind-except] # caller commits the removed path before raising + return close_error + return None + if sys.platform != "win32": # pragma: win32 no cover + Path(directory / name).unlink() + return None + retry_delay = 0.001 # pragma: win32 cover + for attempt in range(_UNLINK_MAX_RETRIES): # pragma: win32 cover + if (error := _unlink_error(directory / name)) is None: + return None + if error.errno not in {EACCES, EPERM} or attempt == _UNLINK_MAX_RETRIES - 1: + raise error + time.sleep(retry_delay) + retry_delay *= 2 + return None # pragma: no cover # the final retry returns or raises + + +def _unlink_error(path: Path) -> OSError | None: # pragma: win32 cover + try: + path.unlink() + except OSError as error: + return error + return None + + +def _pin_claim_directory(directory: Path) -> contextlib.ExitStack[bool | None]: + return _pin_directories(directory.parent, directory) + + +def _pin_directories(*directories: Path) -> contextlib.ExitStack[bool | None]: + # Windows has no dir_fd, so each operation resolves every path component by name again. An open handle on a + # directory blocks a rename of it, and with it a swap for a junction (a directory that points at another directory), + # until the operation ends. Other platforms work through descriptors and hold nothing. + stack: contextlib.ExitStack[bool | None] = contextlib.ExitStack() + if sys.platform == "win32": # pragma: win32 cover + with stack: + for directory in directories: + stack.callback(_unpin_directory, _pin_directory(str(directory))) + return stack.pop_all() + return stack # pragma: win32 no cover + + +def _open_directory(directory: str, dir_fd: int | None = None) -> int: # pragma: needs dir-fd + return os.open(directory, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0), dir_fd=dir_fd) + + +def _open_claim_directory(directory: str) -> int: # pragma: needs dir-fd + # O_NOFOLLOW applies to the last path component. A process that can rename entries in the lock's directory can + # swap the coordination directory for a symlink after any check of its name, so open the claim directory relative + # to a descriptor of the coordination directory: a symlink at either level then fails the open. + coordination_directory, name = os.path.split(directory) + coordination_fd = _open_directory(coordination_directory) + try: + return _open_directory(name, coordination_fd) + finally: + # The walk wrote nothing through it, and an error here would leak the descriptor this function returns. + with contextlib.suppress(OSError): + os.close(coordination_fd) + + +def _scandir_relative( + directory_ref: tuple[str, int | None], +) -> contextlib.AbstractContextManager[Iterator[os.DirEntry[str]]]: + directory, directory_fd = directory_ref + if directory_fd is not None and _SCANDIR_SUPPORTS_FD: # pragma: needs dir-fd + return os.scandir(directory_fd) + return os.scandir(directory) # pragma: lacks dir-fd + + +def _lstat_relative(directory_ref: tuple[str, int | None], name: str) -> os.stat_result: + directory, directory_fd = directory_ref + if directory_fd is not None and _STAT_SUPPORTS_DIR_FD: # pragma: needs dir-fd + return os.stat(name, dir_fd=directory_fd, follow_symlinks=False) + return Path(directory, name).lstat() + + +def _read_record(directory_ref: tuple[str, int | None], name: str, limit: int) -> bytes: + fd, record = _open_record(directory_ref, name, limit, deletable=True) + os.close(fd) + return record + + +def _open_record( + directory_ref: tuple[str, int | None], name: str, limit: int, *, deletable: bool = False +) -> tuple[int, bytes]: + path = Path(directory_ref[0], name) + path_stat = _lstat_relative(directory_ref, name) + if not stat.S_ISREG(path_stat.st_mode): + msg = f"{path} is not a regular file" + raise OSError(msg) + if deletable and sys.platform == "win32": # pragma: win32 cover + # A claim's owner must be able to unlink it while another process reads it. The sentinel stays for the lock's + # lifetime, so it keeps os.open. + fd = _open_deletable_for_reading(str(path)) + else: + flags = os.O_RDONLY | _O_BINARY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0) + fd = _open_relative(directory_ref, name, flags, 0) + try: + record = _read_opened_record(fd, path, path_stat, limit) + except BaseException as read_error: # preserve read and descriptor cleanup errors + try: + os.close(fd) + except BaseException as close_error: # ruff:ignore[blind-except] # preserve read and descriptor cleanup errors + _raise_cleanup_errors("strict record read cleanup failed", read_error, close_error) + raise + return fd, record + + +def _read_opened_record(fd: int, path: Path, path_stat: os.stat_result, limit: int) -> bytes: + opened_stat = os.fstat(fd) + if not stat.S_ISREG(opened_stat.st_mode): + msg = f"{path} is not a regular file" + raise OSError(msg) + if _file_identity(opened_stat) != _file_identity(path_stat): + msg = f"{path} changed while opening" + raise OSError(msg) + record = os.read(fd, limit + 1) + if len(record) > limit: + msg = f"{path} exceeds {limit} bytes" + raise OSError(msg) + return record + + +def _ensure_claim_directory(lock_file: str, directory: Path, mode: int) -> None: + if not (_OPEN_SUPPORTS_DIR_FD and _MKDIR_SUPPORTS_DIR_FD): # pragma: lacks dir-fd + try: + pinned = _pin_directories(directory.parent) + except OSError as error: + raise SoftFileLockProtocolError(lock_file, None, f"cannot open {directory.parent}") from error + with pinned: + _ensure_protocol_directory(lock_file, directory, mode) + else: # pragma: needs dir-fd + # mkdir follows a symlink in every component but the last, so create against a descriptor of the real parent. + try: + coordination_fd = _open_directory(os.fspath(directory.parent)) + except OSError as error: + raise SoftFileLockProtocolError(lock_file, None, f"cannot open {directory.parent}") from error + try: + _ensure_protocol_directory(lock_file, directory, mode, coordination_fd) + finally: + with contextlib.suppress(OSError): + os.close(coordination_fd) + + +def _ensure_protocol_directory(lock_file: str, directory: Path, mode: int, parent_fd: int | None = None) -> None: + try: + if parent_fd is None: + directory.mkdir(mode) + else: # pragma: needs dir-fd + os.mkdir(directory.name, mode, dir_fd=parent_fd) + except FileExistsError: + try: + found = _lstat_relative((os.fspath(directory.parent), parent_fd), directory.name) + owner = foreign_directory_owner( + found, directory.parent.stat() if parent_fd is None else os.fstat(parent_fd) + ) + except OSError as error: + raise SoftFileLockProtocolError(lock_file, None, f"cannot inspect {directory}") from error + if ( + not stat.S_ISDIR(found.st_mode) + or getattr(found, "st_file_attributes", 0) & stat.FILE_ATTRIBUTE_REPARSE_POINT + ): + raise SoftFileLockProtocolError(lock_file, None, f"{directory} is not a real directory") from None + if owner is not None: # pragma: needs foreign-sticky-tmp + reason = f"{directory} belongs to uid {owner}, not to this process's account or the parent's owner" + raise SoftFileLockProtocolError(lock_file, None, reason) from None + _settle_directory_mode(lock_file, directory, mode, parent_fd) + + +def _settle_directory_mode(lock_file: str, directory: Path, mode: int, parent_fd: int | None) -> None: + # The umask or a default ACL can leave the directory wider than the mode, now or in an earlier release, so narrow a + # directory this account owns to the mode. The sticky bit (a directory flag that lets only a file's owner remove it) + # then protects each claim from other accounts even where the mode admits them. Windows has neither mode bits nor a + # sticky bit. + if sys.platform != "win32": # pragma: win32 no cover + try: + fd = os.open( + directory.name if parent_fd is not None else os.fspath(directory), + os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0), + dir_fd=parent_fd, + ) + except OSError as error: + raise SoftFileLockProtocolError(lock_file, None, f"cannot open {directory}") from error + try: + found = os.fstat(fd) + settled = found.st_mode & 0o7000 | found.st_mode & mode & 0o777 | stat.S_ISVTX + if found.st_uid == os.geteuid() and settled != stat.S_IMODE(found.st_mode): + os.fchmod(fd, settled) + except OSError as error: + raise SoftFileLockProtocolError(lock_file, None, f"cannot set the mode of {directory}") from error + finally: + os.close(fd) + + +def _validate_force_break_name(name: str) -> None: + invalid_component = not name or name.startswith(".") or name in {os.curdir, os.pardir} + if invalid_component or any(separator in name for separator in ("/", "\\", "\x00")): + msg = "claim_name must be one public claim basename" + raise ValueError(msg) + + +def _require_exact_name(directory: Path, name: str) -> None: + directory_fd = _open_claim_directory(os.fspath(directory)) if _OPEN_SUPPORTS_DIR_FD else None + try: + with _scandir_relative((os.fspath(directory), directory_fd)) as entries: + if not any(entry.name == name for entry in entries): + raise FileNotFoundError(ENOENT, os.strerror(ENOENT), name) + finally: + if directory_fd is not None: # pragma: needs dir-fd + with contextlib.suppress(OSError): + os.close(directory_fd) + + +def _raise_if_hard_links_unsupported(lock_file: str, error: NotImplementedError | OSError) -> None: + unsupported = ( + isinstance(error, NotImplementedError) + or error.errno in _HARD_LINK_UNSUPPORTED_ERRNOS + or (getattr(error, "winerror", None) in _WINDOWS_HARD_LINK_UNSUPPORTED) + ) + if unsupported: + reason = "filesystem does not support atomic no-replace hard-link publication" + raise SoftFileLockProtocolError(lock_file, None, reason) from error + + +def _file_identity(st: os.stat_result) -> tuple[int, int]: + return st.st_dev, st.st_ino + + +__all__ = [ + "StrictSoftFileClaim", + "StrictSoftFileClaimState", + "StrictSoftFileLock", +] diff --git a/hug/lib/python3.12/site-packages/filelock/_unix.py b/hug/lib/python3.12/site-packages/filelock/_unix.py new file mode 100644 index 0000000000000000000000000000000000000000..30c627638c5c32ead1977bf381342c5836ead72f --- /dev/null +++ b/hug/lib/python3.12/site-packages/filelock/_unix.py @@ -0,0 +1,218 @@ +from __future__ import annotations + +__lazy_modules__ = ["contextlib", "filelock._util", "pathlib", "typing", "warnings"] + +import os +import sys +import warnings +from contextlib import suppress +from errno import EACCES, EAGAIN, ENOSYS, EWOULDBLOCK +from pathlib import Path +from typing import Final, cast + +from filelock._api import BaseFileLock +from filelock._util import remove_own_file + +has_fcntl = False +if sys.platform == "win32": # pragma: win32 cover + + class UnixFileLock(BaseFileLock): + """Uses the :func:`fcntl.flock` to hard lock the lock file on unix systems.""" + + def _acquire(self) -> None: + raise NotImplementedError + + def _release(self) -> None: + raise NotImplementedError + +else: # pragma: win32 no cover + try: + import fcntl + + _ = (fcntl.flock, fcntl.LOCK_EX, fcntl.LOCK_NB, fcntl.LOCK_UN) + except (ImportError, AttributeError): + _FCNTL_UNAVAILABLE: Final[str] = "fcntl is unavailable" + + def _lock_fd_nonblocking(_fd: int) -> bool: + raise OSError(ENOSYS, _FCNTL_UNAVAILABLE) + + def _unlock_fd(_fd: int) -> None: + raise OSError(ENOSYS, _FCNTL_UNAVAILABLE) + + else: + has_fcntl = True + # Contention errnos for a nonblocking flock. EAGAIN/EWOULDBLOCK are the usual "held elsewhere" codes; some + # filesystems report EACCES instead, so treat it as contention too rather than a permanent error. + _CONTENTION_ERRNOS: Final[frozenset[int]] = frozenset({EACCES, EAGAIN, EWOULDBLOCK}) + + def _lock_fd_nonblocking(fd: int) -> bool: + # One nonblocking exclusive flock attempt shared by UnixFileLock and lock_descriptor, so both contend on + # the same lock and classify errors identically. The caller owns fd; this never closes it. + try: + fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + except OSError as exception: + if exception.errno in _CONTENTION_ERRNOS: + return False + raise + return True + + def _unlock_fd(fd: int) -> None: + fcntl.flock(fd, fcntl.LOCK_UN) + + def _flock_is_missing(fd: int) -> bool: + try: + _lock_fd_nonblocking(fd) + except OSError as exception: + return exception.errno == ENOSYS + return False # a flock the probe won is released when the caller closes fd + + class UnixFileLock(BaseFileLock): + """ + Uses the :func:`fcntl.flock` to hard lock the lock file on unix systems. + + We leave the lock file in place after release. Unlinking a locked file on Unix splits + waiters across inodes and breaks mutual exclusion for processes that coordinate via the + same path. + """ + + def _acquire(self) -> None: + if (missing_flock := self._acquire_native()) is not None: + fd, exception, created = missing_flock + self._switch_to_soft_lock(fd, exception, created=created) + + def _acquire_native(self) -> tuple[int, OSError, bool] | None: + if (opened := self._open_lock_file()) is None: + return None + fd, created = opened + self._mark_descriptor_pending(fd) + if created: + # Before the flock: a peer may win it first, and only the creator may set the mode. Outside the flock + # handler below: an fchmod that answers ENOSYS says nothing about flock. + try: + self._apply_explicit_mode(fd) + except BaseException: + self._mark_descriptor_released() + os.close(fd) + raise + # One guard covers the flock through the recording of the hold. A signal handler that raises in between + # would otherwise leave an unreferenced descriptor, and its flock, open until the process exits. + locked = False + try: + locked = _lock_fd_nonblocking(fd) + if locked and self._finalize_locked_fd(fd): + return None + except BaseException as exception: + if not locked and isinstance(exception, OSError) and exception.errno == ENOSYS: + return fd, exception, created + # Contention returns False from _lock_fd_nonblocking, so a raise here is a failure. + self._mark_descriptor_released() + os.close(fd) + raise + self._mark_descriptor_released() + os.close(fd) # contention or an unlinked inode; let the retry loop try again + return None + + def _open_lock_file(self) -> tuple[int, bool] | None: + # No O_TRUNC: a contender that loses the flock must not truncate the holder's file (#591), and neither may + # the winner, since a file renamed or hard-linked to the lock path is indistinguishable from a lock file. + # O_EXCL tells whether this attempt created the inode: only then does it get the explicit mode, and the + # ENOSYS fallback removes only its own placeholder. O_NONBLOCK: a lease (Linux F_SETLEASE) on the file would + # otherwise block the open for up to fs.lease-break-time, beyond timeout, blocking=False and cancel_check. + open_flags = os.O_RDWR | os.O_NONBLOCK + if (o_nofollow := getattr(os, "O_NOFOLLOW", None)) is not None: + open_flags |= o_nofollow + open_mode = self._open_mode() + try: + return os.open(self._lock_path, open_flags | os.O_CREAT | os.O_EXCL, open_mode), True + except FileExistsError: + pass + except FileNotFoundError: + # On FUSE/NFS a split LOOKUP + CREATE lets a concurrent unlink() race the create, so ENOENT is transient + # contention. An empty or separator-terminated path (Path() strips the separator) fails this way on + # every attempt on macOS, so re-raise rather than retry forever. + if self._lock_path and not self._lock_path.endswith(os.sep) and Path(self._lock_path).parent.exists(): + return None + raise + except PermissionError: + # Sticky-bit dirs (e.g. /tmp): O_CREAT fails if another user owns the file (#317). + if not Path(self._lock_path).exists(): + raise + return self._open_existing_lock_file(open_flags, open_mode) + + def _open_existing_lock_file(self, open_flags: int, open_mode: int) -> tuple[int, bool] | None: + try: + return os.open(self._lock_path, open_flags, open_mode), False + except FileNotFoundError: + return None # unlinked since the create attempt; let the retry loop start over + except BlockingIOError: + return None # a lease is breaking: contention, so the retry loop keeps the deadline + except PermissionError: + # A marker another account left is readable but not writable. Without flock, waiting on it only needs + # reading it; with flock the lock writes its file, so the denial stands. + try: + fd = os.open(self.lock_file, open_flags & ~os.O_RDWR | os.O_RDONLY) + except (FileNotFoundError, BlockingIOError): + return None # unlinked, or leased: the kernel checks permission before the lease + if _flock_is_missing(fd): + return fd, False + os.close(fd) + raise + + def _switch_to_soft_lock(self, fd: int, missing_flock: OSError, *, created: bool) -> None: + # The filesystem does not implement flock. Fail closed when the caller opted out of existence-lock + # semantics (#603), asked to preserve the pathname (#605), or set an on_acquired hook (#607), none of which + # a soft lock can honor. + falls_back: Final = self._fallback_to_soft and not self._preserve_lock_file and self._on_acquired is None + try: + # A file this attempt did not create may be a peer's live marker, so it stays. Our own placeholder goes + # before the close: the open descriptor keeps its inode number from a peer's marker, and an exchange + # puts back a peer's marker that replaced it. + if created and falls_back: + with suppress(OSError): + remove_own_file(self._lock_path, fd) + finally: + self._mark_descriptor_released() + os.close(fd) + if not falls_back: + raise missing_flock + self._fallback_to_soft_lock() + self._acquire() + + def _finalize_locked_fd(self, fd: int) -> bool: + # Runs with the flock held; the caller closes fd on failure. A concurrent _release() may have unlinked + # the file between our open() and flock(), which st_nlink 0 shows. That lock guards no name, so report + # it and let the retry loop start over. + if (stat_result := os.fstat(fd)).st_nlink == 0: + return False + self._mark_descriptor_owned(fd, (stat_result.st_dev, stat_result.st_ino)) + return True + + def _apply_explicit_mode(self, fd: int) -> None: + if self.has_explicit_mode: + with suppress(PermissionError): + os.fchmod(fd, self._context.mode) + + def _fallback_to_soft_lock(self: BaseFileLock) -> None: + # Import lazily: this runs only on the rare flock fallback, and asyncio imports _unix, so a + # module-level import of it here would cycle. + from filelock._soft import SoftFileLock # ruff:ignore[import-outside-top-level] + + warnings.warn("flock not supported on this filesystem, falling back to SoftFileLock", stacklevel=2) + from filelock.asyncio import AsyncSoftFileLock, BaseAsyncFileLock # ruff:ignore[import-outside-top-level] + + self.__class__ = AsyncSoftFileLock if isinstance(self, BaseAsyncFileLock) else SoftFileLock + + def _release(self) -> None: + fd = cast("int", self._context.lock_file_fd) + # Retain the descriptor until flock succeeds: a failed unlock leaves the kernel lock held, so is_locked + # must keep reporting held for a retry. Once flock commits, clear held state and close as post-unlock + # cleanup; a close failure (EIO on FUSE/Docker bind mounts) does not make the kernel lock held again. + _unlock_fd(fd) + self._mark_descriptor_released() + self._close_released_fd(fd, default_suppresses=True) + + +if sys.platform == "win32": # pragma: win32 cover + __all__ = ["UnixFileLock", "has_fcntl"] +else: # pragma: win32 no cover + __all__ = ["UnixFileLock", "_lock_fd_nonblocking", "_unlock_fd", "has_fcntl"] diff --git a/hug/lib/python3.12/site-packages/filelock/_util.py b/hug/lib/python3.12/site-packages/filelock/_util.py new file mode 100644 index 0000000000000000000000000000000000000000..8dbdadd74333d6f20c3ea48ebafbfe175ae98b15 --- /dev/null +++ b/hug/lib/python3.12/site-packages/filelock/_util.py @@ -0,0 +1,399 @@ +from __future__ import annotations + +__lazy_modules__ = ["pathlib", "platform", "secrets"] + +import errno +import os +import platform +import secrets +import stat +import sys +from contextlib import contextmanager, suppress +from errno import EACCES, EINVAL, EIO, EISDIR, ENOSYS, EXDEV +from functools import cache +from pathlib import Path +from typing import TYPE_CHECKING, Final, cast + +if TYPE_CHECKING: + import ctypes + from collections.abc import Callable, Generator + from contextlib import AbstractContextManager + + +def write_all(fd: int, data: bytes) -> None: + """ + Write the whole buffer to *fd*, looping over the short writes POSIX lets ``os.write`` make. + + A caller that stopped after one short write would leave a truncated marker behind. A peer that reads between two + writes sees a prefix of the record; each caller has to tolerate that. Lock records need not survive a power loss, so + we skip ``fsync``. + + :param fd: file descriptor open for writing. + :param data: bytes to write in full. + + :raises OSError: if a write reports zero progress before the record is complete. + + """ + remaining = memoryview(data) + while remaining: + if (written := os.write(fd, remaining)) == 0: + raise OSError(EIO, "os.write wrote 0 bytes before the record was complete") + remaining = remaining[written:] + + +def raise_on_not_writable_file(filename: str) -> None: + """ + Raise an exception if attempting to open the file for writing would fail. + + Separates files that can never be written from files that are writable but currently locked. + + :param filename: file to check + + :raises OSError: as if the file was opened for writing. + + """ + try: + # lstat, not stat: settles exists-and-writable in one syscall, and a hostile symlink at the lock path would + # make stat inspect the link target, letting an attacker turn a contended acquire into a misleading + # PermissionError / IsADirectoryError and probe that target's attributes. The real open passes O_NOFOLLOW and + # refuses the symlink anyway. + file_stat = os.lstat(filename) + except OSError: + return # does not exist, or an error the caller cannot act on + + if stat.S_ISDIR(file_stat.st_mode): + if sys.platform == "win32": # pragma: win32 cover + raise PermissionError(EACCES, "Permission denied", filename) + raise IsADirectoryError(EISDIR, "Is a directory", filename) # pragma: win32 no cover + + # No mtime guard: the old `if st_mtime != 0` skip covered NFS/Linux quirks where os.lstat returned an all-zero + # struct, which it no longer does. Skipping on mtime 0 let a read-only file or a directory at the lock path pass + # as missing, so acquire() blocked forever on an open that cannot succeed. + # Match open() credentials where supported; group permissions and ACLs can grant access without S_IWUSR. Do not + # follow a symlink, as the lstat above does not: one swapped in since would be judged by its target. + if not (file_stat.st_mode & stat.S_IWUSR) and not os.access( + filename, + os.W_OK, + effective_ids=os.access in os.supports_effective_ids, + follow_symlinks=os.access not in os.supports_follow_symlinks, + ): + try: + os.lstat(filename) + except FileNotFoundError: + # A holder may unlink its marker between lstat() and access(); let the caller attempt creation. + return + raise PermissionError(EACCES, "Permission denied", filename) + + +def ensure_directory_exists(filename: Path | str) -> None: + """ + Ensure the directory containing the file exists (create it if necessary). + + :param filename: file. + + """ + Path(filename).parent.mkdir(parents=True, exist_ok=True) + + +@contextmanager +def pin_inode(lock_file: str) -> Generator[int | None]: + """ + Keep the file at *lock_file* allocated for the length of the block and yield its inode number. + + An inode is the kernel's identity for a file, independent of its name, and its number names a file only while the + inode stays allocated. ext4 hands a freed number to the next file created. Once a stale marker is unlinked, the next + holder's marker can therefore carry the number a contender read a moment ago. A caller that reads the marker inside + the block, gives up unless it read the yielded inode, and breaks the marker before leaving can rely on the number. + + :param lock_file: path to the lock file about to be read and perhaps broken. + + :returns: the pinned inode number, or ``None`` where nothing can be pinned. Windows cannot rename a file this + process holds open, so it needs no pin. Without ``O_PATH`` a symlink or a socket cannot be opened. + + :raises FileNotFoundError: if the path is gone. + + """ + if sys.platform == "win32": # pragma: win32 cover + yield None + else: # pragma: win32 no cover + # O_PATH pins any node, a symlink or socket included, without read permission and without the filesystem seeing + # an open; elsewhere a nonblocking read-only open pins what it can. + flags: Final = getattr(os, "O_PATH", os.O_RDONLY | getattr(os, "O_NONBLOCK", 0)) | getattr(os, "O_NOFOLLOW", 0) + try: + fd = os.open(lock_file, flags) + except FileNotFoundError: + raise + except OSError: + yield None + return + try: + yield os.fstat(fd).st_ino + finally: + os.close(fd) + + +def remove_own_file(lock_file: str, fd: int) -> None: # pragma: win32 no cover + """ + Remove *lock_file* while it still names the file open as *fd*; the caller closes the descriptor afterwards. + + Where the platform has an atomic exchange, a stale break takes the file through it, so a peer's marker found at the + path goes back. Otherwise the path is unlinked by name if it still names this file. The open descriptor keeps the + inode allocated, so a matching number cannot belong to a successor's marker. + + :raises OSError: if the descriptor cannot be read or the unlink fails. + + """ + own: Final = os.fstat(fd) + with suppress(FileNotFoundError): + break_lock_file(lock_file, own.st_mtime, own.st_ino) + if os.path.samestat(os.lstat(lock_file), own): + Path(lock_file).unlink() + + +def foreign_directory_owner(directory: os.stat_result, parent: os.stat_result) -> int | None: + """ + Return the uid that owns *directory* when another account controls it under a sticky parent. + + A lock that keeps its ownership records in a directory relies on them staying in place. The sticky bit (a directory + flag that lets only a file's owner remove it) on a parent such as ``/tmp`` stops other accounts from renaming the + directory away. A directory that this account or the parent's owner owns is therefore safe there. One that another + account owns is outside the lock's control. Without the sticky bit, any process that can create the directory can + also rename this account's own one away, so a refusal there would change nothing. Linux applies the same rule for + ``fs.protected_regular``. + + :param directory: ``lstat`` of the directory found in place. + :param parent: ``stat`` of the directory that holds it. + + :returns: the owning uid when another account controls the directory, otherwise ``None``. + + """ + if sys.platform != "win32" and parent.st_mode & stat.S_ISVTX: # pragma: needs foreign-sticky-tmp + return None if directory.st_uid in {os.geteuid(), parent.st_uid} else directory.st_uid + return None + + +def break_lock_file(lock_file: str, mtime_before: float, ino_before: int) -> None: + """ + Remove the lock file a caller found stale with modification time *mtime_before* and inode *ino_before*. + + The caller keeps the judged file open until we return, through :func:`pin_inode` or its own descriptor, so no + other file can carry *ino_before* meanwhile. Right before breaking we ``lstat`` the lock again and leave it alone + if a peer recreated it since the caller judged it stale, as Mercurial re-reads a lock under its break lock. + + On Linux (``renameat2``) and macOS (``renamex_np``) the platform can exchange two names in one step. There we swap + the marker with a placeholder of our own, so the lock path never stands empty, and a contender that polls meanwhile + finds a file it cannot parse and waits. If the file we took is not the one judged stale, a peer recreated the lock, + and a second exchange puts it back. Otherwise we remove it and then our placeholder. Of several processes racing to + break the same lock, only one takes the stale file. + + Windows cannot exchange names, but it refuses to rename a marker its live holder keeps open. There we rename the + marker aside and, if it was recreated after all, link it back. ``link`` never replaces a name, so a marker a third + process created meanwhile keeps the path. Elsewhere without an exchange (NFS, SMB and other filesystems that refuse + it, platforms without the call) a rename would leave the path empty beside a possibly live holder. We then do not + move the marker, and the caller waits for ``timeout`` or ``break_lock()``. + + We compare inodes as well as modification times, because NFS and FAT store coarse modification times. The times are + compared for equality, since a file's owner can set any time. We ``lstat`` so a symlink a peer swaps in is never + followed. The break name carries a random token so no other process can guess it. + + :param lock_file: path to the lock file to break. + :param mtime_before: modification time the caller saw when it judged the lock stale. + :param ino_before: inode number the caller saw when it judged the lock stale. + + :raises OSError: if taking the marker fails, for example because the file vanished, another user owns it in a + sticky directory, or on Windows its holder keeps it open. + + """ + if (current := os.lstat(lock_file)).st_mtime != mtime_before or current.st_ino != ino_before: + return + break_path: Final[str] = f"{lock_file}.break.{os.getpid()}.{secrets.token_hex(16)}" + if exchange_supported(): # pragma: win32 no cover + _swap_out(lock_file, break_path, mtime_before, ino_before) + elif _MOVES_WITHOUT_EXCHANGE: # pragma: win32 cover + _break_by_rename(lock_file, break_path, mtime_before, ino_before) + + +def _swap_out(lock_file: str, break_path: str, mtime_before: float, ino_before: int) -> None: # pragma: win32 no cover + # An atomic exchange with a placeholder takes the marker without the lock path ever being empty. No contender can + # then create a marker while this one is judged, and a live marker goes back the same way. + fd: Final = os.open(break_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0), 0o600) + try: + try: + # Not a record any lock writes, so a contender that reads it waits, and one left by a crash ages out. The + # PID tells the contender that evicts it whose break names to search for a marker this process left. + write_all(fd, f"filelock/break\n{os.getpid()}\n".encode()) + exchanged = exchange_names(break_path, lock_file) + except OSError: + exchanged = False + if not exchanged: + Path(break_path).unlink() + return + if (grabbed := os.lstat(break_path)).st_mtime == mtime_before and grabbed.st_ino == ino_before: + Path(break_path).unlink() + # Our descriptor keeps the placeholder's inode allocated, so a matching number at the path is still ours. + if os.path.samestat(os.lstat(lock_file), os.fstat(fd)): + Path(lock_file).unlink() + else: + exchange_names(break_path, lock_file) + Path(break_path).unlink() + finally: + os.close(fd) + + +def _break_by_rename( + lock_file: str, break_path: str, mtime_before: float, ino_before: int +) -> None: # pragma: win32 cover + # Renaming the marker aside empties the path until a recreated live marker is put back. link never replaces a name, + # so a marker a third process created meanwhile keeps the path, and the live one waits under the break name. + # Without os.link, or where the filesystem refuses hard links, the live one waits there too. + with _pinned_for_rename(lock_file): + Path(lock_file).rename(break_path) + if (st_after := os.lstat(break_path)).st_mtime == mtime_before and st_after.st_ino == ino_before: + unlink_path(break_path) + elif stat.S_ISREG(st_after.st_mode) and hasattr(os, "link"): # pragma: needs hard-link + with suppress(OSError): + os.link(break_path, lock_file, follow_symlinks=False) + unlink_path(break_path) + + +if sys.platform == "win32": # pragma: win32 cover + + def _pinned_for_rename(path: str) -> AbstractContextManager[object]: + # A rename by path waits on a Level 1 or batch oplock holder until it answers or closes. Opening first with + # FILE_COMPLETE_IF_OPLOCKED turns that wait into BlockingIOError, and the open handle shares delete, so the + # rename then proceeds. Imported here because _windows imports this module. + from filelock._windows import open_for_delete_without_oplock_wait # ruff:ignore[import-outside-top-level] + + return os.fdopen(open_for_delete_without_oplock_wait(path), "rb") + + def unlink_path(path: str) -> None: + from filelock._windows import unlink_without_oplock_wait # ruff:ignore[import-outside-top-level] + + unlink_without_oplock_wait(path) + +else: # pragma: win32 no cover + + def unlink_path(path: str) -> None: + Path(path).unlink() + + +def exchange_supported() -> bool: + """Whether this platform has a call that swaps two names in one step; a filesystem may still refuse it.""" + return _rename_call() is not None + + +def exchange_names(source: str, target: str) -> bool: # pragma: win32 no cover + """ + Swap the names *source* and *target* in one step, so neither ever stands empty, where :func:`exchange_supported`. + + :returns: ``False`` where the filesystem cannot, so the caller falls back + + :raises OSError: for any other failure, such as a missing name or a denied rename + + """ + renamer, exchange_flag = cast("tuple[Callable[[bytes, bytes, int], int], int]", _rename_call()) + if not (error := renamer(os.fsencode(source), os.fsencode(target), exchange_flag)): + return True + # EXDEV: overlayfs and some FUSE filesystems refuse an exchange between layers. + if error in {EINVAL, ENOSYS, EXDEV, getattr(errno, "ENOTSUP", EINVAL), getattr(errno, "EOPNOTSUPP", EINVAL)}: + return False + raise OSError(error, os.strerror(error), source, None, target) + + +@cache +def _rename_call() -> tuple[Callable[[bytes, bytes, int], int], int] | None: + # The call returns 0 or the errno. Looked up on first use, since most processes never break a lock and ctypes is + # slow to import. None where ctypes is missing (GraalPy) or the platform has no exchange. + try: + import ctypes # ruff:ignore[import-outside-top-level] # only a stale break needs it + except ImportError: # pragma: no cover # GraalPy ships without ctypes + return None + if sys.platform in {"linux", "android"}: # pragma: linux cover + if (renameat2 := _renameat2(ctypes.CDLL(None, use_errno=True))) is None: + return None # pragma: no cover # an architecture the CI matrix does not run + return renameat2, 2 # RENAME_EXCHANGE in linux/fs.h + if sys.platform == "darwin": # pragma: darwin cover + renamex_np: Final = ctypes.CDLL(None, use_errno=True).renamex_np + renamex_np.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_uint] + renamex_np.restype = ctypes.c_int + + def call_renamex_np(source: bytes, target: bytes, flags: int) -> int: + return 0 if renamex_np(source, target, flags) == 0 else ctypes.get_errno() + + return call_renamex_np, 0x2 # RENAME_SWAP in sys/stdio.h + return None # pragma: linux no cover # pragma: android no cover # pragma: darwin no cover + + +def _renameat2(libc: ctypes.CDLL) -> Callable[[bytes, bytes, int], int] | None: # pragma: linux cover + # Reached through syscall(2) rather than the C library's wrapper. glibc 2.28, musl 1.2.2 and bionic API 30 added + # the wrapper, and Termux ships an older bionic; the kernel has had the call since 3.15. The number is the + # architecture's own, so an architecture outside the table gets no exchange rather than another call. + import ctypes # ruff:ignore[import-outside-top-level] # only a stale break needs it + + numbers: Final = { + "x86_64": 316, + "i386": 353, + "i686": 353, + "armv7l": 382, + "armv8l": 382, + "aarch64": 276, + "riscv64": 276, + "loongarch64": 276, + "ppc64le": 357, + "s390x": 347, + } + if (number := numbers.get(platform.machine())) is None: + return None # pragma: no cover # an architecture the CI matrix does not run + syscall: Final = libc.syscall + syscall.restype = ctypes.c_long + at_fdcwd: Final = -100 + + def call_renameat2(source: bytes, target: bytes, flags: int) -> int: + arguments: Final = ( + ctypes.c_long(number), + ctypes.c_int(at_fdcwd), + ctypes.c_char_p(source), + ctypes.c_int(at_fdcwd), + ctypes.c_char_p(target), + ctypes.c_uint(flags), + ) + return 0 if syscall(*arguments) == 0 else ctypes.get_errno() + + return call_renameat2 + + +def touch(name: str, *, fd: int) -> None: + # Prefer the already-open, already-verified fd so a peer that swaps a symlink or a different file in at the + # path after our O_NOFOLLOW read cannot redirect the touch: utime then targets the inode behind the fd. + # Where the platform cannot utime an fd, fall back to a path-based touch that still refuses to follow a + # symlink where supported, matching the O_NOFOLLOW reads used elsewhere here. + if _SUPPORTS_UTIME_FD: # pragma: needs utime-fd + os.utime(fd, None) + return + os.utime(name, None, follow_symlinks=not _SUPPORTS_UTIME_NOFOLLOW) # pragma: lacks utime-fd + + +# Windows refuses to rename a marker its live holder keeps open, so renaming aside only moves a marker nobody holds. +# Elsewhere without an exchange a contender could take the emptied path beside a live holder, so the marker stays and +# the lock counts as held. +_MOVES_WITHOUT_EXCHANGE: Final[bool] = sys.platform == "win32" +# Retargeting os.utime to an open fd lets a heartbeat refresh the exact inode it verified instead of whatever the +# pathname now names. +_SUPPORTS_UTIME_FD: Final[bool] = sys.platform != "win32" and os.utime in os.supports_fd +# os.utime follows symlinks unless told not to; not every platform can refuse the follow, so probe support. +_SUPPORTS_UTIME_NOFOLLOW: Final[bool] = os.utime in os.supports_follow_symlinks + + +__all__ = [ + "break_lock_file", + "ensure_directory_exists", + "exchange_names", + "exchange_supported", + "foreign_directory_owner", + "pin_inode", + "raise_on_not_writable_file", + "remove_own_file", + "touch", + "unlink_path", + "write_all", +] diff --git a/hug/lib/python3.12/site-packages/filelock/_windows.py b/hug/lib/python3.12/site-packages/filelock/_windows.py new file mode 100644 index 0000000000000000000000000000000000000000..0258961a7ec86ae6fd2938890eab949e9a783a6c --- /dev/null +++ b/hug/lib/python3.12/site-packages/filelock/_windows.py @@ -0,0 +1,780 @@ +from __future__ import annotations + +__lazy_modules__ = ["filelock._util", "msvcrt", "struct"] + +import os +import struct +import sys +import time +from contextlib import contextmanager, suppress +from errno import ENOENT, ENOTDIR, EWOULDBLOCK +from functools import cache +from typing import TYPE_CHECKING, Final, cast + +from filelock._api import BaseFileLock +from filelock._util import raise_on_not_writable_file + +if TYPE_CHECKING: + from collections.abc import Generator + +if sys.platform == "win32": # pragma: win32 cover + import ctypes + import msvcrt + from ctypes import wintypes + + _GENERIC_READ: Final[int] = 0x80000000 + _GENERIC_WRITE: Final[int] = 0x40000000 + _SYNCHRONIZE: Final[int] = 0x00100000 + _DELETE: Final[int] = 0x00010000 + _DESIRED_ACCESS: Final[int] = _GENERIC_READ | _GENERIC_WRITE | _SYNCHRONIZE + _FILE_SHARE_READ_WRITE: Final[int] = ( + 0x00000001 | 0x00000002 + ) # read | write; matches os.open (_SH_DENYNO), no delete + _FILE_OPEN: Final[int] = 1 # open the file only if it exists; the NtCreateFile OPEN_EXISTING + _FILE_OPEN_IF: Final[int] = 3 # open the file if it exists, create it otherwise; the NtCreateFile OPEN_ALWAYS + # Attribute-only access skips share-mode checks, so the probe sees a name's deletion state rather than a reader. + _FILE_READ_ATTRIBUTES: Final[int] = 0x00000080 + _FILE_SHARE_ALL: Final[int] = 0x00000001 | 0x00000002 | 0x00000004 # read | write | delete + _FILE_ATTRIBUTE_READONLY: Final[int] = 0x00000001 + _FILE_ATTRIBUTE_NORMAL: Final[int] = 0x00000080 + _FILE_ATTRIBUTE_REPARSE_POINT: Final[int] = 0x00000400 + # CreateOptions: keep the handle synchronous (the CRT and msvcrt.locking rely on a maintained file position), + # refuse a directory, return at once instead of waiting for an oplock holder to answer the break, and open a + # reparse point rather than following it so the check below acts on the link itself. + _FILE_SYNCHRONOUS_IO_NONALERT: Final[int] = 0x00000020 + _FILE_NON_DIRECTORY_FILE: Final[int] = 0x00000040 + _FILE_DELETE_ON_CLOSE: Final[int] = 0x00001000 + _FILE_COMPLETE_IF_OPLOCKED: Final[int] = 0x00000100 + _FILE_OPEN_REPARSE_POINT: Final[int] = 0x00200000 + _CREATE_OPTIONS: Final[int] = ( + _FILE_SYNCHRONOUS_IO_NONALERT | _FILE_NON_DIRECTORY_FILE | _FILE_COMPLETE_IF_OPLOCKED | _FILE_OPEN_REPARSE_POINT + ) + # Listing access takes part in share-mode checks, unlike attribute-only access, so a handle that holds it without + # FILE_SHARE_DELETE blocks renames and removals of the directory until it closes. + _FILE_LIST_DIRECTORY: Final[int] = 0x00000001 + _FILE_DIRECTORY_FILE: Final[int] = 0x00000001 + _DIRECTORY_OPTIONS: Final[int] = _FILE_SYNCHRONOUS_IO_NONALERT | _FILE_DIRECTORY_FILE | _FILE_OPEN_REPARSE_POINT + _OBJ_CASE_INSENSITIVE: Final[int] = 0x00000040 # Win32 name lookups are case-insensitive + _OWNER_WRITE: Final[int] = 0o200 + _GROUP_AND_OTHERS: Final[int] = 0o077 + # The DACL os.mkdir(mode=0o700) applies, less the inheritance flags a file has no use for: protected, so the + # directory's inheritable entries stay out, with full access for SYSTEM, Administrators and the owner. + _OWNER_ONLY_SDDL: Final[str] = "D:P(A;;FA;;;SY)(A;;FA;;;BA)(A;;FA;;;OW)" + _SDDL_REVISION_1: Final[int] = 1 + _INVALID_HANDLE_VALUE: Final[int | None] = ctypes.c_void_p(-1).value + + # LockFileEx locks a byte range at an offset carried in OVERLAPPED, independent of the descriptor's file position. + # msvcrt.locking starts at the current position instead, so a metadata write between lock and unlock could shift + # the byte a later unlock targets; the explicit offset removes that hazard for both the path lock and #608's + # descriptor lock. + _LOCKFILE_FAIL_IMMEDIATELY: Final[int] = 0x00000001 + _LOCKFILE_EXCLUSIVE_LOCK: Final[int] = 0x00000002 + _ERROR_ACCESS_DENIED: Final[int] = 5 + _ERROR_LOCK_VIOLATION: Final[int] = 33 # another handle holds the byte range + _ERROR_NOT_LOCKED: Final[int] = 158 # the byte range is not locked + + # NtCreateFile returns the raw NTSTATUS as its value, where CreateFileW collapses several of these into one + # ERROR_ACCESS_DENIED. Telling them apart is the point (#604): a name pending deletion or a share conflict is + # transient and worth a retry, a real access denial is not. + _STATUS_SUCCESS: Final[int] = 0x00000000 + # A success code that comes with a handle: another handle's oplock is breaking and its holder has not answered yet. + _STATUS_OPLOCK_BREAK_IN_PROGRESS: Final[int] = 0x00000108 + _STATUS_ACCESS_DENIED: Final[int] = 0xC0000022 + _STATUS_OBJECT_NAME_NOT_FOUND: Final[int] = 0xC0000034 + _STATUS_OBJECT_PATH_NOT_FOUND: Final[int] = 0xC000003A + _STATUS_SHARING_VIOLATION: Final[int] = 0xC0000043 + _STATUS_DELETE_PENDING: Final[int] = 0xC0000056 + #: Probes, 2 ms apart, for transient STATUS_ACCESS_DENIED while a peer unlinks the name (#604). A count ensures + #: every probe runs even when the process stalls; oplock breaks use the caller's wait instead. + _ACCESS_DENIED_PROBES: Final[int] = 250 + _ACCESS_DENIED_GRACE: Final[float] = 0.5 + _ACCESS_DENIED_RETRY: Final[float] = 0.002 + + _ntdll: Final[ctypes.WinDLL] = ctypes.WinDLL("ntdll") + _kernel32: Final[ctypes.WinDLL] = ctypes.WinDLL("kernel32", use_last_error=True) + _advapi32: Final[ctypes.WinDLL] = ctypes.WinDLL("advapi32", use_last_error=True) + + class _UNICODE_STRING(ctypes.Structure): # ruff:ignore[invalid-class-name] # mirrors the Win32 struct name + _fields_ = ( + ("Length", wintypes.USHORT), # byte length, not character count + ("MaximumLength", wintypes.USHORT), + ("Buffer", wintypes.LPWSTR), + ) + + class _OBJECT_ATTRIBUTES(ctypes.Structure): # ruff:ignore[invalid-class-name] # mirrors the Win32 struct name + _fields_ = ( + ("Length", wintypes.ULONG), + ("RootDirectory", wintypes.HANDLE), + ("ObjectName", ctypes.POINTER(_UNICODE_STRING)), + ("Attributes", wintypes.ULONG), + ("SecurityDescriptor", ctypes.c_void_p), + ("SecurityQualityOfService", ctypes.c_void_p), + ) + + class _IO_STATUS_BLOCK(ctypes.Structure): # ruff:ignore[invalid-class-name] # mirrors the Win32 struct name + _fields_ = ( + ("Status", ctypes.c_void_p), # a union of NTSTATUS and PVOID, so it is pointer-sized + ("Information", ctypes.c_void_p), + ) + + class _SECURITY_ATTRIBUTES(ctypes.Structure): # ruff:ignore[invalid-class-name] # mirrors the Win32 struct name + _fields_ = ( + ("nLength", wintypes.DWORD), + ("lpSecurityDescriptor", ctypes.c_void_p), + ("bInheritHandle", wintypes.BOOL), + ) + + class _OVERLAPPED(ctypes.Structure): # mirrors the Win32 struct name + _fields_ = ( + ("Internal", ctypes.c_void_p), # ULONG_PTR: pointer-sized, not DWORD, or the x64 layout corrupts Offset + ("InternalHigh", ctypes.c_void_p), + ("Offset", wintypes.DWORD), # the DUMMYUNIONNAME struct, flattened: low 32 bits of the byte offset + ("OffsetHigh", wintypes.DWORD), + ("hEvent", wintypes.HANDLE), + ) + + class _BY_HANDLE_FILE_INFORMATION(ctypes.Structure): # ruff:ignore[invalid-class-name] # mirrors the Win32 struct name + _fields_ = ( + ("dwFileAttributes", wintypes.DWORD), + ("ftCreationTime", wintypes.FILETIME), + ("ftLastAccessTime", wintypes.FILETIME), + ("ftLastWriteTime", wintypes.FILETIME), + ("dwVolumeSerialNumber", wintypes.DWORD), + ("nFileSizeHigh", wintypes.DWORD), + ("nFileSizeLow", wintypes.DWORD), + ("nNumberOfLinks", wintypes.DWORD), + ("nFileIndexHigh", wintypes.DWORD), + ("nFileIndexLow", wintypes.DWORD), + ) + + _ntdll.NtCreateFile.restype = wintypes.LONG # NTSTATUS + _ntdll.NtCreateFile.argtypes = [ + ctypes.POINTER(wintypes.HANDLE), + wintypes.DWORD, + ctypes.POINTER(_OBJECT_ATTRIBUTES), + ctypes.POINTER(_IO_STATUS_BLOCK), + ctypes.POINTER(ctypes.c_longlong), # PLARGE_INTEGER AllocationSize + wintypes.ULONG, + wintypes.ULONG, + wintypes.ULONG, + wintypes.ULONG, + ctypes.c_void_p, + wintypes.ULONG, + ] + _ntdll.RtlDosPathNameToNtPathName_U_WithStatus.restype = wintypes.LONG # NTSTATUS + _ntdll.RtlDosPathNameToNtPathName_U_WithStatus.argtypes = [ + wintypes.LPCWSTR, + ctypes.POINTER(_UNICODE_STRING), + ctypes.c_void_p, + ctypes.c_void_p, + ] + _ntdll.RtlFreeUnicodeString.restype = None + _ntdll.RtlFreeUnicodeString.argtypes = [ctypes.POINTER(_UNICODE_STRING)] + _ntdll.RtlNtStatusToDosError.restype = wintypes.ULONG + _ntdll.RtlNtStatusToDosError.argtypes = [wintypes.LONG] + + _kernel32.CloseHandle.argtypes = [wintypes.HANDLE] + _kernel32.CloseHandle.restype = wintypes.BOOL + _kernel32.GetFileInformationByHandle.argtypes = [wintypes.HANDLE, ctypes.POINTER(_BY_HANDLE_FILE_INFORMATION)] + _kernel32.GetFileInformationByHandle.restype = wintypes.BOOL + _kernel32.LockFileEx.argtypes = [ + wintypes.HANDLE, + wintypes.DWORD, + wintypes.DWORD, + wintypes.DWORD, + wintypes.DWORD, + ctypes.POINTER(_OVERLAPPED), + ] + _kernel32.LockFileEx.restype = wintypes.BOOL + _kernel32.UnlockFileEx.argtypes = [ + wintypes.HANDLE, + wintypes.DWORD, + wintypes.DWORD, + wintypes.DWORD, + ctypes.POINTER(_OVERLAPPED), + ] + _kernel32.UnlockFileEx.restype = wintypes.BOOL + _advapi32.ConvertStringSecurityDescriptorToSecurityDescriptorW.argtypes = [ + wintypes.LPCWSTR, + wintypes.DWORD, + ctypes.POINTER(ctypes.c_void_p), + ctypes.POINTER(wintypes.ULONG), + ] + _advapi32.ConvertStringSecurityDescriptorToSecurityDescriptorW.restype = wintypes.BOOL + _advapi32.GetNamedSecurityInfoW.argtypes = [ + wintypes.LPCWSTR, + ctypes.c_int, + wintypes.DWORD, + ctypes.POINTER(ctypes.c_void_p), + ctypes.POINTER(ctypes.c_void_p), + ctypes.POINTER(ctypes.c_void_p), + ctypes.POINTER(ctypes.c_void_p), + ctypes.POINTER(ctypes.c_void_p), + ] + _advapi32.GetNamedSecurityInfoW.restype = wintypes.DWORD + _advapi32.OpenThreadToken.argtypes = [ + wintypes.HANDLE, + wintypes.DWORD, + wintypes.BOOL, + ctypes.POINTER(wintypes.HANDLE), + ] + _advapi32.OpenThreadToken.restype = wintypes.BOOL + _advapi32.OpenProcessToken.argtypes = [wintypes.HANDLE, wintypes.DWORD, ctypes.POINTER(wintypes.HANDLE)] + _advapi32.OpenProcessToken.restype = wintypes.BOOL + _advapi32.GetTokenInformation.argtypes = [ + wintypes.HANDLE, + ctypes.c_int, + ctypes.c_void_p, + wintypes.DWORD, + ctypes.POINTER(wintypes.DWORD), + ] + _advapi32.GetTokenInformation.restype = wintypes.BOOL + _kernel32.GetCurrentProcess.restype = wintypes.HANDLE + _kernel32.GetCurrentThread.restype = wintypes.HANDLE + _kernel32.LocalFree.argtypes = [ctypes.c_void_p] + _kernel32.LocalFree.restype = ctypes.c_void_p + _kernel32.CreateDirectoryW.argtypes = [wintypes.LPCWSTR, ctypes.POINTER(_SECURITY_ATTRIBUTES)] + _kernel32.CreateDirectoryW.restype = wintypes.BOOL + _kernel32.CreateFileW.argtypes = [ + wintypes.LPCWSTR, + wintypes.DWORD, + wintypes.DWORD, + ctypes.c_void_p, + wintypes.DWORD, + wintypes.DWORD, + wintypes.HANDLE, + ] + _kernel32.CreateFileW.restype = ctypes.c_void_p + + def _lock_fd_nonblocking(fd: int) -> bool: + # One nonblocking exclusive LockFileEx attempt shared by WindowsFileLock and lock_descriptor, over the one-byte + # range at offset 0. True on acquisition, False on contention, raise otherwise. The caller owns fd; the handle + # from get_osfhandle belongs to the CRT descriptor and must not be closed here. + overlapped = _OVERLAPPED() # zero-initialized, so Offset/OffsetHigh/hEvent are 0 + flags = _LOCKFILE_EXCLUSIVE_LOCK | _LOCKFILE_FAIL_IMMEDIATELY + if _kernel32.LockFileEx(msvcrt.get_osfhandle(fd), flags, 0, 1, 0, ctypes.byref(overlapped)): + return True + err = ctypes.get_last_error() + if err == _ERROR_LOCK_VIOLATION: + return False + # A non-contention LockFileEx failure is not reproducible in-process. + raise ctypes.WinError(err) # pragma: no cover + + def _unlock_fd(fd: int) -> None: + overlapped = _OVERLAPPED() # the same offset 0 and one-byte length the lock used + if ( # pragma: no cover # only an interrupted earlier release leaves our own range unlocked + not _kernel32.UnlockFileEx(msvcrt.get_osfhandle(fd), 0, 1, 0, ctypes.byref(overlapped)) + and (err := ctypes.get_last_error()) != _ERROR_NOT_LOCKED + ): + raise ctypes.WinError(err) + + class WindowsFileLock(BaseFileLock): + """ + Uses ``LockFileEx`` to hard lock a byte range of the lock file on Windows systems. + + Lock file cleanup: Windows attempts to delete the lock file after release, but deletion is + not guaranteed in multi-threaded scenarios where another thread holds an open handle. The lock + file may persist on disk, which does not affect lock correctness. + """ + + def _acquire(self) -> None: + raise_on_not_writable_file(self._lock_path) + + # The reparse test is bound to the opened handle, so a symlink or junction swapped in cannot defeat it + # through a check-then-open TOCTOU race. + try: + fd = _open_non_reparse_fd(self._lock_path, self._open_mode()) + except BlockingIOError: + return # an oplock holder has not answered its break; let the retry loop keep its deadline + if fd is None: + return # open contention (share conflict or a name pending deletion); let the retry loop try again + try: + locked = _lock_fd_nonblocking(fd) + if locked: + self._mark_descriptor_owned(fd) + except BaseException: + # Closing drops the byte-range lock, so a hold already recorded for this descriptor must go with it. + self._mark_descriptor_released() + os.close(fd) + raise + if not locked: + os.close(fd) # another holder owns the byte-range lock; let the retry loop try again + + def _release(self) -> None: + fd = cast("int", self._context.lock_file_fd) + # Retain the descriptor until the OS unlock succeeds: if UnlockFileEx raises, the byte-range lock is still + # held, so is_locked must keep reporting held rather than losing the fd. Only after the unlock commits do + # close and unlink run as post-unlock cleanup; their failure cannot make the lock held again. + _unlock_fd(fd) + self._mark_descriptor_released() + self._close_released_fd(fd, default_suppresses=False) + if not self._preserve_lock_file: # preserve_lock_file keeps a stable file identity for the caller (#605) + with suppress(OSError): + unlink_without_oplock_wait(self._lock_path) + + def _open_non_reparse_fd(path: str, mode: int, *, oplock_wait: float = _ACCESS_DENIED_GRACE) -> int | None: + """ + Open *path* for locking while refusing reparse points, bound to the handle actually locked. + + The file is opened through ``NtCreateFile`` with ``FILE_OPEN_REPARSE_POINT`` so a symlink or junction planted + at the path is not followed, and the reparse decision is read from *that* handle via + ``GetFileInformationByHandle`` rather than from a prior pathname query. Reading the held handle closes the + check-then-open race: an attacker cannot swap the path between validation and use because both act on the same + handle. Share mode omits delete so a peer cannot unlink or rename the file out from under a live holder, + matching ``os.open``'s ``_SH_DENYNO``. + + ``NtCreateFile`` is used instead of ``CreateFileW`` because its return value carries the raw ``NTSTATUS``. + Windows collapses a transient delete-pending name and a permanent access denial into the same Win32 + ``ERROR_ACCESS_DENIED``; the status keeps them apart, so a real denial fails fast instead of spinning until the + caller's timeout (#604). + + The reparse option only guards the final path component; Windows still follows reparse points in intermediate + directories. This assumes the lock file sits in a lock directory untrusted users cannot modify. A path with + attacker-controlled parent directories would need component-by-component handle validation. + + :param path: the lock file path. + :param mode: the permission mode; as ``os.open`` does on Windows, a cleared owner-write bit creates the file + read-only. A mode without group and other bits creates it with an owner-only DACL (discretionary access + control list, the Windows permission list); any other mode has no Windows counterpart and leaves the file + the entries its directory hands down. Both only take effect when the file is created, not when an existing + one is opened. + + :param oplock_wait: seconds to wait for the holder of an oplock on the file to answer its break. + + :returns: a file descriptor owning the opened handle, or ``None`` on a sharing violation or a delete-pending + name, which the caller should treat as contention and retry. + + :raises BlockingIOError: if an oplock break stays unanswered for *oplock_wait* seconds. + :raises OSError: if the path resolves to a reparse point, or the open fails for any other reason, raised with + the Win32 error the status maps to. An access denial is raised only once it outlasts a run of probes, since + a peer unlinking the file as it releases can make the open answer ``STATUS_ACCESS_DENIED`` for a moment. + + """ + # Emit the audit event os.open would, so consumers watching "open" still see the path-level open and can veto. + sys.audit("open", path, None, os.O_RDWR | os.O_CREAT) + read_only = not mode & _OWNER_WRITE + owner_only = not mode & 0o077 + handle, status = _nt_open(path, read_only=read_only, owner_only=owner_only) + denied_probes = 0 + oplock_deadline = time.monotonic() + oplock_wait + while True: + if status == _STATUS_ACCESS_DENIED: + if denied_probes >= _ACCESS_DENIED_PROBES: + break + denied_probes += 1 + elif status == _STATUS_OPLOCK_BREAK_IN_PROGRESS: + if time.monotonic() >= oplock_deadline: + break + else: + break + time.sleep(_ACCESS_DENIED_RETRY) + handle, status = _nt_open(path, read_only=read_only, owner_only=owner_only) + if status == _STATUS_OPLOCK_BREAK_IN_PROGRESS: + msg = "oplock break in progress" + raise BlockingIOError(EWOULDBLOCK, msg, path) + if status != _STATUS_SUCCESS: + if status in {_STATUS_SHARING_VIOLATION, _STATUS_DELETE_PENDING}: + return None + raise _status_error(status, path) + + return _descriptor_for_non_reparse(handle, path, os.O_RDWR) + + def open_read_without_oplock_wait(path: str) -> int: + """ + Open *path* read-only and return a descriptor, without waiting for an oplock holder to answer its break. + + ``os.open`` waits inside the system call until the holder of an oplock on the file answers or closes its handle, + so no timeout or ``blocking=False`` ends the wait. A reparse point at *path* is refused rather than followed. + + :param path: the file to read. + + :returns: a read-only file descriptor. + + :raises BlockingIOError: if an oplock break is in progress, which callers treat as contention. + :raises FileNotFoundError: if *path* does not exist. + :raises OSError: if the open fails for any other reason, or *path* is a reparse point. + + """ + sys.audit("open", path, None, os.O_RDONLY) + return _open_existing_without_oplock_wait(path, _GENERIC_READ | _SYNCHRONIZE, os.O_RDONLY) + + def open_for_delete_without_oplock_wait(path: str) -> int: + """ + Open *path* with delete access and return a descriptor, without waiting for an oplock holder to answer. + + The open handle shares read, write and delete, so a rename of *path* can proceed while the caller keeps it. + A holder that took an oplock through another handle is asked to answer by this open, which then raises, so the + caller skips the rename instead of waiting for it. + + :param path: the file to rename. + + :returns: a file descriptor owning the handle; the caller closes it. + + :raises BlockingIOError: if an oplock break is in progress, which callers treat as contention. + :raises FileNotFoundError: if *path* does not exist. + :raises OSError: if the open fails for any other reason, or *path* is a reparse point. + + """ + return _open_existing_without_oplock_wait(path, _DELETE | _FILE_READ_ATTRIBUTES | _SYNCHRONIZE, os.O_RDONLY) + + def unlink_without_oplock_wait(path: str) -> None: + """ + Delete *path* through a handle that does not wait for an oplock holder to answer its break. + + ``os.unlink`` waits like ``os.open`` does. Here an unanswered break leaves the file in place, which callers + accept since deleting a lock file is best effort. A symlink at *path* is deleted as a link. + + :param path: the file to delete. + + :raises FileNotFoundError: if *path* does not exist. + :raises OSError: if the open fails for any other reason, such as a sharing violation. + + """ + sys.audit("os.remove", path, None) + handle, status = _nt_create( + path, + _DELETE | _SYNCHRONIZE, + _FILE_ATTRIBUTE_NORMAL, + _FILE_SHARE_ALL, + _FILE_OPEN, + options=_CREATE_OPTIONS | _FILE_DELETE_ON_CLOSE, + ) + if status == _STATUS_SUCCESS: + _kernel32.CloseHandle(handle) # deletes the file + elif status != _STATUS_OPLOCK_BREAK_IN_PROGRESS: + raise _status_error(status, path) + + def _open_existing_without_oplock_wait(path: str, access: int, flags: int) -> int: + handle, status = _nt_create(path, access, _FILE_ATTRIBUTE_NORMAL, _FILE_SHARE_ALL, _FILE_OPEN) + if status != _STATUS_SUCCESS: + raise _status_error(status, path) + return _descriptor_for_non_reparse(handle, path, flags) + + def _status_error(status: int, path: str) -> OSError: + if status == _STATUS_OPLOCK_BREAK_IN_PROGRESS: + return BlockingIOError(EWOULDBLOCK, "oplock break in progress", path) + if status in {_STATUS_OBJECT_NAME_NOT_FOUND, _STATUS_OBJECT_PATH_NOT_FOUND}: + return FileNotFoundError(ENOENT, os.strerror(ENOENT), path) + winerror = _ntdll.RtlNtStatusToDosError(status) + return OSError(None, ctypes.FormatError(winerror).strip(), path, winerror) + + def _descriptor_for_non_reparse(handle: int, path: str, flags: int) -> int: + info = _BY_HANDLE_FILE_INFORMATION() + # Querying an open handle we just created does not fail. + if not _kernel32.GetFileInformationByHandle(handle, ctypes.byref(info)): # pragma: no cover + err = ctypes.get_last_error() + _kernel32.CloseHandle(handle) + raise ctypes.WinError(err) + if info.dwFileAttributes & _FILE_ATTRIBUTE_REPARSE_POINT: + _kernel32.CloseHandle(handle) + msg = f"Lock file is a reparse point (symlink/junction): {path}" + raise OSError(msg) + + try: + # O_NOINHERIT mirrors os.open on Windows: the lock fd must not leak into child processes. + return msvcrt.open_osfhandle(handle, flags | os.O_NOINHERIT) + except BaseException: # pragma: no cover # open_osfhandle audits too; a hook raising must not leak the handle + _kernel32.CloseHandle(handle) + raise + + def _open_deletable_for_reading(path: str) -> int: + """ + Open *path* read-only without blocking a concurrent unlink and without following a reparse point. + + ``os.open`` omits ``FILE_SHARE_DELETE``, so while a process has a file open that way, a delete of it fails with + a sharing violation. Every contender reads a strict claim and its owner unlinks it, so a reader that keeps the + claim open past the owner's retries makes the release fail. + """ + sys.audit("open", path, None, os.O_RDONLY) + handle, status = _nt_create(path, _GENERIC_READ | _SYNCHRONIZE, 0, _FILE_SHARE_ALL, _FILE_OPEN) + if status != _STATUS_SUCCESS: + winerror = _ntdll.RtlNtStatusToDosError(status) + raise OSError(None, ctypes.FormatError(winerror).strip(), path, winerror) + try: + return msvcrt.open_osfhandle(handle, os.O_RDONLY | os.O_NOINHERIT) + except BaseException: # pragma: no cover # open_osfhandle audits too; a hook raising must not leak the handle + _kernel32.CloseHandle(handle) + raise + + def _pin_directory(path: str) -> int: + """ + Open the directory at *path* so that no process can rename or remove it until :func:`_unpin_directory`. + + Windows has no ``dir_fd``, so an operation resolves every path component by name again. A handle on each + directory of a record path, opened without following a reparse point, keeps that name bound to the directory + checked here until the operation ends. + + :raises NotADirectoryError: when *path* is a junction, a symlink or anything other than a directory. + """ + sys.audit("open", path, None, os.O_RDONLY) + # GetFileInformationByHandle needs FILE_READ_ATTRIBUTES; listing access is what takes part in share checks. + access = _FILE_LIST_DIRECTORY | _FILE_READ_ATTRIBUTES | _SYNCHRONIZE + handle, status = _nt_create(path, access, 0, _FILE_SHARE_READ_WRITE, _FILE_OPEN, _DIRECTORY_OPTIONS) + if status != _STATUS_SUCCESS: + winerror = _ntdll.RtlNtStatusToDosError(status) + raise OSError(None, ctypes.FormatError(winerror).strip(), path, winerror) + info = _BY_HANDLE_FILE_INFORMATION() + # Querying an open handle we just created does not fail. + if not _kernel32.GetFileInformationByHandle(handle, ctypes.byref(info)): # pragma: no cover + error = ctypes.get_last_error() + _kernel32.CloseHandle(handle) + raise ctypes.WinError(error) + if info.dwFileAttributes & _FILE_ATTRIBUTE_REPARSE_POINT: + _kernel32.CloseHandle(handle) + raise NotADirectoryError(ENOTDIR, "is a junction or symlink, not a directory", path) + return handle + + def _unpin_directory(handle: int) -> None: + _kernel32.CloseHandle(handle) + + def _is_unlinked(path: str) -> bool: + """ + Report whether *path* is gone or pending deletion, which ``os.open`` reports as the same EACCES as a denial. + + NTFS keeps an unlinked name listed, refusing every open with ``STATUS_DELETE_PENDING``, until the last handle + to the file closes. Another process holding such a handle, such as an antivirus scan, can keep it that way for + longer than any retry grace, yet the name's owner has already removed it. + """ + handle, status = _nt_create(path, _FILE_READ_ATTRIBUTES | _SYNCHRONIZE, 0, _FILE_SHARE_ALL, _FILE_OPEN) + if handle: + _kernel32.CloseHandle(handle) + return status in {_STATUS_OBJECT_NAME_NOT_FOUND, _STATUS_DELETE_PENDING} + + def _nt_open(path: str, *, read_only: bool, owner_only: bool) -> tuple[int, int]: + """ + Open or create *path* for locking through ``NtCreateFile`` and return ``(handle, status)``. + + Without *owner_only* a file this call creates gets no DACL of its own and inherits its directory's ACL. + """ + attributes = _FILE_ATTRIBUTE_READONLY if read_only else _FILE_ATTRIBUTE_NORMAL + descriptor = _owner_only_descriptor() if owner_only else None + return _nt_create( + path, _DESIRED_ACCESS, attributes, _FILE_SHARE_READ_WRITE, _FILE_OPEN_IF, descriptor=descriptor + ) + + @cache + def _owner_only_descriptor() -> ctypes.c_void_p: + # The DACL os.mkdir(mode=0o700) applies, minus the directory-only inheritance flags: protected (P), so the + # directory's inheritable entries stay out, with full access (FA) for SYSTEM, Administrators and the owner. + # Converted once and never freed: the open retries every few milliseconds while it waits. + descriptor = ctypes.c_void_p() + if not _advapi32.ConvertStringSecurityDescriptorToSecurityDescriptorW( + "D:P(A;;FA;;;SY)(A;;FA;;;BA)(A;;FA;;;OW)", 1, ctypes.byref(descriptor), None + ): # pragma: no cover # a constant string converts + raise ctypes.WinError(ctypes.get_last_error()) + return descriptor + + def _nt_create( # ruff:ignore[too-many-arguments, too-many-positional-arguments] # NtCreateFile's own knobs + path: str, + access: int, + attributes: int, + share: int, + disposition: int, + options: int = _CREATE_OPTIONS, + *, + descriptor: ctypes.c_void_p | None = None, + ) -> tuple[int, int]: + """ + Open *path* through ``NtCreateFile`` and return ``(handle, status)``. + + ``RtlDosPathNameToNtPathName_U_WithStatus`` translates the Win32 path to the NT namespace, handling relative, + drive, UNC and extended-length path forms as Win32 itself would, and allocates a buffer that + ``RtlFreeUnicodeString`` releases. The handle is ``0`` unless the status is ``STATUS_SUCCESS``: one that comes + with ``STATUS_OPLOCK_BREAK_IN_PROGRESS`` is closed here, since the file is not ours until the oplock holder + answers. + """ + nt_name = _UNICODE_STRING() + status = _ntdll.RtlDosPathNameToNtPathName_U_WithStatus(path, ctypes.byref(nt_name), None, None) & 0xFFFFFFFF + if status != _STATUS_SUCCESS: + return 0, status + try: + object_attributes = _OBJECT_ATTRIBUTES() + object_attributes.Length = ctypes.sizeof(_OBJECT_ATTRIBUTES) + object_attributes.ObjectName = ctypes.pointer(nt_name) + object_attributes.Attributes = _OBJ_CASE_INSENSITIVE + object_attributes.SecurityDescriptor = descriptor + handle = wintypes.HANDLE() + io_status = _IO_STATUS_BLOCK() + status = ( + _ntdll.NtCreateFile( + ctypes.byref(handle), + access, + ctypes.byref(object_attributes), + ctypes.byref(io_status), + None, + attributes, + share, + disposition, + options, + None, + 0, + ) + & 0xFFFFFFFF + ) + finally: + _ntdll.RtlFreeUnicodeString(ctypes.byref(nt_name)) + if status == _STATUS_OPLOCK_BREAK_IN_PROGRESS: + _kernel32.CloseHandle(handle) + if status != _STATUS_SUCCESS: + return 0, status + return handle.value or 0, status + + def _directory_is_private(path: str) -> bool: + """ + Report whether only the current user, ``SYSTEM`` and ``Administrators`` can create or remove names in *path*. + + A directory whose owner is anyone else can have its ACL rewritten by that owner. Entries that only a child + inherits count too, since the DACL says nothing more precise about who may write. + """ + owner, dacl, descriptor = ctypes.c_void_p(), ctypes.c_void_p(), ctypes.c_void_p() + error = _advapi32.GetNamedSecurityInfoW( + path, + 1, # SE_FILE_OBJECT + 0x1 | 0x4, # OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION + ctypes.byref(owner), + None, + ctypes.byref(dacl), + None, + ctypes.byref(descriptor), + ) + if error == _ERROR_ACCESS_DENIED: + # C:\Windows\Temp is one. Anything that cannot be proven private is not. + return False + if error: # pragma: no cover # the directory was opened or listed an instant earlier + raise ctypes.WinError(error) + try: + # SYSTEM, Administrators, CREATOR OWNER and OWNER RIGHTS; the last two stand for the owner. + trusted = {_sid_from_text(text) for text in ("S-1-5-18", "S-1-5-32-544", "S-1-3-0", "S-1-3-4")} + trusted.add(_user_sid()) + # A missing DACL grants everyone full access. + return ( + bool(dacl.value) + and _sid_at(cast("int", owner.value)) in trusted + and _name_writers(cast("int", dacl.value)) <= trusted + ) + finally: + _kernel32.LocalFree(descriptor) + + def _name_writers(dacl: int) -> set[bytes]: + (count,) = struct.unpack("<4xH2x", ctypes.string_at(dacl, 8)) + writers = set() + offset = 8 + for _ in range(count): + ace_type, _flags, ace_size, mask = struct.unpack(" bytes: + token = wintypes.HANDLE() + token_query: Final = 0x0008 + open_as_self: Final = True + # An impersonating thread acts as the account it impersonates, which is the one whose names the lock creates. + if not ( + _advapi32.OpenThreadToken(_kernel32.GetCurrentThread(), token_query, open_as_self, ctypes.byref(token)) + or _advapi32.OpenProcessToken(_kernel32.GetCurrentProcess(), token_query, ctypes.byref(token)) + ): # pragma: no cover # every process may query its own token + raise ctypes.WinError(ctypes.get_last_error()) + try: + size = wintypes.DWORD() + _advapi32.GetTokenInformation(token, 1, None, 0, ctypes.byref(size)) + buffer = ctypes.create_string_buffer(size.value) + if not _advapi32.GetTokenInformation( + token, 1, buffer, size, ctypes.byref(size) + ): # pragma: no cover # the buffer has the size the first call asked for + raise ctypes.WinError(ctypes.get_last_error()) + return _sid_at(cast("int", ctypes.c_void_p.from_buffer(buffer).value)) + finally: + _kernel32.CloseHandle(token) + + def _sid_from_text(text: str) -> bytes: + revision, authority, *subauthorities = (int(part) for part in text.removeprefix("S-").split("-")) + return ( + struct.pack(" bytes: + return ctypes.string_at(address, 8 + 4 * ctypes.string_at(address + 1, 1)[0]) + + def _make_private_directory(path: str) -> None: + """Create *path* with the owner-only descriptor, so that nobody else can add a name to it.""" + attributes = _SECURITY_ATTRIBUTES( + nLength=ctypes.sizeof(_SECURITY_ATTRIBUTES), + lpSecurityDescriptor=_owner_only_descriptor(), + bInheritHandle=False, + ) + if not _kernel32.CreateDirectoryW(path, ctypes.byref(attributes)): # pragma: no cover # the name is fresh + raise ctypes.WinError(ctypes.get_last_error()) + + @contextmanager + def _pinned(path: str) -> Generator[tuple[int, int]]: + """Hold *path* open without delete sharing, which keeps its name from being renamed or removed.""" + with _opened(path, _FILE_SHARE_READ_WRITE) as handle: + yield _identity_of(handle) + + def _identity_at(path: str) -> tuple[int, int]: + with _opened(path, _FILE_SHARE_ALL) as handle: + return _identity_of(handle) + + def _identity_of_fd(fd: int) -> tuple[int, int]: + return _identity_of(msvcrt.get_osfhandle(fd)) + + @contextmanager + def _opened(path: str, share: int) -> Generator[int]: + # The final component is opened as itself, never through a reparse point. + handle = _kernel32.CreateFileW( + path, + _FILE_READ_ATTRIBUTES, + share, + None, + 3, # OPEN_EXISTING + 0x02000000 | 0x00200000, # FILE_FLAG_BACKUP_SEMANTICS opens a directory; FILE_FLAG_OPEN_REPARSE_POINT + None, + ) + if handle in {None, _INVALID_HANDLE_VALUE}: # pragma: no cover # callers open a name they just made or listed + raise ctypes.WinError(ctypes.get_last_error()) + try: + yield cast("int", handle) + finally: + _kernel32.CloseHandle(handle) + + def _identity_of(handle: int) -> tuple[int, int]: + info = _BY_HANDLE_FILE_INFORMATION() + if not _kernel32.GetFileInformationByHandle(handle, ctypes.byref(info)): # pragma: no cover + raise ctypes.WinError(ctypes.get_last_error()) + return info.dwVolumeSerialNumber, info.nFileIndexHigh << 32 | info.nFileIndexLow + +else: # pragma: win32 no cover + + class WindowsFileLock(BaseFileLock): + """Uses ``LockFileEx`` to hard lock a byte range of the lock file on Windows systems.""" + + def _acquire(self) -> None: + raise NotImplementedError + + def _release(self) -> None: + raise NotImplementedError + + +__all__ = [ + "WindowsFileLock", + "_directory_is_private", + "_identity_at", + "_identity_of_fd", + "_is_unlinked", + "_make_private_directory", + "_open_deletable_for_reading", + "_open_non_reparse_fd", + "_pin_directory", + "_pinned", + "_unpin_directory", + "open_for_delete_without_oplock_wait", + "open_read_without_oplock_wait", + "unlink_without_oplock_wait", +] diff --git a/hug/lib/python3.12/site-packages/filelock/asyncio.py b/hug/lib/python3.12/site-packages/filelock/asyncio.py new file mode 100644 index 0000000000000000000000000000000000000000..8c3703737ec510da51792b19924d066099afaea6 --- /dev/null +++ b/hug/lib/python3.12/site-packages/filelock/asyncio.py @@ -0,0 +1,925 @@ +"""An asyncio-based implementation of the file lock.""" + +from __future__ import annotations + +__lazy_modules__ = [ + "asyncio", + "concurrent", + "contextlib", + "filelock._async", + "filelock._error", + "filelock._util", + "functools", + "inspect", + "threading", +] + +import asyncio +import contextlib +import functools +import logging +import os +import sys +import time +from concurrent import futures +from dataclasses import dataclass, field, fields +from inspect import iscoroutinefunction +from threading import RLock +from typing import TYPE_CHECKING, Final, NoReturn, TypeVar, cast + +from filelock._api import ( + _UNSET_FILE_MODE, + BaseFileLock, + CloseErrorPolicy, + ContextErrorPolicy, + FileLockContext, + FileLockMeta, + ThreadLocalFileContext, + _append_exception_context, + _canonical, + _delegate_to_current, + _ExtraValue, + _fork_transition, + _grouped_errors, + _is_system_call_failure, + _raise_body_and_release, + _raise_chained_errors, + _raise_cleanup_errors, + _raise_grouped_errors, + _register_fork_object, + _resolve_poll_interval, + _resolve_timeout, +) +from filelock._async import ( + _AsyncTransitionGate, + _AsyncTransitionUnavailableError, + _BackendOutcome, + _capture_awaitable, + _capture_call, + _drain_future, + _future_result, + _run_in_executor, + _wait_until_done, +) +from filelock._error import Timeout +from filelock._lease import SoftFileLease +from filelock._soft import SoftFileLock +from filelock._strict import StrictSoftFileLock +from filelock._unix import UnixFileLock +from filelock._util import ensure_directory_exists +from filelock._windows import WindowsFileLock + +if TYPE_CHECKING: + from collections.abc import Awaitable, Callable, Coroutine, Hashable, Mapping + from types import TracebackType + from typing import Self + + +_LOGGER: Final[logging.Logger] = logging.getLogger("filelock") +_ASYNC_RELEASE_CANCELLATION_ERRORS: Final[str] = "lock release cancellation and backend release both failed" +_ASYNC_CONTEXT_RELEASE_ERRORS: Final[str] = "context body, release cancellation, and backend release failed" +_ASYNC_RELEASE_CANCELLATION_MARKER_ATTR: Final[str] = "_filelock_async_release_cancellation" +_ASYNC_RELEASE_CANCELLATION_MARKER: Final[list[None]] = [] +_FINALIZER_RELEASES: Final[set[asyncio.Task[None] | futures.Future[None]]] = set() + +_AT = TypeVar("_AT", bound="BaseAsyncFileLock") +_T = TypeVar("_T") + + +class AsyncFileLockMeta(FileLockMeta): + def __call__( # ruff:ignore[too-many-arguments] # forwards the public constructor's documented parameters + cls: type[_AT], # ruff:ignore[invalid-first-argument-name-for-method] # metaclass __call__ receives the class being constructed + lock_file: str | os.PathLike[str], + timeout: float = -1, + mode: int = _UNSET_FILE_MODE, + thread_local: bool = False, # ruff:ignore[boolean-type-hint-positional-argument, boolean-default-value-positional-argument] # public API: positional bool kept for backwards compatibility + *, + blocking: bool = True, + is_singleton: bool = False, + poll_interval: float = 0.05, + lifetime: float | None = None, + context_error_policy: ContextErrorPolicy = "chain", + close_error_policy: CloseErrorPolicy = "default", + fallback_to_soft: bool = True, + preserve_lock_file: bool = False, + on_acquired: Callable[[int], None] | None = None, + loop: asyncio.AbstractEventLoop | None = None, + run_in_executor: bool = True, + executor: futures.Executor | None = None, + **kwargs: _ExtraValue, + ) -> _AT: + if thread_local and run_in_executor: + msg = "run_in_executor is not supported when thread_local is True" + raise ValueError(msg) + return super().__call__( # a subclass may add options of its own, as AsyncSoftFileLease does + **kwargs, + lock_file=lock_file, + timeout=timeout, + mode=mode, + thread_local=thread_local, + blocking=blocking, + is_singleton=is_singleton, + poll_interval=poll_interval, + lifetime=lifetime, + context_error_policy=context_error_policy, + close_error_policy=close_error_policy, + fallback_to_soft=fallback_to_soft, + preserve_lock_file=preserve_lock_file, + on_acquired=on_acquired, + loop=loop, + run_in_executor=run_in_executor, + executor=executor, + ) + + +class BaseAsyncFileLock(BaseFileLock, metaclass=AsyncFileLockMeta): + """ + Base class for asynchronous file locks. + + .. versionadded:: 3.15.0 + + """ + + _deadlock_holder_desc: str = "BaseAsyncFileLock instance in this task" + _constructor_lifetime_warning_stacklevel: int = 4 + + @staticmethod + def _deadlock_scope() -> Hashable | None: + # One event loop thread runs every task, so a thread-scoped registry cannot tell a same-task reacquire + # (a real deadlock: the polling task never reaches its own release) from another task queuing behind the + # holder (no deadlock: each poll yields, so the holder runs on and releases). Only the first may fail + # fast, so scope holders to the task. + return asyncio.current_task() + + def __init__( # ruff:ignore[too-many-arguments] # public constructor: one parameter per documented lock option + self, + lock_file: str | os.PathLike[str], + timeout: float = -1, + mode: int = _UNSET_FILE_MODE, + thread_local: bool = False, # ruff:ignore[boolean-type-hint-positional-argument, boolean-default-value-positional-argument] # public API: positional bool kept for backwards compatibility + *, + blocking: bool = True, + is_singleton: bool = False, + poll_interval: float = 0.05, + lifetime: float | None = None, + context_error_policy: ContextErrorPolicy = "chain", + close_error_policy: CloseErrorPolicy = "default", + fallback_to_soft: bool = True, + preserve_lock_file: bool = False, + on_acquired: Callable[[int], None] | None = None, + loop: asyncio.AbstractEventLoop | None = None, + run_in_executor: bool = True, + executor: futures.Executor | None = None, + ) -> None: + """ + Create a new lock object. + + :param lock_file: path to the file + :param timeout: default timeout when acquiring the lock, in seconds. It will be used as fallback value in the + acquire method, if no timeout value (``None``) is given. If you want to disable the timeout, set it to a + negative value. A timeout of 0 means that there is exactly one attempt to acquire the file lock. + :param mode: file permissions for the lockfile. When not specified, the OS controls permissions via umask and + default ACLs, preserving POSIX default ACL inheritance in shared directories. + :param thread_local: Whether this object's internal context should be thread local or not. If this is set to + ``False`` (the default) then the lock will be reentrant across threads. When ``True``, **all fields of the + lock's internal context are per-thread**, including the configuration values ``poll_interval``, ``timeout``, + ``blocking``, ``mode``, and ``lifetime``. Setting one of these properties from one thread does not change + the value seen by another thread; threads that did not perform the write continue to see the value supplied + at construction time. ``mode`` has no setter, so construction is the only place it is ever set. If you need + configuration values to be visible across threads, construct the lock with ``thread_local=False``. + :param blocking: whether the lock should be blocking or not + :param is_singleton: If this is set to ``True`` then only one instance of this class will be created per lock + file. This is useful if you want to use the lock object for reentrant locking without needing to pass the + same object around. + :param poll_interval: default interval for polling the lock file, in seconds. It will be used as fallback value + in the acquire method, if no poll_interval value (``None``) is given. + :param lifetime: for :class:`AsyncSoftFileLock`, the age in seconds after which a waiting process may delete + the marker, even while its holder remains alive. This legacy expiry mode does not provide strict mutual + exclusion. ``None`` (the default) disables age-based expiry. Native OS locks (:class:`AsyncFileLock`) + cannot be revoked by file age and ignore a non-``None`` ``lifetime`` with a warning. + :param context_error_policy: how a context manager reconciles a failure in its body with a failure while + releasing on exit. ``"chain"`` (the default) keeps Python's behavior: the release error propagates with the + body error in its ``__context__``. ``"group"`` raises a :class:`BaseExceptionGroup` holding the body error + first and the release error second, so neither hides the other. + :param close_error_policy: for native locks (:class:`AsyncFileLock`), what to do with an ``os.close`` failure + after the OS unlock has already committed. ``"default"`` keeps each platform's historical behavior, + ``"raise"`` always propagates the ``OSError``, and ``"suppress"`` always ignores it. + :param fallback_to_soft: for :class:`AsyncFileLock`, whether to fall back to soft existence locking when + ``flock`` returns ``ENOSYS``. ``True`` (default) keeps the fallback; ``False`` propagates the error. + :param preserve_lock_file: for native locks (:class:`AsyncFileLock`), whether filelock promises not to unlink + the lock pathname on release. ``False`` (default) keeps each backend's cleanup; ``True`` keeps a stable file + identity (Windows skips its unlink, Unix refuses the ``ENOSYS`` soft fallback). :class:`AsyncSoftFileLock` + rejects ``True``. + :param on_acquired: for native locks (:class:`AsyncFileLock`), a callable invoked with the borrowed lock + descriptor once per physical acquisition, after the lock is held but before + :meth:`~BaseAsyncFileLock.acquire` returns. With ``run_in_executor=True`` (the default) it runs in the + backend executor. It must not close or unlock the descriptor; a raise rolls the acquisition back. + :class:`AsyncSoftFileLock` rejects it. + :param loop: the event loop a garbage-collected lock schedules its release on when no loop runs in the + collecting thread. :meth:`~BaseAsyncFileLock.acquire` and :meth:`~BaseAsyncFileLock.release` always run + on the loop that awaits them. + :param run_in_executor: If this is set to ``True`` then the lock will be acquired in an executor. + :param executor: The executor to use. If not specified, the default executor will be used. + + """ + self._creator_pid = os.getpid() + self._holder_id = id(self) + self._is_thread_local = thread_local + self._is_singleton = is_singleton + self._context_error_policy = context_error_policy # already validated by the metaclass + self._close_error_policy = close_error_policy # already validated by the metaclass + self._fallback_to_soft = fallback_to_soft + self._preserve_lock_file = preserve_lock_file # already validated by the metaclass + self._on_acquired = on_acquired # already validated by the metaclass + # BaseFileLock.__init__ never runs here, and the finalizer releases thread-local holds through these. + self._transition_lock = RLock() + self._thread_holds = {} + self._thread_pending = {} + + self._async_context: Final[AsyncFileLockContext] = ( + AsyncThreadLocalFileContext if thread_local else AsyncFileLockContext + )( + lock_file=os.fspath(lock_file), + timeout=timeout, + mode=mode, + blocking=blocking, + poll_interval=poll_interval, + lifetime=lifetime, + loop=loop, + run_in_executor=run_in_executor, + executor=executor, + ) + # BaseFileLock declares _context as FileLockContext; narrowing that writable attribute here is unsound. + self._context = self._async_context + _register_fork_object(self) + + def _singleton_extra_mismatches(self, kwargs: Mapping[str, _ExtraValue], /) -> dict[str, tuple[str, str]]: + mismatches = super()._singleton_extra_mismatches(kwargs) + # Loops and executors compare by identity: a different one schedules the lock's work elsewhere. + mismatches.update( + (name, (str(kwargs[name]), str(value))) + for name, value in (("loop", self.loop), ("executor", self.executor)) + if kwargs[name] is not value + ) + if kwargs["run_in_executor"] != self.run_in_executor: + mismatches["run_in_executor"] = (str(kwargs["run_in_executor"]), str(self.run_in_executor)) + return mismatches + + @property + def run_in_executor(self) -> bool: + """Whether run in executor.""" + return self._async_context.run_in_executor + + @property + def executor(self) -> futures.Executor | None: + """The executor.""" + return self._async_context.executor + + @executor.setter + def executor(self, value: futures.Executor | None) -> None: # pragma: no cover + """ + Change the executor. + + :param futures.Executor | None value: the new executor or ``None`` + + """ + self._async_context.executor = value + + @property + def loop(self) -> asyncio.AbstractEventLoop | None: + """The event loop.""" + return self._async_context.loop + + async def acquire( # ty: ignore[invalid-method-override] + self, + timeout: float | None = None, + poll_interval: float | None = None, + *, + blocking: bool | None = None, + cancel_check: Callable[[], bool] | None = None, + ) -> AsyncAcquireReturnProxy: + """ + Try to acquire the file lock. + + :param timeout: maximum wait time for acquiring the lock, ``None`` means use the default + :attr:`~BaseFileLock.timeout` is and if ``timeout < 0``, there is no timeout and this method will block + until the lock could be acquired + :param poll_interval: interval of trying to acquire the lock file, ``None`` means use the default + :attr:`~BaseFileLock.poll_interval` + :param blocking: whether to retry until this call holds the lock or the timeout expires, ``None`` means use + the default :attr:`~BaseFileLock.blocking`. ``False`` makes one attempt and raises :class:`~Timeout` on + failure. + :param cancel_check: a callable returning ``True`` when the acquisition should be canceled. Checked on each poll + iteration. When triggered, raises :class:`~Timeout` just like an expired timeout. + + :returns: a context object that will unlock the file when the context is exited + + :raises Timeout: if fails to acquire lock within the timeout period + :raises ValueError: if a blocking call gets a ``nan`` timeout, or *poll_interval* exceeds + :data:`threading.TIMEOUT_MAX` + + .. code-block:: python + + # You can use this method in the context manager (recommended) + async with await lock.acquire(): + pass + + # Or use an equivalent try-finally construct: + await lock.acquire() + try: + pass + finally: + await lock.release() + + """ + self._raise_if_inherited() + if timeout is None: + timeout = self._context.timeout + + if blocking is None: + blocking = self._context.blocking + + if poll_interval is None: + poll_interval = self._context.poll_interval + poll_interval = _resolve_poll_interval(poll_interval) + timeout = _resolve_timeout(timeout, blocking=blocking) + + start_time = time.perf_counter() + try: + return await self._acquire_with_admission( + blocking=blocking, + cancel_check=cancel_check, + timeout=timeout, + poll_interval=poll_interval, + start_time=start_time, + ) + except _AsyncTransitionUnavailableError: + raise Timeout(self.lock_file) from None + + async def _acquire_with_admission( + self, + *, + blocking: bool, + cancel_check: Callable[[], bool] | None, + timeout: float, + poll_interval: float, + start_time: float, + ) -> AsyncAcquireReturnProxy: + async with self._async_context.transition_gate.hold_for_acquire( + blocking=blocking, + cancel_check=cancel_check, + deadline=None if timeout < 0 else start_time + timeout, + poll_interval=poll_interval, + ): + # Re-entry only counts, so it skips the executor round trip that resolving the path costs. + if self.is_locked: + self._context.lock_counter += 1 + return AsyncAcquireReturnProxy(lock=self) + # A canceled provisional acquire must finish rollback before another caller can claim its descriptor. + canonical = await self._off_loop(functools.partial(_canonical, self.lock_file)) + self._context.lock_counter += 1 + self._raise_if_would_deadlock(canonical, timeout=timeout, blocking=blocking) + self._pin_lock_path(canonical) + try: + await self._async_poll_until_acquired( + blocking=blocking, + cancel_check=cancel_check, + timeout=timeout, + poll_interval=poll_interval, + start_time=start_time, + ) + except BaseException: + self._reconcile_failed_acquire(canonical) + raise + self._commit_acquire(canonical) + return AsyncAcquireReturnProxy(lock=self) + + async def _off_loop(self, call: Callable[[], _T]) -> _T: + # A slow mount stalls a filesystem call, and the loop with it; a coroutine backend runs on the loop anyway. + if not self.run_in_executor or iscoroutinefunction(self._acquire): + return call() + future = _run_in_executor(asyncio.get_running_loop(), self.executor, call) + await _wait_until_done(future) + return _future_result(future) + + async def _async_poll_until_acquired( + self, + *, + blocking: bool, + cancel_check: Callable[[], bool] | None, + timeout: float, + poll_interval: float, + start_time: float, + ) -> None: + lock_id = id(self) + lock_filename = self.lock_file + await self._off_loop(functools.partial(ensure_directory_exists, self._lock_path)) + while True: + self._raise_if_inherited() + _LOGGER.debug("Attempting to acquire lock %s on %s", lock_id, lock_filename) + await self._run_acquire_attempt() + self._raise_if_inherited() + if self.is_locked: + _LOGGER.debug("Lock %s acquired on %s", lock_id, lock_filename) + return + if self._check_give_up( + blocking=blocking, + cancel_check=cancel_check, + timeout=timeout, + start_time=start_time, + ): + raise Timeout(lock_filename, await self._off_loop(self._unprobeable_holder)) + delay = poll_interval + if timeout >= 0: + delay = min(delay, max(start_time + timeout - time.perf_counter(), 0.0)) + _LOGGER.debug("Lock %s not acquired on %s, waiting %s seconds ...", lock_id, lock_filename, delay) + await asyncio.sleep(delay) + + async def _run_acquire_attempt(self) -> None: + acquire_future = self._start_internal_method( + self._acquire_with_fork_tracking_async + if iscoroutinefunction(self._acquire) + else self._break_expired_and_acquire + ) + try: + await _wait_until_done(acquire_future) + except asyncio.CancelledError as cancellation: + acquire_error: BaseException | None = None + try: + await _drain_future(acquire_future) + except BaseException as error: # ruff:ignore[blind-except] # reported with the cancellation below + acquire_error = error + + rollback_error: BaseException | None = None + if self.is_locked: + try: + await _drain_future(self._start_tracked_release()) + except BaseException as error: # ruff:ignore[blind-except] # reported with the cancellation below + rollback_error = error + + if acquire_error is not None: + if rollback_error is not None: # pragma: needs fcntl + self._raise_cancelled_errors( + "lock acquisition cancellation, backend attempt, and rollback failed", + cancellation, + acquire_error, + rollback_error, + ) + self._raise_cancelled_errors( + "lock acquisition cancellation and backend attempt both failed", cancellation, acquire_error + ) + if rollback_error is not None: + self._raise_cancelled_errors( + "lock acquisition cancellation and rollback both failed", cancellation, rollback_error + ) + raise + try: + _future_result(acquire_future) + except asyncio.CancelledError as acquire_error: + await self._rollback_backend_cancelled_acquire(acquire_error) + + async def _rollback_backend_cancelled_acquire(self, acquire_error: asyncio.CancelledError) -> NoReturn: + if self.is_locked: + try: + await _drain_future(self._start_tracked_release()) + except BaseException as rollback_error: # ruff:ignore[blind-except] # both backend errors must surface + self._raise_acquire_rollback_errors(acquire_error, rollback_error) + raise acquire_error + + def _raise_acquire_rollback_errors(self, acquire_error: BaseException, rollback_error: BaseException) -> NoReturn: + if self._context_error_policy == "group": + _raise_grouped_errors("lock acquisition backend and rollback both failed", acquire_error, rollback_error) + _raise_chained_errors(acquire_error, rollback_error) + + def _raise_cancelled_errors( + self, + message: str, + cancellation: asyncio.CancelledError, + first_error: BaseException, + second_error: BaseException | None = None, + ) -> NoReturn: + if self._context_error_policy == "group": + marker = ( + (_ASYNC_RELEASE_CANCELLATION_MARKER_ATTR, _ASYNC_RELEASE_CANCELLATION_MARKER) + if message == _ASYNC_RELEASE_CANCELLATION_ERRORS + else None + ) + if second_error is None: + _raise_grouped_errors(message, cancellation, first_error, marker=marker) + _raise_grouped_errors(message, cancellation, first_error, second_error, marker=marker) + if (context := first_error.__context__) is not None and context is not cancellation: + if (cancellation_context := cancellation.__context__) is not None: + _append_exception_context(context, cancellation_context) + cancellation.__context__ = context + first_error.__context__ = cancellation + _raise_chained_errors(first_error, second_error) + + async def release(self, force: bool = False) -> None: # ty: ignore[invalid-method-override] # ruff:ignore[boolean-type-hint-positional-argument, boolean-default-value-positional-argument] # public API: positional bool kept for backwards compatibility + """ + Release the file lock. The lock is only completely released when the lock counter reaches 0. The lock file + itself may be deleted automatically, the behavior is platform-specific. + + :param force: If true, the lock counter is ignored and the lock is released in every case. + + """ + if self._creator_pid != os.getpid() or not self.is_locked: + return + async with self._async_context.transition_gate.hold(): + await self._release_serialized(force=force) + + async def _release_serialized(self, *, force: bool) -> None: + counter_before: Final = self._context.lock_counter + if not self.is_locked: + return + if not force and self._context.lock_counter > 1: + self._context.lock_counter -= 1 + return + + try: + await self._release_through_backend() + except BaseException as release_error: + if self._release_interrupted_before_commit(counter_before, release_error): + with contextlib.suppress(Exception): + self._release_with_fork_tracking() + self._commit_release_if_released() + raise + + def _release_interrupted_before_commit(self, counter_before: int, release_error: BaseException) -> bool: + # Same rule as the sync release: a handler that raised before anything committed leaves the counter as it was, + # and only then does a second attempt release the hold. A worker thread never runs a handler, so a lock that + # runs its backend in the executor has nothing to retry. + return ( + counter_before == self._context.lock_counter + and self.is_locked + and not self.run_in_executor + and not iscoroutinefunction(self._release) + and not isinstance(release_error, asyncio.CancelledError) + and not _is_system_call_failure(release_error) + ) + + async def _release_through_backend(self) -> None: + lock_id, lock_filename = id(self), self.lock_file + _LOGGER.debug("Attempting to release lock %s on %s", lock_id, lock_filename) + release_future = self._start_tracked_release() + try: + await _wait_until_done(release_future) + except asyncio.CancelledError as cancellation: + try: + await _drain_future(release_future) + except BaseException as release_error: # ruff:ignore[blind-except] # cancellation and backend failure must both surface + self._commit_release_if_released() + self._raise_cancelled_errors(_ASYNC_RELEASE_CANCELLATION_ERRORS, cancellation, release_error) + self._commit_release() + raise + try: + _future_result(release_future) + except BaseException: # state follows the backend for control-flow exceptions too + self._commit_release_if_released() + raise + self._commit_release() + _LOGGER.debug("Lock %s released on %s", lock_id, lock_filename) + + def _commit_release_if_released(self) -> None: + # Commit only when the backend actually unlocked (close or unlink failed after the OS unlock). If the lock is + # still held, keep the counter so a later release can retry. + if not self.is_locked: + self._commit_release() + + def _start_internal_method( + self, method: Callable[[], None] | Callable[[], Coroutine[None, None, None]] + ) -> asyncio.Future[_BackendOutcome[None]]: + if iscoroutinefunction(method): + return asyncio.create_task(_capture_awaitable(cast("Callable[[], Coroutine[None, None, None]]", method)())) + loop = asyncio.get_running_loop() + sync_method = cast("Callable[[], None]", method) + if self.run_in_executor: + return _run_in_executor(loop, self.executor, sync_method) + future: asyncio.Future[_BackendOutcome[None]] = loop.create_future() + future.set_result(_capture_call(sync_method)) + return future + + def _start_tracked_release(self) -> asyncio.Future[_BackendOutcome[None]]: + return self._start_internal_method( + self._release_with_fork_tracking_async + if iscoroutinefunction(self._release) + else self._release_with_fork_tracking + ) + + def _break_expired_and_acquire(self) -> None: + # Runs as one backend call so the lifetime check's lstat and unlink stay off the event loop with the acquire. + self._try_break_expired_lock() + self._acquire_with_fork_tracking() + + async def _acquire_with_fork_tracking_async(self) -> None: + self._try_break_expired_lock() + with _fork_transition(self): + try: + await cast("Callable[[], Awaitable[None]]", self._acquire)() + except BaseException as acquisition_error: + await self._rollback_failed_acquire_async(acquisition_error) + raise + try: + self._register_context_descriptor() + except BaseException as registration_error: # cancellation must roll back the descriptor + await self._rollback_failed_registration_async(registration_error) + raise + if self.is_locked: + await self._invoke_on_acquired_async() + + async def _rollback_failed_acquire_async(self, acquisition_error: BaseException) -> None: + if not self.is_locked: + return + registration_error: BaseException | None = None + tracking_error: BaseException | None = None + try: + self._register_context_descriptor() + except BaseException as error: # ruff:ignore[blind-except] # preserve registration and acquisition failures + registration_error = error + try: + # Rollback may fail too; retain the fd so a child can close it without another identity probe. + self._register_unverified_context_descriptor() + except BaseException as error: # ruff:ignore[blind-except] # pragma: no cover - allocation/control-flow during fallback + tracking_error = error + try: + await _drain_future(self._start_tracked_release()) + except BaseException as rollback_error: # ruff:ignore[blind-except] # preserve rollback and acquisition failures + if registration_error is None and tracking_error is None: + self._raise_acquire_rollback_errors(acquisition_error, rollback_error) + _raise_cleanup_errors( + "lock acquisition cleanup failed", + acquisition_error, + registration_error, + tracking_error, + rollback_error, + ) + if registration_error is not None: + _raise_cleanup_errors( + "lock acquisition cleanup failed", acquisition_error, registration_error, tracking_error + ) + + async def _rollback_failed_registration_async(self, registration_error: BaseException) -> None: + tracking_error: BaseException | None = None + try: + # Rollback may fail too; retain the fd so a child can close it without another identity probe. + self._register_unverified_context_descriptor() + except BaseException as error: # ruff:ignore[blind-except] # pragma: no cover - allocation/control-flow during fallback + tracking_error = error + try: + await _drain_future(self._start_tracked_release()) + except BaseException as rollback_error: # ruff:ignore[blind-except] # preserve rollback and registration failures + _raise_cleanup_errors( + "descriptor registration cleanup failed", registration_error, tracking_error, rollback_error + ) + if tracking_error is not None: # pragma: no cover - requires failed in-memory fallback + _raise_cleanup_errors("descriptor registration cleanup failed", registration_error, tracking_error) + + async def _invoke_on_acquired_async(self) -> None: + if self._on_acquired is None or self._context.lock_counter != 1: + return + try: + self._on_acquired(cast("int", self._context.lock_file_fd)) + except BaseException as callback_error: # caller control-flow errors must release the lock + callback_context = callback_error.__context__ + try: + await _drain_future(self._start_tracked_release()) + except BaseException as release_error: # ruff:ignore[blind-except] # both errors surface via the group below + _raise_body_and_release(callback_error, release_error) + callback_error.__context__ = callback_context + raise + + async def _release_with_fork_tracking_async(self) -> None: + with _fork_transition(self): + try: + await cast("Callable[[], Awaitable[None]]", self._release)() + finally: + self._unregister_released_descriptor() + + def __enter__(self) -> NoReturn: + """Sync context manager entry is not supported because lock acquisition is a coroutine.""" + msg = "Use `async with`: acquire/release are coroutines and cannot be awaited in a sync context manager." + raise NotImplementedError(msg) + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """Sync context manager exit is not supported because lock release is a coroutine.""" + msg = "Use `async with`: acquire/release are coroutines and cannot be awaited in a sync context manager." + raise NotImplementedError(msg) + + async def __aenter__(self) -> Self: + """ + Acquire the lock. + + :returns: the lock object + + """ + await self.acquire() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """ + Release the lock, reconciling a release failure with any body failure per :attr:`context_error_policy`. + + :param exc_type: the exception type if raised + :param exc_value: the exception value if raised + :param traceback: the exception traceback if raised + + """ + await self._release_in_context(exc_value) + + async def _release_in_context( # ty: ignore[invalid-method-override] + self, body_error: BaseException | None + ) -> None: + # The async counterpart of BaseFileLock._release_in_context: await release, then apply the same policy. + try: + await self.release() + except BaseException as release_error: + if body_error is None: + raise + if self._context_error_policy == "chain": + _append_exception_context(release_error, body_error) + raise + if ( + errors := _grouped_errors( + release_error, + _ASYNC_RELEASE_CANCELLATION_ERRORS, + (_ASYNC_RELEASE_CANCELLATION_MARKER_ATTR, _ASYNC_RELEASE_CANCELLATION_MARKER), + ) + ) is not None: + match errors: + # The marker is only ever set on a (CancelledError, backend_error) pair, so this always matches. + case (asyncio.CancelledError() as cancellation, backend_error): # pragma: no branch + _raise_grouped_errors(_ASYNC_CONTEXT_RELEASE_ERRORS, body_error, cancellation, backend_error) + _raise_body_and_release(body_error, release_error) + + def _release_abandoned_holds(self) -> None: + # Runs from garbage collection, where no event loop may be running. + with contextlib.suppress(Exception): + # A thread-local lock never uses an executor, so an in-place release matches the loop's and also reaches + # threads whose loops it cannot schedule on. + if self._is_thread_local and not iscoroutinefunction(self._release): + self._release_without_loop() + return + try: + loop = asyncio.get_running_loop() + except RuntimeError: + # At interpreter exit daemon threads no longer run, so a loop running in one never runs the release. + if (loop := self._async_context.loop) is None or loop.is_closed() or sys.is_finalizing(): + self._release_without_loop() + return + if not loop.is_running(): # pragma: no cover + loop.run_until_complete(self.release(force=True)) + return + # Off the deadlock registry, a new lock in this task waits for the scheduled release instead of failing. + self._drop_registry_entry() + # The stored loop runs in another thread, and only its own thread may create tasks on it. + pending = asyncio.run_coroutine_threadsafe(self.release(force=True), loop) + else: + self._drop_registry_entry() + pending = loop.create_task(self.release(force=True)) + # The loop keeps only a weak reference to a task, so hold one until the release finishes. + _FINALIZER_RELEASES.add(pending) + pending.add_done_callback(self._settle_finalizer_release) + + def _settle_finalizer_release(self, pending: asyncio.Task[None] | futures.Future[None]) -> None: + _FINALIZER_RELEASES.discard(pending) + # Nothing awaits this release, so a failure or cancellation falls back to the sync release here. + with contextlib.suppress(asyncio.CancelledError, futures.CancelledError): + if pending.exception() is None: + return + with contextlib.suppress(Exception): + self._release_without_loop() + + def _release_without_loop(self) -> None: + # No loop can run a release here, so call a sync backend directly as BaseFileLock.__del__ does; a coroutine + # backend cannot run and keeps its lock. + if iscoroutinefunction(self._release): + return + if self.is_locked: + try: + self._release_with_fork_tracking() + finally: + self._commit_release_if_released() + # A thread-local lock also holds for the threads that ran its loops, and nothing else releases those now. + self._release_thread_holds(exited_only=False) + + +def _new_transition_gate() -> _AsyncTransitionGate: + # Called per context rather than passed as the factory, so the lazy import resolves on first use, not at import. + return _AsyncTransitionGate() + + +@dataclass +class AsyncFileLockContext(FileLockContext): + """A dataclass which holds the context for a ``BaseAsyncFileLock`` object.""" + + #: Whether run in executor + run_in_executor: bool = True + + #: The executor + executor: futures.Executor | None = None + + #: The loop + loop: asyncio.AbstractEventLoop | None = None + + #: Serializes the tasks that transition this context. An acquire keeps it while it polls, so a thread-local lock + #: needs one per thread. A thread waiting for the lock must not keep the thread that holds it from releasing. + transition_gate: _AsyncTransitionGate = field(default_factory=_new_transition_gate) + + +class AsyncThreadLocalFileContext(AsyncFileLockContext, ThreadLocalFileContext): + """A thread local version of the ``FileLockContext`` class.""" + + # threading.local runs __init__ again with these arguments the first time each thread touches the context. + def __init__( # ruff:ignore[too-many-arguments] # mirrors the AsyncFileLockContext fields set at construction + self, + *, + lock_file: str, + timeout: float, + mode: int, + blocking: bool, + poll_interval: float, + lifetime: float | None = None, + run_in_executor: bool = True, + executor: futures.Executor | None = None, + loop: asyncio.AbstractEventLoop | None = None, + ) -> None: + self.current = AsyncFileLockContext( + lock_file=lock_file, + timeout=timeout, + mode=mode, + blocking=blocking, + poll_interval=poll_interval, + lifetime=lifetime, + run_in_executor=run_in_executor, + executor=executor, + loop=loop, + ) + + +for _field in fields(AsyncFileLockContext): + setattr(AsyncThreadLocalFileContext, _field.name, _delegate_to_current(_field.name)) + + +class AsyncAcquireReturnProxy: + """A context-aware object that will release the lock file when exiting.""" + + def __init__(self, lock: BaseAsyncFileLock) -> None: # ruff:ignore[undocumented-public-init] # trivial release-on-exit proxy + self.lock = lock + + async def __aenter__(self) -> BaseAsyncFileLock: # ruff:ignore[undocumented-magic-method] # returns the wrapped lock + return self.lock + + async def __aexit__( # ruff:ignore[undocumented-magic-method] # releases the wrapped lock on exit + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + await self.lock._release_in_context(exc_value) # ruff:ignore[private-member-access] # releases the wrapped lock's context + + +class AsyncSoftFileLock(SoftFileLock, BaseAsyncFileLock): + """Simply watches the existence of the lock file.""" + + _lifetime_replacements: tuple[str, str] | None = ("AsyncStrictSoftFileLock", "AsyncSoftFileLease") + + +class AsyncStrictSoftFileLock(StrictSoftFileLock, BaseAsyncFileLock): + """Run strict owner-claim locking without blocking the event loop.""" + + +class AsyncSoftFileLease(SoftFileLease, BaseAsyncFileLock): + """Existence lock whose claim expires, so a peer may take it while the previous holder still runs.""" + + +class AsyncUnixFileLock(UnixFileLock, BaseAsyncFileLock): + """Uses the :func:`fcntl.flock` to hard lock the lock file on unix systems.""" + + +class AsyncWindowsFileLock(WindowsFileLock, BaseAsyncFileLock): + """Uses ``LockFileEx`` to hard lock a byte range of the lock file on Windows systems.""" + + +__all__ = [ + "AsyncAcquireReturnProxy", + "AsyncSoftFileLease", + "AsyncSoftFileLock", + "AsyncStrictSoftFileLock", + "AsyncUnixFileLock", + "AsyncWindowsFileLock", + "BaseAsyncFileLock", +] + + +def __dir__() -> list[str]: + # Hide the sync bases and helpers imported at top level. + return list(__all__) diff --git a/hug/lib/python3.12/site-packages/filelock/py.typed b/hug/lib/python3.12/site-packages/filelock/py.typed new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/hug/lib/python3.12/site-packages/filelock/version.py b/hug/lib/python3.12/site-packages/filelock/version.py new file mode 100644 index 0000000000000000000000000000000000000000..e4b292007e51eb25f14c4c0472451059b98ceb9a --- /dev/null +++ b/hug/lib/python3.12/site-packages/filelock/version.py @@ -0,0 +1,24 @@ +# file generated by vcs-versioning +# don't change, don't track in version control +from __future__ import annotations + +__all__ = [ + "__version__", + "__version_tuple__", + "version", + "version_tuple", + "__commit_id__", + "commit_id", +] + +version: str +__version__: str +__version_tuple__: tuple[int | str, ...] +version_tuple: tuple[int | str, ...] +commit_id: str | None +__commit_id__: str | None + +__version__ = version = '4.2.4' +__version_tuple__ = version_tuple = (4, 2, 4) + +__commit_id__ = commit_id = None diff --git a/hug/lib/python3.12/site-packages/fontTools/__init__.py b/hug/lib/python3.12/site-packages/fontTools/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..1a0bc4710649f4a5e18cbb7891a60f43ad306b95 --- /dev/null +++ b/hug/lib/python3.12/site-packages/fontTools/__init__.py @@ -0,0 +1,8 @@ +import logging +from fontTools.misc.loggingTools import configLogger + +log = logging.getLogger(__name__) + +version = __version__ = "4.68.0" + +__all__ = ["version", "log", "configLogger"] diff --git a/hug/lib/python3.12/site-packages/fontTools/__main__.py b/hug/lib/python3.12/site-packages/fontTools/__main__.py new file mode 100644 index 0000000000000000000000000000000000000000..7c74ad3c86e54cb7e9939ed2bf96aa59cc6dcd06 --- /dev/null +++ b/hug/lib/python3.12/site-packages/fontTools/__main__.py @@ -0,0 +1,35 @@ +import sys + + +def main(args=None): + if args is None: + args = sys.argv[1:] + + # TODO Handle library-wide options. Eg.: + # --unicodedata + # --verbose / other logging stuff + + # TODO Allow a way to run arbitrary modules? Useful for setting + # library-wide options and calling another library. Eg.: + # + # $ fonttools --unicodedata=... fontmake ... + # + # This allows for a git-like command where thirdparty commands + # can be added. Should we just try importing the fonttools + # module first and try without if it fails? + + if len(sys.argv) < 2: + sys.argv.append("help") + if sys.argv[1] == "-h" or sys.argv[1] == "--help": + sys.argv[1] = "help" + mod = "fontTools." + sys.argv[1] + sys.argv[1] = sys.argv[0] + " " + sys.argv[1] + del sys.argv[0] + + import runpy + + runpy.run_module(mod, run_name="__main__") + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/hug/lib/python3.12/site-packages/fontTools/__pycache__/__init__.cpython-312.pyc b/hug/lib/python3.12/site-packages/fontTools/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6acb98d7d18dec4a70cddcee41f05cebc307d7a7 Binary files /dev/null and b/hug/lib/python3.12/site-packages/fontTools/__pycache__/__init__.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/fontTools/__pycache__/__main__.cpython-312.pyc b/hug/lib/python3.12/site-packages/fontTools/__pycache__/__main__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5fa17cc0c360db63741cded69a48619b685c4e30 Binary files /dev/null and b/hug/lib/python3.12/site-packages/fontTools/__pycache__/__main__.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/fontTools/__pycache__/afmLib.cpython-312.pyc b/hug/lib/python3.12/site-packages/fontTools/__pycache__/afmLib.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..077cdad9b7344071d9e06801f07306a7ade4325c Binary files /dev/null and b/hug/lib/python3.12/site-packages/fontTools/__pycache__/afmLib.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/fontTools/__pycache__/agl.cpython-312.pyc b/hug/lib/python3.12/site-packages/fontTools/__pycache__/agl.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b4706bc2e4a226ac31b3f8235a1722f5c8ea755f --- /dev/null +++ b/hug/lib/python3.12/site-packages/fontTools/__pycache__/agl.cpython-312.pyc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ade532c6078fca1ea077b5395e139ee90e0593d83b8184b18f5d1497f6e660dd +size 112790 diff --git a/hug/lib/python3.12/site-packages/fontTools/__pycache__/annotations.cpython-312.pyc b/hug/lib/python3.12/site-packages/fontTools/__pycache__/annotations.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d729c985504dd35de1357e8247438eab9fcc589d Binary files /dev/null and b/hug/lib/python3.12/site-packages/fontTools/__pycache__/annotations.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/fontTools/__pycache__/fontBuilder.cpython-312.pyc b/hug/lib/python3.12/site-packages/fontTools/__pycache__/fontBuilder.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e1917e09e11279a91ab5a7d346b42dec611b48fe Binary files /dev/null and b/hug/lib/python3.12/site-packages/fontTools/__pycache__/fontBuilder.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/fontTools/__pycache__/help.cpython-312.pyc b/hug/lib/python3.12/site-packages/fontTools/__pycache__/help.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fe0b7443bb1eb1ba2dafbc1ff104de4a9d8c2b06 Binary files /dev/null and b/hug/lib/python3.12/site-packages/fontTools/__pycache__/help.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/fontTools/__pycache__/tfmLib.cpython-312.pyc b/hug/lib/python3.12/site-packages/fontTools/__pycache__/tfmLib.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e6b527351e66de30337f3f9155882d9cf63bfe1c Binary files /dev/null and b/hug/lib/python3.12/site-packages/fontTools/__pycache__/tfmLib.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/fontTools/__pycache__/ttx.cpython-312.pyc b/hug/lib/python3.12/site-packages/fontTools/__pycache__/ttx.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7657c1b61bbad51af4314da0ba6016de3f1f086a Binary files /dev/null and b/hug/lib/python3.12/site-packages/fontTools/__pycache__/ttx.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/fontTools/__pycache__/unicode.cpython-312.pyc b/hug/lib/python3.12/site-packages/fontTools/__pycache__/unicode.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..98b3ab6d5af6e98ffc3a3aac30a3402eacdf34a9 Binary files /dev/null and b/hug/lib/python3.12/site-packages/fontTools/__pycache__/unicode.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/fontTools/afmLib.py b/hug/lib/python3.12/site-packages/fontTools/afmLib.py new file mode 100644 index 0000000000000000000000000000000000000000..7e0864bc87df16da5b8b2551d45a98a5553708b5 --- /dev/null +++ b/hug/lib/python3.12/site-packages/fontTools/afmLib.py @@ -0,0 +1,439 @@ +"""Module for reading and writing AFM (Adobe Font Metrics) files. + +Note that this has been designed to read in AFM files generated by Fontographer +and has not been tested on many other files. In particular, it does not +implement the whole Adobe AFM specification [#f1]_ but, it should read most +"common" AFM files. + +Here is an example of using `afmLib` to read, modify and write an AFM file: + + >>> from fontTools.afmLib import AFM + >>> f = AFM("Tests/afmLib/data/TestAFM.afm") + >>> + >>> # Accessing a pair gets you the kern value + >>> f[("V","A")] + -60 + >>> + >>> # Accessing a glyph name gets you metrics + >>> f["A"] + (65, 668, (8, -25, 660, 666)) + >>> # (charnum, width, bounding box) + >>> + >>> # Accessing an attribute gets you metadata + >>> f.FontName + 'TestFont-Regular' + >>> f.FamilyName + 'TestFont' + >>> f.Weight + 'Regular' + >>> f.XHeight + 500 + >>> f.Ascender + 750 + >>> + >>> # Attributes and items can also be set + >>> f[("A","V")] = -150 # Tighten kerning + >>> f.FontName = "TestFont Squished" + >>> + >>> # And the font written out again (remove the # in front) + >>> #f.write("testfont-squished.afm") + +.. rubric:: Footnotes + +.. [#f1] `Adobe Technote 5004 `_, + Adobe Font Metrics File Format Specification. + +""" + +import re + +# every single line starts with a "word" +identifierRE = re.compile(r"^([A-Za-z]+).*") + +# regular expression to parse char lines +charRE = re.compile( + r"(-?\d+)" # charnum + r"\s*;\s*WX\s+" # ; WX + r"(-?\d+)" # width + r"\s*;\s*N\s+" # ; N + r"([.A-Za-z0-9_]+)" # charname + r"\s*;\s*B\s+" # ; B + r"(-?\d+)" # left + r"\s+" + r"(-?\d+)" # bottom + r"\s+" + r"(-?\d+)" # right + r"\s+" + r"(-?\d+)" # top + r"\s*;\s*" # ; +) + +# regular expression to parse kerning lines +kernRE = re.compile( + r"([.A-Za-z0-9_]+)" # leftchar + r"\s+" + r"([.A-Za-z0-9_]+)" # rightchar + r"\s+" + r"(-?\d+)" # value + r"\s*" +) + +# regular expressions to parse composite info lines of the form: +# Aacute 2 ; PCC A 0 0 ; PCC acute 182 211 ; +compositeRE = re.compile( + r"([.A-Za-z0-9_]+)" # char name + r"\s+" + r"(\d+)" # number of parts + r"\s*;\s*" +) +componentRE = re.compile( + r"PCC\s+" # PPC + r"([.A-Za-z0-9_]+)" # base char name + r"\s+" + r"(-?\d+)" # x offset + r"\s+" + r"(-?\d+)" # y offset + r"\s*;\s*" +) + +preferredAttributeOrder = [ + "FontName", + "FullName", + "FamilyName", + "Weight", + "ItalicAngle", + "IsFixedPitch", + "FontBBox", + "UnderlinePosition", + "UnderlineThickness", + "Version", + "Notice", + "EncodingScheme", + "CapHeight", + "XHeight", + "Ascender", + "Descender", +] + + +class error(Exception): + pass + + +class AFM(object): + _attrs = None + + _keywords = [ + "StartFontMetrics", + "EndFontMetrics", + "StartCharMetrics", + "EndCharMetrics", + "StartKernData", + "StartKernPairs", + "EndKernPairs", + "EndKernData", + "StartComposites", + "EndComposites", + ] + + def __init__(self, path=None): + """AFM file reader. + + Instantiating an object with a path name will cause the file to be opened, + read, and parsed. Alternatively the path can be left unspecified, and a + file can be parsed later with the :meth:`read` method.""" + self._attrs = {} + self._chars = {} + self._kerning = {} + self._index = {} + self._comments = [] + self._composites = {} + if path is not None: + self.read(path) + + def read(self, path): + """Opens, reads and parses a file.""" + lines = readlines(path) + for line in lines: + if not line.strip(): + continue + m = identifierRE.match(line) + if m is None: + raise error("syntax error in AFM file: " + repr(line)) + + pos = m.regs[1][1] + word = line[:pos] + rest = line[pos:].strip() + if word in self._keywords: + continue + if word == "C": + self.parsechar(rest) + elif word == "KPX": + self.parsekernpair(rest) + elif word == "CC": + self.parsecomposite(rest) + else: + self.parseattr(word, rest) + + def parsechar(self, rest): + m = charRE.match(rest) + if m is None: + raise error("syntax error in AFM file: " + repr(rest)) + things = [] + for fr, to in m.regs[1:]: + things.append(rest[fr:to]) + charname = things[2] + del things[2] + charnum, width, l, b, r, t = (int(thing) for thing in things) + self._chars[charname] = charnum, width, (l, b, r, t) + + def parsekernpair(self, rest): + m = kernRE.match(rest) + if m is None: + raise error("syntax error in AFM file: " + repr(rest)) + things = [] + for fr, to in m.regs[1:]: + things.append(rest[fr:to]) + leftchar, rightchar, value = things + value = int(value) + self._kerning[(leftchar, rightchar)] = value + + def parseattr(self, word, rest): + if word == "FontBBox": + l, b, r, t = [int(thing) for thing in rest.split()] + self._attrs[word] = l, b, r, t + elif word == "Comment": + self._comments.append(rest) + else: + try: + value = int(rest) + except (ValueError, OverflowError): + self._attrs[word] = rest + else: + self._attrs[word] = value + + def parsecomposite(self, rest): + m = compositeRE.match(rest) + if m is None: + raise error("syntax error in AFM file: " + repr(rest)) + charname = m.group(1) + ncomponents = int(m.group(2)) + rest = rest[m.regs[0][1] :] + components = [] + while True: + m = componentRE.match(rest) + if m is None: + raise error("syntax error in AFM file: " + repr(rest)) + basechar = m.group(1) + xoffset = int(m.group(2)) + yoffset = int(m.group(3)) + components.append((basechar, xoffset, yoffset)) + rest = rest[m.regs[0][1] :] + if not rest: + break + assert len(components) == ncomponents + self._composites[charname] = components + + def write(self, path, sep="\r"): + """Writes out an AFM font to the given path.""" + import time + + lines = [ + "StartFontMetrics 2.0", + "Comment Generated by afmLib; at %s" + % (time.strftime("%m/%d/%Y %H:%M:%S", time.localtime(time.time()))), + ] + + # write comments, assuming (possibly wrongly!) they should + # all appear at the top + for comment in self._comments: + lines.append("Comment " + comment) + + # write attributes, first the ones we know about, in + # a preferred order + attrs = self._attrs + for attr in preferredAttributeOrder: + if attr in attrs: + value = attrs[attr] + if attr == "FontBBox": + value = "%s %s %s %s" % value + lines.append(attr + " " + str(value)) + # then write the attributes we don't know about, + # in alphabetical order + items = sorted(attrs.items()) + for attr, value in items: + if attr in preferredAttributeOrder: + continue + lines.append(attr + " " + str(value)) + + # write char metrics + lines.append("StartCharMetrics " + repr(len(self._chars))) + items = [ + (charnum, (charname, width, box)) + for charname, (charnum, width, box) in self._chars.items() + ] + + def myKey(a): + """Custom key function to make sure unencoded chars (-1) + end up at the end of the list after sorting.""" + if a[0] == -1: + a = (0xFFFF,) + a[1:] # 0xffff is an arbitrary large number + return a + + items.sort(key=myKey) + + for charnum, (charname, width, (l, b, r, t)) in items: + lines.append( + "C %d ; WX %d ; N %s ; B %d %d %d %d ;" + % (charnum, width, charname, l, b, r, t) + ) + lines.append("EndCharMetrics") + + # write kerning info + lines.append("StartKernData") + lines.append("StartKernPairs " + repr(len(self._kerning))) + items = sorted(self._kerning.items()) + for (leftchar, rightchar), value in items: + lines.append("KPX %s %s %d" % (leftchar, rightchar, value)) + lines.append("EndKernPairs") + lines.append("EndKernData") + + if self._composites: + composites = sorted(self._composites.items()) + lines.append("StartComposites %s" % len(self._composites)) + for charname, components in composites: + line = "CC %s %s ;" % (charname, len(components)) + for basechar, xoffset, yoffset in components: + line = line + " PCC %s %s %s ;" % (basechar, xoffset, yoffset) + lines.append(line) + lines.append("EndComposites") + + lines.append("EndFontMetrics") + + writelines(path, lines, sep) + + def has_kernpair(self, pair): + """Returns `True` if the given glyph pair (specified as a tuple) exists + in the kerning dictionary.""" + return pair in self._kerning + + def kernpairs(self): + """Returns a list of all kern pairs in the kerning dictionary.""" + return list(self._kerning.keys()) + + def has_char(self, char): + """Returns `True` if the given glyph exists in the font.""" + return char in self._chars + + def chars(self): + """Returns a list of all glyph names in the font.""" + return list(self._chars.keys()) + + def comments(self): + """Returns all comments from the file.""" + return self._comments + + def addComment(self, comment): + """Adds a new comment to the file.""" + self._comments.append(comment) + + def addComposite(self, glyphName, components): + """Specifies that the glyph `glyphName` is made up of the given components. + The components list should be of the following form:: + + [ + (glyphname, xOffset, yOffset), + ... + ] + + """ + self._composites[glyphName] = components + + def __getattr__(self, attr): + if attr in self._attrs: + return self._attrs[attr] + else: + raise AttributeError(attr) + + def __setattr__(self, attr, value): + # all attrs *not* starting with "_" are consider to be AFM keywords + if attr[:1] == "_": + self.__dict__[attr] = value + else: + self._attrs[attr] = value + + def __delattr__(self, attr): + # all attrs *not* starting with "_" are consider to be AFM keywords + if attr[:1] == "_": + try: + del self.__dict__[attr] + except KeyError: + raise AttributeError(attr) + else: + try: + del self._attrs[attr] + except KeyError: + raise AttributeError(attr) + + def __getitem__(self, key): + if isinstance(key, tuple): + # key is a tuple, return the kernpair + return self._kerning[key] + else: + # return the metrics instead + return self._chars[key] + + def __setitem__(self, key, value): + if isinstance(key, tuple): + # key is a tuple, set kernpair + self._kerning[key] = value + else: + # set char metrics + self._chars[key] = value + + def __delitem__(self, key): + if isinstance(key, tuple): + # key is a tuple, del kernpair + del self._kerning[key] + else: + # del char metrics + del self._chars[key] + + def __repr__(self): + if hasattr(self, "FullName"): + return "" % self.FullName + else: + return "" % id(self) + + +def readlines(path): + with open(path, "r", encoding="ascii") as f: + data = f.read() + return data.splitlines() + + +def writelines(path, lines, sep="\r"): + with open(path, "w", encoding="ascii", newline=sep) as f: + f.write("\n".join(lines) + "\n") + + +if __name__ == "__main__": + import EasyDialogs + + path = EasyDialogs.AskFileForOpen() + if path: + afm = AFM(path) + char = "A" + if afm.has_char(char): + print(afm[char]) # print charnum, width and boundingbox + pair = ("A", "V") + if afm.has_kernpair(pair): + print(afm[pair]) # print kerning value for pair + print(afm.Version) # various other afm entries have become attributes + print(afm.Weight) + # afm.comments() returns a list of all Comment lines found in the AFM + print(afm.comments()) + # print afm.chars() + # print afm.kernpairs() + print(afm) + afm.write(path + ".muck") diff --git a/hug/lib/python3.12/site-packages/fontTools/agl.py b/hug/lib/python3.12/site-packages/fontTools/agl.py new file mode 100644 index 0000000000000000000000000000000000000000..5d686ef66cb0e7a55ab40bbcd311b8c3f273e286 --- /dev/null +++ b/hug/lib/python3.12/site-packages/fontTools/agl.py @@ -0,0 +1,5232 @@ +# -*- coding: utf-8 -*- +# The tables below are taken from +# https://github.com/adobe-type-tools/agl-aglfn/raw/4036a9ca80a62f64f9de4f7321a9a045ad0ecfd6/glyphlist.txt +# and +# https://github.com/adobe-type-tools/agl-aglfn/raw/4036a9ca80a62f64f9de4f7321a9a045ad0ecfd6/aglfn.txt +""" +Interface to the Adobe Glyph List + +This module exists to convert glyph names from the Adobe Glyph List +to their Unicode equivalents. Example usage: + + >>> from fontTools.agl import toUnicode + >>> toUnicode("nahiragana") + 'な' + +It also contains two dictionaries, ``UV2AGL`` and ``AGL2UV``, which map from +Unicode codepoints to AGL names and vice versa: + + >>> import fontTools + >>> fontTools.agl.UV2AGL[ord("?")] + 'question' + >>> fontTools.agl.AGL2UV["wcircumflex"] + 373 + +This is used by fontTools when it has to construct glyph names for a font which +doesn't include any (e.g. format 3.0 post tables). +""" + +from fontTools.misc.textTools import tostr +import re + +_aglText = """\ +# ----------------------------------------------------------- +# Copyright 2002-2019 Adobe (http://www.adobe.com/). +# +# Redistribution and use in source and binary forms, with or +# without modification, are permitted provided that the +# following conditions are met: +# +# Redistributions of source code must retain the above +# copyright notice, this list of conditions and the following +# disclaimer. +# +# Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials +# provided with the distribution. +# +# Neither the name of Adobe nor the names of its contributors +# may be used to endorse or promote products derived from this +# software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ----------------------------------------------------------- +# Name: Adobe Glyph List +# Table version: 2.0 +# Date: September 20, 2002 +# URL: https://github.com/adobe-type-tools/agl-aglfn +# +# Format: two semicolon-delimited fields: +# (1) glyph name--upper/lowercase letters and digits +# (2) Unicode scalar value--four uppercase hexadecimal digits +# +A;0041 +AE;00C6 +AEacute;01FC +AEmacron;01E2 +AEsmall;F7E6 +Aacute;00C1 +Aacutesmall;F7E1 +Abreve;0102 +Abreveacute;1EAE +Abrevecyrillic;04D0 +Abrevedotbelow;1EB6 +Abrevegrave;1EB0 +Abrevehookabove;1EB2 +Abrevetilde;1EB4 +Acaron;01CD +Acircle;24B6 +Acircumflex;00C2 +Acircumflexacute;1EA4 +Acircumflexdotbelow;1EAC +Acircumflexgrave;1EA6 +Acircumflexhookabove;1EA8 +Acircumflexsmall;F7E2 +Acircumflextilde;1EAA +Acute;F6C9 +Acutesmall;F7B4 +Acyrillic;0410 +Adblgrave;0200 +Adieresis;00C4 +Adieresiscyrillic;04D2 +Adieresismacron;01DE +Adieresissmall;F7E4 +Adotbelow;1EA0 +Adotmacron;01E0 +Agrave;00C0 +Agravesmall;F7E0 +Ahookabove;1EA2 +Aiecyrillic;04D4 +Ainvertedbreve;0202 +Alpha;0391 +Alphatonos;0386 +Amacron;0100 +Amonospace;FF21 +Aogonek;0104 +Aring;00C5 +Aringacute;01FA +Aringbelow;1E00 +Aringsmall;F7E5 +Asmall;F761 +Atilde;00C3 +Atildesmall;F7E3 +Aybarmenian;0531 +B;0042 +Bcircle;24B7 +Bdotaccent;1E02 +Bdotbelow;1E04 +Becyrillic;0411 +Benarmenian;0532 +Beta;0392 +Bhook;0181 +Blinebelow;1E06 +Bmonospace;FF22 +Brevesmall;F6F4 +Bsmall;F762 +Btopbar;0182 +C;0043 +Caarmenian;053E +Cacute;0106 +Caron;F6CA +Caronsmall;F6F5 +Ccaron;010C +Ccedilla;00C7 +Ccedillaacute;1E08 +Ccedillasmall;F7E7 +Ccircle;24B8 +Ccircumflex;0108 +Cdot;010A +Cdotaccent;010A +Cedillasmall;F7B8 +Chaarmenian;0549 +Cheabkhasiancyrillic;04BC +Checyrillic;0427 +Chedescenderabkhasiancyrillic;04BE +Chedescendercyrillic;04B6 +Chedieresiscyrillic;04F4 +Cheharmenian;0543 +Chekhakassiancyrillic;04CB +Cheverticalstrokecyrillic;04B8 +Chi;03A7 +Chook;0187 +Circumflexsmall;F6F6 +Cmonospace;FF23 +Coarmenian;0551 +Csmall;F763 +D;0044 +DZ;01F1 +DZcaron;01C4 +Daarmenian;0534 +Dafrican;0189 +Dcaron;010E +Dcedilla;1E10 +Dcircle;24B9 +Dcircumflexbelow;1E12 +Dcroat;0110 +Ddotaccent;1E0A +Ddotbelow;1E0C +Decyrillic;0414 +Deicoptic;03EE +Delta;2206 +Deltagreek;0394 +Dhook;018A +Dieresis;F6CB +DieresisAcute;F6CC +DieresisGrave;F6CD +Dieresissmall;F7A8 +Digammagreek;03DC +Djecyrillic;0402 +Dlinebelow;1E0E +Dmonospace;FF24 +Dotaccentsmall;F6F7 +Dslash;0110 +Dsmall;F764 +Dtopbar;018B +Dz;01F2 +Dzcaron;01C5 +Dzeabkhasiancyrillic;04E0 +Dzecyrillic;0405 +Dzhecyrillic;040F +E;0045 +Eacute;00C9 +Eacutesmall;F7E9 +Ebreve;0114 +Ecaron;011A +Ecedillabreve;1E1C +Echarmenian;0535 +Ecircle;24BA +Ecircumflex;00CA +Ecircumflexacute;1EBE +Ecircumflexbelow;1E18 +Ecircumflexdotbelow;1EC6 +Ecircumflexgrave;1EC0 +Ecircumflexhookabove;1EC2 +Ecircumflexsmall;F7EA +Ecircumflextilde;1EC4 +Ecyrillic;0404 +Edblgrave;0204 +Edieresis;00CB +Edieresissmall;F7EB +Edot;0116 +Edotaccent;0116 +Edotbelow;1EB8 +Efcyrillic;0424 +Egrave;00C8 +Egravesmall;F7E8 +Eharmenian;0537 +Ehookabove;1EBA +Eightroman;2167 +Einvertedbreve;0206 +Eiotifiedcyrillic;0464 +Elcyrillic;041B +Elevenroman;216A +Emacron;0112 +Emacronacute;1E16 +Emacrongrave;1E14 +Emcyrillic;041C +Emonospace;FF25 +Encyrillic;041D +Endescendercyrillic;04A2 +Eng;014A +Enghecyrillic;04A4 +Enhookcyrillic;04C7 +Eogonek;0118 +Eopen;0190 +Epsilon;0395 +Epsilontonos;0388 +Ercyrillic;0420 +Ereversed;018E +Ereversedcyrillic;042D +Escyrillic;0421 +Esdescendercyrillic;04AA +Esh;01A9 +Esmall;F765 +Eta;0397 +Etarmenian;0538 +Etatonos;0389 +Eth;00D0 +Ethsmall;F7F0 +Etilde;1EBC +Etildebelow;1E1A +Euro;20AC +Ezh;01B7 +Ezhcaron;01EE +Ezhreversed;01B8 +F;0046 +Fcircle;24BB +Fdotaccent;1E1E +Feharmenian;0556 +Feicoptic;03E4 +Fhook;0191 +Fitacyrillic;0472 +Fiveroman;2164 +Fmonospace;FF26 +Fourroman;2163 +Fsmall;F766 +G;0047 +GBsquare;3387 +Gacute;01F4 +Gamma;0393 +Gammaafrican;0194 +Gangiacoptic;03EA +Gbreve;011E +Gcaron;01E6 +Gcedilla;0122 +Gcircle;24BC +Gcircumflex;011C +Gcommaaccent;0122 +Gdot;0120 +Gdotaccent;0120 +Gecyrillic;0413 +Ghadarmenian;0542 +Ghemiddlehookcyrillic;0494 +Ghestrokecyrillic;0492 +Gheupturncyrillic;0490 +Ghook;0193 +Gimarmenian;0533 +Gjecyrillic;0403 +Gmacron;1E20 +Gmonospace;FF27 +Grave;F6CE +Gravesmall;F760 +Gsmall;F767 +Gsmallhook;029B +Gstroke;01E4 +H;0048 +H18533;25CF +H18543;25AA +H18551;25AB +H22073;25A1 +HPsquare;33CB +Haabkhasiancyrillic;04A8 +Hadescendercyrillic;04B2 +Hardsigncyrillic;042A +Hbar;0126 +Hbrevebelow;1E2A +Hcedilla;1E28 +Hcircle;24BD +Hcircumflex;0124 +Hdieresis;1E26 +Hdotaccent;1E22 +Hdotbelow;1E24 +Hmonospace;FF28 +Hoarmenian;0540 +Horicoptic;03E8 +Hsmall;F768 +Hungarumlaut;F6CF +Hungarumlautsmall;F6F8 +Hzsquare;3390 +I;0049 +IAcyrillic;042F +IJ;0132 +IUcyrillic;042E +Iacute;00CD +Iacutesmall;F7ED +Ibreve;012C +Icaron;01CF +Icircle;24BE +Icircumflex;00CE +Icircumflexsmall;F7EE +Icyrillic;0406 +Idblgrave;0208 +Idieresis;00CF +Idieresisacute;1E2E +Idieresiscyrillic;04E4 +Idieresissmall;F7EF +Idot;0130 +Idotaccent;0130 +Idotbelow;1ECA +Iebrevecyrillic;04D6 +Iecyrillic;0415 +Ifraktur;2111 +Igrave;00CC +Igravesmall;F7EC +Ihookabove;1EC8 +Iicyrillic;0418 +Iinvertedbreve;020A +Iishortcyrillic;0419 +Imacron;012A +Imacroncyrillic;04E2 +Imonospace;FF29 +Iniarmenian;053B +Iocyrillic;0401 +Iogonek;012E +Iota;0399 +Iotaafrican;0196 +Iotadieresis;03AA +Iotatonos;038A +Ismall;F769 +Istroke;0197 +Itilde;0128 +Itildebelow;1E2C +Izhitsacyrillic;0474 +Izhitsadblgravecyrillic;0476 +J;004A +Jaarmenian;0541 +Jcircle;24BF +Jcircumflex;0134 +Jecyrillic;0408 +Jheharmenian;054B +Jmonospace;FF2A +Jsmall;F76A +K;004B +KBsquare;3385 +KKsquare;33CD +Kabashkircyrillic;04A0 +Kacute;1E30 +Kacyrillic;041A +Kadescendercyrillic;049A +Kahookcyrillic;04C3 +Kappa;039A +Kastrokecyrillic;049E +Kaverticalstrokecyrillic;049C +Kcaron;01E8 +Kcedilla;0136 +Kcircle;24C0 +Kcommaaccent;0136 +Kdotbelow;1E32 +Keharmenian;0554 +Kenarmenian;053F +Khacyrillic;0425 +Kheicoptic;03E6 +Khook;0198 +Kjecyrillic;040C +Klinebelow;1E34 +Kmonospace;FF2B +Koppacyrillic;0480 +Koppagreek;03DE +Ksicyrillic;046E +Ksmall;F76B +L;004C +LJ;01C7 +LL;F6BF +Lacute;0139 +Lambda;039B +Lcaron;013D +Lcedilla;013B +Lcircle;24C1 +Lcircumflexbelow;1E3C +Lcommaaccent;013B +Ldot;013F +Ldotaccent;013F +Ldotbelow;1E36 +Ldotbelowmacron;1E38 +Liwnarmenian;053C +Lj;01C8 +Ljecyrillic;0409 +Llinebelow;1E3A +Lmonospace;FF2C +Lslash;0141 +Lslashsmall;F6F9 +Lsmall;F76C +M;004D +MBsquare;3386 +Macron;F6D0 +Macronsmall;F7AF +Macute;1E3E +Mcircle;24C2 +Mdotaccent;1E40 +Mdotbelow;1E42 +Menarmenian;0544 +Mmonospace;FF2D +Msmall;F76D +Mturned;019C +Mu;039C +N;004E +NJ;01CA +Nacute;0143 +Ncaron;0147 +Ncedilla;0145 +Ncircle;24C3 +Ncircumflexbelow;1E4A +Ncommaaccent;0145 +Ndotaccent;1E44 +Ndotbelow;1E46 +Nhookleft;019D +Nineroman;2168 +Nj;01CB +Njecyrillic;040A +Nlinebelow;1E48 +Nmonospace;FF2E +Nowarmenian;0546 +Nsmall;F76E +Ntilde;00D1 +Ntildesmall;F7F1 +Nu;039D +O;004F +OE;0152 +OEsmall;F6FA +Oacute;00D3 +Oacutesmall;F7F3 +Obarredcyrillic;04E8 +Obarreddieresiscyrillic;04EA +Obreve;014E +Ocaron;01D1 +Ocenteredtilde;019F +Ocircle;24C4 +Ocircumflex;00D4 +Ocircumflexacute;1ED0 +Ocircumflexdotbelow;1ED8 +Ocircumflexgrave;1ED2 +Ocircumflexhookabove;1ED4 +Ocircumflexsmall;F7F4 +Ocircumflextilde;1ED6 +Ocyrillic;041E +Odblacute;0150 +Odblgrave;020C +Odieresis;00D6 +Odieresiscyrillic;04E6 +Odieresissmall;F7F6 +Odotbelow;1ECC +Ogoneksmall;F6FB +Ograve;00D2 +Ogravesmall;F7F2 +Oharmenian;0555 +Ohm;2126 +Ohookabove;1ECE +Ohorn;01A0 +Ohornacute;1EDA +Ohorndotbelow;1EE2 +Ohorngrave;1EDC +Ohornhookabove;1EDE +Ohorntilde;1EE0 +Ohungarumlaut;0150 +Oi;01A2 +Oinvertedbreve;020E +Omacron;014C +Omacronacute;1E52 +Omacrongrave;1E50 +Omega;2126 +Omegacyrillic;0460 +Omegagreek;03A9 +Omegaroundcyrillic;047A +Omegatitlocyrillic;047C +Omegatonos;038F +Omicron;039F +Omicrontonos;038C +Omonospace;FF2F +Oneroman;2160 +Oogonek;01EA +Oogonekmacron;01EC +Oopen;0186 +Oslash;00D8 +Oslashacute;01FE +Oslashsmall;F7F8 +Osmall;F76F +Ostrokeacute;01FE +Otcyrillic;047E +Otilde;00D5 +Otildeacute;1E4C +Otildedieresis;1E4E +Otildesmall;F7F5 +P;0050 +Pacute;1E54 +Pcircle;24C5 +Pdotaccent;1E56 +Pecyrillic;041F +Peharmenian;054A +Pemiddlehookcyrillic;04A6 +Phi;03A6 +Phook;01A4 +Pi;03A0 +Piwrarmenian;0553 +Pmonospace;FF30 +Psi;03A8 +Psicyrillic;0470 +Psmall;F770 +Q;0051 +Qcircle;24C6 +Qmonospace;FF31 +Qsmall;F771 +R;0052 +Raarmenian;054C +Racute;0154 +Rcaron;0158 +Rcedilla;0156 +Rcircle;24C7 +Rcommaaccent;0156 +Rdblgrave;0210 +Rdotaccent;1E58 +Rdotbelow;1E5A +Rdotbelowmacron;1E5C +Reharmenian;0550 +Rfraktur;211C +Rho;03A1 +Ringsmall;F6FC +Rinvertedbreve;0212 +Rlinebelow;1E5E +Rmonospace;FF32 +Rsmall;F772 +Rsmallinverted;0281 +Rsmallinvertedsuperior;02B6 +S;0053 +SF010000;250C +SF020000;2514 +SF030000;2510 +SF040000;2518 +SF050000;253C +SF060000;252C +SF070000;2534 +SF080000;251C +SF090000;2524 +SF100000;2500 +SF110000;2502 +SF190000;2561 +SF200000;2562 +SF210000;2556 +SF220000;2555 +SF230000;2563 +SF240000;2551 +SF250000;2557 +SF260000;255D +SF270000;255C +SF280000;255B +SF360000;255E +SF370000;255F +SF380000;255A +SF390000;2554 +SF400000;2569 +SF410000;2566 +SF420000;2560 +SF430000;2550 +SF440000;256C +SF450000;2567 +SF460000;2568 +SF470000;2564 +SF480000;2565 +SF490000;2559 +SF500000;2558 +SF510000;2552 +SF520000;2553 +SF530000;256B +SF540000;256A +Sacute;015A +Sacutedotaccent;1E64 +Sampigreek;03E0 +Scaron;0160 +Scarondotaccent;1E66 +Scaronsmall;F6FD +Scedilla;015E +Schwa;018F +Schwacyrillic;04D8 +Schwadieresiscyrillic;04DA +Scircle;24C8 +Scircumflex;015C +Scommaaccent;0218 +Sdotaccent;1E60 +Sdotbelow;1E62 +Sdotbelowdotaccent;1E68 +Seharmenian;054D +Sevenroman;2166 +Shaarmenian;0547 +Shacyrillic;0428 +Shchacyrillic;0429 +Sheicoptic;03E2 +Shhacyrillic;04BA +Shimacoptic;03EC +Sigma;03A3 +Sixroman;2165 +Smonospace;FF33 +Softsigncyrillic;042C +Ssmall;F773 +Stigmagreek;03DA +T;0054 +Tau;03A4 +Tbar;0166 +Tcaron;0164 +Tcedilla;0162 +Tcircle;24C9 +Tcircumflexbelow;1E70 +Tcommaaccent;0162 +Tdotaccent;1E6A +Tdotbelow;1E6C +Tecyrillic;0422 +Tedescendercyrillic;04AC +Tenroman;2169 +Tetsecyrillic;04B4 +Theta;0398 +Thook;01AC +Thorn;00DE +Thornsmall;F7FE +Threeroman;2162 +Tildesmall;F6FE +Tiwnarmenian;054F +Tlinebelow;1E6E +Tmonospace;FF34 +Toarmenian;0539 +Tonefive;01BC +Tonesix;0184 +Tonetwo;01A7 +Tretroflexhook;01AE +Tsecyrillic;0426 +Tshecyrillic;040B +Tsmall;F774 +Twelveroman;216B +Tworoman;2161 +U;0055 +Uacute;00DA +Uacutesmall;F7FA +Ubreve;016C +Ucaron;01D3 +Ucircle;24CA +Ucircumflex;00DB +Ucircumflexbelow;1E76 +Ucircumflexsmall;F7FB +Ucyrillic;0423 +Udblacute;0170 +Udblgrave;0214 +Udieresis;00DC +Udieresisacute;01D7 +Udieresisbelow;1E72 +Udieresiscaron;01D9 +Udieresiscyrillic;04F0 +Udieresisgrave;01DB +Udieresismacron;01D5 +Udieresissmall;F7FC +Udotbelow;1EE4 +Ugrave;00D9 +Ugravesmall;F7F9 +Uhookabove;1EE6 +Uhorn;01AF +Uhornacute;1EE8 +Uhorndotbelow;1EF0 +Uhorngrave;1EEA +Uhornhookabove;1EEC +Uhorntilde;1EEE +Uhungarumlaut;0170 +Uhungarumlautcyrillic;04F2 +Uinvertedbreve;0216 +Ukcyrillic;0478 +Umacron;016A +Umacroncyrillic;04EE +Umacrondieresis;1E7A +Umonospace;FF35 +Uogonek;0172 +Upsilon;03A5 +Upsilon1;03D2 +Upsilonacutehooksymbolgreek;03D3 +Upsilonafrican;01B1 +Upsilondieresis;03AB +Upsilondieresishooksymbolgreek;03D4 +Upsilonhooksymbol;03D2 +Upsilontonos;038E +Uring;016E +Ushortcyrillic;040E +Usmall;F775 +Ustraightcyrillic;04AE +Ustraightstrokecyrillic;04B0 +Utilde;0168 +Utildeacute;1E78 +Utildebelow;1E74 +V;0056 +Vcircle;24CB +Vdotbelow;1E7E +Vecyrillic;0412 +Vewarmenian;054E +Vhook;01B2 +Vmonospace;FF36 +Voarmenian;0548 +Vsmall;F776 +Vtilde;1E7C +W;0057 +Wacute;1E82 +Wcircle;24CC +Wcircumflex;0174 +Wdieresis;1E84 +Wdotaccent;1E86 +Wdotbelow;1E88 +Wgrave;1E80 +Wmonospace;FF37 +Wsmall;F777 +X;0058 +Xcircle;24CD +Xdieresis;1E8C +Xdotaccent;1E8A +Xeharmenian;053D +Xi;039E +Xmonospace;FF38 +Xsmall;F778 +Y;0059 +Yacute;00DD +Yacutesmall;F7FD +Yatcyrillic;0462 +Ycircle;24CE +Ycircumflex;0176 +Ydieresis;0178 +Ydieresissmall;F7FF +Ydotaccent;1E8E +Ydotbelow;1EF4 +Yericyrillic;042B +Yerudieresiscyrillic;04F8 +Ygrave;1EF2 +Yhook;01B3 +Yhookabove;1EF6 +Yiarmenian;0545 +Yicyrillic;0407 +Yiwnarmenian;0552 +Ymonospace;FF39 +Ysmall;F779 +Ytilde;1EF8 +Yusbigcyrillic;046A +Yusbigiotifiedcyrillic;046C +Yuslittlecyrillic;0466 +Yuslittleiotifiedcyrillic;0468 +Z;005A +Zaarmenian;0536 +Zacute;0179 +Zcaron;017D +Zcaronsmall;F6FF +Zcircle;24CF +Zcircumflex;1E90 +Zdot;017B +Zdotaccent;017B +Zdotbelow;1E92 +Zecyrillic;0417 +Zedescendercyrillic;0498 +Zedieresiscyrillic;04DE +Zeta;0396 +Zhearmenian;053A +Zhebrevecyrillic;04C1 +Zhecyrillic;0416 +Zhedescendercyrillic;0496 +Zhedieresiscyrillic;04DC +Zlinebelow;1E94 +Zmonospace;FF3A +Zsmall;F77A +Zstroke;01B5 +a;0061 +aabengali;0986 +aacute;00E1 +aadeva;0906 +aagujarati;0A86 +aagurmukhi;0A06 +aamatragurmukhi;0A3E +aarusquare;3303 +aavowelsignbengali;09BE +aavowelsigndeva;093E +aavowelsigngujarati;0ABE +abbreviationmarkarmenian;055F +abbreviationsigndeva;0970 +abengali;0985 +abopomofo;311A +abreve;0103 +abreveacute;1EAF +abrevecyrillic;04D1 +abrevedotbelow;1EB7 +abrevegrave;1EB1 +abrevehookabove;1EB3 +abrevetilde;1EB5 +acaron;01CE +acircle;24D0 +acircumflex;00E2 +acircumflexacute;1EA5 +acircumflexdotbelow;1EAD +acircumflexgrave;1EA7 +acircumflexhookabove;1EA9 +acircumflextilde;1EAB +acute;00B4 +acutebelowcmb;0317 +acutecmb;0301 +acutecomb;0301 +acutedeva;0954 +acutelowmod;02CF +acutetonecmb;0341 +acyrillic;0430 +adblgrave;0201 +addakgurmukhi;0A71 +adeva;0905 +adieresis;00E4 +adieresiscyrillic;04D3 +adieresismacron;01DF +adotbelow;1EA1 +adotmacron;01E1 +ae;00E6 +aeacute;01FD +aekorean;3150 +aemacron;01E3 +afii00208;2015 +afii08941;20A4 +afii10017;0410 +afii10018;0411 +afii10019;0412 +afii10020;0413 +afii10021;0414 +afii10022;0415 +afii10023;0401 +afii10024;0416 +afii10025;0417 +afii10026;0418 +afii10027;0419 +afii10028;041A +afii10029;041B +afii10030;041C +afii10031;041D +afii10032;041E +afii10033;041F +afii10034;0420 +afii10035;0421 +afii10036;0422 +afii10037;0423 +afii10038;0424 +afii10039;0425 +afii10040;0426 +afii10041;0427 +afii10042;0428 +afii10043;0429 +afii10044;042A +afii10045;042B +afii10046;042C +afii10047;042D +afii10048;042E +afii10049;042F +afii10050;0490 +afii10051;0402 +afii10052;0403 +afii10053;0404 +afii10054;0405 +afii10055;0406 +afii10056;0407 +afii10057;0408 +afii10058;0409 +afii10059;040A +afii10060;040B +afii10061;040C +afii10062;040E +afii10063;F6C4 +afii10064;F6C5 +afii10065;0430 +afii10066;0431 +afii10067;0432 +afii10068;0433 +afii10069;0434 +afii10070;0435 +afii10071;0451 +afii10072;0436 +afii10073;0437 +afii10074;0438 +afii10075;0439 +afii10076;043A +afii10077;043B +afii10078;043C +afii10079;043D +afii10080;043E +afii10081;043F +afii10082;0440 +afii10083;0441 +afii10084;0442 +afii10085;0443 +afii10086;0444 +afii10087;0445 +afii10088;0446 +afii10089;0447 +afii10090;0448 +afii10091;0449 +afii10092;044A +afii10093;044B +afii10094;044C +afii10095;044D +afii10096;044E +afii10097;044F +afii10098;0491 +afii10099;0452 +afii10100;0453 +afii10101;0454 +afii10102;0455 +afii10103;0456 +afii10104;0457 +afii10105;0458 +afii10106;0459 +afii10107;045A +afii10108;045B +afii10109;045C +afii10110;045E +afii10145;040F +afii10146;0462 +afii10147;0472 +afii10148;0474 +afii10192;F6C6 +afii10193;045F +afii10194;0463 +afii10195;0473 +afii10196;0475 +afii10831;F6C7 +afii10832;F6C8 +afii10846;04D9 +afii299;200E +afii300;200F +afii301;200D +afii57381;066A +afii57388;060C +afii57392;0660 +afii57393;0661 +afii57394;0662 +afii57395;0663 +afii57396;0664 +afii57397;0665 +afii57398;0666 +afii57399;0667 +afii57400;0668 +afii57401;0669 +afii57403;061B +afii57407;061F +afii57409;0621 +afii57410;0622 +afii57411;0623 +afii57412;0624 +afii57413;0625 +afii57414;0626 +afii57415;0627 +afii57416;0628 +afii57417;0629 +afii57418;062A +afii57419;062B +afii57420;062C +afii57421;062D +afii57422;062E +afii57423;062F +afii57424;0630 +afii57425;0631 +afii57426;0632 +afii57427;0633 +afii57428;0634 +afii57429;0635 +afii57430;0636 +afii57431;0637 +afii57432;0638 +afii57433;0639 +afii57434;063A +afii57440;0640 +afii57441;0641 +afii57442;0642 +afii57443;0643 +afii57444;0644 +afii57445;0645 +afii57446;0646 +afii57448;0648 +afii57449;0649 +afii57450;064A +afii57451;064B +afii57452;064C +afii57453;064D +afii57454;064E +afii57455;064F +afii57456;0650 +afii57457;0651 +afii57458;0652 +afii57470;0647 +afii57505;06A4 +afii57506;067E +afii57507;0686 +afii57508;0698 +afii57509;06AF +afii57511;0679 +afii57512;0688 +afii57513;0691 +afii57514;06BA +afii57519;06D2 +afii57534;06D5 +afii57636;20AA +afii57645;05BE +afii57658;05C3 +afii57664;05D0 +afii57665;05D1 +afii57666;05D2 +afii57667;05D3 +afii57668;05D4 +afii57669;05D5 +afii57670;05D6 +afii57671;05D7 +afii57672;05D8 +afii57673;05D9 +afii57674;05DA +afii57675;05DB +afii57676;05DC +afii57677;05DD +afii57678;05DE +afii57679;05DF +afii57680;05E0 +afii57681;05E1 +afii57682;05E2 +afii57683;05E3 +afii57684;05E4 +afii57685;05E5 +afii57686;05E6 +afii57687;05E7 +afii57688;05E8 +afii57689;05E9 +afii57690;05EA +afii57694;FB2A +afii57695;FB2B +afii57700;FB4B +afii57705;FB1F +afii57716;05F0 +afii57717;05F1 +afii57718;05F2 +afii57723;FB35 +afii57793;05B4 +afii57794;05B5 +afii57795;05B6 +afii57796;05BB +afii57797;05B8 +afii57798;05B7 +afii57799;05B0 +afii57800;05B2 +afii57801;05B1 +afii57802;05B3 +afii57803;05C2 +afii57804;05C1 +afii57806;05B9 +afii57807;05BC +afii57839;05BD +afii57841;05BF +afii57842;05C0 +afii57929;02BC +afii61248;2105 +afii61289;2113 +afii61352;2116 +afii61573;202C +afii61574;202D +afii61575;202E +afii61664;200C +afii63167;066D +afii64937;02BD +agrave;00E0 +agujarati;0A85 +agurmukhi;0A05 +ahiragana;3042 +ahookabove;1EA3 +aibengali;0990 +aibopomofo;311E +aideva;0910 +aiecyrillic;04D5 +aigujarati;0A90 +aigurmukhi;0A10 +aimatragurmukhi;0A48 +ainarabic;0639 +ainfinalarabic;FECA +aininitialarabic;FECB +ainmedialarabic;FECC +ainvertedbreve;0203 +aivowelsignbengali;09C8 +aivowelsigndeva;0948 +aivowelsigngujarati;0AC8 +akatakana;30A2 +akatakanahalfwidth;FF71 +akorean;314F +alef;05D0 +alefarabic;0627 +alefdageshhebrew;FB30 +aleffinalarabic;FE8E +alefhamzaabovearabic;0623 +alefhamzaabovefinalarabic;FE84 +alefhamzabelowarabic;0625 +alefhamzabelowfinalarabic;FE88 +alefhebrew;05D0 +aleflamedhebrew;FB4F +alefmaddaabovearabic;0622 +alefmaddaabovefinalarabic;FE82 +alefmaksuraarabic;0649 +alefmaksurafinalarabic;FEF0 +alefmaksurainitialarabic;FEF3 +alefmaksuramedialarabic;FEF4 +alefpatahhebrew;FB2E +alefqamatshebrew;FB2F +aleph;2135 +allequal;224C +alpha;03B1 +alphatonos;03AC +amacron;0101 +amonospace;FF41 +ampersand;0026 +ampersandmonospace;FF06 +ampersandsmall;F726 +amsquare;33C2 +anbopomofo;3122 +angbopomofo;3124 +angkhankhuthai;0E5A +angle;2220 +anglebracketleft;3008 +anglebracketleftvertical;FE3F +anglebracketright;3009 +anglebracketrightvertical;FE40 +angleleft;2329 +angleright;232A +angstrom;212B +anoteleia;0387 +anudattadeva;0952 +anusvarabengali;0982 +anusvaradeva;0902 +anusvaragujarati;0A82 +aogonek;0105 +apaatosquare;3300 +aparen;249C +apostrophearmenian;055A +apostrophemod;02BC +apple;F8FF +approaches;2250 +approxequal;2248 +approxequalorimage;2252 +approximatelyequal;2245 +araeaekorean;318E +araeakorean;318D +arc;2312 +arighthalfring;1E9A +aring;00E5 +aringacute;01FB +aringbelow;1E01 +arrowboth;2194 +arrowdashdown;21E3 +arrowdashleft;21E0 +arrowdashright;21E2 +arrowdashup;21E1 +arrowdblboth;21D4 +arrowdbldown;21D3 +arrowdblleft;21D0 +arrowdblright;21D2 +arrowdblup;21D1 +arrowdown;2193 +arrowdownleft;2199 +arrowdownright;2198 +arrowdownwhite;21E9 +arrowheaddownmod;02C5 +arrowheadleftmod;02C2 +arrowheadrightmod;02C3 +arrowheadupmod;02C4 +arrowhorizex;F8E7 +arrowleft;2190 +arrowleftdbl;21D0 +arrowleftdblstroke;21CD +arrowleftoverright;21C6 +arrowleftwhite;21E6 +arrowright;2192 +arrowrightdblstroke;21CF +arrowrightheavy;279E +arrowrightoverleft;21C4 +arrowrightwhite;21E8 +arrowtableft;21E4 +arrowtabright;21E5 +arrowup;2191 +arrowupdn;2195 +arrowupdnbse;21A8 +arrowupdownbase;21A8 +arrowupleft;2196 +arrowupleftofdown;21C5 +arrowupright;2197 +arrowupwhite;21E7 +arrowvertex;F8E6 +asciicircum;005E +asciicircummonospace;FF3E +asciitilde;007E +asciitildemonospace;FF5E +ascript;0251 +ascriptturned;0252 +asmallhiragana;3041 +asmallkatakana;30A1 +asmallkatakanahalfwidth;FF67 +asterisk;002A +asteriskaltonearabic;066D +asteriskarabic;066D +asteriskmath;2217 +asteriskmonospace;FF0A +asterisksmall;FE61 +asterism;2042 +asuperior;F6E9 +asymptoticallyequal;2243 +at;0040 +atilde;00E3 +atmonospace;FF20 +atsmall;FE6B +aturned;0250 +aubengali;0994 +aubopomofo;3120 +audeva;0914 +augujarati;0A94 +augurmukhi;0A14 +aulengthmarkbengali;09D7 +aumatragurmukhi;0A4C +auvowelsignbengali;09CC +auvowelsigndeva;094C +auvowelsigngujarati;0ACC +avagrahadeva;093D +aybarmenian;0561 +ayin;05E2 +ayinaltonehebrew;FB20 +ayinhebrew;05E2 +b;0062 +babengali;09AC +backslash;005C +backslashmonospace;FF3C +badeva;092C +bagujarati;0AAC +bagurmukhi;0A2C +bahiragana;3070 +bahtthai;0E3F +bakatakana;30D0 +bar;007C +barmonospace;FF5C +bbopomofo;3105 +bcircle;24D1 +bdotaccent;1E03 +bdotbelow;1E05 +beamedsixteenthnotes;266C +because;2235 +becyrillic;0431 +beharabic;0628 +behfinalarabic;FE90 +behinitialarabic;FE91 +behiragana;3079 +behmedialarabic;FE92 +behmeeminitialarabic;FC9F +behmeemisolatedarabic;FC08 +behnoonfinalarabic;FC6D +bekatakana;30D9 +benarmenian;0562 +bet;05D1 +beta;03B2 +betasymbolgreek;03D0 +betdagesh;FB31 +betdageshhebrew;FB31 +bethebrew;05D1 +betrafehebrew;FB4C +bhabengali;09AD +bhadeva;092D +bhagujarati;0AAD +bhagurmukhi;0A2D +bhook;0253 +bihiragana;3073 +bikatakana;30D3 +bilabialclick;0298 +bindigurmukhi;0A02 +birusquare;3331 +blackcircle;25CF +blackdiamond;25C6 +blackdownpointingtriangle;25BC +blackleftpointingpointer;25C4 +blackleftpointingtriangle;25C0 +blacklenticularbracketleft;3010 +blacklenticularbracketleftvertical;FE3B +blacklenticularbracketright;3011 +blacklenticularbracketrightvertical;FE3C +blacklowerlefttriangle;25E3 +blacklowerrighttriangle;25E2 +blackrectangle;25AC +blackrightpointingpointer;25BA +blackrightpointingtriangle;25B6 +blacksmallsquare;25AA +blacksmilingface;263B +blacksquare;25A0 +blackstar;2605 +blackupperlefttriangle;25E4 +blackupperrighttriangle;25E5 +blackuppointingsmalltriangle;25B4 +blackuppointingtriangle;25B2 +blank;2423 +blinebelow;1E07 +block;2588 +bmonospace;FF42 +bobaimaithai;0E1A +bohiragana;307C +bokatakana;30DC +bparen;249D +bqsquare;33C3 +braceex;F8F4 +braceleft;007B +braceleftbt;F8F3 +braceleftmid;F8F2 +braceleftmonospace;FF5B +braceleftsmall;FE5B +bracelefttp;F8F1 +braceleftvertical;FE37 +braceright;007D +bracerightbt;F8FE +bracerightmid;F8FD +bracerightmonospace;FF5D +bracerightsmall;FE5C +bracerighttp;F8FC +bracerightvertical;FE38 +bracketleft;005B +bracketleftbt;F8F0 +bracketleftex;F8EF +bracketleftmonospace;FF3B +bracketlefttp;F8EE +bracketright;005D +bracketrightbt;F8FB +bracketrightex;F8FA +bracketrightmonospace;FF3D +bracketrighttp;F8F9 +breve;02D8 +brevebelowcmb;032E +brevecmb;0306 +breveinvertedbelowcmb;032F +breveinvertedcmb;0311 +breveinverteddoublecmb;0361 +bridgebelowcmb;032A +bridgeinvertedbelowcmb;033A +brokenbar;00A6 +bstroke;0180 +bsuperior;F6EA +btopbar;0183 +buhiragana;3076 +bukatakana;30D6 +bullet;2022 +bulletinverse;25D8 +bulletoperator;2219 +bullseye;25CE +c;0063 +caarmenian;056E +cabengali;099A +cacute;0107 +cadeva;091A +cagujarati;0A9A +cagurmukhi;0A1A +calsquare;3388 +candrabindubengali;0981 +candrabinducmb;0310 +candrabindudeva;0901 +candrabindugujarati;0A81 +capslock;21EA +careof;2105 +caron;02C7 +caronbelowcmb;032C +caroncmb;030C +carriagereturn;21B5 +cbopomofo;3118 +ccaron;010D +ccedilla;00E7 +ccedillaacute;1E09 +ccircle;24D2 +ccircumflex;0109 +ccurl;0255 +cdot;010B +cdotaccent;010B +cdsquare;33C5 +cedilla;00B8 +cedillacmb;0327 +cent;00A2 +centigrade;2103 +centinferior;F6DF +centmonospace;FFE0 +centoldstyle;F7A2 +centsuperior;F6E0 +chaarmenian;0579 +chabengali;099B +chadeva;091B +chagujarati;0A9B +chagurmukhi;0A1B +chbopomofo;3114 +cheabkhasiancyrillic;04BD +checkmark;2713 +checyrillic;0447 +chedescenderabkhasiancyrillic;04BF +chedescendercyrillic;04B7 +chedieresiscyrillic;04F5 +cheharmenian;0573 +chekhakassiancyrillic;04CC +cheverticalstrokecyrillic;04B9 +chi;03C7 +chieuchacirclekorean;3277 +chieuchaparenkorean;3217 +chieuchcirclekorean;3269 +chieuchkorean;314A +chieuchparenkorean;3209 +chochangthai;0E0A +chochanthai;0E08 +chochingthai;0E09 +chochoethai;0E0C +chook;0188 +cieucacirclekorean;3276 +cieucaparenkorean;3216 +cieuccirclekorean;3268 +cieuckorean;3148 +cieucparenkorean;3208 +cieucuparenkorean;321C +circle;25CB +circlemultiply;2297 +circleot;2299 +circleplus;2295 +circlepostalmark;3036 +circlewithlefthalfblack;25D0 +circlewithrighthalfblack;25D1 +circumflex;02C6 +circumflexbelowcmb;032D +circumflexcmb;0302 +clear;2327 +clickalveolar;01C2 +clickdental;01C0 +clicklateral;01C1 +clickretroflex;01C3 +club;2663 +clubsuitblack;2663 +clubsuitwhite;2667 +cmcubedsquare;33A4 +cmonospace;FF43 +cmsquaredsquare;33A0 +coarmenian;0581 +colon;003A +colonmonetary;20A1 +colonmonospace;FF1A +colonsign;20A1 +colonsmall;FE55 +colontriangularhalfmod;02D1 +colontriangularmod;02D0 +comma;002C +commaabovecmb;0313 +commaaboverightcmb;0315 +commaaccent;F6C3 +commaarabic;060C +commaarmenian;055D +commainferior;F6E1 +commamonospace;FF0C +commareversedabovecmb;0314 +commareversedmod;02BD +commasmall;FE50 +commasuperior;F6E2 +commaturnedabovecmb;0312 +commaturnedmod;02BB +compass;263C +congruent;2245 +contourintegral;222E +control;2303 +controlACK;0006 +controlBEL;0007 +controlBS;0008 +controlCAN;0018 +controlCR;000D +controlDC1;0011 +controlDC2;0012 +controlDC3;0013 +controlDC4;0014 +controlDEL;007F +controlDLE;0010 +controlEM;0019 +controlENQ;0005 +controlEOT;0004 +controlESC;001B +controlETB;0017 +controlETX;0003 +controlFF;000C +controlFS;001C +controlGS;001D +controlHT;0009 +controlLF;000A +controlNAK;0015 +controlRS;001E +controlSI;000F +controlSO;000E +controlSOT;0002 +controlSTX;0001 +controlSUB;001A +controlSYN;0016 +controlUS;001F +controlVT;000B +copyright;00A9 +copyrightsans;F8E9 +copyrightserif;F6D9 +cornerbracketleft;300C +cornerbracketlefthalfwidth;FF62 +cornerbracketleftvertical;FE41 +cornerbracketright;300D +cornerbracketrighthalfwidth;FF63 +cornerbracketrightvertical;FE42 +corporationsquare;337F +cosquare;33C7 +coverkgsquare;33C6 +cparen;249E +cruzeiro;20A2 +cstretched;0297 +curlyand;22CF +curlyor;22CE +currency;00A4 +cyrBreve;F6D1 +cyrFlex;F6D2 +cyrbreve;F6D4 +cyrflex;F6D5 +d;0064 +daarmenian;0564 +dabengali;09A6 +dadarabic;0636 +dadeva;0926 +dadfinalarabic;FEBE +dadinitialarabic;FEBF +dadmedialarabic;FEC0 +dagesh;05BC +dageshhebrew;05BC +dagger;2020 +daggerdbl;2021 +dagujarati;0AA6 +dagurmukhi;0A26 +dahiragana;3060 +dakatakana;30C0 +dalarabic;062F +dalet;05D3 +daletdagesh;FB33 +daletdageshhebrew;FB33 +dalethatafpatah;05D3 05B2 +dalethatafpatahhebrew;05D3 05B2 +dalethatafsegol;05D3 05B1 +dalethatafsegolhebrew;05D3 05B1 +dalethebrew;05D3 +dalethiriq;05D3 05B4 +dalethiriqhebrew;05D3 05B4 +daletholam;05D3 05B9 +daletholamhebrew;05D3 05B9 +daletpatah;05D3 05B7 +daletpatahhebrew;05D3 05B7 +daletqamats;05D3 05B8 +daletqamatshebrew;05D3 05B8 +daletqubuts;05D3 05BB +daletqubutshebrew;05D3 05BB +daletsegol;05D3 05B6 +daletsegolhebrew;05D3 05B6 +daletsheva;05D3 05B0 +daletshevahebrew;05D3 05B0 +dalettsere;05D3 05B5 +dalettserehebrew;05D3 05B5 +dalfinalarabic;FEAA +dammaarabic;064F +dammalowarabic;064F +dammatanaltonearabic;064C +dammatanarabic;064C +danda;0964 +dargahebrew;05A7 +dargalefthebrew;05A7 +dasiapneumatacyrilliccmb;0485 +dblGrave;F6D3 +dblanglebracketleft;300A +dblanglebracketleftvertical;FE3D +dblanglebracketright;300B +dblanglebracketrightvertical;FE3E +dblarchinvertedbelowcmb;032B +dblarrowleft;21D4 +dblarrowright;21D2 +dbldanda;0965 +dblgrave;F6D6 +dblgravecmb;030F +dblintegral;222C +dbllowline;2017 +dbllowlinecmb;0333 +dbloverlinecmb;033F +dblprimemod;02BA +dblverticalbar;2016 +dblverticallineabovecmb;030E +dbopomofo;3109 +dbsquare;33C8 +dcaron;010F +dcedilla;1E11 +dcircle;24D3 +dcircumflexbelow;1E13 +dcroat;0111 +ddabengali;09A1 +ddadeva;0921 +ddagujarati;0AA1 +ddagurmukhi;0A21 +ddalarabic;0688 +ddalfinalarabic;FB89 +dddhadeva;095C +ddhabengali;09A2 +ddhadeva;0922 +ddhagujarati;0AA2 +ddhagurmukhi;0A22 +ddotaccent;1E0B +ddotbelow;1E0D +decimalseparatorarabic;066B +decimalseparatorpersian;066B +decyrillic;0434 +degree;00B0 +dehihebrew;05AD +dehiragana;3067 +deicoptic;03EF +dekatakana;30C7 +deleteleft;232B +deleteright;2326 +delta;03B4 +deltaturned;018D +denominatorminusonenumeratorbengali;09F8 +dezh;02A4 +dhabengali;09A7 +dhadeva;0927 +dhagujarati;0AA7 +dhagurmukhi;0A27 +dhook;0257 +dialytikatonos;0385 +dialytikatonoscmb;0344 +diamond;2666 +diamondsuitwhite;2662 +dieresis;00A8 +dieresisacute;F6D7 +dieresisbelowcmb;0324 +dieresiscmb;0308 +dieresisgrave;F6D8 +dieresistonos;0385 +dihiragana;3062 +dikatakana;30C2 +dittomark;3003 +divide;00F7 +divides;2223 +divisionslash;2215 +djecyrillic;0452 +dkshade;2593 +dlinebelow;1E0F +dlsquare;3397 +dmacron;0111 +dmonospace;FF44 +dnblock;2584 +dochadathai;0E0E +dodekthai;0E14 +dohiragana;3069 +dokatakana;30C9 +dollar;0024 +dollarinferior;F6E3 +dollarmonospace;FF04 +dollaroldstyle;F724 +dollarsmall;FE69 +dollarsuperior;F6E4 +dong;20AB +dorusquare;3326 +dotaccent;02D9 +dotaccentcmb;0307 +dotbelowcmb;0323 +dotbelowcomb;0323 +dotkatakana;30FB +dotlessi;0131 +dotlessj;F6BE +dotlessjstrokehook;0284 +dotmath;22C5 +dottedcircle;25CC +doubleyodpatah;FB1F +doubleyodpatahhebrew;FB1F +downtackbelowcmb;031E +downtackmod;02D5 +dparen;249F +dsuperior;F6EB +dtail;0256 +dtopbar;018C +duhiragana;3065 +dukatakana;30C5 +dz;01F3 +dzaltone;02A3 +dzcaron;01C6 +dzcurl;02A5 +dzeabkhasiancyrillic;04E1 +dzecyrillic;0455 +dzhecyrillic;045F +e;0065 +eacute;00E9 +earth;2641 +ebengali;098F +ebopomofo;311C +ebreve;0115 +ecandradeva;090D +ecandragujarati;0A8D +ecandravowelsigndeva;0945 +ecandravowelsigngujarati;0AC5 +ecaron;011B +ecedillabreve;1E1D +echarmenian;0565 +echyiwnarmenian;0587 +ecircle;24D4 +ecircumflex;00EA +ecircumflexacute;1EBF +ecircumflexbelow;1E19 +ecircumflexdotbelow;1EC7 +ecircumflexgrave;1EC1 +ecircumflexhookabove;1EC3 +ecircumflextilde;1EC5 +ecyrillic;0454 +edblgrave;0205 +edeva;090F +edieresis;00EB +edot;0117 +edotaccent;0117 +edotbelow;1EB9 +eegurmukhi;0A0F +eematragurmukhi;0A47 +efcyrillic;0444 +egrave;00E8 +egujarati;0A8F +eharmenian;0567 +ehbopomofo;311D +ehiragana;3048 +ehookabove;1EBB +eibopomofo;311F +eight;0038 +eightarabic;0668 +eightbengali;09EE +eightcircle;2467 +eightcircleinversesansserif;2791 +eightdeva;096E +eighteencircle;2471 +eighteenparen;2485 +eighteenperiod;2499 +eightgujarati;0AEE +eightgurmukhi;0A6E +eighthackarabic;0668 +eighthangzhou;3028 +eighthnotebeamed;266B +eightideographicparen;3227 +eightinferior;2088 +eightmonospace;FF18 +eightoldstyle;F738 +eightparen;247B +eightperiod;248F +eightpersian;06F8 +eightroman;2177 +eightsuperior;2078 +eightthai;0E58 +einvertedbreve;0207 +eiotifiedcyrillic;0465 +ekatakana;30A8 +ekatakanahalfwidth;FF74 +ekonkargurmukhi;0A74 +ekorean;3154 +elcyrillic;043B +element;2208 +elevencircle;246A +elevenparen;247E +elevenperiod;2492 +elevenroman;217A +ellipsis;2026 +ellipsisvertical;22EE +emacron;0113 +emacronacute;1E17 +emacrongrave;1E15 +emcyrillic;043C +emdash;2014 +emdashvertical;FE31 +emonospace;FF45 +emphasismarkarmenian;055B +emptyset;2205 +enbopomofo;3123 +encyrillic;043D +endash;2013 +endashvertical;FE32 +endescendercyrillic;04A3 +eng;014B +engbopomofo;3125 +enghecyrillic;04A5 +enhookcyrillic;04C8 +enspace;2002 +eogonek;0119 +eokorean;3153 +eopen;025B +eopenclosed;029A +eopenreversed;025C +eopenreversedclosed;025E +eopenreversedhook;025D +eparen;24A0 +epsilon;03B5 +epsilontonos;03AD +equal;003D +equalmonospace;FF1D +equalsmall;FE66 +equalsuperior;207C +equivalence;2261 +erbopomofo;3126 +ercyrillic;0440 +ereversed;0258 +ereversedcyrillic;044D +escyrillic;0441 +esdescendercyrillic;04AB +esh;0283 +eshcurl;0286 +eshortdeva;090E +eshortvowelsigndeva;0946 +eshreversedloop;01AA +eshsquatreversed;0285 +esmallhiragana;3047 +esmallkatakana;30A7 +esmallkatakanahalfwidth;FF6A +estimated;212E +esuperior;F6EC +eta;03B7 +etarmenian;0568 +etatonos;03AE +eth;00F0 +etilde;1EBD +etildebelow;1E1B +etnahtafoukhhebrew;0591 +etnahtafoukhlefthebrew;0591 +etnahtahebrew;0591 +etnahtalefthebrew;0591 +eturned;01DD +eukorean;3161 +euro;20AC +evowelsignbengali;09C7 +evowelsigndeva;0947 +evowelsigngujarati;0AC7 +exclam;0021 +exclamarmenian;055C +exclamdbl;203C +exclamdown;00A1 +exclamdownsmall;F7A1 +exclammonospace;FF01 +exclamsmall;F721 +existential;2203 +ezh;0292 +ezhcaron;01EF +ezhcurl;0293 +ezhreversed;01B9 +ezhtail;01BA +f;0066 +fadeva;095E +fagurmukhi;0A5E +fahrenheit;2109 +fathaarabic;064E +fathalowarabic;064E +fathatanarabic;064B +fbopomofo;3108 +fcircle;24D5 +fdotaccent;1E1F +feharabic;0641 +feharmenian;0586 +fehfinalarabic;FED2 +fehinitialarabic;FED3 +fehmedialarabic;FED4 +feicoptic;03E5 +female;2640 +ff;FB00 +ffi;FB03 +ffl;FB04 +fi;FB01 +fifteencircle;246E +fifteenparen;2482 +fifteenperiod;2496 +figuredash;2012 +filledbox;25A0 +filledrect;25AC +finalkaf;05DA +finalkafdagesh;FB3A +finalkafdageshhebrew;FB3A +finalkafhebrew;05DA +finalkafqamats;05DA 05B8 +finalkafqamatshebrew;05DA 05B8 +finalkafsheva;05DA 05B0 +finalkafshevahebrew;05DA 05B0 +finalmem;05DD +finalmemhebrew;05DD +finalnun;05DF +finalnunhebrew;05DF +finalpe;05E3 +finalpehebrew;05E3 +finaltsadi;05E5 +finaltsadihebrew;05E5 +firsttonechinese;02C9 +fisheye;25C9 +fitacyrillic;0473 +five;0035 +fivearabic;0665 +fivebengali;09EB +fivecircle;2464 +fivecircleinversesansserif;278E +fivedeva;096B +fiveeighths;215D +fivegujarati;0AEB +fivegurmukhi;0A6B +fivehackarabic;0665 +fivehangzhou;3025 +fiveideographicparen;3224 +fiveinferior;2085 +fivemonospace;FF15 +fiveoldstyle;F735 +fiveparen;2478 +fiveperiod;248C +fivepersian;06F5 +fiveroman;2174 +fivesuperior;2075 +fivethai;0E55 +fl;FB02 +florin;0192 +fmonospace;FF46 +fmsquare;3399 +fofanthai;0E1F +fofathai;0E1D +fongmanthai;0E4F +forall;2200 +four;0034 +fourarabic;0664 +fourbengali;09EA +fourcircle;2463 +fourcircleinversesansserif;278D +fourdeva;096A +fourgujarati;0AEA +fourgurmukhi;0A6A +fourhackarabic;0664 +fourhangzhou;3024 +fourideographicparen;3223 +fourinferior;2084 +fourmonospace;FF14 +fournumeratorbengali;09F7 +fouroldstyle;F734 +fourparen;2477 +fourperiod;248B +fourpersian;06F4 +fourroman;2173 +foursuperior;2074 +fourteencircle;246D +fourteenparen;2481 +fourteenperiod;2495 +fourthai;0E54 +fourthtonechinese;02CB +fparen;24A1 +fraction;2044 +franc;20A3 +g;0067 +gabengali;0997 +gacute;01F5 +gadeva;0917 +gafarabic;06AF +gaffinalarabic;FB93 +gafinitialarabic;FB94 +gafmedialarabic;FB95 +gagujarati;0A97 +gagurmukhi;0A17 +gahiragana;304C +gakatakana;30AC +gamma;03B3 +gammalatinsmall;0263 +gammasuperior;02E0 +gangiacoptic;03EB +gbopomofo;310D +gbreve;011F +gcaron;01E7 +gcedilla;0123 +gcircle;24D6 +gcircumflex;011D +gcommaaccent;0123 +gdot;0121 +gdotaccent;0121 +gecyrillic;0433 +gehiragana;3052 +gekatakana;30B2 +geometricallyequal;2251 +gereshaccenthebrew;059C +gereshhebrew;05F3 +gereshmuqdamhebrew;059D +germandbls;00DF +gershayimaccenthebrew;059E +gershayimhebrew;05F4 +getamark;3013 +ghabengali;0998 +ghadarmenian;0572 +ghadeva;0918 +ghagujarati;0A98 +ghagurmukhi;0A18 +ghainarabic;063A +ghainfinalarabic;FECE +ghaininitialarabic;FECF +ghainmedialarabic;FED0 +ghemiddlehookcyrillic;0495 +ghestrokecyrillic;0493 +gheupturncyrillic;0491 +ghhadeva;095A +ghhagurmukhi;0A5A +ghook;0260 +ghzsquare;3393 +gihiragana;304E +gikatakana;30AE +gimarmenian;0563 +gimel;05D2 +gimeldagesh;FB32 +gimeldageshhebrew;FB32 +gimelhebrew;05D2 +gjecyrillic;0453 +glottalinvertedstroke;01BE +glottalstop;0294 +glottalstopinverted;0296 +glottalstopmod;02C0 +glottalstopreversed;0295 +glottalstopreversedmod;02C1 +glottalstopreversedsuperior;02E4 +glottalstopstroke;02A1 +glottalstopstrokereversed;02A2 +gmacron;1E21 +gmonospace;FF47 +gohiragana;3054 +gokatakana;30B4 +gparen;24A2 +gpasquare;33AC +gradient;2207 +grave;0060 +gravebelowcmb;0316 +gravecmb;0300 +gravecomb;0300 +gravedeva;0953 +gravelowmod;02CE +gravemonospace;FF40 +gravetonecmb;0340 +greater;003E +greaterequal;2265 +greaterequalorless;22DB +greatermonospace;FF1E +greaterorequivalent;2273 +greaterorless;2277 +greateroverequal;2267 +greatersmall;FE65 +gscript;0261 +gstroke;01E5 +guhiragana;3050 +guillemotleft;00AB +guillemotright;00BB +guilsinglleft;2039 +guilsinglright;203A +gukatakana;30B0 +guramusquare;3318 +gysquare;33C9 +h;0068 +haabkhasiancyrillic;04A9 +haaltonearabic;06C1 +habengali;09B9 +hadescendercyrillic;04B3 +hadeva;0939 +hagujarati;0AB9 +hagurmukhi;0A39 +haharabic;062D +hahfinalarabic;FEA2 +hahinitialarabic;FEA3 +hahiragana;306F +hahmedialarabic;FEA4 +haitusquare;332A +hakatakana;30CF +hakatakanahalfwidth;FF8A +halantgurmukhi;0A4D +hamzaarabic;0621 +hamzadammaarabic;0621 064F +hamzadammatanarabic;0621 064C +hamzafathaarabic;0621 064E +hamzafathatanarabic;0621 064B +hamzalowarabic;0621 +hamzalowkasraarabic;0621 0650 +hamzalowkasratanarabic;0621 064D +hamzasukunarabic;0621 0652 +hangulfiller;3164 +hardsigncyrillic;044A +harpoonleftbarbup;21BC +harpoonrightbarbup;21C0 +hasquare;33CA +hatafpatah;05B2 +hatafpatah16;05B2 +hatafpatah23;05B2 +hatafpatah2f;05B2 +hatafpatahhebrew;05B2 +hatafpatahnarrowhebrew;05B2 +hatafpatahquarterhebrew;05B2 +hatafpatahwidehebrew;05B2 +hatafqamats;05B3 +hatafqamats1b;05B3 +hatafqamats28;05B3 +hatafqamats34;05B3 +hatafqamatshebrew;05B3 +hatafqamatsnarrowhebrew;05B3 +hatafqamatsquarterhebrew;05B3 +hatafqamatswidehebrew;05B3 +hatafsegol;05B1 +hatafsegol17;05B1 +hatafsegol24;05B1 +hatafsegol30;05B1 +hatafsegolhebrew;05B1 +hatafsegolnarrowhebrew;05B1 +hatafsegolquarterhebrew;05B1 +hatafsegolwidehebrew;05B1 +hbar;0127 +hbopomofo;310F +hbrevebelow;1E2B +hcedilla;1E29 +hcircle;24D7 +hcircumflex;0125 +hdieresis;1E27 +hdotaccent;1E23 +hdotbelow;1E25 +he;05D4 +heart;2665 +heartsuitblack;2665 +heartsuitwhite;2661 +hedagesh;FB34 +hedageshhebrew;FB34 +hehaltonearabic;06C1 +heharabic;0647 +hehebrew;05D4 +hehfinalaltonearabic;FBA7 +hehfinalalttwoarabic;FEEA +hehfinalarabic;FEEA +hehhamzaabovefinalarabic;FBA5 +hehhamzaaboveisolatedarabic;FBA4 +hehinitialaltonearabic;FBA8 +hehinitialarabic;FEEB +hehiragana;3078 +hehmedialaltonearabic;FBA9 +hehmedialarabic;FEEC +heiseierasquare;337B +hekatakana;30D8 +hekatakanahalfwidth;FF8D +hekutaarusquare;3336 +henghook;0267 +herutusquare;3339 +het;05D7 +hethebrew;05D7 +hhook;0266 +hhooksuperior;02B1 +hieuhacirclekorean;327B +hieuhaparenkorean;321B +hieuhcirclekorean;326D +hieuhkorean;314E +hieuhparenkorean;320D +hihiragana;3072 +hikatakana;30D2 +hikatakanahalfwidth;FF8B +hiriq;05B4 +hiriq14;05B4 +hiriq21;05B4 +hiriq2d;05B4 +hiriqhebrew;05B4 +hiriqnarrowhebrew;05B4 +hiriqquarterhebrew;05B4 +hiriqwidehebrew;05B4 +hlinebelow;1E96 +hmonospace;FF48 +hoarmenian;0570 +hohipthai;0E2B +hohiragana;307B +hokatakana;30DB +hokatakanahalfwidth;FF8E +holam;05B9 +holam19;05B9 +holam26;05B9 +holam32;05B9 +holamhebrew;05B9 +holamnarrowhebrew;05B9 +holamquarterhebrew;05B9 +holamwidehebrew;05B9 +honokhukthai;0E2E +hookabovecomb;0309 +hookcmb;0309 +hookpalatalizedbelowcmb;0321 +hookretroflexbelowcmb;0322 +hoonsquare;3342 +horicoptic;03E9 +horizontalbar;2015 +horncmb;031B +hotsprings;2668 +house;2302 +hparen;24A3 +hsuperior;02B0 +hturned;0265 +huhiragana;3075 +huiitosquare;3333 +hukatakana;30D5 +hukatakanahalfwidth;FF8C +hungarumlaut;02DD +hungarumlautcmb;030B +hv;0195 +hyphen;002D +hypheninferior;F6E5 +hyphenmonospace;FF0D +hyphensmall;FE63 +hyphensuperior;F6E6 +hyphentwo;2010 +i;0069 +iacute;00ED +iacyrillic;044F +ibengali;0987 +ibopomofo;3127 +ibreve;012D +icaron;01D0 +icircle;24D8 +icircumflex;00EE +icyrillic;0456 +idblgrave;0209 +ideographearthcircle;328F +ideographfirecircle;328B +ideographicallianceparen;323F +ideographiccallparen;323A +ideographiccentrecircle;32A5 +ideographicclose;3006 +ideographiccomma;3001 +ideographiccommaleft;FF64 +ideographiccongratulationparen;3237 +ideographiccorrectcircle;32A3 +ideographicearthparen;322F +ideographicenterpriseparen;323D +ideographicexcellentcircle;329D +ideographicfestivalparen;3240 +ideographicfinancialcircle;3296 +ideographicfinancialparen;3236 +ideographicfireparen;322B +ideographichaveparen;3232 +ideographichighcircle;32A4 +ideographiciterationmark;3005 +ideographiclaborcircle;3298 +ideographiclaborparen;3238 +ideographicleftcircle;32A7 +ideographiclowcircle;32A6 +ideographicmedicinecircle;32A9 +ideographicmetalparen;322E +ideographicmoonparen;322A +ideographicnameparen;3234 +ideographicperiod;3002 +ideographicprintcircle;329E +ideographicreachparen;3243 +ideographicrepresentparen;3239 +ideographicresourceparen;323E +ideographicrightcircle;32A8 +ideographicsecretcircle;3299 +ideographicselfparen;3242 +ideographicsocietyparen;3233 +ideographicspace;3000 +ideographicspecialparen;3235 +ideographicstockparen;3231 +ideographicstudyparen;323B +ideographicsunparen;3230 +ideographicsuperviseparen;323C +ideographicwaterparen;322C +ideographicwoodparen;322D +ideographiczero;3007 +ideographmetalcircle;328E +ideographmooncircle;328A +ideographnamecircle;3294 +ideographsuncircle;3290 +ideographwatercircle;328C +ideographwoodcircle;328D +ideva;0907 +idieresis;00EF +idieresisacute;1E2F +idieresiscyrillic;04E5 +idotbelow;1ECB +iebrevecyrillic;04D7 +iecyrillic;0435 +ieungacirclekorean;3275 +ieungaparenkorean;3215 +ieungcirclekorean;3267 +ieungkorean;3147 +ieungparenkorean;3207 +igrave;00EC +igujarati;0A87 +igurmukhi;0A07 +ihiragana;3044 +ihookabove;1EC9 +iibengali;0988 +iicyrillic;0438 +iideva;0908 +iigujarati;0A88 +iigurmukhi;0A08 +iimatragurmukhi;0A40 +iinvertedbreve;020B +iishortcyrillic;0439 +iivowelsignbengali;09C0 +iivowelsigndeva;0940 +iivowelsigngujarati;0AC0 +ij;0133 +ikatakana;30A4 +ikatakanahalfwidth;FF72 +ikorean;3163 +ilde;02DC +iluyhebrew;05AC +imacron;012B +imacroncyrillic;04E3 +imageorapproximatelyequal;2253 +imatragurmukhi;0A3F +imonospace;FF49 +increment;2206 +infinity;221E +iniarmenian;056B +integral;222B +integralbottom;2321 +integralbt;2321 +integralex;F8F5 +integraltop;2320 +integraltp;2320 +intersection;2229 +intisquare;3305 +invbullet;25D8 +invcircle;25D9 +invsmileface;263B +iocyrillic;0451 +iogonek;012F +iota;03B9 +iotadieresis;03CA +iotadieresistonos;0390 +iotalatin;0269 +iotatonos;03AF +iparen;24A4 +irigurmukhi;0A72 +ismallhiragana;3043 +ismallkatakana;30A3 +ismallkatakanahalfwidth;FF68 +issharbengali;09FA +istroke;0268 +isuperior;F6ED +iterationhiragana;309D +iterationkatakana;30FD +itilde;0129 +itildebelow;1E2D +iubopomofo;3129 +iucyrillic;044E +ivowelsignbengali;09BF +ivowelsigndeva;093F +ivowelsigngujarati;0ABF +izhitsacyrillic;0475 +izhitsadblgravecyrillic;0477 +j;006A +jaarmenian;0571 +jabengali;099C +jadeva;091C +jagujarati;0A9C +jagurmukhi;0A1C +jbopomofo;3110 +jcaron;01F0 +jcircle;24D9 +jcircumflex;0135 +jcrossedtail;029D +jdotlessstroke;025F +jecyrillic;0458 +jeemarabic;062C +jeemfinalarabic;FE9E +jeeminitialarabic;FE9F +jeemmedialarabic;FEA0 +jeharabic;0698 +jehfinalarabic;FB8B +jhabengali;099D +jhadeva;091D +jhagujarati;0A9D +jhagurmukhi;0A1D +jheharmenian;057B +jis;3004 +jmonospace;FF4A +jparen;24A5 +jsuperior;02B2 +k;006B +kabashkircyrillic;04A1 +kabengali;0995 +kacute;1E31 +kacyrillic;043A +kadescendercyrillic;049B +kadeva;0915 +kaf;05DB +kafarabic;0643 +kafdagesh;FB3B +kafdageshhebrew;FB3B +kaffinalarabic;FEDA +kafhebrew;05DB +kafinitialarabic;FEDB +kafmedialarabic;FEDC +kafrafehebrew;FB4D +kagujarati;0A95 +kagurmukhi;0A15 +kahiragana;304B +kahookcyrillic;04C4 +kakatakana;30AB +kakatakanahalfwidth;FF76 +kappa;03BA +kappasymbolgreek;03F0 +kapyeounmieumkorean;3171 +kapyeounphieuphkorean;3184 +kapyeounpieupkorean;3178 +kapyeounssangpieupkorean;3179 +karoriisquare;330D +kashidaautoarabic;0640 +kashidaautonosidebearingarabic;0640 +kasmallkatakana;30F5 +kasquare;3384 +kasraarabic;0650 +kasratanarabic;064D +kastrokecyrillic;049F +katahiraprolongmarkhalfwidth;FF70 +kaverticalstrokecyrillic;049D +kbopomofo;310E +kcalsquare;3389 +kcaron;01E9 +kcedilla;0137 +kcircle;24DA +kcommaaccent;0137 +kdotbelow;1E33 +keharmenian;0584 +kehiragana;3051 +kekatakana;30B1 +kekatakanahalfwidth;FF79 +kenarmenian;056F +kesmallkatakana;30F6 +kgreenlandic;0138 +khabengali;0996 +khacyrillic;0445 +khadeva;0916 +khagujarati;0A96 +khagurmukhi;0A16 +khaharabic;062E +khahfinalarabic;FEA6 +khahinitialarabic;FEA7 +khahmedialarabic;FEA8 +kheicoptic;03E7 +khhadeva;0959 +khhagurmukhi;0A59 +khieukhacirclekorean;3278 +khieukhaparenkorean;3218 +khieukhcirclekorean;326A +khieukhkorean;314B +khieukhparenkorean;320A +khokhaithai;0E02 +khokhonthai;0E05 +khokhuatthai;0E03 +khokhwaithai;0E04 +khomutthai;0E5B +khook;0199 +khorakhangthai;0E06 +khzsquare;3391 +kihiragana;304D +kikatakana;30AD +kikatakanahalfwidth;FF77 +kiroguramusquare;3315 +kiromeetorusquare;3316 +kirosquare;3314 +kiyeokacirclekorean;326E +kiyeokaparenkorean;320E +kiyeokcirclekorean;3260 +kiyeokkorean;3131 +kiyeokparenkorean;3200 +kiyeoksioskorean;3133 +kjecyrillic;045C +klinebelow;1E35 +klsquare;3398 +kmcubedsquare;33A6 +kmonospace;FF4B +kmsquaredsquare;33A2 +kohiragana;3053 +kohmsquare;33C0 +kokaithai;0E01 +kokatakana;30B3 +kokatakanahalfwidth;FF7A +kooposquare;331E +koppacyrillic;0481 +koreanstandardsymbol;327F +koroniscmb;0343 +kparen;24A6 +kpasquare;33AA +ksicyrillic;046F +ktsquare;33CF +kturned;029E +kuhiragana;304F +kukatakana;30AF +kukatakanahalfwidth;FF78 +kvsquare;33B8 +kwsquare;33BE +l;006C +labengali;09B2 +lacute;013A +ladeva;0932 +lagujarati;0AB2 +lagurmukhi;0A32 +lakkhangyaothai;0E45 +lamaleffinalarabic;FEFC +lamalefhamzaabovefinalarabic;FEF8 +lamalefhamzaaboveisolatedarabic;FEF7 +lamalefhamzabelowfinalarabic;FEFA +lamalefhamzabelowisolatedarabic;FEF9 +lamalefisolatedarabic;FEFB +lamalefmaddaabovefinalarabic;FEF6 +lamalefmaddaaboveisolatedarabic;FEF5 +lamarabic;0644 +lambda;03BB +lambdastroke;019B +lamed;05DC +lameddagesh;FB3C +lameddageshhebrew;FB3C +lamedhebrew;05DC +lamedholam;05DC 05B9 +lamedholamdagesh;05DC 05B9 05BC +lamedholamdageshhebrew;05DC 05B9 05BC +lamedholamhebrew;05DC 05B9 +lamfinalarabic;FEDE +lamhahinitialarabic;FCCA +laminitialarabic;FEDF +lamjeeminitialarabic;FCC9 +lamkhahinitialarabic;FCCB +lamlamhehisolatedarabic;FDF2 +lammedialarabic;FEE0 +lammeemhahinitialarabic;FD88 +lammeeminitialarabic;FCCC +lammeemjeeminitialarabic;FEDF FEE4 FEA0 +lammeemkhahinitialarabic;FEDF FEE4 FEA8 +largecircle;25EF +lbar;019A +lbelt;026C +lbopomofo;310C +lcaron;013E +lcedilla;013C +lcircle;24DB +lcircumflexbelow;1E3D +lcommaaccent;013C +ldot;0140 +ldotaccent;0140 +ldotbelow;1E37 +ldotbelowmacron;1E39 +leftangleabovecmb;031A +lefttackbelowcmb;0318 +less;003C +lessequal;2264 +lessequalorgreater;22DA +lessmonospace;FF1C +lessorequivalent;2272 +lessorgreater;2276 +lessoverequal;2266 +lesssmall;FE64 +lezh;026E +lfblock;258C +lhookretroflex;026D +lira;20A4 +liwnarmenian;056C +lj;01C9 +ljecyrillic;0459 +ll;F6C0 +lladeva;0933 +llagujarati;0AB3 +llinebelow;1E3B +llladeva;0934 +llvocalicbengali;09E1 +llvocalicdeva;0961 +llvocalicvowelsignbengali;09E3 +llvocalicvowelsigndeva;0963 +lmiddletilde;026B +lmonospace;FF4C +lmsquare;33D0 +lochulathai;0E2C +logicaland;2227 +logicalnot;00AC +logicalnotreversed;2310 +logicalor;2228 +lolingthai;0E25 +longs;017F +lowlinecenterline;FE4E +lowlinecmb;0332 +lowlinedashed;FE4D +lozenge;25CA +lparen;24A7 +lslash;0142 +lsquare;2113 +lsuperior;F6EE +ltshade;2591 +luthai;0E26 +lvocalicbengali;098C +lvocalicdeva;090C +lvocalicvowelsignbengali;09E2 +lvocalicvowelsigndeva;0962 +lxsquare;33D3 +m;006D +mabengali;09AE +macron;00AF +macronbelowcmb;0331 +macroncmb;0304 +macronlowmod;02CD +macronmonospace;FFE3 +macute;1E3F +madeva;092E +magujarati;0AAE +magurmukhi;0A2E +mahapakhhebrew;05A4 +mahapakhlefthebrew;05A4 +mahiragana;307E +maichattawalowleftthai;F895 +maichattawalowrightthai;F894 +maichattawathai;0E4B +maichattawaupperleftthai;F893 +maieklowleftthai;F88C +maieklowrightthai;F88B +maiekthai;0E48 +maiekupperleftthai;F88A +maihanakatleftthai;F884 +maihanakatthai;0E31 +maitaikhuleftthai;F889 +maitaikhuthai;0E47 +maitholowleftthai;F88F +maitholowrightthai;F88E +maithothai;0E49 +maithoupperleftthai;F88D +maitrilowleftthai;F892 +maitrilowrightthai;F891 +maitrithai;0E4A +maitriupperleftthai;F890 +maiyamokthai;0E46 +makatakana;30DE +makatakanahalfwidth;FF8F +male;2642 +mansyonsquare;3347 +maqafhebrew;05BE +mars;2642 +masoracirclehebrew;05AF +masquare;3383 +mbopomofo;3107 +mbsquare;33D4 +mcircle;24DC +mcubedsquare;33A5 +mdotaccent;1E41 +mdotbelow;1E43 +meemarabic;0645 +meemfinalarabic;FEE2 +meeminitialarabic;FEE3 +meemmedialarabic;FEE4 +meemmeeminitialarabic;FCD1 +meemmeemisolatedarabic;FC48 +meetorusquare;334D +mehiragana;3081 +meizierasquare;337E +mekatakana;30E1 +mekatakanahalfwidth;FF92 +mem;05DE +memdagesh;FB3E +memdageshhebrew;FB3E +memhebrew;05DE +menarmenian;0574 +merkhahebrew;05A5 +merkhakefulahebrew;05A6 +merkhakefulalefthebrew;05A6 +merkhalefthebrew;05A5 +mhook;0271 +mhzsquare;3392 +middledotkatakanahalfwidth;FF65 +middot;00B7 +mieumacirclekorean;3272 +mieumaparenkorean;3212 +mieumcirclekorean;3264 +mieumkorean;3141 +mieumpansioskorean;3170 +mieumparenkorean;3204 +mieumpieupkorean;316E +mieumsioskorean;316F +mihiragana;307F +mikatakana;30DF +mikatakanahalfwidth;FF90 +minus;2212 +minusbelowcmb;0320 +minuscircle;2296 +minusmod;02D7 +minusplus;2213 +minute;2032 +miribaarusquare;334A +mirisquare;3349 +mlonglegturned;0270 +mlsquare;3396 +mmcubedsquare;33A3 +mmonospace;FF4D +mmsquaredsquare;339F +mohiragana;3082 +mohmsquare;33C1 +mokatakana;30E2 +mokatakanahalfwidth;FF93 +molsquare;33D6 +momathai;0E21 +moverssquare;33A7 +moverssquaredsquare;33A8 +mparen;24A8 +mpasquare;33AB +mssquare;33B3 +msuperior;F6EF +mturned;026F +mu;00B5 +mu1;00B5 +muasquare;3382 +muchgreater;226B +muchless;226A +mufsquare;338C +mugreek;03BC +mugsquare;338D +muhiragana;3080 +mukatakana;30E0 +mukatakanahalfwidth;FF91 +mulsquare;3395 +multiply;00D7 +mumsquare;339B +munahhebrew;05A3 +munahlefthebrew;05A3 +musicalnote;266A +musicalnotedbl;266B +musicflatsign;266D +musicsharpsign;266F +mussquare;33B2 +muvsquare;33B6 +muwsquare;33BC +mvmegasquare;33B9 +mvsquare;33B7 +mwmegasquare;33BF +mwsquare;33BD +n;006E +nabengali;09A8 +nabla;2207 +nacute;0144 +nadeva;0928 +nagujarati;0AA8 +nagurmukhi;0A28 +nahiragana;306A +nakatakana;30CA +nakatakanahalfwidth;FF85 +napostrophe;0149 +nasquare;3381 +nbopomofo;310B +nbspace;00A0 +ncaron;0148 +ncedilla;0146 +ncircle;24DD +ncircumflexbelow;1E4B +ncommaaccent;0146 +ndotaccent;1E45 +ndotbelow;1E47 +nehiragana;306D +nekatakana;30CD +nekatakanahalfwidth;FF88 +newsheqelsign;20AA +nfsquare;338B +ngabengali;0999 +ngadeva;0919 +ngagujarati;0A99 +ngagurmukhi;0A19 +ngonguthai;0E07 +nhiragana;3093 +nhookleft;0272 +nhookretroflex;0273 +nieunacirclekorean;326F +nieunaparenkorean;320F +nieuncieuckorean;3135 +nieuncirclekorean;3261 +nieunhieuhkorean;3136 +nieunkorean;3134 +nieunpansioskorean;3168 +nieunparenkorean;3201 +nieunsioskorean;3167 +nieuntikeutkorean;3166 +nihiragana;306B +nikatakana;30CB +nikatakanahalfwidth;FF86 +nikhahitleftthai;F899 +nikhahitthai;0E4D +nine;0039 +ninearabic;0669 +ninebengali;09EF +ninecircle;2468 +ninecircleinversesansserif;2792 +ninedeva;096F +ninegujarati;0AEF +ninegurmukhi;0A6F +ninehackarabic;0669 +ninehangzhou;3029 +nineideographicparen;3228 +nineinferior;2089 +ninemonospace;FF19 +nineoldstyle;F739 +nineparen;247C +nineperiod;2490 +ninepersian;06F9 +nineroman;2178 +ninesuperior;2079 +nineteencircle;2472 +nineteenparen;2486 +nineteenperiod;249A +ninethai;0E59 +nj;01CC +njecyrillic;045A +nkatakana;30F3 +nkatakanahalfwidth;FF9D +nlegrightlong;019E +nlinebelow;1E49 +nmonospace;FF4E +nmsquare;339A +nnabengali;09A3 +nnadeva;0923 +nnagujarati;0AA3 +nnagurmukhi;0A23 +nnnadeva;0929 +nohiragana;306E +nokatakana;30CE +nokatakanahalfwidth;FF89 +nonbreakingspace;00A0 +nonenthai;0E13 +nonuthai;0E19 +noonarabic;0646 +noonfinalarabic;FEE6 +noonghunnaarabic;06BA +noonghunnafinalarabic;FB9F +noonhehinitialarabic;FEE7 FEEC +nooninitialarabic;FEE7 +noonjeeminitialarabic;FCD2 +noonjeemisolatedarabic;FC4B +noonmedialarabic;FEE8 +noonmeeminitialarabic;FCD5 +noonmeemisolatedarabic;FC4E +noonnoonfinalarabic;FC8D +notcontains;220C +notelement;2209 +notelementof;2209 +notequal;2260 +notgreater;226F +notgreaternorequal;2271 +notgreaternorless;2279 +notidentical;2262 +notless;226E +notlessnorequal;2270 +notparallel;2226 +notprecedes;2280 +notsubset;2284 +notsucceeds;2281 +notsuperset;2285 +nowarmenian;0576 +nparen;24A9 +nssquare;33B1 +nsuperior;207F +ntilde;00F1 +nu;03BD +nuhiragana;306C +nukatakana;30CC +nukatakanahalfwidth;FF87 +nuktabengali;09BC +nuktadeva;093C +nuktagujarati;0ABC +nuktagurmukhi;0A3C +numbersign;0023 +numbersignmonospace;FF03 +numbersignsmall;FE5F +numeralsigngreek;0374 +numeralsignlowergreek;0375 +numero;2116 +nun;05E0 +nundagesh;FB40 +nundageshhebrew;FB40 +nunhebrew;05E0 +nvsquare;33B5 +nwsquare;33BB +nyabengali;099E +nyadeva;091E +nyagujarati;0A9E +nyagurmukhi;0A1E +o;006F +oacute;00F3 +oangthai;0E2D +obarred;0275 +obarredcyrillic;04E9 +obarreddieresiscyrillic;04EB +obengali;0993 +obopomofo;311B +obreve;014F +ocandradeva;0911 +ocandragujarati;0A91 +ocandravowelsigndeva;0949 +ocandravowelsigngujarati;0AC9 +ocaron;01D2 +ocircle;24DE +ocircumflex;00F4 +ocircumflexacute;1ED1 +ocircumflexdotbelow;1ED9 +ocircumflexgrave;1ED3 +ocircumflexhookabove;1ED5 +ocircumflextilde;1ED7 +ocyrillic;043E +odblacute;0151 +odblgrave;020D +odeva;0913 +odieresis;00F6 +odieresiscyrillic;04E7 +odotbelow;1ECD +oe;0153 +oekorean;315A +ogonek;02DB +ogonekcmb;0328 +ograve;00F2 +ogujarati;0A93 +oharmenian;0585 +ohiragana;304A +ohookabove;1ECF +ohorn;01A1 +ohornacute;1EDB +ohorndotbelow;1EE3 +ohorngrave;1EDD +ohornhookabove;1EDF +ohorntilde;1EE1 +ohungarumlaut;0151 +oi;01A3 +oinvertedbreve;020F +okatakana;30AA +okatakanahalfwidth;FF75 +okorean;3157 +olehebrew;05AB +omacron;014D +omacronacute;1E53 +omacrongrave;1E51 +omdeva;0950 +omega;03C9 +omega1;03D6 +omegacyrillic;0461 +omegalatinclosed;0277 +omegaroundcyrillic;047B +omegatitlocyrillic;047D +omegatonos;03CE +omgujarati;0AD0 +omicron;03BF +omicrontonos;03CC +omonospace;FF4F +one;0031 +onearabic;0661 +onebengali;09E7 +onecircle;2460 +onecircleinversesansserif;278A +onedeva;0967 +onedotenleader;2024 +oneeighth;215B +onefitted;F6DC +onegujarati;0AE7 +onegurmukhi;0A67 +onehackarabic;0661 +onehalf;00BD +onehangzhou;3021 +oneideographicparen;3220 +oneinferior;2081 +onemonospace;FF11 +onenumeratorbengali;09F4 +oneoldstyle;F731 +oneparen;2474 +oneperiod;2488 +onepersian;06F1 +onequarter;00BC +oneroman;2170 +onesuperior;00B9 +onethai;0E51 +onethird;2153 +oogonek;01EB +oogonekmacron;01ED +oogurmukhi;0A13 +oomatragurmukhi;0A4B +oopen;0254 +oparen;24AA +openbullet;25E6 +option;2325 +ordfeminine;00AA +ordmasculine;00BA +orthogonal;221F +oshortdeva;0912 +oshortvowelsigndeva;094A +oslash;00F8 +oslashacute;01FF +osmallhiragana;3049 +osmallkatakana;30A9 +osmallkatakanahalfwidth;FF6B +ostrokeacute;01FF +osuperior;F6F0 +otcyrillic;047F +otilde;00F5 +otildeacute;1E4D +otildedieresis;1E4F +oubopomofo;3121 +overline;203E +overlinecenterline;FE4A +overlinecmb;0305 +overlinedashed;FE49 +overlinedblwavy;FE4C +overlinewavy;FE4B +overscore;00AF +ovowelsignbengali;09CB +ovowelsigndeva;094B +ovowelsigngujarati;0ACB +p;0070 +paampssquare;3380 +paasentosquare;332B +pabengali;09AA +pacute;1E55 +padeva;092A +pagedown;21DF +pageup;21DE +pagujarati;0AAA +pagurmukhi;0A2A +pahiragana;3071 +paiyannoithai;0E2F +pakatakana;30D1 +palatalizationcyrilliccmb;0484 +palochkacyrillic;04C0 +pansioskorean;317F +paragraph;00B6 +parallel;2225 +parenleft;0028 +parenleftaltonearabic;FD3E +parenleftbt;F8ED +parenleftex;F8EC +parenleftinferior;208D +parenleftmonospace;FF08 +parenleftsmall;FE59 +parenleftsuperior;207D +parenlefttp;F8EB +parenleftvertical;FE35 +parenright;0029 +parenrightaltonearabic;FD3F +parenrightbt;F8F8 +parenrightex;F8F7 +parenrightinferior;208E +parenrightmonospace;FF09 +parenrightsmall;FE5A +parenrightsuperior;207E +parenrighttp;F8F6 +parenrightvertical;FE36 +partialdiff;2202 +paseqhebrew;05C0 +pashtahebrew;0599 +pasquare;33A9 +patah;05B7 +patah11;05B7 +patah1d;05B7 +patah2a;05B7 +patahhebrew;05B7 +patahnarrowhebrew;05B7 +patahquarterhebrew;05B7 +patahwidehebrew;05B7 +pazerhebrew;05A1 +pbopomofo;3106 +pcircle;24DF +pdotaccent;1E57 +pe;05E4 +pecyrillic;043F +pedagesh;FB44 +pedageshhebrew;FB44 +peezisquare;333B +pefinaldageshhebrew;FB43 +peharabic;067E +peharmenian;057A +pehebrew;05E4 +pehfinalarabic;FB57 +pehinitialarabic;FB58 +pehiragana;307A +pehmedialarabic;FB59 +pekatakana;30DA +pemiddlehookcyrillic;04A7 +perafehebrew;FB4E +percent;0025 +percentarabic;066A +percentmonospace;FF05 +percentsmall;FE6A +period;002E +periodarmenian;0589 +periodcentered;00B7 +periodhalfwidth;FF61 +periodinferior;F6E7 +periodmonospace;FF0E +periodsmall;FE52 +periodsuperior;F6E8 +perispomenigreekcmb;0342 +perpendicular;22A5 +perthousand;2030 +peseta;20A7 +pfsquare;338A +phabengali;09AB +phadeva;092B +phagujarati;0AAB +phagurmukhi;0A2B +phi;03C6 +phi1;03D5 +phieuphacirclekorean;327A +phieuphaparenkorean;321A +phieuphcirclekorean;326C +phieuphkorean;314D +phieuphparenkorean;320C +philatin;0278 +phinthuthai;0E3A +phisymbolgreek;03D5 +phook;01A5 +phophanthai;0E1E +phophungthai;0E1C +phosamphaothai;0E20 +pi;03C0 +pieupacirclekorean;3273 +pieupaparenkorean;3213 +pieupcieuckorean;3176 +pieupcirclekorean;3265 +pieupkiyeokkorean;3172 +pieupkorean;3142 +pieupparenkorean;3205 +pieupsioskiyeokkorean;3174 +pieupsioskorean;3144 +pieupsiostikeutkorean;3175 +pieupthieuthkorean;3177 +pieuptikeutkorean;3173 +pihiragana;3074 +pikatakana;30D4 +pisymbolgreek;03D6 +piwrarmenian;0583 +plus;002B +plusbelowcmb;031F +pluscircle;2295 +plusminus;00B1 +plusmod;02D6 +plusmonospace;FF0B +plussmall;FE62 +plussuperior;207A +pmonospace;FF50 +pmsquare;33D8 +pohiragana;307D +pointingindexdownwhite;261F +pointingindexleftwhite;261C +pointingindexrightwhite;261E +pointingindexupwhite;261D +pokatakana;30DD +poplathai;0E1B +postalmark;3012 +postalmarkface;3020 +pparen;24AB +precedes;227A +prescription;211E +primemod;02B9 +primereversed;2035 +product;220F +projective;2305 +prolongedkana;30FC +propellor;2318 +propersubset;2282 +propersuperset;2283 +proportion;2237 +proportional;221D +psi;03C8 +psicyrillic;0471 +psilipneumatacyrilliccmb;0486 +pssquare;33B0 +puhiragana;3077 +pukatakana;30D7 +pvsquare;33B4 +pwsquare;33BA +q;0071 +qadeva;0958 +qadmahebrew;05A8 +qafarabic;0642 +qaffinalarabic;FED6 +qafinitialarabic;FED7 +qafmedialarabic;FED8 +qamats;05B8 +qamats10;05B8 +qamats1a;05B8 +qamats1c;05B8 +qamats27;05B8 +qamats29;05B8 +qamats33;05B8 +qamatsde;05B8 +qamatshebrew;05B8 +qamatsnarrowhebrew;05B8 +qamatsqatanhebrew;05B8 +qamatsqatannarrowhebrew;05B8 +qamatsqatanquarterhebrew;05B8 +qamatsqatanwidehebrew;05B8 +qamatsquarterhebrew;05B8 +qamatswidehebrew;05B8 +qarneyparahebrew;059F +qbopomofo;3111 +qcircle;24E0 +qhook;02A0 +qmonospace;FF51 +qof;05E7 +qofdagesh;FB47 +qofdageshhebrew;FB47 +qofhatafpatah;05E7 05B2 +qofhatafpatahhebrew;05E7 05B2 +qofhatafsegol;05E7 05B1 +qofhatafsegolhebrew;05E7 05B1 +qofhebrew;05E7 +qofhiriq;05E7 05B4 +qofhiriqhebrew;05E7 05B4 +qofholam;05E7 05B9 +qofholamhebrew;05E7 05B9 +qofpatah;05E7 05B7 +qofpatahhebrew;05E7 05B7 +qofqamats;05E7 05B8 +qofqamatshebrew;05E7 05B8 +qofqubuts;05E7 05BB +qofqubutshebrew;05E7 05BB +qofsegol;05E7 05B6 +qofsegolhebrew;05E7 05B6 +qofsheva;05E7 05B0 +qofshevahebrew;05E7 05B0 +qoftsere;05E7 05B5 +qoftserehebrew;05E7 05B5 +qparen;24AC +quarternote;2669 +qubuts;05BB +qubuts18;05BB +qubuts25;05BB +qubuts31;05BB +qubutshebrew;05BB +qubutsnarrowhebrew;05BB +qubutsquarterhebrew;05BB +qubutswidehebrew;05BB +question;003F +questionarabic;061F +questionarmenian;055E +questiondown;00BF +questiondownsmall;F7BF +questiongreek;037E +questionmonospace;FF1F +questionsmall;F73F +quotedbl;0022 +quotedblbase;201E +quotedblleft;201C +quotedblmonospace;FF02 +quotedblprime;301E +quotedblprimereversed;301D +quotedblright;201D +quoteleft;2018 +quoteleftreversed;201B +quotereversed;201B +quoteright;2019 +quoterightn;0149 +quotesinglbase;201A +quotesingle;0027 +quotesinglemonospace;FF07 +r;0072 +raarmenian;057C +rabengali;09B0 +racute;0155 +radeva;0930 +radical;221A +radicalex;F8E5 +radoverssquare;33AE +radoverssquaredsquare;33AF +radsquare;33AD +rafe;05BF +rafehebrew;05BF +ragujarati;0AB0 +ragurmukhi;0A30 +rahiragana;3089 +rakatakana;30E9 +rakatakanahalfwidth;FF97 +ralowerdiagonalbengali;09F1 +ramiddlediagonalbengali;09F0 +ramshorn;0264 +ratio;2236 +rbopomofo;3116 +rcaron;0159 +rcedilla;0157 +rcircle;24E1 +rcommaaccent;0157 +rdblgrave;0211 +rdotaccent;1E59 +rdotbelow;1E5B +rdotbelowmacron;1E5D +referencemark;203B +reflexsubset;2286 +reflexsuperset;2287 +registered;00AE +registersans;F8E8 +registerserif;F6DA +reharabic;0631 +reharmenian;0580 +rehfinalarabic;FEAE +rehiragana;308C +rehyehaleflamarabic;0631 FEF3 FE8E 0644 +rekatakana;30EC +rekatakanahalfwidth;FF9A +resh;05E8 +reshdageshhebrew;FB48 +reshhatafpatah;05E8 05B2 +reshhatafpatahhebrew;05E8 05B2 +reshhatafsegol;05E8 05B1 +reshhatafsegolhebrew;05E8 05B1 +reshhebrew;05E8 +reshhiriq;05E8 05B4 +reshhiriqhebrew;05E8 05B4 +reshholam;05E8 05B9 +reshholamhebrew;05E8 05B9 +reshpatah;05E8 05B7 +reshpatahhebrew;05E8 05B7 +reshqamats;05E8 05B8 +reshqamatshebrew;05E8 05B8 +reshqubuts;05E8 05BB +reshqubutshebrew;05E8 05BB +reshsegol;05E8 05B6 +reshsegolhebrew;05E8 05B6 +reshsheva;05E8 05B0 +reshshevahebrew;05E8 05B0 +reshtsere;05E8 05B5 +reshtserehebrew;05E8 05B5 +reversedtilde;223D +reviahebrew;0597 +reviamugrashhebrew;0597 +revlogicalnot;2310 +rfishhook;027E +rfishhookreversed;027F +rhabengali;09DD +rhadeva;095D +rho;03C1 +rhook;027D +rhookturned;027B +rhookturnedsuperior;02B5 +rhosymbolgreek;03F1 +rhotichookmod;02DE +rieulacirclekorean;3271 +rieulaparenkorean;3211 +rieulcirclekorean;3263 +rieulhieuhkorean;3140 +rieulkiyeokkorean;313A +rieulkiyeoksioskorean;3169 +rieulkorean;3139 +rieulmieumkorean;313B +rieulpansioskorean;316C +rieulparenkorean;3203 +rieulphieuphkorean;313F +rieulpieupkorean;313C +rieulpieupsioskorean;316B +rieulsioskorean;313D +rieulthieuthkorean;313E +rieultikeutkorean;316A +rieulyeorinhieuhkorean;316D +rightangle;221F +righttackbelowcmb;0319 +righttriangle;22BF +rihiragana;308A +rikatakana;30EA +rikatakanahalfwidth;FF98 +ring;02DA +ringbelowcmb;0325 +ringcmb;030A +ringhalfleft;02BF +ringhalfleftarmenian;0559 +ringhalfleftbelowcmb;031C +ringhalfleftcentered;02D3 +ringhalfright;02BE +ringhalfrightbelowcmb;0339 +ringhalfrightcentered;02D2 +rinvertedbreve;0213 +rittorusquare;3351 +rlinebelow;1E5F +rlongleg;027C +rlonglegturned;027A +rmonospace;FF52 +rohiragana;308D +rokatakana;30ED +rokatakanahalfwidth;FF9B +roruathai;0E23 +rparen;24AD +rrabengali;09DC +rradeva;0931 +rragurmukhi;0A5C +rreharabic;0691 +rrehfinalarabic;FB8D +rrvocalicbengali;09E0 +rrvocalicdeva;0960 +rrvocalicgujarati;0AE0 +rrvocalicvowelsignbengali;09C4 +rrvocalicvowelsigndeva;0944 +rrvocalicvowelsigngujarati;0AC4 +rsuperior;F6F1 +rtblock;2590 +rturned;0279 +rturnedsuperior;02B4 +ruhiragana;308B +rukatakana;30EB +rukatakanahalfwidth;FF99 +rupeemarkbengali;09F2 +rupeesignbengali;09F3 +rupiah;F6DD +ruthai;0E24 +rvocalicbengali;098B +rvocalicdeva;090B +rvocalicgujarati;0A8B +rvocalicvowelsignbengali;09C3 +rvocalicvowelsigndeva;0943 +rvocalicvowelsigngujarati;0AC3 +s;0073 +sabengali;09B8 +sacute;015B +sacutedotaccent;1E65 +sadarabic;0635 +sadeva;0938 +sadfinalarabic;FEBA +sadinitialarabic;FEBB +sadmedialarabic;FEBC +sagujarati;0AB8 +sagurmukhi;0A38 +sahiragana;3055 +sakatakana;30B5 +sakatakanahalfwidth;FF7B +sallallahoualayhewasallamarabic;FDFA +samekh;05E1 +samekhdagesh;FB41 +samekhdageshhebrew;FB41 +samekhhebrew;05E1 +saraaathai;0E32 +saraaethai;0E41 +saraaimaimalaithai;0E44 +saraaimaimuanthai;0E43 +saraamthai;0E33 +saraathai;0E30 +saraethai;0E40 +saraiileftthai;F886 +saraiithai;0E35 +saraileftthai;F885 +saraithai;0E34 +saraothai;0E42 +saraueeleftthai;F888 +saraueethai;0E37 +saraueleftthai;F887 +sarauethai;0E36 +sarauthai;0E38 +sarauuthai;0E39 +sbopomofo;3119 +scaron;0161 +scarondotaccent;1E67 +scedilla;015F +schwa;0259 +schwacyrillic;04D9 +schwadieresiscyrillic;04DB +schwahook;025A +scircle;24E2 +scircumflex;015D +scommaaccent;0219 +sdotaccent;1E61 +sdotbelow;1E63 +sdotbelowdotaccent;1E69 +seagullbelowcmb;033C +second;2033 +secondtonechinese;02CA +section;00A7 +seenarabic;0633 +seenfinalarabic;FEB2 +seeninitialarabic;FEB3 +seenmedialarabic;FEB4 +segol;05B6 +segol13;05B6 +segol1f;05B6 +segol2c;05B6 +segolhebrew;05B6 +segolnarrowhebrew;05B6 +segolquarterhebrew;05B6 +segoltahebrew;0592 +segolwidehebrew;05B6 +seharmenian;057D +sehiragana;305B +sekatakana;30BB +sekatakanahalfwidth;FF7E +semicolon;003B +semicolonarabic;061B +semicolonmonospace;FF1B +semicolonsmall;FE54 +semivoicedmarkkana;309C +semivoicedmarkkanahalfwidth;FF9F +sentisquare;3322 +sentosquare;3323 +seven;0037 +sevenarabic;0667 +sevenbengali;09ED +sevencircle;2466 +sevencircleinversesansserif;2790 +sevendeva;096D +seveneighths;215E +sevengujarati;0AED +sevengurmukhi;0A6D +sevenhackarabic;0667 +sevenhangzhou;3027 +sevenideographicparen;3226 +seveninferior;2087 +sevenmonospace;FF17 +sevenoldstyle;F737 +sevenparen;247A +sevenperiod;248E +sevenpersian;06F7 +sevenroman;2176 +sevensuperior;2077 +seventeencircle;2470 +seventeenparen;2484 +seventeenperiod;2498 +seventhai;0E57 +sfthyphen;00AD +shaarmenian;0577 +shabengali;09B6 +shacyrillic;0448 +shaddaarabic;0651 +shaddadammaarabic;FC61 +shaddadammatanarabic;FC5E +shaddafathaarabic;FC60 +shaddafathatanarabic;0651 064B +shaddakasraarabic;FC62 +shaddakasratanarabic;FC5F +shade;2592 +shadedark;2593 +shadelight;2591 +shademedium;2592 +shadeva;0936 +shagujarati;0AB6 +shagurmukhi;0A36 +shalshelethebrew;0593 +shbopomofo;3115 +shchacyrillic;0449 +sheenarabic;0634 +sheenfinalarabic;FEB6 +sheeninitialarabic;FEB7 +sheenmedialarabic;FEB8 +sheicoptic;03E3 +sheqel;20AA +sheqelhebrew;20AA +sheva;05B0 +sheva115;05B0 +sheva15;05B0 +sheva22;05B0 +sheva2e;05B0 +shevahebrew;05B0 +shevanarrowhebrew;05B0 +shevaquarterhebrew;05B0 +shevawidehebrew;05B0 +shhacyrillic;04BB +shimacoptic;03ED +shin;05E9 +shindagesh;FB49 +shindageshhebrew;FB49 +shindageshshindot;FB2C +shindageshshindothebrew;FB2C +shindageshsindot;FB2D +shindageshsindothebrew;FB2D +shindothebrew;05C1 +shinhebrew;05E9 +shinshindot;FB2A +shinshindothebrew;FB2A +shinsindot;FB2B +shinsindothebrew;FB2B +shook;0282 +sigma;03C3 +sigma1;03C2 +sigmafinal;03C2 +sigmalunatesymbolgreek;03F2 +sihiragana;3057 +sikatakana;30B7 +sikatakanahalfwidth;FF7C +siluqhebrew;05BD +siluqlefthebrew;05BD +similar;223C +sindothebrew;05C2 +siosacirclekorean;3274 +siosaparenkorean;3214 +sioscieuckorean;317E +sioscirclekorean;3266 +sioskiyeokkorean;317A +sioskorean;3145 +siosnieunkorean;317B +siosparenkorean;3206 +siospieupkorean;317D +siostikeutkorean;317C +six;0036 +sixarabic;0666 +sixbengali;09EC +sixcircle;2465 +sixcircleinversesansserif;278F +sixdeva;096C +sixgujarati;0AEC +sixgurmukhi;0A6C +sixhackarabic;0666 +sixhangzhou;3026 +sixideographicparen;3225 +sixinferior;2086 +sixmonospace;FF16 +sixoldstyle;F736 +sixparen;2479 +sixperiod;248D +sixpersian;06F6 +sixroman;2175 +sixsuperior;2076 +sixteencircle;246F +sixteencurrencydenominatorbengali;09F9 +sixteenparen;2483 +sixteenperiod;2497 +sixthai;0E56 +slash;002F +slashmonospace;FF0F +slong;017F +slongdotaccent;1E9B +smileface;263A +smonospace;FF53 +sofpasuqhebrew;05C3 +softhyphen;00AD +softsigncyrillic;044C +sohiragana;305D +sokatakana;30BD +sokatakanahalfwidth;FF7F +soliduslongoverlaycmb;0338 +solidusshortoverlaycmb;0337 +sorusithai;0E29 +sosalathai;0E28 +sosothai;0E0B +sosuathai;0E2A +space;0020 +spacehackarabic;0020 +spade;2660 +spadesuitblack;2660 +spadesuitwhite;2664 +sparen;24AE +squarebelowcmb;033B +squarecc;33C4 +squarecm;339D +squarediagonalcrosshatchfill;25A9 +squarehorizontalfill;25A4 +squarekg;338F +squarekm;339E +squarekmcapital;33CE +squareln;33D1 +squarelog;33D2 +squaremg;338E +squaremil;33D5 +squaremm;339C +squaremsquared;33A1 +squareorthogonalcrosshatchfill;25A6 +squareupperlefttolowerrightfill;25A7 +squareupperrighttolowerleftfill;25A8 +squareverticalfill;25A5 +squarewhitewithsmallblack;25A3 +srsquare;33DB +ssabengali;09B7 +ssadeva;0937 +ssagujarati;0AB7 +ssangcieuckorean;3149 +ssanghieuhkorean;3185 +ssangieungkorean;3180 +ssangkiyeokkorean;3132 +ssangnieunkorean;3165 +ssangpieupkorean;3143 +ssangsioskorean;3146 +ssangtikeutkorean;3138 +ssuperior;F6F2 +sterling;00A3 +sterlingmonospace;FFE1 +strokelongoverlaycmb;0336 +strokeshortoverlaycmb;0335 +subset;2282 +subsetnotequal;228A +subsetorequal;2286 +succeeds;227B +suchthat;220B +suhiragana;3059 +sukatakana;30B9 +sukatakanahalfwidth;FF7D +sukunarabic;0652 +summation;2211 +sun;263C +superset;2283 +supersetnotequal;228B +supersetorequal;2287 +svsquare;33DC +syouwaerasquare;337C +t;0074 +tabengali;09A4 +tackdown;22A4 +tackleft;22A3 +tadeva;0924 +tagujarati;0AA4 +tagurmukhi;0A24 +taharabic;0637 +tahfinalarabic;FEC2 +tahinitialarabic;FEC3 +tahiragana;305F +tahmedialarabic;FEC4 +taisyouerasquare;337D +takatakana;30BF +takatakanahalfwidth;FF80 +tatweelarabic;0640 +tau;03C4 +tav;05EA +tavdages;FB4A +tavdagesh;FB4A +tavdageshhebrew;FB4A +tavhebrew;05EA +tbar;0167 +tbopomofo;310A +tcaron;0165 +tccurl;02A8 +tcedilla;0163 +tcheharabic;0686 +tchehfinalarabic;FB7B +tchehinitialarabic;FB7C +tchehmedialarabic;FB7D +tchehmeeminitialarabic;FB7C FEE4 +tcircle;24E3 +tcircumflexbelow;1E71 +tcommaaccent;0163 +tdieresis;1E97 +tdotaccent;1E6B +tdotbelow;1E6D +tecyrillic;0442 +tedescendercyrillic;04AD +teharabic;062A +tehfinalarabic;FE96 +tehhahinitialarabic;FCA2 +tehhahisolatedarabic;FC0C +tehinitialarabic;FE97 +tehiragana;3066 +tehjeeminitialarabic;FCA1 +tehjeemisolatedarabic;FC0B +tehmarbutaarabic;0629 +tehmarbutafinalarabic;FE94 +tehmedialarabic;FE98 +tehmeeminitialarabic;FCA4 +tehmeemisolatedarabic;FC0E +tehnoonfinalarabic;FC73 +tekatakana;30C6 +tekatakanahalfwidth;FF83 +telephone;2121 +telephoneblack;260E +telishagedolahebrew;05A0 +telishaqetanahebrew;05A9 +tencircle;2469 +tenideographicparen;3229 +tenparen;247D +tenperiod;2491 +tenroman;2179 +tesh;02A7 +tet;05D8 +tetdagesh;FB38 +tetdageshhebrew;FB38 +tethebrew;05D8 +tetsecyrillic;04B5 +tevirhebrew;059B +tevirlefthebrew;059B +thabengali;09A5 +thadeva;0925 +thagujarati;0AA5 +thagurmukhi;0A25 +thalarabic;0630 +thalfinalarabic;FEAC +thanthakhatlowleftthai;F898 +thanthakhatlowrightthai;F897 +thanthakhatthai;0E4C +thanthakhatupperleftthai;F896 +theharabic;062B +thehfinalarabic;FE9A +thehinitialarabic;FE9B +thehmedialarabic;FE9C +thereexists;2203 +therefore;2234 +theta;03B8 +theta1;03D1 +thetasymbolgreek;03D1 +thieuthacirclekorean;3279 +thieuthaparenkorean;3219 +thieuthcirclekorean;326B +thieuthkorean;314C +thieuthparenkorean;320B +thirteencircle;246C +thirteenparen;2480 +thirteenperiod;2494 +thonangmonthothai;0E11 +thook;01AD +thophuthaothai;0E12 +thorn;00FE +thothahanthai;0E17 +thothanthai;0E10 +thothongthai;0E18 +thothungthai;0E16 +thousandcyrillic;0482 +thousandsseparatorarabic;066C +thousandsseparatorpersian;066C +three;0033 +threearabic;0663 +threebengali;09E9 +threecircle;2462 +threecircleinversesansserif;278C +threedeva;0969 +threeeighths;215C +threegujarati;0AE9 +threegurmukhi;0A69 +threehackarabic;0663 +threehangzhou;3023 +threeideographicparen;3222 +threeinferior;2083 +threemonospace;FF13 +threenumeratorbengali;09F6 +threeoldstyle;F733 +threeparen;2476 +threeperiod;248A +threepersian;06F3 +threequarters;00BE +threequartersemdash;F6DE +threeroman;2172 +threesuperior;00B3 +threethai;0E53 +thzsquare;3394 +tihiragana;3061 +tikatakana;30C1 +tikatakanahalfwidth;FF81 +tikeutacirclekorean;3270 +tikeutaparenkorean;3210 +tikeutcirclekorean;3262 +tikeutkorean;3137 +tikeutparenkorean;3202 +tilde;02DC +tildebelowcmb;0330 +tildecmb;0303 +tildecomb;0303 +tildedoublecmb;0360 +tildeoperator;223C +tildeoverlaycmb;0334 +tildeverticalcmb;033E +timescircle;2297 +tipehahebrew;0596 +tipehalefthebrew;0596 +tippigurmukhi;0A70 +titlocyrilliccmb;0483 +tiwnarmenian;057F +tlinebelow;1E6F +tmonospace;FF54 +toarmenian;0569 +tohiragana;3068 +tokatakana;30C8 +tokatakanahalfwidth;FF84 +tonebarextrahighmod;02E5 +tonebarextralowmod;02E9 +tonebarhighmod;02E6 +tonebarlowmod;02E8 +tonebarmidmod;02E7 +tonefive;01BD +tonesix;0185 +tonetwo;01A8 +tonos;0384 +tonsquare;3327 +topatakthai;0E0F +tortoiseshellbracketleft;3014 +tortoiseshellbracketleftsmall;FE5D +tortoiseshellbracketleftvertical;FE39 +tortoiseshellbracketright;3015 +tortoiseshellbracketrightsmall;FE5E +tortoiseshellbracketrightvertical;FE3A +totaothai;0E15 +tpalatalhook;01AB +tparen;24AF +trademark;2122 +trademarksans;F8EA +trademarkserif;F6DB +tretroflexhook;0288 +triagdn;25BC +triaglf;25C4 +triagrt;25BA +triagup;25B2 +ts;02A6 +tsadi;05E6 +tsadidagesh;FB46 +tsadidageshhebrew;FB46 +tsadihebrew;05E6 +tsecyrillic;0446 +tsere;05B5 +tsere12;05B5 +tsere1e;05B5 +tsere2b;05B5 +tserehebrew;05B5 +tserenarrowhebrew;05B5 +tserequarterhebrew;05B5 +tserewidehebrew;05B5 +tshecyrillic;045B +tsuperior;F6F3 +ttabengali;099F +ttadeva;091F +ttagujarati;0A9F +ttagurmukhi;0A1F +tteharabic;0679 +ttehfinalarabic;FB67 +ttehinitialarabic;FB68 +ttehmedialarabic;FB69 +tthabengali;09A0 +tthadeva;0920 +tthagujarati;0AA0 +tthagurmukhi;0A20 +tturned;0287 +tuhiragana;3064 +tukatakana;30C4 +tukatakanahalfwidth;FF82 +tusmallhiragana;3063 +tusmallkatakana;30C3 +tusmallkatakanahalfwidth;FF6F +twelvecircle;246B +twelveparen;247F +twelveperiod;2493 +twelveroman;217B +twentycircle;2473 +twentyhangzhou;5344 +twentyparen;2487 +twentyperiod;249B +two;0032 +twoarabic;0662 +twobengali;09E8 +twocircle;2461 +twocircleinversesansserif;278B +twodeva;0968 +twodotenleader;2025 +twodotleader;2025 +twodotleadervertical;FE30 +twogujarati;0AE8 +twogurmukhi;0A68 +twohackarabic;0662 +twohangzhou;3022 +twoideographicparen;3221 +twoinferior;2082 +twomonospace;FF12 +twonumeratorbengali;09F5 +twooldstyle;F732 +twoparen;2475 +twoperiod;2489 +twopersian;06F2 +tworoman;2171 +twostroke;01BB +twosuperior;00B2 +twothai;0E52 +twothirds;2154 +u;0075 +uacute;00FA +ubar;0289 +ubengali;0989 +ubopomofo;3128 +ubreve;016D +ucaron;01D4 +ucircle;24E4 +ucircumflex;00FB +ucircumflexbelow;1E77 +ucyrillic;0443 +udattadeva;0951 +udblacute;0171 +udblgrave;0215 +udeva;0909 +udieresis;00FC +udieresisacute;01D8 +udieresisbelow;1E73 +udieresiscaron;01DA +udieresiscyrillic;04F1 +udieresisgrave;01DC +udieresismacron;01D6 +udotbelow;1EE5 +ugrave;00F9 +ugujarati;0A89 +ugurmukhi;0A09 +uhiragana;3046 +uhookabove;1EE7 +uhorn;01B0 +uhornacute;1EE9 +uhorndotbelow;1EF1 +uhorngrave;1EEB +uhornhookabove;1EED +uhorntilde;1EEF +uhungarumlaut;0171 +uhungarumlautcyrillic;04F3 +uinvertedbreve;0217 +ukatakana;30A6 +ukatakanahalfwidth;FF73 +ukcyrillic;0479 +ukorean;315C +umacron;016B +umacroncyrillic;04EF +umacrondieresis;1E7B +umatragurmukhi;0A41 +umonospace;FF55 +underscore;005F +underscoredbl;2017 +underscoremonospace;FF3F +underscorevertical;FE33 +underscorewavy;FE4F +union;222A +universal;2200 +uogonek;0173 +uparen;24B0 +upblock;2580 +upperdothebrew;05C4 +upsilon;03C5 +upsilondieresis;03CB +upsilondieresistonos;03B0 +upsilonlatin;028A +upsilontonos;03CD +uptackbelowcmb;031D +uptackmod;02D4 +uragurmukhi;0A73 +uring;016F +ushortcyrillic;045E +usmallhiragana;3045 +usmallkatakana;30A5 +usmallkatakanahalfwidth;FF69 +ustraightcyrillic;04AF +ustraightstrokecyrillic;04B1 +utilde;0169 +utildeacute;1E79 +utildebelow;1E75 +uubengali;098A +uudeva;090A +uugujarati;0A8A +uugurmukhi;0A0A +uumatragurmukhi;0A42 +uuvowelsignbengali;09C2 +uuvowelsigndeva;0942 +uuvowelsigngujarati;0AC2 +uvowelsignbengali;09C1 +uvowelsigndeva;0941 +uvowelsigngujarati;0AC1 +v;0076 +vadeva;0935 +vagujarati;0AB5 +vagurmukhi;0A35 +vakatakana;30F7 +vav;05D5 +vavdagesh;FB35 +vavdagesh65;FB35 +vavdageshhebrew;FB35 +vavhebrew;05D5 +vavholam;FB4B +vavholamhebrew;FB4B +vavvavhebrew;05F0 +vavyodhebrew;05F1 +vcircle;24E5 +vdotbelow;1E7F +vecyrillic;0432 +veharabic;06A4 +vehfinalarabic;FB6B +vehinitialarabic;FB6C +vehmedialarabic;FB6D +vekatakana;30F9 +venus;2640 +verticalbar;007C +verticallineabovecmb;030D +verticallinebelowcmb;0329 +verticallinelowmod;02CC +verticallinemod;02C8 +vewarmenian;057E +vhook;028B +vikatakana;30F8 +viramabengali;09CD +viramadeva;094D +viramagujarati;0ACD +visargabengali;0983 +visargadeva;0903 +visargagujarati;0A83 +vmonospace;FF56 +voarmenian;0578 +voicediterationhiragana;309E +voicediterationkatakana;30FE +voicedmarkkana;309B +voicedmarkkanahalfwidth;FF9E +vokatakana;30FA +vparen;24B1 +vtilde;1E7D +vturned;028C +vuhiragana;3094 +vukatakana;30F4 +w;0077 +wacute;1E83 +waekorean;3159 +wahiragana;308F +wakatakana;30EF +wakatakanahalfwidth;FF9C +wakorean;3158 +wasmallhiragana;308E +wasmallkatakana;30EE +wattosquare;3357 +wavedash;301C +wavyunderscorevertical;FE34 +wawarabic;0648 +wawfinalarabic;FEEE +wawhamzaabovearabic;0624 +wawhamzaabovefinalarabic;FE86 +wbsquare;33DD +wcircle;24E6 +wcircumflex;0175 +wdieresis;1E85 +wdotaccent;1E87 +wdotbelow;1E89 +wehiragana;3091 +weierstrass;2118 +wekatakana;30F1 +wekorean;315E +weokorean;315D +wgrave;1E81 +whitebullet;25E6 +whitecircle;25CB +whitecircleinverse;25D9 +whitecornerbracketleft;300E +whitecornerbracketleftvertical;FE43 +whitecornerbracketright;300F +whitecornerbracketrightvertical;FE44 +whitediamond;25C7 +whitediamondcontainingblacksmalldiamond;25C8 +whitedownpointingsmalltriangle;25BF +whitedownpointingtriangle;25BD +whiteleftpointingsmalltriangle;25C3 +whiteleftpointingtriangle;25C1 +whitelenticularbracketleft;3016 +whitelenticularbracketright;3017 +whiterightpointingsmalltriangle;25B9 +whiterightpointingtriangle;25B7 +whitesmallsquare;25AB +whitesmilingface;263A +whitesquare;25A1 +whitestar;2606 +whitetelephone;260F +whitetortoiseshellbracketleft;3018 +whitetortoiseshellbracketright;3019 +whiteuppointingsmalltriangle;25B5 +whiteuppointingtriangle;25B3 +wihiragana;3090 +wikatakana;30F0 +wikorean;315F +wmonospace;FF57 +wohiragana;3092 +wokatakana;30F2 +wokatakanahalfwidth;FF66 +won;20A9 +wonmonospace;FFE6 +wowaenthai;0E27 +wparen;24B2 +wring;1E98 +wsuperior;02B7 +wturned;028D +wynn;01BF +x;0078 +xabovecmb;033D +xbopomofo;3112 +xcircle;24E7 +xdieresis;1E8D +xdotaccent;1E8B +xeharmenian;056D +xi;03BE +xmonospace;FF58 +xparen;24B3 +xsuperior;02E3 +y;0079 +yaadosquare;334E +yabengali;09AF +yacute;00FD +yadeva;092F +yaekorean;3152 +yagujarati;0AAF +yagurmukhi;0A2F +yahiragana;3084 +yakatakana;30E4 +yakatakanahalfwidth;FF94 +yakorean;3151 +yamakkanthai;0E4E +yasmallhiragana;3083 +yasmallkatakana;30E3 +yasmallkatakanahalfwidth;FF6C +yatcyrillic;0463 +ycircle;24E8 +ycircumflex;0177 +ydieresis;00FF +ydotaccent;1E8F +ydotbelow;1EF5 +yeharabic;064A +yehbarreearabic;06D2 +yehbarreefinalarabic;FBAF +yehfinalarabic;FEF2 +yehhamzaabovearabic;0626 +yehhamzaabovefinalarabic;FE8A +yehhamzaaboveinitialarabic;FE8B +yehhamzaabovemedialarabic;FE8C +yehinitialarabic;FEF3 +yehmedialarabic;FEF4 +yehmeeminitialarabic;FCDD +yehmeemisolatedarabic;FC58 +yehnoonfinalarabic;FC94 +yehthreedotsbelowarabic;06D1 +yekorean;3156 +yen;00A5 +yenmonospace;FFE5 +yeokorean;3155 +yeorinhieuhkorean;3186 +yerahbenyomohebrew;05AA +yerahbenyomolefthebrew;05AA +yericyrillic;044B +yerudieresiscyrillic;04F9 +yesieungkorean;3181 +yesieungpansioskorean;3183 +yesieungsioskorean;3182 +yetivhebrew;059A +ygrave;1EF3 +yhook;01B4 +yhookabove;1EF7 +yiarmenian;0575 +yicyrillic;0457 +yikorean;3162 +yinyang;262F +yiwnarmenian;0582 +ymonospace;FF59 +yod;05D9 +yoddagesh;FB39 +yoddageshhebrew;FB39 +yodhebrew;05D9 +yodyodhebrew;05F2 +yodyodpatahhebrew;FB1F +yohiragana;3088 +yoikorean;3189 +yokatakana;30E8 +yokatakanahalfwidth;FF96 +yokorean;315B +yosmallhiragana;3087 +yosmallkatakana;30E7 +yosmallkatakanahalfwidth;FF6E +yotgreek;03F3 +yoyaekorean;3188 +yoyakorean;3187 +yoyakthai;0E22 +yoyingthai;0E0D +yparen;24B4 +ypogegrammeni;037A +ypogegrammenigreekcmb;0345 +yr;01A6 +yring;1E99 +ysuperior;02B8 +ytilde;1EF9 +yturned;028E +yuhiragana;3086 +yuikorean;318C +yukatakana;30E6 +yukatakanahalfwidth;FF95 +yukorean;3160 +yusbigcyrillic;046B +yusbigiotifiedcyrillic;046D +yuslittlecyrillic;0467 +yuslittleiotifiedcyrillic;0469 +yusmallhiragana;3085 +yusmallkatakana;30E5 +yusmallkatakanahalfwidth;FF6D +yuyekorean;318B +yuyeokorean;318A +yyabengali;09DF +yyadeva;095F +z;007A +zaarmenian;0566 +zacute;017A +zadeva;095B +zagurmukhi;0A5B +zaharabic;0638 +zahfinalarabic;FEC6 +zahinitialarabic;FEC7 +zahiragana;3056 +zahmedialarabic;FEC8 +zainarabic;0632 +zainfinalarabic;FEB0 +zakatakana;30B6 +zaqefgadolhebrew;0595 +zaqefqatanhebrew;0594 +zarqahebrew;0598 +zayin;05D6 +zayindagesh;FB36 +zayindageshhebrew;FB36 +zayinhebrew;05D6 +zbopomofo;3117 +zcaron;017E +zcircle;24E9 +zcircumflex;1E91 +zcurl;0291 +zdot;017C +zdotaccent;017C +zdotbelow;1E93 +zecyrillic;0437 +zedescendercyrillic;0499 +zedieresiscyrillic;04DF +zehiragana;305C +zekatakana;30BC +zero;0030 +zeroarabic;0660 +zerobengali;09E6 +zerodeva;0966 +zerogujarati;0AE6 +zerogurmukhi;0A66 +zerohackarabic;0660 +zeroinferior;2080 +zeromonospace;FF10 +zerooldstyle;F730 +zeropersian;06F0 +zerosuperior;2070 +zerothai;0E50 +zerowidthjoiner;FEFF +zerowidthnonjoiner;200C +zerowidthspace;200B +zeta;03B6 +zhbopomofo;3113 +zhearmenian;056A +zhebrevecyrillic;04C2 +zhecyrillic;0436 +zhedescendercyrillic;0497 +zhedieresiscyrillic;04DD +zihiragana;3058 +zikatakana;30B8 +zinorhebrew;05AE +zlinebelow;1E95 +zmonospace;FF5A +zohiragana;305E +zokatakana;30BE +zparen;24B5 +zretroflexhook;0290 +zstroke;01B6 +zuhiragana;305A +zukatakana;30BA +# END +""" + + +_aglfnText = """\ +# ----------------------------------------------------------- +# Copyright 2002-2019 Adobe (http://www.adobe.com/). +# +# Redistribution and use in source and binary forms, with or +# without modification, are permitted provided that the +# following conditions are met: +# +# Redistributions of source code must retain the above +# copyright notice, this list of conditions and the following +# disclaimer. +# +# Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials +# provided with the distribution. +# +# Neither the name of Adobe nor the names of its contributors +# may be used to endorse or promote products derived from this +# software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ----------------------------------------------------------- +# Name: Adobe Glyph List For New Fonts +# Table version: 1.7 +# Date: November 6, 2008 +# URL: https://github.com/adobe-type-tools/agl-aglfn +# +# Description: +# +# AGLFN (Adobe Glyph List For New Fonts) provides a list of base glyph +# names that are recommended for new fonts, which are compatible with +# the AGL (Adobe Glyph List) Specification, and which should be used +# as described in Section 6 of that document. AGLFN comprises the set +# of glyph names from AGL that map via the AGL Specification rules to +# the semantically correct UV (Unicode Value). For example, "Asmall" +# is omitted because AGL maps this glyph name to the PUA (Private Use +# Area) value U+F761, rather than to the UV that maps from the glyph +# name "A." Also omitted is "ffi," because AGL maps this to the +# Alphabetic Presentation Forms value U+FB03, rather than decomposing +# it into the following sequence of three UVs: U+0066, U+0066, and +# U+0069. The name "arrowvertex" has been omitted because this glyph +# now has a real UV, and AGL is now incorrect in mapping it to the PUA +# value U+F8E6. If you do not find an appropriate name for your glyph +# in this list, then please refer to Section 6 of the AGL +# Specification. +# +# Format: three semicolon-delimited fields: +# (1) Standard UV or CUS UV--four uppercase hexadecimal digits +# (2) Glyph name--upper/lowercase letters and digits +# (3) Character names: Unicode character names for standard UVs, and +# descriptive names for CUS UVs--uppercase letters, hyphen, and +# space +# +# The records are sorted by glyph name in increasing ASCII order, +# entries with the same glyph name are sorted in decreasing priority +# order, the UVs and Unicode character names are provided for +# convenience, lines starting with "#" are comments, and blank lines +# should be ignored. +# +# Revision History: +# +# 1.7 [6 November 2008] +# - Reverted to the original 1.4 and earlier mappings for Delta, +# Omega, and mu. +# - Removed mappings for "afii" names. These should now be assigned +# "uni" names. +# - Removed mappings for "commaaccent" names. These should now be +# assigned "uni" names. +# +# 1.6 [30 January 2006] +# - Completed work intended in 1.5. +# +# 1.5 [23 November 2005] +# - Removed duplicated block at end of file. +# - Changed mappings: +# 2206;Delta;INCREMENT changed to 0394;Delta;GREEK CAPITAL LETTER DELTA +# 2126;Omega;OHM SIGN changed to 03A9;Omega;GREEK CAPITAL LETTER OMEGA +# 03BC;mu;MICRO SIGN changed to 03BC;mu;GREEK SMALL LETTER MU +# - Corrected statement above about why "ffi" is omitted. +# +# 1.4 [24 September 2003] +# - Changed version to 1.4, to avoid confusion with the AGL 1.3. +# - Fixed spelling errors in the header. +# - Fully removed "arrowvertex," as it is mapped only to a PUA Unicode +# value in some fonts. +# +# 1.1 [17 April 2003] +# - Renamed [Tt]cedilla back to [Tt]commaaccent. +# +# 1.0 [31 January 2003] +# - Original version. +# - Derived from the AGLv1.2 by: +# removing the PUA area codes; +# removing duplicate Unicode mappings; and +# renaming "tcommaaccent" to "tcedilla" and "Tcommaaccent" to "Tcedilla" +# +0041;A;LATIN CAPITAL LETTER A +00C6;AE;LATIN CAPITAL LETTER AE +01FC;AEacute;LATIN CAPITAL LETTER AE WITH ACUTE +00C1;Aacute;LATIN CAPITAL LETTER A WITH ACUTE +0102;Abreve;LATIN CAPITAL LETTER A WITH BREVE +00C2;Acircumflex;LATIN CAPITAL LETTER A WITH CIRCUMFLEX +00C4;Adieresis;LATIN CAPITAL LETTER A WITH DIAERESIS +00C0;Agrave;LATIN CAPITAL LETTER A WITH GRAVE +0391;Alpha;GREEK CAPITAL LETTER ALPHA +0386;Alphatonos;GREEK CAPITAL LETTER ALPHA WITH TONOS +0100;Amacron;LATIN CAPITAL LETTER A WITH MACRON +0104;Aogonek;LATIN CAPITAL LETTER A WITH OGONEK +00C5;Aring;LATIN CAPITAL LETTER A WITH RING ABOVE +01FA;Aringacute;LATIN CAPITAL LETTER A WITH RING ABOVE AND ACUTE +00C3;Atilde;LATIN CAPITAL LETTER A WITH TILDE +0042;B;LATIN CAPITAL LETTER B +0392;Beta;GREEK CAPITAL LETTER BETA +0043;C;LATIN CAPITAL LETTER C +0106;Cacute;LATIN CAPITAL LETTER C WITH ACUTE +010C;Ccaron;LATIN CAPITAL LETTER C WITH CARON +00C7;Ccedilla;LATIN CAPITAL LETTER C WITH CEDILLA +0108;Ccircumflex;LATIN CAPITAL LETTER C WITH CIRCUMFLEX +010A;Cdotaccent;LATIN CAPITAL LETTER C WITH DOT ABOVE +03A7;Chi;GREEK CAPITAL LETTER CHI +0044;D;LATIN CAPITAL LETTER D +010E;Dcaron;LATIN CAPITAL LETTER D WITH CARON +0110;Dcroat;LATIN CAPITAL LETTER D WITH STROKE +2206;Delta;INCREMENT +0045;E;LATIN CAPITAL LETTER E +00C9;Eacute;LATIN CAPITAL LETTER E WITH ACUTE +0114;Ebreve;LATIN CAPITAL LETTER E WITH BREVE +011A;Ecaron;LATIN CAPITAL LETTER E WITH CARON +00CA;Ecircumflex;LATIN CAPITAL LETTER E WITH CIRCUMFLEX +00CB;Edieresis;LATIN CAPITAL LETTER E WITH DIAERESIS +0116;Edotaccent;LATIN CAPITAL LETTER E WITH DOT ABOVE +00C8;Egrave;LATIN CAPITAL LETTER E WITH GRAVE +0112;Emacron;LATIN CAPITAL LETTER E WITH MACRON +014A;Eng;LATIN CAPITAL LETTER ENG +0118;Eogonek;LATIN CAPITAL LETTER E WITH OGONEK +0395;Epsilon;GREEK CAPITAL LETTER EPSILON +0388;Epsilontonos;GREEK CAPITAL LETTER EPSILON WITH TONOS +0397;Eta;GREEK CAPITAL LETTER ETA +0389;Etatonos;GREEK CAPITAL LETTER ETA WITH TONOS +00D0;Eth;LATIN CAPITAL LETTER ETH +20AC;Euro;EURO SIGN +0046;F;LATIN CAPITAL LETTER F +0047;G;LATIN CAPITAL LETTER G +0393;Gamma;GREEK CAPITAL LETTER GAMMA +011E;Gbreve;LATIN CAPITAL LETTER G WITH BREVE +01E6;Gcaron;LATIN CAPITAL LETTER G WITH CARON +011C;Gcircumflex;LATIN CAPITAL LETTER G WITH CIRCUMFLEX +0120;Gdotaccent;LATIN CAPITAL LETTER G WITH DOT ABOVE +0048;H;LATIN CAPITAL LETTER H +25CF;H18533;BLACK CIRCLE +25AA;H18543;BLACK SMALL SQUARE +25AB;H18551;WHITE SMALL SQUARE +25A1;H22073;WHITE SQUARE +0126;Hbar;LATIN CAPITAL LETTER H WITH STROKE +0124;Hcircumflex;LATIN CAPITAL LETTER H WITH CIRCUMFLEX +0049;I;LATIN CAPITAL LETTER I +0132;IJ;LATIN CAPITAL LIGATURE IJ +00CD;Iacute;LATIN CAPITAL LETTER I WITH ACUTE +012C;Ibreve;LATIN CAPITAL LETTER I WITH BREVE +00CE;Icircumflex;LATIN CAPITAL LETTER I WITH CIRCUMFLEX +00CF;Idieresis;LATIN CAPITAL LETTER I WITH DIAERESIS +0130;Idotaccent;LATIN CAPITAL LETTER I WITH DOT ABOVE +2111;Ifraktur;BLACK-LETTER CAPITAL I +00CC;Igrave;LATIN CAPITAL LETTER I WITH GRAVE +012A;Imacron;LATIN CAPITAL LETTER I WITH MACRON +012E;Iogonek;LATIN CAPITAL LETTER I WITH OGONEK +0399;Iota;GREEK CAPITAL LETTER IOTA +03AA;Iotadieresis;GREEK CAPITAL LETTER IOTA WITH DIALYTIKA +038A;Iotatonos;GREEK CAPITAL LETTER IOTA WITH TONOS +0128;Itilde;LATIN CAPITAL LETTER I WITH TILDE +004A;J;LATIN CAPITAL LETTER J +0134;Jcircumflex;LATIN CAPITAL LETTER J WITH CIRCUMFLEX +004B;K;LATIN CAPITAL LETTER K +039A;Kappa;GREEK CAPITAL LETTER KAPPA +004C;L;LATIN CAPITAL LETTER L +0139;Lacute;LATIN CAPITAL LETTER L WITH ACUTE +039B;Lambda;GREEK CAPITAL LETTER LAMDA +013D;Lcaron;LATIN CAPITAL LETTER L WITH CARON +013F;Ldot;LATIN CAPITAL LETTER L WITH MIDDLE DOT +0141;Lslash;LATIN CAPITAL LETTER L WITH STROKE +004D;M;LATIN CAPITAL LETTER M +039C;Mu;GREEK CAPITAL LETTER MU +004E;N;LATIN CAPITAL LETTER N +0143;Nacute;LATIN CAPITAL LETTER N WITH ACUTE +0147;Ncaron;LATIN CAPITAL LETTER N WITH CARON +00D1;Ntilde;LATIN CAPITAL LETTER N WITH TILDE +039D;Nu;GREEK CAPITAL LETTER NU +004F;O;LATIN CAPITAL LETTER O +0152;OE;LATIN CAPITAL LIGATURE OE +00D3;Oacute;LATIN CAPITAL LETTER O WITH ACUTE +014E;Obreve;LATIN CAPITAL LETTER O WITH BREVE +00D4;Ocircumflex;LATIN CAPITAL LETTER O WITH CIRCUMFLEX +00D6;Odieresis;LATIN CAPITAL LETTER O WITH DIAERESIS +00D2;Ograve;LATIN CAPITAL LETTER O WITH GRAVE +01A0;Ohorn;LATIN CAPITAL LETTER O WITH HORN +0150;Ohungarumlaut;LATIN CAPITAL LETTER O WITH DOUBLE ACUTE +014C;Omacron;LATIN CAPITAL LETTER O WITH MACRON +2126;Omega;OHM SIGN +038F;Omegatonos;GREEK CAPITAL LETTER OMEGA WITH TONOS +039F;Omicron;GREEK CAPITAL LETTER OMICRON +038C;Omicrontonos;GREEK CAPITAL LETTER OMICRON WITH TONOS +00D8;Oslash;LATIN CAPITAL LETTER O WITH STROKE +01FE;Oslashacute;LATIN CAPITAL LETTER O WITH STROKE AND ACUTE +00D5;Otilde;LATIN CAPITAL LETTER O WITH TILDE +0050;P;LATIN CAPITAL LETTER P +03A6;Phi;GREEK CAPITAL LETTER PHI +03A0;Pi;GREEK CAPITAL LETTER PI +03A8;Psi;GREEK CAPITAL LETTER PSI +0051;Q;LATIN CAPITAL LETTER Q +0052;R;LATIN CAPITAL LETTER R +0154;Racute;LATIN CAPITAL LETTER R WITH ACUTE +0158;Rcaron;LATIN CAPITAL LETTER R WITH CARON +211C;Rfraktur;BLACK-LETTER CAPITAL R +03A1;Rho;GREEK CAPITAL LETTER RHO +0053;S;LATIN CAPITAL LETTER S +250C;SF010000;BOX DRAWINGS LIGHT DOWN AND RIGHT +2514;SF020000;BOX DRAWINGS LIGHT UP AND RIGHT +2510;SF030000;BOX DRAWINGS LIGHT DOWN AND LEFT +2518;SF040000;BOX DRAWINGS LIGHT UP AND LEFT +253C;SF050000;BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL +252C;SF060000;BOX DRAWINGS LIGHT DOWN AND HORIZONTAL +2534;SF070000;BOX DRAWINGS LIGHT UP AND HORIZONTAL +251C;SF080000;BOX DRAWINGS LIGHT VERTICAL AND RIGHT +2524;SF090000;BOX DRAWINGS LIGHT VERTICAL AND LEFT +2500;SF100000;BOX DRAWINGS LIGHT HORIZONTAL +2502;SF110000;BOX DRAWINGS LIGHT VERTICAL +2561;SF190000;BOX DRAWINGS VERTICAL SINGLE AND LEFT DOUBLE +2562;SF200000;BOX DRAWINGS VERTICAL DOUBLE AND LEFT SINGLE +2556;SF210000;BOX DRAWINGS DOWN DOUBLE AND LEFT SINGLE +2555;SF220000;BOX DRAWINGS DOWN SINGLE AND LEFT DOUBLE +2563;SF230000;BOX DRAWINGS DOUBLE VERTICAL AND LEFT +2551;SF240000;BOX DRAWINGS DOUBLE VERTICAL +2557;SF250000;BOX DRAWINGS DOUBLE DOWN AND LEFT +255D;SF260000;BOX DRAWINGS DOUBLE UP AND LEFT +255C;SF270000;BOX DRAWINGS UP DOUBLE AND LEFT SINGLE +255B;SF280000;BOX DRAWINGS UP SINGLE AND LEFT DOUBLE +255E;SF360000;BOX DRAWINGS VERTICAL SINGLE AND RIGHT DOUBLE +255F;SF370000;BOX DRAWINGS VERTICAL DOUBLE AND RIGHT SINGLE +255A;SF380000;BOX DRAWINGS DOUBLE UP AND RIGHT +2554;SF390000;BOX DRAWINGS DOUBLE DOWN AND RIGHT +2569;SF400000;BOX DRAWINGS DOUBLE UP AND HORIZONTAL +2566;SF410000;BOX DRAWINGS DOUBLE DOWN AND HORIZONTAL +2560;SF420000;BOX DRAWINGS DOUBLE VERTICAL AND RIGHT +2550;SF430000;BOX DRAWINGS DOUBLE HORIZONTAL +256C;SF440000;BOX DRAWINGS DOUBLE VERTICAL AND HORIZONTAL +2567;SF450000;BOX DRAWINGS UP SINGLE AND HORIZONTAL DOUBLE +2568;SF460000;BOX DRAWINGS UP DOUBLE AND HORIZONTAL SINGLE +2564;SF470000;BOX DRAWINGS DOWN SINGLE AND HORIZONTAL DOUBLE +2565;SF480000;BOX DRAWINGS DOWN DOUBLE AND HORIZONTAL SINGLE +2559;SF490000;BOX DRAWINGS UP DOUBLE AND RIGHT SINGLE +2558;SF500000;BOX DRAWINGS UP SINGLE AND RIGHT DOUBLE +2552;SF510000;BOX DRAWINGS DOWN SINGLE AND RIGHT DOUBLE +2553;SF520000;BOX DRAWINGS DOWN DOUBLE AND RIGHT SINGLE +256B;SF530000;BOX DRAWINGS VERTICAL DOUBLE AND HORIZONTAL SINGLE +256A;SF540000;BOX DRAWINGS VERTICAL SINGLE AND HORIZONTAL DOUBLE +015A;Sacute;LATIN CAPITAL LETTER S WITH ACUTE +0160;Scaron;LATIN CAPITAL LETTER S WITH CARON +015E;Scedilla;LATIN CAPITAL LETTER S WITH CEDILLA +015C;Scircumflex;LATIN CAPITAL LETTER S WITH CIRCUMFLEX +03A3;Sigma;GREEK CAPITAL LETTER SIGMA +0054;T;LATIN CAPITAL LETTER T +03A4;Tau;GREEK CAPITAL LETTER TAU +0166;Tbar;LATIN CAPITAL LETTER T WITH STROKE +0164;Tcaron;LATIN CAPITAL LETTER T WITH CARON +0398;Theta;GREEK CAPITAL LETTER THETA +00DE;Thorn;LATIN CAPITAL LETTER THORN +0055;U;LATIN CAPITAL LETTER U +00DA;Uacute;LATIN CAPITAL LETTER U WITH ACUTE +016C;Ubreve;LATIN CAPITAL LETTER U WITH BREVE +00DB;Ucircumflex;LATIN CAPITAL LETTER U WITH CIRCUMFLEX +00DC;Udieresis;LATIN CAPITAL LETTER U WITH DIAERESIS +00D9;Ugrave;LATIN CAPITAL LETTER U WITH GRAVE +01AF;Uhorn;LATIN CAPITAL LETTER U WITH HORN +0170;Uhungarumlaut;LATIN CAPITAL LETTER U WITH DOUBLE ACUTE +016A;Umacron;LATIN CAPITAL LETTER U WITH MACRON +0172;Uogonek;LATIN CAPITAL LETTER U WITH OGONEK +03A5;Upsilon;GREEK CAPITAL LETTER UPSILON +03D2;Upsilon1;GREEK UPSILON WITH HOOK SYMBOL +03AB;Upsilondieresis;GREEK CAPITAL LETTER UPSILON WITH DIALYTIKA +038E;Upsilontonos;GREEK CAPITAL LETTER UPSILON WITH TONOS +016E;Uring;LATIN CAPITAL LETTER U WITH RING ABOVE +0168;Utilde;LATIN CAPITAL LETTER U WITH TILDE +0056;V;LATIN CAPITAL LETTER V +0057;W;LATIN CAPITAL LETTER W +1E82;Wacute;LATIN CAPITAL LETTER W WITH ACUTE +0174;Wcircumflex;LATIN CAPITAL LETTER W WITH CIRCUMFLEX +1E84;Wdieresis;LATIN CAPITAL LETTER W WITH DIAERESIS +1E80;Wgrave;LATIN CAPITAL LETTER W WITH GRAVE +0058;X;LATIN CAPITAL LETTER X +039E;Xi;GREEK CAPITAL LETTER XI +0059;Y;LATIN CAPITAL LETTER Y +00DD;Yacute;LATIN CAPITAL LETTER Y WITH ACUTE +0176;Ycircumflex;LATIN CAPITAL LETTER Y WITH CIRCUMFLEX +0178;Ydieresis;LATIN CAPITAL LETTER Y WITH DIAERESIS +1EF2;Ygrave;LATIN CAPITAL LETTER Y WITH GRAVE +005A;Z;LATIN CAPITAL LETTER Z +0179;Zacute;LATIN CAPITAL LETTER Z WITH ACUTE +017D;Zcaron;LATIN CAPITAL LETTER Z WITH CARON +017B;Zdotaccent;LATIN CAPITAL LETTER Z WITH DOT ABOVE +0396;Zeta;GREEK CAPITAL LETTER ZETA +0061;a;LATIN SMALL LETTER A +00E1;aacute;LATIN SMALL LETTER A WITH ACUTE +0103;abreve;LATIN SMALL LETTER A WITH BREVE +00E2;acircumflex;LATIN SMALL LETTER A WITH CIRCUMFLEX +00B4;acute;ACUTE ACCENT +0301;acutecomb;COMBINING ACUTE ACCENT +00E4;adieresis;LATIN SMALL LETTER A WITH DIAERESIS +00E6;ae;LATIN SMALL LETTER AE +01FD;aeacute;LATIN SMALL LETTER AE WITH ACUTE +00E0;agrave;LATIN SMALL LETTER A WITH GRAVE +2135;aleph;ALEF SYMBOL +03B1;alpha;GREEK SMALL LETTER ALPHA +03AC;alphatonos;GREEK SMALL LETTER ALPHA WITH TONOS +0101;amacron;LATIN SMALL LETTER A WITH MACRON +0026;ampersand;AMPERSAND +2220;angle;ANGLE +2329;angleleft;LEFT-POINTING ANGLE BRACKET +232A;angleright;RIGHT-POINTING ANGLE BRACKET +0387;anoteleia;GREEK ANO TELEIA +0105;aogonek;LATIN SMALL LETTER A WITH OGONEK +2248;approxequal;ALMOST EQUAL TO +00E5;aring;LATIN SMALL LETTER A WITH RING ABOVE +01FB;aringacute;LATIN SMALL LETTER A WITH RING ABOVE AND ACUTE +2194;arrowboth;LEFT RIGHT ARROW +21D4;arrowdblboth;LEFT RIGHT DOUBLE ARROW +21D3;arrowdbldown;DOWNWARDS DOUBLE ARROW +21D0;arrowdblleft;LEFTWARDS DOUBLE ARROW +21D2;arrowdblright;RIGHTWARDS DOUBLE ARROW +21D1;arrowdblup;UPWARDS DOUBLE ARROW +2193;arrowdown;DOWNWARDS ARROW +2190;arrowleft;LEFTWARDS ARROW +2192;arrowright;RIGHTWARDS ARROW +2191;arrowup;UPWARDS ARROW +2195;arrowupdn;UP DOWN ARROW +21A8;arrowupdnbse;UP DOWN ARROW WITH BASE +005E;asciicircum;CIRCUMFLEX ACCENT +007E;asciitilde;TILDE +002A;asterisk;ASTERISK +2217;asteriskmath;ASTERISK OPERATOR +0040;at;COMMERCIAL AT +00E3;atilde;LATIN SMALL LETTER A WITH TILDE +0062;b;LATIN SMALL LETTER B +005C;backslash;REVERSE SOLIDUS +007C;bar;VERTICAL LINE +03B2;beta;GREEK SMALL LETTER BETA +2588;block;FULL BLOCK +007B;braceleft;LEFT CURLY BRACKET +007D;braceright;RIGHT CURLY BRACKET +005B;bracketleft;LEFT SQUARE BRACKET +005D;bracketright;RIGHT SQUARE BRACKET +02D8;breve;BREVE +00A6;brokenbar;BROKEN BAR +2022;bullet;BULLET +0063;c;LATIN SMALL LETTER C +0107;cacute;LATIN SMALL LETTER C WITH ACUTE +02C7;caron;CARON +21B5;carriagereturn;DOWNWARDS ARROW WITH CORNER LEFTWARDS +010D;ccaron;LATIN SMALL LETTER C WITH CARON +00E7;ccedilla;LATIN SMALL LETTER C WITH CEDILLA +0109;ccircumflex;LATIN SMALL LETTER C WITH CIRCUMFLEX +010B;cdotaccent;LATIN SMALL LETTER C WITH DOT ABOVE +00B8;cedilla;CEDILLA +00A2;cent;CENT SIGN +03C7;chi;GREEK SMALL LETTER CHI +25CB;circle;WHITE CIRCLE +2297;circlemultiply;CIRCLED TIMES +2295;circleplus;CIRCLED PLUS +02C6;circumflex;MODIFIER LETTER CIRCUMFLEX ACCENT +2663;club;BLACK CLUB SUIT +003A;colon;COLON +20A1;colonmonetary;COLON SIGN +002C;comma;COMMA +2245;congruent;APPROXIMATELY EQUAL TO +00A9;copyright;COPYRIGHT SIGN +00A4;currency;CURRENCY SIGN +0064;d;LATIN SMALL LETTER D +2020;dagger;DAGGER +2021;daggerdbl;DOUBLE DAGGER +010F;dcaron;LATIN SMALL LETTER D WITH CARON +0111;dcroat;LATIN SMALL LETTER D WITH STROKE +00B0;degree;DEGREE SIGN +03B4;delta;GREEK SMALL LETTER DELTA +2666;diamond;BLACK DIAMOND SUIT +00A8;dieresis;DIAERESIS +0385;dieresistonos;GREEK DIALYTIKA TONOS +00F7;divide;DIVISION SIGN +2593;dkshade;DARK SHADE +2584;dnblock;LOWER HALF BLOCK +0024;dollar;DOLLAR SIGN +20AB;dong;DONG SIGN +02D9;dotaccent;DOT ABOVE +0323;dotbelowcomb;COMBINING DOT BELOW +0131;dotlessi;LATIN SMALL LETTER DOTLESS I +22C5;dotmath;DOT OPERATOR +0065;e;LATIN SMALL LETTER E +00E9;eacute;LATIN SMALL LETTER E WITH ACUTE +0115;ebreve;LATIN SMALL LETTER E WITH BREVE +011B;ecaron;LATIN SMALL LETTER E WITH CARON +00EA;ecircumflex;LATIN SMALL LETTER E WITH CIRCUMFLEX +00EB;edieresis;LATIN SMALL LETTER E WITH DIAERESIS +0117;edotaccent;LATIN SMALL LETTER E WITH DOT ABOVE +00E8;egrave;LATIN SMALL LETTER E WITH GRAVE +0038;eight;DIGIT EIGHT +2208;element;ELEMENT OF +2026;ellipsis;HORIZONTAL ELLIPSIS +0113;emacron;LATIN SMALL LETTER E WITH MACRON +2014;emdash;EM DASH +2205;emptyset;EMPTY SET +2013;endash;EN DASH +014B;eng;LATIN SMALL LETTER ENG +0119;eogonek;LATIN SMALL LETTER E WITH OGONEK +03B5;epsilon;GREEK SMALL LETTER EPSILON +03AD;epsilontonos;GREEK SMALL LETTER EPSILON WITH TONOS +003D;equal;EQUALS SIGN +2261;equivalence;IDENTICAL TO +212E;estimated;ESTIMATED SYMBOL +03B7;eta;GREEK SMALL LETTER ETA +03AE;etatonos;GREEK SMALL LETTER ETA WITH TONOS +00F0;eth;LATIN SMALL LETTER ETH +0021;exclam;EXCLAMATION MARK +203C;exclamdbl;DOUBLE EXCLAMATION MARK +00A1;exclamdown;INVERTED EXCLAMATION MARK +2203;existential;THERE EXISTS +0066;f;LATIN SMALL LETTER F +2640;female;FEMALE SIGN +2012;figuredash;FIGURE DASH +25A0;filledbox;BLACK SQUARE +25AC;filledrect;BLACK RECTANGLE +0035;five;DIGIT FIVE +215D;fiveeighths;VULGAR FRACTION FIVE EIGHTHS +0192;florin;LATIN SMALL LETTER F WITH HOOK +0034;four;DIGIT FOUR +2044;fraction;FRACTION SLASH +20A3;franc;FRENCH FRANC SIGN +0067;g;LATIN SMALL LETTER G +03B3;gamma;GREEK SMALL LETTER GAMMA +011F;gbreve;LATIN SMALL LETTER G WITH BREVE +01E7;gcaron;LATIN SMALL LETTER G WITH CARON +011D;gcircumflex;LATIN SMALL LETTER G WITH CIRCUMFLEX +0121;gdotaccent;LATIN SMALL LETTER G WITH DOT ABOVE +00DF;germandbls;LATIN SMALL LETTER SHARP S +2207;gradient;NABLA +0060;grave;GRAVE ACCENT +0300;gravecomb;COMBINING GRAVE ACCENT +003E;greater;GREATER-THAN SIGN +2265;greaterequal;GREATER-THAN OR EQUAL TO +00AB;guillemotleft;LEFT-POINTING DOUBLE ANGLE QUOTATION MARK +00BB;guillemotright;RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK +2039;guilsinglleft;SINGLE LEFT-POINTING ANGLE QUOTATION MARK +203A;guilsinglright;SINGLE RIGHT-POINTING ANGLE QUOTATION MARK +0068;h;LATIN SMALL LETTER H +0127;hbar;LATIN SMALL LETTER H WITH STROKE +0125;hcircumflex;LATIN SMALL LETTER H WITH CIRCUMFLEX +2665;heart;BLACK HEART SUIT +0309;hookabovecomb;COMBINING HOOK ABOVE +2302;house;HOUSE +02DD;hungarumlaut;DOUBLE ACUTE ACCENT +002D;hyphen;HYPHEN-MINUS +0069;i;LATIN SMALL LETTER I +00ED;iacute;LATIN SMALL LETTER I WITH ACUTE +012D;ibreve;LATIN SMALL LETTER I WITH BREVE +00EE;icircumflex;LATIN SMALL LETTER I WITH CIRCUMFLEX +00EF;idieresis;LATIN SMALL LETTER I WITH DIAERESIS +00EC;igrave;LATIN SMALL LETTER I WITH GRAVE +0133;ij;LATIN SMALL LIGATURE IJ +012B;imacron;LATIN SMALL LETTER I WITH MACRON +221E;infinity;INFINITY +222B;integral;INTEGRAL +2321;integralbt;BOTTOM HALF INTEGRAL +2320;integraltp;TOP HALF INTEGRAL +2229;intersection;INTERSECTION +25D8;invbullet;INVERSE BULLET +25D9;invcircle;INVERSE WHITE CIRCLE +263B;invsmileface;BLACK SMILING FACE +012F;iogonek;LATIN SMALL LETTER I WITH OGONEK +03B9;iota;GREEK SMALL LETTER IOTA +03CA;iotadieresis;GREEK SMALL LETTER IOTA WITH DIALYTIKA +0390;iotadieresistonos;GREEK SMALL LETTER IOTA WITH DIALYTIKA AND TONOS +03AF;iotatonos;GREEK SMALL LETTER IOTA WITH TONOS +0129;itilde;LATIN SMALL LETTER I WITH TILDE +006A;j;LATIN SMALL LETTER J +0135;jcircumflex;LATIN SMALL LETTER J WITH CIRCUMFLEX +006B;k;LATIN SMALL LETTER K +03BA;kappa;GREEK SMALL LETTER KAPPA +0138;kgreenlandic;LATIN SMALL LETTER KRA +006C;l;LATIN SMALL LETTER L +013A;lacute;LATIN SMALL LETTER L WITH ACUTE +03BB;lambda;GREEK SMALL LETTER LAMDA +013E;lcaron;LATIN SMALL LETTER L WITH CARON +0140;ldot;LATIN SMALL LETTER L WITH MIDDLE DOT +003C;less;LESS-THAN SIGN +2264;lessequal;LESS-THAN OR EQUAL TO +258C;lfblock;LEFT HALF BLOCK +20A4;lira;LIRA SIGN +2227;logicaland;LOGICAL AND +00AC;logicalnot;NOT SIGN +2228;logicalor;LOGICAL OR +017F;longs;LATIN SMALL LETTER LONG S +25CA;lozenge;LOZENGE +0142;lslash;LATIN SMALL LETTER L WITH STROKE +2591;ltshade;LIGHT SHADE +006D;m;LATIN SMALL LETTER M +00AF;macron;MACRON +2642;male;MALE SIGN +2212;minus;MINUS SIGN +2032;minute;PRIME +00B5;mu;MICRO SIGN +00D7;multiply;MULTIPLICATION SIGN +266A;musicalnote;EIGHTH NOTE +266B;musicalnotedbl;BEAMED EIGHTH NOTES +006E;n;LATIN SMALL LETTER N +0144;nacute;LATIN SMALL LETTER N WITH ACUTE +0149;napostrophe;LATIN SMALL LETTER N PRECEDED BY APOSTROPHE +0148;ncaron;LATIN SMALL LETTER N WITH CARON +0039;nine;DIGIT NINE +2209;notelement;NOT AN ELEMENT OF +2260;notequal;NOT EQUAL TO +2284;notsubset;NOT A SUBSET OF +00F1;ntilde;LATIN SMALL LETTER N WITH TILDE +03BD;nu;GREEK SMALL LETTER NU +0023;numbersign;NUMBER SIGN +006F;o;LATIN SMALL LETTER O +00F3;oacute;LATIN SMALL LETTER O WITH ACUTE +014F;obreve;LATIN SMALL LETTER O WITH BREVE +00F4;ocircumflex;LATIN SMALL LETTER O WITH CIRCUMFLEX +00F6;odieresis;LATIN SMALL LETTER O WITH DIAERESIS +0153;oe;LATIN SMALL LIGATURE OE +02DB;ogonek;OGONEK +00F2;ograve;LATIN SMALL LETTER O WITH GRAVE +01A1;ohorn;LATIN SMALL LETTER O WITH HORN +0151;ohungarumlaut;LATIN SMALL LETTER O WITH DOUBLE ACUTE +014D;omacron;LATIN SMALL LETTER O WITH MACRON +03C9;omega;GREEK SMALL LETTER OMEGA +03D6;omega1;GREEK PI SYMBOL +03CE;omegatonos;GREEK SMALL LETTER OMEGA WITH TONOS +03BF;omicron;GREEK SMALL LETTER OMICRON +03CC;omicrontonos;GREEK SMALL LETTER OMICRON WITH TONOS +0031;one;DIGIT ONE +2024;onedotenleader;ONE DOT LEADER +215B;oneeighth;VULGAR FRACTION ONE EIGHTH +00BD;onehalf;VULGAR FRACTION ONE HALF +00BC;onequarter;VULGAR FRACTION ONE QUARTER +2153;onethird;VULGAR FRACTION ONE THIRD +25E6;openbullet;WHITE BULLET +00AA;ordfeminine;FEMININE ORDINAL INDICATOR +00BA;ordmasculine;MASCULINE ORDINAL INDICATOR +221F;orthogonal;RIGHT ANGLE +00F8;oslash;LATIN SMALL LETTER O WITH STROKE +01FF;oslashacute;LATIN SMALL LETTER O WITH STROKE AND ACUTE +00F5;otilde;LATIN SMALL LETTER O WITH TILDE +0070;p;LATIN SMALL LETTER P +00B6;paragraph;PILCROW SIGN +0028;parenleft;LEFT PARENTHESIS +0029;parenright;RIGHT PARENTHESIS +2202;partialdiff;PARTIAL DIFFERENTIAL +0025;percent;PERCENT SIGN +002E;period;FULL STOP +00B7;periodcentered;MIDDLE DOT +22A5;perpendicular;UP TACK +2030;perthousand;PER MILLE SIGN +20A7;peseta;PESETA SIGN +03C6;phi;GREEK SMALL LETTER PHI +03D5;phi1;GREEK PHI SYMBOL +03C0;pi;GREEK SMALL LETTER PI +002B;plus;PLUS SIGN +00B1;plusminus;PLUS-MINUS SIGN +211E;prescription;PRESCRIPTION TAKE +220F;product;N-ARY PRODUCT +2282;propersubset;SUBSET OF +2283;propersuperset;SUPERSET OF +221D;proportional;PROPORTIONAL TO +03C8;psi;GREEK SMALL LETTER PSI +0071;q;LATIN SMALL LETTER Q +003F;question;QUESTION MARK +00BF;questiondown;INVERTED QUESTION MARK +0022;quotedbl;QUOTATION MARK +201E;quotedblbase;DOUBLE LOW-9 QUOTATION MARK +201C;quotedblleft;LEFT DOUBLE QUOTATION MARK +201D;quotedblright;RIGHT DOUBLE QUOTATION MARK +2018;quoteleft;LEFT SINGLE QUOTATION MARK +201B;quotereversed;SINGLE HIGH-REVERSED-9 QUOTATION MARK +2019;quoteright;RIGHT SINGLE QUOTATION MARK +201A;quotesinglbase;SINGLE LOW-9 QUOTATION MARK +0027;quotesingle;APOSTROPHE +0072;r;LATIN SMALL LETTER R +0155;racute;LATIN SMALL LETTER R WITH ACUTE +221A;radical;SQUARE ROOT +0159;rcaron;LATIN SMALL LETTER R WITH CARON +2286;reflexsubset;SUBSET OF OR EQUAL TO +2287;reflexsuperset;SUPERSET OF OR EQUAL TO +00AE;registered;REGISTERED SIGN +2310;revlogicalnot;REVERSED NOT SIGN +03C1;rho;GREEK SMALL LETTER RHO +02DA;ring;RING ABOVE +2590;rtblock;RIGHT HALF BLOCK +0073;s;LATIN SMALL LETTER S +015B;sacute;LATIN SMALL LETTER S WITH ACUTE +0161;scaron;LATIN SMALL LETTER S WITH CARON +015F;scedilla;LATIN SMALL LETTER S WITH CEDILLA +015D;scircumflex;LATIN SMALL LETTER S WITH CIRCUMFLEX +2033;second;DOUBLE PRIME +00A7;section;SECTION SIGN +003B;semicolon;SEMICOLON +0037;seven;DIGIT SEVEN +215E;seveneighths;VULGAR FRACTION SEVEN EIGHTHS +2592;shade;MEDIUM SHADE +03C3;sigma;GREEK SMALL LETTER SIGMA +03C2;sigma1;GREEK SMALL LETTER FINAL SIGMA +223C;similar;TILDE OPERATOR +0036;six;DIGIT SIX +002F;slash;SOLIDUS +263A;smileface;WHITE SMILING FACE +0020;space;SPACE +2660;spade;BLACK SPADE SUIT +00A3;sterling;POUND SIGN +220B;suchthat;CONTAINS AS MEMBER +2211;summation;N-ARY SUMMATION +263C;sun;WHITE SUN WITH RAYS +0074;t;LATIN SMALL LETTER T +03C4;tau;GREEK SMALL LETTER TAU +0167;tbar;LATIN SMALL LETTER T WITH STROKE +0165;tcaron;LATIN SMALL LETTER T WITH CARON +2234;therefore;THEREFORE +03B8;theta;GREEK SMALL LETTER THETA +03D1;theta1;GREEK THETA SYMBOL +00FE;thorn;LATIN SMALL LETTER THORN +0033;three;DIGIT THREE +215C;threeeighths;VULGAR FRACTION THREE EIGHTHS +00BE;threequarters;VULGAR FRACTION THREE QUARTERS +02DC;tilde;SMALL TILDE +0303;tildecomb;COMBINING TILDE +0384;tonos;GREEK TONOS +2122;trademark;TRADE MARK SIGN +25BC;triagdn;BLACK DOWN-POINTING TRIANGLE +25C4;triaglf;BLACK LEFT-POINTING POINTER +25BA;triagrt;BLACK RIGHT-POINTING POINTER +25B2;triagup;BLACK UP-POINTING TRIANGLE +0032;two;DIGIT TWO +2025;twodotenleader;TWO DOT LEADER +2154;twothirds;VULGAR FRACTION TWO THIRDS +0075;u;LATIN SMALL LETTER U +00FA;uacute;LATIN SMALL LETTER U WITH ACUTE +016D;ubreve;LATIN SMALL LETTER U WITH BREVE +00FB;ucircumflex;LATIN SMALL LETTER U WITH CIRCUMFLEX +00FC;udieresis;LATIN SMALL LETTER U WITH DIAERESIS +00F9;ugrave;LATIN SMALL LETTER U WITH GRAVE +01B0;uhorn;LATIN SMALL LETTER U WITH HORN +0171;uhungarumlaut;LATIN SMALL LETTER U WITH DOUBLE ACUTE +016B;umacron;LATIN SMALL LETTER U WITH MACRON +005F;underscore;LOW LINE +2017;underscoredbl;DOUBLE LOW LINE +222A;union;UNION +2200;universal;FOR ALL +0173;uogonek;LATIN SMALL LETTER U WITH OGONEK +2580;upblock;UPPER HALF BLOCK +03C5;upsilon;GREEK SMALL LETTER UPSILON +03CB;upsilondieresis;GREEK SMALL LETTER UPSILON WITH DIALYTIKA +03B0;upsilondieresistonos;GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND TONOS +03CD;upsilontonos;GREEK SMALL LETTER UPSILON WITH TONOS +016F;uring;LATIN SMALL LETTER U WITH RING ABOVE +0169;utilde;LATIN SMALL LETTER U WITH TILDE +0076;v;LATIN SMALL LETTER V +0077;w;LATIN SMALL LETTER W +1E83;wacute;LATIN SMALL LETTER W WITH ACUTE +0175;wcircumflex;LATIN SMALL LETTER W WITH CIRCUMFLEX +1E85;wdieresis;LATIN SMALL LETTER W WITH DIAERESIS +2118;weierstrass;SCRIPT CAPITAL P +1E81;wgrave;LATIN SMALL LETTER W WITH GRAVE +0078;x;LATIN SMALL LETTER X +03BE;xi;GREEK SMALL LETTER XI +0079;y;LATIN SMALL LETTER Y +00FD;yacute;LATIN SMALL LETTER Y WITH ACUTE +0177;ycircumflex;LATIN SMALL LETTER Y WITH CIRCUMFLEX +00FF;ydieresis;LATIN SMALL LETTER Y WITH DIAERESIS +00A5;yen;YEN SIGN +1EF3;ygrave;LATIN SMALL LETTER Y WITH GRAVE +007A;z;LATIN SMALL LETTER Z +017A;zacute;LATIN SMALL LETTER Z WITH ACUTE +017E;zcaron;LATIN SMALL LETTER Z WITH CARON +017C;zdotaccent;LATIN SMALL LETTER Z WITH DOT ABOVE +0030;zero;DIGIT ZERO +03B6;zeta;GREEK SMALL LETTER ZETA +# END +""" + + +class AGLError(Exception): + pass + + +LEGACY_AGL2UV = {} +AGL2UV = {} +UV2AGL = {} + + +def _builddicts(): + import re + + lines = _aglText.splitlines() + + parseAGL_RE = re.compile("([A-Za-z0-9]+);((?:[0-9A-F]{4})(?: (?:[0-9A-F]{4}))*)$") + + for line in lines: + if not line or line[:1] == "#": + continue + m = parseAGL_RE.match(line) + if not m: + raise AGLError("syntax error in glyphlist.txt: %s" % repr(line[:20])) + unicodes = m.group(2) + assert len(unicodes) % 5 == 4 + unicodes = [int(unicode, 16) for unicode in unicodes.split()] + glyphName = tostr(m.group(1)) + LEGACY_AGL2UV[glyphName] = unicodes + + lines = _aglfnText.splitlines() + + parseAGLFN_RE = re.compile("([0-9A-F]{4});([A-Za-z0-9]+);.*?$") + + for line in lines: + if not line or line[:1] == "#": + continue + m = parseAGLFN_RE.match(line) + if not m: + raise AGLError("syntax error in aglfn.txt: %s" % repr(line[:20])) + unicode = m.group(1) + assert len(unicode) == 4 + unicode = int(unicode, 16) + glyphName = tostr(m.group(2)) + AGL2UV[glyphName] = unicode + UV2AGL[unicode] = glyphName + + +_builddicts() + + +def toUnicode(glyph, isZapfDingbats=False): + """Convert glyph names to Unicode, such as ``'longs_t.oldstyle'`` --> ``u'ſt'`` + + If ``isZapfDingbats`` is ``True``, the implementation recognizes additional + glyph names (as required by the AGL specification). + """ + # https://github.com/adobe-type-tools/agl-specification#2-the-mapping + # + # 1. Drop all the characters from the glyph name starting with + # the first occurrence of a period (U+002E; FULL STOP), if any. + glyph = glyph.split(".", 1)[0] + + # 2. Split the remaining string into a sequence of components, + # using underscore (U+005F; LOW LINE) as the delimiter. + components = glyph.split("_") + + # 3. Map each component to a character string according to the + # procedure below, and concatenate those strings; the result + # is the character string to which the glyph name is mapped. + result = [_glyphComponentToUnicode(c, isZapfDingbats) for c in components] + return "".join(result) + + +def _glyphComponentToUnicode(component, isZapfDingbats): + # If the font is Zapf Dingbats (PostScript FontName: ZapfDingbats), + # and the component is in the ITC Zapf Dingbats Glyph List, then + # map it to the corresponding character in that list. + dingbat = _zapfDingbatsToUnicode(component) if isZapfDingbats else None + if dingbat: + return dingbat + + # Otherwise, if the component is in AGL, then map it + # to the corresponding character in that list. + uchars = LEGACY_AGL2UV.get(component) + if uchars: + return "".join(map(chr, uchars)) + + # Otherwise, if the component is of the form "uni" (U+0075, + # U+006E, and U+0069) followed by a sequence of uppercase + # hexadecimal digits (0–9 and A–F, meaning U+0030 through + # U+0039 and U+0041 through U+0046), if the length of that + # sequence is a multiple of four, and if each group of four + # digits represents a value in the ranges 0000 through D7FF + # or E000 through FFFF, then interpret each as a Unicode scalar + # value and map the component to the string made of those + # scalar values. Note that the range and digit-length + # restrictions mean that the "uni" glyph name prefix can be + # used only with UVs in the Basic Multilingual Plane (BMP). + uni = _uniToUnicode(component) + if uni: + return uni + + # Otherwise, if the component is of the form "u" (U+0075) + # followed by a sequence of four to six uppercase hexadecimal + # digits (0–9 and A–F, meaning U+0030 through U+0039 and + # U+0041 through U+0046), and those digits represents a value + # in the ranges 0000 through D7FF or E000 through 10FFFF, then + # interpret it as a Unicode scalar value and map the component + # to the string made of this scalar value. + uni = _uToUnicode(component) + if uni: + return uni + + # Otherwise, map the component to an empty string. + return "" + + +# https://github.com/adobe-type-tools/agl-aglfn/blob/master/zapfdingbats.txt +_AGL_ZAPF_DINGBATS = ( + " ✁✂✄☎✆✝✞✟✠✡☛☞✌✍✎✏✑✒✓✔✕✖✗✘✙✚✛✜✢✣✤✥✦✧★✩✪✫✬✭✮✯✰✱✲✳✴✵✶✷✸✹✺✻✼✽✾✿❀" + "❁❂❃❄❅❆❇❈❉❊❋●❍■❏❑▲▼◆❖ ◗❘❙❚❯❱❲❳❨❩❬❭❪❫❴❵❛❜❝❞❡❢❣❤✐❥❦❧♠♥♦♣ ✉✈✇" + "①②③④⑤⑥⑦⑧⑨⑩❶❷❸❹❺❻❼❽❾❿➀➁➂➃➄➅➆➇➈➉➊➋➌➍➎➏➐➑➒➓➔→➣↔" + "↕➙➛➜➝➞➟➠➡➢➤➥➦➧➨➩➫➭➯➲➳➵➸➺➻➼➽➾➚➪➶➹➘➴➷➬➮➱✃❐❒❮❰" +) + + +def _zapfDingbatsToUnicode(glyph): + """Helper for toUnicode().""" + if len(glyph) < 2 or glyph[0] != "a": + return None + try: + gid = int(glyph[1:]) + except ValueError: + return None + if gid < 0 or gid >= len(_AGL_ZAPF_DINGBATS): + return None + uchar = _AGL_ZAPF_DINGBATS[gid] + return uchar if uchar != " " else None + + +_re_uni = re.compile("^uni([0-9A-F]+)$") + + +def _uniToUnicode(component): + """Helper for toUnicode() to handle "uniABCD" components.""" + match = _re_uni.match(component) + if match is None: + return None + digits = match.group(1) + if len(digits) % 4 != 0: + return None + chars = [int(digits[i : i + 4], 16) for i in range(0, len(digits), 4)] + if any(c >= 0xD800 and c <= 0xDFFF for c in chars): + # The AGL specification explicitly excluded surrogate pairs. + return None + return "".join([chr(c) for c in chars]) + + +_re_u = re.compile("^u([0-9A-F]{4,6})$") + + +def _uToUnicode(component): + """Helper for toUnicode() to handle "u1ABCD" components.""" + match = _re_u.match(component) + if match is None: + return None + digits = match.group(1) + try: + value = int(digits, 16) + except ValueError: + return None + if (value >= 0x0000 and value <= 0xD7FF) or (value >= 0xE000 and value <= 0x10FFFF): + return chr(value) + return None diff --git a/hug/lib/python3.12/site-packages/fontTools/annotations.py b/hug/lib/python3.12/site-packages/fontTools/annotations.py new file mode 100644 index 0000000000000000000000000000000000000000..5ff5972e36cae66d7aaa80d54a5c69ca608b348d --- /dev/null +++ b/hug/lib/python3.12/site-packages/fontTools/annotations.py @@ -0,0 +1,30 @@ +from __future__ import annotations +from typing import TYPE_CHECKING, Iterable, Optional, TypeVar, Union +from collections.abc import Callable, Sequence +from fontTools.misc.filesystem._base import FS +from os import PathLike +from xml.etree.ElementTree import Element as ElementTreeElement + +if TYPE_CHECKING: + from fontTools.ufoLib import UFOFormatVersion + from fontTools.ufoLib.glifLib import GLIFFormatVersion + from lxml.etree import _Element as LxmlElement + + +T = TypeVar("T") # Generic type +K = TypeVar("K") # Generic dict key type +V = TypeVar("V") # Generic dict value type + +GlyphNameToFileNameFunc = Optional[Callable[[str, set[str]], str]] +ElementType = Union[ElementTreeElement, "LxmlElement"] +FormatVersion = Union[int, tuple[int, int]] +FormatVersions = Optional[Iterable[FormatVersion]] +GLIFFormatVersionInput = Optional[Union[int, tuple[int, int], "GLIFFormatVersion"]] +UFOFormatVersionInput = Optional[Union[int, tuple[int, int], "UFOFormatVersion"]] +IntFloat = Union[int, float] +KerningPair = tuple[str, str] +KerningDict = dict[KerningPair, IntFloat] +KerningGroups = dict[str, Sequence[str]] +KerningNested = dict[str, dict[str, IntFloat]] +PathStr = Union[str, PathLike[str]] +PathOrFS = Union[PathStr, FS] diff --git a/hug/lib/python3.12/site-packages/fontTools/cffLib/CFF2ToCFF.py b/hug/lib/python3.12/site-packages/fontTools/cffLib/CFF2ToCFF.py new file mode 100644 index 0000000000000000000000000000000000000000..45ad7a9a6d2048c851fb7e255ab90a8e5987a22e --- /dev/null +++ b/hug/lib/python3.12/site-packages/fontTools/cffLib/CFF2ToCFF.py @@ -0,0 +1,273 @@ +"""CFF2 to CFF converter.""" + +from fontTools.ttLib import TTFont, newTable +from fontTools.misc.cliTools import makeOutputFileName +from fontTools.misc.psCharStrings import T2StackUseExtractor +from fontTools.cffLib import ( + TopDictIndex, + buildOrder, + buildDefaults, + topDictOperators, + privateDictOperators, + FDSelect, +) +from .transforms import desubroutinizeCharString +from .specializer import specializeProgram +from .width import _get_hmtx, optimizeWidths +from collections import defaultdict +import logging + +__all__ = ["convertCFF2ToCFF", "main"] + + +log = logging.getLogger("fontTools.cffLib") + + +def _convertCFF2ToCFF(cff, otFont): + """Converts this object from CFF2 format to CFF format. This conversion + is done 'in-place'. The conversion cannot be reversed. + + The CFF2 font cannot be variable. (TODO Accept those and convert to the + default instance?) + + This assumes a decompiled CFF2 table. (i.e. that the object has been + filled via :meth:`decompile` and e.g. not loaded from XML.)""" + + cff.major = 1 + + topDictData = TopDictIndex(None) + for item in cff.topDictIndex: + # Iterate over, such that all are decompiled + item.cff2GetGlyphOrder = None + topDictData.append(item) + cff.topDictIndex = topDictData + topDict = topDictData[0] + + if hasattr(topDict, "VarStore"): + raise ValueError("Variable CFF2 font cannot be converted to CFF format.") + + opOrder = buildOrder(topDictOperators) + topDict.order = opOrder + for key in topDict.rawDict.keys(): + if key not in opOrder: + del topDict.rawDict[key] + if hasattr(topDict, key): + delattr(topDict, key) + + charStrings = topDict.CharStrings + + fdArray = topDict.FDArray + if not hasattr(topDict, "FDSelect"): + # FDSelect is optional in CFF2, but required in CFF. + fdSelect = topDict.FDSelect = FDSelect() + fdSelect.gidArray = [0] * len(charStrings.charStrings) + + defaults = buildDefaults(privateDictOperators) + order = buildOrder(privateDictOperators) + for fd in fdArray: + # Load the Private dict (and hence its local Subrs) before switching the + # FontDict to CFF, otherwise they would be decompiled as CFF data. + privateDict = fd.Private + fd.setCFF2(False) + privateDict.order = order + for key in order: + if key not in privateDict.rawDict and key in defaults: + privateDict.rawDict[key] = defaults[key] + for key in privateDict.rawDict.keys(): + if key not in order: + del privateDict.rawDict[key] + if hasattr(privateDict, key): + delattr(privateDict, key) + + # Add ending operators + for cs in charStrings.values(): + cs.decompile() + cs.program.append("endchar") + for subrSets in [cff.GlobalSubrs] + [ + getattr(fd.Private, "Subrs", []) for fd in fdArray + ]: + for cs in subrSets: + cs.program.append("return") + + # Add (optimal) width to CharStrings that need it. + widths = defaultdict(list) + metrics = _get_hmtx(otFont).metrics + for glyphName in charStrings.keys(): + cs, fdIndex = charStrings.getItemAndSelector(glyphName) + if fdIndex == None: + fdIndex = 0 + widths[fdIndex].append(metrics[glyphName][0]) + for fdIndex, widthList in widths.items(): + bestDefault, bestNominal = optimizeWidths(widthList) + private = fdArray[fdIndex].Private + private.defaultWidthX = bestDefault + private.nominalWidthX = bestNominal + for glyphName in charStrings.keys(): + cs, fdIndex = charStrings.getItemAndSelector(glyphName) + if fdIndex == None: + fdIndex = 0 + private = fdArray[fdIndex].Private + width = metrics[glyphName][0] + if width != private.defaultWidthX: + cs.program.insert(0, width - private.nominalWidthX) + + # Handle stack use since stack-depth is lower in CFF than in CFF2. + for glyphName in charStrings.keys(): + cs, fdIndex = charStrings.getItemAndSelector(glyphName) + if fdIndex is None: + fdIndex = 0 + private = fdArray[fdIndex].Private + extractor = T2StackUseExtractor( + getattr(private, "Subrs", []), cff.GlobalSubrs, private=private + ) + stackUse = extractor.execute(cs) + if stackUse > 48: # CFF stack depth is 48 + desubroutinizeCharString(cs) + cs.program = specializeProgram(cs.program) + + # Unused subroutines are still in CFF2 (ie. lacking 'return' operator) + # because they were not decompiled when we added the 'return'. + # Moreover, some used subroutines may have become unused after the + # stack-use fixup. So we remove all unused subroutines now. + cff.remove_unused_subroutines() + + if len(fdArray) == 1: + # With a single FontDict there's no need for a CID-keyed font; make a + # name-keyed one instead, which keeps the glyph names and is what most + # client code expects. + topDict.Private = fdArray[0].Private + for key in ("FDArray", "FDSelect"): + topDict.rawDict.pop(key, None) + if hasattr(topDict, key): + delattr(topDict, key) + for attr in ("fdArray", "fdSelect"): + if hasattr(charStrings, attr): + delattr(charStrings, attr) + return + + mapping = { + name: ("cid" + str(n).zfill(5) if n else ".notdef") + for n, name in enumerate(topDict.charset) + } + topDict.charset = [ + "cid" + str(n).zfill(5) if n else ".notdef" for n in range(len(topDict.charset)) + ] + charStrings.charStrings = { + mapping[name]: v for name, v in charStrings.charStrings.items() + } + + topDict.ROS = ("Adobe", "Identity", 0) + + +def convertCFF2ToCFF(font, *, updatePostTable=True): + if "CFF2" not in font: + raise ValueError("Input font does not contain a CFF2 table.") + cff = font["CFF2"].cff + _convertCFF2ToCFF(cff, font) + del font["CFF2"] + table = font["CFF "] = newTable("CFF ") + table.cff = cff + + if updatePostTable and "post" in font: + # Only version supported for fonts with CFF table is 0x00030000 not 0x20000 + post = font["post"] + if post.formatType == 2.0: + post.formatType = 3.0 + + +def main(args=None): + """Convert CFF2 OTF font to CFF OTF font""" + if args is None: + import sys + + args = sys.argv[1:] + + import argparse + + parser = argparse.ArgumentParser( + "fonttools cffLib.CFF2ToCFF", + description="Convert a non-variable CFF2 font to CFF.", + ) + parser.add_argument( + "input", metavar="INPUT.ttf", help="Input OTF file with CFF table." + ) + parser.add_argument( + "-o", + "--output", + metavar="OUTPUT.ttf", + default=None, + help="Output instance OTF file (default: INPUT-CFF2.ttf).", + ) + parser.add_argument( + "--no-recalc-timestamp", + dest="recalc_timestamp", + action="store_false", + help="Don't set the output font's timestamp to the current time.", + ) + parser.add_argument( + "--remove-overlaps", + action="store_true", + help="Merge overlapping contours and components. Requires skia-pathops", + ) + parser.add_argument( + "--ignore-overlap-errors", + action="store_true", + help="Don't crash if the remove-overlaps operation fails for some glyphs.", + ) + loggingGroup = parser.add_mutually_exclusive_group(required=False) + loggingGroup.add_argument( + "-v", "--verbose", action="store_true", help="Run more verbosely." + ) + loggingGroup.add_argument( + "-q", "--quiet", action="store_true", help="Turn verbosity off." + ) + options = parser.parse_args(args) + + from fontTools import configLogger + + configLogger( + level=("DEBUG" if options.verbose else "ERROR" if options.quiet else "INFO") + ) + + import os + + infile = options.input + if not os.path.isfile(infile): + parser.error("No such file '{}'".format(infile)) + + outfile = ( + makeOutputFileName(infile, overWrite=True, suffix="-CFF") + if not options.output + else options.output + ) + + font = TTFont(infile, recalcTimestamp=options.recalc_timestamp, recalcBBoxes=False) + + convertCFF2ToCFF(font) + + if options.remove_overlaps: + from fontTools.ttLib.removeOverlaps import removeOverlaps + from io import BytesIO + + log.debug("Removing overlaps") + + stream = BytesIO() + font.save(stream) + stream.seek(0) + font = TTFont(stream, recalcTimestamp=False, recalcBBoxes=False) + removeOverlaps( + font, + ignoreErrors=options.ignore_overlap_errors, + ) + + log.info( + "Saving %s", + outfile, + ) + font.save(outfile) + + +if __name__ == "__main__": + import sys + + sys.exit(main(sys.argv[1:])) diff --git a/hug/lib/python3.12/site-packages/fontTools/cffLib/CFFToCFF2.py b/hug/lib/python3.12/site-packages/fontTools/cffLib/CFFToCFF2.py new file mode 100644 index 0000000000000000000000000000000000000000..2555f0b242591cde7738f46932fd1cbe2d0a6ccf --- /dev/null +++ b/hug/lib/python3.12/site-packages/fontTools/cffLib/CFFToCFF2.py @@ -0,0 +1,305 @@ +"""CFF to CFF2 converter.""" + +from fontTools.ttLib import TTFont, newTable +from fontTools.misc.cliTools import makeOutputFileName +from fontTools.misc.psCharStrings import T2WidthExtractor +from fontTools.cffLib import ( + TopDictIndex, + FDArrayIndex, + FontDict, + buildOrder, + topDictOperators, + privateDictOperators, + topDictOperators2, + privateDictOperators2, +) +from io import BytesIO +import logging + +__all__ = ["convertCFFToCFF2", "main"] + + +log = logging.getLogger("fontTools.cffLib") + + +class _NominalWidthUsedError(Exception): + def __add__(self, other): + raise self + + def __radd__(self, other): + raise self + + +def _convertCFFToCFF2(cff, otFont): + """Converts this object from CFF format to CFF2 format. This conversion + is done 'in-place'. The conversion cannot be reversed. + + This assumes a decompiled CFF table. (i.e. that the object has been + filled via :meth:`decompile` and e.g. not loaded from XML.)""" + + # Clean up T2CharStrings + + topDict = cff.topDictIndex[0] + fdArray = topDict.FDArray if hasattr(topDict, "FDArray") else None + charStrings = topDict.CharStrings + globalSubrs = cff.GlobalSubrs + localSubrs = ( + [getattr(fd.Private, "Subrs", []) for fd in fdArray] + if fdArray + else ( + [topDict.Private.Subrs] + if hasattr(topDict, "Private") and hasattr(topDict.Private, "Subrs") + else [] + ) + ) + + for glyphName in charStrings.keys(): + cs, fdIndex = charStrings.getItemAndSelector(glyphName) + cs.decompile() + + # Clean up subroutines first + for subrs in [globalSubrs] + localSubrs: + for subr in subrs: + program = subr.program + i = j = len(program) + try: + i = program.index("return") + except ValueError: + pass + try: + j = program.index("endchar") + except ValueError: + pass + program[min(i, j) :] = [] + + # Clean up glyph charstrings + removeUnusedSubrs = False + nominalWidthXError = _NominalWidthUsedError() + for glyphName in charStrings.keys(): + cs, fdIndex = charStrings.getItemAndSelector(glyphName) + program = cs.program + + thisLocalSubrs = ( + localSubrs[fdIndex] + if fdIndex is not None + else ( + getattr(topDict.Private, "Subrs", []) + if hasattr(topDict, "Private") + else [] + ) + ) + + # Intentionally use custom type for nominalWidthX, such that any + # CharString that has an explicit width encoded will throw back to us. + extractor = T2WidthExtractor( + thisLocalSubrs, + globalSubrs, + nominalWidthXError, + 0, + ) + try: + extractor.execute(cs) + except _NominalWidthUsedError: + # Program has explicit width. We want to drop it, but can't + # just pop the first number since it may be a subroutine call. + # Instead, when seeing that, we embed the subroutine and recurse. + # If this ever happened, we later prune unused subroutines. + while len(program) >= 2 and program[1] in ["callsubr", "callgsubr"]: + removeUnusedSubrs = True + subrNumber = program.pop(0) + assert isinstance(subrNumber, int), subrNumber + op = program.pop(0) + bias = extractor.localBias if op == "callsubr" else extractor.globalBias + subrNumber += bias + subrSet = thisLocalSubrs if op == "callsubr" else globalSubrs + subrProgram = subrSet[subrNumber].program + program[:0] = subrProgram + # Now pop the actual width + assert len(program) >= 1, program + program.pop(0) + + if program and program[-1] == "endchar": + program.pop() + + if removeUnusedSubrs: + cff.remove_unused_subroutines() + + # Upconvert TopDict + + cff.major = 2 + cff2GetGlyphOrder = cff.otFont.getGlyphOrder + topDictData = TopDictIndex(None, cff2GetGlyphOrder) + for item in cff.topDictIndex: + # Iterate over, such that all are decompiled + topDictData.append(item) + cff.topDictIndex = topDictData + topDict = topDictData[0] + if hasattr(topDict, "Private"): + privateDict = topDict.Private + else: + privateDict = None + opOrder = buildOrder(topDictOperators2) + topDict.order = opOrder + topDict.cff2GetGlyphOrder = cff2GetGlyphOrder + + if not hasattr(topDict, "FDArray"): + fdArray = topDict.FDArray = FDArrayIndex() + fdArray.strings = None + fdArray.GlobalSubrs = topDict.GlobalSubrs + topDict.GlobalSubrs.fdArray = fdArray + charStrings = topDict.CharStrings + if charStrings.charStringsAreIndexed: + charStrings.charStringsIndex.fdArray = fdArray + else: + charStrings.fdArray = fdArray + fontDict = FontDict() + fontDict.setCFF2(True) + fdArray.append(fontDict) + fontDict.Private = privateDict + privateOpOrder = buildOrder(privateDictOperators2) + if privateDict is not None: + for entry in privateDictOperators: + key = entry[1] + if key not in privateOpOrder: + if key in privateDict.rawDict: + # print "Removing private dict", key + del privateDict.rawDict[key] + if hasattr(privateDict, key): + delattr(privateDict, key) + # print "Removing privateDict attr", key + else: + # clean up the PrivateDicts in the fdArray + fdArray = topDict.FDArray + privateOpOrder = buildOrder(privateDictOperators2) + for fontDict in fdArray: + fontDict.setCFF2(True) + for key in list(fontDict.rawDict.keys()): + if key not in fontDict.order: + del fontDict.rawDict[key] + if hasattr(fontDict, key): + delattr(fontDict, key) + + privateDict = fontDict.Private + for entry in privateDictOperators: + key = entry[1] + if key not in privateOpOrder: + if key in list(privateDict.rawDict.keys()): + # print "Removing private dict", key + del privateDict.rawDict[key] + if hasattr(privateDict, key): + delattr(privateDict, key) + # print "Removing privateDict attr", key + + # Now delete up the deprecated topDict operators from CFF 1.0 + for entry in topDictOperators: + key = entry[1] + # We seem to need to keep the charset operator for now, + # or we fail to compile with some fonts, like AdditionFont.otf. + # I don't know which kind of CFF font those are. But keeping + # charset seems to work. It will be removed when we save and + # read the font again. + # + # AdditionFont.otf has . + if key == "charset": + continue + if key not in opOrder: + if key in topDict.rawDict: + del topDict.rawDict[key] + if hasattr(topDict, key): + delattr(topDict, key) + + # TODO(behdad): What does the following comment even mean? Both CFF and CFF2 + # use the same T2Charstring class. I *think* what it means is that the CharStrings + # were loaded for CFF1, and we need to reload them for CFF2 to set varstore, etc + # on them. At least that's what I understand. It's probably safe to remove this + # and just set vstore where needed. + # + # See comment above about charset as well. + + # At this point, the Subrs and Charstrings are all still T2Charstring class + # easiest to fix this by compiling, then decompiling again + file = BytesIO() + cff.compile(file, otFont, isCFF2=True) + file.seek(0) + cff.decompile(file, otFont, isCFF2=True) + + +def convertCFFToCFF2(font): + cff = font["CFF "].cff + del font["CFF "] + _convertCFFToCFF2(cff, font) + table = font["CFF2"] = newTable("CFF2") + table.cff = cff + + +def main(args=None): + """Convert CFF OTF font to CFF2 OTF font""" + if args is None: + import sys + + args = sys.argv[1:] + + import argparse + + parser = argparse.ArgumentParser( + "fonttools cffLib.CFFToCFF2", + description="Upgrade a CFF font to CFF2.", + ) + parser.add_argument( + "input", metavar="INPUT.ttf", help="Input OTF file with CFF table." + ) + parser.add_argument( + "-o", + "--output", + metavar="OUTPUT.ttf", + default=None, + help="Output instance OTF file (default: INPUT-CFF2.ttf).", + ) + parser.add_argument( + "--no-recalc-timestamp", + dest="recalc_timestamp", + action="store_false", + help="Don't set the output font's timestamp to the current time.", + ) + loggingGroup = parser.add_mutually_exclusive_group(required=False) + loggingGroup.add_argument( + "-v", "--verbose", action="store_true", help="Run more verbosely." + ) + loggingGroup.add_argument( + "-q", "--quiet", action="store_true", help="Turn verbosity off." + ) + options = parser.parse_args(args) + + from fontTools import configLogger + + configLogger( + level=("DEBUG" if options.verbose else "ERROR" if options.quiet else "INFO") + ) + + import os + + infile = options.input + if not os.path.isfile(infile): + parser.error("No such file '{}'".format(infile)) + + outfile = ( + makeOutputFileName(infile, overWrite=True, suffix="-CFF2") + if not options.output + else options.output + ) + + font = TTFont(infile, recalcTimestamp=options.recalc_timestamp, recalcBBoxes=False) + + convertCFFToCFF2(font) + + log.info( + "Saving %s", + outfile, + ) + font.save(outfile) + + +if __name__ == "__main__": + import sys + + sys.exit(main(sys.argv[1:])) diff --git a/hug/lib/python3.12/site-packages/fontTools/cffLib/__init__.py b/hug/lib/python3.12/site-packages/fontTools/cffLib/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a1e1ece1a2db8046720e3d1c48ba7db0d2fddf50 --- /dev/null +++ b/hug/lib/python3.12/site-packages/fontTools/cffLib/__init__.py @@ -0,0 +1,3729 @@ +"""cffLib: read/write Adobe CFF fonts + +OpenType fonts with PostScript outlines embed a completely independent +font file in Adobe's *Compact Font Format*. So dealing with OpenType fonts +requires also dealing with CFF. This module allows you to read and write +fonts written in the CFF format. + +In 2016, OpenType 1.8 introduced the `CFF2 `_ +format which, along with other changes, extended the CFF format to deal with +the demands of variable fonts. This module parses both original CFF and CFF2. + +""" + +from fontTools.misc import sstruct +from fontTools.misc import psCharStrings +from fontTools.misc.arrayTools import unionRect, intRect +from fontTools.misc.textTools import ( + bytechr, + byteord, + bytesjoin, + tobytes, + tostr, + safeEval, +) +from fontTools.ttLib import TTFont +from fontTools.ttLib.tables.otBase import OTTableWriter +from fontTools.ttLib.tables.otBase import OTTableReader +from fontTools.ttLib.tables import otTables as ot +from io import BytesIO +import struct +import logging +import re + +# mute cffLib debug messages when running ttx in verbose mode +DEBUG = logging.DEBUG - 1 +log = logging.getLogger(__name__) + +cffHeaderFormat = """ + major: B + minor: B + hdrSize: B +""" + +maxStackLimit = 513 +# maxstack operator has been deprecated. max stack is now always 513. + + +class CFFFontSet(object): + """A CFF font "file" can contain more than one font, although this is + extremely rare (and not allowed within OpenType fonts). + + This class is the entry point for parsing a CFF table. To actually + manipulate the data inside the CFF font, you will want to access the + ``CFFFontSet``'s :class:`TopDict` object. To do this, a ``CFFFontSet`` + object can either be treated as a dictionary (with appropriate + ``keys()`` and ``values()`` methods) mapping font names to :class:`TopDict` + objects, or as a list. + + .. code:: python + + from fontTools import ttLib + tt = ttLib.TTFont("Tests/cffLib/data/LinLibertine_RBI.otf") + tt["CFF "].cff + # + tt["CFF "].cff[0] # Here's your actual font data + # + + """ + + def decompile(self, file, otFont, isCFF2=None): + """Parse a binary CFF file into an internal representation. ``file`` + should be a file handle object. ``otFont`` is the top-level + :py:class:`fontTools.ttLib.ttFont.TTFont` object containing this CFF file. + + If ``isCFF2`` is passed and set to ``True`` or ``False``, then the + library makes an assertion that the CFF header is of the appropriate + version. + """ + + self.otFont = otFont + sstruct.unpack(cffHeaderFormat, file.read(3), self) + if isCFF2 is not None: + # called from ttLib: assert 'major' as read from file matches the + # expected version + expected_major = 2 if isCFF2 else 1 + if self.major != expected_major: + raise ValueError( + "Invalid CFF 'major' version: expected %d, found %d" + % (expected_major, self.major) + ) + else: + # use 'major' version from file to determine if isCFF2 + assert self.major in (1, 2), "Unknown CFF format" + isCFF2 = self.major == 2 + if not isCFF2: + self.offSize = struct.unpack("B", file.read(1))[0] + file.seek(self.hdrSize) + self.fontNames = list( + tostr(s, encoding="latin1") for s in Index(file, isCFF2=isCFF2) + ) + self.topDictIndex = TopDictIndex(file, isCFF2=isCFF2) + self.strings = IndexedStrings(file) + else: # isCFF2 + self.topDictSize = struct.unpack(">H", file.read(2))[0] + file.seek(self.hdrSize) + self.fontNames = ["CFF2Font"] + cff2GetGlyphOrder = otFont.getGlyphOrder + # in CFF2, offsetSize is the size of the TopDict data. + self.topDictIndex = TopDictIndex( + file, cff2GetGlyphOrder, self.topDictSize, isCFF2=isCFF2 + ) + self.strings = None + self.GlobalSubrs = GlobalSubrsIndex(file, isCFF2=isCFF2) + self.topDictIndex.strings = self.strings + self.topDictIndex.GlobalSubrs = self.GlobalSubrs + + def __len__(self): + return len(self.fontNames) + + def keys(self): + return list(self.fontNames) + + def values(self): + return self.topDictIndex + + def __getitem__(self, nameOrIndex): + """Return TopDict instance identified by name (str) or index (int + or any object that implements `__index__`). + """ + if hasattr(nameOrIndex, "__index__"): + index = nameOrIndex.__index__() + elif isinstance(nameOrIndex, str): + name = nameOrIndex + try: + index = self.fontNames.index(name) + except ValueError: + raise KeyError(nameOrIndex) + else: + raise TypeError(nameOrIndex) + return self.topDictIndex[index] + + def compile(self, file, otFont, isCFF2=None): + """Write the object back into binary representation onto the given file. + ``file`` should be a file handle object. ``otFont`` is the top-level + :py:class:`fontTools.ttLib.ttFont.TTFont` object containing this CFF file. + + If ``isCFF2`` is passed and set to ``True`` or ``False``, then the + library makes an assertion that the CFF header is of the appropriate + version. + """ + self.otFont = otFont + if isCFF2 is not None: + # called from ttLib: assert 'major' value matches expected version + expected_major = 2 if isCFF2 else 1 + if self.major != expected_major: + raise ValueError( + "Invalid CFF 'major' version: expected %d, found %d" + % (expected_major, self.major) + ) + else: + # use current 'major' value to determine output format + assert self.major in (1, 2), "Unknown CFF format" + isCFF2 = self.major == 2 + + if otFont.recalcBBoxes and not isCFF2: + for topDict in self.topDictIndex: + topDict.recalcFontBBox() + + if not isCFF2: + strings = IndexedStrings() + else: + strings = None + writer = CFFWriter(isCFF2) + topCompiler = self.topDictIndex.getCompiler(strings, self, isCFF2=isCFF2) + if isCFF2: + self.hdrSize = 5 + writer.add(sstruct.pack(cffHeaderFormat, self)) + # Note: topDictSize will most likely change in CFFWriter.toFile(). + self.topDictSize = topCompiler.getDataLength() + writer.add(struct.pack(">H", self.topDictSize)) + else: + self.hdrSize = 4 + self.offSize = 4 # will most likely change in CFFWriter.toFile(). + writer.add(sstruct.pack(cffHeaderFormat, self)) + writer.add(struct.pack("B", self.offSize)) + if not isCFF2: + fontNames = Index() + for name in self.fontNames: + fontNames.append(name) + writer.add(fontNames.getCompiler(strings, self, isCFF2=isCFF2)) + writer.add(topCompiler) + if not isCFF2: + writer.add(strings.getCompiler()) + writer.add(self.GlobalSubrs.getCompiler(strings, self, isCFF2=isCFF2)) + + for topDict in self.topDictIndex: + if isCFF2: + glyphOrder = otFont.getGlyphOrder() + count = len(topDict.CharStrings) + if count > len(glyphOrder) or ( + count != len(glyphOrder) and "VARC" not in otFont + ): + raise ValueError("CFF2 CharStrings count does not match the font") + topDict.charset = glyphOrder[:count] + if set(topDict.charset) != set(topDict.CharStrings.keys()): + raise ValueError("CFF2 CharStrings must form a glyph-order prefix") + elif not hasattr(topDict, "charset") or topDict.charset is None: + charset = otFont.getGlyphOrder() + topDict.charset = charset + children = topCompiler.getChildren(strings) + for child in children: + writer.add(child) + + writer.toFile(file) + + def toXML(self, xmlWriter): + """Write the object into XML representation onto the given + :class:`fontTools.misc.xmlWriter.XMLWriter`. + + .. code:: python + + writer = xmlWriter.XMLWriter(sys.stdout) + tt["CFF "].cff.toXML(writer) + + """ + + xmlWriter.simpletag("major", value=self.major) + xmlWriter.newline() + xmlWriter.simpletag("minor", value=self.minor) + xmlWriter.newline() + for fontName in self.fontNames: + xmlWriter.begintag("CFFFont", name=tostr(fontName)) + xmlWriter.newline() + font = self[fontName] + font.toXML(xmlWriter) + xmlWriter.endtag("CFFFont") + xmlWriter.newline() + xmlWriter.newline() + xmlWriter.begintag("GlobalSubrs") + xmlWriter.newline() + self.GlobalSubrs.toXML(xmlWriter) + xmlWriter.endtag("GlobalSubrs") + xmlWriter.newline() + + def fromXML(self, name, attrs, content, otFont=None): + """Reads data from the XML element into the ``CFFFontSet`` object.""" + self.otFont = otFont + + # set defaults. These will be replaced if there are entries for them + # in the XML file. + if not hasattr(self, "major"): + self.major = 1 + if not hasattr(self, "minor"): + self.minor = 0 + + if name == "CFFFont": + if self.major == 1: + if not hasattr(self, "offSize"): + # this will be recalculated when the cff is compiled. + self.offSize = 4 + if not hasattr(self, "hdrSize"): + self.hdrSize = 4 + if not hasattr(self, "GlobalSubrs"): + self.GlobalSubrs = GlobalSubrsIndex() + if not hasattr(self, "fontNames"): + self.fontNames = [] + self.topDictIndex = TopDictIndex() + fontName = attrs["name"] + self.fontNames.append(fontName) + topDict = TopDict(GlobalSubrs=self.GlobalSubrs) + topDict.charset = None # gets filled in later + elif self.major == 2: + if not hasattr(self, "hdrSize"): + self.hdrSize = 5 + if not hasattr(self, "GlobalSubrs"): + self.GlobalSubrs = GlobalSubrsIndex() + if not hasattr(self, "fontNames"): + self.fontNames = ["CFF2Font"] + cff2GetGlyphOrder = self.otFont.getGlyphOrder + topDict = TopDict( + GlobalSubrs=self.GlobalSubrs, cff2GetGlyphOrder=cff2GetGlyphOrder + ) + self.topDictIndex = TopDictIndex(None, cff2GetGlyphOrder) + self.topDictIndex.append(topDict) + for element in content: + if isinstance(element, str): + continue + name, attrs, content = element + topDict.fromXML(name, attrs, content) + + if hasattr(topDict, "VarStore") and topDict.FDArray[0].vstore is None: + fdArray = topDict.FDArray + for fontDict in fdArray: + if hasattr(fontDict, "Private"): + fontDict.Private.vstore = topDict.VarStore + + elif name == "GlobalSubrs": + subrCharStringClass = psCharStrings.T2CharString + if not hasattr(self, "GlobalSubrs"): + self.GlobalSubrs = GlobalSubrsIndex() + for element in content: + if isinstance(element, str): + continue + name, attrs, content = element + subr = subrCharStringClass() + subr.fromXML(name, attrs, content) + self.GlobalSubrs.append(subr) + elif name == "major": + self.major = int(attrs["value"]) + elif name == "minor": + self.minor = int(attrs["value"]) + + def convertCFFToCFF2(self, otFont): + from .CFFToCFF2 import _convertCFFToCFF2 + + _convertCFFToCFF2(self, otFont) + + def convertCFF2ToCFF(self, otFont): + from .CFF2ToCFF import _convertCFF2ToCFF + + _convertCFF2ToCFF(self, otFont) + + def desubroutinize(self): + from .transforms import desubroutinize + + desubroutinize(self) + + def remove_hints(self): + from .transforms import remove_hints + + remove_hints(self) + + def remove_unused_subroutines(self): + from .transforms import remove_unused_subroutines + + remove_unused_subroutines(self) + + +class CFFWriter(object): + """Helper class for serializing CFF data to binary. Used by + :meth:`CFFFontSet.compile`.""" + + def __init__(self, isCFF2): + self.data = [] + self.isCFF2 = isCFF2 + + def add(self, table): + self.data.append(table) + + def toFile(self, file): + lastPosList = None + count = 1 + while True: + log.log(DEBUG, "CFFWriter.toFile() iteration: %d", count) + count = count + 1 + pos = 0 + posList = [pos] + for item in self.data: + if hasattr(item, "getDataLength"): + endPos = pos + item.getDataLength() + if isinstance(item, TopDictIndexCompiler) and item.isCFF2: + self.topDictSize = item.getDataLength() + else: + endPos = pos + len(item) + if hasattr(item, "setPos"): + item.setPos(pos, endPos) + pos = endPos + posList.append(pos) + if posList == lastPosList: + break + lastPosList = posList + log.log(DEBUG, "CFFWriter.toFile() writing to file.") + begin = file.tell() + if self.isCFF2: + self.data[1] = struct.pack(">H", self.topDictSize) + else: + self.offSize = calcOffSize(lastPosList[-1]) + self.data[1] = struct.pack("B", self.offSize) + posList = [0] + for item in self.data: + if hasattr(item, "toFile"): + item.toFile(file) + else: + file.write(item) + posList.append(file.tell() - begin) + assert posList == lastPosList + + +def calcOffSize(largestOffset): + if largestOffset < 0x100: + offSize = 1 + elif largestOffset < 0x10000: + offSize = 2 + elif largestOffset < 0x1000000: + offSize = 3 + else: + offSize = 4 + return offSize + + +class IndexCompiler(object): + """Base class for writing CFF `INDEX data `_ + to binary.""" + + def __init__(self, items, strings, parent, isCFF2=None): + if isCFF2 is None and hasattr(parent, "isCFF2"): + isCFF2 = parent.isCFF2 + assert isCFF2 is not None + self.isCFF2 = isCFF2 + self.items = self.getItems(items, strings) + self.parent = parent + + def getItems(self, items, strings): + return items + + def getOffsets(self): + # An empty INDEX contains only the count field. + if self.items: + pos = 1 + offsets = [pos] + for item in self.items: + if hasattr(item, "getDataLength"): + pos = pos + item.getDataLength() + else: + pos = pos + len(item) + offsets.append(pos) + else: + offsets = [] + return offsets + + def getDataLength(self): + if self.isCFF2: + countSize = 4 + else: + countSize = 2 + + if self.items: + lastOffset = self.getOffsets()[-1] + offSize = calcOffSize(lastOffset) + dataLength = ( + countSize + + 1 # count + + (len(self.items) + 1) * offSize # offSize + + lastOffset # the offsets + - 1 # size of object data + ) + else: + # count. For empty INDEX tables, this is the only entry. + dataLength = countSize + + return dataLength + + def toFile(self, file): + offsets = self.getOffsets() + if self.isCFF2: + writeCard32(file, len(self.items)) + else: + writeCard16(file, len(self.items)) + # An empty INDEX contains only the count field. + if self.items: + offSize = calcOffSize(offsets[-1]) + writeCard8(file, offSize) + offSize = -offSize + pack = struct.pack + for offset in offsets: + binOffset = pack(">l", offset)[offSize:] + assert len(binOffset) == -offSize + file.write(binOffset) + for item in self.items: + if hasattr(item, "toFile"): + item.toFile(file) + else: + data = tobytes(item, encoding="latin1") + file.write(data) + + +class IndexedStringsCompiler(IndexCompiler): + def getItems(self, items, strings): + return items.strings + + +class TopDictIndexCompiler(IndexCompiler): + """Helper class for writing the TopDict to binary.""" + + def getItems(self, items, strings): + out = [] + for item in items: + out.append(item.getCompiler(strings, self)) + return out + + def getChildren(self, strings): + children = [] + for topDict in self.items: + children.extend(topDict.getChildren(strings)) + return children + + def getOffsets(self): + if self.isCFF2: + offsets = [0, self.items[0].getDataLength()] + return offsets + else: + return super(TopDictIndexCompiler, self).getOffsets() + + def getDataLength(self): + if self.isCFF2: + dataLength = self.items[0].getDataLength() + return dataLength + else: + return super(TopDictIndexCompiler, self).getDataLength() + + def toFile(self, file): + if self.isCFF2: + self.items[0].toFile(file) + else: + super(TopDictIndexCompiler, self).toFile(file) + + +class FDArrayIndexCompiler(IndexCompiler): + """Helper class for writing the + `Font DICT INDEX `_ + to binary.""" + + def getItems(self, items, strings): + out = [] + for item in items: + out.append(item.getCompiler(strings, self)) + return out + + def getChildren(self, strings): + children = [] + for fontDict in self.items: + children.extend(fontDict.getChildren(strings)) + return children + + def toFile(self, file): + offsets = self.getOffsets() + if self.isCFF2: + writeCard32(file, len(self.items)) + else: + writeCard16(file, len(self.items)) + offSize = calcOffSize(offsets[-1]) + writeCard8(file, offSize) + offSize = -offSize + pack = struct.pack + for offset in offsets: + binOffset = pack(">l", offset)[offSize:] + assert len(binOffset) == -offSize + file.write(binOffset) + for item in self.items: + if hasattr(item, "toFile"): + item.toFile(file) + else: + file.write(item) + + def setPos(self, pos, endPos): + self.parent.rawDict["FDArray"] = pos + + +class GlobalSubrsCompiler(IndexCompiler): + """Helper class for writing the `global subroutine INDEX `_ + to binary.""" + + def getItems(self, items, strings): + out = [] + for cs in items: + cs.compile(self.isCFF2) + out.append(cs.bytecode) + return out + + +class SubrsCompiler(GlobalSubrsCompiler): + """Helper class for writing the `local subroutine INDEX `_ + to binary.""" + + def setPos(self, pos, endPos): + offset = pos - self.parent.pos + self.parent.rawDict["Subrs"] = offset + + +class CharStringsCompiler(GlobalSubrsCompiler): + """Helper class for writing the `CharStrings INDEX `_ + to binary.""" + + def getItems(self, items, strings): + out = [] + for cs in items: + cs.compile(self.isCFF2) + out.append(cs.bytecode) + return out + + def setPos(self, pos, endPos): + self.parent.rawDict["CharStrings"] = pos + + +class Index(object): + """This class represents what the CFF spec calls an INDEX (an array of + variable-sized objects). `Index` items can be addressed and set using + Python list indexing.""" + + compilerClass = IndexCompiler + + def __init__(self, file=None, isCFF2=None): + self.items = [] + self.offsets = offsets = [] + name = self.__class__.__name__ + if file is None: + return + self._isCFF2 = isCFF2 + log.log(DEBUG, "loading %s at %s", name, file.tell()) + self.file = file + if isCFF2: + count = readCard32(file) + else: + count = readCard16(file) + if count == 0: + return + self.items = [None] * count + offSize = readCard8(file) + log.log(DEBUG, " index count: %s offSize: %s", count, offSize) + assert offSize <= 4, "offSize too large: %s" % offSize + pad = b"\0" * (4 - offSize) + for index in range(count + 1): + chunk = file.read(offSize) + chunk = pad + chunk + (offset,) = struct.unpack(">L", chunk) + offsets.append(int(offset)) + self.offsetBase = file.tell() - 1 + file.seek(self.offsetBase + offsets[-1]) # pretend we've read the whole lot + log.log(DEBUG, " end of %s at %s", name, file.tell()) + + def __len__(self): + return len(self.items) + + def __getitem__(self, index): + item = self.items[index] + if item is not None: + return item + offset = self.offsets[index] + self.offsetBase + size = self.offsets[index + 1] - self.offsets[index] + file = self.file + file.seek(offset) + data = file.read(size) + assert len(data) == size + item = self.produceItem(index, data, file, offset) + self.items[index] = item + return item + + def __setitem__(self, index, item): + self.items[index] = item + + def produceItem(self, index, data, file, offset): + return data + + def append(self, item): + """Add an item to an INDEX.""" + self.items.append(item) + + def getCompiler(self, strings, parent, isCFF2=None): + return self.compilerClass(self, strings, parent, isCFF2=isCFF2) + + def clear(self): + """Empty the INDEX.""" + del self.items[:] + + +class GlobalSubrsIndex(Index): + """This index contains all the global subroutines in the font. A global + subroutine is a set of ``CharString`` data which is accessible to any + glyph in the font, and are used to store repeated instructions - for + example, components may be encoded as global subroutines, but so could + hinting instructions. + + Remember that when interpreting a ``callgsubr`` instruction (or indeed + a ``callsubr`` instruction) that you will need to add the "subroutine + number bias" to number given: + + .. code:: python + + tt = ttLib.TTFont("Almendra-Bold.otf") + u = tt["CFF "].cff[0].CharStrings["udieresis"] + u.decompile() + + u.toXML(XMLWriter(sys.stdout)) + # + # -64 callgsubr <-- Subroutine which implements the dieresis mark + # + + tt["CFF "].cff[0].GlobalSubrs[-64] # <-- WRONG + # + + tt["CFF "].cff[0].GlobalSubrs[-64 + 107] # <-- RIGHT + # + + ("The bias applied depends on the number of subrs (gsubrs). If the number of + subrs (gsubrs) is less than 1240, the bias is 107. Otherwise if it is less + than 33900, it is 1131; otherwise it is 32768.", + `Subroutine Operators `) + """ + + compilerClass = GlobalSubrsCompiler + subrClass = psCharStrings.T2CharString + charStringClass = psCharStrings.T2CharString + + def __init__( + self, + file=None, + globalSubrs=None, + private=None, + fdSelect=None, + fdArray=None, + isCFF2=None, + ): + super(GlobalSubrsIndex, self).__init__(file, isCFF2=isCFF2) + self.globalSubrs = globalSubrs + self.private = private + if fdSelect: + self.fdSelect = fdSelect + if fdArray: + self.fdArray = fdArray + + def produceItem(self, index, data, file, offset): + if self.private is not None: + private = self.private + elif hasattr(self, "fdArray") and self.fdArray is not None: + if hasattr(self, "fdSelect") and self.fdSelect is not None: + fdIndex = self.fdSelect[index] + else: + fdIndex = 0 + private = self.fdArray[fdIndex].Private + else: + private = None + return self.subrClass(data, private=private, globalSubrs=self.globalSubrs) + + def toXML(self, xmlWriter): + """Write the subroutines index into XML representation onto the given + :class:`fontTools.misc.xmlWriter.XMLWriter`. + + .. code:: python + + writer = xmlWriter.XMLWriter(sys.stdout) + tt["CFF "].cff[0].GlobalSubrs.toXML(writer) + + """ + xmlWriter.comment( + "The 'index' attribute is only for humans; " "it is ignored when parsed." + ) + xmlWriter.newline() + for i in range(len(self)): + subr = self[i] + if subr.needsDecompilation(): + xmlWriter.begintag("CharString", index=i, raw=1) + else: + xmlWriter.begintag("CharString", index=i) + xmlWriter.newline() + subr.toXML(xmlWriter) + xmlWriter.endtag("CharString") + xmlWriter.newline() + + def fromXML(self, name, attrs, content): + if name != "CharString": + return + subr = self.subrClass() + subr.fromXML(name, attrs, content) + self.append(subr) + + def getItemAndSelector(self, index): + sel = None + if hasattr(self, "fdSelect"): + sel = self.fdSelect[index] + return self[index], sel + + +class SubrsIndex(GlobalSubrsIndex): + """This index contains a glyph's local subroutines. A local subroutine is a + private set of ``CharString`` data which is accessible only to the glyph to + which the index is attached.""" + + compilerClass = SubrsCompiler + + +class TopDictIndex(Index): + """This index represents the array of ``TopDict`` structures in the font + (again, usually only one entry is present). Hence the following calls are + equivalent: + + .. code:: python + + tt["CFF "].cff[0] + # + tt["CFF "].cff.topDictIndex[0] + # + + """ + + compilerClass = TopDictIndexCompiler + + def __init__(self, file=None, cff2GetGlyphOrder=None, topSize=0, isCFF2=None): + self.cff2GetGlyphOrder = cff2GetGlyphOrder + if file is not None and isCFF2: + self._isCFF2 = isCFF2 + self.items = [] + name = self.__class__.__name__ + log.log(DEBUG, "loading %s at %s", name, file.tell()) + self.file = file + count = 1 + self.items = [None] * count + self.offsets = [0, topSize] + self.offsetBase = file.tell() + # pretend we've read the whole lot + file.seek(self.offsetBase + topSize) + log.log(DEBUG, " end of %s at %s", name, file.tell()) + else: + super(TopDictIndex, self).__init__(file, isCFF2=isCFF2) + + def produceItem(self, index, data, file, offset): + top = TopDict( + self.strings, + file, + offset, + self.GlobalSubrs, + self.cff2GetGlyphOrder, + isCFF2=self._isCFF2, + ) + top.decompile(data) + return top + + def toXML(self, xmlWriter): + for i in range(len(self)): + xmlWriter.begintag("FontDict", index=i) + xmlWriter.newline() + self[i].toXML(xmlWriter) + xmlWriter.endtag("FontDict") + xmlWriter.newline() + + +class FDArrayIndex(Index): + compilerClass = FDArrayIndexCompiler + + def toXML(self, xmlWriter): + for i in range(len(self)): + xmlWriter.begintag("FontDict", index=i) + xmlWriter.newline() + self[i].toXML(xmlWriter) + xmlWriter.endtag("FontDict") + xmlWriter.newline() + + def produceItem(self, index, data, file, offset): + fontDict = FontDict( + self.strings, + file, + offset, + self.GlobalSubrs, + isCFF2=self._isCFF2, + vstore=self.vstore, + ) + fontDict.decompile(data) + return fontDict + + def fromXML(self, name, attrs, content): + if name != "FontDict": + return + fontDict = FontDict() + for element in content: + if isinstance(element, str): + continue + name, attrs, content = element + fontDict.fromXML(name, attrs, content) + self.append(fontDict) + + +class VarStoreData(object): + def __init__(self, file=None, otVarStore=None): + self.file = file + self.data = None + self.otVarStore = otVarStore + self.font = TTFont() # dummy font for the decompile function. + + def decompile(self): + if self.file: + # read data in from file. Assume position is correct. + length = readCard16(self.file) + # https://github.com/fonttools/fonttools/issues/3673 + if length == 65535: + self.data = self.file.read() + else: + self.data = self.file.read(length) + globalState = {} + reader = OTTableReader(self.data, globalState) + self.otVarStore = ot.VarStore() + self.otVarStore.decompile(reader, self.font) + self.data = None + return self + + def compile(self): + writer = OTTableWriter() + self.otVarStore.compile(writer, self.font) + # Note that this omits the initial Card16 length from the CFF2 + # VarStore data block + self.data = writer.getAllData() + + def writeXML(self, xmlWriter, name): + self.otVarStore.toXML(xmlWriter, self.font) + + def xmlRead(self, name, attrs, content, parent): + self.otVarStore = ot.VarStore() + for element in content: + if isinstance(element, tuple): + name, attrs, content = element + self.otVarStore.fromXML(name, attrs, content, self.font) + else: + pass + return None + + def __len__(self): + return len(self.data) + + def getNumRegions(self, vsIndex): + if vsIndex is None: + vsIndex = 0 + varData = self.otVarStore.VarData[vsIndex] + numRegions = varData.VarRegionCount + return numRegions + + +class FDSelect(object): + def __init__(self, file=None, numGlyphs=None, format=None, maxGlyphs=None): + if file: + # read data in from file + self.format = readCard8(file) + if self.format == 0: + from array import array + + self.gidArray = array("B", file.read(numGlyphs)).tolist() + elif self.format in (3, 4): + readGlyph = readCard16 if self.format == 3 else readCard32 + readFD = readCard8 if self.format == 3 else readCard16 + nRanges = readGlyph(file) + gidArray = [] + fd = None + prev = 0 + maximum = numGlyphs if maxGlyphs is None else maxGlyphs + for i in range(nRanges): + first = readGlyph(file) + if (i == 0 and first != 0) or first < prev or first > maximum: + raise ValueError("Invalid FDSelect range") + gidArray.extend([fd] * (first - prev)) + prev = first + fd = readFD(file) + sentinel = readGlyph(file) + if ( + not nRanges + or not numGlyphs <= sentinel <= maximum + or sentinel <= prev + ): + raise ValueError("Invalid FDSelect sentinel") + gidArray.extend([fd] * (sentinel - prev)) + self.gidArray = gidArray + else: + assert False, "unsupported FDSelect format: %s" % format + else: + # reading from XML. Make empty gidArray, and leave format as passed in. + # format is None will result in the smallest representation being used. + self.format = format + self.gidArray = [] + + def __len__(self): + return len(self.gidArray) + + def __getitem__(self, index): + return self.gidArray[index] + + def __setitem__(self, index, fdSelectValue): + self.gidArray[index] = fdSelectValue + + def append(self, fdSelectValue): + self.gidArray.append(fdSelectValue) + + +class CharStrings(object): + """The ``CharStrings`` in the font represent the instructions for drawing + each glyph. This object presents a dictionary interface to the font's + CharStrings, indexed by glyph name: + + .. code:: python + + tt["CFF "].cff[0].CharStrings["a"] + # + + See :class:`fontTools.misc.psCharStrings.T1CharString` and + :class:`fontTools.misc.psCharStrings.T2CharString` for how to decompile, + compile and interpret the glyph drawing instructions in the returned objects. + + """ + + def __init__( + self, + file, + charset, + globalSubrs, + private, + fdSelect, + fdArray, + isCFF2=None, + varStore=None, + ): + self.globalSubrs = globalSubrs + self.varStore = varStore + if file is not None: + self.charStringsIndex = SubrsIndex( + file, globalSubrs, private, fdSelect, fdArray, isCFF2=isCFF2 + ) + self.charStrings = charStrings = {} + for i in range(len(charset)): + charStrings[charset[i]] = i + # read from OTF file: charStrings.values() are indices into + # charStringsIndex. + self.charStringsAreIndexed = 1 + else: + self.charStrings = {} + # read from ttx file: charStrings.values() are actual charstrings + self.charStringsAreIndexed = 0 + self.private = private + if fdSelect is not None: + self.fdSelect = fdSelect + if fdArray is not None: + self.fdArray = fdArray + + def keys(self): + return list(self.charStrings.keys()) + + def values(self): + if self.charStringsAreIndexed: + return self.charStringsIndex + else: + return list(self.charStrings.values()) + + def has_key(self, name): + return name in self.charStrings + + __contains__ = has_key + + def __len__(self): + return len(self.charStrings) + + def __getitem__(self, name): + charString = self.charStrings[name] + if self.charStringsAreIndexed: + charString = self.charStringsIndex[charString] + return charString + + def __setitem__(self, name, charString): + if self.charStringsAreIndexed: + index = self.charStrings[name] + self.charStringsIndex[index] = charString + else: + self.charStrings[name] = charString + + def getItemAndSelector(self, name): + if self.charStringsAreIndexed: + index = self.charStrings[name] + return self.charStringsIndex.getItemAndSelector(index) + else: + if hasattr(self, "fdArray"): + if hasattr(self, "fdSelect"): + sel = self.charStrings[name].fdSelectIndex + else: + sel = 0 + else: + sel = None + return self.charStrings[name], sel + + def toXML(self, xmlWriter): + names = sorted(self.keys()) + for name in names: + charStr, fdSelectIndex = self.getItemAndSelector(name) + if charStr.needsDecompilation(): + raw = [("raw", 1)] + else: + raw = [] + if fdSelectIndex is None: + xmlWriter.begintag("CharString", [("name", name)] + raw) + else: + xmlWriter.begintag( + "CharString", + [("name", name), ("fdSelectIndex", fdSelectIndex)] + raw, + ) + xmlWriter.newline() + charStr.toXML(xmlWriter) + xmlWriter.endtag("CharString") + xmlWriter.newline() + + def fromXML(self, name, attrs, content): + for element in content: + if isinstance(element, str): + continue + name, attrs, content = element + if name != "CharString": + continue + fdID = -1 + if hasattr(self, "fdArray"): + try: + fdID = safeEval(attrs["fdSelectIndex"]) + except KeyError: + fdID = 0 + private = self.fdArray[fdID].Private + else: + private = self.private + + glyphName = attrs["name"] + charStringClass = psCharStrings.T2CharString + charString = charStringClass(private=private, globalSubrs=self.globalSubrs) + charString.fromXML(name, attrs, content) + if fdID >= 0: + charString.fdSelectIndex = fdID + self[glyphName] = charString + + +def readCard8(file): + return byteord(file.read(1)) + + +def readCard16(file): + (value,) = struct.unpack(">H", file.read(2)) + return value + + +def readCard32(file): + (value,) = struct.unpack(">L", file.read(4)) + return value + + +def writeCard8(file, value): + file.write(bytechr(value)) + + +def writeCard16(file, value): + file.write(struct.pack(">H", value)) + + +def writeCard32(file, value): + file.write(struct.pack(">L", value)) + + +def packCard8(value): + return bytechr(value) + + +def packCard16(value): + return struct.pack(">H", value) + + +def packCard32(value): + return struct.pack(">L", value) + + +def buildOperatorDict(table): + d = {} + for op, name, arg, default, conv in table: + d[op] = (name, arg) + return d + + +def buildOpcodeDict(table): + d = {} + for op, name, arg, default, conv in table: + if isinstance(op, tuple): + op = bytechr(op[0]) + bytechr(op[1]) + else: + op = bytechr(op) + d[name] = (op, arg) + return d + + +def buildOrder(table): + l = [] + for op, name, arg, default, conv in table: + l.append(name) + return l + + +def buildDefaults(table): + d = {} + for op, name, arg, default, conv in table: + if default is not None: + d[name] = default + return d + + +def buildConverters(table): + d = {} + for op, name, arg, default, conv in table: + d[name] = conv + return d + + +class SimpleConverter(object): + def read(self, parent, value): + if not hasattr(parent, "file"): + return self._read(parent, value) + file = parent.file + pos = file.tell() + try: + return self._read(parent, value) + finally: + file.seek(pos) + + def _read(self, parent, value): + return value + + def write(self, parent, value): + return value + + def xmlWrite(self, xmlWriter, name, value): + xmlWriter.simpletag(name, value=value) + xmlWriter.newline() + + def xmlRead(self, name, attrs, content, parent): + return attrs["value"] + + +class ASCIIConverter(SimpleConverter): + def _read(self, parent, value): + return tostr(value, encoding="ascii") + + def write(self, parent, value): + return tobytes(value, encoding="ascii") + + def xmlWrite(self, xmlWriter, name, value): + xmlWriter.simpletag(name, value=tostr(value, encoding="ascii")) + xmlWriter.newline() + + def xmlRead(self, name, attrs, content, parent): + return tobytes(attrs["value"], encoding=("ascii")) + + +class Latin1Converter(SimpleConverter): + def _read(self, parent, value): + return tostr(value, encoding="latin1") + + def write(self, parent, value): + return tobytes(value, encoding="latin1") + + def xmlWrite(self, xmlWriter, name, value): + value = tostr(value, encoding="latin1") + if name in ["Notice", "Copyright"]: + value = re.sub(r"[\r\n]\s+", " ", value) + xmlWriter.simpletag(name, value=value) + xmlWriter.newline() + + def xmlRead(self, name, attrs, content, parent): + return tobytes(attrs["value"], encoding=("latin1")) + + +def parseNum(s): + try: + value = int(s) + except: + value = float(s) + return value + + +def parseBlendList(s): + valueList = [] + for element in s: + if isinstance(element, str): + continue + name, attrs, content = element + blendList = attrs["value"].split() + blendList = [safeEval(val) for val in blendList] + valueList.append(blendList) + if len(valueList) == 1: + valueList = valueList[0] + return valueList + + +class NumberConverter(SimpleConverter): + def xmlWrite(self, xmlWriter, name, value): + if isinstance(value, list): + xmlWriter.begintag(name) + xmlWriter.newline() + xmlWriter.indent() + blendValue = " ".join([str(val) for val in value]) + xmlWriter.simpletag(kBlendDictOpName, value=blendValue) + xmlWriter.newline() + xmlWriter.dedent() + xmlWriter.endtag(name) + xmlWriter.newline() + else: + xmlWriter.simpletag(name, value=value) + xmlWriter.newline() + + def xmlRead(self, name, attrs, content, parent): + valueString = attrs.get("value", None) + if valueString is None: + value = parseBlendList(content) + else: + value = parseNum(attrs["value"]) + return value + + +class ArrayConverter(SimpleConverter): + def xmlWrite(self, xmlWriter, name, value): + if value and isinstance(value[0], list): + xmlWriter.begintag(name) + xmlWriter.newline() + xmlWriter.indent() + for valueList in value: + blendValue = " ".join([str(val) for val in valueList]) + xmlWriter.simpletag(kBlendDictOpName, value=blendValue) + xmlWriter.newline() + xmlWriter.dedent() + xmlWriter.endtag(name) + xmlWriter.newline() + else: + value = " ".join([str(val) for val in value]) + xmlWriter.simpletag(name, value=value) + xmlWriter.newline() + + def xmlRead(self, name, attrs, content, parent): + valueString = attrs.get("value", None) + if valueString is None: + valueList = parseBlendList(content) + else: + values = valueString.split() + valueList = [parseNum(value) for value in values] + return valueList + + +class TableConverter(SimpleConverter): + def xmlWrite(self, xmlWriter, name, value): + xmlWriter.begintag(name) + xmlWriter.newline() + value.toXML(xmlWriter) + xmlWriter.endtag(name) + xmlWriter.newline() + + def xmlRead(self, name, attrs, content, parent): + ob = self.getClass()() + for element in content: + if isinstance(element, str): + continue + name, attrs, content = element + ob.fromXML(name, attrs, content) + return ob + + +class PrivateDictConverter(TableConverter): + def getClass(self): + return PrivateDict + + def _read(self, parent, value): + size, offset = value + file = parent.file + isCFF2 = parent._isCFF2 + try: + vstore = parent.vstore + except AttributeError: + vstore = None + priv = PrivateDict(parent.strings, file, offset, isCFF2=isCFF2, vstore=vstore) + file.seek(offset) + data = file.read(size) + assert len(data) == size + priv.decompile(data) + return priv + + def write(self, parent, value): + return (0, 0) # dummy value + + +class SubrsConverter(TableConverter): + def getClass(self): + return SubrsIndex + + def _read(self, parent, value): + file = parent.file + isCFF2 = parent._isCFF2 + file.seek(parent.offset + value) # Offset(self) + return SubrsIndex(file, isCFF2=isCFF2) + + def write(self, parent, value): + return 0 # dummy value + + +class CharStringsConverter(TableConverter): + def _read(self, parent, value): + file = parent.file + isCFF2 = parent._isCFF2 + charset = parent.charset + varStore = getattr(parent, "VarStore", None) + globalSubrs = parent.GlobalSubrs + if hasattr(parent, "FDArray"): + fdArray = parent.FDArray + if hasattr(parent, "FDSelect"): + fdSelect = parent.FDSelect + else: + fdSelect = None + private = None + else: + fdSelect, fdArray = None, None + private = parent.Private + file.seek(value) # Offset(0) + charStrings = CharStrings( + file, + charset, + globalSubrs, + private, + fdSelect, + fdArray, + isCFF2=isCFF2, + varStore=varStore, + ) + return charStrings + + def write(self, parent, value): + return 0 # dummy value + + def xmlRead(self, name, attrs, content, parent): + if hasattr(parent, "FDArray"): + # if it is a CID-keyed font, then the private Dict is extracted from the + # parent.FDArray + fdArray = parent.FDArray + if hasattr(parent, "FDSelect"): + fdSelect = parent.FDSelect + else: + fdSelect = None + private = None + else: + # if it is a name-keyed font, then the private dict is in the top dict, + # and + # there is no fdArray. + private, fdSelect, fdArray = parent.Private, None, None + charStrings = CharStrings( + None, + None, + parent.GlobalSubrs, + private, + fdSelect, + fdArray, + varStore=getattr(parent, "VarStore", None), + ) + charStrings.fromXML(name, attrs, content) + return charStrings + + +class CharsetConverter(SimpleConverter): + def _read(self, parent, value): + isCID = hasattr(parent, "ROS") + if value > 2: + numGlyphs = parent.numGlyphs + file = parent.file + file.seek(value) + log.log(DEBUG, "loading charset at %s", value) + format = readCard8(file) + if format == 0: + charset = parseCharset0(numGlyphs, file, parent.strings, isCID) + elif format == 1 or format == 2: + charset = parseCharset(numGlyphs, file, parent.strings, isCID, format) + else: + raise NotImplementedError + assert len(charset) == numGlyphs + log.log(DEBUG, " charset end at %s", file.tell()) + # make sure glyph names are unique + allNames = {} + newCharset = [] + for glyphName in charset: + if glyphName in allNames: + # make up a new glyphName that's unique + n = allNames[glyphName] + names = set(allNames) | set(charset) + while (glyphName + "." + str(n)) in names: + n += 1 + allNames[glyphName] = n + 1 + glyphName = glyphName + "." + str(n) + allNames[glyphName] = 1 + newCharset.append(glyphName) + charset = newCharset + else: # offset == 0 -> no charset data. + if isCID or "CharStrings" not in parent.rawDict: + # We get here only when processing fontDicts from the FDArray of + # CFF-CID fonts. Only the real topDict references the charset. + assert value == 0 + charset = None + elif value == 0: + charset = cffISOAdobeStrings + elif value == 1: + charset = cffIExpertStrings + elif value == 2: + charset = cffExpertSubsetStrings + if charset and (len(charset) != parent.numGlyphs): + charset = charset[: parent.numGlyphs] + return charset + + def write(self, parent, value): + return 0 # dummy value + + def xmlWrite(self, xmlWriter, name, value): + # XXX only write charset when not in OT/TTX context, where we + # dump charset as a separate "GlyphOrder" table. + # # xmlWriter.simpletag("charset") + xmlWriter.comment("charset is dumped separately as the 'GlyphOrder' element") + xmlWriter.newline() + + def xmlRead(self, name, attrs, content, parent): + pass + + +class CharsetCompiler(object): + def __init__(self, strings, charset, parent): + assert charset[0] == ".notdef" + isCID = hasattr(parent.dictObj, "ROS") + data0 = packCharset0(charset, isCID, strings) + data = packCharset(charset, isCID, strings) + if len(data) < len(data0): + self.data = data + else: + self.data = data0 + self.parent = parent + + def setPos(self, pos, endPos): + self.parent.rawDict["charset"] = pos + + def getDataLength(self): + return len(self.data) + + def toFile(self, file): + file.write(self.data) + + +def getStdCharSet(charset): + # check to see if we can use a predefined charset value. + predefinedCharSetVal = None + predefinedCharSets = [ + (cffISOAdobeStringCount, cffISOAdobeStrings, 0), + (cffExpertStringCount, cffIExpertStrings, 1), + (cffExpertSubsetStringCount, cffExpertSubsetStrings, 2), + ] + lcs = len(charset) + for cnt, pcs, csv in predefinedCharSets: + if predefinedCharSetVal is not None: + break + if lcs > cnt: + continue + predefinedCharSetVal = csv + for i in range(lcs): + if charset[i] != pcs[i]: + predefinedCharSetVal = None + break + return predefinedCharSetVal + + +def getCIDfromName(name, strings): + return int(name[3:]) + + +def getSIDfromName(name, strings): + return strings.getSID(name) + + +def packCharset0(charset, isCID, strings): + fmt = 0 + data = [packCard8(fmt)] + if isCID: + getNameID = getCIDfromName + else: + getNameID = getSIDfromName + + for name in charset[1:]: + data.append(packCard16(getNameID(name, strings))) + return bytesjoin(data) + + +def packCharset(charset, isCID, strings): + fmt = 1 + ranges = [] + first = None + end = 0 + if isCID: + getNameID = getCIDfromName + else: + getNameID = getSIDfromName + + for name in charset[1:]: + SID = getNameID(name, strings) + if first is None: + first = SID + elif end + 1 != SID: + nLeft = end - first + if nLeft > 255: + fmt = 2 + ranges.append((first, nLeft)) + first = SID + end = SID + if end: + nLeft = end - first + if nLeft > 255: + fmt = 2 + ranges.append((first, nLeft)) + + data = [packCard8(fmt)] + if fmt == 1: + nLeftFunc = packCard8 + else: + nLeftFunc = packCard16 + for first, nLeft in ranges: + data.append(packCard16(first) + nLeftFunc(nLeft)) + return bytesjoin(data) + + +def parseCharset0(numGlyphs, file, strings, isCID): + charset = [".notdef"] + if isCID: + for i in range(numGlyphs - 1): + CID = readCard16(file) + charset.append("cid" + str(CID).zfill(5)) + else: + for i in range(numGlyphs - 1): + SID = readCard16(file) + charset.append(strings[SID]) + return charset + + +def parseCharset(numGlyphs, file, strings, isCID, fmt): + charset = [".notdef"] + count = 1 + if fmt == 1: + nLeftFunc = readCard8 + else: + nLeftFunc = readCard16 + while count < numGlyphs: + first = readCard16(file) + nLeft = nLeftFunc(file) + if isCID: + for CID in range(first, first + nLeft + 1): + charset.append("cid" + str(CID).zfill(5)) + else: + for SID in range(first, first + nLeft + 1): + charset.append(strings[SID]) + count = count + nLeft + 1 + return charset + + +class EncodingCompiler(object): + def __init__(self, strings, encoding, parent): + assert not isinstance(encoding, str) + data0 = packEncoding0(parent.dictObj.charset, encoding, parent.strings) + data1 = packEncoding1(parent.dictObj.charset, encoding, parent.strings) + if len(data0) < len(data1): + self.data = data0 + else: + self.data = data1 + self.parent = parent + + def setPos(self, pos, endPos): + self.parent.rawDict["Encoding"] = pos + + def getDataLength(self): + return len(self.data) + + def toFile(self, file): + file.write(self.data) + + +class EncodingConverter(SimpleConverter): + def _read(self, parent, value): + if value == 0: + return "StandardEncoding" + elif value == 1: + return "ExpertEncoding" + # custom encoding at offset `value` + assert value > 1 + file = parent.file + file.seek(value) + log.log(DEBUG, "loading Encoding at %s", value) + fmt = readCard8(file) + haveSupplement = bool(fmt & 0x80) + fmt = fmt & 0x7F + + if fmt == 0: + encoding = parseEncoding0(parent.charset, file) + elif fmt == 1: + encoding = parseEncoding1(parent.charset, file) + else: + raise ValueError(f"Unknown Encoding format: {fmt}") + + if haveSupplement: + parseEncodingSupplement(file, encoding, parent.strings) + + return encoding + + def write(self, parent, value): + if value == "StandardEncoding": + return 0 + elif value == "ExpertEncoding": + return 1 + return 0 # dummy value + + def xmlWrite(self, xmlWriter, name, value): + if value in ("StandardEncoding", "ExpertEncoding"): + xmlWriter.simpletag(name, name=value) + xmlWriter.newline() + return + xmlWriter.begintag(name) + xmlWriter.newline() + for code in range(len(value)): + glyphName = value[code] + if glyphName != ".notdef": + xmlWriter.simpletag("map", code=hex(code), name=glyphName) + xmlWriter.newline() + xmlWriter.endtag(name) + xmlWriter.newline() + + def xmlRead(self, name, attrs, content, parent): + if "name" in attrs: + return attrs["name"] + encoding = [".notdef"] * 256 + for element in content: + if isinstance(element, str): + continue + name, attrs, content = element + code = safeEval(attrs["code"]) + glyphName = attrs["name"] + encoding[code] = glyphName + return encoding + + +def readSID(file): + """Read a String ID (SID) — 2-byte unsigned integer.""" + data = file.read(2) + if len(data) != 2: + raise EOFError("Unexpected end of file while reading SID") + return struct.unpack(">H", data)[0] # big-endian uint16 + + +def parseEncodingSupplement(file, encoding, strings): + """ + Parse the CFF Encoding supplement data: + - nSups: number of supplementary mappings + - each mapping: (code, SID) pair + and apply them to the `encoding` list in place. + """ + nSups = readCard8(file) + for _ in range(nSups): + code = readCard8(file) + sid = readSID(file) + name = strings[sid] + encoding[code] = name + + +def parseEncoding0(charset, file): + """ + Format 0: simple list of codes. + After reading the base table, optionally parse the supplement. + """ + nCodes = readCard8(file) + encoding = [".notdef"] * 256 + for glyphID in range(1, nCodes + 1): + code = readCard8(file) + if code != 0: + encoding[code] = charset[glyphID] + + return encoding + + +def parseEncoding1(charset, file): + """ + Format 1: range-based encoding. + After reading the base ranges, optionally parse the supplement. + """ + nRanges = readCard8(file) + encoding = [".notdef"] * 256 + glyphID = 1 + for _ in range(nRanges): + code = readCard8(file) + nLeft = readCard8(file) + for _ in range(nLeft + 1): + encoding[code] = charset[glyphID] + code += 1 + glyphID += 1 + + return encoding + + +def packEncoding0(charset, encoding, strings): + fmt = 0 + m = {} + for code in range(len(encoding)): + name = encoding[code] + if name != ".notdef": + m[name] = code + codes = [] + for name in charset[1:]: + code = m.get(name) + codes.append(code) + + while codes and codes[-1] is None: + codes.pop() + + data = [packCard8(fmt), packCard8(len(codes))] + for code in codes: + if code is None: + code = 0 + data.append(packCard8(code)) + return bytesjoin(data) + + +def packEncoding1(charset, encoding, strings): + fmt = 1 + m = {} + for code in range(len(encoding)): + name = encoding[code] + if name != ".notdef": + m[name] = code + ranges = [] + first = None + end = 0 + for name in charset[1:]: + code = m.get(name, -1) + if first is None: + first = code + elif end + 1 != code: + nLeft = end - first + ranges.append((first, nLeft)) + first = code + end = code + nLeft = end - first + ranges.append((first, nLeft)) + + # remove unencoded glyphs at the end. + while ranges and ranges[-1][0] == -1: + ranges.pop() + + data = [packCard8(fmt), packCard8(len(ranges))] + for first, nLeft in ranges: + if first == -1: # unencoded + first = 0 + data.append(packCard8(first) + packCard8(nLeft)) + return bytesjoin(data) + + +class FDArrayConverter(TableConverter): + def _read(self, parent, value): + try: + vstore = parent.VarStore + except AttributeError: + vstore = None + file = parent.file + isCFF2 = parent._isCFF2 + file.seek(value) + fdArray = FDArrayIndex(file, isCFF2=isCFF2) + fdArray.vstore = vstore + fdArray.strings = parent.strings + fdArray.GlobalSubrs = parent.GlobalSubrs + return fdArray + + def write(self, parent, value): + return 0 # dummy value + + def xmlRead(self, name, attrs, content, parent): + fdArray = FDArrayIndex() + for element in content: + if isinstance(element, str): + continue + name, attrs, content = element + fdArray.fromXML(name, attrs, content) + return fdArray + + +class FDSelectConverter(SimpleConverter): + def _read(self, parent, value): + file = parent.file + file.seek(value) + maximum = ( + len(parent.cff2GetGlyphOrder()) if parent._isCFF2 else parent.numGlyphs + ) + fdSelect = FDSelect(file, parent.numGlyphs, maxGlyphs=maximum) + return fdSelect + + def write(self, parent, value): + return 0 # dummy value + + # The FDSelect glyph data is written out to XML in the charstring keys, + # so we write out only the format selector + def xmlWrite(self, xmlWriter, name, value): + attrs = [("format", value.format)] + if len(value.gidArray) > getattr(value, "_numCharStrings", len(value.gidArray)): + # VARC-only glyphs have no CharString on which to store their FD. + attrs.append(("gidArray", " ".join(str(fd) for fd in value.gidArray))) + xmlWriter.simpletag(name, attrs) + xmlWriter.newline() + + def xmlRead(self, name, attrs, content, parent): + fmt = safeEval(attrs["format"]) + file = None + numGlyphs = None + fdSelect = FDSelect(file, numGlyphs, fmt) + if "gidArray" in attrs: + fdSelect.gidArray = [int(fd) for fd in attrs["gidArray"].split()] + return fdSelect + + +class VarStoreConverter(SimpleConverter): + def _read(self, parent, value): + file = parent.file + file.seek(value) + varStore = VarStoreData(file) + varStore.decompile() + return varStore + + def write(self, parent, value): + return 0 # dummy value + + def xmlWrite(self, xmlWriter, name, value): + value.writeXML(xmlWriter, name) + + def xmlRead(self, name, attrs, content, parent): + varStore = VarStoreData() + varStore.xmlRead(name, attrs, content, parent) + return varStore + + +def packFDSelect0(fdSelectArray): + fmt = 0 + data = [packCard8(fmt)] + for index in fdSelectArray: + data.append(packCard8(index)) + return bytesjoin(data) + + +def packFDSelect3(fdSelectArray): + fmt = 3 + fdRanges = [] + lenArray = len(fdSelectArray) + lastFDIndex = -1 + for i in range(lenArray): + fdIndex = fdSelectArray[i] + if lastFDIndex != fdIndex: + fdRanges.append([i, fdIndex]) + lastFDIndex = fdIndex + sentinelGID = i + 1 + + data = [packCard8(fmt)] + data.append(packCard16(len(fdRanges))) + for fdRange in fdRanges: + data.append(packCard16(fdRange[0])) + data.append(packCard8(fdRange[1])) + data.append(packCard16(sentinelGID)) + return bytesjoin(data) + + +def packFDSelect4(fdSelectArray): + fmt = 4 + fdRanges = [] + lenArray = len(fdSelectArray) + lastFDIndex = -1 + for i in range(lenArray): + fdIndex = fdSelectArray[i] + if lastFDIndex != fdIndex: + fdRanges.append([i, fdIndex]) + lastFDIndex = fdIndex + sentinelGID = i + 1 + + data = [packCard8(fmt)] + data.append(packCard32(len(fdRanges))) + for fdRange in fdRanges: + data.append(packCard32(fdRange[0])) + data.append(packCard16(fdRange[1])) + data.append(packCard32(sentinelGID)) + return bytesjoin(data) + + +class FDSelectCompiler(object): + def __init__(self, fdSelect, parent): + fmt = fdSelect.format + fdSelectArray = fdSelect.gidArray + if fmt == 0: + self.data = packFDSelect0(fdSelectArray) + elif fmt == 3: + self.data = packFDSelect3(fdSelectArray) + elif fmt == 4: + self.data = packFDSelect4(fdSelectArray) + else: + # choose smaller of the two formats + data0 = packFDSelect0(fdSelectArray) + data3 = packFDSelect3(fdSelectArray) + if len(data0) < len(data3): + self.data = data0 + fdSelect.format = 0 + else: + self.data = data3 + fdSelect.format = 3 + + self.parent = parent + + def setPos(self, pos, endPos): + self.parent.rawDict["FDSelect"] = pos + + def getDataLength(self): + return len(self.data) + + def toFile(self, file): + file.write(self.data) + + +class VarStoreCompiler(object): + def __init__(self, varStoreData, parent): + self.parent = parent + varStoreData.compile() + varStoreDataLen = min(0xFFFF, len(varStoreData.data)) + data = [packCard16(varStoreDataLen), varStoreData.data] + self.data = bytesjoin(data) + + def setPos(self, pos, endPos): + self.parent.rawDict["VarStore"] = pos + + def getDataLength(self): + return len(self.data) + + def toFile(self, file): + file.write(self.data) + + +class ROSConverter(SimpleConverter): + def xmlWrite(self, xmlWriter, name, value): + registry, order, supplement = value + xmlWriter.simpletag( + name, + [ + ("Registry", tostr(registry)), + ("Order", tostr(order)), + ("Supplement", supplement), + ], + ) + xmlWriter.newline() + + def xmlRead(self, name, attrs, content, parent): + return (attrs["Registry"], attrs["Order"], safeEval(attrs["Supplement"])) + + +topDictOperators = [ + # opcode name argument type default converter + (25, "maxstack", "number", None, None), + ((12, 30), "ROS", ("SID", "SID", "number"), None, ROSConverter()), + ((12, 20), "SyntheticBase", "number", None, None), + (0, "version", "SID", None, None), + (1, "Notice", "SID", None, Latin1Converter()), + ((12, 0), "Copyright", "SID", None, Latin1Converter()), + (2, "FullName", "SID", None, Latin1Converter()), + ((12, 38), "FontName", "SID", None, Latin1Converter()), + (3, "FamilyName", "SID", None, Latin1Converter()), + (4, "Weight", "SID", None, None), + ((12, 1), "isFixedPitch", "number", 0, None), + ((12, 2), "ItalicAngle", "number", 0, None), + ((12, 3), "UnderlinePosition", "number", -100, None), + ((12, 4), "UnderlineThickness", "number", 50, None), + ((12, 5), "PaintType", "number", 0, None), + ((12, 6), "CharstringType", "number", 2, None), + ((12, 7), "FontMatrix", "array", [0.001, 0, 0, 0.001, 0, 0], None), + (13, "UniqueID", "number", None, None), + (5, "FontBBox", "array", [0, 0, 0, 0], None), + ((12, 8), "StrokeWidth", "number", 0, None), + (14, "XUID", "array", None, None), + ((12, 21), "PostScript", "SID", None, None), + ((12, 22), "BaseFontName", "SID", None, None), + ((12, 23), "BaseFontBlend", "delta", None, None), + ((12, 31), "CIDFontVersion", "number", 0, None), + ((12, 32), "CIDFontRevision", "number", 0, None), + ((12, 33), "CIDFontType", "number", 0, None), + ((12, 34), "CIDCount", "number", 8720, None), + (15, "charset", "number", None, CharsetConverter()), + ((12, 35), "UIDBase", "number", None, None), + (16, "Encoding", "number", 0, EncodingConverter()), + (18, "Private", ("number", "number"), None, PrivateDictConverter()), + ((12, 37), "FDSelect", "number", None, FDSelectConverter()), + ((12, 36), "FDArray", "number", None, FDArrayConverter()), + (17, "CharStrings", "number", None, CharStringsConverter()), + (24, "VarStore", "number", None, VarStoreConverter()), +] + +topDictOperators2 = [ + # opcode name argument type default converter + (25, "maxstack", "number", None, None), + ((12, 7), "FontMatrix", "array", [0.001, 0, 0, 0.001, 0, 0], None), + ((12, 37), "FDSelect", "number", None, FDSelectConverter()), + ((12, 36), "FDArray", "number", None, FDArrayConverter()), + (17, "CharStrings", "number", None, CharStringsConverter()), + (24, "VarStore", "number", None, VarStoreConverter()), +] + +# Note! FDSelect and FDArray must both preceed CharStrings in the output XML build order, +# in order for the font to compile back from xml. + +kBlendDictOpName = "blend" +blendOp = 23 + +privateDictOperators = [ + # opcode name argument type default converter + (22, "vsindex", "number", None, None), + ( + blendOp, + kBlendDictOpName, + "blendList", + None, + None, + ), # This is for reading to/from XML: it not written to CFF. + (6, "BlueValues", "delta", None, None), + (7, "OtherBlues", "delta", None, None), + (8, "FamilyBlues", "delta", None, None), + (9, "FamilyOtherBlues", "delta", None, None), + ((12, 9), "BlueScale", "number", 0.039625, None), + ((12, 10), "BlueShift", "number", 7, None), + ((12, 11), "BlueFuzz", "number", 1, None), + (10, "StdHW", "number", None, None), + (11, "StdVW", "number", None, None), + ((12, 12), "StemSnapH", "delta", None, None), + ((12, 13), "StemSnapV", "delta", None, None), + ((12, 14), "ForceBold", "number", 0, None), + ((12, 15), "ForceBoldThreshold", "number", None, None), # deprecated + ((12, 16), "lenIV", "number", None, None), # deprecated + ((12, 17), "LanguageGroup", "number", 0, None), + ((12, 18), "ExpansionFactor", "number", 0.06, None), + ((12, 19), "initialRandomSeed", "number", 0, None), + (20, "defaultWidthX", "number", 0, None), + (21, "nominalWidthX", "number", 0, None), + (19, "Subrs", "number", None, SubrsConverter()), +] + +privateDictOperators2 = [ + # opcode name argument type default converter + (22, "vsindex", "number", None, None), + ( + blendOp, + kBlendDictOpName, + "blendList", + None, + None, + ), # This is for reading to/from XML: it not written to CFF. + (6, "BlueValues", "delta", None, None), + (7, "OtherBlues", "delta", None, None), + (8, "FamilyBlues", "delta", None, None), + (9, "FamilyOtherBlues", "delta", None, None), + ((12, 9), "BlueScale", "number", 0.039625, None), + ((12, 10), "BlueShift", "number", 7, None), + ((12, 11), "BlueFuzz", "number", 1, None), + (10, "StdHW", "number", None, None), + (11, "StdVW", "number", None, None), + ((12, 12), "StemSnapH", "delta", None, None), + ((12, 13), "StemSnapV", "delta", None, None), + ((12, 17), "LanguageGroup", "number", 0, None), + ((12, 18), "ExpansionFactor", "number", 0.06, None), + (19, "Subrs", "number", None, SubrsConverter()), +] + + +def addConverters(table): + for i in range(len(table)): + op, name, arg, default, conv = table[i] + if conv is not None: + continue + if arg in ("delta", "array"): + conv = ArrayConverter() + elif arg == "number": + conv = NumberConverter() + elif arg == "SID": + conv = ASCIIConverter() + elif arg == "blendList": + conv = None + else: + assert False + table[i] = op, name, arg, default, conv + + +addConverters(privateDictOperators) +addConverters(topDictOperators) + + +class TopDictDecompiler(psCharStrings.DictDecompiler): + operators = buildOperatorDict(topDictOperators) + + +class PrivateDictDecompiler(psCharStrings.DictDecompiler): + operators = buildOperatorDict(privateDictOperators) + + +class DictCompiler(object): + maxBlendStack = 0 + + def __init__(self, dictObj, strings, parent, isCFF2=None): + if strings: + assert isinstance(strings, IndexedStrings) + if isCFF2 is None and hasattr(parent, "isCFF2"): + isCFF2 = parent.isCFF2 + assert isCFF2 is not None + self.isCFF2 = isCFF2 + self.dictObj = dictObj + self.strings = strings + self.parent = parent + rawDict = {} + for name in dictObj.order: + value = getattr(dictObj, name, None) + if value is None: + continue + conv = dictObj.converters[name] + value = conv.write(dictObj, value) + if value == dictObj.defaults.get(name): + continue + rawDict[name] = value + self.rawDict = rawDict + + def setPos(self, pos, endPos): + pass + + def getDataLength(self): + return len(self.compile("getDataLength")) + + def compile(self, reason): + log.log(DEBUG, "-- compiling %s for %s", self.__class__.__name__, reason) + rawDict = self.rawDict + data = [] + for name in self.dictObj.order: + value = rawDict.get(name) + if value is None: + continue + op, argType = self.opcodes[name] + if isinstance(argType, tuple): + l = len(argType) + assert len(value) == l, "value doesn't match arg type" + for i in range(l): + arg = argType[i] + v = value[i] + arghandler = getattr(self, "arg_" + arg) + data.append(arghandler(v)) + else: + arghandler = getattr(self, "arg_" + argType) + data.append(arghandler(value)) + data.append(op) + data = bytesjoin(data) + return data + + def toFile(self, file): + data = self.compile("toFile") + file.write(data) + + def arg_number(self, num): + if isinstance(num, list): + data = [encodeNumber(val) for val in num] + data.append(encodeNumber(1)) + data.append(bytechr(blendOp)) + datum = bytesjoin(data) + else: + datum = encodeNumber(num) + return datum + + def arg_SID(self, s): + return psCharStrings.encodeIntCFF(self.strings.getSID(s)) + + def arg_array(self, value): + data = [] + for num in value: + data.append(self.arg_number(num)) + return bytesjoin(data) + + def arg_delta(self, value): + if not value: + return b"" + val0 = value[0] + if isinstance(val0, list): + data = self.arg_delta_blend(value) + else: + out = [] + last = 0 + for v in value: + out.append(v - last) + last = v + data = [] + for num in out: + data.append(encodeNumber(num)) + return bytesjoin(data) + + def arg_delta_blend(self, value, prevVal=0, stackDepth=0): + """A delta list with blend lists has to be *all* blend lists. + + The value is a list is arranged as follows:: + + [ + [V0, d0..dn] + [V1, d0..dn] + ... + [Vm, d0..dn] + ] + + ``V`` is the absolute coordinate value from the default font, and ``d0-dn`` + are the delta values from the *n* regions. Each ``V`` is an absolute + coordinate from the default font. + + We want to return a list:: + + [ + [v0, v1..vm] + [d0..dn] + ... + [d0..dn] + numBlends + blendOp + ] + + where each ``v`` is relative to the previous default font value. + """ + numMasters = len(value[0]) + numBlends = len(value) + numStack = stackDepth + (numBlends * numMasters) + 1 + if numStack > self.maxBlendStack: + # Figure out the max number of value we can blend + # and divide this list up into chunks of that size. + + out = [] + while True: + if not value: + break + numBlendValues = (self.maxBlendStack - stackDepth - 1) // numMasters + if numBlendValues <= 0: + raise ValueError("CFF2 blend does not fit the operand stack") + numVal = min(len(value), numBlendValues) + valList = value[:numVal] + out1 = self.arg_delta_blend(valList, prevVal, stackDepth) + out.extend(out1) + prevVal = valList[-1][0] + stackDepth += numVal + value = value[numVal:] + else: + firstList = [0] * numBlends + deltaList = [None] * numBlends + i = 0 + while i < numBlends: + # For PrivateDict BlueValues, the default font + # values are absolute, not relative. + # Must convert these back to relative coordinates + # before writing to CFF2. + defaultValue = value[i][0] + firstList[i] = defaultValue - prevVal + prevVal = defaultValue + deltaList[i] = value[i][1:] + i += 1 + + relValueList = firstList + for blendList in deltaList: + relValueList.extend(blendList) + out = [encodeNumber(val) for val in relValueList] + out.append(encodeNumber(numBlends)) + out.append(bytechr(blendOp)) + return out + + +def encodeNumber(num): + if isinstance(num, float): + return psCharStrings.encodeFloat(num) + else: + return psCharStrings.encodeIntCFF(num) + + +class TopDictCompiler(DictCompiler): + opcodes = buildOpcodeDict(topDictOperators) + + def getChildren(self, strings): + isCFF2 = self.isCFF2 + children = [] + if self.dictObj.cff2GetGlyphOrder is None: + if hasattr(self.dictObj, "charset") and self.dictObj.charset: + if hasattr(self.dictObj, "ROS"): # aka isCID + charsetCode = None + else: + charsetCode = getStdCharSet(self.dictObj.charset) + if charsetCode is None: + children.append( + CharsetCompiler(strings, self.dictObj.charset, self) + ) + else: + self.rawDict["charset"] = charsetCode + if hasattr(self.dictObj, "Encoding") and self.dictObj.Encoding: + encoding = self.dictObj.Encoding + if not isinstance(encoding, str): + children.append(EncodingCompiler(strings, encoding, self)) + else: + if hasattr(self.dictObj, "VarStore"): + varStoreData = self.dictObj.VarStore + varStoreComp = VarStoreCompiler(varStoreData, self) + children.append(varStoreComp) + if hasattr(self.dictObj, "FDSelect"): + # I have not yet supported merging a ttx CFF-CID font, as there are + # interesting issues about merging the FDArrays. Here I assume that + # either the font was read from XML, and the FDSelect indices are all + # in the charstring data, or the FDSelect array is already fully defined. + fdSelect = self.dictObj.FDSelect + # probably read in from XML; assume fdIndex in CharString data + if len(fdSelect) == 0: + charStrings = self.dictObj.CharStrings + for name in self.dictObj.charset: + fdSelect.append(charStrings[name].fdSelectIndex) + if isCFF2 and len(fdSelect) > len(self.dictObj.CharStrings): + # Format 0 has exactly one entry per CharString; only range + # formats can preserve FDs for a tail of VARC-only glyphs. + if fdSelect.format not in (3, 4): + fdSelect.format = ( + 4 + if len(fdSelect) > 0xFFFF or max(fdSelect.gidArray) > 0xFF + else 3 + ) + fdSelectComp = FDSelectCompiler(fdSelect, self) + children.append(fdSelectComp) + if hasattr(self.dictObj, "CharStrings"): + items = [] + charStrings = self.dictObj.CharStrings + for name in self.dictObj.charset: + items.append(charStrings[name]) + charStringsComp = CharStringsCompiler(items, strings, self, isCFF2=isCFF2) + children.append(charStringsComp) + if hasattr(self.dictObj, "FDArray"): + # I have not yet supported merging a ttx CFF-CID font, as there are + # interesting issues about merging the FDArrays. Here I assume that the + # FDArray info is correct and complete. + fdArrayIndexComp = self.dictObj.FDArray.getCompiler(strings, self) + children.append(fdArrayIndexComp) + children.extend(fdArrayIndexComp.getChildren(strings)) + if hasattr(self.dictObj, "Private"): + privComp = self.dictObj.Private.getCompiler(strings, self) + children.append(privComp) + children.extend(privComp.getChildren(strings)) + return children + + +class FontDictCompiler(DictCompiler): + opcodes = buildOpcodeDict(topDictOperators) + + def __init__(self, dictObj, strings, parent, isCFF2=None): + super(FontDictCompiler, self).__init__(dictObj, strings, parent, isCFF2=isCFF2) + # + # We now take some effort to detect if there were any key/value pairs + # supplied that were ignored in the FontDict context, and issue a warning + # for those cases. + # + ignoredNames = [] + dictObj = self.dictObj + for name in sorted(set(dictObj.converters) - set(dictObj.order)): + if name in dictObj.rawDict: + # The font was directly read from binary. In this + # case, we want to report *all* "useless" key/value + # pairs that are in the font, not just the ones that + # are different from the default. + ignoredNames.append(name) + else: + # The font was probably read from a TTX file. We only + # warn about keys whos value is not the default. The + # ones that have the default value will not be written + # to binary anyway. + default = dictObj.defaults.get(name) + if default is not None: + conv = dictObj.converters[name] + default = conv.read(dictObj, default) + if getattr(dictObj, name, None) != default: + ignoredNames.append(name) + if ignoredNames: + log.warning( + "Some CFF FDArray/FontDict keys were ignored upon compile: " + + " ".join(sorted(ignoredNames)) + ) + + def getChildren(self, strings): + children = [] + if hasattr(self.dictObj, "Private"): + privComp = self.dictObj.Private.getCompiler(strings, self) + children.append(privComp) + children.extend(privComp.getChildren(strings)) + return children + + +class PrivateDictCompiler(DictCompiler): + maxBlendStack = maxStackLimit + opcodes = buildOpcodeDict(privateDictOperators) + + def setPos(self, pos, endPos): + size = endPos - pos + self.parent.rawDict["Private"] = size, pos + self.pos = pos + + def getChildren(self, strings): + children = [] + if hasattr(self.dictObj, "Subrs"): + children.append(self.dictObj.Subrs.getCompiler(strings, self)) + return children + + +class BaseDict(object): + def __init__(self, strings=None, file=None, offset=None, isCFF2=None): + assert (isCFF2 is None) == (file is None) + self.rawDict = {} + self.skipNames = [] + self.strings = strings + if file is None: + return + self._isCFF2 = isCFF2 + self.file = file + if offset is not None: + log.log(DEBUG, "loading %s at %s", self.__class__.__name__, offset) + self.offset = offset + + def decompile(self, data): + log.log(DEBUG, " length %s is %d", self.__class__.__name__, len(data)) + dec = self.decompilerClass(self.strings, self) + dec.decompile(data) + self.rawDict = dec.getDict() + self.postDecompile() + + def postDecompile(self): + pass + + def getCompiler(self, strings, parent, isCFF2=None): + return self.compilerClass(self, strings, parent, isCFF2=isCFF2) + + def __getattr__(self, name): + if name[:2] == name[-2:] == "__": + # to make deepcopy() and pickle.load() work, we need to signal with + # AttributeError that dunder methods like '__deepcopy__' or '__getstate__' + # aren't implemented. For more details, see: + # https://github.com/fonttools/fonttools/pull/1488 + raise AttributeError(name) + value = self.rawDict.get(name, None) + if value is None: + value = self.defaults.get(name) + if value is None: + raise AttributeError(name) + conv = self.converters[name] + value = conv.read(self, value) + setattr(self, name, value) + return value + + def __delattr__(self, name): + # Also drop the raw value, if any, otherwise __getattr__ would just + # load the attribute again from rawDict. + inRawDict = self.rawDict.pop(name, None) is not None + if name in self.__dict__ or not inRawDict: + super().__delattr__(name) + + def toXML(self, xmlWriter): + for name in self.order: + if name in self.skipNames: + continue + value = getattr(self, name, None) + # XXX For "charset" we never skip calling xmlWrite even if the + # value is None, so we always write the following XML comment: + # + # + # + # Charset is None when 'CFF ' table is imported from XML into an + # empty TTFont(). By writing this comment all the time, we obtain + # the same XML output whether roundtripping XML-to-XML or + # dumping binary-to-XML + if value is None and name != "charset": + continue + conv = self.converters[name] + conv.xmlWrite(xmlWriter, name, value) + ignoredNames = set(self.rawDict) - set(self.order) + if ignoredNames: + xmlWriter.comment( + "some keys were ignored: %s" % " ".join(sorted(ignoredNames)) + ) + xmlWriter.newline() + + def fromXML(self, name, attrs, content): + conv = self.converters[name] + value = conv.xmlRead(name, attrs, content, self) + setattr(self, name, value) + + +class TopDict(BaseDict): + """The ``TopDict`` represents the top-level dictionary holding font + information. CFF2 tables contain a restricted set of top-level entries + as described `here `_, + but CFF tables may contain a wider range of information. This information + can be accessed through attributes or through the dictionary returned + through the ``rawDict`` property: + + .. code:: python + + font = tt["CFF "].cff[0] + font.FamilyName + # 'Linux Libertine O' + font.rawDict["FamilyName"] + # 'Linux Libertine O' + + More information is available in the CFF file's private dictionary, accessed + via the ``Private`` property: + + .. code:: python + + tt["CFF "].cff[0].Private.BlueValues + # [-15, 0, 515, 515, 666, 666] + + """ + + defaults = buildDefaults(topDictOperators) + converters = buildConverters(topDictOperators) + compilerClass = TopDictCompiler + order = buildOrder(topDictOperators) + decompilerClass = TopDictDecompiler + + def __init__( + self, + strings=None, + file=None, + offset=None, + GlobalSubrs=None, + cff2GetGlyphOrder=None, + isCFF2=None, + ): + super(TopDict, self).__init__(strings, file, offset, isCFF2=isCFF2) + self.cff2GetGlyphOrder = cff2GetGlyphOrder + self.GlobalSubrs = GlobalSubrs + if isCFF2: + self.defaults = buildDefaults(topDictOperators2) + self.charset = cff2GetGlyphOrder() + self.order = buildOrder(topDictOperators2) + else: + self.defaults = buildDefaults(topDictOperators) + self.order = buildOrder(topDictOperators) + + def getGlyphOrder(self): + """Returns a list of glyph names in the CFF font.""" + return self.charset + + def postDecompile(self): + offset = self.rawDict.get("CharStrings") + if offset is None: + return + # get the number of glyphs beforehand. + self.file.seek(offset) + if self._isCFF2: + self.numGlyphs = readCard32(self.file) + if self.numGlyphs > len(self.charset): + raise ValueError("CFF2 CharStrings count exceeds the font glyph count") + # The INDEX can omit a tail of glyphs supplied entirely by VARC. + self.charset = self.charset[: self.numGlyphs] + else: + self.numGlyphs = readCard16(self.file) + + def toXML(self, xmlWriter): + if hasattr(self, "CharStrings"): + self.decompileAllCharStrings() + if hasattr(self, "FDSelect"): + self.FDSelect._numCharStrings = len(self.CharStrings) + if hasattr(self, "ROS"): + self.skipNames = ["Encoding"] + if not hasattr(self, "ROS") or not hasattr(self, "CharStrings"): + # these values have default values, but I only want them to show up + # in CID fonts. + self.skipNames = [ + "CIDFontVersion", + "CIDFontRevision", + "CIDFontType", + "CIDCount", + ] + BaseDict.toXML(self, xmlWriter) + + def decompileAllCharStrings(self): + # Make sure that all the Private Dicts have been instantiated. + for i, charString in enumerate(self.CharStrings.values()): + try: + charString.decompile() + except: + log.error("Error in charstring %s", i) + raise + + def recalcFontBBox(self): + fontBBox = None + for charString in self.CharStrings.values(): + bounds = charString.calcBounds(self.CharStrings) + if bounds is not None: + if fontBBox is not None: + fontBBox = unionRect(fontBBox, bounds) + else: + fontBBox = bounds + + if fontBBox is None: + self.FontBBox = self.defaults["FontBBox"][:] + else: + self.FontBBox = list(intRect(fontBBox)) + + +class FontDict(BaseDict): + # + # Since fonttools used to pass a lot of fields that are not relevant in the FDArray + # FontDict, there are 'ttx' files in the wild that contain all these. These got in + # the ttx files because fonttools writes explicit values for all the TopDict default + # values. These are not actually illegal in the context of an FDArray FontDict - you + # can legally, per spec, put any arbitrary key/value pair in a FontDict - but are + # useless since current major company CFF interpreters ignore anything but the set + # listed in this file. So, we just silently skip them. An exception is Weight: this + # is not used by any interpreter, but some foundries have asked that this be + # supported in FDArray FontDicts just to preserve information about the design when + # the font is being inspected. + # + # On top of that, there are fonts out there that contain such useless FontDict values. + # + # By subclassing TopDict, we *allow* all key/values from TopDict, both when reading + # from binary or when reading from XML, but by overriding `order` with a limited + # list of names, we ensure that only the useful names ever get exported to XML and + # ever get compiled into the binary font. + # + # We override compilerClass so we can warn about "useless" key/value pairs, either + # from the original binary font or from TTX input. + # + # See: + # - https://github.com/fonttools/fonttools/issues/740 + # - https://github.com/fonttools/fonttools/issues/601 + # - https://github.com/adobe-type-tools/afdko/issues/137 + # + defaults = {} + converters = buildConverters(topDictOperators) + compilerClass = FontDictCompiler + orderCFF = ["FontName", "FontMatrix", "Weight", "Private"] + orderCFF2 = ["Private"] + decompilerClass = TopDictDecompiler + + def __init__( + self, + strings=None, + file=None, + offset=None, + GlobalSubrs=None, + isCFF2=None, + vstore=None, + ): + super(FontDict, self).__init__(strings, file, offset, isCFF2=isCFF2) + self.vstore = vstore + self.setCFF2(isCFF2) + + def setCFF2(self, isCFF2): + # isCFF2 may be None. + if isCFF2: + self.order = self.orderCFF2 + self._isCFF2 = True + else: + self.order = self.orderCFF + self._isCFF2 = False + + +class PrivateDict(BaseDict): + defaults = buildDefaults(privateDictOperators) + converters = buildConverters(privateDictOperators) + order = buildOrder(privateDictOperators) + decompilerClass = PrivateDictDecompiler + compilerClass = PrivateDictCompiler + + def __init__(self, strings=None, file=None, offset=None, isCFF2=None, vstore=None): + super(PrivateDict, self).__init__(strings, file, offset, isCFF2=isCFF2) + self.vstore = vstore + if isCFF2: + self.defaults = buildDefaults(privateDictOperators2) + self.order = buildOrder(privateDictOperators2) + # Provide dummy values. This avoids needing to provide + # an isCFF2 state in a lot of places. + self.nominalWidthX = self.defaultWidthX = None + self._isCFF2 = True + else: + self.defaults = buildDefaults(privateDictOperators) + self.order = buildOrder(privateDictOperators) + self._isCFF2 = False + + @property + def in_cff2(self): + return self._isCFF2 + + def getNumRegions(self, vi=None): # called from misc/psCharStrings.py + # if getNumRegions is being called, we can assume that VarStore exists. + if vi is None: + if hasattr(self, "vsindex"): + vi = self.vsindex + else: + vi = 0 + numRegions = self.vstore.getNumRegions(vi) + return numRegions + + +class IndexedStrings(object): + """SID -> string mapping.""" + + def __init__(self, file=None): + if file is None: + strings = [] + else: + strings = [tostr(s, encoding="latin1") for s in Index(file, isCFF2=False)] + self.strings = strings + + def getCompiler(self): + return IndexedStringsCompiler(self, None, self, isCFF2=False) + + def __len__(self): + return len(self.strings) + + def __getitem__(self, SID): + if SID < cffStandardStringCount: + return cffStandardStrings[SID] + else: + return self.strings[SID - cffStandardStringCount] + + def getSID(self, s): + if not hasattr(self, "stringMapping"): + self.buildStringMapping() + s = tostr(s, encoding="latin1") + if s in cffStandardStringMapping: + SID = cffStandardStringMapping[s] + elif s in self.stringMapping: + SID = self.stringMapping[s] + else: + SID = len(self.strings) + cffStandardStringCount + self.strings.append(s) + self.stringMapping[s] = SID + return SID + + def getStrings(self): + return self.strings + + def buildStringMapping(self): + self.stringMapping = {} + for index in range(len(self.strings)): + self.stringMapping[self.strings[index]] = index + cffStandardStringCount + + +# The 391 Standard Strings as used in the CFF format. +# from Adobe Technical None #5176, version 1.0, 18 March 1998 + +cffStandardStrings = [ + ".notdef", + "space", + "exclam", + "quotedbl", + "numbersign", + "dollar", + "percent", + "ampersand", + "quoteright", + "parenleft", + "parenright", + "asterisk", + "plus", + "comma", + "hyphen", + "period", + "slash", + "zero", + "one", + "two", + "three", + "four", + "five", + "six", + "seven", + "eight", + "nine", + "colon", + "semicolon", + "less", + "equal", + "greater", + "question", + "at", + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + "bracketleft", + "backslash", + "bracketright", + "asciicircum", + "underscore", + "quoteleft", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "braceleft", + "bar", + "braceright", + "asciitilde", + "exclamdown", + "cent", + "sterling", + "fraction", + "yen", + "florin", + "section", + "currency", + "quotesingle", + "quotedblleft", + "guillemotleft", + "guilsinglleft", + "guilsinglright", + "fi", + "fl", + "endash", + "dagger", + "daggerdbl", + "periodcentered", + "paragraph", + "bullet", + "quotesinglbase", + "quotedblbase", + "quotedblright", + "guillemotright", + "ellipsis", + "perthousand", + "questiondown", + "grave", + "acute", + "circumflex", + "tilde", + "macron", + "breve", + "dotaccent", + "dieresis", + "ring", + "cedilla", + "hungarumlaut", + "ogonek", + "caron", + "emdash", + "AE", + "ordfeminine", + "Lslash", + "Oslash", + "OE", + "ordmasculine", + "ae", + "dotlessi", + "lslash", + "oslash", + "oe", + "germandbls", + "onesuperior", + "logicalnot", + "mu", + "trademark", + "Eth", + "onehalf", + "plusminus", + "Thorn", + "onequarter", + "divide", + "brokenbar", + "degree", + "thorn", + "threequarters", + "twosuperior", + "registered", + "minus", + "eth", + "multiply", + "threesuperior", + "copyright", + "Aacute", + "Acircumflex", + "Adieresis", + "Agrave", + "Aring", + "Atilde", + "Ccedilla", + "Eacute", + "Ecircumflex", + "Edieresis", + "Egrave", + "Iacute", + "Icircumflex", + "Idieresis", + "Igrave", + "Ntilde", + "Oacute", + "Ocircumflex", + "Odieresis", + "Ograve", + "Otilde", + "Scaron", + "Uacute", + "Ucircumflex", + "Udieresis", + "Ugrave", + "Yacute", + "Ydieresis", + "Zcaron", + "aacute", + "acircumflex", + "adieresis", + "agrave", + "aring", + "atilde", + "ccedilla", + "eacute", + "ecircumflex", + "edieresis", + "egrave", + "iacute", + "icircumflex", + "idieresis", + "igrave", + "ntilde", + "oacute", + "ocircumflex", + "odieresis", + "ograve", + "otilde", + "scaron", + "uacute", + "ucircumflex", + "udieresis", + "ugrave", + "yacute", + "ydieresis", + "zcaron", + "exclamsmall", + "Hungarumlautsmall", + "dollaroldstyle", + "dollarsuperior", + "ampersandsmall", + "Acutesmall", + "parenleftsuperior", + "parenrightsuperior", + "twodotenleader", + "onedotenleader", + "zerooldstyle", + "oneoldstyle", + "twooldstyle", + "threeoldstyle", + "fouroldstyle", + "fiveoldstyle", + "sixoldstyle", + "sevenoldstyle", + "eightoldstyle", + "nineoldstyle", + "commasuperior", + "threequartersemdash", + "periodsuperior", + "questionsmall", + "asuperior", + "bsuperior", + "centsuperior", + "dsuperior", + "esuperior", + "isuperior", + "lsuperior", + "msuperior", + "nsuperior", + "osuperior", + "rsuperior", + "ssuperior", + "tsuperior", + "ff", + "ffi", + "ffl", + "parenleftinferior", + "parenrightinferior", + "Circumflexsmall", + "hyphensuperior", + "Gravesmall", + "Asmall", + "Bsmall", + "Csmall", + "Dsmall", + "Esmall", + "Fsmall", + "Gsmall", + "Hsmall", + "Ismall", + "Jsmall", + "Ksmall", + "Lsmall", + "Msmall", + "Nsmall", + "Osmall", + "Psmall", + "Qsmall", + "Rsmall", + "Ssmall", + "Tsmall", + "Usmall", + "Vsmall", + "Wsmall", + "Xsmall", + "Ysmall", + "Zsmall", + "colonmonetary", + "onefitted", + "rupiah", + "Tildesmall", + "exclamdownsmall", + "centoldstyle", + "Lslashsmall", + "Scaronsmall", + "Zcaronsmall", + "Dieresissmall", + "Brevesmall", + "Caronsmall", + "Dotaccentsmall", + "Macronsmall", + "figuredash", + "hypheninferior", + "Ogoneksmall", + "Ringsmall", + "Cedillasmall", + "questiondownsmall", + "oneeighth", + "threeeighths", + "fiveeighths", + "seveneighths", + "onethird", + "twothirds", + "zerosuperior", + "foursuperior", + "fivesuperior", + "sixsuperior", + "sevensuperior", + "eightsuperior", + "ninesuperior", + "zeroinferior", + "oneinferior", + "twoinferior", + "threeinferior", + "fourinferior", + "fiveinferior", + "sixinferior", + "seveninferior", + "eightinferior", + "nineinferior", + "centinferior", + "dollarinferior", + "periodinferior", + "commainferior", + "Agravesmall", + "Aacutesmall", + "Acircumflexsmall", + "Atildesmall", + "Adieresissmall", + "Aringsmall", + "AEsmall", + "Ccedillasmall", + "Egravesmall", + "Eacutesmall", + "Ecircumflexsmall", + "Edieresissmall", + "Igravesmall", + "Iacutesmall", + "Icircumflexsmall", + "Idieresissmall", + "Ethsmall", + "Ntildesmall", + "Ogravesmall", + "Oacutesmall", + "Ocircumflexsmall", + "Otildesmall", + "Odieresissmall", + "OEsmall", + "Oslashsmall", + "Ugravesmall", + "Uacutesmall", + "Ucircumflexsmall", + "Udieresissmall", + "Yacutesmall", + "Thornsmall", + "Ydieresissmall", + "001.000", + "001.001", + "001.002", + "001.003", + "Black", + "Bold", + "Book", + "Light", + "Medium", + "Regular", + "Roman", + "Semibold", +] + +cffStandardStringCount = 391 +assert len(cffStandardStrings) == cffStandardStringCount +# build reverse mapping +cffStandardStringMapping = {} +for _i in range(cffStandardStringCount): + cffStandardStringMapping[cffStandardStrings[_i]] = _i + +cffISOAdobeStrings = [ + ".notdef", + "space", + "exclam", + "quotedbl", + "numbersign", + "dollar", + "percent", + "ampersand", + "quoteright", + "parenleft", + "parenright", + "asterisk", + "plus", + "comma", + "hyphen", + "period", + "slash", + "zero", + "one", + "two", + "three", + "four", + "five", + "six", + "seven", + "eight", + "nine", + "colon", + "semicolon", + "less", + "equal", + "greater", + "question", + "at", + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + "bracketleft", + "backslash", + "bracketright", + "asciicircum", + "underscore", + "quoteleft", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "braceleft", + "bar", + "braceright", + "asciitilde", + "exclamdown", + "cent", + "sterling", + "fraction", + "yen", + "florin", + "section", + "currency", + "quotesingle", + "quotedblleft", + "guillemotleft", + "guilsinglleft", + "guilsinglright", + "fi", + "fl", + "endash", + "dagger", + "daggerdbl", + "periodcentered", + "paragraph", + "bullet", + "quotesinglbase", + "quotedblbase", + "quotedblright", + "guillemotright", + "ellipsis", + "perthousand", + "questiondown", + "grave", + "acute", + "circumflex", + "tilde", + "macron", + "breve", + "dotaccent", + "dieresis", + "ring", + "cedilla", + "hungarumlaut", + "ogonek", + "caron", + "emdash", + "AE", + "ordfeminine", + "Lslash", + "Oslash", + "OE", + "ordmasculine", + "ae", + "dotlessi", + "lslash", + "oslash", + "oe", + "germandbls", + "onesuperior", + "logicalnot", + "mu", + "trademark", + "Eth", + "onehalf", + "plusminus", + "Thorn", + "onequarter", + "divide", + "brokenbar", + "degree", + "thorn", + "threequarters", + "twosuperior", + "registered", + "minus", + "eth", + "multiply", + "threesuperior", + "copyright", + "Aacute", + "Acircumflex", + "Adieresis", + "Agrave", + "Aring", + "Atilde", + "Ccedilla", + "Eacute", + "Ecircumflex", + "Edieresis", + "Egrave", + "Iacute", + "Icircumflex", + "Idieresis", + "Igrave", + "Ntilde", + "Oacute", + "Ocircumflex", + "Odieresis", + "Ograve", + "Otilde", + "Scaron", + "Uacute", + "Ucircumflex", + "Udieresis", + "Ugrave", + "Yacute", + "Ydieresis", + "Zcaron", + "aacute", + "acircumflex", + "adieresis", + "agrave", + "aring", + "atilde", + "ccedilla", + "eacute", + "ecircumflex", + "edieresis", + "egrave", + "iacute", + "icircumflex", + "idieresis", + "igrave", + "ntilde", + "oacute", + "ocircumflex", + "odieresis", + "ograve", + "otilde", + "scaron", + "uacute", + "ucircumflex", + "udieresis", + "ugrave", + "yacute", + "ydieresis", + "zcaron", +] + +cffISOAdobeStringCount = 229 +assert len(cffISOAdobeStrings) == cffISOAdobeStringCount + +cffIExpertStrings = [ + ".notdef", + "space", + "exclamsmall", + "Hungarumlautsmall", + "dollaroldstyle", + "dollarsuperior", + "ampersandsmall", + "Acutesmall", + "parenleftsuperior", + "parenrightsuperior", + "twodotenleader", + "onedotenleader", + "comma", + "hyphen", + "period", + "fraction", + "zerooldstyle", + "oneoldstyle", + "twooldstyle", + "threeoldstyle", + "fouroldstyle", + "fiveoldstyle", + "sixoldstyle", + "sevenoldstyle", + "eightoldstyle", + "nineoldstyle", + "colon", + "semicolon", + "commasuperior", + "threequartersemdash", + "periodsuperior", + "questionsmall", + "asuperior", + "bsuperior", + "centsuperior", + "dsuperior", + "esuperior", + "isuperior", + "lsuperior", + "msuperior", + "nsuperior", + "osuperior", + "rsuperior", + "ssuperior", + "tsuperior", + "ff", + "fi", + "fl", + "ffi", + "ffl", + "parenleftinferior", + "parenrightinferior", + "Circumflexsmall", + "hyphensuperior", + "Gravesmall", + "Asmall", + "Bsmall", + "Csmall", + "Dsmall", + "Esmall", + "Fsmall", + "Gsmall", + "Hsmall", + "Ismall", + "Jsmall", + "Ksmall", + "Lsmall", + "Msmall", + "Nsmall", + "Osmall", + "Psmall", + "Qsmall", + "Rsmall", + "Ssmall", + "Tsmall", + "Usmall", + "Vsmall", + "Wsmall", + "Xsmall", + "Ysmall", + "Zsmall", + "colonmonetary", + "onefitted", + "rupiah", + "Tildesmall", + "exclamdownsmall", + "centoldstyle", + "Lslashsmall", + "Scaronsmall", + "Zcaronsmall", + "Dieresissmall", + "Brevesmall", + "Caronsmall", + "Dotaccentsmall", + "Macronsmall", + "figuredash", + "hypheninferior", + "Ogoneksmall", + "Ringsmall", + "Cedillasmall", + "onequarter", + "onehalf", + "threequarters", + "questiondownsmall", + "oneeighth", + "threeeighths", + "fiveeighths", + "seveneighths", + "onethird", + "twothirds", + "zerosuperior", + "onesuperior", + "twosuperior", + "threesuperior", + "foursuperior", + "fivesuperior", + "sixsuperior", + "sevensuperior", + "eightsuperior", + "ninesuperior", + "zeroinferior", + "oneinferior", + "twoinferior", + "threeinferior", + "fourinferior", + "fiveinferior", + "sixinferior", + "seveninferior", + "eightinferior", + "nineinferior", + "centinferior", + "dollarinferior", + "periodinferior", + "commainferior", + "Agravesmall", + "Aacutesmall", + "Acircumflexsmall", + "Atildesmall", + "Adieresissmall", + "Aringsmall", + "AEsmall", + "Ccedillasmall", + "Egravesmall", + "Eacutesmall", + "Ecircumflexsmall", + "Edieresissmall", + "Igravesmall", + "Iacutesmall", + "Icircumflexsmall", + "Idieresissmall", + "Ethsmall", + "Ntildesmall", + "Ogravesmall", + "Oacutesmall", + "Ocircumflexsmall", + "Otildesmall", + "Odieresissmall", + "OEsmall", + "Oslashsmall", + "Ugravesmall", + "Uacutesmall", + "Ucircumflexsmall", + "Udieresissmall", + "Yacutesmall", + "Thornsmall", + "Ydieresissmall", +] + +cffExpertStringCount = 166 +assert len(cffIExpertStrings) == cffExpertStringCount + +cffExpertSubsetStrings = [ + ".notdef", + "space", + "dollaroldstyle", + "dollarsuperior", + "parenleftsuperior", + "parenrightsuperior", + "twodotenleader", + "onedotenleader", + "comma", + "hyphen", + "period", + "fraction", + "zerooldstyle", + "oneoldstyle", + "twooldstyle", + "threeoldstyle", + "fouroldstyle", + "fiveoldstyle", + "sixoldstyle", + "sevenoldstyle", + "eightoldstyle", + "nineoldstyle", + "colon", + "semicolon", + "commasuperior", + "threequartersemdash", + "periodsuperior", + "asuperior", + "bsuperior", + "centsuperior", + "dsuperior", + "esuperior", + "isuperior", + "lsuperior", + "msuperior", + "nsuperior", + "osuperior", + "rsuperior", + "ssuperior", + "tsuperior", + "ff", + "fi", + "fl", + "ffi", + "ffl", + "parenleftinferior", + "parenrightinferior", + "hyphensuperior", + "colonmonetary", + "onefitted", + "rupiah", + "centoldstyle", + "figuredash", + "hypheninferior", + "onequarter", + "onehalf", + "threequarters", + "oneeighth", + "threeeighths", + "fiveeighths", + "seveneighths", + "onethird", + "twothirds", + "zerosuperior", + "onesuperior", + "twosuperior", + "threesuperior", + "foursuperior", + "fivesuperior", + "sixsuperior", + "sevensuperior", + "eightsuperior", + "ninesuperior", + "zeroinferior", + "oneinferior", + "twoinferior", + "threeinferior", + "fourinferior", + "fiveinferior", + "sixinferior", + "seveninferior", + "eightinferior", + "nineinferior", + "centinferior", + "dollarinferior", + "periodinferior", + "commainferior", +] + +cffExpertSubsetStringCount = 87 +assert len(cffExpertSubsetStrings) == cffExpertSubsetStringCount diff --git a/hug/lib/python3.12/site-packages/fontTools/cffLib/__pycache__/CFF2ToCFF.cpython-312.pyc b/hug/lib/python3.12/site-packages/fontTools/cffLib/__pycache__/CFF2ToCFF.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..27982cde79bf5cf2bfd537da18b24799579d91af Binary files /dev/null and b/hug/lib/python3.12/site-packages/fontTools/cffLib/__pycache__/CFF2ToCFF.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/fontTools/cffLib/__pycache__/CFFToCFF2.cpython-312.pyc b/hug/lib/python3.12/site-packages/fontTools/cffLib/__pycache__/CFFToCFF2.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a59adb357ec59b944d173fdce3891373deffb632 Binary files /dev/null and b/hug/lib/python3.12/site-packages/fontTools/cffLib/__pycache__/CFFToCFF2.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/fontTools/cffLib/__pycache__/__init__.cpython-312.pyc b/hug/lib/python3.12/site-packages/fontTools/cffLib/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..686a02177a7f03e48eac60a96b3f866596f7fc05 --- /dev/null +++ b/hug/lib/python3.12/site-packages/fontTools/cffLib/__pycache__/__init__.cpython-312.pyc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e8421131187edf61ecb2e763c4d182df2d75a061ee0dfa434587bed625af084a +size 126905 diff --git a/hug/lib/python3.12/site-packages/fontTools/cffLib/__pycache__/specializer.cpython-312.pyc b/hug/lib/python3.12/site-packages/fontTools/cffLib/__pycache__/specializer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0e421e352651f2411754d5e949edd8eca72ad2cf Binary files /dev/null and b/hug/lib/python3.12/site-packages/fontTools/cffLib/__pycache__/specializer.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/fontTools/cffLib/__pycache__/transforms.cpython-312.pyc b/hug/lib/python3.12/site-packages/fontTools/cffLib/__pycache__/transforms.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..37da6d44aa78fe280ec24123f1e2c79412c74876 Binary files /dev/null and b/hug/lib/python3.12/site-packages/fontTools/cffLib/__pycache__/transforms.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/fontTools/cffLib/__pycache__/width.cpython-312.pyc b/hug/lib/python3.12/site-packages/fontTools/cffLib/__pycache__/width.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..055a3b7cecda971dbd72449755bdb634ca7a3c37 Binary files /dev/null and b/hug/lib/python3.12/site-packages/fontTools/cffLib/__pycache__/width.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/fontTools/cffLib/specializer.py b/hug/lib/python3.12/site-packages/fontTools/cffLib/specializer.py new file mode 100644 index 0000000000000000000000000000000000000000..ba311d384633a4ea8637ba0ff3419c05fdc9a8fb --- /dev/null +++ b/hug/lib/python3.12/site-packages/fontTools/cffLib/specializer.py @@ -0,0 +1,1101 @@ +# -*- coding: utf-8 -*- + +"""T2CharString operator specializer and generalizer. + +PostScript glyph drawing operations can be expressed in multiple different +ways. For example, as well as the ``lineto`` operator, there is also a +``hlineto`` operator which draws a horizontal line, removing the need to +specify a ``dx`` coordinate, and a ``vlineto`` operator which draws a +vertical line, removing the need to specify a ``dy`` coordinate. As well +as decompiling :class:`fontTools.misc.psCharStrings.T2CharString` objects +into lists of operations, this module allows for conversion between general +and specific forms of the operation. + +""" + +import functools + +from fontTools.cffLib import maxStackLimit + + +def stringToProgram(string): + if isinstance(string, str): + string = string.split() + program = [] + for token in string: + try: + token = int(token) + except ValueError: + try: + token = float(token) + except ValueError: + pass + program.append(token) + return program + + +def programToString(program): + return " ".join(str(x) for x in program) + + +def programToCommands(program, getNumRegions=None, vsIndex=0): + """Takes a T2CharString program list and returns list of commands. + Each command is a two-tuple of commandname,arg-list. The commandname might + be empty string if no commandname shall be emitted (used for glyph width, + hintmask/cntrmask argument, as well as stray arguments at the end of the + program (🤷). + 'getNumRegions' may be None, or a callable object. It must return the + number of regions. 'getNumRegions' takes a single argument, vsindex. It + returns the numRegions for the vsindex. + 'vsIndex' is the vsindex in effect until the program sets one: that of + the Private DICT the charstring uses. + The Charstring may or may not start with a width value. If the first + non-blend operator has an odd number of arguments, then the first argument is + a width, and is popped off. This is complicated with blend operators, as + there may be more than one before the first hint or moveto operator, and each + one reduces several arguments to just one list argument. We have to sum the + number of arguments that are not part of the blend arguments, and all the + 'numBlends' values. We could instead have said that by definition, if there + is a blend operator, there is no width value, since CFF2 Charstrings don't + have width values. I discussed this with Behdad, and we are allowing for an + initial width value in this case because developers may assemble a CFF2 + charstring from CFF Charstrings, which could have width values. + """ + + seenWidthOp = False + lenBlendStack = 0 + lastBlendIndex = 0 + commands = [] + stack = [] + it = iter(program) + + for token in it: + if not isinstance(token, str): + stack.append(token) + continue + + if token == "blend": + assert getNumRegions is not None + numSourceFonts = 1 + getNumRegions(vsIndex) + # replace the blend op args on the stack with a single list + # containing all the blend op args. + numBlends = stack[-1] + numBlendArgs = numBlends * numSourceFonts + 1 + # replace first blend op by a list of the blend ops. + stack[-numBlendArgs:] = [stack[-numBlendArgs:]] + lenStack = len(stack) + lenBlendStack += numBlends + lenStack - 1 - lastBlendIndex + lastBlendIndex = lenStack + # if a blend op exists, this is or will be a CFF2 charstring. + continue + + elif token == "vsindex": + vsIndex = stack[-1] + assert type(vsIndex) is int + + elif (not seenWidthOp) and token in { + "hstem", + "hstemhm", + "vstem", + "vstemhm", + "cntrmask", + "hintmask", + "hmoveto", + "vmoveto", + "rmoveto", + "endchar", + }: + seenWidthOp = True + parity = token in {"hmoveto", "vmoveto"} + if lenBlendStack: + # lenBlendStack has the number of args represented by the last blend + # arg and all the preceding args. We need to now add the number of + # args following the last blend arg. + numArgs = lenBlendStack + len(stack[lastBlendIndex:]) + else: + numArgs = len(stack) + if numArgs and (numArgs % 2) ^ parity: + width = stack.pop(0) + commands.append(("", [width])) + + if token in {"hintmask", "cntrmask"}: + if stack: + commands.append(("", stack)) + commands.append((token, [])) + commands.append(("", [next(it)])) + else: + commands.append((token, stack)) + stack = [] + if stack: + commands.append(("", stack)) + return commands + + +def _flattenBlendArgs(args): + token_list = [] + for arg in args: + if isinstance(arg, list): + token_list.extend(arg) + token_list.append("blend") + else: + token_list.append(arg) + return token_list + + +def commandsToProgram(commands): + """Takes a commands list as returned by programToCommands() and converts + it back to a T2CharString program list.""" + program = [] + for op, args in commands: + if any(isinstance(arg, list) for arg in args): + args = _flattenBlendArgs(args) + program.extend(args) + if op: + program.append(op) + return program + + +def _everyN(el, n): + """Group the list el into groups of size n""" + l = len(el) + if l % n != 0: + raise ValueError(el) + for i in range(0, l, n): + yield el[i : i + n] + + +class _GeneralizerDecombinerCommandsMap(object): + def __init__(self, getScalarRanges=None, vsIndex=0): + self.getScalarRanges = getScalarRanges + self.vsIndex = vsIndex + + def vsindex(self, args): + self.vsIndex = args[0] + yield ("vsindex", args) + + @staticmethod + def rmoveto(args): + if len(args) != 2: + raise ValueError(args) + yield ("rmoveto", args) + + @staticmethod + def hmoveto(args): + if len(args) != 1: + raise ValueError(args) + yield ("rmoveto", [args[0], 0]) + + @staticmethod + def vmoveto(args): + if len(args) != 1: + raise ValueError(args) + yield ("rmoveto", [0, args[0]]) + + @staticmethod + def rlineto(args): + if not args: + raise ValueError(args) + for args in _everyN(args, 2): + yield ("rlineto", args) + + @staticmethod + def hlineto(args): + if not args: + raise ValueError(args) + it = iter(args) + try: + while True: + yield ("rlineto", [next(it), 0]) + yield ("rlineto", [0, next(it)]) + except StopIteration: + pass + + @staticmethod + def vlineto(args): + if not args: + raise ValueError(args) + it = iter(args) + try: + while True: + yield ("rlineto", [0, next(it)]) + yield ("rlineto", [next(it), 0]) + except StopIteration: + pass + + @staticmethod + def rrcurveto(args): + if not args: + raise ValueError(args) + for args in _everyN(args, 6): + yield ("rrcurveto", args) + + @staticmethod + def hhcurveto(args): + l = len(args) + if l < 4 or l % 4 > 1: + raise ValueError(args) + if l % 2 == 1: + yield ("rrcurveto", [args[1], args[0], args[2], args[3], args[4], 0]) + args = args[5:] + for args in _everyN(args, 4): + yield ("rrcurveto", [args[0], 0, args[1], args[2], args[3], 0]) + + @staticmethod + def vvcurveto(args): + l = len(args) + if l < 4 or l % 4 > 1: + raise ValueError(args) + if l % 2 == 1: + yield ("rrcurveto", [args[0], args[1], args[2], args[3], 0, args[4]]) + args = args[5:] + for args in _everyN(args, 4): + yield ("rrcurveto", [0, args[0], args[1], args[2], 0, args[3]]) + + @staticmethod + def hvcurveto(args): + l = len(args) + if l < 4 or l % 8 not in {0, 1, 4, 5}: + raise ValueError(args) + last_args = None + if l % 2 == 1: + lastStraight = l % 8 == 5 + args, last_args = args[:-5], args[-5:] + it = _everyN(args, 4) + try: + while True: + args = next(it) + yield ("rrcurveto", [args[0], 0, args[1], args[2], 0, args[3]]) + args = next(it) + yield ("rrcurveto", [0, args[0], args[1], args[2], args[3], 0]) + except StopIteration: + pass + if last_args: + args = last_args + if lastStraight: + yield ("rrcurveto", [args[0], 0, args[1], args[2], args[4], args[3]]) + else: + yield ("rrcurveto", [0, args[0], args[1], args[2], args[3], args[4]]) + + @staticmethod + def vhcurveto(args): + l = len(args) + if l < 4 or l % 8 not in {0, 1, 4, 5}: + raise ValueError(args) + last_args = None + if l % 2 == 1: + lastStraight = l % 8 == 5 + args, last_args = args[:-5], args[-5:] + it = _everyN(args, 4) + try: + while True: + args = next(it) + yield ("rrcurveto", [0, args[0], args[1], args[2], args[3], 0]) + args = next(it) + yield ("rrcurveto", [args[0], 0, args[1], args[2], 0, args[3]]) + except StopIteration: + pass + if last_args: + args = last_args + if lastStraight: + yield ("rrcurveto", [0, args[0], args[1], args[2], args[3], args[4]]) + else: + yield ("rrcurveto", [args[0], 0, args[1], args[2], args[4], args[3]]) + + @staticmethod + def rcurveline(args): + l = len(args) + if l < 8 or l % 6 != 2: + raise ValueError(args) + args, last_args = args[:-2], args[-2:] + for args in _everyN(args, 6): + yield ("rrcurveto", args) + yield ("rlineto", last_args) + + @staticmethod + def rlinecurve(args): + l = len(args) + if l < 8 or l % 2 != 0: + raise ValueError(args) + args, last_args = args[:-6], args[-6:] + for args in _everyN(args, 2): + yield ("rlineto", args) + yield ("rrcurveto", last_args) + + @staticmethod + def hflex(args): + dx1, dx2, dy2, dx3, dx4, dx5, dx6 = args + yield ( + "flex", + [ + dx1, + 0, + dx2, + dy2, + dx3, + 0, + dx4, + 0, + dx5, + _negateBlendArg(dy2), + dx6, + 0, + 50, + ], + ) + + @staticmethod + def hflex1(args): + dx1, dy1, dx2, dy2, dx3, dx4, dx5, dy5, dx6 = args + dy6 = _negateBlendArg(_sumBlendArgs([dy1, dy2, dy5])) + yield ("flex", [dx1, dy1, dx2, dy2, dx3, 0, dx4, 0, dx5, dy5, dx6, dy6, 50]) + + def flex1(self, args): + if len(args) != 11: + raise ValueError(args) + scalarRanges = ( + self.getScalarRanges(self.vsIndex) if self.getScalarRanges else None + ) + dx = _sumBlendArgs(args[0:10:2]) + dy = _sumBlendArgs(args[1:10:2]) + horizontal = _isFlex1Horizontal(dx, dy, scalarRanges) + if horizontal is None: + # A flex1 whose direction changes cannot be represented as flex. + yield ("flex1", args) + else: + if horizontal: + last = [args[10], _negateBlendArg(dy)] + else: + last = [_negateBlendArg(dx), args[10]] + yield ("flex", args[0:10] + last + [50]) + + +def _convertBlendOpToArgs(blendList): + # args is list of blend op args. Since we are supporting + # recursive blend op calls, some of these args may also + # be a list of blend op args, and need to be converted before + # we convert the current list. + if any([isinstance(arg, list) for arg in blendList]): + args = [ + i + for e in blendList + for i in (_convertBlendOpToArgs(e) if isinstance(e, list) else [e]) + ] + else: + args = blendList + + # We now know that blendList contains a blend op argument list, even if + # some of the args are lists that each contain a blend op argument list. + # Convert from: + # [default font arg sequence x0,...,xn] + [delta tuple for x0] + ... + [delta tuple for xn] + # to: + # [ [x0] + [delta tuple for x0], + # ..., + # [xn] + [delta tuple for xn] ] + numBlends = args[-1] + # Can't use args.pop() when the args are being used in a nested list + # comprehension. See calling context + args = args[:-1] + + l = len(args) + numRegions = l // numBlends - 1 + if not (numBlends * (numRegions + 1) == l): + raise ValueError(blendList) + + if numRegions == 0: + return args + + defaultArgs = [[arg] for arg in args[:numBlends]] + deltaArgs = args[numBlends:] + numDeltaValues = len(deltaArgs) + deltaList = [ + deltaArgs[i : i + numRegions] for i in range(0, numDeltaValues, numRegions) + ] + blend_args = [a + b + [1] for a, b in zip(defaultArgs, deltaList)] + return blend_args + + +def generalizeCommands(commands, ignoreErrors=False, getScalarRanges=None, vsIndex=0): + result = [] + mapping = _GeneralizerDecombinerCommandsMap(getScalarRanges, vsIndex) + for op, args in commands: + # First, generalize any blend args in the arg list. + if any([isinstance(arg, list) for arg in args]): + try: + args = [ + n + for arg in args + for n in ( + _convertBlendOpToArgs(arg) if isinstance(arg, list) else [arg] + ) + ] + except ValueError: + if ignoreErrors: + # Store op as data, such that consumers of commands do not have to + # deal with incorrect number of arguments. + result.append(("", args)) + result.append(("", [op])) + else: + raise + + func = getattr(mapping, op, None) + if func is None: + result.append((op, args)) + continue + try: + for command in func(args): + result.append(command) + except ValueError: + if ignoreErrors: + # Store op as data, such that consumers of commands do not have to + # deal with incorrect number of arguments. + result.append(("", args)) + result.append(("", [op])) + else: + raise + return result + + +def generalizeProgram(program, getNumRegions=None, vsIndex=0, **kwargs): + return commandsToProgram( + generalizeCommands( + programToCommands(program, getNumRegions, vsIndex=vsIndex), + vsIndex=vsIndex, + **kwargs, + ) + ) + + +def _categorizeVector(v): + """ + Takes X,Y vector v and returns one of r, h, v, or 0 depending on which + of X and/or Y are zero, plus tuple of nonzero ones. If both are zero, + it returns a single zero still. + + >>> _categorizeVector((0,0)) + ('0', (0,)) + >>> _categorizeVector((1,0)) + ('h', (1,)) + >>> _categorizeVector((0,2)) + ('v', (2,)) + >>> _categorizeVector((1,2)) + ('r', (1, 2)) + """ + if not v[0]: + if not v[1]: + return "0", v[:1] + else: + return "v", v[1:] + else: + if not v[1]: + return "h", v[:1] + else: + return "r", v + + +def _mergeCategories(a, b): + if a == "0": + return b + if b == "0": + return a + if a == b: + return a + return None + + +def _negateCategory(a): + if a == "h": + return "v" + if a == "v": + return "h" + assert a in "0r" + return a + + +def _convertToBlendCmds(args): + # return a list of blend commands, and + # the remaining non-blended args, if any. + num_args = len(args) + stack_use = 0 + new_args = [] + i = 0 + while i < num_args: + arg = args[i] + i += 1 + if not isinstance(arg, list): + new_args.append(arg) + stack_use += 1 + else: + prev_stack_use = stack_use + # The arg is a tuple of blend values. + # These are each (master 0,delta 1..delta n, 1) + # Combine as many successive tuples as we can, + # up to the max stack limit. + num_sources = len(arg) - 1 + blendlist = [arg] + stack_use += 1 + num_sources # 1 for the num_blends arg + + # if we are here, max stack is the CFF2 max stack. + # I use the CFF2 max stack limit here rather than + # the 'maxstack' chosen by the client, as the default + # maxstack may have been used unintentionally. For all + # the other operators, this just produces a little less + # optimization, but here it puts a hard (and low) limit + # on the number of source fonts that can be used. + # + # Make sure the stack depth does not exceed (maxstack - 1), so + # that subroutinizer can insert subroutine calls at any point. + while ( + (i < num_args) + and isinstance(args[i], list) + and stack_use + num_sources < maxStackLimit + ): + blendlist.append(args[i]) + i += 1 + stack_use += num_sources + # blendList now contains as many single blend tuples as can be + # combined without exceeding the CFF2 stack limit. + num_blends = len(blendlist) + # append the 'num_blends' default font values + blend_args = [] + for arg in blendlist: + blend_args.append(arg[0]) + for arg in blendlist: + assert arg[-1] == 1 + blend_args.extend(arg[1:-1]) + blend_args.append(num_blends) + new_args.append(blend_args) + stack_use = prev_stack_use + num_blends + + return new_args + + +def _addArgs(a, b): + if isinstance(b, list): + if isinstance(a, list): + if len(a) != len(b) or a[-1] != b[-1]: + raise ValueError() + return [_addArgs(va, vb) for va, vb in zip(a[:-1], b[:-1])] + [a[-1]] + else: + a, b = b, a + if isinstance(a, list): + assert a[-1] == 1 + return [_addArgs(a[0], b)] + a[1:] + return a + b + + +def blendArgValues(arg, numValues): + """Return a generalized argument's default and deltas, padded with zeros. + + ``arg`` is a number or a single-value blend list ``[default, *deltas, 1]`` + as returned by :func:`generalizeCommands`. The result has ``numValues`` + entries, including the default. + """ + values = arg[:-1] if isinstance(arg, list) else [arg] + if any(isinstance(value, list) for value in values): + raise NotImplementedError("Nested blend lists not supported (yet)") + assert len(values) <= numValues + return values + [0] * (numValues - len(values)) + + +def numBlendArgValues(arg): + """Return the number of default and delta values in a generalized argument.""" + return len(arg) - 1 if isinstance(arg, list) else 1 + + +def _blendArgsEqual(arg1, arg2): + numValues = max(numBlendArgValues(arg1), numBlendArgValues(arg2)) + return blendArgValues(arg1, numValues) == blendArgValues(arg2, numValues) + + +def _negateBlendArg(arg): + if isinstance(arg, list): + return [-v for v in arg[:-1]] + arg[-1:] + return -arg + + +def _sumBlendArgs(args): + return functools.reduce(_addArgs, args) + + +def _isFlex1Horizontal(dx, dy, scalarRanges=None): + """Whether a flex1 ends horizontally throughout the scalar ranges. + + ``scalarRanges`` contains a ``(minimum, maximum)`` pair per variation + region and defaults to ``(0, 1)`` for each region. Return ``None`` when + the direction may change. + """ + + def bounds(arg): + default, *deltas = blendArgValues(arg, numBlendArgValues(arg)) + ranges = scalarRanges or [(0, 1)] * len(deltas) + lo = hi = default + for delta, (minScalar, maxScalar) in zip(deltas, ranges): + lo += min(minScalar * delta, maxScalar * delta) + hi += max(minScalar * delta, maxScalar * delta) + return lo, hi + + def minAbs(lo, hi): + return 0 if lo <= 0 <= hi else min(abs(lo), abs(hi)) + + def maxAbs(lo, hi): + return max(abs(lo), abs(hi)) + + dxBounds, dyBounds = bounds(dx), bounds(dy) + if minAbs(*dxBounds) > maxAbs(*dyBounds): + return True + if maxAbs(*dxBounds) <= minAbs(*dyBounds): + return False + return None + + +def _specializeFlex(args): + """Return the shortest flex operator and arguments for ``flex`` args.""" + dx1, dy1, dx2, dy2, dx3, dy3, dx4, dy4, dx5, dy5, dx6, dy6, fd = args + if not _blendArgsEqual(fd, 50): + return "flex", args + if _blendArgsEqual(dy3, 0) and _blendArgsEqual(dy4, 0): + if ( + _blendArgsEqual(dy1, 0) + and _blendArgsEqual(dy6, 0) + and _blendArgsEqual(dy5, _negateBlendArg(dy2)) + ): + return "hflex", [dx1, dx2, dy2, dx3, dx4, dx5, dx6] + if _blendArgsEqual(dy6, _negateBlendArg(_sumBlendArgs([dy1, dy2, dy5]))): + return "hflex1", [dx1, dy1, dx2, dy2, dx3, dx4, dx5, dy5, dx6] + dx = _sumBlendArgs(args[0:10:2]) + dy = _sumBlendArgs(args[1:10:2]) + horizontal = _isFlex1Horizontal(dx, dy) + if horizontal and _blendArgsEqual(dy6, _negateBlendArg(dy)): + return "flex1", args[0:10] + [dx6] + if horizontal is False and _blendArgsEqual(dx6, _negateBlendArg(dx)): + return "flex1", args[0:10] + [dy6] + return "flex", args + + +def _argsStackUse(args): + stackLen = 0 + maxLen = 0 + for arg in args: + if type(arg) is list: + # Blended arg + maxLen = max(maxLen, stackLen + _argsStackUse(arg)) + stackLen += arg[-1] + else: + stackLen += 1 + return max(stackLen, maxLen) + + +def specializeCommands( + commands, + ignoreErrors=False, + generalizeFirst=True, + preserveTopology=False, + maxstack=48, + getScalarRanges=None, + vsIndex=0, +): + # We perform several rounds of optimizations. They are carefully ordered and are: + # + # 0. Generalize commands. + # This ensures that they are in our expected simple form, with each line/curve only + # having arguments for one segment, and using the generic form (rlineto/rrcurveto). + # If caller is sure the input is in this form, they can turn off generalization to + # save time. + # + # 1. Combine successive rmoveto operations. + # + # 2. Specialize rmoveto/rlineto/rrcurveto into horizontal/vertical variants, + # and flex into hflex/hflex1/flex1. + # We specialize into some, made-up, variants as well, which simplifies following + # passes. + # + # 3. Merge or delete redundant operations, to the extent requested. + # OpenType spec declares point numbers in CFF undefined. As such, we happily + # change topology. If client relies on point numbers (in GPOS anchors, or for + # hinting purposes(what?)) they can turn this off. + # + # 4. Peephole optimization to revert back some of the h/v variants back into their + # original "relative" operator (rline/rrcurveto) if that saves a byte. + # + # 5. Combine adjacent operators when possible, minding not to go over max stack size. + # + # 6. Resolve any remaining made-up operators into real operators. + # + # I have convinced myself that this produces optimal bytecode (except for, possibly + # one byte each time maxstack size prohibits combining.) YMMV, but you'd be wrong. :-) + # A dynamic-programming approach can do the same but would be significantly slower. + # + # 7. For any args which are blend lists, convert them to a blend command. + + # 0. Generalize commands. + if generalizeFirst: + commands = generalizeCommands( + commands, + ignoreErrors=ignoreErrors, + getScalarRanges=getScalarRanges, + vsIndex=vsIndex, + ) + else: + commands = list(commands) # Make copy since we modify in-place later. + + # 1. Combine successive rmoveto operations. + for i in range(len(commands) - 1, 0, -1): + if "rmoveto" == commands[i][0] == commands[i - 1][0]: + v1, v2 = commands[i - 1][1], commands[i][1] + commands[i - 1] = ( + "rmoveto", + [_addArgs(v1[0], v2[0]), _addArgs(v1[1], v2[1])], + ) + del commands[i] + + # 2. Specialize rmoveto/rlineto/rrcurveto into horizontal/vertical variants, + # and flex into hflex/hflex1/flex1. + # + # We, in fact, specialize into more, made-up, variants that special-case when both + # X and Y components are zero. This simplifies the following optimization passes. + # This case is rare, but OCD does not let me skip it. + # + # After this round, we will have four variants that use the following mnemonics: + # + # - 'r' for relative, ie. non-zero X and non-zero Y, + # - 'h' for horizontal, ie. zero X and non-zero Y, + # - 'v' for vertical, ie. non-zero X and zero Y, + # - '0' for zeros, ie. zero X and zero Y. + # + # The '0' pseudo-operators are not part of the spec, but help simplify the following + # optimization rounds. We resolve them at the end. So, after this, we will have four + # moveto and four lineto variants: + # + # - 0moveto, 0lineto + # - hmoveto, hlineto + # - vmoveto, vlineto + # - rmoveto, rlineto + # + # and sixteen curveto variants. For example, a '0hcurveto' operator means a curve + # dx0,dy0,dx1,dy1,dx2,dy2,dx3,dy3 where dx0, dx1, and dy3 are zero but not dx3. + # An 'rvcurveto' means dx3 is zero but not dx0,dy0,dy3. + # + # There are nine different variants of curves without the '0'. Those nine map exactly + # to the existing curve variants in the spec: rrcurveto, and the four variants hhcurveto, + # vvcurveto, hvcurveto, and vhcurveto each cover two cases, one with an odd number of + # arguments and one without. Eg. an hhcurveto with an extra argument (odd number of + # arguments) is in fact an rhcurveto. The operators in the spec are designed such that + # all four of rhcurveto, rvcurveto, hrcurveto, and vrcurveto are encodable for one curve. + # + # Of the curve types with '0', the 00curveto is equivalent to a lineto variant. The rest + # of the curve types with a 0 need to be encoded as a h or v variant. Ie. a '0' can be + # thought of a "don't care" and can be used as either an 'h' or a 'v'. As such, we always + # encode a number 0 as argument when we use a '0' variant. Later on, we can just substitute + # the '0' with either 'h' or 'v' and it works. + # + # When we get to curve splines however, things become more complicated... XXX finish this. + # There's one more complexity with splines. If one side of the spline is not horizontal or + # vertical (or zero), ie. if it's 'r', then it limits which spline types we can encode. + # Only hhcurveto and vvcurveto operators can encode a spline starting with 'r', and + # only hvcurveto and vhcurveto operators can encode a spline ending with 'r'. + # This limits our merge opportunities later. + # + for i in range(len(commands)): + op, args = commands[i] + + if op in {"rmoveto", "rlineto"}: + c, args = _categorizeVector(args) + commands[i] = c + op[1:], args + continue + + if op == "rrcurveto": + c1, args1 = _categorizeVector(args[:2]) + c2, args2 = _categorizeVector(args[-2:]) + commands[i] = c1 + c2 + "curveto", args1 + args[2:4] + args2 + continue + + if op == "flex": + commands[i] = _specializeFlex(args) + continue + + # 3. Merge or delete redundant operations, to the extent requested. + # + # TODO + # A 0moveto that comes before all other path operations can be removed. + # though I find conflicting evidence for this. + # + # TODO + # "If hstem and vstem hints are both declared at the beginning of a + # CharString, and this sequence is followed directly by the hintmask or + # cntrmask operators, then the vstem hint operator (or, if applicable, + # the vstemhm operator) need not be included." + # + # "The sequence and form of a CFF2 CharString program may be represented as: + # {hs* vs* cm* hm* mt subpath}? {mt subpath}*" + # + # https://www.microsoft.com/typography/otspec/cff2charstr.htm#section3.1 + # + # For Type2 CharStrings the sequence is: + # w? {hs* vs* cm* hm* mt subpath}? {mt subpath}* endchar" + + # Some other redundancies change topology (point numbers). + if not preserveTopology: + for i in range(len(commands) - 1, -1, -1): + op, args = commands[i] + + # A 00curveto is demoted to a (specialized) lineto. + if op == "00curveto": + assert len(args) == 4 + c, args = _categorizeVector(args[1:3]) + op = c + "lineto" + commands[i] = op, args + # and then... + + # A 0lineto can be deleted. + if op == "0lineto": + del commands[i] + continue + + # Merge adjacent hlineto's and vlineto's. + # In CFF2 charstrings from variable fonts, each + # arg item may be a list of blendable values, one from + # each source font. + if i and op in {"hlineto", "vlineto"} and (op == commands[i - 1][0]): + _, other_args = commands[i - 1] + assert len(args) == 1 and len(other_args) == 1 + try: + new_args = [_addArgs(args[0], other_args[0])] + except ValueError: + continue + commands[i - 1] = (op, new_args) + del commands[i] + continue + + # 4. Peephole optimization to revert back some of the h/v variants back into their + # original "relative" operator (rline/rrcurveto) if that saves a byte. + for i in range(1, len(commands) - 1): + op, args = commands[i] + prv, nxt = commands[i - 1][0], commands[i + 1][0] + + if op in {"0lineto", "hlineto", "vlineto"} and prv == nxt == "rlineto": + assert len(args) == 1 + args = [0, args[0]] if op[0] == "v" else [args[0], 0] + commands[i] = ("rlineto", args) + continue + + if op[2:] == "curveto" and len(args) == 5 and prv == nxt == "rrcurveto": + assert (op[0] == "r") ^ (op[1] == "r") + if op[0] == "v": + pos = 0 + elif op[0] != "r": + pos = 1 + elif op[1] == "v": + pos = 4 + else: + pos = 5 + # Insert, while maintaining the type of args (can be tuple or list). + args = args[:pos] + type(args)((0,)) + args[pos:] + commands[i] = ("rrcurveto", args) + continue + + # 5. Combine adjacent operators when possible, minding not to go over max stack size. + stackUse = _argsStackUse(commands[-1][1]) if commands else 0 + for i in range(len(commands) - 1, 0, -1): + op1, args1 = commands[i - 1] + op2, args2 = commands[i] + new_op = None + + # Merge logic... + if {op1, op2} <= {"rlineto", "rrcurveto"}: + if op1 == op2: + new_op = op1 + else: + l = len(args2) + if op2 == "rrcurveto" and l == 6: + new_op = "rlinecurve" + elif l == 2: + new_op = "rcurveline" + + elif (op1, op2) in {("rlineto", "rlinecurve"), ("rrcurveto", "rcurveline")}: + new_op = op2 + + elif {op1, op2} == {"vlineto", "hlineto"}: + new_op = op1 + + elif "curveto" == op1[2:] == op2[2:]: + d0, d1 = op1[:2] + d2, d3 = op2[:2] + + if d1 == "r" or d2 == "r" or d0 == d3 == "r": + continue + + d = _mergeCategories(d1, d2) + if d is None: + continue + if d0 == "r": + d = _mergeCategories(d, d3) + if d is None: + continue + new_op = "r" + d + "curveto" + elif d3 == "r": + d0 = _mergeCategories(d0, _negateCategory(d)) + if d0 is None: + continue + new_op = d0 + "r" + "curveto" + else: + d0 = _mergeCategories(d0, d3) + if d0 is None: + continue + new_op = d0 + d + "curveto" + + # Make sure the stack depth does not exceed (maxstack - 1), so + # that subroutinizer can insert subroutine calls at any point. + args1StackUse = _argsStackUse(args1) + combinedStackUse = max(args1StackUse, len(args1) + stackUse) + if new_op and combinedStackUse < maxstack: + commands[i - 1] = (new_op, args1 + args2) + del commands[i] + stackUse = combinedStackUse + else: + stackUse = args1StackUse + + # 6. Resolve any remaining made-up operators into real operators. + for i in range(len(commands)): + op, args = commands[i] + + if op in {"0moveto", "0lineto"}: + commands[i] = "h" + op[1:], args + continue + + if op[2:] == "curveto" and op[:2] not in {"rr", "hh", "vv", "vh", "hv"}: + l = len(args) + + op0, op1 = op[:2] + if (op0 == "r") ^ (op1 == "r"): + assert l % 2 == 1 + if op0 == "0": + op0 = "h" + if op1 == "0": + op1 = "h" + if op0 == "r": + op0 = op1 + if op1 == "r": + op1 = _negateCategory(op0) + assert {op0, op1} <= {"h", "v"}, (op0, op1) + + if l % 2: + if op0 != op1: # vhcurveto / hvcurveto + if (op0 == "h") ^ (l % 8 == 1): + # Swap last two args order + args = args[:-2] + args[-1:] + args[-2:-1] + else: # hhcurveto / vvcurveto + if op0 == "h": # hhcurveto + # Swap first two args order + args = args[1:2] + args[:1] + args[2:] + + commands[i] = op0 + op1 + "curveto", args + continue + + # 7. For any series of args which are blend lists, convert the series to a single blend arg. + for i in range(len(commands)): + op, args = commands[i] + if any(isinstance(arg, list) for arg in args): + commands[i] = op, _convertToBlendCmds(args) + + return commands + + +def specializeProgram(program, getNumRegions=None, vsIndex=0, **kwargs): + return commandsToProgram( + specializeCommands( + programToCommands(program, getNumRegions, vsIndex=vsIndex), + vsIndex=vsIndex, + **kwargs, + ) + ) + + +if __name__ == "__main__": + import sys + + if len(sys.argv) == 1: + import doctest + + sys.exit(doctest.testmod().failed) + + import argparse + + parser = argparse.ArgumentParser( + "fonttools cffLib.specializer", + description="CFF CharString generalizer/specializer", + ) + parser.add_argument("program", metavar="command", nargs="*", help="Commands.") + parser.add_argument( + "--num-regions", + metavar="NumRegions", + nargs="*", + default=None, + help="Number of variable-font regions for blend opertaions.", + ) + parser.add_argument( + "--font", + metavar="FONTFILE", + default=None, + help="CFF2 font to specialize.", + ) + parser.add_argument( + "-o", + "--output-file", + type=str, + help="Output font file name.", + ) + + options = parser.parse_args(sys.argv[1:]) + + if options.program: + getNumRegions = ( + None + if options.num_regions is None + else lambda vsIndex: int( + options.num_regions[0 if vsIndex is None else vsIndex] + ) + ) + + program = stringToProgram(options.program) + print("Program:") + print(programToString(program)) + commands = programToCommands(program, getNumRegions) + print("Commands:") + print(commands) + program2 = commandsToProgram(commands) + print("Program from commands:") + print(programToString(program2)) + assert program == program2 + print("Generalized program:") + print(programToString(generalizeProgram(program, getNumRegions))) + print("Specialized program:") + print(programToString(specializeProgram(program, getNumRegions))) + + if options.font: + from fontTools.ttLib import TTFont + + font = TTFont(options.font) + cff2 = font["CFF2"].cff.topDictIndex[0] + charstrings = cff2.CharStrings + for glyphName in charstrings.keys(): + charstring = charstrings[glyphName] + charstring.decompile() + getNumRegions = charstring.private.getNumRegions + charstring.program = specializeProgram( + charstring.program, + getNumRegions, + vsIndex=getattr(charstring.private, "vsindex", 0), + maxstack=maxStackLimit, + ) + + if options.output_file is None: + from fontTools.misc.cliTools import makeOutputFileName + + outfile = makeOutputFileName( + options.font, overWrite=True, suffix=".specialized" + ) + else: + outfile = options.output_file + if outfile: + print("Saving", outfile) + font.save(outfile) diff --git a/hug/lib/python3.12/site-packages/fontTools/cffLib/transforms.py b/hug/lib/python3.12/site-packages/fontTools/cffLib/transforms.py new file mode 100644 index 0000000000000000000000000000000000000000..b9b7c86c8b450d8a599780b1af53004afa9004d7 --- /dev/null +++ b/hug/lib/python3.12/site-packages/fontTools/cffLib/transforms.py @@ -0,0 +1,495 @@ +from fontTools.misc.psCharStrings import ( + SimpleT2Decompiler, + T2WidthExtractor, + calcSubrBias, +) + + +def _uniq_sort(l): + return sorted(set(l)) + + +class StopHintCountEvent(Exception): + pass + + +class _DesubroutinizingT2Decompiler(SimpleT2Decompiler): + stop_hintcount_ops = ( + "op_hintmask", + "op_cntrmask", + "op_rmoveto", + "op_hmoveto", + "op_vmoveto", + ) + + def __init__(self, localSubrs, globalSubrs, private=None): + SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs, private) + + def execute(self, charString): + self.need_hintcount = True # until proven otherwise + for op_name in self.stop_hintcount_ops: + setattr(self, op_name, self.stop_hint_count) + + if hasattr(charString, "_desubroutinized"): + # If a charstring has already been desubroutinized, we will still + # need to execute it if we need to count hints in order to + # compute the byte length for mask arguments, and haven't finished + # counting hints pairs. + if self.need_hintcount and self.callingStack: + try: + SimpleT2Decompiler.execute(self, charString) + except StopHintCountEvent: + del self.callingStack[-1] + return + + charString._patches = [] + SimpleT2Decompiler.execute(self, charString) + desubroutinized = charString.program[:] + for idx, expansion in reversed(charString._patches): + assert idx >= 2 + assert desubroutinized[idx - 1] in [ + "callsubr", + "callgsubr", + ], desubroutinized[idx - 1] + assert type(desubroutinized[idx - 2]) == int + if expansion[-1] == "return": + expansion = expansion[:-1] + desubroutinized[idx - 2 : idx] = expansion + if not self.private.in_cff2: + if "endchar" in desubroutinized: + # Cut off after first endchar + desubroutinized = desubroutinized[ + : desubroutinized.index("endchar") + 1 + ] + + charString._desubroutinized = desubroutinized + del charString._patches + + def op_callsubr(self, index): + subr = self.localSubrs[self.operandStack[-1] + self.localBias] + SimpleT2Decompiler.op_callsubr(self, index) + self.processSubr(index, subr) + + def op_callgsubr(self, index): + subr = self.globalSubrs[self.operandStack[-1] + self.globalBias] + SimpleT2Decompiler.op_callgsubr(self, index) + self.processSubr(index, subr) + + def stop_hint_count(self, *args): + self.need_hintcount = False + for op_name in self.stop_hintcount_ops: + setattr(self, op_name, None) + cs = self.callingStack[-1] + if hasattr(cs, "_desubroutinized"): + raise StopHintCountEvent() + + def op_hintmask(self, index): + SimpleT2Decompiler.op_hintmask(self, index) + if self.need_hintcount: + self.stop_hint_count() + + def processSubr(self, index, subr): + cs = self.callingStack[-1] + if not hasattr(cs, "_desubroutinized"): + cs._patches.append((index, subr._desubroutinized)) + + +def desubroutinizeCharString(cs): + """Desubroutinize a charstring in-place.""" + cs.decompile() + subrs = getattr(cs.private, "Subrs", []) + decompiler = _DesubroutinizingT2Decompiler(subrs, cs.globalSubrs, cs.private) + decompiler.execute(cs) + cs.program = cs._desubroutinized + del cs._desubroutinized + + +def desubroutinize(cff): + for fontName in cff.fontNames: + font = cff[fontName] + cs = font.CharStrings + for c in cs.values(): + desubroutinizeCharString(c) + # Delete all the local subrs + if hasattr(font, "FDArray"): + for fd in font.FDArray: + pd = fd.Private + if hasattr(pd, "Subrs"): + del pd.Subrs + if "Subrs" in pd.rawDict: + del pd.rawDict["Subrs"] + else: + pd = font.Private + if hasattr(pd, "Subrs"): + del pd.Subrs + if "Subrs" in pd.rawDict: + del pd.rawDict["Subrs"] + # as well as the global subrs + cff.GlobalSubrs.clear() + + +class _MarkingT2Decompiler(SimpleT2Decompiler): + def __init__(self, localSubrs, globalSubrs, private): + SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs, private) + for subrs in [localSubrs, globalSubrs]: + if subrs and not hasattr(subrs, "_used"): + subrs._used = set() + + def op_callsubr(self, index): + self.localSubrs._used.add(self.operandStack[-1] + self.localBias) + SimpleT2Decompiler.op_callsubr(self, index) + + def op_callgsubr(self, index): + self.globalSubrs._used.add(self.operandStack[-1] + self.globalBias) + SimpleT2Decompiler.op_callgsubr(self, index) + + +class _DehintingT2Decompiler(T2WidthExtractor): + class Hints(object): + def __init__(self): + # Whether calling this charstring produces any hint stems + # Note that if a charstring starts with hintmask, it will + # have has_hint set to True, because it *might* produce an + # implicit vstem if called under certain conditions. + self.has_hint = False + # Index to start at to drop all hints + self.last_hint = 0 + # Index up to which we know more hints are possible. + # Only relevant if status is 0 or 1. + self.last_checked = 0 + # The status means: + # 0: after dropping hints, this charstring is empty + # 1: after dropping hints, there may be more hints + # continuing after this, or there might be + # other things. Not clear yet. + # 2: no more hints possible after this charstring + self.status = 0 + # Has hintmask instructions; not recursive + self.has_hintmask = False + # List of indices of calls to empty subroutines to remove. + self.deletions = [] + + pass + + def __init__( + self, css, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private=None + ): + self._css = css + T2WidthExtractor.__init__( + self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX + ) + self.private = private + + def execute(self, charString): + old_hints = charString._hints if hasattr(charString, "_hints") else None + charString._hints = self.Hints() + + T2WidthExtractor.execute(self, charString) + + hints = charString._hints + + if hints.has_hint or hints.has_hintmask: + self._css.add(charString) + + if hints.status != 2: + # Check from last_check, make sure we didn't have any operators. + for i in range(hints.last_checked, len(charString.program) - 1): + if isinstance(charString.program[i], str): + hints.status = 2 + break + else: + hints.status = 1 # There's *something* here + hints.last_checked = len(charString.program) + + if old_hints: + assert hints.__dict__ == old_hints.__dict__ + + def op_callsubr(self, index): + subr = self.localSubrs[self.operandStack[-1] + self.localBias] + T2WidthExtractor.op_callsubr(self, index) + self.processSubr(index, subr) + + def op_callgsubr(self, index): + subr = self.globalSubrs[self.operandStack[-1] + self.globalBias] + T2WidthExtractor.op_callgsubr(self, index) + self.processSubr(index, subr) + + def op_hstem(self, index): + T2WidthExtractor.op_hstem(self, index) + self.processHint(index) + + def op_vstem(self, index): + T2WidthExtractor.op_vstem(self, index) + self.processHint(index) + + def op_hstemhm(self, index): + T2WidthExtractor.op_hstemhm(self, index) + self.processHint(index) + + def op_vstemhm(self, index): + T2WidthExtractor.op_vstemhm(self, index) + self.processHint(index) + + def op_hintmask(self, index): + rv = T2WidthExtractor.op_hintmask(self, index) + self.processHintmask(index) + return rv + + def op_cntrmask(self, index): + rv = T2WidthExtractor.op_cntrmask(self, index) + self.processHintmask(index) + return rv + + def processHintmask(self, index): + cs = self.callingStack[-1] + hints = cs._hints + hints.has_hintmask = True + if hints.status != 2: + # Check from last_check, see if we may be an implicit vstem + for i in range(hints.last_checked, index - 1): + if isinstance(cs.program[i], str): + hints.status = 2 + break + else: + # We are an implicit vstem + hints.has_hint = True + hints.last_hint = index + 1 + hints.status = 0 + hints.last_checked = index + 1 + + def processHint(self, index): + cs = self.callingStack[-1] + hints = cs._hints + hints.has_hint = True + hints.last_hint = index + hints.last_checked = index + + def processSubr(self, index, subr): + cs = self.callingStack[-1] + hints = cs._hints + subr_hints = subr._hints + + # Check from last_check, make sure we didn't have + # any operators. + if hints.status != 2: + for i in range(hints.last_checked, index - 1): + if isinstance(cs.program[i], str): + hints.status = 2 + break + hints.last_checked = index + + if hints.status != 2: + if subr_hints.has_hint: + hints.has_hint = True + + # Decide where to chop off from + if subr_hints.status == 0: + hints.last_hint = index + else: + hints.last_hint = index - 2 # Leave the subr call in + + elif subr_hints.status == 0: + hints.deletions.append(index) + + hints.status = max(hints.status, subr_hints.status) + + +def _cs_subset_subroutines(charstring, subrs, gsubrs): + p = charstring.program + for i in range(1, len(p)): + if p[i] == "callsubr": + assert isinstance(p[i - 1], int) + p[i - 1] = subrs._used.index(p[i - 1] + subrs._old_bias) - subrs._new_bias + elif p[i] == "callgsubr": + assert isinstance(p[i - 1], int) + p[i - 1] = ( + gsubrs._used.index(p[i - 1] + gsubrs._old_bias) - gsubrs._new_bias + ) + + +def _cs_drop_hints(charstring): + hints = charstring._hints + + if hints.deletions: + p = charstring.program + for idx in reversed(hints.deletions): + del p[idx - 2 : idx] + + if hints.has_hint: + assert not hints.deletions or hints.last_hint <= hints.deletions[0] + charstring.program = charstring.program[hints.last_hint :] + if not charstring.program: + # TODO CFF2 no need for endchar. + charstring.program.append("endchar") + if hasattr(charstring, "width"): + # Insert width back if needed + if charstring.width != charstring.private.defaultWidthX: + # For CFF2 charstrings, this should never happen + assert ( + charstring.private.defaultWidthX is not None + ), "CFF2 CharStrings must not have an initial width value" + charstring.program.insert( + 0, charstring.width - charstring.private.nominalWidthX + ) + + if hints.has_hintmask: + i = 0 + p = charstring.program + while i < len(p): + if p[i] in ["hintmask", "cntrmask"]: + assert i + 1 <= len(p) + del p[i : i + 2] + continue + i += 1 + + assert len(charstring.program) + + del charstring._hints + + +def remove_hints(cff, *, removeUnusedSubrs: bool = True): + for fontname in cff.keys(): + font = cff[fontname] + cs = font.CharStrings + # This can be tricky, but doesn't have to. What we do is: + # + # - Run all used glyph charstrings and recurse into subroutines, + # - For each charstring (including subroutines), if it has any + # of the hint stem operators, we mark it as such. + # Upon returning, for each charstring we note all the + # subroutine calls it makes that (recursively) contain a stem, + # - Dropping hinting then consists of the following two ops: + # * Drop the piece of the program in each charstring before the + # last call to a stem op or a stem-calling subroutine, + # * Drop all hintmask operations. + # - It's trickier... A hintmask right after hints and a few numbers + # will act as an implicit vstemhm. As such, we track whether + # we have seen any non-hint operators so far and do the right + # thing, recursively... Good luck understanding that :( + css = set() + for c in cs.values(): + c.decompile() + subrs = getattr(c.private, "Subrs", []) + decompiler = _DehintingT2Decompiler( + css, + subrs, + c.globalSubrs, + c.private.nominalWidthX, + c.private.defaultWidthX, + c.private, + ) + decompiler.execute(c) + c.width = decompiler.width + for charstring in css: + _cs_drop_hints(charstring) + del css + + # Drop font-wide hinting values + all_privs = [] + if hasattr(font, "FDArray"): + all_privs.extend(fd.Private for fd in font.FDArray) + else: + all_privs.append(font.Private) + for priv in all_privs: + for k in [ + "BlueValues", + "OtherBlues", + "FamilyBlues", + "FamilyOtherBlues", + "BlueScale", + "BlueShift", + "BlueFuzz", + "StemSnapH", + "StemSnapV", + "StdHW", + "StdVW", + "ForceBold", + "LanguageGroup", + "ExpansionFactor", + ]: + if hasattr(priv, k): + setattr(priv, k, None) + if removeUnusedSubrs: + remove_unused_subroutines(cff) + + +def _pd_delete_empty_subrs(private_dict): + if hasattr(private_dict, "Subrs") and not private_dict.Subrs: + if "Subrs" in private_dict.rawDict: + del private_dict.rawDict["Subrs"] + del private_dict.Subrs + + +def remove_unused_subroutines(cff): + for fontname in cff.keys(): + font = cff[fontname] + cs = font.CharStrings + # Renumber subroutines to remove unused ones + + # Mark all used subroutines + for c in cs.values(): + subrs = getattr(c.private, "Subrs", []) + decompiler = _MarkingT2Decompiler(subrs, c.globalSubrs, c.private) + decompiler.execute(c) + + all_subrs = [font.GlobalSubrs] + if hasattr(font, "FDArray"): + all_subrs.extend( + fd.Private.Subrs + for fd in font.FDArray + if hasattr(fd.Private, "Subrs") and fd.Private.Subrs + ) + elif hasattr(font.Private, "Subrs") and font.Private.Subrs: + all_subrs.append(font.Private.Subrs) + + subrs = set(subrs) # Remove duplicates + + # Prepare + for subrs in all_subrs: + if not hasattr(subrs, "_used"): + subrs._used = set() + subrs._used = _uniq_sort(subrs._used) + subrs._old_bias = calcSubrBias(subrs) + subrs._new_bias = calcSubrBias(subrs._used) + + # Renumber glyph charstrings + for c in cs.values(): + subrs = getattr(c.private, "Subrs", None) + _cs_subset_subroutines(c, subrs, font.GlobalSubrs) + + # Renumber subroutines themselves + for subrs in all_subrs: + if subrs == font.GlobalSubrs: + if not hasattr(font, "FDArray") and hasattr(font.Private, "Subrs"): + local_subrs = font.Private.Subrs + elif ( + hasattr(font, "FDArray") + and len(font.FDArray) == 1 + and hasattr(font.FDArray[0].Private, "Subrs") + ): + # Technically we shouldn't do this. But I've run into fonts that do it. + local_subrs = font.FDArray[0].Private.Subrs + else: + local_subrs = None + else: + local_subrs = subrs + + subrs.items = [subrs.items[i] for i in subrs._used] + if hasattr(subrs, "file"): + del subrs.file + if hasattr(subrs, "offsets"): + del subrs.offsets + + for subr in subrs.items: + _cs_subset_subroutines(subr, local_subrs, font.GlobalSubrs) + + # Delete local SubrsIndex if empty + if hasattr(font, "FDArray"): + for fd in font.FDArray: + _pd_delete_empty_subrs(fd.Private) + else: + _pd_delete_empty_subrs(font.Private) + + # Cleanup + for subrs in all_subrs: + del subrs._used, subrs._old_bias, subrs._new_bias diff --git a/hug/lib/python3.12/site-packages/fontTools/cffLib/width.py b/hug/lib/python3.12/site-packages/fontTools/cffLib/width.py new file mode 100644 index 0000000000000000000000000000000000000000..fa1aa3f23a8b3a634a9d5ff0ab2e6df558361c64 --- /dev/null +++ b/hug/lib/python3.12/site-packages/fontTools/cffLib/width.py @@ -0,0 +1,213 @@ +# -*- coding: utf-8 -*- + +"""T2CharString glyph width optimizer. + +CFF glyphs whose width equals the CFF Private dictionary's ``defaultWidthX`` +value do not need to specify their width in their charstring, saving bytes. +This module determines the optimum ``defaultWidthX`` and ``nominalWidthX`` +values for a font, when provided with a list of glyph widths.""" + +from fontTools.ttLib import TTFont +from collections import defaultdict +from operator import add +from functools import reduce + +__all__ = ["optimizeWidths", "main"] + + +def _get_hmtx(font): + return font["HMTX"] if "HMTX" in font else font["hmtx"] + + +class missingdict(dict): + def __init__(self, missing_func): + self.missing_func = missing_func + + def __missing__(self, v): + return self.missing_func(v) + + +def cumSum(f, op=add, start=0, decreasing=False): + keys = sorted(f.keys()) + minx, maxx = keys[0], keys[-1] + + total = reduce(op, f.values(), start) + + if decreasing: + missing = lambda x: start if x > maxx else total + domain = range(maxx, minx - 1, -1) + else: + missing = lambda x: start if x < minx else total + domain = range(minx, maxx + 1) + + out = missingdict(missing) + + v = start + for x in domain: + v = op(v, f[x]) + out[x] = v + + return out + + +def byteCost(widths, default, nominal): + if not hasattr(widths, "items"): + d = defaultdict(int) + for w in widths: + d[w] += 1 + widths = d + + cost = 0 + for w, freq in widths.items(): + if w == default: + continue + diff = abs(w - nominal) + if diff <= 107: + cost += freq + elif diff <= 1131: + cost += freq * 2 + else: + cost += freq * 5 + return cost + + +def optimizeWidthsBruteforce(widths): + """Bruteforce version. Veeeeeeeeeeeeeeeeery slow. Only works for smallests of fonts.""" + + d = defaultdict(int) + for w in widths: + d[w] += 1 + + # Maximum number of bytes using default can possibly save + maxDefaultAdvantage = 5 * max(d.values()) + + minw, maxw = min(widths), max(widths) + domain = list(range(minw, maxw + 1)) + + bestCostWithoutDefault = min(byteCost(widths, None, nominal) for nominal in domain) + + bestCost = len(widths) * 5 + 1 + for nominal in domain: + if byteCost(widths, None, nominal) > bestCost + maxDefaultAdvantage: + continue + for default in domain: + cost = byteCost(widths, default, nominal) + if cost < bestCost: + bestCost = cost + bestDefault = default + bestNominal = nominal + + return bestDefault, bestNominal + + +def optimizeWidths(widths): + """Given a list of glyph widths, or dictionary mapping glyph width to number of + glyphs having that, returns a tuple of best CFF default and nominal glyph widths. + + This algorithm is linear in UPEM+numGlyphs.""" + + if not hasattr(widths, "items"): + d = defaultdict(int) + for w in widths: + d[w] += 1 + widths = d + + keys = sorted(widths.keys()) + minw, maxw = keys[0], keys[-1] + domain = list(range(minw, maxw + 1)) + + # Cumulative sum/max forward/backward. + cumFrqU = cumSum(widths, op=add) + cumMaxU = cumSum(widths, op=max) + cumFrqD = cumSum(widths, op=add, decreasing=True) + cumMaxD = cumSum(widths, op=max, decreasing=True) + + # Cost per nominal choice, without default consideration. + nomnCostU = missingdict( + lambda x: cumFrqU[x] + cumFrqU[x - 108] + cumFrqU[x - 1132] * 3 + ) + nomnCostD = missingdict( + lambda x: cumFrqD[x] + cumFrqD[x + 108] + cumFrqD[x + 1132] * 3 + ) + nomnCost = missingdict(lambda x: nomnCostU[x] + nomnCostD[x] - widths[x]) + + # Cost-saving per nominal choice, by best default choice. + dfltCostU = missingdict( + lambda x: max(cumMaxU[x], cumMaxU[x - 108] * 2, cumMaxU[x - 1132] * 5) + ) + dfltCostD = missingdict( + lambda x: max(cumMaxD[x], cumMaxD[x + 108] * 2, cumMaxD[x + 1132] * 5) + ) + dfltCost = missingdict(lambda x: max(dfltCostU[x], dfltCostD[x])) + + # Combined cost per nominal choice. + bestCost = missingdict(lambda x: nomnCost[x] - dfltCost[x]) + + # Best nominal. + nominal = min(domain, key=lambda x: bestCost[x]) + + # Work back the best default. + bestC = bestCost[nominal] + dfltC = nomnCost[nominal] - bestCost[nominal] + ends = [] + if dfltC == dfltCostU[nominal]: + starts = [nominal, nominal - 108, nominal - 1132] + for start in starts: + while cumMaxU[start] and cumMaxU[start] == cumMaxU[start - 1]: + start -= 1 + ends.append(start) + else: + starts = [nominal, nominal + 108, nominal + 1132] + for start in starts: + while cumMaxD[start] and cumMaxD[start] == cumMaxD[start + 1]: + start += 1 + ends.append(start) + default = min(ends, key=lambda default: byteCost(widths, default, nominal)) + + return default, nominal + + +def main(args=None): + """Calculate optimum defaultWidthX/nominalWidthX values""" + + import argparse + + parser = argparse.ArgumentParser( + "fonttools cffLib.width", + description=main.__doc__, + ) + parser.add_argument( + "inputs", metavar="FILE", type=str, nargs="+", help="Input TTF files" + ) + parser.add_argument( + "-b", + "--brute-force", + dest="brute", + action="store_true", + help="Use brute-force approach (VERY slow)", + ) + + args = parser.parse_args(args) + + for fontfile in args.inputs: + font = TTFont(fontfile) + hmtx = _get_hmtx(font) + widths = [m[0] for m in hmtx.metrics.values()] + if args.brute: + default, nominal = optimizeWidthsBruteforce(widths) + else: + default, nominal = optimizeWidths(widths) + print( + "glyphs=%d default=%d nominal=%d byteCost=%d" + % (len(widths), default, nominal, byteCost(widths, default, nominal)) + ) + + +if __name__ == "__main__": + import sys + + if len(sys.argv) == 1: + import doctest + + sys.exit(doctest.testmod().failed) + main() diff --git a/hug/lib/python3.12/site-packages/fontTools/colorLib/__init__.py b/hug/lib/python3.12/site-packages/fontTools/colorLib/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/hug/lib/python3.12/site-packages/fontTools/colorLib/__pycache__/__init__.cpython-312.pyc b/hug/lib/python3.12/site-packages/fontTools/colorLib/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..71ea6559561f6adae81c4d9d6a289514a38d4845 Binary files /dev/null and b/hug/lib/python3.12/site-packages/fontTools/colorLib/__pycache__/__init__.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/fontTools/colorLib/__pycache__/builder.cpython-312.pyc b/hug/lib/python3.12/site-packages/fontTools/colorLib/__pycache__/builder.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d3d76e0df1dd576c745bccccd22d2006f1e263ec Binary files /dev/null and b/hug/lib/python3.12/site-packages/fontTools/colorLib/__pycache__/builder.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/fontTools/colorLib/__pycache__/errors.cpython-312.pyc b/hug/lib/python3.12/site-packages/fontTools/colorLib/__pycache__/errors.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3ec26380cfa7c7ce7e93cfb9c0022f9a6d350f55 Binary files /dev/null and b/hug/lib/python3.12/site-packages/fontTools/colorLib/__pycache__/errors.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/fontTools/colorLib/__pycache__/geometry.cpython-312.pyc b/hug/lib/python3.12/site-packages/fontTools/colorLib/__pycache__/geometry.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9c54c875f0901b722b9b54eff00b3a3994db5cfd Binary files /dev/null and b/hug/lib/python3.12/site-packages/fontTools/colorLib/__pycache__/geometry.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/fontTools/colorLib/__pycache__/table_builder.cpython-312.pyc b/hug/lib/python3.12/site-packages/fontTools/colorLib/__pycache__/table_builder.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..01543f366b992d6b3a96ea8cb6450d268a6c6c45 Binary files /dev/null and b/hug/lib/python3.12/site-packages/fontTools/colorLib/__pycache__/table_builder.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/fontTools/colorLib/__pycache__/unbuilder.cpython-312.pyc b/hug/lib/python3.12/site-packages/fontTools/colorLib/__pycache__/unbuilder.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4d8a69090f032ed688d84c0cf5b7a11041ea4c4f Binary files /dev/null and b/hug/lib/python3.12/site-packages/fontTools/colorLib/__pycache__/unbuilder.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/fontTools/colorLib/builder.py b/hug/lib/python3.12/site-packages/fontTools/colorLib/builder.py new file mode 100644 index 0000000000000000000000000000000000000000..66a0698de3f2bbd1ce624c6d0662e2e46f497363 --- /dev/null +++ b/hug/lib/python3.12/site-packages/fontTools/colorLib/builder.py @@ -0,0 +1,715 @@ +""" +colorLib.builder: Build COLR/CPAL tables from scratch + +""" + +import collections +import copy +import enum +from functools import partial +from math import ceil, log +from typing import ( + Any, + Dict, + Generator, + Iterable, + List, + Mapping, + Optional, + Sequence, + Tuple, + Type, + TypeVar, + Union, +) +from fontTools.misc.arrayTools import intRect +from fontTools.misc.fixedTools import fixedToFloat +from fontTools.misc.treeTools import build_n_ary_tree +from fontTools.ttLib.tables import C_O_L_R_ +from fontTools.ttLib.tables import C_P_A_L_ +from fontTools.ttLib.tables import _n_a_m_e +from fontTools.ttLib.tables import otTables as ot +from fontTools.ttLib.tables.otTables import ExtendMode, CompositeMode +from .errors import ColorLibError +from .geometry import round_start_circle_stable_containment +from .table_builder import BuildCallback, TableBuilder + +# TODO move type aliases to colorLib.types? +T = TypeVar("T") +_Kwargs = Mapping[str, Any] +_PaintInput = Union[int, _Kwargs, ot.Paint, Tuple[str, "_PaintInput"]] +_PaintInputList = Sequence[_PaintInput] +_ColorGlyphsDict = Dict[str, Union[_PaintInputList, _PaintInput]] +_ColorGlyphsV0Dict = Dict[str, Sequence[Tuple[str, int]]] +_ClipBoxInput = Union[ + Tuple[int, int, int, int, int], # format 1, variable + Tuple[int, int, int, int], # format 0, non-variable + ot.ClipBox, +] + + +MAX_PAINT_COLR_LAYER_COUNT = 255 +_DEFAULT_ALPHA = 1.0 +_MAX_REUSE_LEN = 32 + + +def _beforeBuildPaintRadialGradient(paint, source): + x0 = source["x0"] + y0 = source["y0"] + r0 = source["r0"] + x1 = source["x1"] + y1 = source["y1"] + r1 = source["r1"] + + # TODO apparently no builder_test confirms this works (?) + + # avoid abrupt change after rounding when c0 is near c1's perimeter + c = round_start_circle_stable_containment((x0, y0), r0, (x1, y1), r1) + x0, y0 = c.centre + r0 = c.radius + + # update source to ensure paint is built with corrected values + source["x0"] = x0 + source["y0"] = y0 + source["r0"] = r0 + source["x1"] = x1 + source["y1"] = y1 + source["r1"] = r1 + + return paint, source + + +def _defaultColorStop(): + colorStop = ot.ColorStop() + colorStop.Alpha = _DEFAULT_ALPHA + return colorStop + + +def _defaultVarColorStop(): + colorStop = ot.VarColorStop() + colorStop.Alpha = _DEFAULT_ALPHA + return colorStop + + +def _defaultColorLine(): + colorLine = ot.ColorLine() + colorLine.Extend = ExtendMode.PAD + return colorLine + + +def _defaultVarColorLine(): + colorLine = ot.VarColorLine() + colorLine.Extend = ExtendMode.PAD + return colorLine + + +def _defaultPaintSolid(): + paint = ot.Paint() + paint.Alpha = _DEFAULT_ALPHA + return paint + + +def _buildPaintCallbacks(): + return { + ( + BuildCallback.BEFORE_BUILD, + ot.Paint, + ot.PaintFormat.PaintRadialGradient, + ): _beforeBuildPaintRadialGradient, + ( + BuildCallback.BEFORE_BUILD, + ot.Paint, + ot.PaintFormat.PaintVarRadialGradient, + ): _beforeBuildPaintRadialGradient, + (BuildCallback.CREATE_DEFAULT, ot.ColorStop): _defaultColorStop, + (BuildCallback.CREATE_DEFAULT, ot.VarColorStop): _defaultVarColorStop, + (BuildCallback.CREATE_DEFAULT, ot.ColorLine): _defaultColorLine, + (BuildCallback.CREATE_DEFAULT, ot.VarColorLine): _defaultVarColorLine, + ( + BuildCallback.CREATE_DEFAULT, + ot.Paint, + ot.PaintFormat.PaintSolid, + ): _defaultPaintSolid, + ( + BuildCallback.CREATE_DEFAULT, + ot.Paint, + ot.PaintFormat.PaintVarSolid, + ): _defaultPaintSolid, + } + + +def populateCOLRv0( + table: ot.COLR, + colorGlyphsV0: _ColorGlyphsV0Dict, + glyphMap: Optional[Mapping[str, int]] = None, +): + """Build v0 color layers and add to existing COLR table. + + Args: + table: a raw ``otTables.COLR()`` object (not ttLib's ``table_C_O_L_R_``). + colorGlyphsV0: map of base glyph names to lists of (layer glyph names, + color palette index) tuples. Can be empty. + glyphMap: a map from glyph names to glyph indices, as returned from + ``TTFont.getReverseGlyphMap()``, to optionally sort base records by GID. + """ + if glyphMap is not None: + _check_base_glyphs_exist(colorGlyphsV0, glyphMap, "populateCOLRv0") + colorGlyphItems = sorted( + colorGlyphsV0.items(), key=lambda item: glyphMap[item[0]] + ) + else: + colorGlyphItems = colorGlyphsV0.items() + baseGlyphRecords = [] + layerRecords = [] + for baseGlyph, layers in colorGlyphItems: + baseRec = ot.BaseGlyphRecord() + baseRec.BaseGlyph = baseGlyph + baseRec.FirstLayerIndex = len(layerRecords) + baseRec.NumLayers = len(layers) + baseGlyphRecords.append(baseRec) + + for layerGlyph, paletteIndex in layers: + if glyphMap is not None and layerGlyph not in glyphMap: + raise ColorLibError( + f"populateCOLRv0: layer glyph not found in glyphMap: {layerGlyph!r}" + ) + layerRec = ot.LayerRecord() + layerRec.LayerGlyph = layerGlyph + layerRec.PaletteIndex = paletteIndex + layerRecords.append(layerRec) + + table.BaseGlyphRecordArray = table.LayerRecordArray = None + if baseGlyphRecords: + table.BaseGlyphRecordArray = ot.BaseGlyphRecordArray() + table.BaseGlyphRecordArray.BaseGlyphRecord = baseGlyphRecords + if layerRecords: + table.LayerRecordArray = ot.LayerRecordArray() + table.LayerRecordArray.LayerRecord = layerRecords + table.BaseGlyphRecordCount = len(baseGlyphRecords) + table.LayerRecordCount = len(layerRecords) + + +def buildCOLR( + colorGlyphs: _ColorGlyphsDict, + version: Optional[int] = None, + *, + glyphMap: Optional[Mapping[str, int]] = None, + varStore: Optional[ot.VarStore] = None, + varIndexMap: Optional[ot.DeltaSetIndexMap] = None, + clipBoxes: Optional[Dict[str, _ClipBoxInput]] = None, + allowLayerReuse: bool = True, +) -> C_O_L_R_.table_C_O_L_R_: + """Build COLR table from color layers mapping. + + Args: + + colorGlyphs: map of base glyph name to, either list of (layer glyph name, + color palette index) tuples for COLRv0; or a single ``Paint`` (dict) or + list of ``Paint`` for COLRv1. + version: the version of COLR table. If None, the version is determined + by the presence of COLRv1 paints or variation data (varStore), which + require version 1; otherwise, if all base glyphs use only simple color + layers, version 0 is used. + glyphMap: a map from glyph names to glyph indices, as returned from + TTFont.getReverseGlyphMap(), to optionally sort base records by GID. + varStore: Optional ItemVarationStore for deltas associated with v1 layer. + varIndexMap: Optional DeltaSetIndexMap for deltas associated with v1 layer. + clipBoxes: Optional map of base glyph name to clip box 4- or 5-tuples: + (xMin, yMin, xMax, yMax) or (xMin, yMin, xMax, yMax, varIndexBase). + + Returns: + A new COLR table. + """ + self = C_O_L_R_.table_C_O_L_R_() + + if varStore is not None and version == 0: + raise ValueError("Can't add VarStore to COLRv0") + + if version in (None, 0) and not varStore: + # split color glyphs into v0 and v1 and encode separately + colorGlyphsV0, colorGlyphsV1 = _split_color_glyphs_by_version(colorGlyphs) + if version == 0 and colorGlyphsV1: + raise ValueError("Can't encode COLRv1 glyphs in COLRv0") + else: + # unless explicitly requested for v1 or have variations, in which case + # we encode all color glyph as v1 + colorGlyphsV0, colorGlyphsV1 = {}, colorGlyphs + + colr = ot.COLR() + + populateCOLRv0(colr, colorGlyphsV0, glyphMap) + + colr.LayerList, colr.BaseGlyphList = buildColrV1( + colorGlyphsV1, + glyphMap, + allowLayerReuse=allowLayerReuse, + ) + + if version is None: + version = 1 if (varStore or colorGlyphsV1) else 0 + elif version not in (0, 1): + raise NotImplementedError(version) + self.version = colr.Version = version + + if version == 0: + self.ColorLayers = self._decompileColorLayersV0(colr) + else: + colr.ClipList = buildClipList(clipBoxes) if clipBoxes else None + colr.VarIndexMap = varIndexMap + colr.VarStore = varStore + self.table = colr + + return self + + +def buildClipList(clipBoxes: Dict[str, _ClipBoxInput]) -> ot.ClipList: + clipList = ot.ClipList() + clipList.Format = 1 + clipList.clips = {name: buildClipBox(box) for name, box in clipBoxes.items()} + return clipList + + +def buildClipBox(clipBox: _ClipBoxInput) -> ot.ClipBox: + if isinstance(clipBox, ot.ClipBox): + return clipBox + n = len(clipBox) + clip = ot.ClipBox() + if n not in (4, 5): + raise ValueError(f"Invalid ClipBox: expected 4 or 5 values, found {n}") + clip.xMin, clip.yMin, clip.xMax, clip.yMax = intRect(clipBox[:4]) + clip.Format = int(n == 5) + 1 + if n == 5: + clip.VarIndexBase = int(clipBox[4]) + return clip + + +class ColorPaletteType(enum.IntFlag): + USABLE_WITH_LIGHT_BACKGROUND = 0x0001 + USABLE_WITH_DARK_BACKGROUND = 0x0002 + + @classmethod + def _missing_(cls, value): + # enforce reserved bits + if isinstance(value, int) and (value < 0 or value & 0xFFFC != 0): + raise ValueError(f"{value} is not a valid {cls.__name__}") + return super()._missing_(value) + + +# None, 'abc' or {'en': 'abc', 'de': 'xyz'} +_OptionalLocalizedString = Union[None, str, Dict[str, str]] + + +def buildPaletteLabels( + labels: Iterable[_OptionalLocalizedString], nameTable: _n_a_m_e.table__n_a_m_e +) -> List[Optional[int]]: + return [ + ( + nameTable.addMultilingualName(l, mac=False) + if isinstance(l, dict) + else ( + C_P_A_L_.table_C_P_A_L_.NO_NAME_ID + if l is None + else nameTable.addMultilingualName({"en": l}, mac=False) + ) + ) + for l in labels + ] + + +def buildCPAL( + palettes: Sequence[Sequence[Tuple[float, float, float, float]]], + paletteTypes: Optional[Sequence[ColorPaletteType]] = None, + paletteLabels: Optional[Sequence[_OptionalLocalizedString]] = None, + paletteEntryLabels: Optional[Sequence[_OptionalLocalizedString]] = None, + nameTable: Optional[_n_a_m_e.table__n_a_m_e] = None, +) -> C_P_A_L_.table_C_P_A_L_: + """Build CPAL table from list of color palettes. + + Args: + palettes: list of lists of colors encoded as tuples of (R, G, B, A) floats + in the range [0..1]. + paletteTypes: optional list of ColorPaletteType, one for each palette. + paletteLabels: optional list of palette labels. Each lable can be either: + None (no label), a string (for for default English labels), or a + localized string (as a dict keyed with BCP47 language codes). + paletteEntryLabels: optional list of palette entry labels, one for each + palette entry (see paletteLabels). + nameTable: optional name table where to store palette and palette entry + labels. Required if either paletteLabels or paletteEntryLabels is set. + + Return: + A new CPAL v0 or v1 table, if custom palette types or labels are specified. + """ + if len({len(p) for p in palettes}) != 1: + raise ColorLibError("color palettes have different lengths") + + if (paletteLabels or paletteEntryLabels) and not nameTable: + raise TypeError( + "nameTable is required if palette or palette entries have labels" + ) + + cpal = C_P_A_L_.table_C_P_A_L_() + cpal.numPaletteEntries = len(palettes[0]) + + cpal.palettes = [] + for i, palette in enumerate(palettes): + colors = [] + for j, color in enumerate(palette): + if not isinstance(color, tuple) or len(color) != 4: + raise ColorLibError( + f"In palette[{i}][{j}]: expected (R, G, B, A) tuple, got {color!r}" + ) + if any(v > 1 or v < 0 for v in color): + raise ColorLibError( + f"palette[{i}][{j}] has invalid out-of-range [0..1] color: {color!r}" + ) + # input colors are RGBA, CPAL encodes them as BGRA + red, green, blue, alpha = color + colors.append( + C_P_A_L_.Color(*(round(v * 255) for v in (blue, green, red, alpha))) + ) + cpal.palettes.append(colors) + + if any(v is not None for v in (paletteTypes, paletteLabels, paletteEntryLabels)): + cpal.version = 1 + + if paletteTypes is not None: + if len(paletteTypes) != len(palettes): + raise ColorLibError( + f"Expected {len(palettes)} paletteTypes, got {len(paletteTypes)}" + ) + cpal.paletteTypes = [ColorPaletteType(t).value for t in paletteTypes] + else: + cpal.paletteTypes = [C_P_A_L_.table_C_P_A_L_.DEFAULT_PALETTE_TYPE] * len( + palettes + ) + + if paletteLabels is not None: + if len(paletteLabels) != len(palettes): + raise ColorLibError( + f"Expected {len(palettes)} paletteLabels, got {len(paletteLabels)}" + ) + cpal.paletteLabels = buildPaletteLabels(paletteLabels, nameTable) + else: + cpal.paletteLabels = [C_P_A_L_.table_C_P_A_L_.NO_NAME_ID] * len(palettes) + + if paletteEntryLabels is not None: + if len(paletteEntryLabels) != cpal.numPaletteEntries: + raise ColorLibError( + f"Expected {cpal.numPaletteEntries} paletteEntryLabels, " + f"got {len(paletteEntryLabels)}" + ) + cpal.paletteEntryLabels = buildPaletteLabels(paletteEntryLabels, nameTable) + else: + cpal.paletteEntryLabels = [ + C_P_A_L_.table_C_P_A_L_.NO_NAME_ID + ] * cpal.numPaletteEntries + else: + cpal.version = 0 + + return cpal + + +# COLR v1 tables +# See draft proposal at: https://github.com/googlefonts/colr-gradients-spec + + +def _is_colrv0_layer(layer: Any) -> bool: + # Consider as COLRv0 layer any sequence of length 2 (be it tuple or list) in which + # the first element is a str (the layerGlyph) and the second element is an int + # (CPAL paletteIndex). + # https://github.com/googlefonts/ufo2ft/issues/426 + try: + layerGlyph, paletteIndex = layer + except (TypeError, ValueError): + return False + else: + return isinstance(layerGlyph, str) and isinstance(paletteIndex, int) + + +def _split_color_glyphs_by_version( + colorGlyphs: _ColorGlyphsDict, +) -> Tuple[_ColorGlyphsV0Dict, _ColorGlyphsDict]: + colorGlyphsV0 = {} + colorGlyphsV1 = {} + for baseGlyph, layers in colorGlyphs.items(): + if all(_is_colrv0_layer(l) for l in layers): + colorGlyphsV0[baseGlyph] = layers + else: + colorGlyphsV1[baseGlyph] = layers + + # sanity check + assert set(colorGlyphs) == (set(colorGlyphsV0) | set(colorGlyphsV1)) + + return colorGlyphsV0, colorGlyphsV1 + + +def _reuse_ranges(num_layers: int) -> Generator[Tuple[int, int], None, None]: + # TODO feels like something itertools might have already + for lbound in range(num_layers): + # Reuse of very large #s of layers is relatively unlikely + # +2: we want sequences of at least 2 + # otData handles single-record duplication + for ubound in range( + lbound + 2, min(num_layers + 1, lbound + 2 + _MAX_REUSE_LEN) + ): + yield (lbound, ubound) + + +class LayerReuseCache: + reusePool: Mapping[Tuple[Any, ...], int] + tuples: Mapping[int, Tuple[Any, ...]] + keepAlive: List[ot.Paint] # we need id to remain valid + + def __init__(self): + self.reusePool = {} + self.tuples = {} + self.keepAlive = [] + + def _paint_tuple(self, paint: ot.Paint): + # start simple, who even cares about cyclic graphs or interesting field types + def _tuple_safe(value): + if isinstance(value, enum.Enum): + return value + elif hasattr(value, "__dict__"): + return tuple( + (k, _tuple_safe(v)) for k, v in sorted(value.__dict__.items()) + ) + elif isinstance(value, collections.abc.MutableSequence): + return tuple(_tuple_safe(e) for e in value) + return value + + # Cache the tuples for individual Paint instead of the whole sequence + # because the seq could be a transient slice + result = self.tuples.get(id(paint), None) + if result is None: + result = _tuple_safe(paint) + self.tuples[id(paint)] = result + self.keepAlive.append(paint) + return result + + def _as_tuple(self, paints: Sequence[ot.Paint]) -> Tuple[Any, ...]: + return tuple(self._paint_tuple(p) for p in paints) + + def try_reuse(self, layers: List[ot.Paint]) -> List[ot.Paint]: + found_reuse = True + while found_reuse: + found_reuse = False + + ranges = sorted( + _reuse_ranges(len(layers)), + key=lambda t: (t[1] - t[0], t[1], t[0]), + reverse=True, + ) + for lbound, ubound in ranges: + reuse_lbound = self.reusePool.get( + self._as_tuple(layers[lbound:ubound]), -1 + ) + if reuse_lbound == -1: + continue + new_slice = ot.Paint() + new_slice.Format = int(ot.PaintFormat.PaintColrLayers) + new_slice.NumLayers = ubound - lbound + new_slice.FirstLayerIndex = reuse_lbound + layers = layers[:lbound] + [new_slice] + layers[ubound:] + found_reuse = True + break + return layers + + def add(self, layers: List[ot.Paint], first_layer_index: int): + for lbound, ubound in _reuse_ranges(len(layers)): + self.reusePool[self._as_tuple(layers[lbound:ubound])] = ( + lbound + first_layer_index + ) + + +class LayerListBuilder: + layers: List[ot.Paint] + cache: LayerReuseCache + allowLayerReuse: bool + + def __init__(self, *, glyphMap=None, allowLayerReuse=True): + self.glyphMap = glyphMap + self.layers = [] + if allowLayerReuse: + self.cache = LayerReuseCache() + else: + self.cache = None + + # We need to intercept construction of PaintColrLayers + callbacks = _buildPaintCallbacks() + callbacks[ + ( + BuildCallback.BEFORE_BUILD, + ot.Paint, + ot.PaintFormat.PaintColrLayers, + ) + ] = self._beforeBuildPaintColrLayers + # When a glyphMap is available, sanity check that glyphs referenced by + # PaintGlyph, PaintGlyph2 and PaintColrGlyph actually exist, so a + # missing glyph raises a legible error here instead of failing + # obscurely later (e.g. a struct.error at compile time). + # https://github.com/fonttools/fonttools/issues/2629 + if glyphMap is not None: + for paintFormat in ( + ot.PaintFormat.PaintGlyph, + ot.PaintFormat.PaintGlyph2, + ot.PaintFormat.PaintColrGlyph, + ): + callbacks[(BuildCallback.AFTER_BUILD, ot.Paint, paintFormat)] = ( + self._checkPaintGlyphExists + ) + self.tableBuilder = TableBuilder(callbacks) + + def _checkPaintGlyphExists(self, paint: ot.Paint) -> ot.Paint: + if paint.Glyph not in self.glyphMap: + paintName = ot.PaintFormat(paint.Format).name + raise ColorLibError( + f"{paintName}: glyph not found in glyphMap: {paint.Glyph!r}" + ) + if ( + paint.Format == ot.PaintFormat.PaintGlyph + and self.glyphMap[paint.Glyph] > 0xFFFF + ): + paint.Format = ot.PaintFormat.PaintGlyph2 + return paint + + # COLR layers is unusual in that it modifies shared state + # so we need a callback into an object + def _beforeBuildPaintColrLayers(self, dest, source): + # Sketchy gymnastics: a sequence input will have dropped it's layers + # into NumLayers; get it back + if isinstance(source.get("NumLayers", None), collections.abc.Sequence): + layers = source["NumLayers"] + else: + layers = source["Layers"] + + # Convert maps seqs or whatever into typed objects + layers = [self.buildPaint(l) for l in layers] + + # No reason to have a colr layers with just one entry + if len(layers) == 1: + return layers[0], {} + + if self.cache is not None: + # Look for reuse, with preference to longer sequences + # This may make the layer list smaller + layers = self.cache.try_reuse(layers) + + # The layer list is now final; if it's too big we need to tree it + is_tree = len(layers) > MAX_PAINT_COLR_LAYER_COUNT + layers = build_n_ary_tree(layers, n=MAX_PAINT_COLR_LAYER_COUNT) + + # We now have a tree of sequences with Paint leaves. + # Convert the sequences into PaintColrLayers. + def listToColrLayers(layer): + if isinstance(layer, collections.abc.Sequence): + return self.buildPaint( + { + "Format": ot.PaintFormat.PaintColrLayers, + "Layers": [listToColrLayers(l) for l in layer], + } + ) + return layer + + layers = [listToColrLayers(l) for l in layers] + + # No reason to have a colr layers with just one entry + if len(layers) == 1: + return layers[0], {} + + paint = ot.Paint() + paint.Format = int(ot.PaintFormat.PaintColrLayers) + paint.NumLayers = len(layers) + paint.FirstLayerIndex = len(self.layers) + self.layers.extend(layers) + + # Register our parts for reuse provided we aren't a tree + # If we are a tree the leaves registered for reuse and that will suffice + if self.cache is not None and not is_tree: + self.cache.add(layers, paint.FirstLayerIndex) + + # we've fully built dest; empty source prevents generalized build from kicking in + return paint, {} + + def buildPaint(self, paint: _PaintInput) -> ot.Paint: + return self.tableBuilder.build(ot.Paint, paint) + + def build(self) -> Optional[ot.LayerList]: + if not self.layers: + return None + layers = ot.LayerList() + layers.LayerCount = len(self.layers) + layers.Paint = self.layers + return layers + + +def buildBaseGlyphPaintRecord( + baseGlyph: str, layerBuilder: LayerListBuilder, paint: _PaintInput +) -> ot.BaseGlyphList: + self = ot.BaseGlyphPaintRecord() + self.BaseGlyph = baseGlyph + self.Paint = layerBuilder.buildPaint(paint) + return self + + +def _check_base_glyphs_exist(colorGlyphs, glyphMap, where): + """Checks that every base glyph name in colorGlyphs exists in glyphMap.""" + + missing = [] + for baseGlyph in colorGlyphs.keys(): + if baseGlyph not in glyphMap: + missing.append(baseGlyph) + + if missing: + preview = ", ".join(missing[:10]) + extra = "" + if len(missing) > 10: + extra = f" (and {len(missing) - 10} more)" + raise ColorLibError( + f"{where}: base glyph(s) not found in glyphMap: {preview}{extra}" + ) + + +def _format_glyph_errors(errors: Mapping[str, Exception]) -> str: + lines = [] + for baseGlyph, error in sorted(errors.items()): + lines.append(f" {baseGlyph} => {type(error).__name__}: {error}") + return "\n".join(lines) + + +def buildColrV1( + colorGlyphs: _ColorGlyphsDict, + glyphMap: Optional[Mapping[str, int]] = None, + *, + allowLayerReuse: bool = True, +) -> Tuple[Optional[ot.LayerList], ot.BaseGlyphList]: + if glyphMap is not None: + _check_base_glyphs_exist(colorGlyphs, glyphMap, "buildColrV1") + colorGlyphItems = sorted( + colorGlyphs.items(), key=lambda item: glyphMap[item[0]] + ) + else: + colorGlyphItems = colorGlyphs.items() + + errors = {} + baseGlyphs = [] + layerBuilder = LayerListBuilder(glyphMap=glyphMap, allowLayerReuse=allowLayerReuse) + for baseGlyph, paint in colorGlyphItems: + try: + baseGlyphs.append(buildBaseGlyphPaintRecord(baseGlyph, layerBuilder, paint)) + + except (ColorLibError, OverflowError, ValueError, TypeError) as e: + errors[baseGlyph] = e + + if errors: + failed_glyphs = _format_glyph_errors(errors) + exc = ColorLibError(f"Failed to build BaseGlyphList:\n{failed_glyphs}") + exc.errors = errors + raise exc from next(iter(errors.values())) + + layers = layerBuilder.build() + glyphs = ot.BaseGlyphList() + glyphs.BaseGlyphCount = len(baseGlyphs) + glyphs.BaseGlyphPaintRecord = baseGlyphs + return (layers, glyphs) diff --git a/hug/lib/python3.12/site-packages/fontTools/colorLib/errors.py b/hug/lib/python3.12/site-packages/fontTools/colorLib/errors.py new file mode 100644 index 0000000000000000000000000000000000000000..18cbebbaf91ff7d5a515321a006be3eb1d83faaf --- /dev/null +++ b/hug/lib/python3.12/site-packages/fontTools/colorLib/errors.py @@ -0,0 +1,2 @@ +class ColorLibError(Exception): + pass diff --git a/hug/lib/python3.12/site-packages/fontTools/colorLib/geometry.py b/hug/lib/python3.12/site-packages/fontTools/colorLib/geometry.py new file mode 100644 index 0000000000000000000000000000000000000000..1ce161bfa117df1632b507d161f0dd4abb633bcc --- /dev/null +++ b/hug/lib/python3.12/site-packages/fontTools/colorLib/geometry.py @@ -0,0 +1,143 @@ +"""Helpers for manipulating 2D points and vectors in COLR table.""" + +from math import copysign, cos, hypot, isclose, pi +from fontTools.misc.roundTools import otRound + + +def _vector_between(origin, target): + return (target[0] - origin[0], target[1] - origin[1]) + + +def _round_point(pt): + return (otRound(pt[0]), otRound(pt[1])) + + +def _unit_vector(vec): + length = hypot(*vec) + if length == 0: + return None + return (vec[0] / length, vec[1] / length) + + +_CIRCLE_INSIDE_TOLERANCE = 1e-4 + + +# The unit vector's X and Y components are respectively +# U = (cos(α), sin(α)) +# where α is the angle between the unit vector and the positive x axis. +_UNIT_VECTOR_THRESHOLD = cos(3 / 8 * pi) # == sin(1/8 * pi) == 0.38268343236508984 + + +def _rounding_offset(direction): + # Return 2-tuple of -/+ 1.0 or 0.0 approximately based on the direction vector. + # We divide the unit circle in 8 equal slices oriented towards the cardinal + # (N, E, S, W) and intermediate (NE, SE, SW, NW) directions. To each slice we + # map one of the possible cases: -1, 0, +1 for either X and Y coordinate. + # E.g. Return (+1.0, -1.0) if unit vector is oriented towards SE, or + # (-1.0, 0.0) if it's pointing West, etc. + uv = _unit_vector(direction) + if not uv: + return (0, 0) + + result = [] + for uv_component in uv: + if -_UNIT_VECTOR_THRESHOLD <= uv_component < _UNIT_VECTOR_THRESHOLD: + # unit vector component near 0: direction almost orthogonal to the + # direction of the current axis, thus keep coordinate unchanged + result.append(0) + else: + # nudge coord by +/- 1.0 in direction of unit vector + result.append(copysign(1.0, uv_component)) + return tuple(result) + + +class Circle: + def __init__(self, centre, radius): + self.centre = centre + self.radius = radius + + def __repr__(self): + return f"Circle(centre={self.centre}, radius={self.radius})" + + def round(self): + return Circle(_round_point(self.centre), otRound(self.radius)) + + def inside(self, outer_circle, tolerance=_CIRCLE_INSIDE_TOLERANCE): + dist = self.radius + hypot(*_vector_between(self.centre, outer_circle.centre)) + return ( + isclose(outer_circle.radius, dist, rel_tol=_CIRCLE_INSIDE_TOLERANCE) + or outer_circle.radius > dist + ) + + def concentric(self, other): + return self.centre == other.centre + + def move(self, dx, dy): + self.centre = (self.centre[0] + dx, self.centre[1] + dy) + + +def round_start_circle_stable_containment(c0, r0, c1, r1): + """Round start circle so that it stays inside/outside end circle after rounding. + + The rounding of circle coordinates to integers may cause an abrupt change + if the start circle c0 is so close to the end circle c1's perimiter that + it ends up falling outside (or inside) as a result of the rounding. + To keep the gradient unchanged, we nudge it in the right direction. + + See: + https://github.com/googlefonts/colr-gradients-spec/issues/204 + https://github.com/googlefonts/picosvg/issues/158 + """ + start, end = Circle(c0, r0), Circle(c1, r1) + + inside_before_round = start.inside(end) + + round_start = start.round() + round_end = end.round() + inside_after_round = round_start.inside(round_end) + + if inside_before_round == inside_after_round: + return round_start + elif inside_after_round: + # start was outside before rounding: we need to push start away from end + direction = _vector_between(round_end.centre, round_start.centre) + radius_delta = +1.0 + else: + # start was inside before rounding: we need to push start towards end + direction = _vector_between(round_start.centre, round_end.centre) + radius_delta = -1.0 + dx, dy = _rounding_offset(direction) + + # At most 2 iterations ought to be enough to converge. Before the loop, we + # know the start circle didn't keep containment after normal rounding; thus + # we continue adjusting by -/+ 1.0 until containment is restored. + # Normal rounding can at most move each coordinates -/+0.5; in the worst case + # both the start and end circle's centres and radii will be rounded in opposite + # directions, e.g. when they move along a 45 degree diagonal: + # c0 = (1.5, 1.5) ===> (2.0, 2.0) + # r0 = 0.5 ===> 1.0 + # c1 = (0.499, 0.499) ===> (0.0, 0.0) + # r1 = 2.499 ===> 2.0 + # In this example, the relative distance between the circles, calculated + # as r1 - (r0 + distance(c0, c1)) is initially 0.57437 (c0 is inside c1), and + # -1.82842 after rounding (c0 is now outside c1). Nudging c0 by -1.0 on both + # x and y axes moves it towards c1 by hypot(-1.0, -1.0) = 1.41421. Two of these + # moves cover twice that distance, which is enough to restore containment. + max_attempts = 2 + for _ in range(max_attempts): + if round_start.concentric(round_end): + # can't move c0 towards c1 (they are the same), so we change the radius + round_start.radius += radius_delta + assert round_start.radius >= 0 + else: + round_start.move(dx, dy) + if inside_before_round == round_start.inside(round_end): + break + else: # likely a bug + raise AssertionError( + f"Rounding circle {start} " + f"{'inside' if inside_before_round else 'outside'} " + f"{end} failed after {max_attempts} attempts!" + ) + + return round_start diff --git a/hug/lib/python3.12/site-packages/fontTools/colorLib/table_builder.py b/hug/lib/python3.12/site-packages/fontTools/colorLib/table_builder.py new file mode 100644 index 0000000000000000000000000000000000000000..f1e182c432eae48545b2264d57d58c754ff6cd30 --- /dev/null +++ b/hug/lib/python3.12/site-packages/fontTools/colorLib/table_builder.py @@ -0,0 +1,223 @@ +""" +colorLib.table_builder: Generic helper for filling in BaseTable derivatives from tuples and maps and such. + +""" + +import collections +import enum +from fontTools.ttLib.tables.otBase import ( + BaseTable, + FormatSwitchingBaseTable, + UInt8FormatSwitchingBaseTable, +) +from fontTools.ttLib.tables.otConverters import ( + ComputedInt, + SimpleValue, + Struct, + Short, + UInt8, + UShort, + IntValue, + FloatValue, + OptionalValue, +) +from fontTools.misc.roundTools import otRound + + +class BuildCallback(enum.Enum): + """Keyed on (BEFORE_BUILD, class[, Format if available]). + Receives (dest, source). + Should return (dest, source), which can be new objects. + """ + + BEFORE_BUILD = enum.auto() + + """Keyed on (AFTER_BUILD, class[, Format if available]). + Receives (dest). + Should return dest, which can be a new object. + """ + AFTER_BUILD = enum.auto() + + """Keyed on (CREATE_DEFAULT, class[, Format if available]). + Receives no arguments. + Should return a new instance of class. + """ + CREATE_DEFAULT = enum.auto() + + +def _assignable(convertersByName): + return {k: v for k, v in convertersByName.items() if not isinstance(v, ComputedInt)} + + +def _isNonStrSequence(value): + return isinstance(value, collections.abc.Sequence) and not isinstance(value, str) + + +def _split_format(cls, source): + if _isNonStrSequence(source): + assert len(source) > 0, f"{cls} needs at least format from {source}" + fmt, remainder = source[0], source[1:] + elif isinstance(source, collections.abc.Mapping): + assert "Format" in source, f"{cls} needs at least Format from {source}" + remainder = source.copy() + fmt = remainder.pop("Format") + else: + raise ValueError(f"Not sure how to populate {cls} from {source}") + + assert isinstance( + fmt, collections.abc.Hashable + ), f"{cls} Format is not hashable: {fmt!r}" + assert fmt in cls.convertersByName, f"{cls} invalid Format: {fmt!r}" + + return fmt, remainder + + +class TableBuilder: + """ + Helps to populate things derived from BaseTable from maps, tuples, etc. + + A table of lifecycle callbacks may be provided to add logic beyond what is possible + based on otData info for the target class. See BuildCallbacks. + """ + + def __init__(self, callbackTable=None): + if callbackTable is None: + callbackTable = {} + self._callbackTable = callbackTable + + def _convert(self, dest, field, converter, value): + enumClass = getattr(converter, "enumClass", None) + + if enumClass: + if isinstance(value, enumClass): + pass + elif isinstance(value, str): + try: + value = getattr(enumClass, value.upper()) + except AttributeError: + raise ValueError(f"{value} is not a valid {enumClass}") + else: + value = enumClass(value) + + elif isinstance(converter, IntValue): + value = otRound(value) + elif isinstance(converter, FloatValue): + value = float(value) + + elif isinstance(converter, Struct): + if converter.repeat: + if _isNonStrSequence(value): + value = [self.build(converter.tableClass, v) for v in value] + else: + value = [self.build(converter.tableClass, value)] + setattr(dest, converter.repeat, len(value)) + else: + value = self.build(converter.tableClass, value) + elif callable(converter): + value = converter(value) + + setattr(dest, field, value) + + def build(self, cls, source): + assert issubclass(cls, BaseTable) + + if isinstance(source, cls): + return source + + callbackKey = (cls,) + fmt = None + if issubclass(cls, FormatSwitchingBaseTable): + fmt, source = _split_format(cls, source) + callbackKey = (cls, fmt) + + dest = self._callbackTable.get( + (BuildCallback.CREATE_DEFAULT,) + callbackKey, lambda: cls() + )() + assert isinstance(dest, cls) + + convByName = _assignable(cls.convertersByName) + skippedFields = set() + + # For format switchers we need to resolve converters based on format + if issubclass(cls, FormatSwitchingBaseTable): + dest.Format = fmt + convByName = _assignable(convByName[dest.Format]) + skippedFields.add("Format") + + # Convert sequence => mapping so before thunk only has to handle one format + if _isNonStrSequence(source): + # Sequence (typically list or tuple) assumed to match fields in declaration order + assert len(source) <= len( + convByName + ), f"Sequence of {len(source)} too long for {cls}; expected <= {len(convByName)} values" + source = dict(zip(convByName.keys(), source)) + + dest, source = self._callbackTable.get( + (BuildCallback.BEFORE_BUILD,) + callbackKey, lambda d, s: (d, s) + )(dest, source) + + if isinstance(source, collections.abc.Mapping): + for field, value in source.items(): + if field in skippedFields: + continue + converter = convByName.get(field, None) + if not converter: + raise ValueError( + f"Unrecognized field {field} for {cls}; expected one of {sorted(convByName.keys())}" + ) + self._convert(dest, field, converter, value) + else: + # let's try as a 1-tuple + dest = self.build(cls, (source,)) + + for field, conv in convByName.items(): + if not hasattr(dest, field) and isinstance(conv, OptionalValue): + setattr(dest, field, conv.DEFAULT) + + dest = self._callbackTable.get( + (BuildCallback.AFTER_BUILD,) + callbackKey, lambda d: d + )(dest) + + return dest + + +class TableUnbuilder: + def __init__(self, callbackTable=None): + if callbackTable is None: + callbackTable = {} + self._callbackTable = callbackTable + + def unbuild(self, table): + assert isinstance(table, BaseTable) + + source = {} + + callbackKey = (type(table),) + if isinstance(table, FormatSwitchingBaseTable): + source["Format"] = int(table.Format) + callbackKey += (table.Format,) + + for converter in table.getConverters(): + if isinstance(converter, ComputedInt): + continue + value = getattr(table, converter.name) + + enumClass = getattr(converter, "enumClass", None) + if enumClass: + source[converter.name] = value.name.lower() + elif isinstance(converter, Struct): + if converter.repeat: + source[converter.name] = [self.unbuild(v) for v in value] + else: + source[converter.name] = self.unbuild(value) + elif isinstance(converter, SimpleValue): + # "simple" values (e.g. int, float, str) need no further un-building + source[converter.name] = value + else: + raise NotImplementedError( + "Don't know how unbuild {value!r} with {converter!r}" + ) + + source = self._callbackTable.get(callbackKey, lambda s: s)(source) + + return source diff --git a/hug/lib/python3.12/site-packages/fontTools/colorLib/unbuilder.py b/hug/lib/python3.12/site-packages/fontTools/colorLib/unbuilder.py new file mode 100644 index 0000000000000000000000000000000000000000..ac243550b8908aef120b395e740b9974559d65b5 --- /dev/null +++ b/hug/lib/python3.12/site-packages/fontTools/colorLib/unbuilder.py @@ -0,0 +1,81 @@ +from fontTools.ttLib.tables import otTables as ot +from .table_builder import TableUnbuilder + + +def unbuildColrV1(layerList, baseGlyphList): + layers = [] + if layerList: + layers = layerList.Paint + unbuilder = LayerListUnbuilder(layers) + return { + rec.BaseGlyph: unbuilder.unbuildPaint(rec.Paint) + for rec in baseGlyphList.BaseGlyphPaintRecord + } + + +def _flatten_layers(lst): + for paint in lst: + if paint["Format"] == ot.PaintFormat.PaintColrLayers: + yield from _flatten_layers(paint["Layers"]) + else: + yield paint + + +class LayerListUnbuilder: + def __init__(self, layers): + self.layers = layers + + callbacks = { + ( + ot.Paint, + ot.PaintFormat.PaintColrLayers, + ): self._unbuildPaintColrLayers, + } + self.tableUnbuilder = TableUnbuilder(callbacks) + + def unbuildPaint(self, paint): + assert isinstance(paint, ot.Paint) + return self.tableUnbuilder.unbuild(paint) + + def _unbuildPaintColrLayers(self, source): + assert source["Format"] == ot.PaintFormat.PaintColrLayers + + layers = list( + _flatten_layers( + [ + self.unbuildPaint(childPaint) + for childPaint in self.layers[ + source["FirstLayerIndex"] : source["FirstLayerIndex"] + + source["NumLayers"] + ] + ] + ) + ) + + if len(layers) == 1: + return layers[0] + + return {"Format": source["Format"], "Layers": layers} + + +if __name__ == "__main__": + from pprint import pprint + import sys + from fontTools.ttLib import TTFont + + try: + fontfile = sys.argv[1] + except IndexError: + sys.exit("usage: fonttools colorLib.unbuilder FONTFILE") + + font = TTFont(fontfile) + colr = font["COLR"] + if colr.version < 1: + sys.exit(f"error: No COLR table version=1 found in {fontfile}") + + colorGlyphs = unbuildColrV1( + colr.table.LayerList, + colr.table.BaseGlyphList, + ) + + pprint(colorGlyphs) diff --git a/hug/lib/python3.12/site-packages/fontTools/config/__init__.py b/hug/lib/python3.12/site-packages/fontTools/config/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..4517af79e4def0c47b7e18d69c9a64c4bdffdad7 --- /dev/null +++ b/hug/lib/python3.12/site-packages/fontTools/config/__init__.py @@ -0,0 +1,82 @@ +""" +Define all configuration options that can affect the working of fontTools +modules. E.g. optimization levels of varLib IUP, otlLib GPOS compression level, +etc. If this file gets too big, split it into smaller files per-module. + +An instance of the Config class can be attached to a TTFont object, so that +the various modules can access their configuration options from it. +""" + +from textwrap import dedent + +from fontTools.misc.configTools import * + + +class Config(AbstractConfig): + options = Options() + + +OPTIONS = Config.options + + +Config.register_option( + name="fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL", + help=dedent("""\ + GPOS Lookup type 2 (PairPos) compression level: + 0 = do not attempt to compact PairPos lookups; + 1 to 8 = create at most 1 to 8 new subtables for each existing + subtable, provided that it would yield a 50%% file size saving; + 9 = create as many new subtables as needed to yield a file size saving. + Default: 0. + + This compaction aims to save file size, by splitting large class + kerning subtables (Format 2) that contain many zero values into + smaller and denser subtables. It's a trade-off between the overhead + of several subtables versus the sparseness of one big subtable. + + See the pull request: https://github.com/fonttools/fonttools/pull/2326 + """), + default=0, + parse=int, + validate=lambda v: v in range(10), +) + +Config.register_option( + name="fontTools.ttLib.tables.otBase:USE_HARFBUZZ_REPACKER", + help=dedent("""\ + FontTools tries to use the HarfBuzz Repacker to serialize GPOS/GSUB tables + if the uharfbuzz python bindings are importable, otherwise falls back to its + slower, less efficient serializer. Set to False to always use the latter. + Set to True to explicitly request the HarfBuzz Repacker (will raise an + error if uharfbuzz cannot be imported). + """), + default=None, + parse=Option.parse_optional_bool, + validate=Option.validate_optional_bool, +) + +Config.register_option( + name="fontTools.otlLib.builder:WRITE_GPOS7", + help=dedent("""\ + macOS before 13.2 didn’t support GPOS LookupType 7 (non-chaining + ContextPos lookups), so FontTools.otlLib.builder disables a file size + optimisation that would use LookupType 7 instead of 8 when there is no + chaining (no prefix or suffix). Set to True to enable the optimization. + """), + default=False, + parse=Option.parse_optional_bool, + validate=Option.validate_optional_bool, +) + +Config.register_option( + name="fontTools.ttLib:OPTIMIZE_FONT_SPEED", + help=dedent("""\ + Enable optimizations that prioritize speed over file size. This + mainly affects how glyf table and gvar / VARC tables are compiled. + The produced fonts will be larger, but rendering performance will + be improved with HarfBuzz and other text layout engines. + """), + default=False, + parse=Option.parse_optional_bool, + validate=Option.validate_optional_bool, +) diff --git a/hug/lib/python3.12/site-packages/fontTools/config/__pycache__/__init__.cpython-312.pyc b/hug/lib/python3.12/site-packages/fontTools/config/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cf0b79aaf73dcc898c4374ad13b226aa6a007d9a Binary files /dev/null and b/hug/lib/python3.12/site-packages/fontTools/config/__pycache__/__init__.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/fontTools/cu2qu/__init__.py b/hug/lib/python3.12/site-packages/fontTools/cu2qu/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..4ae6356e44e1fed074b6283bcb4365bf2b770529 --- /dev/null +++ b/hug/lib/python3.12/site-packages/fontTools/cu2qu/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .cu2qu import * diff --git a/hug/lib/python3.12/site-packages/fontTools/cu2qu/__main__.py b/hug/lib/python3.12/site-packages/fontTools/cu2qu/__main__.py new file mode 100644 index 0000000000000000000000000000000000000000..bdab1b916289c7f9fb4275e24c01be130f224cd8 --- /dev/null +++ b/hug/lib/python3.12/site-packages/fontTools/cu2qu/__main__.py @@ -0,0 +1,5 @@ +import sys +from .cli import _main as main + +if __name__ == "__main__": + sys.exit(main()) diff --git a/hug/lib/python3.12/site-packages/fontTools/cu2qu/__pycache__/__init__.cpython-312.pyc b/hug/lib/python3.12/site-packages/fontTools/cu2qu/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b683fb84526622ccdd7bbfa05c65e83108ef6640 Binary files /dev/null and b/hug/lib/python3.12/site-packages/fontTools/cu2qu/__pycache__/__init__.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/fontTools/cu2qu/__pycache__/__main__.cpython-312.pyc b/hug/lib/python3.12/site-packages/fontTools/cu2qu/__pycache__/__main__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6500306bbabff01151d85a278a5f95697dcba9d5 Binary files /dev/null and b/hug/lib/python3.12/site-packages/fontTools/cu2qu/__pycache__/__main__.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/fontTools/cu2qu/__pycache__/benchmark.cpython-312.pyc b/hug/lib/python3.12/site-packages/fontTools/cu2qu/__pycache__/benchmark.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b8d482c301d24ff10ad18a1a96970674f55fe211 Binary files /dev/null and b/hug/lib/python3.12/site-packages/fontTools/cu2qu/__pycache__/benchmark.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/fontTools/cu2qu/__pycache__/cli.cpython-312.pyc b/hug/lib/python3.12/site-packages/fontTools/cu2qu/__pycache__/cli.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4a4f7b8af61027f1c05e3e088ab196fc622a2a72 Binary files /dev/null and b/hug/lib/python3.12/site-packages/fontTools/cu2qu/__pycache__/cli.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/fontTools/cu2qu/__pycache__/cu2qu.cpython-312.pyc b/hug/lib/python3.12/site-packages/fontTools/cu2qu/__pycache__/cu2qu.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..36a39e0fd74cae04d8a369b975af7fee1381975e Binary files /dev/null and b/hug/lib/python3.12/site-packages/fontTools/cu2qu/__pycache__/cu2qu.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/fontTools/cu2qu/__pycache__/errors.cpython-312.pyc b/hug/lib/python3.12/site-packages/fontTools/cu2qu/__pycache__/errors.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b38718a33b45cb559cde0d4c1513163976841d16 Binary files /dev/null and b/hug/lib/python3.12/site-packages/fontTools/cu2qu/__pycache__/errors.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/fontTools/cu2qu/__pycache__/ufo.cpython-312.pyc b/hug/lib/python3.12/site-packages/fontTools/cu2qu/__pycache__/ufo.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ef15e463d14fb3b020c12f5fe6757a43f9685a6e Binary files /dev/null and b/hug/lib/python3.12/site-packages/fontTools/cu2qu/__pycache__/ufo.cpython-312.pyc differ diff --git a/hug/lib/python3.12/site-packages/fontTools/cu2qu/benchmark.py b/hug/lib/python3.12/site-packages/fontTools/cu2qu/benchmark.py new file mode 100644 index 0000000000000000000000000000000000000000..007f75d887e312b68a1859546629c6410070770d --- /dev/null +++ b/hug/lib/python3.12/site-packages/fontTools/cu2qu/benchmark.py @@ -0,0 +1,54 @@ +"""Benchmark the cu2qu algorithm performance.""" + +from .cu2qu import * +import random +import timeit + +MAX_ERR = 0.05 + + +def generate_curve(): + return [ + tuple(float(random.randint(0, 2048)) for coord in range(2)) + for point in range(4) + ] + + +def setup_curve_to_quadratic(): + return generate_curve(), MAX_ERR + + +def setup_curves_to_quadratic(): + num_curves = 3 + return ([generate_curve() for curve in range(num_curves)], [MAX_ERR] * num_curves) + + +def run_benchmark(module, function, setup_suffix="", repeat=5, number=1000): + setup_func = "setup_" + function + if setup_suffix: + print("%s with %s:" % (function, setup_suffix), end="") + setup_func += "_" + setup_suffix + else: + print("%s:" % function, end="") + + def wrapper(function, setup_func): + function = globals()[function] + setup_func = globals()[setup_func] + + def wrapped(): + return function(*setup_func()) + + return wrapped + + results = timeit.repeat(wrapper(function, setup_func), repeat=repeat, number=number) + print("\t%5.1fus" % (min(results) * 1000000.0 / number)) + + +def main(): + run_benchmark("cu2qu", "curve_to_quadratic") + run_benchmark("cu2qu", "curves_to_quadratic") + + +if __name__ == "__main__": + random.seed(1) + main() diff --git a/hug/lib/python3.12/site-packages/fontTools/cu2qu/cli.py b/hug/lib/python3.12/site-packages/fontTools/cu2qu/cli.py new file mode 100644 index 0000000000000000000000000000000000000000..eacc9df78fae2b0e5bec7c610bf630bf36db8958 --- /dev/null +++ b/hug/lib/python3.12/site-packages/fontTools/cu2qu/cli.py @@ -0,0 +1,198 @@ +import os +import argparse +import logging +import shutil +import multiprocessing as mp +from contextlib import closing +from functools import partial + +import fontTools +from .ufo import font_to_quadratic, fonts_to_quadratic + +ufo_module = None +try: + import ufoLib2 as ufo_module +except ImportError: + try: + import defcon as ufo_module + except ImportError as e: + pass + + +logger = logging.getLogger("fontTools.cu2qu") + + +def _cpu_count(): + try: + return mp.cpu_count() + except NotImplementedError: # pragma: no cover + return 1 + + +def open_ufo(path): + if hasattr(ufo_module.Font, "open"): # ufoLib2 + return ufo_module.Font.open(path) + return ufo_module.Font(path) # defcon + + +def _font_to_quadratic(input_path, output_path=None, **kwargs): + ufo = open_ufo(input_path) + logger.info("Converting curves for %s", input_path) + if font_to_quadratic(ufo, **kwargs): + logger.info("Saving %s", output_path) + if output_path: + ufo.save(output_path) + else: + ufo.save() # save in-place + elif output_path: + _copytree(input_path, output_path) + + +def _samepath(path1, path2): + # TODO on python3+, there's os.path.samefile + path1 = os.path.normcase(os.path.abspath(os.path.realpath(path1))) + path2 = os.path.normcase(os.path.abspath(os.path.realpath(path2))) + return path1 == path2 + + +def _copytree(input_path, output_path): + if _samepath(input_path, output_path): + logger.debug("input and output paths are the same file; skipped copy") + return + if os.path.exists(output_path): + shutil.rmtree(output_path) + shutil.copytree(input_path, output_path) + + +def _main(args=None): + """Convert a UFO font from cubic to quadratic curves""" + parser = argparse.ArgumentParser(prog="cu2qu") + parser.add_argument("--version", action="version", version=fontTools.__version__) + parser.add_argument( + "infiles", + nargs="+", + metavar="INPUT", + help="one or more input UFO source file(s).", + ) + parser.add_argument("-v", "--verbose", action="count", default=0) + parser.add_argument( + "-e", + "--conversion-error", + type=float, + metavar="ERROR", + default=None, + help="maximum approximation error measured in EM (default: 0.001)", + ) + parser.add_argument( + "-m", + "--mixed", + default=False, + action="store_true", + help="whether to used mixed quadratic and cubic curves", + ) + parser.add_argument( + "--keep-direction", + dest="reverse_direction", + action="store_false", + help="do not reverse the contour direction", + ) + + mode_parser = parser.add_mutually_exclusive_group() + mode_parser.add_argument( + "-i", + "--interpolatable", + action="store_true", + help="whether curve conversion should keep interpolation compatibility", + ) + mode_parser.add_argument( + "-j", + "--jobs", + type=int, + nargs="?", + default=1, + const=_cpu_count(), + metavar="N", + help="convert using N multiple processes (default: %(default)s)", + ) + + output_parser = parser.add_mutually_exclusive_group() + output_parser.add_argument( + "-o", + "--output-file", + default=None, + metavar="OUTPUT", + help=( + "output filename for the converted UFO. By default fonts are " + "modified in place. This only works with a single input." + ), + ) + output_parser.add_argument( + "-d", + "--output-dir", + default=None, + metavar="DIRECTORY", + help="output directory where to save converted UFOs", + ) + + options = parser.parse_args(args) + + if ufo_module is None: + parser.error("Either ufoLib2 or defcon are required to run this script.") + + if not options.verbose: + level = "WARNING" + elif options.verbose == 1: + level = "INFO" + else: + level = "DEBUG" + logging.basicConfig(level=level) + + if len(options.infiles) > 1 and options.output_file: + parser.error("-o/--output-file can't be used with multile inputs") + + if options.output_dir: + output_dir = options.output_dir + if not os.path.exists(output_dir): + os.mkdir(output_dir) + elif not os.path.isdir(output_dir): + parser.error("'%s' is not a directory" % output_dir) + output_paths = [ + os.path.join(output_dir, os.path.basename(p)) for p in options.infiles + ] + elif options.output_file: + output_paths = [options.output_file] + else: + # save in-place + output_paths = [None] * len(options.infiles) + + kwargs = dict( + dump_stats=options.verbose > 0, + max_err_em=options.conversion_error, + reverse_direction=options.reverse_direction, + all_quadratic=False if options.mixed else True, + ) + + if options.interpolatable: + logger.info("Converting curves compatibly") + ufos = [open_ufo(infile) for infile in options.infiles] + if fonts_to_quadratic(ufos, **kwargs): + for ufo, output_path in zip(ufos, output_paths): + logger.info("Saving %s", output_path) + if output_path: + ufo.save(output_path) + else: + ufo.save() + else: + for input_path, output_path in zip(options.infiles, output_paths): + if output_path: + _copytree(input_path, output_path) + else: + jobs = min(len(options.infiles), options.jobs) if options.jobs > 1 else 1 + if jobs > 1: + func = partial(_font_to_quadratic, **kwargs) + logger.info("Running %d parallel processes", jobs) + with closing(mp.Pool(jobs)) as pool: + pool.starmap(func, zip(options.infiles, output_paths)) + else: + for input_path, output_path in zip(options.infiles, output_paths): + _font_to_quadratic(input_path, output_path, **kwargs) diff --git a/hug/lib/python3.12/site-packages/fontTools/cu2qu/cu2qu.c b/hug/lib/python3.12/site-packages/fontTools/cu2qu/cu2qu.c new file mode 100644 index 0000000000000000000000000000000000000000..011e86133c203666708b59f493362d3a12da905a --- /dev/null +++ b/hug/lib/python3.12/site-packages/fontTools/cu2qu/cu2qu.c @@ -0,0 +1,17253 @@ +/* Generated by Cython 3.3.0 */ + +/* BEGIN: Cython Metadata +{ + "distutils": { + "define_macros": [ + [ + "CYTHON_TRACE_NOGIL", + "1" + ] + ], + "libraries": [ + "m" + ], + "name": "fontTools.cu2qu.cu2qu", + "sources": [ + "Lib/fontTools/cu2qu/cu2qu.py" + ] + }, + "module_name": "fontTools.cu2qu.cu2qu" +} +END: Cython Metadata */ + +#ifndef PY_SSIZE_T_CLEAN +#define PY_SSIZE_T_CLEAN +#endif /* PY_SSIZE_T_CLEAN */ +/* InitLimitedAPI */ +#if defined(Py_LIMITED_API) + #if !defined(CYTHON_LIMITED_API) + #define CYTHON_LIMITED_API 1 + #endif +#elif defined(CYTHON_LIMITED_API) + #ifdef _MSC_VER + #pragma message ("Limited API usage is enabled with 'CYTHON_LIMITED_API' but 'Py_LIMITED_API' does not define a Python target version. Consider setting 'Py_LIMITED_API' instead.") + #else + #warning Limited API usage is enabled with 'CYTHON_LIMITED_API' but 'Py_LIMITED_API' does not define a Python target version. Consider setting 'Py_LIMITED_API' instead. + #endif +#endif + +#include "Python.h" +#ifndef Py_PYTHON_H + #error Python headers needed to compile C extensions, please install development version of Python. +#elif PY_VERSION_HEX < 0x03090000 + #error Cython requires Python 3.9+. +#elif defined(Py_LIMITED_API) && (Py_LIMITED_API & 0xFFFF0000) > (PY_VERSION_HEX & 0xFFFF0000) + #error 'Py_LIMITED_API' can only select past Python X.Y versions, not future ones. +#else +#define __PYX_ABI_VERSION "3_3_0" +#define CYTHON_HEX_VERSION 0x030300F0 +#define CYTHON_FUTURE_DIVISION 1 +/* CModulePreamble */ +#include +#ifndef offsetof + #define offsetof(type, member) ( (size_t) & ((type*)0) -> member ) +#endif +#if !defined(_WIN32) && !defined(WIN32) && !defined(MS_WINDOWS) + #ifndef __stdcall + #define __stdcall + #endif + #ifndef __cdecl + #define __cdecl + #endif + #ifndef __fastcall + #define __fastcall + #endif +#endif +#ifdef __has_builtin + #define __Pyx_has_cbuiltin(name) __has_builtin(name) +#else + #define __Pyx_has_cbuiltin(name) (0) +#endif +#ifndef DL_IMPORT + #define DL_IMPORT(t) t +#endif +#ifndef DL_EXPORT + #define DL_EXPORT(t) t +#endif +#define __PYX_COMMA , +#ifndef PY_LONG_LONG + #define PY_LONG_LONG LONG_LONG +#endif +#ifndef Py_HUGE_VAL + #define Py_HUGE_VAL HUGE_VAL +#endif +#define __PYX_LIMITED_VERSION_HEX PY_VERSION_HEX +#if defined(CYTHON_LIMITED_API) + #ifdef Py_LIMITED_API + #undef __PYX_LIMITED_VERSION_HEX + #define __PYX_LIMITED_VERSION_HEX Py_LIMITED_API + #if Py_LIMITED_API < 0x03090000 + #error "Cython 3.3 requires the Python Limited API version to be 3.9 or greater." + #endif + #endif + #if defined(GRAALVM_PYTHON) || defined(PYPY_VERSION) + #ifdef _MSC_VER + #pragma message ("Py_LIMITED_API is defined on PyPy or GraalPy. This takes precedence over Cython's specialized\ + code for PyPy and GraalPy and is unlikely to work.") + #else + #warning "Py_LIMITED_API is defined on PyPy or GraalPy. This takes precedence over Cython's specialized\ + code for PyPy and GraalPy and is unlikely to work." + #endif + #endif + #define CYTHON_COMPILING_IN_PYPY 0 + #define CYTHON_COMPILING_IN_CPYTHON 0 + #define CYTHON_COMPILING_IN_LIMITED_API 1 + #define CYTHON_COMPILING_IN_GRAAL 0 + #define CYTHON_COMPILING_IN_CPYTHON_FREETHREADING 0 + #undef CYTHON_USE_TYPE_SLOTS + #define CYTHON_USE_TYPE_SLOTS 0 + #undef CYTHON_USE_TYPE_SPECS + #define CYTHON_USE_TYPE_SPECS 1 + #undef CYTHON_USE_PYTYPE_LOOKUP + #define CYTHON_USE_PYTYPE_LOOKUP 0 + #undef CYTHON_USE_PYLIST_INTERNALS + #define CYTHON_USE_PYLIST_INTERNALS 0 + #undef CYTHON_USE_UNICODE_INTERNALS + #define CYTHON_USE_UNICODE_INTERNALS 0 + #ifndef CYTHON_USE_UNICODE_WRITER + #define CYTHON_USE_UNICODE_WRITER 0 + #endif + #undef CYTHON_USE_PYLONG_INTERNALS + #define CYTHON_USE_PYLONG_INTERNALS 0 + #ifndef CYTHON_AVOID_BORROWED_REFS + #define CYTHON_AVOID_BORROWED_REFS 0 + #endif + #ifndef CYTHON_AVOID_THREAD_UNSAFE_BORROWED_REFS + #define CYTHON_AVOID_THREAD_UNSAFE_BORROWED_REFS 0 + #endif + #undef CYTHON_ASSUME_SAFE_MACROS + #define CYTHON_ASSUME_SAFE_MACROS 0 + #undef CYTHON_ASSUME_SAFE_SIZE + #define CYTHON_ASSUME_SAFE_SIZE 0 + #undef CYTHON_UNPACK_METHODS + #define CYTHON_UNPACK_METHODS 0 + #undef CYTHON_FAST_THREAD_STATE + #define CYTHON_FAST_THREAD_STATE 0 + #undef CYTHON_FAST_GIL + #define CYTHON_FAST_GIL 0 + #undef CYTHON_VECTORCALL + #define CYTHON_VECTORCALL (__PYX_LIMITED_VERSION_HEX >= 0x030C0000) + #ifndef CYTHON_VECTORCALL_TPNEW + #define CYTHON_VECTORCALL_TPNEW (CYTHON_VECTORCALL && __PYX_LIMITED_VERSION_HEX >= 0x030E0000) + #endif + #ifndef CYTHON_PEP487_INIT_SUBCLASS + #define CYTHON_PEP487_INIT_SUBCLASS 1 + #endif + #ifndef CYTHON_PEP489_MULTI_PHASE_INIT + #define CYTHON_PEP489_MULTI_PHASE_INIT 1 + #endif + #ifndef CYTHON_USE_MODULE_STATE + #define CYTHON_USE_MODULE_STATE 0 + #endif + #undef CYTHON_USE_SYS_MONITORING + #define CYTHON_USE_SYS_MONITORING 0 + #ifndef CYTHON_USE_TP_FINALIZE + #define CYTHON_USE_TP_FINALIZE (__PYX_LIMITED_VERSION_HEX >= 0x030F0000 && PY_VERSION_HEX > 0x030F00A8) + #endif + #ifndef CYTHON_USE_AM_SEND + #define CYTHON_USE_AM_SEND (__PYX_LIMITED_VERSION_HEX >= 0x030A0000) + #endif + #undef CYTHON_USE_DICT_VERSIONS + #define CYTHON_USE_DICT_VERSIONS 0 + #undef CYTHON_USE_EXC_INFO_STACK + #define CYTHON_USE_EXC_INFO_STACK 0 + #ifndef CYTHON_UPDATE_DESCRIPTOR_DOC + #define CYTHON_UPDATE_DESCRIPTOR_DOC 0 + #endif + #ifndef CYTHON_USE_OWN_PREP_RERAISE_STAR + #define CYTHON_USE_OWN_PREP_RERAISE_STAR 1 + #endif + #ifndef CYTHON_USE_FREELISTS + #define CYTHON_USE_FREELISTS 1 + #endif + #undef CYTHON_IMMORTAL_CONSTANTS + #define CYTHON_IMMORTAL_CONSTANTS 0 + #if __PYX_LIMITED_VERSION_HEX < 0x030E0000 + #undef CYTHON_OPAQUE_OBJECTS + #define CYTHON_OPAQUE_OBJECTS 0 + #elif !defined(CYTHON_OPAQUE_OBJECTS) + #define CYTHON_OPAQUE_OBJECTS (__PYX_LIMITED_VERSION_HEX >= 0x030F0000) + #endif +#elif defined(GRAALVM_PYTHON) + /* For very preliminary testing purposes. Most variables are set the same as PyPy. + The existence of this section does not imply that anything works or is even tested */ + #define CYTHON_COMPILING_IN_PYPY 0 + #define CYTHON_COMPILING_IN_CPYTHON 0 + #define CYTHON_COMPILING_IN_LIMITED_API 0 + #define CYTHON_COMPILING_IN_GRAAL 1 + #define CYTHON_COMPILING_IN_CPYTHON_FREETHREADING 0 + #ifndef CYTHON_USE_TYPE_SLOTS + #define CYTHON_USE_TYPE_SLOTS 0 + #endif + #undef CYTHON_USE_TYPE_SPECS + #define CYTHON_USE_TYPE_SPECS 0 + #undef CYTHON_USE_PYTYPE_LOOKUP + #define CYTHON_USE_PYTYPE_LOOKUP 0 + #undef CYTHON_USE_PYLIST_INTERNALS + #define CYTHON_USE_PYLIST_INTERNALS 0 + #undef CYTHON_USE_UNICODE_INTERNALS + #define CYTHON_USE_UNICODE_INTERNALS 0 + #undef CYTHON_USE_UNICODE_WRITER + #define CYTHON_USE_UNICODE_WRITER 0 + #undef CYTHON_USE_PYLONG_INTERNALS + #define CYTHON_USE_PYLONG_INTERNALS 0 + #undef CYTHON_AVOID_BORROWED_REFS + #define CYTHON_AVOID_BORROWED_REFS 1 + #undef CYTHON_AVOID_THREAD_UNSAFE_BORROWED_REFS + #define CYTHON_AVOID_THREAD_UNSAFE_BORROWED_REFS 0 + #undef CYTHON_ASSUME_SAFE_MACROS + #define CYTHON_ASSUME_SAFE_MACROS 0 + #undef CYTHON_ASSUME_SAFE_SIZE + #define CYTHON_ASSUME_SAFE_SIZE 0 + #undef CYTHON_UNPACK_METHODS + #define CYTHON_UNPACK_METHODS 0 + #undef CYTHON_FAST_THREAD_STATE + #define CYTHON_FAST_THREAD_STATE 0 + #undef CYTHON_FAST_GIL + #define CYTHON_FAST_GIL 0 + #ifndef CYTHON_VECTORCALL + #define CYTHON_VECTORCALL 1 + #endif + #if CYTHON_USE_TYPE_SPECS && PY_VERSION_HEX < 0x030E0000 + #undef CYTHON_VECTORCALL_TPNEW + #define CYTHON_VECTORCALL_TPNEW 0 + #elif !defined(CYTHON_VECTORCALL_TPNEW) + #define CYTHON_VECTORCALL_TPNEW CYTHON_VECTORCALL + #endif + #ifndef CYTHON_PEP487_INIT_SUBCLASS + #define CYTHON_PEP487_INIT_SUBCLASS 1 + #endif + #undef CYTHON_PEP489_MULTI_PHASE_INIT + #define CYTHON_PEP489_MULTI_PHASE_INIT 1 + #undef CYTHON_USE_MODULE_STATE + #define CYTHON_USE_MODULE_STATE 0 + #undef CYTHON_USE_SYS_MONITORING + #define CYTHON_USE_SYS_MONITORING 0 + #undef CYTHON_USE_TP_FINALIZE + #define CYTHON_USE_TP_FINALIZE 0 + #undef CYTHON_USE_AM_SEND + #define CYTHON_USE_AM_SEND 0 + #undef CYTHON_USE_DICT_VERSIONS + #define CYTHON_USE_DICT_VERSIONS 0 + #undef CYTHON_USE_EXC_INFO_STACK + #define CYTHON_USE_EXC_INFO_STACK 1 + #ifndef CYTHON_UPDATE_DESCRIPTOR_DOC + #define CYTHON_UPDATE_DESCRIPTOR_DOC 0 + #endif + #ifndef CYTHON_USE_OWN_PREP_RERAISE_STAR + #define CYTHON_USE_OWN_PREP_RERAISE_STAR 1 + #endif + #undef CYTHON_USE_FREELISTS + #define CYTHON_USE_FREELISTS 0 + #undef CYTHON_IMMORTAL_CONSTANTS + #define CYTHON_IMMORTAL_CONSTANTS 0 + #undef CYTHON_OPAQUE_OBJECTS + #define CYTHON_OPAQUE_OBJECTS 0 +#elif defined(PYPY_VERSION) + #define CYTHON_COMPILING_IN_PYPY 1 + #define CYTHON_COMPILING_IN_CPYTHON 0 + #define CYTHON_COMPILING_IN_LIMITED_API 0 + #define CYTHON_COMPILING_IN_GRAAL 0 + #define CYTHON_COMPILING_IN_CPYTHON_FREETHREADING 0 + #undef CYTHON_USE_TYPE_SLOTS + #define CYTHON_USE_TYPE_SLOTS 1 + #ifndef CYTHON_USE_TYPE_SPECS + #define CYTHON_USE_TYPE_SPECS 0 + #endif + #undef CYTHON_USE_PYTYPE_LOOKUP + #define CYTHON_USE_PYTYPE_LOOKUP 0 + #undef CYTHON_USE_PYLIST_INTERNALS + #define CYTHON_USE_PYLIST_INTERNALS 0 + #undef CYTHON_USE_UNICODE_INTERNALS + #define CYTHON_USE_UNICODE_INTERNALS 0 + #undef CYTHON_USE_UNICODE_WRITER + #define CYTHON_USE_UNICODE_WRITER 0 + #undef CYTHON_USE_PYLONG_INTERNALS + #define CYTHON_USE_PYLONG_INTERNALS 0 + #undef CYTHON_AVOID_BORROWED_REFS + #define CYTHON_AVOID_BORROWED_REFS 1 + #undef CYTHON_AVOID_THREAD_UNSAFE_BORROWED_REFS + #define CYTHON_AVOID_THREAD_UNSAFE_BORROWED_REFS 1 + #undef CYTHON_ASSUME_SAFE_MACROS + #define CYTHON_ASSUME_SAFE_MACROS 0 + #ifndef CYTHON_ASSUME_SAFE_SIZE + #define CYTHON_ASSUME_SAFE_SIZE 1 + #endif + #undef CYTHON_UNPACK_METHODS + #define CYTHON_UNPACK_METHODS 0 + #undef CYTHON_FAST_THREAD_STATE + #define CYTHON_FAST_THREAD_STATE 0 + #undef CYTHON_FAST_GIL + #define CYTHON_FAST_GIL 0 + #ifndef CYTHON_VECTORCALL + #define CYTHON_VECTORCALL 1 + #endif + #if CYTHON_USE_TYPE_SPECS && PY_VERSION_HEX < 0x030E0000 + #undef CYTHON_VECTORCALL_TPNEW + #define CYTHON_VECTORCALL_TPNEW 0 + #elif !defined(CYTHON_VECTORCALL_TPNEW) + #define CYTHON_VECTORCALL_TPNEW (PYPY_VERSION_NUM >= 0x07030800 && CYTHON_VECTORCALL) + #endif + #ifndef CYTHON_PEP487_INIT_SUBCLASS + #define CYTHON_PEP487_INIT_SUBCLASS 1 + #endif + #ifndef CYTHON_PEP489_MULTI_PHASE_INIT + #define CYTHON_PEP489_MULTI_PHASE_INIT 1 + #endif + #undef CYTHON_USE_MODULE_STATE + #define CYTHON_USE_MODULE_STATE 0 + #undef CYTHON_USE_SYS_MONITORING + #define CYTHON_USE_SYS_MONITORING 0 + #ifndef CYTHON_USE_TP_FINALIZE + #define CYTHON_USE_TP_FINALIZE (PYPY_VERSION_NUM >= 0x07030C00) + #endif + #undef CYTHON_USE_AM_SEND + #define CYTHON_USE_AM_SEND 0 + #undef CYTHON_USE_DICT_VERSIONS + #define CYTHON_USE_DICT_VERSIONS 0 + #undef CYTHON_USE_EXC_INFO_STACK + #define CYTHON_USE_EXC_INFO_STACK 0 + #ifndef CYTHON_UPDATE_DESCRIPTOR_DOC + #define CYTHON_UPDATE_DESCRIPTOR_DOC (PYPY_VERSION_NUM >= 0x07031100) + #endif + #ifndef CYTHON_USE_OWN_PREP_RERAISE_STAR + #define CYTHON_USE_OWN_PREP_RERAISE_STAR 1 + #endif + #undef CYTHON_USE_FREELISTS + #define CYTHON_USE_FREELISTS 0 + #undef CYTHON_IMMORTAL_CONSTANTS + #define CYTHON_IMMORTAL_CONSTANTS 0 + #undef CYTHON_OPAQUE_OBJECTS + #define CYTHON_OPAQUE_OBJECTS 0 +#else + #define CYTHON_COMPILING_IN_PYPY 0 + #define CYTHON_COMPILING_IN_CPYTHON 1 + #define CYTHON_COMPILING_IN_LIMITED_API 0 + #define CYTHON_COMPILING_IN_GRAAL 0 + #ifdef Py_GIL_DISABLED + #define CYTHON_COMPILING_IN_CPYTHON_FREETHREADING 1 + #else + #define CYTHON_COMPILING_IN_CPYTHON_FREETHREADING 0 + #endif + #if PY_VERSION_HEX < 0x030A0000 + #undef CYTHON_USE_TYPE_SLOTS + #define CYTHON_USE_TYPE_SLOTS 1 + #elif !defined(CYTHON_USE_TYPE_SLOTS) + #define CYTHON_USE_TYPE_SLOTS 1 + #endif + #ifndef CYTHON_USE_TYPE_SPECS + #define CYTHON_USE_TYPE_SPECS 0 + #endif + #ifndef CYTHON_USE_PYTYPE_LOOKUP + #define CYTHON_USE_PYTYPE_LOOKUP 1 + #endif + #ifndef CYTHON_USE_PYLONG_INTERNALS + #define CYTHON_USE_PYLONG_INTERNALS 1 + #endif + #if CYTHON_COMPILING_IN_CPYTHON_FREETHREADING + #undef CYTHON_USE_PYLIST_INTERNALS + #define CYTHON_USE_PYLIST_INTERNALS 0 + #elif !defined(CYTHON_USE_PYLIST_INTERNALS) + #define CYTHON_USE_PYLIST_INTERNALS 1 + #endif + #ifndef CYTHON_USE_UNICODE_INTERNALS + #define CYTHON_USE_UNICODE_INTERNALS 1 + #endif + #if CYTHON_COMPILING_IN_CPYTHON_FREETHREADING || PY_VERSION_HEX >= 0x030B00A2 + #undef CYTHON_USE_UNICODE_WRITER + #define CYTHON_USE_UNICODE_WRITER 0 + #elif !defined(CYTHON_USE_UNICODE_WRITER) + #define CYTHON_USE_UNICODE_WRITER 1 + #endif + #ifndef CYTHON_AVOID_BORROWED_REFS + #define CYTHON_AVOID_BORROWED_REFS 0 + #endif + #if CYTHON_COMPILING_IN_CPYTHON_FREETHREADING + #undef CYTHON_AVOID_THREAD_UNSAFE_BORROWED_REFS + #define CYTHON_AVOID_THREAD_UNSAFE_BORROWED_REFS 1 + #elif !defined(CYTHON_AVOID_THREAD_UNSAFE_BORROWED_REFS) + #define CYTHON_AVOID_THREAD_UNSAFE_BORROWED_REFS 0 + #endif + #ifndef CYTHON_ASSUME_SAFE_MACROS + #define CYTHON_ASSUME_SAFE_MACROS 1 + #endif + #ifndef CYTHON_ASSUME_SAFE_SIZE + #define CYTHON_ASSUME_SAFE_SIZE 1 + #endif + #ifndef CYTHON_UNPACK_METHODS + #define CYTHON_UNPACK_METHODS 1 + #endif + #ifndef CYTHON_FAST_THREAD_STATE + #define CYTHON_FAST_THREAD_STATE 1 + #endif + #if CYTHON_COMPILING_IN_CPYTHON_FREETHREADING + #undef CYTHON_FAST_GIL + #define CYTHON_FAST_GIL 0 + #elif !defined(CYTHON_FAST_GIL) + #define CYTHON_FAST_GIL (PY_VERSION_HEX < 0x030C00A6) + #endif + #ifndef CYTHON_VECTORCALL + #define CYTHON_VECTORCALL 1 + #endif + #if CYTHON_USE_TYPE_SPECS && PY_VERSION_HEX < 0x030E0000 + #undef CYTHON_VECTORCALL_TPNEW + #define CYTHON_VECTORCALL_TPNEW 0 + #elif !defined(CYTHON_VECTORCALL_TPNEW) + #define CYTHON_VECTORCALL_TPNEW CYTHON_VECTORCALL + #endif + #ifndef CYTHON_PEP487_INIT_SUBCLASS + #define CYTHON_PEP487_INIT_SUBCLASS 1 + #endif + #ifndef CYTHON_PEP489_MULTI_PHASE_INIT + #define CYTHON_PEP489_MULTI_PHASE_INIT 1 + #endif + #ifndef CYTHON_USE_MODULE_STATE + #define CYTHON_USE_MODULE_STATE 0 + #endif + #ifndef CYTHON_USE_SYS_MONITORING + #define CYTHON_USE_SYS_MONITORING (PY_VERSION_HEX >= 0x030d00B1) + #endif + #ifndef CYTHON_USE_TP_FINALIZE + #define CYTHON_USE_TP_FINALIZE 1 + #endif + #ifndef CYTHON_USE_AM_SEND + #define CYTHON_USE_AM_SEND 1 + #endif + #if CYTHON_COMPILING_IN_CPYTHON_FREETHREADING + #undef CYTHON_USE_DICT_VERSIONS + #define CYTHON_USE_DICT_VERSIONS 0 + #elif !defined(CYTHON_USE_DICT_VERSIONS) + #define CYTHON_USE_DICT_VERSIONS (PY_VERSION_HEX < 0x030C00A5 && !CYTHON_USE_MODULE_STATE) + #endif + #ifndef CYTHON_USE_EXC_INFO_STACK + #define CYTHON_USE_EXC_INFO_STACK 1 + #endif + #ifndef CYTHON_UPDATE_DESCRIPTOR_DOC + #define CYTHON_UPDATE_DESCRIPTOR_DOC 1 + #endif + #ifndef CYTHON_USE_OWN_PREP_RERAISE_STAR + #define CYTHON_USE_OWN_PREP_RERAISE_STAR (PY_VERSION_HEX < 0x030C00B2) + #endif + #ifndef CYTHON_USE_FREELISTS + #define CYTHON_USE_FREELISTS (!CYTHON_COMPILING_IN_CPYTHON_FREETHREADING) + #endif + #if defined(CYTHON_IMMORTAL_CONSTANTS) && PY_VERSION_HEX < 0x030C0000 + #undef CYTHON_IMMORTAL_CONSTANTS + #define CYTHON_IMMORTAL_CONSTANTS 0 // definitely won't work + #elif !defined(CYTHON_IMMORTAL_CONSTANTS) + #define CYTHON_IMMORTAL_CONSTANTS (PY_VERSION_HEX >= 0x030C0000 && !CYTHON_USE_MODULE_STATE && CYTHON_COMPILING_IN_CPYTHON_FREETHREADING) + #endif + #ifndef CYTHON_OPAQUE_OBJECTS + #define CYTHON_OPAQUE_OBJECTS 0 + #endif +#endif +#if CYTHON_USE_PYLONG_INTERNALS + #undef SHIFT + #undef BASE + #undef MASK + #ifdef SIZEOF_VOID_P + enum { __pyx_check_sizeof_voidp = 1 / (int)(SIZEOF_VOID_P == sizeof(void*)) }; + #endif +#endif +#ifndef __has_attribute + #define __has_attribute(x) 0 +#endif +#ifndef __has_cpp_attribute + #define __has_cpp_attribute(x) 0 +#endif +#ifndef CYTHON_RESTRICT + #if defined(__GNUC__) + #define CYTHON_RESTRICT __restrict__ + #elif defined(_MSC_VER) && _MSC_VER >= 1400 + #define CYTHON_RESTRICT __restrict + #elif defined (__STDC_VERSION__) && __STDC_VERSION__ >= 199901L + #define CYTHON_RESTRICT restrict + #else + #define CYTHON_RESTRICT + #endif +#endif +#ifndef CYTHON_UNUSED + #if defined(__cplusplus) + /* for clang __has_cpp_attribute(maybe_unused) is true even before C++17 + * but leads to warnings with -pedantic, since it is a C++17 feature */ + #if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || __cplusplus >= 201703L) + #if __has_cpp_attribute(maybe_unused) + #define CYTHON_UNUSED [[maybe_unused]] + #endif + #endif + #elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L + #define CYTHON_UNUSED [[maybe_unused]] + #endif +#endif +#ifndef CYTHON_UNUSED +# if defined(__GNUC__) +# if !(defined(__cplusplus)) || (__GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ >= 4)) +# define CYTHON_UNUSED __attribute__ ((__unused__)) +# else +# define CYTHON_UNUSED +# endif +# elif defined(__ICC) || (defined(__INTEL_COMPILER) && !defined(_MSC_VER)) +# define CYTHON_UNUSED __attribute__ ((__unused__)) +# else +# define CYTHON_UNUSED +# endif +#endif +#ifndef CYTHON_UNUSED_VAR +# if defined(__cplusplus) + template void CYTHON_UNUSED_VAR( const T& ) { } +# else +# define CYTHON_UNUSED_VAR(x) (void)(x) +# endif +#endif +#ifndef CYTHON_MAYBE_UNUSED_VAR + #define CYTHON_MAYBE_UNUSED_VAR(x) CYTHON_UNUSED_VAR(x) +#endif +#ifndef CYTHON_NCP_UNUSED +# if CYTHON_COMPILING_IN_CPYTHON && !CYTHON_COMPILING_IN_CPYTHON_FREETHREADING +# define CYTHON_NCP_UNUSED +# else +# define CYTHON_NCP_UNUSED CYTHON_UNUSED +# endif +#endif +#ifndef CYTHON_USE_CPP_STD_MOVE + #if defined(__cplusplus) && (\ + __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1600)) + #define CYTHON_USE_CPP_STD_MOVE 1 + #else + #define CYTHON_USE_CPP_STD_MOVE 0 + #endif +#endif +#define __Pyx_void_to_None(void_result) ((void)(void_result), Py_INCREF(Py_None), Py_None) +#include +typedef uintptr_t __pyx_uintptr_t; +#ifndef CYTHON_FALLTHROUGH + #if defined(__cplusplus) + /* for clang __has_cpp_attribute(fallthrough) is true even before C++17 + * but leads to warnings with -pedantic, since it is a C++17 feature */ + #if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || __cplusplus >= 201703L) + #if __has_cpp_attribute(fallthrough) + #define CYTHON_FALLTHROUGH [[fallthrough]] + #endif + #endif + #ifndef CYTHON_FALLTHROUGH + #if __has_cpp_attribute(clang::fallthrough) + #define CYTHON_FALLTHROUGH [[clang::fallthrough]] + #elif __has_cpp_attribute(gnu::fallthrough) + #define CYTHON_FALLTHROUGH [[gnu::fallthrough]] + #endif + #endif + #endif + #ifndef CYTHON_FALLTHROUGH + #if __has_attribute(fallthrough) + #define CYTHON_FALLTHROUGH __attribute__((fallthrough)) + #else + #define CYTHON_FALLTHROUGH + #endif + #endif + #if defined(__clang__) && defined(__apple_build_version__) + #if __apple_build_version__ < 7000000 + #undef CYTHON_FALLTHROUGH + #define CYTHON_FALLTHROUGH + #endif + #endif +#endif +#ifdef Py_UNREACHABLE + #define __Pyx_UNREACHABLE() Py_UNREACHABLE() +#elif __Pyx_has_cbuiltin(__builtin_unreachable) + #define __Pyx_UNREACHABLE() __builtin_unreachable() +#elif defined(__clang__) || defined(__INTEL_COMPILER) || (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 5))) + #define __Pyx_UNREACHABLE() __builtin_unreachable() +#elif defined(_MSC_VER) + #define __Pyx_UNREACHABLE() __assume(0) +#else + #define __Pyx_UNREACHABLE() Py_FatalError("Unreachable C code path reached") +#endif +#ifndef Py_UNREACHABLE + #define Py_UNREACHABLE() __Pyx_UNREACHABLE() +#endif +#ifdef __cplusplus + template + struct __PYX_IS_UNSIGNED_IMPL {static const bool value = T(0) < T(-1);}; + #define __PYX_IS_UNSIGNED(type) (__PYX_IS_UNSIGNED_IMPL::value) +#else + #define __PYX_IS_UNSIGNED(type) (((type)-1) > 0) +#endif +#if CYTHON_COMPILING_IN_PYPY == 1 + #define __PYX_NEED_TP_PRINT_SLOT (PY_VERSION_HEX < 0x030A0000) +#else + #define __PYX_NEED_TP_PRINT_SLOT 0 +#endif +#define __PYX_REINTERPRET_FUNCION(func_pointer, other_pointer) ((func_pointer)(void(*)(void))(other_pointer)) +#if __PYX_LIMITED_VERSION_HEX < 0x030C0000 +#define __Pyx_PyErr_FetchException(petype, peval, petb) PyErr_Fetch(petype, peval, petb) +#define __Pyx_PyErr_RestoreException(etype, eval, etb) PyErr_Restore(etype, eval, etb) +#else +#define __Pyx_PyErr_FetchException(petype, peval, petb) *(petype)=NULL; *(peval)=PyErr_GetRaisedException(); *(petb)=NULL +#define __Pyx_PyErr_RestoreException(etype, eval, etb) PyErr_SetRaisedException(eval) +#endif + +/* CInitCode */ +#ifndef CYTHON_INLINE + #if defined(__clang__) + #define CYTHON_INLINE __inline__ __attribute__ ((__unused__)) + #elif defined(__GNUC__) + #define CYTHON_INLINE __inline__ + #elif defined(_MSC_VER) + #define CYTHON_INLINE __inline + #elif defined (__STDC_VERSION__) && __STDC_VERSION__ >= 199901L + #define CYTHON_INLINE inline + #else + #define CYTHON_INLINE + #endif +#endif + +/* PythonCompatibility */ +#define __PYX_BUILD_PY_SSIZE_T "n" +#define CYTHON_FORMAT_SSIZE_T "z" +#define __Pyx_BUILTIN_MODULE_NAME "builtins" +#define __Pyx_DefaultClassType PyType_Type +#if CYTHON_COMPILING_IN_LIMITED_API + #ifndef CO_OPTIMIZED + static int CO_OPTIMIZED; + #endif + #ifndef CO_NEWLOCALS + static int CO_NEWLOCALS; + #endif + #ifndef CO_VARARGS + static int CO_VARARGS; + #endif + #ifndef CO_VARKEYWORDS + static int CO_VARKEYWORDS; + #endif + #ifndef CO_ASYNC_GENERATOR + static int CO_ASYNC_GENERATOR; + #endif + #ifndef CO_GENERATOR + static int CO_GENERATOR; + #endif + #ifndef CO_COROUTINE + static int CO_COROUTINE; + #endif +#else + #ifndef CO_COROUTINE + #define CO_COROUTINE 0x80 + #endif + #ifndef CO_ASYNC_GENERATOR + #define CO_ASYNC_GENERATOR 0x200 + #endif +#endif +static int __Pyx_init_co_variables(void); +#if PY_VERSION_HEX >= 0x030A00B1 || defined(Py_Is) + #define __Pyx_Py_Is(x, y) Py_Is(x, y) +#else + #define __Pyx_Py_Is(x, y) ((x) == (y)) +#endif +#if PY_VERSION_HEX >= 0x030A00B1 || defined(Py_IsNone) + #define __Pyx_Py_IsNone(ob) Py_IsNone(ob) +#else + #define __Pyx_Py_IsNone(ob) __Pyx_Py_Is((ob), Py_None) +#endif +#if PY_VERSION_HEX >= 0x030A00B1 || defined(Py_IsTrue) + #define __Pyx_Py_IsTrue(ob) Py_IsTrue(ob) +#else + #define __Pyx_Py_IsTrue(ob) __Pyx_Py_Is((ob), Py_True) +#endif +#if PY_VERSION_HEX >= 0x030A00B1 || defined(Py_IsFalse) + #define __Pyx_Py_IsFalse(ob) Py_IsFalse(ob) +#else + #define __Pyx_Py_IsFalse(ob) __Pyx_Py_Is((ob), Py_False) +#endif +#define __Pyx_NoneAsNull(obj) (__Pyx_Py_IsNone(obj) ? NULL : (obj)) +#if CYTHON_COMPILING_IN_PYPY + #define __Pyx_PyObject_GC_IsFinalized(o) _PyGC_FINALIZED(o) +#else + #define __Pyx_PyObject_GC_IsFinalized(o) PyObject_GC_IsFinalized(o) +#endif +#if CYTHON_COMPILING_IN_LIMITED_API +static unsigned long __Pyx_Runtime_TPFLAGS_SEQUENCE; +static unsigned long __Pyx_Runtime_TPFLAGS_MAPPING; +#else +#define __Pyx_Runtime_TPFLAGS_SEQUENCE Py_TPFLAGS_SEQUENCE +#define __Pyx_Runtime_TPFLAGS_MAPPING Py_TPFLAGS_MAPPING +#endif +static int __Pyx_init_tpflags_variables(void); +#ifndef Py_TPFLAGS_HAVE_FINALIZE + #define Py_TPFLAGS_HAVE_FINALIZE 0 +#endif +#ifndef Py_TPFLAGS_SEQUENCE + #define Py_TPFLAGS_SEQUENCE (CYTHON_COMPILING_IN_LIMITED_API ? 0 : 1 << 5) +#endif +#ifndef Py_TPFLAGS_MAPPING + #define Py_TPFLAGS_MAPPING (CYTHON_COMPILING_IN_LIMITED_API ? 0 : 1 << 6) +#endif +#ifndef Py_TPFLAGS_IMMUTABLETYPE + #define Py_TPFLAGS_IMMUTABLETYPE (1UL << 8) +#endif +#ifndef Py_TPFLAGS_DISALLOW_INSTANTIATION + #define Py_TPFLAGS_DISALLOW_INSTANTIATION (1UL << 7) +#endif +#ifndef METH_STACKLESS + #define METH_STACKLESS 0 +#endif +#if !defined(METH_FASTCALL) || CYTHON_COMPILING_IN_PYPY + #ifndef METH_FASTCALL + #define METH_FASTCALL 0x80 + #endif + typedef PyObject *(*__Pyx_PyCFunctionFast) (PyObject *self, PyObject *const *args, Py_ssize_t nargs); + typedef PyObject *(*__Pyx_PyCFunctionFastWithKeywords) (PyObject *self, PyObject *const *args, + Py_ssize_t nargs, PyObject *kwnames); +#else + #if PY_VERSION_HEX >= 0x030d00A4 + # define __Pyx_PyCFunctionFast PyCFunctionFast + # define __Pyx_PyCFunctionFastWithKeywords PyCFunctionFastWithKeywords + #else + # define __Pyx_PyCFunctionFast _PyCFunctionFast + # define __Pyx_PyCFunctionFastWithKeywords _PyCFunctionFastWithKeywords + #endif +#endif +#if CYTHON_VECTORCALL + #define __Pyx_METH_FASTCALL METH_FASTCALL + #define __Pyx_PyCFunction_FastCall __Pyx_PyCFunctionFast + #define __Pyx_PyCFunction_FastCallWithKeywords __Pyx_PyCFunctionFastWithKeywords +#else + #define __Pyx_METH_FASTCALL METH_VARARGS + #define __Pyx_PyCFunction_FastCall PyCFunction + #define __Pyx_PyCFunction_FastCallWithKeywords PyCFunctionWithKeywords +#endif +#if CYTHON_VECTORCALL + #define __pyx_vectorcallfunc vectorcallfunc + #define __Pyx_PY_VECTORCALL_ARGUMENTS_OFFSET PY_VECTORCALL_ARGUMENTS_OFFSET + #define __Pyx_PyVectorcall_NARGS(n) PyVectorcall_NARGS((size_t)(n)) +#else + #define __Pyx_PY_VECTORCALL_ARGUMENTS_OFFSET 0 + #define __Pyx_PyVectorcall_NARGS(n) ((Py_ssize_t)(n)) +#endif +#define __Pyx_PyCFunction_CheckExact(func) PyCFunction_CheckExact(func) +#define __Pyx_CyOrPyCFunction_Check(func) PyCFunction_Check(func) +#if CYTHON_COMPILING_IN_CPYTHON +#define __Pyx_CyOrPyCFunction_GET_FUNCTION(func) (((PyCFunctionObject*)(func))->m_ml->ml_meth) +#elif !CYTHON_COMPILING_IN_LIMITED_API +#define __Pyx_CyOrPyCFunction_GET_FUNCTION(func) PyCFunction_GET_FUNCTION(func) +#endif +#if CYTHON_COMPILING_IN_CPYTHON +#define __Pyx_CyOrPyCFunction_GET_FLAGS(func) (((PyCFunctionObject*)(func))->m_ml->ml_flags) +static CYTHON_INLINE PyObject* __Pyx_CyOrPyCFunction_GET_SELF(PyObject *func) { + return (__Pyx_CyOrPyCFunction_GET_FLAGS(func) & METH_STATIC) ? NULL : ((PyCFunctionObject*)func)->m_self; +} +#endif +static CYTHON_INLINE int __Pyx__IsSameCFunction(PyObject *func, void (*cfunc)(void)) { +#if CYTHON_COMPILING_IN_LIMITED_API + return PyCFunction_Check(func) && PyCFunction_GetFunction(func) == (PyCFunction) cfunc; +#else + return PyCFunction_Check(func) && PyCFunction_GET_FUNCTION(func) == (PyCFunction) cfunc; +#endif +} +#define __Pyx_IsSameCFunction(func, cfunc) __Pyx__IsSameCFunction(func, cfunc) +#if CYTHON_COMPILING_IN_LIMITED_API && __PYX_LIMITED_VERSION_HEX < 0x030A0000 + #define __Pyx_PyType_FromModuleAndSpec(m, s, b) ((void)m, PyType_FromSpecWithBases(s, b)) +#else + #define __Pyx_PyType_FromModuleAndSpec(m, s, b) PyType_FromModuleAndSpec(m, s, b) +#endif +#if CYTHON_COMPILING_IN_PYPY + typedef PyObject *(*__Pyx_PyCMethod)(PyObject *, PyTypeObject *, PyObject *const *, size_t, PyObject *); +#else + #define __Pyx_PyCMethod PyCMethod +#endif +#ifndef METH_METHOD + #define METH_METHOD 0x200 +#endif +#if CYTHON_COMPILING_IN_PYPY && !defined(PyObject_Malloc) + #define PyObject_Malloc(s) PyMem_Malloc(s) + #define PyObject_Free(p) PyMem_Free(p) + #define PyObject_Realloc(p) PyMem_Realloc(p) +#endif +#if CYTHON_COMPILING_IN_LIMITED_API + #define __Pyx_PyFrame_SetLineNumber(frame, lineno) +#elif CYTHON_COMPILING_IN_GRAAL && defined(GRAALPY_VERSION_NUM) && GRAALPY_VERSION_NUM > 0x19000000 + #define __Pyx_PyCode_HasFreeVars(co) (PyCode_GetNumFree(co) > 0) + #define __Pyx_PyFrame_SetLineNumber(frame, lineno) GraalPyFrame_SetLineNumber((frame), (lineno)) +#elif CYTHON_COMPILING_IN_GRAAL + #define __Pyx_PyCode_HasFreeVars(co) (PyCode_GetNumFree(co) > 0) + #define __Pyx_PyFrame_SetLineNumber(frame, lineno) _PyFrame_SetLineNumber((frame), (lineno)) +#else + #define __Pyx_PyCode_HasFreeVars(co) (PyCode_GetNumFree(co) > 0) + #define __Pyx_PyFrame_SetLineNumber(frame, lineno) (frame)->f_lineno = (lineno) +#endif +#if CYTHON_COMPILING_IN_LIMITED_API + #define __Pyx_PyThreadState_Current PyThreadState_Get() +#elif !CYTHON_FAST_THREAD_STATE + #define __Pyx_PyThreadState_Current PyThreadState_GET() +#elif PY_VERSION_HEX >= 0x030d00A1 + #define __Pyx_PyThreadState_Current PyThreadState_GetUnchecked() +#else + #define __Pyx_PyThreadState_Current _PyThreadState_UncheckedGet() +#endif +#if CYTHON_OPAQUE_OBJECTS && CYTHON_COMPILING_IN_LIMITED_API + #define __PYX_SHARED_SIZEOF(T) -((int)sizeof(T)) + #define __PYX_SHARED_RELATIVE_OFFSET Py_RELATIVE_OFFSET + #define CYTHON_OPAQUE_SHARED_TYPES 1 +#else + #define __PYX_SHARED_SIZEOF(T) sizeof(T) + #define __PYX_SHARED_RELATIVE_OFFSET 0 + #define CYTHON_OPAQUE_SHARED_TYPES 0 +#endif +#if CYTHON_USE_MODULE_STATE +static CYTHON_INLINE void *__Pyx__PyModule_GetState(PyObject *op) +{ + void *result; + result = PyModule_GetState(op); + if (!result) + Py_FatalError("Couldn't find the module state"); + return result; +} +#define __Pyx_PyModule_GetState(o) (__pyx_mstatetype *)__Pyx__PyModule_GetState(o) +#else +#define __Pyx_PyModule_GetState(op) ((void)op,__pyx_mstate_global) +#endif +#define __Pyx_PyObject_GetSlot(obj, name, func_ctype) __Pyx_PyType_GetSlot(Py_TYPE((PyObject *) obj), name, func_ctype) +#define __Pyx_PyObject_TryGetSlot(obj, name, func_ctype) __Pyx_PyType_TryGetSlot(Py_TYPE(obj), name, func_ctype) +#define __Pyx_PyObject_GetSubSlot(obj, sub, name, func_ctype) __Pyx_PyType_GetSubSlot(Py_TYPE(obj), sub, name, func_ctype) +#define __Pyx_PyObject_TryGetSubSlot(obj, sub, name, func_ctype) __Pyx_PyType_TryGetSubSlot(Py_TYPE(obj), sub, name, func_ctype) +#if CYTHON_USE_TYPE_SLOTS + #define __Pyx_PyType_GetSlot(type, name, func_ctype) ((type)->name) + #define __Pyx_PyType_TryGetSlot(type, name, func_ctype) __Pyx_PyType_GetSlot(type, name, func_ctype) + #define __Pyx_PyType_GetSubSlot(type, sub, name, func_ctype) (((type)->sub) ? ((type)->sub->name) : NULL) + #define __Pyx_PyType_TryGetSubSlot(type, sub, name, func_ctype) __Pyx_PyType_GetSubSlot(type, sub, name, func_ctype) +#else + #define __Pyx_PyType_GetSlot(type, name, func_ctype) ((func_ctype) PyType_GetSlot((type), Py_##name)) + #define __Pyx_PyType_TryGetSlot(type, name, func_ctype)\ + ((__PYX_LIMITED_VERSION_HEX >= 0x030A0000 ||\ + (PyType_GetFlags(type) & Py_TPFLAGS_HEAPTYPE) || __Pyx_get_runtime_version() >= 0x030A0000) ?\ + __Pyx_PyType_GetSlot(type, name, func_ctype) : NULL) + #define __Pyx_PyType_GetSubSlot(obj, sub, name, func_ctype) __Pyx_PyType_GetSlot(obj, name, func_ctype) + #define __Pyx_PyType_TryGetSubSlot(obj, sub, name, func_ctype) __Pyx_PyType_TryGetSlot(obj, name, func_ctype) +#endif +#if CYTHON_COMPILING_IN_CPYTHON || defined(_PyDict_NewPresized) +#define __Pyx_PyDict_NewPresized(n) ((n <= 8) ? PyDict_New() : _PyDict_NewPresized(n)) +#else +#define __Pyx_PyDict_NewPresized(n) PyDict_New() +#endif +#define __Pyx_PyNumber_Divide(x,y) PyNumber_TrueDivide(x,y) +#define __Pyx_PyNumber_InPlaceDivide(x,y) PyNumber_InPlaceTrueDivide(x,y) +#if CYTHON_COMPILING_IN_CPYTHON && CYTHON_USE_UNICODE_INTERNALS +#define __Pyx_PyDict_GetItemStrWithError(dict, name) _PyDict_GetItem_KnownHash(dict, name, ((PyASCIIObject *) name)->hash) +static CYTHON_INLINE PyObject * __Pyx_PyDict_GetItemStr(PyObject *dict, PyObject *name) { + PyObject *res = __Pyx_PyDict_GetItemStrWithError(dict, name); + if (res == NULL && PyErr_Occurred()) { + PyErr_WriteUnraisable(NULL); + } + return res; +} +#elif !CYTHON_COMPILING_IN_PYPY || PYPY_VERSION_NUM >= 0x07020000 +#define __Pyx_PyDict_GetItemStrWithError PyDict_GetItemWithError +#define __Pyx_PyDict_GetItemStr PyDict_GetItem +#else +static CYTHON_INLINE PyObject * __Pyx_PyDict_GetItemStrWithError(PyObject *dict, PyObject *name) { +#if CYTHON_COMPILING_IN_PYPY + return PyDict_GetItem(dict, name); +#else + PyDictEntry *ep; + PyDictObject *mp = (PyDictObject*) dict; + long hash = ((PyStringObject *) name)->ob_shash; + assert(hash != -1); + ep = (mp->ma_lookup)(mp, name, hash); + if (ep == NULL) { + return NULL; + } + return ep->me_value; +#endif +} +#define __Pyx_PyDict_GetItemStr PyDict_GetItem +#endif +#if CYTHON_USE_TYPE_SLOTS + #define __Pyx_PyType_GetFlags(tp) (((PyTypeObject *)tp)->tp_flags) + #define __Pyx_PyType_HasFeature(type, feature) ((__Pyx_PyType_GetFlags(type) & (feature)) != 0) +#else + #define __Pyx_PyType_GetFlags(tp) (PyType_GetFlags((PyTypeObject *)tp)) + #define __Pyx_PyType_HasFeature(type, feature) PyType_HasFeature(type, feature) +#endif +#define __Pyx_PyObject_GetIterNextFunc(iterator) __Pyx_PyObject_GetSlot(iterator, tp_iternext, iternextfunc) +#if CYTHON_USE_TYPE_SPECS +#define __Pyx_PyHeapTypeObject_GC_Del(obj) {\ + PyTypeObject *type = Py_TYPE((PyObject*)obj);\ + assert(__Pyx_PyType_HasFeature(type, Py_TPFLAGS_HEAPTYPE));\ + PyObject_GC_Del(obj);\ + Py_DECREF(type);\ +} +#else +#define __Pyx_PyHeapTypeObject_GC_Del(obj) PyObject_GC_Del(obj) +#endif +#if CYTHON_COMPILING_IN_LIMITED_API + #define __Pyx_PyUnicode_READY(op) (0) + #define __Pyx_PyUnicode_READ_CHAR(u, i) PyUnicode_ReadChar(u, i) + #define __Pyx_PyUnicode_MAX_CHAR_VALUE(u) ((void)u, 1114111U) + #define __Pyx_PyUnicode_KIND(u) ((void)u, (0)) + #define __Pyx_PyUnicode_KIND_04(u) __Pyx_PyUnicode_KIND(u) + #define __Pyx_PyUnicode_DATA(u) ((void*)u) + #define __Pyx_PyUnicode_READ(k, d, i) ((void)k, PyUnicode_ReadChar((PyObject*)(d), i)) + #define __Pyx_PyUnicode_IS_TRUE(u) (0 != PyUnicode_GetLength(u)) +#else + #if PY_VERSION_HEX >= 0x030C0000 + #define __Pyx_PyUnicode_READY(op) (0) + #else + #define __Pyx_PyUnicode_READY(op) (likely(PyUnicode_IS_READY(op)) ?\ + 0 : _PyUnicode_Ready((PyObject *)(op))) + #endif + #define __Pyx_PyUnicode_READ_CHAR(u, i) PyUnicode_READ_CHAR(u, i) + #define __Pyx_PyUnicode_MAX_CHAR_VALUE(u) PyUnicode_MAX_CHAR_VALUE(u) + #define __Pyx_PyUnicode_KIND(u) ((int)PyUnicode_KIND(u)) + #define __Pyx_PyUnicode_DATA(u) PyUnicode_DATA(u) + #define __Pyx_PyUnicode_READ(k, d, i) PyUnicode_READ(k, d, i) + #define __Pyx_PyUnicode_WRITE(k, d, i, ch) PyUnicode_WRITE(k, d, i, (Py_UCS4) ch) + #if PY_VERSION_HEX >= 0x030C0000 + #define __Pyx_PyUnicode_IS_TRUE(u) (0 != PyUnicode_GET_LENGTH(u)) + #else + #if CYTHON_COMPILING_IN_CPYTHON + #define __Pyx_PyUnicode_IS_TRUE(u) (0 != (likely(PyUnicode_IS_READY(u)) ? PyUnicode_GET_LENGTH(u) : ((PyCompactUnicodeObject *)(u))->wstr_length)) + #else + #define __Pyx_PyUnicode_IS_TRUE(u) (0 != (likely(PyUnicode_IS_READY(u)) ? PyUnicode_GET_LENGTH(u) : PyUnicode_GET_SIZE(u))) + #endif + #endif + static CYTHON_INLINE int __Pyx_PyUnicode_KIND_04(PyObject *o) { + return __Pyx_PyUnicode_KIND(o) - (int) !!PyUnicode_IS_ASCII(o); + } +#endif +#if CYTHON_COMPILING_IN_PYPY + #define __Pyx_PyUnicode_Concat(a, b) PyNumber_Add(a, b) + #define __Pyx_PyUnicode_ConcatSafe(a, b) PyNumber_Add(a, b) +#else + #define __Pyx_PyUnicode_Concat(a, b) PyUnicode_Concat(a, b) + #define __Pyx_PyUnicode_ConcatSafe(a, b) ((unlikely((a) == Py_None) || unlikely((b) == Py_None)) ?\ + PyNumber_Add(a, b) : __Pyx_PyUnicode_Concat(a, b)) +#endif +#if CYTHON_COMPILING_IN_PYPY + #if !defined(PyUnicode_DecodeUnicodeEscape) + #define PyUnicode_DecodeUnicodeEscape(s, size, errors) PyUnicode_Decode(s, size, "unicode_escape", errors) + #endif + #if !defined(PyUnicode_Contains) + #define PyUnicode_Contains(u, s) PySequence_Contains(u, s) + #endif + #if !defined(PyByteArray_Check) + #define PyByteArray_Check(obj) PyObject_TypeCheck(obj, &PyByteArray_Type) + #endif + #if !defined(PyObject_Format) + #define PyObject_Format(obj, fmt) PyObject_CallMethod(obj, "__format__", "O", fmt) + #endif +#endif +#define __Pyx_PyUnicode_FormatSafe(a, b) ((unlikely((a) == Py_None || (PyUnicode_Check(b) && !PyUnicode_CheckExact(b)))) ? PyNumber_Remainder(a, b) : PyUnicode_Format(a, b)) +#if CYTHON_COMPILING_IN_CPYTHON && PY_VERSION_HEX >= 0x030E0000 + #define __Pyx_PySequence_ListKeepNew(obj)\ + (likely(PyList_CheckExact(obj) && PyUnstable_Object_IsUniquelyReferenced(obj)) ? __Pyx_NewRef(obj) : PySequence_List(obj)) +#elif CYTHON_COMPILING_IN_CPYTHON + #define __Pyx_PySequence_ListKeepNew(obj)\ + (likely(PyList_CheckExact(obj) && Py_REFCNT(obj) == 1) ? __Pyx_NewRef(obj) : PySequence_List(obj)) +#else + #define __Pyx_PySequence_ListKeepNew(obj) PySequence_List(obj) +#endif +#ifndef PySet_CheckExact + #define PySet_CheckExact(obj) Py_IS_TYPE(obj, &PySet_Type) +#endif +enum __Pyx_ReferenceSharing { + __Pyx_ReferenceSharing_DefinitelyUnique, // We created it so we know it's unshared - no need to check + __Pyx_ReferenceSharing_OwnStrongReference, + __Pyx_ReferenceSharing_FunctionArgument, + __Pyx_ReferenceSharing_SharedReference, // Never trust it to be unshared because it's a global or similar +}; +#if CYTHON_COMPILING_IN_CPYTHON_FREETHREADING && PY_VERSION_HEX >= 0x030E0000 +#define __Pyx_IS_UNIQUELY_REFERENCED(o, sharing)\ + (sharing == __Pyx_ReferenceSharing_DefinitelyUnique ? 1 :\ + (sharing == __Pyx_ReferenceSharing_FunctionArgument ? PyUnstable_Object_IsUniqueReferencedTemporary(o) :\ + (sharing == __Pyx_ReferenceSharing_OwnStrongReference ? PyUnstable_Object_IsUniquelyReferenced(o) : 0))) +#elif (CYTHON_COMPILING_IN_CPYTHON && !CYTHON_COMPILING_IN_CPYTHON_FREETHREADING) || CYTHON_COMPILING_IN_LIMITED_API +#define __Pyx_IS_UNIQUELY_REFERENCED(o, sharing) (((void)sharing), Py_REFCNT(o) == 1) +#else +#define __Pyx_IS_UNIQUELY_REFERENCED(o, sharing) (((void)o), ((void)sharing), 0) +#endif +#if __PYX_LIMITED_VERSION_HEX >= 0x030d0000 + #define __Pyx_PyList_GetItemRef(o, i) PyList_GetItemRef(o, i) +#elif CYTHON_AVOID_BORROWED_REFS || CYTHON_AVOID_THREAD_UNSAFE_BORROWED_REFS + #if CYTHON_COMPILING_IN_LIMITED_API || !CYTHON_ASSUME_SAFE_MACROS + #define __Pyx_PyList_GetItemRef(o, i) (likely((i) >= 0) ? PySequence_GetItem(o, i) : (PyErr_SetString(PyExc_IndexError, "list index out of range"), (PyObject*)NULL)) + #else + #define __Pyx_PyList_GetItemRef(o, i) PySequence_ITEM(o, i) + #endif +#elif CYTHON_COMPILING_IN_LIMITED_API || !(CYTHON_ASSUME_SAFE_MACROS && CYTHON_ASSUME_SAFE_SIZE) + #define __Pyx_PyList_GetItemRef(o, i) __Pyx_XNewRef(PyList_GetItem(o, i)) +#else + #define __Pyx_PyList_GetItemRef(o, i) (likely(__Pyx_is_valid_index(i, PyList_GET_SIZE(o))) ?\ + __Pyx_NewRef(PyList_GET_ITEM(o, i)) : (PyErr_SetString(PyExc_IndexError, "list index out of range"), (PyObject*)NULL)) +#endif +#if CYTHON_AVOID_BORROWED_REFS || CYTHON_COMPILING_IN_LIMITED_API + #define __Pyx_PyList_GET_ITEM_REF(o, i, unsafe_shared) ((void)(unsafe_shared),\ + __Pyx_PyList_GetItemRef(o, i)) +#elif CYTHON_AVOID_THREAD_UNSAFE_BORROWED_REFS + #if CYTHON_ASSUME_SAFE_MACROS + #define __Pyx_PyList_GET_ITEM_REF(o, i, unsafe_shared) (\ + __Pyx_IS_UNIQUELY_REFERENCED(o, unsafe_shared) ?\ + __Pyx_NewRef(PyList_GET_ITEM(o, i)) : __Pyx_PyList_GetItemRef(o, i)) + #else + #define __Pyx_PyList_GET_ITEM_REF(o, i, unsafe_shared) (\ + __Pyx_IS_UNIQUELY_REFERENCED(o, unsafe_shared) ?\ + __Pyx_XNewRef(PyList_GetItem(o, i)) : __Pyx_PyList_GetItemRef(o, i)) + #endif +#elif CYTHON_ASSUME_SAFE_MACROS + #define __Pyx_PyList_GET_ITEM_REF(o, i, unsafe_shared) ((void)(unsafe_shared),\ + __Pyx_NewRef(PyList_GET_ITEM(o, i))) +#else + #define __Pyx_PyList_GET_ITEM_REF(o, i, unsafe_shared) ((void)(unsafe_shared),\ + __Pyx_XNewRef(PyList_GetItem(o, i))) +#endif +#if __PYX_LIMITED_VERSION_HEX >= 0x030d0000 +#define __Pyx_PyDict_GetItemRef(dict, key, result) PyDict_GetItemRef(dict, key, result) +#elif CYTHON_AVOID_BORROWED_REFS || CYTHON_AVOID_THREAD_UNSAFE_BORROWED_REFS +static CYTHON_INLINE int __Pyx_PyDict_GetItemRef(PyObject *dict, PyObject *key, PyObject **result) { + *result = PyObject_GetItem(dict, key); + if (*result == NULL) { + if (PyErr_ExceptionMatches(PyExc_KeyError)) { + PyErr_Clear(); + return 0; + } + return -1; + } + return 1; +} +#else +static CYTHON_INLINE int __Pyx_PyDict_GetItemRef(PyObject *dict, PyObject *key, PyObject **result) { + *result = PyDict_GetItemWithError(dict, key); + if (*result == NULL) { + return PyErr_Occurred() ? -1 : 0; + } + Py_INCREF(*result); + return 1; +} +#endif +#if defined(CYTHON_DEBUG_VISIT_CONST) && CYTHON_DEBUG_VISIT_CONST + #define __Pyx_VISIT_CONST(obj) Py_VISIT(obj) +#else + #define __Pyx_VISIT_CONST(obj) +#endif +#if CYTHON_ASSUME_SAFE_MACROS + #define __Pyx_PySequence_ITEM(o, i) PySequence_ITEM(o, i) + #define __Pyx_PySequence_SIZE(seq) Py_SIZE(seq) + #define __Pyx_PyTuple_SET_ITEM(o, i, v) (PyTuple_SET_ITEM(o, i, v), (0)) + #define __Pyx_PyTuple_GET_ITEM(o, i) PyTuple_GET_ITEM(o, i) + #define __Pyx_PyList_SET_ITEM(o, i, v) (PyList_SET_ITEM(o, i, v), (0)) + #define __Pyx_PyList_GET_ITEM(o, i) PyList_GET_ITEM(o, i) +#else + #define __Pyx_PySequence_ITEM(o, i) PySequence_GetItem(o, i) + #define __Pyx_PySequence_SIZE(seq) PySequence_Size(seq) + #define __Pyx_PyTuple_SET_ITEM(o, i, v) PyTuple_SetItem(o, i, v) + #define __Pyx_PyTuple_GET_ITEM(o, i) PyTuple_GetItem(o, i) + #define __Pyx_PyList_SET_ITEM(o, i, v) PyList_SetItem(o, i, v) + #define __Pyx_PyList_GET_ITEM(o, i) PyList_GetItem(o, i) +#endif +#if CYTHON_ASSUME_SAFE_SIZE + #define __Pyx_PyTuple_GET_SIZE(o) PyTuple_GET_SIZE(o) + #define __Pyx_PyList_GET_SIZE(o) PyList_GET_SIZE(o) + #define __Pyx_PySet_GET_SIZE(o) PySet_GET_SIZE(o) + #define __Pyx_PyDict_GET_SIZE(o) PyDict_GET_SIZE(o) + #define __Pyx_PyBytes_GET_SIZE(o) PyBytes_GET_SIZE(o) + #define __Pyx_PyByteArray_GET_SIZE(o) PyByteArray_GET_SIZE(o) + #define __Pyx_PyUnicode_GET_LENGTH(o) PyUnicode_GET_LENGTH(o) +#else + #define __Pyx_PyTuple_GET_SIZE(o) PyTuple_Size(o) + #define __Pyx_PyList_GET_SIZE(o) PyList_Size(o) + #define __Pyx_PySet_GET_SIZE(o) PySet_Size(o) + #define __Pyx_PyDict_GET_SIZE(o) PyDict_Size(o) + #define __Pyx_PyBytes_GET_SIZE(o) PyBytes_Size(o) + #define __Pyx_PyByteArray_GET_SIZE(o) PyByteArray_Size(o) + #define __Pyx_PyUnicode_GET_LENGTH(o) PyUnicode_GetLength(o) +#endif +#if CYTHON_COMPILING_IN_PYPY && !defined(PyUnicode_InternFromString) + #define PyUnicode_InternFromString(s) PyUnicode_FromString(s) +#endif +#define __Pyx_PyLong_FromHash_t PyLong_FromSsize_t +#define __Pyx_PyLong_AsHash_t __Pyx_PyIndex_AsSsize_t +#if __PYX_LIMITED_VERSION_HEX >= 0x030A0000 + #define __Pyx_PySendResult PySendResult +#else + typedef enum { + PYGEN_RETURN = 0, + PYGEN_ERROR = -1, + PYGEN_NEXT = 1, + } __Pyx_PySendResult; +#endif +#if CYTHON_COMPILING_IN_LIMITED_API || PY_VERSION_HEX < 0x030A00A3 + typedef __Pyx_PySendResult (*__Pyx_pyiter_sendfunc)(PyObject *iter, PyObject *value, PyObject **result); +#else + #define __Pyx_pyiter_sendfunc sendfunc +#endif +#if !CYTHON_USE_AM_SEND +#define __PYX_HAS_PY_AM_SEND 0 +#elif __PYX_LIMITED_VERSION_HEX >= 0x030A0000 +#define __PYX_HAS_PY_AM_SEND 1 +#else +#define __PYX_HAS_PY_AM_SEND 2 // our own backported implementation +#endif +#if __PYX_HAS_PY_AM_SEND < 2 + #define __Pyx_PyAsyncMethodsStruct PyAsyncMethods +#else + typedef struct { + unaryfunc am_await; + unaryfunc am_aiter; + unaryfunc am_anext; + __Pyx_pyiter_sendfunc am_send; + } __Pyx_PyAsyncMethodsStruct; + #define __Pyx_SlotTpAsAsync(s) ((PyAsyncMethods*)(s)) +#endif +#if CYTHON_USE_AM_SEND && PY_VERSION_HEX < 0x030A00F0 + #define __Pyx_TPFLAGS_HAVE_AM_SEND (1UL << 21) +#else + #define __Pyx_TPFLAGS_HAVE_AM_SEND (0) +#endif +#if CYTHON_COMPILING_IN_LIMITED_API && PY_VERSION_HEX < 0x030A0000 +#ifdef __cplusplus +extern "C" +#endif +PyAPI_FUNC(void *) PyMem_Calloc(size_t nelem, size_t elsize); +#endif +#if CYTHON_COMPILING_IN_LIMITED_API +static int __Pyx_init_co_variable(PyObject *inspect, const char* name, int *write_to) { + int value; + PyObject *py_value = PyObject_GetAttrString(inspect, name); + if (!py_value) return 0; + value = (int) PyLong_AsLong(py_value); + Py_DECREF(py_value); + *write_to = value; + return value != -1 || !PyErr_Occurred(); +} +static int __Pyx_init_co_variables(void) { + PyObject *inspect; + int result; + inspect = PyImport_ImportModule("inspect"); + result = +#if !defined(CO_OPTIMIZED) + __Pyx_init_co_variable(inspect, "CO_OPTIMIZED", &CO_OPTIMIZED) && +#endif +#if !defined(CO_NEWLOCALS) + __Pyx_init_co_variable(inspect, "CO_NEWLOCALS", &CO_NEWLOCALS) && +#endif +#if !defined(CO_VARARGS) + __Pyx_init_co_variable(inspect, "CO_VARARGS", &CO_VARARGS) && +#endif +#if !defined(CO_VARKEYWORDS) + __Pyx_init_co_variable(inspect, "CO_VARKEYWORDS", &CO_VARKEYWORDS) && +#endif +#if !defined(CO_ASYNC_GENERATOR) + __Pyx_init_co_variable(inspect, "CO_ASYNC_GENERATOR", &CO_ASYNC_GENERATOR) && +#endif +#if !defined(CO_GENERATOR) + __Pyx_init_co_variable(inspect, "CO_GENERATOR", &CO_GENERATOR) && +#endif +#if !defined(CO_COROUTINE) + __Pyx_init_co_variable(inspect, "CO_COROUTINE", &CO_COROUTINE) && +#endif + 1; + Py_DECREF(inspect); + return result ? 0 : -1; +} +static int __Pyx_init_tpflags_bitcount(unsigned long flag) { + int count = 0; + while (flag) { + count += (int) (flag & 1); + flag >>= 1; + } + return count; +} +static int __Pyx_init_tpflags_variables(void) { + if (__Pyx_Runtime_TPFLAGS_SEQUENCE != 0 && __Pyx_Runtime_TPFLAGS_MAPPING != 0) { + return 0; + } + PyObject *collections_abc = PyImport_ImportModule("collections.abc"); + if (!collections_abc) return -1; + int result = 0; + PyObject *sequence = NULL, *mapping = NULL; +#if __PYX_LIMITED_VERSION_HEX >= 0x030D0000 + if (PyObject_GetOptionalAttrString(collections_abc, "Sequence", &sequence) != 1) goto fail; + if (PyObject_GetOptionalAttrString(collections_abc, "Mapping", &mapping) != 1) goto fail; +#else + sequence = PyObject_GetAttrString(collections_abc, "Sequence"); + if (!sequence) goto fail_attr_lookup; + mapping = PyObject_GetAttrString(collections_abc, "Mapping"); + if (!mapping) goto fail_attr_lookup; +#endif + if (!PyType_Check(sequence) || !PyType_Check(mapping)) goto fail; + { + unsigned long sequence_flags = PyType_GetFlags((PyTypeObject*)sequence); + unsigned long mapping_flags = PyType_GetFlags((PyTypeObject*)mapping); + unsigned long mutual_flags = sequence_flags & mapping_flags; + sequence_flags = sequence_flags ^ mutual_flags; + mapping_flags = mapping_flags ^ mutual_flags; + if (__Pyx_Runtime_TPFLAGS_SEQUENCE == 0 && __Pyx_init_tpflags_bitcount(sequence_flags) == 1) { + __Pyx_Runtime_TPFLAGS_SEQUENCE = sequence_flags; + } + if (__Pyx_Runtime_TPFLAGS_MAPPING == 0 && __Pyx_init_tpflags_bitcount(mapping_flags) == 1) { + __Pyx_Runtime_TPFLAGS_MAPPING = mapping_flags; + } + } + cleanup: + Py_XDECREF(mapping); + Py_XDECREF(sequence); + Py_DECREF(collections_abc); + return result; +#if __PYX_LIMITED_VERSION_HEX < 0x030D0000 + fail_attr_lookup: + if (PyErr_ExceptionMatches(PyExc_AttributeError)) { + PyErr_Clear(); + } +#endif + fail: + result = PyErr_Occurred() ? -1 : 0; + goto cleanup; +} +#else +static int __Pyx_init_co_variables(void) { + return 0; // It's a limited API-only feature +} +static int __Pyx_init_tpflags_variables(void) { + return 0; // It's a limited API-only feature +} +#endif + +/* MathInitCode */ +#if defined(_WIN32) || defined(WIN32) || defined(MS_WINDOWS) + #ifndef _USE_MATH_DEFINES + #define _USE_MATH_DEFINES + #endif +#endif +#include +#if defined(__CYGWIN__) && defined(_LDBL_EQ_DBL) +#define __Pyx_truncl trunc +#else +#define __Pyx_truncl truncl +#endif + +#ifndef CYTHON_CLINE_IN_TRACEBACK_RUNTIME +#define CYTHON_CLINE_IN_TRACEBACK_RUNTIME 0 +#endif +#ifndef CYTHON_CLINE_IN_TRACEBACK +#define CYTHON_CLINE_IN_TRACEBACK CYTHON_CLINE_IN_TRACEBACK_RUNTIME +#endif +#if CYTHON_CLINE_IN_TRACEBACK +#define __PYX_MARK_ERR_POS(f_index, lineno) { __pyx_filename = __pyx_f[f_index]; (void) __pyx_filename; __pyx_lineno = lineno; (void) __pyx_lineno; __pyx_clineno = __LINE__; (void) __pyx_clineno; } +#else +#define __PYX_MARK_ERR_POS(f_index, lineno) { __pyx_filename = __pyx_f[f_index]; (void) __pyx_filename; __pyx_lineno = lineno; (void) __pyx_lineno; (void) __pyx_clineno; } +#endif +#define __PYX_ERR(f_index, lineno, Ln_error) \ + { __PYX_MARK_ERR_POS(f_index, lineno) goto Ln_error; } + +#ifdef CYTHON_EXTERN_C + #undef __PYX_EXTERN_C + #define __PYX_EXTERN_C CYTHON_EXTERN_C +#elif defined(__PYX_EXTERN_C) + #ifdef _MSC_VER + #pragma message ("Please do not define the '__PYX_EXTERN_C' macro externally. Use 'CYTHON_EXTERN_C' instead.") + #else + #warning Please do not define the '__PYX_EXTERN_C' macro externally. Use 'CYTHON_EXTERN_C' instead. + #endif +#else + #ifdef __cplusplus + #define __PYX_EXTERN_C extern "C" + #else + #define __PYX_EXTERN_C extern + #endif +#endif + +#define __PYX_HAVE__fontTools__cu2qu__cu2qu +#define __PYX_HAVE_API__fontTools__cu2qu__cu2qu +/* Early includes */ +#ifdef _OPENMP +#include +#endif /* _OPENMP */ + +#if defined(PYREX_WITHOUT_ASSERTIONS) && !defined(CYTHON_WITHOUT_ASSERTIONS) +#define CYTHON_WITHOUT_ASSERTIONS +#endif + +#ifdef CYTHON_FREETHREADING_COMPATIBLE +#if CYTHON_FREETHREADING_COMPATIBLE +#define __Pyx_FREETHREADING_COMPATIBLE Py_MOD_GIL_NOT_USED +#else +#define __Pyx_FREETHREADING_COMPATIBLE Py_MOD_GIL_USED +#endif +#else +#define __Pyx_FREETHREADING_COMPATIBLE Py_MOD_GIL_USED +#endif +#define __PYX_DEFAULT_STRING_ENCODING_IS_ASCII 0 +#define __PYX_DEFAULT_STRING_ENCODING_IS_UTF8 0 +#define __PYX_DEFAULT_STRING_ENCODING "" +#define __Pyx_PyObject_FromString __Pyx_PyBytes_FromString +#define __Pyx_PyObject_FromStringAndSize __Pyx_PyBytes_FromStringAndSize +#define __Pyx_uchar_cast(c) ((unsigned char)c) +#define __Pyx_long_cast(x) ((long)x) +#define __Pyx_fits_Py_ssize_t(v, type, is_signed) (\ + (sizeof(type) < sizeof(Py_ssize_t)) ||\ + (sizeof(type) > sizeof(Py_ssize_t) &&\ + likely(v < (type)PY_SSIZE_T_MAX ||\ + v == (type)PY_SSIZE_T_MAX) &&\ + (!is_signed || likely(v > (type)PY_SSIZE_T_MIN ||\ + v == (type)PY_SSIZE_T_MIN))) ||\ + (sizeof(type) == sizeof(Py_ssize_t) &&\ + (is_signed || likely(v < (type)PY_SSIZE_T_MAX ||\ + v == (type)PY_SSIZE_T_MAX))) ) +static CYTHON_INLINE int __Pyx_is_valid_index(Py_ssize_t i, Py_ssize_t limit) { + return (size_t) i < (size_t) limit; +} +#if defined (__cplusplus) && __cplusplus >= 201103L + #include + #define __Pyx_sst_abs(value) std::abs(value) +#elif SIZEOF_INT >= SIZEOF_SIZE_T + #define __Pyx_sst_abs(value) abs(value) +#elif SIZEOF_LONG >= SIZEOF_SIZE_T + #define __Pyx_sst_abs(value) labs(value) +#elif defined (_MSC_VER) + #define __Pyx_sst_abs(value) ((Py_ssize_t)_abs64(value)) +#elif defined (__STDC_VERSION__) && __STDC_VERSION__ >= 199901L + #define __Pyx_sst_abs(value) llabs(value) +#elif defined (__GNUC__) + #define __Pyx_sst_abs(value) __builtin_llabs(value) +#else + #define __Pyx_sst_abs(value) ((value<0) ? -value : value) +#endif +static CYTHON_INLINE Py_ssize_t __Pyx_ssize_strlen(const char *s); +static CYTHON_INLINE const char* __Pyx_PyObject_AsString(PyObject*); +static CYTHON_INLINE const char* __Pyx_PyObject_AsStringAndSize(PyObject*, Py_ssize_t* length); +static CYTHON_INLINE PyObject* __Pyx_PyByteArray_FromString(const char*); +#define __Pyx_PyByteArray_FromStringAndSize(s, l) PyByteArray_FromStringAndSize((const char*)s, l) +#define __Pyx_PyBytes_FromString PyBytes_FromString +#define __Pyx_PyBytes_FromStringAndSize PyBytes_FromStringAndSize +static CYTHON_INLINE PyObject* __Pyx_PyUnicode_FromString(const char*); +#if CYTHON_ASSUME_SAFE_MACROS + #define __Pyx_PyBytes_AsWritableString(s) ((char*) PyBytes_AS_STRING(s)) + #define __Pyx_PyBytes_AsWritableSString(s) ((signed char*) PyBytes_AS_STRING(s)) + #define __Pyx_PyBytes_AsWritableUString(s) ((unsigned char*) PyBytes_AS_STRING(s)) + #define __Pyx_PyBytes_AsString(s) ((const char*) PyBytes_AS_STRING(s)) + #define __Pyx_PyBytes_AsSString(s) ((const signed char*) PyBytes_AS_STRING(s)) + #define __Pyx_PyBytes_AsUString(s) ((const unsigned char*) PyBytes_AS_STRING(s)) + #define __Pyx_PyByteArray_AsString(s) PyByteArray_AS_STRING(s) +#else + #define __Pyx_PyBytes_AsWritableString(s) ((char*) PyBytes_AsString(s)) + #define __Pyx_PyBytes_AsWritableSString(s) ((signed char*) PyBytes_AsString(s)) + #define __Pyx_PyBytes_AsWritableUString(s) ((unsigned char*) PyBytes_AsString(s)) + #define __Pyx_PyBytes_AsString(s) ((const char*) PyBytes_AsString(s)) + #define __Pyx_PyBytes_AsSString(s) ((const signed char*) PyBytes_AsString(s)) + #define __Pyx_PyBytes_AsUString(s) ((const unsigned char*) PyBytes_AsString(s)) + #define __Pyx_PyByteArray_AsString(s) PyByteArray_AsString(s) +#endif +#define __Pyx_PyObject_AsWritableString(s) ((char*)(__pyx_uintptr_t) __Pyx_PyObject_AsString(s)) +#define __Pyx_PyObject_AsWritableSString(s) ((signed char*)(__pyx_uintptr_t) __Pyx_PyObject_AsString(s)) +#define __Pyx_PyObject_AsWritableUString(s) ((unsigned char*)(__pyx_uintptr_t) __Pyx_PyObject_AsString(s)) +#define __Pyx_PyObject_AsSString(s) ((const signed char*) __Pyx_PyObject_AsString(s)) +#define __Pyx_PyObject_AsUString(s) ((const unsigned char*) __Pyx_PyObject_AsString(s)) +#define __Pyx_PyObject_FromCString(s) __Pyx_PyObject_FromString((const char*)s) +#define __Pyx_PyBytes_FromCString(s) __Pyx_PyBytes_FromString((const char*)s) +#define __Pyx_PyByteArray_FromCString(s) __Pyx_PyByteArray_FromString((const char*)s) +#define __Pyx_PyUnicode_FromCString(s) __Pyx_PyUnicode_FromString((const char*)s) +#define __Pyx_PyUnicode_FromOrdinal(o) PyUnicode_FromOrdinal((int)o) +#define __Pyx_PyUnicode_AsUnicode PyUnicode_AsUnicode +static CYTHON_INLINE PyObject *__Pyx_NewRef(PyObject *obj) { +#if CYTHON_COMPILING_IN_CPYTHON && PY_VERSION_HEX >= 0x030a0000 || defined(Py_NewRef) + return Py_NewRef(obj); +#else + Py_INCREF(obj); + return obj; +#endif +} +static CYTHON_INLINE PyObject *__Pyx_XNewRef(PyObject *obj) { +#if CYTHON_COMPILING_IN_CPYTHON && PY_VERSION_HEX >= 0x030a0000 || defined(Py_XNewRef) + return Py_XNewRef(obj); +#else + Py_XINCREF(obj); + return obj; +#endif +} +static CYTHON_INLINE PyObject *__Pyx_Owned_Py_None(int b); +static CYTHON_INLINE PyObject * __Pyx_PyBool_FromLong(long b); +static CYTHON_INLINE int __Pyx_PyObject_IsTrue(PyObject*); +static CYTHON_INLINE int __Pyx_PyObject_IsTrueAndDecref(PyObject*); +static CYTHON_INLINE PyObject* __Pyx_PyNumber_Long(PyObject* x); +#define __Pyx_PyObject_RichCompareBool(a,b,cmp) __Pyx_PyObject_IsTrueAndDecref(PyObject_RichCompare((a),(b),(cmp))) +#define __Pyx_PySequence_Tuple(obj)\ + (likely(PyTuple_CheckExact(obj)) ? __Pyx_NewRef(obj) : PySequence_Tuple(obj)) +static CYTHON_INLINE Py_ssize_t __Pyx_PyIndex_AsSsize_t(PyObject*); +static CYTHON_INLINE PyObject * __Pyx_PyLong_FromSize_t(size_t); +static CYTHON_INLINE Py_hash_t __Pyx_PyIndex_AsHash_t(PyObject*); +#if CYTHON_ASSUME_SAFE_MACROS +#define __Pyx_PyFloat_AsDouble(x) (PyFloat_CheckExact(x) ? PyFloat_AS_DOUBLE(x) : PyFloat_AsDouble(x)) +#define __Pyx_PyFloat_AS_DOUBLE(x) PyFloat_AS_DOUBLE(x) +#define __Pyx_PyFloat_IsNonZero(x) (PyFloat_AS_DOUBLE(x) != 0.0) +#else +#define __Pyx_PyFloat_AsDouble(x) PyFloat_AsDouble(x) +#define __Pyx_PyFloat_AS_DOUBLE(x) PyFloat_AsDouble(x) +#define __Pyx_PyFloat_IsNonZero(x) PyObject_IsTrue(x) +#endif +#define __Pyx_PyFloat_AsFloat(x) ((float) __Pyx_PyFloat_AsDouble(x)) +#define __Pyx_PyNumber_Int(x) (PyLong_CheckExact(x) ? __Pyx_NewRef(x) : PyNumber_Long(x)) +#if CYTHON_USE_PYLONG_INTERNALS + #if PY_VERSION_HEX >= 0x030C00A7 + #ifndef _PyLong_SIGN_MASK + #define _PyLong_SIGN_MASK 3 + #endif + #ifndef _PyLong_NON_SIZE_BITS + #define _PyLong_NON_SIZE_BITS 3 + #endif + #define __Pyx_PyLong_SignBits(x) ((int) (((PyLongObject*)x)->long_value.lv_tag & _PyLong_SIGN_MASK)) + #define __Pyx_PyLong_Sign(x) (1 - __Pyx_PyLong_SignBits(x)) + #define __Pyx_PyLong_IsNeg(x) ((__Pyx_PyLong_SignBits(x) & 2) != 0) + #define __Pyx_PyLong_IsNonNeg(x) (!__Pyx_PyLong_IsNeg(x)) + #define __Pyx_PyLong_IsZero(x) (__Pyx_PyLong_SignBits(x) & 1) + #define __Pyx_PyLong_IsPos(x) (__Pyx_PyLong_SignBits(x) == 0) + #define __Pyx_PyLong_CompactValueUnsigned(x) (__Pyx_PyLong_Digits(x)[0]) + #define __Pyx_PyLong_DigitCount(x) ((Py_ssize_t) (((PyLongObject*)x)->long_value.lv_tag >> _PyLong_NON_SIZE_BITS)) + #define __Pyx_PyLong_SignedDigitCount(x)\ + (((Py_ssize_t) __Pyx_PyLong_Sign(x)) * __Pyx_PyLong_DigitCount(x)) + #if defined(PyUnstable_Long_IsCompact) && defined(PyUnstable_Long_CompactValue) + #define __Pyx_PyLong_IsCompact(x) PyUnstable_Long_IsCompact((PyLongObject*) x) + #define __Pyx_PyLong_CompactValue(x) PyUnstable_Long_CompactValue((PyLongObject*) x) + #else + #define __Pyx_PyLong_IsCompact(x) (((PyLongObject*)x)->long_value.lv_tag < (2 << _PyLong_NON_SIZE_BITS)) + #define __Pyx_PyLong_CompactValue(x) (((Py_ssize_t) __Pyx_PyLong_Sign(x)) * (Py_ssize_t) __Pyx_PyLong_Digits(x)[0]) + #endif + static CYTHON_INLINE Py_ssize_t __Pyx_PyLong_CompareSignAndSize(PyObject *a, PyObject *b) { + uintptr_t tag_a = ((PyLongObject*)a)->long_value.lv_tag; + uintptr_t tag_b = ((PyLongObject*)b)->long_value.lv_tag; + if (tag_a == tag_b) return 0; + int sign_a = (int) (tag_a & _PyLong_SIGN_MASK); + int sign_b = (int) (tag_b & _PyLong_SIGN_MASK); + if (sign_a > sign_b) return -1; + if (sign_a < sign_b) return 1; + Py_ssize_t size_a = (Py_ssize_t) (tag_a >> _PyLong_NON_SIZE_BITS); + Py_ssize_t size_b = (Py_ssize_t) (tag_b >> _PyLong_NON_SIZE_BITS); + return (1 - sign_a) * (size_a - size_b); + } + typedef Py_ssize_t __Pyx_compact_pylong; + typedef size_t __Pyx_compact_upylong; + #else + #define __Pyx_PyLong_Sign(x) ((int) ((Py_SIZE(x) == 0) ? 0 : (Py_SIZE(x) < 0) ? -1 : 1)) + #define __Pyx_PyLong_IsNeg(x) (Py_SIZE(x) < 0) + #define __Pyx_PyLong_IsNonNeg(x) (Py_SIZE(x) >= 0) + #define __Pyx_PyLong_IsZero(x) (Py_SIZE(x) == 0) + #define __Pyx_PyLong_IsPos(x) (Py_SIZE(x) > 0) + #define __Pyx_PyLong_CompactValueUnsigned(x) ((Py_SIZE(x) == 0) ? 0 : __Pyx_PyLong_Digits(x)[0]) + #define __Pyx_PyLong_DigitCount(x) __Pyx_sst_abs(Py_SIZE(x)) + #define __Pyx_PyLong_SignedDigitCount(x) Py_SIZE(x) + #define __Pyx_PyLong_IsCompact(x) (Py_SIZE(x) == 0 || Py_SIZE(x) == 1 || Py_SIZE(x) == -1) + #define __Pyx_PyLong_CompactValue(x)\ + ((Py_SIZE(x) == 0) ? (sdigit) 0 : ((Py_SIZE(x) < 0) ? -(sdigit)__Pyx_PyLong_Digits(x)[0] : (sdigit)__Pyx_PyLong_Digits(x)[0])) + #define __Pyx_PyLong_CompareSignAndSize(a, b) (Py_SIZE(a) - Py_SIZE(b)) + typedef sdigit __Pyx_compact_pylong; + typedef digit __Pyx_compact_upylong; + #endif + #if PY_VERSION_HEX >= 0x030C00A5 + #define __Pyx_PyLong_Digits(x) (((PyLongObject*)x)->long_value.ob_digit) + #else + #define __Pyx_PyLong_Digits(x) (((PyLongObject*)x)->ob_digit) + #endif + #define __Pyx_PyLong_IsNonZero(x) (!__Pyx_PyLong_IsZero(x)) +#else + #define __Pyx_PyLong_IsNonZero(x) PyObject_IsTrue(x) +#endif +#if __PYX_DEFAULT_STRING_ENCODING_IS_UTF8 + #define __Pyx_PyUnicode_FromStringAndSize(c_str, size) PyUnicode_DecodeUTF8(c_str, size, NULL) +#elif __PYX_DEFAULT_STRING_ENCODING_IS_ASCII + #define __Pyx_PyUnicode_FromStringAndSize(c_str, size) PyUnicode_DecodeASCII(c_str, size, NULL) +#else + #define __Pyx_PyUnicode_FromStringAndSize(c_str, size) PyUnicode_Decode(c_str, size, __PYX_DEFAULT_STRING_ENCODING, NULL) +#endif + + +/* Test for GCC > 2.95 */ +#if defined(__GNUC__) && (__GNUC__ > 2 || (__GNUC__ == 2 && (__GNUC_MINOR__ > 95))) + #define likely(x) __builtin_expect(!!(x), 1) + #define unlikely(x) __builtin_expect(!!(x), 0) +#else /* !__GNUC__ or GCC < 2.95 */ + #define likely(x) (x) + #define unlikely(x) (x) +#endif /* __GNUC__ */ +/* PretendToInitialize */ +#ifdef __cplusplus +#if __cplusplus > 201103L +#include +#endif +template +static void __Pyx_pretend_to_initialize(T* ptr) { +#if __cplusplus > 201103L + if ((std::is_trivially_default_constructible::value)) +#endif + *ptr = T(); + (void)ptr; +} +#else +static CYTHON_INLINE void __Pyx_pretend_to_initialize(void* ptr) { (void)ptr; } +#endif + + +#if !CYTHON_USE_MODULE_STATE +static PyObject *__pyx_m = NULL; +#endif +static const char * const __pyx_cfilenm = __FILE__; + +/* Header.proto */ +#if !defined(CYTHON_CCOMPLEX) + #if defined(__cplusplus) + #define CYTHON_CCOMPLEX 1 + #elif (defined(_Complex_I) && !defined(_MSC_VER)) || ((defined (__STDC_VERSION__) && __STDC_VERSION__ >= 201112L) && !defined(__STDC_NO_COMPLEX__) && !defined(_MSC_VER)) + #define CYTHON_CCOMPLEX 1 + #else + #define CYTHON_CCOMPLEX 0 + #endif +#endif +#if CYTHON_CCOMPLEX + #ifdef __cplusplus + #include + #else + #include + #endif +#endif +#if CYTHON_CCOMPLEX && !defined(__cplusplus) && defined(__sun__) && defined(__GNUC__) + #undef _Complex_I + #define _Complex_I 1.0fj +#endif + +/* #### Code section: filename_table ### */ + +static const char* const __pyx_f[] = { + "Lib/fontTools/cu2qu/cu2qu.py", +}; +/* #### Code section: utility_code_proto_before_types ### */ +/* Atomics.proto (used by UnpackUnboundCMethod) */ +#include +#ifndef CYTHON_ATOMICS + #define CYTHON_ATOMICS 1 +#endif +#define __PYX_CYTHON_ATOMICS_ENABLED() CYTHON_ATOMICS +#define __PYX_GET_CYTHON_COMPILING_IN_CPYTHON_FREETHREADING() CYTHON_COMPILING_IN_CPYTHON_FREETHREADING +#define __pyx_atomic_int_type int +#define __pyx_nonatomic_int_type int +#if CYTHON_ATOMICS && (defined(__STDC_VERSION__) &&\ + (__STDC_VERSION__ >= 201112L) &&\ + !defined(__STDC_NO_ATOMICS__)) + #include +#elif CYTHON_ATOMICS && (defined(__cplusplus) && (\ + (__cplusplus >= 201103L) ||\ + (defined(_MSC_VER) && _MSC_VER >= 1700))) + #include +#endif +#if CYTHON_ATOMICS && (defined(__STDC_VERSION__) &&\ + (__STDC_VERSION__ >= 201112L) &&\ + !defined(__STDC_NO_ATOMICS__) &&\ + ATOMIC_INT_LOCK_FREE == 2) + #undef __pyx_atomic_int_type + #define __pyx_atomic_int_type atomic_int + #define __pyx_atomic_ptr_type atomic_uintptr_t + #define __pyx_nonatomic_ptr_type uintptr_t + #define __pyx_atomic_incr_relaxed(value) atomic_fetch_add_explicit(value, 1, memory_order_relaxed) + #define __pyx_atomic_incr_acq_rel(value) atomic_fetch_add_explicit(value, 1, memory_order_acq_rel) + #define __pyx_atomic_decr_acq_rel(value) atomic_fetch_sub_explicit(value, 1, memory_order_acq_rel) + #define __pyx_atomic_sub(value, arg) atomic_fetch_sub(value, arg) + #define __pyx_atomic_int_cmp_exchange(value, expected, desired) atomic_compare_exchange_strong(value, expected, desired) + #define __pyx_atomic_load(value) atomic_load(value) + #define __pyx_atomic_store(value, new_value) atomic_store(value, new_value) + #define __pyx_atomic_pointer_load_relaxed(value) atomic_load_explicit(value, memory_order_relaxed) + #define __pyx_atomic_pointer_load_acquire(value) atomic_load_explicit(value, memory_order_acquire) + #define __pyx_atomic_pointer_exchange(value, new_value) atomic_exchange(value, (__pyx_nonatomic_ptr_type)new_value) + #define __pyx_atomic_pointer_cmp_exchange(value, expected, desired) atomic_compare_exchange_strong(value, expected, desired) + #if defined(__PYX_DEBUG_ATOMICS) && defined(_MSC_VER) + #pragma message ("Using standard C atomics") + #elif defined(__PYX_DEBUG_ATOMICS) + #warning "Using standard C atomics" + #endif +#elif CYTHON_ATOMICS && (defined(__cplusplus) && (\ + (__cplusplus >= 201103L) ||\ +\ + (defined(_MSC_VER) && _MSC_VER >= 1700)) &&\ + ATOMIC_INT_LOCK_FREE == 2) + #undef __pyx_atomic_int_type + #define __pyx_atomic_int_type std::atomic_int + #define __pyx_atomic_ptr_type std::atomic_uintptr_t + #define __pyx_nonatomic_ptr_type uintptr_t + #define __pyx_atomic_incr_relaxed(value) std::atomic_fetch_add_explicit(value, 1, std::memory_order_relaxed) + #define __pyx_atomic_incr_acq_rel(value) std::atomic_fetch_add_explicit(value, 1, std::memory_order_acq_rel) + #define __pyx_atomic_decr_acq_rel(value) std::atomic_fetch_sub_explicit(value, 1, std::memory_order_acq_rel) + #define __pyx_atomic_sub(value, arg) std::atomic_fetch_sub(value, arg) + #define __pyx_atomic_int_cmp_exchange(value, expected, desired) std::atomic_compare_exchange_strong(value, expected, desired) + #define __pyx_atomic_load(value) std::atomic_load(value) + #define __pyx_atomic_store(value, new_value) std::atomic_store(value, new_value) + #define __pyx_atomic_pointer_load_relaxed(value) std::atomic_load_explicit(value, std::memory_order_relaxed) + #define __pyx_atomic_pointer_load_acquire(value) std::atomic_load_explicit(value, std::memory_order_acquire) + #define __pyx_atomic_pointer_exchange(value, new_value) std::atomic_exchange(value, (__pyx_nonatomic_ptr_type)new_value) + #define __pyx_atomic_pointer_cmp_exchange(value, expected, desired) std::atomic_compare_exchange_strong(value, expected, desired) + #if defined(__PYX_DEBUG_ATOMICS) && defined(_MSC_VER) + #pragma message ("Using standard C++ atomics") + #elif defined(__PYX_DEBUG_ATOMICS) + #warning "Using standard C++ atomics" + #endif +#elif CYTHON_ATOMICS && (__GNUC__ >= 5 || (__GNUC__ == 4 &&\ + (__GNUC_MINOR__ > 1 ||\ + (__GNUC_MINOR__ == 1 && __GNUC_PATCHLEVEL__ >= 2)))) + #define __pyx_atomic_ptr_type void* + #define __pyx_nonatomic_ptr_type void* + #define __pyx_atomic_incr_relaxed(value) __sync_fetch_and_add(value, 1) + #define __pyx_atomic_incr_acq_rel(value) __sync_fetch_and_add(value, 1) + #define __pyx_atomic_decr_acq_rel(value) __sync_fetch_and_sub(value, 1) + #define __pyx_atomic_sub(value, arg) __sync_fetch_and_sub(value, arg) + static CYTHON_INLINE int __pyx_atomic_int_cmp_exchange(__pyx_atomic_int_type* value, __pyx_nonatomic_int_type* expected, __pyx_nonatomic_int_type desired) { + __pyx_nonatomic_int_type old = __sync_val_compare_and_swap(value, *expected, desired); + int result = old == *expected; + *expected = old; + return result; + } + #define __pyx_atomic_load(value) __sync_fetch_and_add(value, 0) + #define __pyx_atomic_store(value, new_value) __sync_lock_test_and_set(value, new_value) + #define __pyx_atomic_pointer_load_relaxed(value) __sync_fetch_and_add(value, 0) + #define __pyx_atomic_pointer_load_acquire(value) __sync_fetch_and_add(value, 0) + #define __pyx_atomic_pointer_exchange(value, new_value) __sync_lock_test_and_set(value, (__pyx_atomic_ptr_type)new_value) + static CYTHON_INLINE int __pyx_atomic_pointer_cmp_exchange(__pyx_atomic_ptr_type* value, __pyx_nonatomic_ptr_type* expected, __pyx_nonatomic_ptr_type desired) { + __pyx_nonatomic_ptr_type old = __sync_val_compare_and_swap(value, *expected, desired); + int result = old == *expected; + *expected = old; + return result; + } + #ifdef __PYX_DEBUG_ATOMICS + #warning "Using GNU atomics" + #endif +#elif CYTHON_ATOMICS && defined(_MSC_VER) + #include + #undef __pyx_atomic_int_type + #define __pyx_atomic_int_type long + #define __pyx_atomic_ptr_type void* + #undef __pyx_nonatomic_int_type + #define __pyx_nonatomic_int_type long + #define __pyx_nonatomic_ptr_type void* + #pragma intrinsic (_InterlockedExchangeAdd, _InterlockedExchange, _InterlockedCompareExchange, _InterlockedCompareExchangePointer, _InterlockedExchangePointer) + #define __pyx_atomic_incr_relaxed(value) _InterlockedExchangeAdd(value, 1) + #define __pyx_atomic_incr_acq_rel(value) _InterlockedExchangeAdd(value, 1) + #define __pyx_atomic_decr_acq_rel(value) _InterlockedExchangeAdd(value, -1) + #define __pyx_atomic_sub(value, arg) _InterlockedExchangeAdd(value, -arg) + static CYTHON_INLINE int __pyx_atomic_int_cmp_exchange(__pyx_atomic_int_type* value, __pyx_nonatomic_int_type* expected, __pyx_nonatomic_int_type desired) { + __pyx_nonatomic_int_type old = _InterlockedCompareExchange(value, desired, *expected); + int result = old == *expected; + *expected = old; + return result; + } + #define __pyx_atomic_load(value) _InterlockedExchangeAdd(value, 0) + #define __pyx_atomic_store(value, new_value) _InterlockedExchange(value, new_value) + #define __pyx_atomic_pointer_load_relaxed(value) *(void * volatile *)value + #define __pyx_atomic_pointer_load_acquire(value) _InterlockedCompareExchangePointer(value, 0, 0) + #define __pyx_atomic_pointer_exchange(value, new_value) _InterlockedExchangePointer(value, (__pyx_atomic_ptr_type)new_value) + static CYTHON_INLINE int __pyx_atomic_pointer_cmp_exchange(__pyx_atomic_ptr_type* value, __pyx_nonatomic_ptr_type* expected, __pyx_nonatomic_ptr_type desired) { + __pyx_atomic_ptr_type old = _InterlockedCompareExchangePointer(value, desired, *expected); + int result = old == *expected; + *expected = old; + return result; + } + #ifdef __PYX_DEBUG_ATOMICS + #pragma message ("Using MSVC atomics") + #endif +#else + #undef CYTHON_ATOMICS + #define CYTHON_ATOMICS 0 + #ifdef __PYX_DEBUG_ATOMICS + #warning "Not using atomics" + #endif +#endif + +/* CriticalSectionsDefinition.proto (used by CriticalSections) */ +#if !CYTHON_COMPILING_IN_CPYTHON_FREETHREADING +#define __Pyx_PyCriticalSection void* +#define __Pyx_PyCriticalSection2 void* +#define __Pyx_PyCriticalSection_End(cs) +#define __Pyx_PyCriticalSection2_End(cs) +#else +#define __Pyx_PyCriticalSection PyCriticalSection +#define __Pyx_PyCriticalSection2 PyCriticalSection2 +#define __Pyx_PyCriticalSection_End PyCriticalSection_End +#define __Pyx_PyCriticalSection2_End PyCriticalSection2_End +#endif + +/* CriticalSections.proto (used by ParseKeywordsImpl) */ +#if !CYTHON_COMPILING_IN_CPYTHON_FREETHREADING +#define __Pyx_PyCriticalSection_Begin(cs, arg) (void)(cs) +#define __Pyx_PyCriticalSection2_Begin(cs, arg1, arg2) (void)(cs) +#else +#define __Pyx_PyCriticalSection_Begin PyCriticalSection_Begin +#define __Pyx_PyCriticalSection2_Begin PyCriticalSection2_Begin +#endif +#if PY_VERSION_HEX < 0x030d0000 || CYTHON_COMPILING_IN_LIMITED_API +#define __Pyx_BEGIN_CRITICAL_SECTION(o) { +#define __Pyx_END_CRITICAL_SECTION() } +#else +#define __Pyx_BEGIN_CRITICAL_SECTION Py_BEGIN_CRITICAL_SECTION +#define __Pyx_END_CRITICAL_SECTION Py_END_CRITICAL_SECTION +#endif + +/* IncludeStructmemberH.proto (used by CythonFunctionShared) */ +#include + +/* #### Code section: numeric_typedefs ### */ +/* #### Code section: complex_type_declarations ### */ +/* Declarations.proto */ +#if CYTHON_CCOMPLEX && (1) && (!0 || __cplusplus) + #ifdef __cplusplus + typedef ::std::complex< double > __pyx_t_double_complex; + #else + typedef double _Complex __pyx_t_double_complex; + #endif +#else + typedef struct { double real, imag; } __pyx_t_double_complex; +#endif +static CYTHON_INLINE __pyx_t_double_complex __pyx_t_double_complex_from_parts(double, double); + +/* #### Code section: type_declarations ### */ + +/*--- Type declarations ---*/ +struct __pyx_obj_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen; + +/* "fontTools/cu2qu/cu2qu.py":149 + * + * + * @cython.locals( # <<<<<<<<<<<<<< + * p0=cython.complex, + * p1=cython.complex, +*/ +struct __pyx_obj_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen { + PyObject_HEAD + __pyx_t_double_complex __pyx_v_a; + __pyx_t_double_complex __pyx_v_a1; + __pyx_t_double_complex __pyx_v_b; + __pyx_t_double_complex __pyx_v_b1; + __pyx_t_double_complex __pyx_v_c; + __pyx_t_double_complex __pyx_v_c1; + __pyx_t_double_complex __pyx_v_d; + __pyx_t_double_complex __pyx_v_d1; + double __pyx_v_delta_2; + double __pyx_v_delta_3; + double __pyx_v_dt; + int __pyx_v_i; + int __pyx_v_n; + __pyx_t_double_complex __pyx_v_p0; + __pyx_t_double_complex __pyx_v_p1; + __pyx_t_double_complex __pyx_v_p2; + __pyx_t_double_complex __pyx_v_p3; + double __pyx_v_t1; + double __pyx_v_t1_2; + int __pyx_t_0; + int __pyx_t_1; + int __pyx_t_2; +}; + +/* #### Code section: utility_code_proto ### */ + +/* --- Runtime support code (head) --- */ +/* Refnanny.proto */ +#ifndef CYTHON_REFNANNY + #define CYTHON_REFNANNY 0 +#endif +#if CYTHON_REFNANNY + typedef struct { + void (*INCREF)(void*, PyObject*, Py_ssize_t); + void (*DECREF)(void*, PyObject*, Py_ssize_t); + void (*GOTREF)(void*, PyObject*, Py_ssize_t); + void (*GIVEREF)(void*, PyObject*, Py_ssize_t); + void* (*SetupContext)(const char*, Py_ssize_t, const char*); + void (*FinishContext)(void**); + } __Pyx_RefNannyAPIStruct; + static __Pyx_RefNannyAPIStruct *__Pyx_RefNanny = NULL; + static __Pyx_RefNannyAPIStruct *__Pyx_RefNannyImportAPI(const char *modname); + #define __Pyx_RefNannyDeclarations void *__pyx_refnanny = NULL; + #define __Pyx_RefNannySetupContext(name, acquire_gil)\ + if (acquire_gil) {\ + PyGILState_STATE __pyx_gilstate_save = PyGILState_Ensure();\ + __pyx_refnanny = __Pyx_RefNanny->SetupContext((name), (__LINE__), (__FILE__));\ + PyGILState_Release(__pyx_gilstate_save);\ + } else {\ + __pyx_refnanny = __Pyx_RefNanny->SetupContext((name), (__LINE__), (__FILE__));\ + } + #define __Pyx_RefNannyFinishContextNogil() {\ + PyGILState_STATE __pyx_gilstate_save = PyGILState_Ensure();\ + __Pyx_RefNannyFinishContext();\ + PyGILState_Release(__pyx_gilstate_save);\ + } + #define __Pyx_RefNannyFinishContextNogil() {\ + PyGILState_STATE __pyx_gilstate_save = PyGILState_Ensure();\ + __Pyx_RefNannyFinishContext();\ + PyGILState_Release(__pyx_gilstate_save);\ + } + #define __Pyx_RefNannyFinishContext()\ + __Pyx_RefNanny->FinishContext(&__pyx_refnanny) + #define __Pyx_INCREF(r) __Pyx_RefNanny->INCREF(__pyx_refnanny, (PyObject *)(r), (__LINE__)) + #define __Pyx_DECREF(r) __Pyx_RefNanny->DECREF(__pyx_refnanny, (PyObject *)(r), (__LINE__)) + #define __Pyx_GOTREF(r) __Pyx_RefNanny->GOTREF(__pyx_refnanny, (PyObject *)(r), (__LINE__)) + #define __Pyx_GIVEREF(r) __Pyx_RefNanny->GIVEREF(__pyx_refnanny, (PyObject *)(r), (__LINE__)) + #define __Pyx_XINCREF(r) do { if((r) == NULL); else {__Pyx_INCREF(r); }} while(0) + #define __Pyx_XDECREF(r) do { if((r) == NULL); else {__Pyx_DECREF(r); }} while(0) + #define __Pyx_XGOTREF(r) do { if((r) == NULL); else {__Pyx_GOTREF(r); }} while(0) + #define __Pyx_XGIVEREF(r) do { if((r) == NULL); else {__Pyx_GIVEREF(r);}} while(0) +#else + #define __Pyx_RefNannyDeclarations + #define __Pyx_RefNannySetupContext(name, acquire_gil) + #define __Pyx_RefNannyFinishContextNogil() + #define __Pyx_RefNannyFinishContext() + #define __Pyx_INCREF(r) Py_INCREF(r) + #define __Pyx_DECREF(r) Py_DECREF(r) + #define __Pyx_GOTREF(r) + #define __Pyx_GIVEREF(r) + #define __Pyx_XINCREF(r) Py_XINCREF(r) + #define __Pyx_XDECREF(r) Py_XDECREF(r) + #define __Pyx_XGOTREF(r) + #define __Pyx_XGIVEREF(r) +#endif +#define __Pyx_Py_XDECREF_SET(r, v) do {\ + PyObject *tmp = (PyObject *) r;\ + r = v; Py_XDECREF(tmp);\ + } while (0) +#define __Pyx_XDECREF_SET(r, v) do {\ + PyObject *tmp = (PyObject *) r;\ + r = v; __Pyx_XDECREF(tmp);\ + } while (0) +#define __Pyx_DECREF_SET(r, v) do {\ + PyObject *tmp = (PyObject *) r;\ + r = v; __Pyx_DECREF(tmp);\ + } while (0) +#define __Pyx_CLEAR(r) do { PyObject* tmp = ((PyObject*)(r)); r = NULL; __Pyx_DECREF(tmp);} while(0) +#define __Pyx_XCLEAR(r) do { if((r) != NULL) {PyObject* tmp = ((PyObject*)(r)); r = NULL; __Pyx_DECREF(tmp);}} while(0) + +/* IncludeStdlibH.proto */ +#include + +/* PyObjectCall.proto (used by PyObjectFastCall) */ +#if CYTHON_COMPILING_IN_CPYTHON +static CYTHON_INLINE PyObject* __Pyx_PyObject_Call(PyObject *func, PyObject *arg, PyObject *kw); +#else +#define __Pyx_PyObject_Call(func, arg, kw) PyObject_Call(func, arg, kw) +#endif + +/* PyObjectCallMethO.proto (used by PyObjectFastCall) */ +#if CYTHON_COMPILING_IN_CPYTHON +static CYTHON_INLINE PyObject* __Pyx_PyObject_CallMethO(PyObject *func, PyObject *arg); +#endif + +/* PyObjectFastCall.proto */ +#define __Pyx_PyObject_FastCall(func, args, nargs) __Pyx_PyObject_FastCallDict(func, args, (size_t)(nargs), NULL) +static CYTHON_INLINE PyObject* __Pyx_PyObject_FastCallDict(PyObject *func, PyObject * const*args, size_t nargsf, PyObject *kwargs); + +/* PyLongCompare.proto */ +static CYTHON_INLINE int __Pyx_PyLong_BoolEqObjC(PyObject *op1, PyObject *op2, long intval, long inplace); + +/* RaiseTooManyValuesToUnpack.proto */ +static CYTHON_INLINE void __Pyx_RaiseTooManyValuesError(Py_ssize_t expected); + +/* RaiseNeedMoreValuesToUnpack.proto */ +static CYTHON_INLINE void __Pyx_RaiseNeedMoreValuesError(Py_ssize_t index); + +/* FastTypeChecks.proto (used by GivenExceptionMatches) */ +#if CYTHON_COMPILING_IN_CPYTHON +#define __Pyx_TypeCheck(obj, type) __Pyx_IsSubtype(Py_TYPE(obj), (PyTypeObject *)type) +#define __Pyx_TypeCheck2(obj, type1, type2) __Pyx_IsAnySubtype2(Py_TYPE(obj), (PyTypeObject *)type1, (PyTypeObject *)type2) +static CYTHON_INLINE int __Pyx_IsSubtype(PyTypeObject *a, PyTypeObject *b); +static CYTHON_INLINE int __Pyx_IsAnySubtype2(PyTypeObject *cls, PyTypeObject *a, PyTypeObject *b); +#define __Pyx_PyAnySet_Check(obj) __Pyx_TypeCheck2(obj, &PySet_Type, &PyFrozenSet_Type) +#else +#define __Pyx_TypeCheck(obj, type) PyObject_TypeCheck(obj, (PyTypeObject *)type) +#define __Pyx_TypeCheck2(obj, type1, type2) (PyObject_TypeCheck(obj, (PyTypeObject *)type1) || PyObject_TypeCheck(obj, (PyTypeObject *)type2)) +#define __Pyx_PyAnySet_Check(obj) PyAnySet_Check(obj) +#endif + +/* PyThreadStateGet.proto (used by PyErrFetchRestore) */ +#if CYTHON_FAST_THREAD_STATE +#define __Pyx_PyThreadState_declare PyThreadState *__pyx_tstate; +#define __Pyx_PyThreadState_assign __pyx_tstate = __Pyx_PyThreadState_Current; +#if PY_VERSION_HEX >= 0x030C00A6 +#define __Pyx_PyErr_Occurred() (__pyx_tstate->current_exception != NULL) +#define __Pyx_PyErr_CurrentExceptionType() (__pyx_tstate->current_exception ? (PyObject*) Py_TYPE(__pyx_tstate->current_exception) : (PyObject*) NULL) +#else +#define __Pyx_PyErr_Occurred() (__pyx_tstate->curexc_type != NULL) +#define __Pyx_PyErr_CurrentExceptionType() (__pyx_tstate->curexc_type) +#endif +#else +#define __Pyx_PyThreadState_declare +#define __Pyx_PyThreadState_assign +#define __Pyx_PyErr_Occurred() (PyErr_Occurred() != NULL) +#define __Pyx_PyErr_CurrentExceptionType() PyErr_Occurred() +#endif + +/* PyErrFetchRestore.proto (used by GivenExceptionMatches) */ +#if CYTHON_FAST_THREAD_STATE +#define __Pyx_PyErr_Clear() __Pyx_ErrRestore(NULL, NULL, NULL) +#define __Pyx_ErrRestoreWithState(type, value, tb) __Pyx_ErrRestoreInState(PyThreadState_GET(), type, value, tb) +#define __Pyx_ErrFetchWithState(type, value, tb) __Pyx_ErrFetchInState(PyThreadState_GET(), type, value, tb) +#define __Pyx_ErrRestore(type, value, tb) __Pyx_ErrRestoreInState(__pyx_tstate, type, value, tb) +#define __Pyx_ErrFetch(type, value, tb) __Pyx_ErrFetchInState(__pyx_tstate, type, value, tb) +static CYTHON_INLINE void __Pyx_ErrRestoreInState(PyThreadState *tstate, PyObject *type, PyObject *value, PyObject *tb); +static CYTHON_INLINE void __Pyx_ErrFetchInState(PyThreadState *tstate, PyObject **type, PyObject **value, PyObject **tb); +#if CYTHON_COMPILING_IN_CPYTHON && PY_VERSION_HEX < 0x030C00A6 +#define __Pyx_PyErr_SetNone(exc) (Py_INCREF(exc), __Pyx_ErrRestore((exc), NULL, NULL)) +#else +#define __Pyx_PyErr_SetNone(exc) PyErr_SetNone(exc) +#endif +#else +#define __Pyx_PyErr_Clear() PyErr_Clear() +#define __Pyx_PyErr_SetNone(exc) PyErr_SetNone(exc) +#define __Pyx_ErrRestoreWithState(type, value, tb) PyErr_Restore(type, value, tb) +#define __Pyx_ErrFetchWithState(type, value, tb) PyErr_Fetch(type, value, tb) +#define __Pyx_ErrRestoreInState(tstate, type, value, tb) PyErr_Restore(type, value, tb) +#define __Pyx_ErrFetchInState(tstate, type, value, tb) PyErr_Fetch(type, value, tb) +#define __Pyx_ErrRestore(type, value, tb) PyErr_Restore(type, value, tb) +#define __Pyx_ErrFetch(type, value, tb) PyErr_Fetch(type, value, tb) +#endif + +/* GivenExceptionMatches.proto (used by IterFinish) */ +#if CYTHON_COMPILING_IN_CPYTHON +static CYTHON_INLINE int __Pyx_PyErr_GivenExceptionMatches(PyObject *err, PyObject *type); +static CYTHON_INLINE int __Pyx_PyErr_GivenExceptionMatches2(PyObject *err, PyObject *type1, PyObject *type2); +#else +#define __Pyx_PyErr_GivenExceptionMatches(err, type) PyErr_GivenExceptionMatches(err, type) +static CYTHON_INLINE int __Pyx_PyErr_GivenExceptionMatches2(PyObject *err, PyObject *type1, PyObject *type2) { + return PyErr_GivenExceptionMatches(err, type1) || PyErr_GivenExceptionMatches(err, type2); +} +#endif +#define __Pyx_PyErr_ExceptionMatches2(err1, err2) __Pyx_PyErr_GivenExceptionMatches2(__Pyx_PyErr_CurrentExceptionType(), err1, err2) + +/* IterFinish.proto */ +static CYTHON_INLINE int __Pyx_IterFinish(void); + +/* UnpackItemEndCheck.proto */ +static int __Pyx_IternextUnpackEndCheck(PyObject *retval, Py_ssize_t expected); + +/* PyFrozenDict.proto (used by GetItemInt) */ +#if CYTHON_COMPILING_IN_LIMITED_API +#define __Pyx_PyFrozenDict_TypePtr ((PyTypeObject*) __pyx_mstate_global->__Pyx_PyFrozenDictType) +#define __Pyx_PyFrozenDict_New(it) __Pyx__PyFrozenDict_New(__pyx_mstate_global->__Pyx_PyFrozenDictType, it) +static CYTHON_INLINE PyObject* __Pyx__PyFrozenDict_New(PyObject* frozendict_type, PyObject* it); +#define __Pyx_PyFrozenDict_NewEmpty() __Pyx_PyFrozenDict_New(NULL) +#define __Pyx_PyFrozenDict_Check(obj) PyObject_TypeCheck((obj), __Pyx_PyFrozenDict_TypePtr) +#define __Pyx_PyFrozenDict_CheckExact(obj) Py_IS_TYPE((obj), __Pyx_PyFrozenDict_TypePtr) +#define __Pyx_PyAnyDict_Check(obj) __Pyx__PyAnyDict_Check(obj, __Pyx_PyFrozenDict_TypePtr) +static CYTHON_INLINE int __Pyx__PyAnyDict_Check(PyObject *obj, PyTypeObject* frozendict_type) { + return PyObject_TypeCheck(obj, &PyDict_Type) || PyObject_TypeCheck(obj, frozendict_type); +} +#define __Pyx_PyAnyDict_CheckExact(obj) __Pyx__PyAnyDict_CheckExact(obj, __Pyx_PyFrozenDict_TypePtr) +static CYTHON_INLINE int __Pyx__PyAnyDict_CheckExact(PyObject *obj, PyTypeObject* frozendict_type) { + return Py_IS_TYPE(obj, &PyDict_Type) || Py_IS_TYPE(obj, frozendict_type); +} +#elif PY_VERSION_HEX >= 0x030f00a6 ||\ + (defined(PyFrozenDict_Check) && defined(PyAnyDict_Check) && defined(PyFrozenDict_New)) +#define __Pyx_PyFrozenDict_TypePtr (&PyFrozenDict_Type) +#define __Pyx_PyFrozenDict_New(it) PyFrozenDict_New(it) +#define __Pyx_PyFrozenDict_NewEmpty() PyFrozenDict_New(NULL) +#define __Pyx_PyFrozenDict_Check(obj) PyFrozenDict_Check(obj) +#define __Pyx_PyFrozenDict_CheckExact(obj) PyFrozenDict_CheckExact(obj) +#define __Pyx_PyAnyDict_Check(obj) PyAnyDict_Check(obj) +#define __Pyx_PyAnyDict_CheckExact(obj) PyAnyDict_CheckExact(obj) +#else +#define __Pyx_PyFrozenDict_TypePtr (&PyDict_Type) +static CYTHON_INLINE PyObject* __Pyx_PyFrozenDict_New(PyObject* it) { + if (!it) { + return PyDict_New(); + } else if (PyDict_Check(it)) { + return PyDict_Copy(it); + } else { + PyObject *dict = PyDict_New(); + if (!dict) return NULL; + PyObject *result = PyNumber_InPlaceOr(dict, it); + Py_DECREF(dict); + return result; + } +} +#define __Pyx_PyFrozenDict_NewEmpty() PyDict_New() +#define __Pyx_PyFrozenDict_Check(obj) PyDict_Check(obj) +#define __Pyx_PyFrozenDict_CheckExact(obj) PyDict_CheckExact(obj) +#define __Pyx_PyAnyDict_Check(obj) PyDict_Check(obj) +#define __Pyx_PyAnyDict_CheckExact(obj) PyDict_CheckExact(obj) +#endif + +/* GetItemInt.proto */ +#define __Pyx_GetItemInt(o, i, type, is_signed, to_py_func, wraparound, boundscheck, has_gil, unsafe_shared)\ + (__Pyx_fits_Py_ssize_t(i, type, is_signed) ?\ + __Pyx_GetItemInt_Fast(o, (Py_ssize_t)i, wraparound, boundscheck, unsafe_shared) :\ + __Pyx_GetItemInt_Generic(o, to_py_func(i))) +#define __Pyx_GetItemInt_List(o, i, type, is_signed, to_py_func, wraparound, boundscheck, has_gil, unsafe_shared)\ + (__Pyx_fits_Py_ssize_t(i, type, is_signed) ?\ + __Pyx_GetItemInt_List_Fast(o, (Py_ssize_t)i, wraparound, boundscheck, unsafe_shared) :\ + (PyErr_SetString(PyExc_IndexError, "list index out of range"), (PyObject*)NULL)) +static CYTHON_INLINE PyObject *__Pyx_GetItemInt_List_Fast(PyObject *o, Py_ssize_t i, + int wraparound, int boundscheck, int unsafe_shared); +#define __Pyx_GetItemInt_Tuple(o, i, type, is_signed, to_py_func, wraparound, boundscheck, has_gil, unsafe_shared)\ + (__Pyx_fits_Py_ssize_t(i, type, is_signed) ?\ + __Pyx_GetItemInt_Tuple_Fast(o, (Py_ssize_t)i, wraparound, boundscheck, unsafe_shared) :\ + (PyErr_SetString(PyExc_IndexError, "tuple index out of range"), (PyObject*)NULL)) +static CYTHON_INLINE PyObject *__Pyx_GetItemInt_Tuple_Fast(PyObject *o, Py_ssize_t i, + int wraparound, int boundscheck, int unsafe_shared); +static PyObject *__Pyx_GetItemInt_Generic(PyObject *o, PyObject* j); +static CYTHON_INLINE PyObject *__Pyx_GetItemInt_Fast(PyObject *o, Py_ssize_t i, + int wraparound, int boundscheck, int unsafe_shared); + +/* FormatTypeName.proto (used by RaiseErrorWithObjectTypes) */ +#if CYTHON_COMPILING_IN_LIMITED_API && __PYX_LIMITED_VERSION_HEX >= 0x030d0000 +typedef PyObject *__Pyx_TypeName; +#define __Pyx_FMT_TYPENAME "%N" +#define __Pyx_PyType_GetFullyQualifiedName(tp) Py_NewRef((PyObject*)tp) +#define __Pyx_DECREF_TypeName(obj) Py_DECREF(obj) +#elif CYTHON_COMPILING_IN_LIMITED_API +typedef PyObject *__Pyx_TypeName; +#define __Pyx_FMT_TYPENAME "%U" +#define __Pyx_DECREF_TypeName(obj) Py_XDECREF(obj) +static __Pyx_TypeName __Pyx_PyType_GetFullyQualifiedName(PyTypeObject* tp); +#else // !LIMITED_API +typedef const char *__Pyx_TypeName; +#define __Pyx_FMT_TYPENAME "%.200s" +#define __Pyx_PyType_GetFullyQualifiedName(tp) ((tp)->tp_name) +#define __Pyx_DECREF_TypeName(obj) +#endif + +/* RaiseErrorWithObjectTypes.proto (used by PyNumberBinop) */ +#define __Pyx_RaiseErrorWithObjectTypes1(exc_type, message, arg, obj1, obj2) __Pyx_RaiseErrorWithTypes1(exc_type, message, arg, Py_TYPE(obj1), Py_TYPE(obj2)) +#define __Pyx_RaiseTypeErrorWithObjectTypes(message, obj1, obj2) __Pyx_RaiseTypeErrorWithTypes(message, Py_TYPE(obj1), Py_TYPE(obj2)) +#define __Pyx_RaiseTypeErrorWithTypes(message, type_obj1, type_obj2) __Pyx_RaiseErrorWithTypes1(PyExc_TypeError, "%.1s" message, "", type_obj1, type_obj2) +CYTHON_UNUSED +static void __Pyx_RaiseErrorWithTypes1(PyObject* exc_type, const char *message, const char *arg, PyTypeObject *type_obj1, PyTypeObject *type_obj2); + +/* PyNumberBinop.proto */ +#if CYTHON_COMPILING_IN_PYPY || CYTHON_COMPILING_IN_GRAAL || CYTHON_COMPILING_IN_LIMITED_API +#define __Pyx_PyNumber_Add_object_object(op1, op2) PyNumber_Add(op1, op2) +#define __Pyx_PyNumber_InPlaceAdd_object_object(op1, op2) PyNumber_InPlaceAdd(op1, op2) +#else +#define __Pyx_PyNumber_Add_object_object(op1, op2) __Pyx__PyNumber_Add_object_object(op1, op2, 0) +#define __Pyx_PyNumber_InPlaceAdd_object_object(op1, op2) __Pyx__PyNumber_Add_object_object(op1, op2, 1) +static CYTHON_INLINE PyObject* __Pyx__PyNumber_Add_object_object(PyObject *op1, PyObject *op2, int inplace); +#endif + +/* IgnoreException.proto (used by GetModuleGlobalName) */ +static CYTHON_INLINE int __Pyx_IgnoreGivenException(PyObject *given_exception, PyObject *ignorable_exception); +#define __Pyx_IgnoreException(ignorable_exception) __Pyx_IgnoreGivenException(NULL, ignorable_exception) + +/* PyErrExceptionMatches.proto (used by PyObjectGetAttrStrNoError) */ +#if CYTHON_FAST_THREAD_STATE +#define __Pyx_PyErr_ExceptionMatches(err) __Pyx_PyErr_ExceptionMatchesInState(__pyx_tstate, err) +static CYTHON_INLINE int __Pyx_PyErr_ExceptionMatchesInState(PyThreadState* tstate, PyObject* err); +#else +#define __Pyx_PyErr_ExceptionMatches(err) PyErr_ExceptionMatches(err) +#endif + +/* PyObjectGetAttrStr.proto (used by PyObjectGetAttrStrNoError) */ +#if CYTHON_USE_TYPE_SLOTS +static CYTHON_INLINE PyObject* __Pyx_PyObject_GetAttrStr(PyObject* obj, PyObject* attr_name); +#else +#define __Pyx_PyObject_GetAttrStr(o,n) PyObject_GetAttr(o,n) +#endif + +/* PyObjectGetAttrStrNoError.proto (used by GetBuiltinName) */ +static CYTHON_INLINE PyObject* __Pyx_PyObject_GetAttrStrNoError(PyObject* obj, PyObject* attr_name); + +/* GetBuiltinName.proto (used by GetModuleGlobalName) */ +static PyObject *__Pyx_GetBuiltinName(PyObject *name); + +/* PyDictVersioning.proto (used by GetModuleGlobalName) */ +#if CYTHON_USE_DICT_VERSIONS && CYTHON_USE_TYPE_SLOTS +#define __PYX_DICT_VERSION_INIT ((PY_UINT64_T) -1) +#define __PYX_GET_DICT_VERSION(dict) (((PyDictObject*)(dict))->ma_version_tag) +#define __PYX_UPDATE_DICT_CACHE(dict, value, cache_var, version_var)\ + (version_var) = __PYX_GET_DICT_VERSION(dict);\ + (cache_var) = (value); +#define __PYX_PY_DICT_LOOKUP_IF_MODIFIED(VAR, DICT, LOOKUP) {\ + static PY_UINT64_T __pyx_dict_version = 0;\ + static PyObject *__pyx_dict_cached_value = NULL;\ + if (likely(__PYX_GET_DICT_VERSION(DICT) == __pyx_dict_version)) {\ + (VAR) = __Pyx_XNewRef(__pyx_dict_cached_value);\ + } else {\ + (VAR) = __pyx_dict_cached_value = (LOOKUP);\ + __pyx_dict_version = __PYX_GET_DICT_VERSION(DICT);\ + }\ +} +static CYTHON_INLINE PY_UINT64_T __Pyx_get_tp_dict_version(PyObject *obj); +static CYTHON_INLINE PY_UINT64_T __Pyx_get_object_dict_version(PyObject *obj); +static CYTHON_INLINE int __Pyx_object_dict_version_matches(PyObject* obj, PY_UINT64_T tp_dict_version, PY_UINT64_T obj_dict_version); +#else +#define __PYX_GET_DICT_VERSION(dict) (0) +#define __PYX_UPDATE_DICT_CACHE(dict, value, cache_var, version_var) +#define __PYX_PY_DICT_LOOKUP_IF_MODIFIED(VAR, DICT, LOOKUP) (VAR) = (LOOKUP); +#endif + +/* GetModuleGlobalName.proto */ +#if CYTHON_USE_DICT_VERSIONS +#define __Pyx_GetModuleGlobalName(var, name) do {\ + static PY_UINT64_T __pyx_dict_version = 0;\ + static PyObject *__pyx_dict_cached_value = NULL;\ + (var) = (likely(__pyx_dict_version == __PYX_GET_DICT_VERSION(__pyx_mstate_global->__pyx_d))) ?\ + (likely(__pyx_dict_cached_value) ? __Pyx_NewRef(__pyx_dict_cached_value) : __Pyx_GetBuiltinName(name)) :\ + __Pyx__GetModuleGlobalName(name, &__pyx_dict_version, &__pyx_dict_cached_value);\ +} while(0) +#define __Pyx_GetModuleGlobalNameUncached(var, name) do {\ + PY_UINT64_T __pyx_dict_version;\ + PyObject *__pyx_dict_cached_value;\ + (var) = __Pyx__GetModuleGlobalName(name, &__pyx_dict_version, &__pyx_dict_cached_value);\ +} while(0) +static PyObject *__Pyx__GetModuleGlobalName(PyObject *name, PY_UINT64_T *dict_version, PyObject **dict_cached_value); +#else +#define __Pyx_GetModuleGlobalName(var, name) (var) = __Pyx__GetModuleGlobalName(name) +#define __Pyx_GetModuleGlobalNameUncached(var, name) (var) = __Pyx__GetModuleGlobalName(name) +static CYTHON_INLINE PyObject *__Pyx__GetModuleGlobalName(PyObject *name); +#endif + +/* CopyObjectArray.proto (used by TupleOrListFromArrayImpl) */ +#if CYTHON_COMPILING_IN_CPYTHON +static CYTHON_INLINE void __Pyx_copy_object_array(PyObject *const *CYTHON_RESTRICT src, PyObject** CYTHON_RESTRICT dest, Py_ssize_t length); +#endif + +/* TupleOrListFromArrayImpl.proto (used by TupleFromArray) */ +#if PY_VERSION_HEX >= 0x030F0000 && !CYTHON_COMPILING_IN_LIMITED_API +#define __Pyx_PyTuple_FromArray(src, n) PyTuple_FromArray(src, ((n)<0) ? 0 : (n)) +#else +CYTHON_UNUSED static PyObject * +__Pyx_PyTuple_FromArray(PyObject *const *src, Py_ssize_t n); +#endif + +/* TupleFromArray.proto (used by fastcall) */ + + +/* IncludeStringH.proto (used by PyObjectCompare) */ +#include + +/* PyObjectCompare.proto (used by UnicodeEquals) */ +static CYTHON_INLINE int __Pyx_PyObject_CompareBoolEq_str_str(PyObject *op1, PyObject *op2, int pyop); + +/* UnicodeEquals.proto (used by fastcall) */ +#define __Pyx_PyUnicode_Equals(s1, s2) __Pyx_PyObject_CompareBoolEq_str_str(s1, s2, Py_EQ) + +/* fastcall.proto */ +#if CYTHON_AVOID_BORROWED_REFS + #define __Pyx_ArgRef_VARARGS(args, i) __Pyx_PySequence_ITEM(args, i) +#elif CYTHON_ASSUME_SAFE_MACROS + #define __Pyx_ArgRef_VARARGS(args, i) __Pyx_NewRef(__Pyx_PyTuple_GET_ITEM(args, i)) +#else + #define __Pyx_ArgRef_VARARGS(args, i) __Pyx_XNewRef(PyTuple_GetItem(args, i)) +#endif +#define __Pyx_NumKwargs_VARARGS(kwds) PyDict_Size(kwds) +#define __Pyx_KwValues_VARARGS(args, nargs) NULL +#define __Pyx_GetKwValue_VARARGS(kw, kwvalues, s) __Pyx_PyDict_GetItemStrWithError(kw, s) +#define __Pyx_KwargsAsDict_VARARGS(kw, kwvalues) PyDict_Copy(kw) +#if CYTHON_VECTORCALL + #define __Pyx_ArgRef_FASTCALL(args, i) __Pyx_NewRef(args[i]) + #define __Pyx_NumKwargs_FASTCALL(kwds) __Pyx_PyTuple_GET_SIZE(kwds) + #define __Pyx_KwValues_FASTCALL(args, nargs) ((args) + (nargs)) + static CYTHON_INLINE PyObject * __Pyx_GetKwValue_FASTCALL(PyObject *kwnames, PyObject *const *kwvalues, PyObject *s); + #if CYTHON_COMPILING_IN_CPYTHON && PY_VERSION_HEX >= 0x030d0000 || CYTHON_COMPILING_IN_LIMITED_API || CYTHON_COMPILING_IN_PYPY || CYTHON_COMPILING_IN_GRAAL + CYTHON_UNUSED static PyObject *__Pyx_KwargsAsDict_FASTCALL(PyObject *kwnames, PyObject *const *kwvalues); + #else + #define __Pyx_KwargsAsDict_FASTCALL(kw, kwvalues) _PyStack_AsDict(kwvalues, kw) + #endif +#else + #define __Pyx_ArgRef_FASTCALL __Pyx_ArgRef_VARARGS + #define __Pyx_NumKwargs_FASTCALL __Pyx_NumKwargs_VARARGS + #define __Pyx_KwValues_FASTCALL __Pyx_KwValues_VARARGS + #define __Pyx_GetKwValue_FASTCALL __Pyx_GetKwValue_VARARGS + #define __Pyx_KwargsAsDict_FASTCALL __Pyx_KwargsAsDict_VARARGS +#endif +#if CYTHON_VECTORCALL_TPNEW + #if !CYTHON_VECTORCALL + #error Enabling CYTHON_VECTORCALL_TPNEW without CYTHON_VECTORCALL is not supported + #endif + #define __Pyx_ArgRef_FASTCALL_TPNEW __Pyx_ArgRef_FASTCALL + #define __Pyx_NumKwargs_FASTCALL_TPNEW __Pyx_NumKwargs_FASTCALL + #define __Pyx_KwValues_FASTCALL_TPNEW __Pyx_KwValues_FASTCALL + #define __Pyx_GetKwValue_FASTCALL_TPNEW __Pyx_GetKwValue_FASTCALL + #define __Pyx_KwargsAsDict_FASTCALL_TPNEW __Pyx_KwargsAsDict_FASTCALL +#else + #define __Pyx_ArgRef_FASTCALL_TPNEW __Pyx_ArgRef_VARARGS + #define __Pyx_NumKwargs_FASTCALL_TPNEW __Pyx_NumKwargs_VARARGS + #define __Pyx_KwValues_FASTCALL_TPNEW __Pyx_KwValues_VARARGS + #define __Pyx_GetKwValue_FASTCALL_TPNEW __Pyx_GetKwValue_VARARGS + #define __Pyx_KwargsAsDict_FASTCALL_TPNEW __Pyx_KwargsAsDict_VARARGS +#endif +#define __Pyx_ArgsSlice_VARARGS(args, start, stop) PyTuple_GetSlice(args, start, stop) +#if CYTHON_VECTORCALL +#define __Pyx_ArgsSlice_FASTCALL(args, start, stop) __Pyx_PyTuple_FromArray(args + start, stop - start) +#else +#define __Pyx_ArgsSlice_FASTCALL __Pyx_ArgsSlice_VARARGS +#endif + +/* py_dict_items.proto (used by OwnedDictNext) */ +#define __Pyx_PyDict_items_TypePtr (&PyDictKeys_Type) +#define __Pyx_PyDict_items_Check(obj) PyObject_TypeCheck((obj), __Pyx_PyDictItems_TypePtr) +#define __Pyx_PyDict_items_CheckExact(obj) Py_IS_TYPE((obj), __Pyx_PyDictItems_TypePtr) +static CYTHON_INLINE PyObject* __Pyx_PyDict_Items(PyObject* d); + +/* CallCFunction.proto (used by CallUnboundCMethod0) */ +#define __Pyx_CallCFunction(cfunc, self, args)\ + ((PyCFunction)(void(*)(void))(cfunc)->func)(self, args) +#define __Pyx_CallCFunctionWithKeywords(cfunc, self, args, kwargs)\ + ((PyCFunctionWithKeywords)(void(*)(void))(cfunc)->func)(self, args, kwargs) +#define __Pyx_CallCFunctionFast(cfunc, self, args, nargs)\ + ((__Pyx_PyCFunctionFast)(void(*)(void))(PyCFunction)(cfunc)->func)(self, args, nargs) +#define __Pyx_CallCFunctionFastWithKeywords(cfunc, self, args, nargs, kwnames)\ + ((__Pyx_PyCFunctionFastWithKeywords)(void(*)(void))(PyCFunction)(cfunc)->func)(self, args, nargs, kwnames) + +/* PyObjectCallOneArg.proto (used by CallUnboundCMethod0) */ +static CYTHON_INLINE PyObject* __Pyx_PyObject_CallOneArg(PyObject *func, PyObject *arg); + +/* UnpackUnboundCMethod_decl.proto (used by UnpackUnboundCMethod) */ +typedef struct { + PyObject *type; + PyObject **method_name; + PyCFunction func; + PyObject *method; + int flag; +#if CYTHON_COMPILING_IN_CPYTHON_FREETHREADING && CYTHON_ATOMICS + __pyx_atomic_int_type initialized; +#endif +} __Pyx_CachedCFunction; + +/* UnpackUnboundCMethod_impl.export */ +static int __Pyx_TryUnpackUnboundCMethod(__Pyx_CachedCFunction* target); + +/* UnpackUnboundCMethod.proto (used by CallUnboundCMethod0) */ +#if CYTHON_COMPILING_IN_CPYTHON_FREETHREADING +static CYTHON_INLINE int __Pyx_CachedCFunction_GetAndSetInitializing(__Pyx_CachedCFunction *cfunc) { +#if !CYTHON_ATOMICS + return 1; +#else + __pyx_nonatomic_int_type expected = 0; + if (__pyx_atomic_int_cmp_exchange(&cfunc->initialized, &expected, 1)) { + return 0; + } + return expected; +#endif +} +static CYTHON_INLINE void __Pyx_CachedCFunction_SetFinishedInitializing(__Pyx_CachedCFunction *cfunc) { +#if CYTHON_ATOMICS + __pyx_atomic_store(&cfunc->initialized, 2); +#endif +} +#else +#define __Pyx_CachedCFunction_GetAndSetInitializing(cfunc) 2 +#define __Pyx_CachedCFunction_SetFinishedInitializing(cfunc) +#endif + +/* CallUnboundCMethod0.proto */ +CYTHON_UNUSED +static PyObject* __Pyx__CallUnboundCMethod0(__Pyx_CachedCFunction* cfunc, PyObject* self); +#if CYTHON_COMPILING_IN_CPYTHON +static CYTHON_INLINE PyObject* __Pyx_CallUnboundCMethod0(__Pyx_CachedCFunction* cfunc, PyObject* self); +#else +#define __Pyx_CallUnboundCMethod0(cfunc, self) __Pyx__CallUnboundCMethod0(cfunc, self) +#endif + +/* py_dict_values.proto (used by OwnedDictNext) */ +#define __Pyx_PyDict_values_TypePtr (&PyDictKeys_Type) +#define __Pyx_PyDict_values_Check(obj) PyObject_TypeCheck((obj), __Pyx_PyDictValues_TypePtr) +#define __Pyx_PyDict_values_CheckExact(obj) Py_IS_TYPE((obj), __Pyx_PyDictValues_TypePtr) +static CYTHON_INLINE PyObject* __Pyx_PyDict_Values(PyObject* d); + +/* OwnedDictNext.proto (used by ParseKeywordsImpl) */ +#if CYTHON_AVOID_BORROWED_REFS +static int __Pyx_PyDict_NextRef(PyObject *p, PyObject **ppos, PyObject **pkey, PyObject **pvalue); +#else +CYTHON_INLINE +static int __Pyx_PyDict_NextRef(PyObject *p, Py_ssize_t *ppos, PyObject **pkey, PyObject **pvalue); +#endif + +/* RaiseDoubleKeywords.proto (used by ParseKeywordsImpl) */ +static void __Pyx_RaiseDoubleKeywordsError(const char* func_name, PyObject* kw_name); + +/* ParseKeywordsImpl.export */ +static int __Pyx_ParseKeywordsTuple( + PyObject *kwds, + PyObject * const *kwvalues, + PyObject ** const argnames[], + PyObject *kwds2, + PyObject *values[], + Py_ssize_t num_pos_args, + Py_ssize_t num_kwargs, + const char* function_name, + int ignore_unknown_kwargs +); +static int __Pyx_ParseKeywordDictToDict( + PyObject *kwds, + PyObject ** const argnames[], + PyObject *kwds2, + PyObject *values[], + Py_ssize_t num_pos_args, + const char* function_name +); +static int __Pyx_ParseKeywordDict( + PyObject *kwds, + PyObject ** const argnames[], + PyObject *values[], + Py_ssize_t num_pos_args, + Py_ssize_t num_kwargs, + const char* function_name, + int ignore_unknown_kwargs +); + +/* CallUnboundCMethod2.proto */ +CYTHON_UNUSED +static PyObject* __Pyx__CallUnboundCMethod2(__Pyx_CachedCFunction* cfunc, PyObject* self, PyObject* arg1, PyObject* arg2); +#if CYTHON_COMPILING_IN_CPYTHON +static CYTHON_INLINE PyObject *__Pyx_CallUnboundCMethod2(__Pyx_CachedCFunction *cfunc, PyObject *self, PyObject *arg1, PyObject *arg2); +#else +#define __Pyx_CallUnboundCMethod2(cfunc, self, arg1, arg2) __Pyx__CallUnboundCMethod2(cfunc, self, arg1, arg2) +#endif + +/* ParseKeywords.proto */ +static CYTHON_INLINE int __Pyx_ParseKeywords( + PyObject *kwds, PyObject *const *kwvalues, PyObject ** const argnames[], + PyObject *kwds2, PyObject *values[], + Py_ssize_t num_pos_args, Py_ssize_t num_kwargs, + const char* function_name, + int ignore_unknown_kwargs +); + +/* RaiseArgTupleInvalid.export */ +static void __Pyx_RaiseArgtupleInvalid(const char* func_name, int exact, + Py_ssize_t num_min, Py_ssize_t num_max, Py_ssize_t num_found); + +/* GetException.proto (used by pep479) */ +#if CYTHON_FAST_THREAD_STATE +#define __Pyx_GetException(type, value, tb) __Pyx__GetException(__pyx_tstate, type, value, tb) +static int __Pyx__GetException(PyThreadState *tstate, PyObject **type, PyObject **value, PyObject **tb); +#else +static int __Pyx_GetException(PyObject **type, PyObject **value, PyObject **tb); +#endif + +/* pep479.proto */ +static void __Pyx_Generator_Replace_StopIteration(int in_async_gen); + +/* GetTopmostException.proto (used by SaveResetException) */ +#if CYTHON_USE_EXC_INFO_STACK && CYTHON_FAST_THREAD_STATE +static _PyErr_StackItem * __Pyx_PyErr_GetTopmostException(PyThreadState *tstate); +#endif + +/* SaveResetException.proto */ +#if CYTHON_FAST_THREAD_STATE +#define __Pyx_ExceptionSave(type, value, tb) __Pyx__ExceptionSave(__pyx_tstate, type, value, tb) +static CYTHON_INLINE void __Pyx__ExceptionSave(PyThreadState *tstate, PyObject **type, PyObject **value, PyObject **tb); +#define __Pyx_ExceptionReset(type, value, tb) __Pyx__ExceptionReset(__pyx_tstate, type, value, tb) +static CYTHON_INLINE void __Pyx__ExceptionReset(PyThreadState *tstate, PyObject *type, PyObject *value, PyObject *tb); +#else +#define __Pyx_ExceptionSave(type, value, tb) PyErr_GetExcInfo(type, value, tb) +#define __Pyx_ExceptionReset(type, value, tb) PyErr_SetExcInfo(type, value, tb) +#endif + +/* PyZeroDivisionError_Check.proto */ +#define __Pyx_PyExc_ZeroDivisionError_Check(obj) __Pyx_TypeCheck(obj, PyExc_ZeroDivisionError) + +/* IterNextPlain.proto (used by IterNext) */ +static CYTHON_INLINE PyObject *__Pyx_PyIter_Next_Plain(PyObject *iterator); +#if CYTHON_COMPILING_IN_LIMITED_API && __PYX_LIMITED_VERSION_HEX < 0x030A0000 +static PyObject *__Pyx_GetBuiltinNext_LimitedAPI(void); +#endif + +/* RaiseErrorWithObjectType.proto (used by IterNext) */ +#define __Pyx_RaiseTypeErrorWithObjectType(message, obj) __Pyx_RaiseErrorWithObjectType(PyExc_TypeError, message, obj) +#define __Pyx_RaiseErrorWithObjectType(exc_type, message, obj) __Pyx_RaiseErrorWithType(exc_type, message, Py_TYPE(obj)) +CYTHON_UNUSED +static void __Pyx_RaiseErrorWithType(PyObject* exc_type, const char* message, PyTypeObject *type_obj); + +/* IterNext.proto */ +#define __Pyx_PyIter_Next(obj) __Pyx_PyIter_Next2(obj, NULL) +static CYTHON_INLINE PyObject *__Pyx_PyIter_Next2(PyObject *, PyObject *); + +/* ListAppend.proto */ +#if CYTHON_USE_PYLIST_INTERNALS && CYTHON_ASSUME_SAFE_MACROS && CYTHON_ASSUME_SAFE_SIZE +static CYTHON_INLINE int __Pyx_PyList_Append(PyObject* list, PyObject* x); +#else +#define __Pyx_PyList_Append(L,x) PyList_Append(L,x) +#endif + +/* ListCompAppendAndDecref.proto */ +static CYTHON_INLINE int __Pyx_ListComp_AppendAndDecref(PyObject* list, PyObject* x); + +/* PyLongBinop.proto */ +#if !CYTHON_COMPILING_IN_PYPY +static CYTHON_INLINE PyObject* __Pyx_PyLong_AddObjC(PyObject *op1, PyObject *op2, long intval, int inplace, int zerodivision_check); +#else +#define __Pyx_PyLong_AddObjC(op1, op2, intval, inplace, zerodivision_check)\ + (inplace ? PyNumber_InPlaceAdd(op1, op2) : PyNumber_Add(op1, op2)) +#endif + +/* RaiseException.export */ +static void __Pyx_Raise(PyObject *type, PyObject *value, PyObject *tb, PyObject *cause); + +/* PyValueError_Check.proto */ +#define __Pyx_PyExc_ValueError_Check(obj) __Pyx_TypeCheck(obj, PyExc_ValueError) + +/* PyObjectCompare.proto */ +static CYTHON_INLINE int __Pyx_PyObject_CompareBoolEq_int_object(PyObject *op1, PyObject *op2, int pyop); + +/* SetItemInt.proto */ +#define __Pyx_SetItemInt(o, i, v, type, is_signed, to_py_func, wraparound, boundscheck, has_gil, unsafe_shared)\ + (__Pyx_fits_Py_ssize_t(i, type, is_signed) ?\ + __Pyx_SetItemInt_Fast(o, (Py_ssize_t)i, v, wraparound, boundscheck, unsafe_shared) :\ + __Pyx_SetItemInt_Generic(o, to_py_func(i), v)) +static int __Pyx_SetItemInt_Generic(PyObject *o, PyObject *j, PyObject *v); +static CYTHON_INLINE int __Pyx_SetItemInt_Fast(PyObject *o, Py_ssize_t i, PyObject *v, + int wraparound, int boundscheck, int unsafe_shared); + +/* ModInt[long].proto */ +static CYTHON_INLINE long __Pyx_mod_long(long, long, int b_is_constant); + +/* CheckTypeForFreelists.proto */ +#if CYTHON_USE_FREELISTS +#if CYTHON_USE_TYPE_SPECS +#define __PYX_CHECK_FINAL_TYPE_FOR_FREELISTS(t, expected_tp, expected_size) ((int) ((t) == (expected_tp))) +#define __PYX_CHECK_TYPE_FOR_FREELIST_FLAGS Py_TPFLAGS_IS_ABSTRACT +#else +#define __PYX_CHECK_FINAL_TYPE_FOR_FREELISTS(t, expected_tp, expected_size) ((int) ((t)->tp_basicsize == (expected_size))) +#define __PYX_CHECK_TYPE_FOR_FREELIST_FLAGS (Py_TPFLAGS_IS_ABSTRACT | Py_TPFLAGS_HEAPTYPE) +#endif +#define __PYX_CHECK_TYPE_FOR_FREELISTS(t, expected_tp, expected_size)\ + (__PYX_CHECK_FINAL_TYPE_FOR_FREELISTS((t), (expected_tp), (expected_size)) &\ + (int) (!__Pyx_PyType_HasFeature((t), __PYX_CHECK_TYPE_FOR_FREELIST_FLAGS))) +#endif + +/* AllocateExtensionType.proto */ +static PyObject *__Pyx_AllocateExtensionType(PyTypeObject *t, int is_final); + +/* CallSlotAsVectorcall.proto */ +#if CYTHON_VECTORCALL_TPNEW +typedef PyObject * (*__Pyx_tpnewvectorcallfunc)(PyTypeObject* o, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames); +static PyObject * __Pyx_CallTpnewAsVectorcall(__Pyx_tpnewvectorcallfunc f, PyTypeObject* o, PyObject *a, PyObject *k); +#endif + +/* CallNewInitFromVectorcall.proto */ +#if CYTHON_VECTORCALL_TPNEW +static PyObject *__Pyx_CallNewInitFromVectorcall(PyTypeObject *t, PyObject *const *args, size_t nargsf, PyObject *kwnames); +#endif + +/* PyObjectCallMethod0.proto (used by PyType_Ready) */ +static CYTHON_INLINE PyObject* __Pyx_PyObject_CallMethod0(PyObject* obj, PyObject* method_name); + +/* GetTypeDictOffset.proto (used by ValidateBasesTuple) */ +#if !CYTHON_USE_TYPE_SLOTS +CYTHON_UNUSED static Py_ssize_t __Pyx_GetTypeDictOffset(PyObject *tp, int require_cython_valid_result); +#endif + +/* RaiseErrorWithObjectType1.proto (used by ValidateBasesTuple) */ +#define __Pyx_RaiseTypeErrorWithObjectType1(message, arg, obj) __Pyx_RaiseErrorWithObjectType1(PyExc_TypeError, message, arg, obj) +#define __Pyx_RaiseErrorWithObjectType1(exc_type, message, arg, obj) __Pyx_RaiseErrorWithType1(exc_type, message, arg, Py_TYPE(obj)) +CYTHON_UNUSED +static void __Pyx_RaiseErrorWithType1(PyObject* exc_type, const char* message, const char *arg, PyTypeObject *type_obj); + +/* ValidateBasesTuple.proto (used by PyType_Ready) */ +#if CYTHON_COMPILING_IN_CPYTHON || CYTHON_COMPILING_IN_LIMITED_API || CYTHON_USE_TYPE_SPECS +static int __Pyx_validate_bases_tuple(const char *type_name, int has_dictoffset, PyObject *bases); +#endif + +/* PyType_Ready.export */ +CYTHON_UNUSED static int __Pyx_PyType_Ready(PyTypeObject *t); + +/* HasAttr.proto (used by ImportImpl) */ +#if __PYX_LIMITED_VERSION_HEX >= 0x030d0000 +#define __Pyx_HasAttr(o, n) PyObject_HasAttrWithError(o, n) +#else +static CYTHON_INLINE int __Pyx_HasAttr(PyObject *, PyObject *); +#endif + +/* TupleOrListFromArrayImpl.proto (used by ListFromArray) */ +CYTHON_UNUSED static PyObject * +__Pyx_PyList_FromArray(PyObject *const *src, Py_ssize_t n); + +/* ListFromArray.proto (used by ImportImpl) */ + + +/* ImportImpl.export */ +static PyObject *__Pyx__Import(PyObject *name, PyObject *const *imported_names, Py_ssize_t len_imported_names, PyObject *qualname, PyObject *moddict, int level); + +/* Import.proto */ +static CYTHON_INLINE PyObject *__Pyx_Import(PyObject *name, PyObject *const *imported_names, Py_ssize_t len_imported_names, PyObject *qualname, int level); + +/* ImportFrom.export */ +static PyObject* __Pyx_ImportFrom(PyObject* module, PyObject* name); + +/* pybytes_as_double.proto (used by pyunicode_as_double) */ +static double __Pyx_SlowPyString_AsDouble(PyObject *obj); +static double __Pyx__PyBytes_AsDouble(PyObject *obj, const char* start, Py_ssize_t length); +static CYTHON_INLINE double __Pyx_PyBytes_AsDouble(PyObject *obj) { + char* as_c_string; + Py_ssize_t size; +#if CYTHON_ASSUME_SAFE_MACROS && CYTHON_ASSUME_SAFE_SIZE + as_c_string = PyBytes_AS_STRING(obj); + size = PyBytes_GET_SIZE(obj); +#else + if (PyBytes_AsStringAndSize(obj, &as_c_string, &size) < 0) { + return (double)-1; + } +#endif + return __Pyx__PyBytes_AsDouble(obj, as_c_string, size); +} +static CYTHON_INLINE double __Pyx_PyByteArray_AsDouble(PyObject *obj) { + char* as_c_string; + Py_ssize_t size; +#if CYTHON_ASSUME_SAFE_MACROS && CYTHON_ASSUME_SAFE_SIZE + as_c_string = PyByteArray_AS_STRING(obj); + size = PyByteArray_GET_SIZE(obj); +#else + as_c_string = PyByteArray_AsString(obj); + if (as_c_string == NULL) { + return (double)-1; + } + size = PyByteArray_Size(obj); +#endif + return __Pyx__PyBytes_AsDouble(obj, as_c_string, size); +} + +/* pyunicode_as_double.proto */ +#if !CYTHON_COMPILING_IN_PYPY && CYTHON_ASSUME_SAFE_MACROS +static const char* __Pyx__PyUnicode_AsDouble_Copy(const void* data, const int kind, char* buffer, Py_ssize_t start, Py_ssize_t end) { + int last_was_punctuation; + Py_ssize_t i; + last_was_punctuation = 1; + for (i=start; i <= end; i++) { + Py_UCS4 chr = PyUnicode_READ(kind, data, i); + int is_punctuation = (chr == '_') | (chr == '.'); + *buffer = (char)chr; + buffer += (chr != '_'); + if (unlikely(chr > 127)) goto parse_failure; + if (unlikely(last_was_punctuation & is_punctuation)) goto parse_failure; + last_was_punctuation = is_punctuation; + } + if (unlikely(last_was_punctuation)) goto parse_failure; + *buffer = '\0'; + return buffer; +parse_failure: + return NULL; +} +static double __Pyx__PyUnicode_AsDouble_inf_nan(const void* data, int kind, Py_ssize_t start, Py_ssize_t length) { + int matches = 1; + Py_UCS4 chr; + Py_UCS4 sign = PyUnicode_READ(kind, data, start); + int is_signed = (sign == '-') | (sign == '+'); + start += is_signed; + length -= is_signed; + switch (PyUnicode_READ(kind, data, start)) { + #ifdef Py_NAN + case 'n': + case 'N': + if (unlikely(length != 3)) goto parse_failure; + chr = PyUnicode_READ(kind, data, start+1); + matches &= (chr == 'a') | (chr == 'A'); + chr = PyUnicode_READ(kind, data, start+2); + matches &= (chr == 'n') | (chr == 'N'); + if (unlikely(!matches)) goto parse_failure; + return (sign == '-') ? -Py_NAN : Py_NAN; + #endif + case 'i': + case 'I': + if (unlikely(length < 3)) goto parse_failure; + chr = PyUnicode_READ(kind, data, start+1); + matches &= (chr == 'n') | (chr == 'N'); + chr = PyUnicode_READ(kind, data, start+2); + matches &= (chr == 'f') | (chr == 'F'); + if (likely(length == 3 && matches)) + return (sign == '-') ? -Py_HUGE_VAL : Py_HUGE_VAL; + if (unlikely(length != 8)) goto parse_failure; + chr = PyUnicode_READ(kind, data, start+3); + matches &= (chr == 'i') | (chr == 'I'); + chr = PyUnicode_READ(kind, data, start+4); + matches &= (chr == 'n') | (chr == 'N'); + chr = PyUnicode_READ(kind, data, start+5); + matches &= (chr == 'i') | (chr == 'I'); + chr = PyUnicode_READ(kind, data, start+6); + matches &= (chr == 't') | (chr == 'T'); + chr = PyUnicode_READ(kind, data, start+7); + matches &= (chr == 'y') | (chr == 'Y'); + if (unlikely(!matches)) goto parse_failure; + return (sign == '-') ? -Py_HUGE_VAL : Py_HUGE_VAL; + case '.': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': + break; + default: + goto parse_failure; + } + return 0.0; +parse_failure: + return -1.0; +} +static double __Pyx_PyUnicode_AsDouble_WithSpaces(PyObject *obj) { + double value; + const char *last; + char *end; + int valid_parse; + Py_ssize_t start, length = PyUnicode_GET_LENGTH(obj); + const int kind = PyUnicode_KIND(obj); + const void* data = PyUnicode_DATA(obj); + start = 0; + while (Py_UNICODE_ISSPACE(PyUnicode_READ(kind, data, start))) + start++; + while (start < length - 1 && Py_UNICODE_ISSPACE(PyUnicode_READ(kind, data, length - 1))) + length--; + length -= start; + if (unlikely(length <= 0)) goto fallback; + value = __Pyx__PyUnicode_AsDouble_inf_nan(data, kind, start, length); + if (value != 0.0) { + if (unlikely(value == -1.0)) goto fallback; + return value; + } + if (length < 40) { + char number[40]; + last = __Pyx__PyUnicode_AsDouble_Copy(data, kind, number, start, start + length); + if (unlikely(!last)) goto fallback; + value = PyOS_string_to_double(number, &end, NULL); + valid_parse = (end == last); + } else { + char *number = (char*) PyMem_Malloc(((size_t) length + 1) * sizeof(char)); + if (unlikely(!number)) goto fallback; + last = __Pyx__PyUnicode_AsDouble_Copy(data, kind, number, start, start + length); + if (unlikely(!last)) { + PyMem_Free(number); + goto fallback; + } + value = PyOS_string_to_double(number, &end, NULL); + valid_parse = (end == last); + PyMem_Free(number); + } + if (likely(valid_parse) || (value == (double)-1 && PyErr_Occurred())) { + return value; + } +fallback: + return __Pyx_SlowPyString_AsDouble(obj); +} +#endif +static CYTHON_INLINE double __Pyx_PyUnicode_AsDouble(PyObject *obj) { +#if !CYTHON_COMPILING_IN_PYPY && CYTHON_ASSUME_SAFE_MACROS + if (unlikely(__Pyx_PyUnicode_READY(obj) == -1)) + return (double)-1; + if (likely(PyUnicode_IS_ASCII(obj))) { + const char *s; + Py_ssize_t length; + s = PyUnicode_AsUTF8AndSize(obj, &length); + return __Pyx__PyBytes_AsDouble(obj, s, length); + } + return __Pyx_PyUnicode_AsDouble_WithSpaces(obj); +#else + return __Pyx_SlowPyString_AsDouble(obj); +#endif +} + +/* FloatExceptionCheck.proto */ +#define __PYX_CHECK_FLOAT_EXCEPTION(value, error_value)\ + ((error_value) == (error_value) ?\ + (value) == (error_value) :\ + (value) != (value)) + +/* dict_setdefault.proto (used by FetchCommonType) */ +static CYTHON_INLINE PyObject *__Pyx_PyDict_SetDefault(PyObject *d, PyObject *key, PyObject *default_value); + +/* AddModuleRef.proto (used by FetchSharedCythonModule) */ +#if ((CYTHON_COMPILING_IN_CPYTHON_FREETHREADING && PY_VERSION_HEX < 0x030F00a3) ||\ + __PYX_LIMITED_VERSION_HEX < 0x030d0000) + static PyObject *__Pyx_PyImport_AddModuleRef(const char *name); +#else + #define __Pyx_PyImport_AddModuleRef(name) PyImport_AddModuleRef(name) +#endif + +/* FetchSharedCythonModule.proto (used by FetchCommonType) */ +static PyObject *__Pyx_FetchSharedCythonABIModule(void); + +/* VerifyCachedType.proto (used by FetchCommonType) */ +static int __Pyx_VerifyCachedType(PyObject *cached_type, + const char *name, + Py_ssize_t expected_basicsize); + +/* FetchCommonType.proto (used by CommonTypesMetaclass) */ +static PyTypeObject* __Pyx_FetchCommonTypeFromSpec(PyTypeObject *metaclass, PyObject *module, PyType_Spec *spec, PyObject *bases); + +/* CommonTypesMetaclass.proto (used by CythonFunctionShared) */ +static int __pyx_CommonTypesMetaclass_init(PyObject *module); +#define __Pyx_CommonTypesMetaclass_USED + +/* CythonFunctionPerModule.proto (used by CythonFunctionShared) */ +#define __Pyx_CyFunction_USED +#if CYTHON_OPAQUE_SHARED_TYPES +#define __Pyx_as_CyFunctionObject(o) ((__pyx_CyFunctionObject *)PyObject_GetTypeData((o), __pyx_mstate_global->__pyx_CyFunctionType)) +#else +#define __Pyx_as_CyFunctionObject(o) ((__pyx_CyFunctionObject *)o) +#endif +#define __Pyx_CYFUNCTION_STATICMETHOD 0x01 +#define __Pyx_CYFUNCTION_CLASSMETHOD 0x02 +#define __Pyx_CYFUNCTION_CCLASS 0x04 +#define __Pyx_CYFUNCTION_COROUTINE 0x08 +#define __Pyx_CyFunction_GetClosure(f)\ + ((__Pyx_as_CyFunctionObject(f))->func_closure) +#if CYTHON_COMPILING_IN_LIMITED_API + #define __Pyx__CyFunction_GetClassObj(f)\ + ((f)->func_classobj) +#else + #define __Pyx__CyFunction_GetClassObj(f)\ + ((PyObject*) ((PyCMethodObject *) (f))->mm_class) +#endif +#define __Pyx_CyFunction_GetClassObj(f)\ + __Pyx__CyFunction_GetClassObj(__Pyx_as_CyFunctionObject(f)) +#define __Pyx_CyFunction_SetClassObj(f, classobj)\ + __Pyx__CyFunction_SetClassObj(__Pyx_as_CyFunctionObject(f), (classobj)) +#define __Pyx_CyFunction_Defaults(type, f)\ + ((type *)((__Pyx_as_CyFunctionObject(f))->defaults)) +#define __Pyx_CyFunction_SetDefaultsGetter(f, g)\ + (__Pyx_as_CyFunctionObject(f))->defaults_getter = (g) +typedef struct { +#if CYTHON_COMPILING_IN_LIMITED_API +#if !CYTHON_OPAQUE_OBJECTS + PyObject_HEAD +#endif + PyMethodDef *func_methoddef; + PyObject *func_module; +#else + PyCMethodObject func; +#endif +#if (CYTHON_COMPILING_IN_LIMITED_API || CYTHON_COMPILING_IN_PYPY) && CYTHON_VECTORCALL + __pyx_vectorcallfunc func_vectorcall; +#endif +#if CYTHON_COMPILING_IN_LIMITED_API + PyObject *func_weakreflist; +#endif +#if PY_VERSION_HEX < 0x030C0000 || CYTHON_COMPILING_IN_LIMITED_API + PyObject *func_dict; +#endif + PyObject *func_name; + PyObject *func_qualname; + PyObject *func_doc; + PyObject *func_globals; + PyObject *func_code; + PyObject *func_closure; +#if CYTHON_COMPILING_IN_LIMITED_API + PyObject *func_classobj; +#endif + PyObject *defaults; + int flags; + PyObject *defaults_tuple; + PyObject *defaults_kwdict; + PyObject *(*defaults_getter)(PyObject *); + PyObject *func_annotations; +#if __PYX_LIMITED_VERSION_HEX < 0x030B0000 + PyObject *func_is_coroutine; +#endif +} __pyx_CyFunctionObject; +#undef __Pyx_CyOrPyCFunction_Check +#define __Pyx_CyFunction_Check(obj) __Pyx_TypeCheck(obj, __pyx_mstate_global->__pyx_CyFunctionType) +#define __Pyx_CyOrPyCFunction_Check(obj) __Pyx_TypeCheck2(obj, __pyx_mstate_global->__pyx_CyFunctionType, &PyCFunction_Type) +#define __Pyx_CyFunction_CheckExact(obj) Py_IS_TYPE(obj, __pyx_mstate_global->__pyx_CyFunctionType) +static CYTHON_INLINE int __Pyx__IsSameCyOrCFunction(PyObject *func, void (*cfunc)(void)); +#undef __Pyx_IsSameCFunction +#define __Pyx_IsSameCFunction(func, cfunc) __Pyx__IsSameCyOrCFunction(func, cfunc) +static CYTHON_INLINE void __Pyx__CyFunction_SetClassObj(__pyx_CyFunctionObject* f, PyObject* classobj); +static CYTHON_INLINE PyObject *__Pyx_CyFunction_InitDefaults(PyObject *func, + PyTypeObject *defaults_type); +static CYTHON_INLINE void __Pyx_CyFunction_SetDefaultsTuple(PyObject *m, + PyObject *tuple); +static CYTHON_INLINE void __Pyx_CyFunction_SetDefaultsKwDict(PyObject *m, + PyObject *dict); +static CYTHON_INLINE void __Pyx_CyFunction_SetAnnotationsDict(PyObject *m, + PyObject *dict); +static int __pyx_CyFunction_init(PyObject *module); +#if CYTHON_VECTORCALL +#if CYTHON_COMPILING_IN_LIMITED_API || CYTHON_COMPILING_IN_PYPY +#define __Pyx_CyFunction_func_vectorcall(f) ((f)->func_vectorcall) +#else +#define __Pyx_CyFunction_func_vectorcall(f) (((PyCFunctionObject*)f)->vectorcall) +#endif +#endif + +/* CallTypeTraverse.proto (used by CythonFunctionShared) */ +#if !CYTHON_USE_TYPE_SPECS +#define __Pyx_call_type_traverse(o, always_call, visit, arg) 0 +#else +static int __Pyx_call_type_traverse(PyObject *o, int always_call, visitproc visit, void *arg); +#endif + +/* PyMethodNew.proto (used by CythonFunctionShared) */ +static PyObject *__Pyx_PyMethod_New(PyObject *func, PyObject *self, PyObject *typ); + +/* PyVectorcallFastCallDict.proto (used by CythonFunctionShared) */ +#if CYTHON_VECTORCALL +static CYTHON_INLINE PyObject *__Pyx_PyVectorcall_FastCallDict(PyObject *func, __pyx_vectorcallfunc vc, PyObject *const *args, size_t nargs, PyObject *kw); +#endif + +/* CythonFunctionShared.proto (used by CythonFunction) */ +static PyObject *__Pyx_CyFunction_Init(PyObject *op_in, PyMethodDef *ml, + int flags, PyObject* qualname, + PyObject *closure, + PyObject *module, PyObject *globals, + PyObject* code); +#if CYTHON_VECTORCALL +static PyObject * __Pyx_CyFunction_Vectorcall_NOARGS(PyObject *func, PyObject *const *args, size_t nargsf, PyObject *kwnames); +static PyObject * __Pyx_CyFunction_Vectorcall_O(PyObject *func, PyObject *const *args, size_t nargsf, PyObject *kwnames); +static PyObject * __Pyx_CyFunction_Vectorcall_FASTCALL_KEYWORDS(PyObject *func, PyObject *const *args, size_t nargsf, PyObject *kwnames); +static PyObject * __Pyx_CyFunction_Vectorcall_FASTCALL_KEYWORDS_METHOD(PyObject *func, PyObject *const *args, size_t nargsf, PyObject *kwnames); +#endif + +/* CythonFunction.export */ +static PyObject *__Pyx_CyFunction_New(PyMethodDef *ml, + int flags, PyObject* qualname, + PyObject *closure, + PyObject *module, PyObject *globals, + PyObject* code); +static PyTypeObject *__Pyx_Get_CyFunction_Type(void); + +/* CLineInTraceback.proto (used by AddTraceback) */ +#if CYTHON_CLINE_IN_TRACEBACK && CYTHON_CLINE_IN_TRACEBACK_RUNTIME +static int __Pyx_CLineForTraceback(PyThreadState *tstate, int c_line); +#else +#define __Pyx_CLineForTraceback(tstate, c_line) (((CYTHON_CLINE_IN_TRACEBACK)) ? c_line : 0) +#endif + +/* CodeObjectCache.proto (used by AddTraceback) */ +#if CYTHON_COMPILING_IN_LIMITED_API +typedef PyObject __Pyx_CachedCodeObjectType; +#else +typedef PyCodeObject __Pyx_CachedCodeObjectType; +#endif +typedef struct { + __Pyx_CachedCodeObjectType* code_object; + int code_line; +} __Pyx_CodeObjectCacheEntry; +struct __Pyx_CodeObjectCache { + int count; + int max_count; + __Pyx_CodeObjectCacheEntry* entries; + #if CYTHON_COMPILING_IN_CPYTHON_FREETHREADING + __pyx_atomic_int_type accessor_count; + #endif +}; +static int __pyx_bisect_code_objects(__Pyx_CodeObjectCacheEntry* entries, int count, int code_line); +static __Pyx_CachedCodeObjectType *__pyx_find_code_object(int code_line); +static void __pyx_insert_code_object(int code_line, __Pyx_CachedCodeObjectType* code_object); + +/* AddTraceback.proto */ +static void __Pyx_AddTraceback(const char *funcname, int c_line, + int py_line, const char *filename); + +/* RealImag.proto */ +#if CYTHON_CCOMPLEX + #ifdef __cplusplus + #define __Pyx_CREAL(z) ((z).real()) + #define __Pyx_CIMAG(z) ((z).imag()) + #else + #define __Pyx_CREAL(z) (__real__(z)) + #define __Pyx_CIMAG(z) (__imag__(z)) + #endif +#else + #define __Pyx_CREAL(z) ((z).real) + #define __Pyx_CIMAG(z) ((z).imag) +#endif +#if defined(__cplusplus) && CYTHON_CCOMPLEX\ + && (defined(_WIN32) || defined(__clang__) || (defined(__GNUC__) && (__GNUC__ >= 5 || __GNUC__ == 4 && __GNUC_MINOR__ >= 4 )) || __cplusplus >= 201103) + #define __Pyx_SET_CREAL(z,x) ((z).real(x)) + #define __Pyx_SET_CIMAG(z,y) ((z).imag(y)) +#else + #define __Pyx_SET_CREAL(z,x) __Pyx_CREAL(z) = (x) + #define __Pyx_SET_CIMAG(z,y) __Pyx_CIMAG(z) = (y) +#endif + +/* Arithmetic.proto */ +#if CYTHON_CCOMPLEX && (1) && (!0 || __cplusplus) + #define __Pyx_c_eq_double(a, b) ((a)==(b)) + #define __Pyx_c_sum_double(a, b) ((a)+(b)) + #define __Pyx_c_diff_double(a, b) ((a)-(b)) + #define __Pyx_c_prod_double(a, b) ((a)*(b)) + #define __Pyx_c_quot_double(a, b) ((a)/(b)) + #define __Pyx_c_neg_double(a) (-(a)) + #ifdef __cplusplus + #define __Pyx_c_is_zero_double(z) ((z)==(double)0) + #define __Pyx_c_conj_double(z) (::std::conj(z)) + #if 1 + #define __Pyx_c_abs_double(z) (::std::abs(z)) + #define __Pyx_c_pow_double(a, b) (::std::pow(a, b)) + #endif + #else + #define __Pyx_c_is_zero_double(z) ((z)==0) + #define __Pyx_c_conj_double(z) (conj(z)) + #if 1 + #define __Pyx_c_abs_double(z) (cabs(z)) + #define __Pyx_c_pow_double(a, b) (cpow(a, b)) + #endif + #endif +#else + static CYTHON_INLINE int __Pyx_c_eq_double(__pyx_t_double_complex, __pyx_t_double_complex); + static CYTHON_INLINE __pyx_t_double_complex __Pyx_c_sum_double(__pyx_t_double_complex, __pyx_t_double_complex); + static CYTHON_INLINE __pyx_t_double_complex __Pyx_c_diff_double(__pyx_t_double_complex, __pyx_t_double_complex); + static CYTHON_INLINE __pyx_t_double_complex __Pyx_c_prod_double(__pyx_t_double_complex, __pyx_t_double_complex); + static CYTHON_INLINE __pyx_t_double_complex __Pyx_c_quot_double(__pyx_t_double_complex, __pyx_t_double_complex); + static CYTHON_INLINE __pyx_t_double_complex __Pyx_c_neg_double(__pyx_t_double_complex); + static CYTHON_INLINE int __Pyx_c_is_zero_double(__pyx_t_double_complex); + static CYTHON_INLINE __pyx_t_double_complex __Pyx_c_conj_double(__pyx_t_double_complex); + #if 1 + static CYTHON_INLINE double __Pyx_c_abs_double(__pyx_t_double_complex); + static CYTHON_INLINE __pyx_t_double_complex __Pyx_c_pow_double(__pyx_t_double_complex, __pyx_t_double_complex); + #endif +#endif + +/* FromPy.proto */ +static __pyx_t_double_complex __Pyx_PyComplex_As___pyx_t_double_complex(PyObject*); + +/* GCCDiagnostics.proto */ +#if !defined(__INTEL_COMPILER) && defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6)) +#define __Pyx_HAS_GCC_DIAGNOSTIC +#endif + +/* ToPy.proto */ +#define __pyx_PyComplex_FromComplex(z)\ + PyComplex_FromDoubles((double)__Pyx_CREAL(z),\ + (double)__Pyx_CIMAG(z)) + +/* CIntFromPy.proto */ +static CYTHON_INLINE int __Pyx_PyLong_As_int(PyObject *); + +/* PyObjectVectorcallKwds.proto (used by PyObjectVectorcallMethodKwds) */ +#if CYTHON_VECTORCALL +#define __Pyx_Object_VectorcallKwds PyObject_Vectorcall +CYTHON_UNUSED static int __Pyx_CheckVectorcallKwarg(PyObject *kwnames, Py_ssize_t i); +#else +#define __Pyx_Object_VectorcallKwds __Pyx_PyObject_FastCallDict +CYTHON_UNUSED static PyObject *__Pyx_MakeKwargDict(PyObject **keys, PyObject **values, Py_ssize_t n); +CYTHON_UNUSED static int __Pyx_CheckVectorcallKwarg(PyObject **kwnames, Py_ssize_t i); +#endif + +/* PyObjectVectorcallMethodKwds.proto (used by CIntToPy) */ +#if CYTHON_VECTORCALL +#define __Pyx_Object_VectorcallMethodKwds PyObject_VectorcallMethod +#else +static PyObject *__Pyx_Object_VectorcallMethodKwds(PyObject *name, PyObject *const *args, size_t nargsf, PyObject *kwnames); +#endif + +/* CIntToPy.proto */ +static CYTHON_INLINE PyObject* __Pyx_PyLong_From_long(long value); + +/* CIntToPy.proto */ +static CYTHON_INLINE PyObject* __Pyx_PyLong_From_int(int value); + +/* CIntFromPy.proto */ +static CYTHON_INLINE long __Pyx_PyLong_As_long(PyObject *); + +/* GetRuntimeVersion.proto */ +#if __PYX_LIMITED_VERSION_HEX < 0x030b0000 +static unsigned long __Pyx_cached_runtime_version = 0; +static void __Pyx_init_runtime_version(void); +#else +#define __Pyx_init_runtime_version() +#endif +static unsigned long __Pyx_get_runtime_version(void); + +/* SwapException.proto (used by CoroutineBase) */ +#if CYTHON_FAST_THREAD_STATE +#define __Pyx_ExceptionSwap(type, value, tb) __Pyx__ExceptionSwap(__pyx_tstate, type, value, tb) +static CYTHON_INLINE void __Pyx__ExceptionSwap(PyThreadState *tstate, PyObject **type, PyObject **value, PyObject **tb); +#else +static CYTHON_INLINE void __Pyx_ExceptionSwap(PyObject **type, PyObject **value, PyObject **tb); +#endif + +/* PyObjectCallMethod1.proto (used by CoroutineBase) */ +static CYTHON_INLINE PyObject* __Pyx_PyObject_CallMethod1(PyObject* obj, PyObject* method_name, PyObject* arg); + +/* PyObjectCallNoArg.proto (used by CoroutineBase) */ +static CYTHON_INLINE PyObject* __Pyx_PyObject_CallNoArg(PyObject *func); + +/* ReturnWithStopIteration.proto (used by CoroutineBase) */ +static CYTHON_INLINE void __Pyx_ReturnWithStopIteration(PyObject* value, int async, int iternext); + +/* CoroutineBase.proto (used by Generator) */ +struct __pyx_CoroutineObject; +typedef PyObject *(*__pyx_coroutine_body_t)(struct __pyx_CoroutineObject *, PyThreadState *, PyObject *); +#if CYTHON_USE_EXC_INFO_STACK +#define __Pyx_ExcInfoStruct _PyErr_StackItem +#else +typedef struct { + PyObject *exc_type; + PyObject *exc_value; + PyObject *exc_traceback; +} __Pyx_ExcInfoStruct; +#endif +typedef struct __pyx_CoroutineObject { + PyObject_HEAD + __pyx_coroutine_body_t body; + PyObject *closure; + __Pyx_ExcInfoStruct gi_exc_state; +#if PY_VERSION_HEX < 0x030C0000 || CYTHON_COMPILING_IN_LIMITED_API + PyObject *gi_weakreflist; +#endif + PyObject *classobj; + PyObject *yieldfrom; + __Pyx_pyiter_sendfunc yieldfrom_am_send; + PyObject *gi_name; + PyObject *gi_qualname; + PyObject *gi_modulename; + PyObject *gi_code; + PyObject *gi_frame; +#if CYTHON_USE_SYS_MONITORING && (CYTHON_PROFILE || CYTHON_TRACE) + PyMonitoringState __pyx_pymonitoring_state[__Pyx_MonitoringEventTypes_CyGen_count]; + uint64_t __pyx_pymonitoring_version; +#endif + int resume_label; + char is_running; +} __pyx_CoroutineObject; +static __pyx_CoroutineObject *__Pyx__Coroutine_New( + PyTypeObject *type, __pyx_coroutine_body_t body, PyObject *code, PyObject *closure, + PyObject *name, PyObject *qualname, PyObject *module_name); +static __pyx_CoroutineObject *__Pyx__Coroutine_NewInit( + __pyx_CoroutineObject *gen, __pyx_coroutine_body_t body, PyObject *code, PyObject *closure, + PyObject *name, PyObject *qualname, PyObject *module_name); +static CYTHON_INLINE void __Pyx_Coroutine_ExceptionClear(__Pyx_ExcInfoStruct *self); +static int __Pyx_Coroutine_clear(PyObject *self); +static __Pyx_PySendResult __Pyx_Coroutine_AmSend(PyObject *self, PyObject *value, PyObject **retval); +static PyObject *__Pyx_Coroutine_Send(PyObject *self, PyObject *value); +static __Pyx_PySendResult __Pyx_Coroutine_Close(PyObject *self, PyObject **retval); +static PyObject *__Pyx_Coroutine_Throw(PyObject *gen, +#if CYTHON_COMPILING_IN_LIMITED_API && __PYX_LIMITED_VERSION_HEX < 0x030A0000 + PyObject *args +#else + PyObject *const *args, Py_ssize_t nargs +#endif + ); +#if CYTHON_USE_EXC_INFO_STACK +#define __Pyx_Coroutine_SwapException(self) +#define __Pyx_Coroutine_ResetAndClearException(self) __Pyx_Coroutine_ExceptionClear(&(self)->gi_exc_state) +#else +#define __Pyx_Coroutine_SwapException(self) {\ + __Pyx_ExceptionSwap(&(self)->gi_exc_state.exc_type, &(self)->gi_exc_state.exc_value, &(self)->gi_exc_state.exc_traceback);\ + __Pyx_Coroutine_ResetFrameBackpointer(&(self)->gi_exc_state);\ + } +#define __Pyx_Coroutine_ResetAndClearException(self) {\ + __Pyx_ExceptionReset((self)->gi_exc_state.exc_type, (self)->gi_exc_state.exc_value, (self)->gi_exc_state.exc_traceback);\ + (self)->gi_exc_state.exc_type = (self)->gi_exc_state.exc_value = (self)->gi_exc_state.exc_traceback = NULL;\ + } +#endif +#if CYTHON_FAST_THREAD_STATE +#define __Pyx_PyGen_FetchStopIterationValue(pvalue)\ + __Pyx_PyGen__FetchStopIterationValue(__pyx_tstate, pvalue) +#else +#define __Pyx_PyGen_FetchStopIterationValue(pvalue)\ + __Pyx_PyGen__FetchStopIterationValue(__Pyx_PyThreadState_Current, pvalue) +#endif +static int __Pyx_PyGen__FetchStopIterationValue(PyThreadState *tstate, PyObject **pvalue); +static CYTHON_INLINE void __Pyx_Coroutine_ResetFrameBackpointer(__Pyx_ExcInfoStruct *exc_state); +static char __Pyx_Coroutine_test_and_set_is_running(__pyx_CoroutineObject *gen); +static void __Pyx_Coroutine_unset_is_running(__pyx_CoroutineObject *gen); +static char __Pyx_Coroutine_get_is_running(__pyx_CoroutineObject *gen); +static PyObject *__Pyx_Coroutine_get_is_running_getter(PyObject *gen, void *closure); +#if __PYX_HAS_PY_AM_SEND == 2 +static void __Pyx_SetBackportTypeAmSend(PyTypeObject *type, __Pyx_PyAsyncMethodsStruct *static_amsend_methods, __Pyx_pyiter_sendfunc am_send); +#endif +static PyObject *__Pyx_Coroutine_fail_reduce_ex(PyObject *self, PyObject *arg); + +/* Generator.proto */ +#define __Pyx_Generator_USED +#define __Pyx_Generator_CheckExact(obj) Py_IS_TYPE(obj, __pyx_mstate_global->__pyx_GeneratorType) +#define __Pyx_Generator_New(body, code, closure, name, qualname, module_name)\ + __Pyx__Coroutine_New(__pyx_mstate_global->__pyx_GeneratorType, body, code, closure, name, qualname, module_name) +static PyObject *__Pyx_Generator_Next(PyObject *self); +static int __pyx_Generator_init(PyObject *module); +static CYTHON_INLINE PyObject *__Pyx_Generator_GetInlinedResult(PyObject *self); + +/* CheckBinaryVersion.proto */ +static int __Pyx_check_binary_version(unsigned long ct_version, unsigned long rt_version, int allow_newer); + +/* DecompressString.proto */ +static PyObject *__Pyx_DecompressString(const char *s, Py_ssize_t length, int algo); + +/* DecompressString_LZSS.proto */ +static PyObject *__Pyx_DecompressString_LZSS(const char *s, size_t compressed_length, size_t uncompressed_length); + +/* MultiPhaseInitModuleState.proto */ +#if CYTHON_PEP489_MULTI_PHASE_INIT && CYTHON_USE_MODULE_STATE +#include +static PyObject *__Pyx_State_FindModule(void*); +static int __Pyx_State_AddModule(PyObject* module, void*); +static int __Pyx_State_RemoveModule(void*); +#elif CYTHON_USE_MODULE_STATE +#define __Pyx_State_FindModule PyState_FindModule +#define __Pyx_State_AddModule PyState_AddModule +#define __Pyx_State_RemoveModule PyState_RemoveModule +#endif + +/* #### Code section: module_declarations ### */ +/* CythonABIVersion.proto */ +#if CYTHON_COMPILING_IN_LIMITED_API + #if CYTHON_VECTORCALL + #define __PYX_VECTORCALL_ABI_SUFFIX "_vectorcall" + #else + #define __PYX_VECTORCALL_ABI_SUFFIX + #endif + #define __PYX_LIMITED_ABI_SUFFIX "limited" __PYX_VECTORCALL_ABI_SUFFIX __PYX_AM_SEND_ABI_SUFFIX +#else + #define __PYX_LIMITED_ABI_SUFFIX +#endif +#if __PYX_HAS_PY_AM_SEND == 1 + #define __PYX_AM_SEND_ABI_SUFFIX +#elif __PYX_HAS_PY_AM_SEND == 2 + #define __PYX_AM_SEND_ABI_SUFFIX "amsendbackport" +#else + #define __PYX_AM_SEND_ABI_SUFFIX "noamsend" +#endif +#ifndef __PYX_MONITORING_ABI_SUFFIX + #define __PYX_MONITORING_ABI_SUFFIX +#endif +#if CYTHON_USE_TP_FINALIZE + #define __PYX_TP_FINALIZE_ABI_SUFFIX +#else + #define __PYX_TP_FINALIZE_ABI_SUFFIX "nofinalize" +#endif +#if CYTHON_USE_FREELISTS || !defined(__Pyx_AsyncGen_USED) + #define __PYX_FREELISTS_ABI_SUFFIX +#else + #define __PYX_FREELISTS_ABI_SUFFIX "nofreelists" +#endif +#if CYTHON_OPAQUE_OBJECTS && CYTHON_COMPILING_IN_LIMITED_API + #define __PYX_OPAQUE_OBJECTS_ABI_SUFFIX "opaque" +#else + #define __PYX_OPAQUE_OBJECTS_ABI_SUFFIX +#endif +#define CYTHON_ABI __PYX_ABI_VERSION __PYX_LIMITED_ABI_SUFFIX __PYX_MONITORING_ABI_SUFFIX __PYX_TP_FINALIZE_ABI_SUFFIX __PYX_FREELISTS_ABI_SUFFIX __PYX_AM_SEND_ABI_SUFFIX __PYX_OPAQUE_OBJECTS_ABI_SUFFIX +#define __PYX_ABI_MODULE_NAME "_cython_" CYTHON_ABI +#define __PYX_TYPE_MODULE_PREFIX __PYX_ABI_MODULE_NAME "." + + +/* Module declarations from "cython" */ + +/* Module declarations from "fontTools.cu2qu.cu2qu" */ +static CYTHON_INLINE double __pyx_f_9fontTools_5cu2qu_5cu2qu_dot(__pyx_t_double_complex, __pyx_t_double_complex); /*proto*/ +static PyObject *__pyx_f_9fontTools_5cu2qu_5cu2qu__complex_div_by_real(__pyx_t_double_complex, double); /*proto*/ +static CYTHON_INLINE PyObject *__pyx_f_9fontTools_5cu2qu_5cu2qu_calc_cubic_points(__pyx_t_double_complex, __pyx_t_double_complex, __pyx_t_double_complex, __pyx_t_double_complex); /*proto*/ +static CYTHON_INLINE PyObject *__pyx_f_9fontTools_5cu2qu_5cu2qu_calc_cubic_parameters(__pyx_t_double_complex, __pyx_t_double_complex, __pyx_t_double_complex, __pyx_t_double_complex); /*proto*/ +static CYTHON_INLINE PyObject *__pyx_f_9fontTools_5cu2qu_5cu2qu_split_cubic_into_n_iter(__pyx_t_double_complex, __pyx_t_double_complex, __pyx_t_double_complex, __pyx_t_double_complex, PyObject *); /*proto*/ +static CYTHON_INLINE PyObject *__pyx_f_9fontTools_5cu2qu_5cu2qu_split_cubic_into_two(__pyx_t_double_complex, __pyx_t_double_complex, __pyx_t_double_complex, __pyx_t_double_complex); /*proto*/ +static CYTHON_INLINE PyObject *__pyx_f_9fontTools_5cu2qu_5cu2qu_split_cubic_into_three(__pyx_t_double_complex, __pyx_t_double_complex, __pyx_t_double_complex, __pyx_t_double_complex); /*proto*/ +static CYTHON_INLINE __pyx_t_double_complex __pyx_f_9fontTools_5cu2qu_5cu2qu_cubic_approx_control(double, __pyx_t_double_complex, __pyx_t_double_complex, __pyx_t_double_complex, __pyx_t_double_complex); /*proto*/ +static CYTHON_INLINE __pyx_t_double_complex __pyx_f_9fontTools_5cu2qu_5cu2qu_calc_intersect(__pyx_t_double_complex, __pyx_t_double_complex, __pyx_t_double_complex, __pyx_t_double_complex); /*proto*/ +static int __pyx_f_9fontTools_5cu2qu_5cu2qu_cubic_farthest_fit_inside(__pyx_t_double_complex, __pyx_t_double_complex, __pyx_t_double_complex, __pyx_t_double_complex, double); /*proto*/ +static CYTHON_INLINE PyObject *__pyx_f_9fontTools_5cu2qu_5cu2qu_cubic_approx_quadratic(PyObject *, double); /*proto*/ +static PyObject *__pyx_f_9fontTools_5cu2qu_5cu2qu_cubic_approx_spline(PyObject *, int, double, int); /*proto*/ +/* #### Code section: typeinfo ### */ +/* #### Code section: before_global_var ### */ +#define __Pyx_MODULE_NAME "fontTools.cu2qu.cu2qu" +extern int __pyx_module_is_main_fontTools__cu2qu__cu2qu; +int __pyx_module_is_main_fontTools__cu2qu__cu2qu = 0; + +/* Implementation of "fontTools.cu2qu.cu2qu" */ +/* #### Code section: global_var ### */ +/* #### Code section: string_decls ### */ +/* #### Code section: decls ### */ +static PyObject *__pyx_pf_9fontTools_5cu2qu_5cu2qu__split_cubic_into_n_gen(CYTHON_UNUSED PyObject *__pyx_self, __pyx_t_double_complex __pyx_v_p0, __pyx_t_double_complex __pyx_v_p1, __pyx_t_double_complex __pyx_v_p2, __pyx_t_double_complex __pyx_v_p3, int __pyx_v_n); /* proto */ +static PyObject *__pyx_pf_9fontTools_5cu2qu_5cu2qu_3curve_to_quadratic(CYTHON_UNUSED PyObject *__pyx_self, PyObject *__pyx_v_curve, double __pyx_v_max_err, int __pyx_v_all_quadratic); /* proto */ +static PyObject *__pyx_pf_9fontTools_5cu2qu_5cu2qu_5curves_to_quadratic(CYTHON_UNUSED PyObject *__pyx_self, PyObject *__pyx_v_curves, PyObject *__pyx_v_max_errors, int __pyx_v_all_quadratic); /* proto */ +static PyObject *__pyx_tp_new__initialisation_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen(PyObject *o, +#if CYTHON_VECTORCALL_TPNEW + PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames +#else + PyObject *a, PyObject *k +#endif +); /*proto*/ +static PyObject *__pyx_tp_new_vectorcall_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen(PyTypeObject *t, +#if CYTHON_VECTORCALL_TPNEW + PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames +#else + PyObject *a, PyObject *k +#endif +); /*proto*/ +#if CYTHON_VECTORCALL_TPNEW +static PyObject *__pyx_tp_new_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen(PyTypeObject *t, PyObject *a, PyObject *k); /*proto*/ +#endif +#if !CYTHON_VECTORCALL_TPNEW +#define __pyx_tp_new_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen __pyx_tp_new_vectorcall_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen +#endif +#if CYTHON_VECTORCALL_TPNEW +static PyObject *__pyx_tp_vectorcall_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen(PyObject *t, PyObject *const *args, size_t nargsf, PyObject *kwnames); /*proto*/ +#endif +/* #### Code section: late_includes ### */ +/* #### Code section: module_state ### */ +/* SmallCodeConfig */ +#ifndef CYTHON_SMALL_CODE +#if defined(__clang__) + #define CYTHON_SMALL_CODE +#elif defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 3)) + #define CYTHON_SMALL_CODE __attribute__((cold)) +#else + #define CYTHON_SMALL_CODE +#endif +#endif + +#ifdef __cplusplus +namespace { + #endif + typedef struct { + PyObject *__pyx_d; + PyObject *__pyx_b; + PyObject *__pyx_cython_runtime; + PyObject *__pyx_empty_tuple; + PyObject *__pyx_empty_bytes; + PyObject *__pyx_empty_unicode; + PyObject *__pyx_type_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen; + PyTypeObject *__pyx_ptype_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen; + __Pyx_CachedCFunction __pyx_umethod_PyDict_Type_items; + __Pyx_CachedCFunction __pyx_umethod_PyDict_Type_pop; + __Pyx_CachedCFunction __pyx_umethod_PyDict_Type_values; + PyObject *__pyx_codeobj_tab[3]; + PyObject *__pyx_string_tab[81]; + PyObject *__pyx_number_tab[6]; +/* #### Code section: module_state_contents ### */ +/* PyFrozenDict.module_state_decls */ +#if CYTHON_COMPILING_IN_LIMITED_API +PyObject *__Pyx_PyFrozenDictType; +#endif + +/* IterNextPlain.module_state_decls */ +#if CYTHON_COMPILING_IN_LIMITED_API && __PYX_LIMITED_VERSION_HEX < 0x030A0000 +PyObject *__Pyx_GetBuiltinNext_LimitedAPI_cache; +#endif + + +#if CYTHON_USE_FREELISTS +struct __pyx_obj_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen *__pyx_freelist_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen[8]; +int __pyx_freecount_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen; +#endif +/* CommonTypesMetaclass.module_state_decls */ +PyTypeObject *__pyx_CommonTypesMetaclassType; + +/* CachedMethodType.module_state_decls */ +#if CYTHON_COMPILING_IN_LIMITED_API +PyObject *__Pyx_CachedMethodType; +#endif + +/* CythonFunctionPerModule.module_state_decls */ +PyTypeObject *__pyx_CyFunctionType; + +/* CodeObjectCache.module_state_decls */ +struct __Pyx_CodeObjectCache __pyx_code_cache; + +/* Generator.module_state_decls */ +PyTypeObject *__pyx_GeneratorType; + +/* #### Code section: module_state_end ### */ +} __pyx_mstatetype; +#ifdef __cplusplus +} /* anonymous namespace */ +#endif + +#if CYTHON_USE_MODULE_STATE +#ifdef __cplusplus +namespace { +extern struct PyModuleDef __pyx_moduledef; +} /* anonymous namespace */ +#else +static struct PyModuleDef __pyx_moduledef; +#endif + +#define __pyx_mstate_global (__Pyx_PyModule_GetState(__Pyx_State_FindModule(&__pyx_moduledef))) + +#define __pyx_m (__Pyx_State_FindModule(&__pyx_moduledef)) +#else +static __pyx_mstatetype __pyx_mstate_global_static = +#ifdef __cplusplus + {}; +#else + {0}; +#endif +static __pyx_mstatetype * const __pyx_mstate_global = &__pyx_mstate_global_static; +#endif +/* #### Code section: constant_name_defines ### */ +#define __pyx_kp_u__2 __pyx_string_tab[0] +#define __pyx_kp_u_ __pyx_string_tab[1] +#define __pyx_kp_u_Lib_fontTools_cu2qu_cu2qu_py __pyx_string_tab[2] +#define __pyx_kp_u_Return_quadratic_Bezier_splines __pyx_string_tab[3] +#define __pyx_kp_u_curves_to_quadratic_line_504 __pyx_string_tab[4] +#define __pyx_kp_u_disable __pyx_string_tab[5] +#define __pyx_kp_u_enable __pyx_string_tab[6] +#define __pyx_kp_u_fontTools_cu2qu_errors __pyx_string_tab[7] +#define __pyx_kp_u_gc __pyx_string_tab[8] +#define __pyx_kp_u_isenabled __pyx_string_tab[9] +#define __pyx_kp_u_max_errors_must_match_the_number __pyx_string_tab[10] +#define __pyx_n_u_ApproxNotFoundError __pyx_string_tab[11] +#define __pyx_n_u_COMPILED __pyx_string_tab[12] +#define __pyx_n_u_Cu2QuError __pyx_string_tab[13] +#define __pyx_n_u_Error __pyx_string_tab[14] +#define __pyx_n_u_MAX_N __pyx_string_tab[15] +#define __pyx_n_u_NAN __pyx_string_tab[16] +#define __pyx_n_u_NaN __pyx_string_tab[17] +#define __pyx_n_u_Pyx_PyDict_NextRef __pyx_string_tab[18] +#define __pyx_n_u_all __pyx_string_tab[19] +#define __pyx_n_u_annotate __pyx_string_tab[20] +#define __pyx_n_u_func __pyx_string_tab[21] +#define __pyx_n_u_main __pyx_string_tab[22] +#define __pyx_n_u_module __pyx_string_tab[23] +#define __pyx_n_u_name __pyx_string_tab[24] +#define __pyx_n_u_qualname __pyx_string_tab[25] +#define __pyx_n_u_test __pyx_string_tab[26] +#define __pyx_n_u_is_coroutine __pyx_string_tab[27] +#define __pyx_n_u_split_cubic_into_n_gen __pyx_string_tab[28] +#define __pyx_n_u_a __pyx_string_tab[29] +#define __pyx_n_u_a1 __pyx_string_tab[30] +#define __pyx_n_u_all_quadratic __pyx_string_tab[31] +#define __pyx_n_u_asyncio_coroutines __pyx_string_tab[32] +#define __pyx_n_u_b __pyx_string_tab[33] +#define __pyx_n_u_b1 __pyx_string_tab[34] +#define __pyx_n_u_c __pyx_string_tab[35] +#define __pyx_n_u_c1 __pyx_string_tab[36] +#define __pyx_n_u_cline_in_traceback __pyx_string_tab[37] +#define __pyx_n_u_close __pyx_string_tab[38] +#define __pyx_n_u_curve __pyx_string_tab[39] +#define __pyx_n_u_curve_to_quadratic __pyx_string_tab[40] +#define __pyx_n_u_curves __pyx_string_tab[41] +#define __pyx_n_u_curves_to_quadratic __pyx_string_tab[42] +#define __pyx_n_u_d __pyx_string_tab[43] +#define __pyx_n_u_d1 __pyx_string_tab[44] +#define __pyx_n_u_delta_2 __pyx_string_tab[45] +#define __pyx_n_u_delta_3 __pyx_string_tab[46] +#define __pyx_n_u_dt __pyx_string_tab[47] +#define __pyx_n_u_errors __pyx_string_tab[48] +#define __pyx_n_u_fontTools_cu2qu_cu2qu __pyx_string_tab[49] +#define __pyx_n_u_i __pyx_string_tab[50] +#define __pyx_n_u_imag __pyx_string_tab[51] +#define __pyx_n_u_isnan __pyx_string_tab[52] +#define __pyx_n_u_items __pyx_string_tab[53] +#define __pyx_n_u_l __pyx_string_tab[54] +#define __pyx_n_u_last_i __pyx_string_tab[55] +#define __pyx_n_u_math __pyx_string_tab[56] +#define __pyx_n_u_max_err __pyx_string_tab[57] +#define __pyx_n_u_max_errors __pyx_string_tab[58] +#define __pyx_n_u_n __pyx_string_tab[59] +#define __pyx_n_u_next __pyx_string_tab[60] +#define __pyx_n_u_p __pyx_string_tab[61] +#define __pyx_n_u_p0 __pyx_string_tab[62] +#define __pyx_n_u_p1 __pyx_string_tab[63] +#define __pyx_n_u_p2 __pyx_string_tab[64] +#define __pyx_n_u_p3 __pyx_string_tab[65] +#define __pyx_n_u_pop __pyx_string_tab[66] +#define __pyx_n_u_real __pyx_string_tab[67] +#define __pyx_n_u_s __pyx_string_tab[68] +#define __pyx_n_u_send __pyx_string_tab[69] +#define __pyx_n_u_setdefault __pyx_string_tab[70] +#define __pyx_n_u_spline __pyx_string_tab[71] +#define __pyx_n_u_splines __pyx_string_tab[72] +#define __pyx_n_u_t1 __pyx_string_tab[73] +#define __pyx_n_u_t1_2 __pyx_string_tab[74] +#define __pyx_n_u_throw __pyx_string_tab[75] +#define __pyx_n_u_value __pyx_string_tab[76] +#define __pyx_n_u_values __pyx_string_tab[77] +#define __pyx_kp_b_iso88591__3 __pyx_string_tab[78] +#define __pyx_kp_b_iso88591_L_Qawb_4uG4y_s_s_Qa_j_t1_q_1A_a __pyx_string_tab[79] +#define __pyx_kp_b_iso88591_AWBc_U_U_3fBa_AWCy_7_2QgQgT_a_Q __pyx_string_tab[80] +#define __pyx_int_1 __pyx_number_tab[0] +#define __pyx_int_2 __pyx_number_tab[1] +#define __pyx_int_3 __pyx_number_tab[2] +#define __pyx_int_4 __pyx_number_tab[3] +#define __pyx_int_6 __pyx_number_tab[4] +#define __pyx_int_100 __pyx_number_tab[5] +/* #### Code section: module_state_clear ### */ +#if CYTHON_USE_MODULE_STATE +static CYTHON_SMALL_CODE int __pyx_m_clear(PyObject *m) { + __pyx_mstatetype *clear_module_state = __Pyx_PyModule_GetState(m); + if (!clear_module_state) return 0; + Py_CLEAR(clear_module_state->__pyx_d); + Py_CLEAR(clear_module_state->__pyx_b); + Py_CLEAR(clear_module_state->__pyx_cython_runtime); + Py_CLEAR(clear_module_state->__pyx_empty_tuple); + Py_CLEAR(clear_module_state->__pyx_empty_bytes); + Py_CLEAR(clear_module_state->__pyx_empty_unicode); + #if CYTHON_PEP489_MULTI_PHASE_INIT + __Pyx_State_RemoveModule(NULL); + #endif + Py_CLEAR(clear_module_state->__pyx_ptype_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen); + Py_CLEAR(clear_module_state->__pyx_type_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen); + Py_CLEAR(clear_module_state->__pyx_umethod_PyDict_Type_items.method); + Py_CLEAR(clear_module_state->__pyx_umethod_PyDict_Type_pop.method); + Py_CLEAR(clear_module_state->__pyx_umethod_PyDict_Type_values.method); + for (int i=0; i<3; ++i) { Py_CLEAR(clear_module_state->__pyx_codeobj_tab[i]); } + for (int i=0; i<81; ++i) { Py_CLEAR(clear_module_state->__pyx_string_tab[i]); } + for (int i=0; i<6; ++i) { Py_CLEAR(clear_module_state->__pyx_number_tab[i]); } +/* #### Code section: module_state_clear_contents ### */ +/* CommonTypesMetaclass.module_state_clear */ +Py_CLEAR(clear_module_state->__pyx_CommonTypesMetaclassType); + +/* CythonFunctionPerModule.module_state_clear */ +Py_CLEAR(clear_module_state->__pyx_CyFunctionType); + +/* Generator.module_state_clear */ +Py_CLEAR(clear_module_state->__pyx_GeneratorType); + +/* #### Code section: module_state_clear_end ### */ +return 0; +} +#endif +/* #### Code section: module_state_traverse ### */ +#if CYTHON_USE_MODULE_STATE +static CYTHON_SMALL_CODE int __pyx_m_traverse(PyObject *m, visitproc visit, void *arg) { + __pyx_mstatetype *traverse_module_state = __Pyx_PyModule_GetState(m); + if (!traverse_module_state) return 0; + Py_VISIT(traverse_module_state->__pyx_d); + Py_VISIT(traverse_module_state->__pyx_b); + Py_VISIT(traverse_module_state->__pyx_cython_runtime); + __Pyx_VISIT_CONST(traverse_module_state->__pyx_empty_tuple); + __Pyx_VISIT_CONST(traverse_module_state->__pyx_empty_bytes); + __Pyx_VISIT_CONST(traverse_module_state->__pyx_empty_unicode); + Py_VISIT(traverse_module_state->__pyx_ptype_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen); + Py_VISIT(traverse_module_state->__pyx_type_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen); + Py_VISIT(traverse_module_state->__pyx_umethod_PyDict_Type_items.method); + Py_VISIT(traverse_module_state->__pyx_umethod_PyDict_Type_pop.method); + Py_VISIT(traverse_module_state->__pyx_umethod_PyDict_Type_values.method); + for (int i=0; i<3; ++i) { __Pyx_VISIT_CONST(traverse_module_state->__pyx_codeobj_tab[i]); } + for (int i=0; i<81; ++i) { __Pyx_VISIT_CONST(traverse_module_state->__pyx_string_tab[i]); } + for (int i=0; i<6; ++i) { __Pyx_VISIT_CONST(traverse_module_state->__pyx_number_tab[i]); } +/* #### Code section: module_state_traverse_contents ### */ +/* CommonTypesMetaclass.module_state_traverse */ +Py_VISIT(traverse_module_state->__pyx_CommonTypesMetaclassType); + +/* CythonFunctionPerModule.module_state_traverse */ +Py_VISIT(traverse_module_state->__pyx_CyFunctionType); + +/* Generator.module_state_traverse */ +Py_VISIT(traverse_module_state->__pyx_GeneratorType); + +/* #### Code section: module_state_traverse_end ### */ +return 0; +} +#endif +/* #### Code section: module_code ### */ + +/* "fontTools/cu2qu/cu2qu.py":36 + * + * + * @cython.cfunc # <<<<<<<<<<<<<< + * @cython.inline + * @cython.returns(cython.double) +*/ + +static CYTHON_INLINE double __pyx_f_9fontTools_5cu2qu_5cu2qu_dot(__pyx_t_double_complex __pyx_v_v1, __pyx_t_double_complex __pyx_v_v2) { + double __pyx_v_result; + double __pyx_r; + double __pyx_t_1; + int __pyx_t_2; + + /* "fontTools/cu2qu/cu2qu.py":50 + * double: Dot product. + * """ + * result = (v1 * v2.conjugate()).real # <<<<<<<<<<<<<< + * # When vectors are perpendicular (i.e. dot product is 0), the above expression may + * # yield slightly different results when running in pure Python vs C/Cython, +*/ + __pyx_t_1 = __Pyx_CREAL(__Pyx_c_prod_double(__pyx_v_v1, __Pyx_c_conj_double(__pyx_v_v2))); + + __pyx_v_result = __pyx_t_1; + + /* "fontTools/cu2qu/cu2qu.py":57 + * # implementation. Because we are using the result in a denominator and catching + * # ZeroDivisionError (see `calc_intersect`), it's best to normalize the result here. + * if abs(result) < 1e-15: # <<<<<<<<<<<<<< + * result = 0.0 + * return result +*/ + __pyx_t_1 = fabs(__pyx_v_result); + __pyx_t_2 = (__pyx_t_1 < 1e-15); + + + if (__pyx_t_2) { + + + /* "fontTools/cu2qu/cu2qu.py":58 + * # ZeroDivisionError (see `calc_intersect`), it's best to normalize the result here. + * if abs(result) < 1e-15: + * result = 0.0 # <<<<<<<<<<<<<< + * return result + * +*/ + __pyx_v_result = 0.0; + + /* "fontTools/cu2qu/cu2qu.py":57 + * # implementation. Because we are using the result in a denominator and catching + * # ZeroDivisionError (see `calc_intersect`), it's best to normalize the result here. + * if abs(result) < 1e-15: # <<<<<<<<<<<<<< + * result = 0.0 + * return result +*/ + } + + /* "fontTools/cu2qu/cu2qu.py":59 + * if abs(result) < 1e-15: + * result = 0.0 + * return result # <<<<<<<<<<<<<< + * + * +*/ + { + + __pyx_r = __pyx_v_result; + } + goto __pyx_L0; + + /* "fontTools/cu2qu/cu2qu.py":36 + * + * + * @cython.cfunc # <<<<<<<<<<<<<< + * @cython.inline + * @cython.returns(cython.double) +*/ + + /* function exit code */ + __pyx_L0:; + + + return __pyx_r; +} + +/* "fontTools/cu2qu/cu2qu.py":62 + * + * + * @cython.cfunc # <<<<<<<<<<<<<< + * @cython.locals(z=cython.complex, den=cython.double) + * @cython.locals(zr=cython.double, zi=cython.double) +*/ + +static PyObject *__pyx_f_9fontTools_5cu2qu_5cu2qu__complex_div_by_real(__pyx_t_double_complex __pyx_v_z, double __pyx_v_den) { + double __pyx_v_zr; + double __pyx_v_zi; + PyObject *__pyx_r = NULL; + __Pyx_RefNannyDeclarations + double __pyx_t_1; + PyObject *__pyx_t_2 = NULL; + PyObject *__pyx_t_3 = NULL; + PyObject *__pyx_t_4 = NULL; + PyObject *__pyx_t_5 = NULL; + size_t __pyx_t_6; + int __pyx_lineno = 0; + const char *__pyx_filename = NULL; + int __pyx_clineno = 0; + __Pyx_RefNannySetupContext("_complex_div_by_real", 0); + + /* "fontTools/cu2qu/cu2qu.py":74 + * https://github.com/fonttools/fonttools/issues/3928 + * """ + * zr = z.real # <<<<<<<<<<<<<< + * zi = z.imag + * return complex(zr / den, zi / den) +*/ + __pyx_t_1 = __Pyx_CREAL(__pyx_v_z); + + __pyx_v_zr = __pyx_t_1; + + /* "fontTools/cu2qu/cu2qu.py":75 + * """ + * zr = z.real + * zi = z.imag # <<<<<<<<<<<<<< + * return complex(zr / den, zi / den) + * +*/ + __pyx_t_1 = __Pyx_CIMAG(__pyx_v_z); + + __pyx_v_zi = __pyx_t_1; + + /* "fontTools/cu2qu/cu2qu.py":76 + * zr = z.real + * zi = z.imag + * return complex(zr / den, zi / den) # <<<<<<<<<<<<<< + * + * +*/ + __pyx_t_3 = NULL; + if (unlikely(__pyx_v_den == 0)) { + PyErr_SetString(PyExc_ZeroDivisionError, "float division"); + __PYX_ERR(0, 76, __pyx_L1_error) + } + __pyx_t_4 = PyFloat_FromDouble((__pyx_v_zr / __pyx_v_den)); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 76, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_4); + if (unlikely(__pyx_v_den == 0)) { + PyErr_SetString(PyExc_ZeroDivisionError, "float division"); + __PYX_ERR(0, 76, __pyx_L1_error) + } + __pyx_t_5 = PyFloat_FromDouble((__pyx_v_zi / __pyx_v_den)); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 76, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_5); + __pyx_t_6 = 1; + { + PyObject *__pyx_callargs[3] = {__pyx_t_3, __pyx_t_4, __pyx_t_5}; + __pyx_t_2 = __Pyx_PyObject_FastCall((PyObject*)(&PyComplex_Type), __pyx_callargs+__pyx_t_6, (3-__pyx_t_6) | (__pyx_t_6*__Pyx_PY_VECTORCALL_ARGUMENTS_OFFSET)); + __Pyx_XDECREF(__pyx_t_3); __pyx_t_3 = 0; + __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0; + __Pyx_DECREF(__pyx_t_5); __pyx_t_5 = 0; + if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 76, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + } + { + PyObject *__pyx_temp; + { + __pyx_temp = __pyx_r; + __pyx_r = __pyx_t_2; + } + __Pyx_XDECREF(__pyx_temp); + } + __pyx_t_2 = 0; + goto __pyx_L0; + + /* "fontTools/cu2qu/cu2qu.py":62 + * + * + * @cython.cfunc # <<<<<<<<<<<<<< + * @cython.locals(z=cython.complex, den=cython.double) + * @cython.locals(zr=cython.double, zi=cython.double) +*/ + + /* function exit code */ + __pyx_L1_error:; + __Pyx_XDECREF(__pyx_t_2); + __Pyx_XDECREF(__pyx_t_3); + __Pyx_XDECREF(__pyx_t_4); + __Pyx_XDECREF(__pyx_t_5); + __Pyx_AddTraceback("fontTools.cu2qu.cu2qu._complex_div_by_real", __pyx_clineno, __pyx_lineno, __pyx_filename); + __pyx_r = 0; + __pyx_L0:; + + + __Pyx_XGIVEREF(__pyx_r); + __Pyx_RefNannyFinishContext(); + return __pyx_r; +} + +/* "fontTools/cu2qu/cu2qu.py":79 + * + * + * @cython.cfunc # <<<<<<<<<<<<<< + * @cython.inline + * @cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex) +*/ + +static CYTHON_INLINE PyObject *__pyx_f_9fontTools_5cu2qu_5cu2qu_calc_cubic_points(__pyx_t_double_complex __pyx_v_a, __pyx_t_double_complex __pyx_v_b, __pyx_t_double_complex __pyx_v_c, __pyx_t_double_complex __pyx_v_d) { + __pyx_t_double_complex __pyx_v__1; + __pyx_t_double_complex __pyx_v__2; + __pyx_t_double_complex __pyx_v__3; + __pyx_t_double_complex __pyx_v__4; + PyObject *__pyx_r = NULL; + __Pyx_RefNannyDeclarations + PyObject *__pyx_t_1 = NULL; + PyObject *__pyx_t_2 = NULL; + PyObject *__pyx_t_3 = NULL; + __pyx_t_double_complex __pyx_t_4; + PyObject *__pyx_t_5 = NULL; + PyObject *__pyx_t_6 = NULL; + int __pyx_lineno = 0; + const char *__pyx_filename = NULL; + int __pyx_clineno = 0; + __Pyx_RefNannySetupContext("calc_cubic_points", 0); + + /* "fontTools/cu2qu/cu2qu.py":86 + * ) + * def calc_cubic_points(a, b, c, d): + * _1 = d # <<<<<<<<<<<<<< + * _2 = _complex_div_by_real(c, 3.0) + d + * _3 = _complex_div_by_real(b + c, 3.0) + _2 +*/ + __pyx_v__1 = __pyx_v_d; + + /* "fontTools/cu2qu/cu2qu.py":87 + * def calc_cubic_points(a, b, c, d): + * _1 = d + * _2 = _complex_div_by_real(c, 3.0) + d # <<<<<<<<<<<<<< + * _3 = _complex_div_by_real(b + c, 3.0) + _2 + * _4 = a + d + c + b +*/ + __pyx_t_1 = __pyx_f_9fontTools_5cu2qu_5cu2qu__complex_div_by_real(__pyx_v_c, 3.0); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 87, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_1); + __pyx_t_2 = __pyx_PyComplex_FromComplex(__pyx_v_d); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 87, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + __pyx_t_3 = PyNumber_Add(__pyx_t_1, __pyx_t_2); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 87, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_3); + __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0; + __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0; + __pyx_t_4 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_3); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 87, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0; + __pyx_v__2 = __pyx_t_4; + + /* "fontTools/cu2qu/cu2qu.py":88 + * _1 = d + * _2 = _complex_div_by_real(c, 3.0) + d + * _3 = _complex_div_by_real(b + c, 3.0) + _2 # <<<<<<<<<<<<<< + * _4 = a + d + c + b + * return _1, _2, _3, _4 +*/ + __pyx_t_3 = __pyx_f_9fontTools_5cu2qu_5cu2qu__complex_div_by_real(__Pyx_c_sum_double(__pyx_v_b, __pyx_v_c), 3.0); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 88, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_3); + __pyx_t_2 = __pyx_PyComplex_FromComplex(__pyx_v__2); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 88, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + __pyx_t_1 = PyNumber_Add(__pyx_t_3, __pyx_t_2); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 88, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_1); + __Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0; + __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0; + __pyx_t_4 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_1); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 88, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0; + __pyx_v__3 = __pyx_t_4; + + /* "fontTools/cu2qu/cu2qu.py":89 + * _2 = _complex_div_by_real(c, 3.0) + d + * _3 = _complex_div_by_real(b + c, 3.0) + _2 + * _4 = a + d + c + b # <<<<<<<<<<<<<< + * return _1, _2, _3, _4 + * +*/ + __pyx_v__4 = __Pyx_c_sum_double(__Pyx_c_sum_double(__Pyx_c_sum_double(__pyx_v_a, __pyx_v_d), __pyx_v_c), __pyx_v_b); + + /* "fontTools/cu2qu/cu2qu.py":90 + * _3 = _complex_div_by_real(b + c, 3.0) + _2 + * _4 = a + d + c + b + * return _1, _2, _3, _4 # <<<<<<<<<<<<<< + * + * +*/ + __pyx_t_1 = __pyx_PyComplex_FromComplex(__pyx_v__1); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 90, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_1); + __pyx_t_2 = __pyx_PyComplex_FromComplex(__pyx_v__2); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 90, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + __pyx_t_3 = __pyx_PyComplex_FromComplex(__pyx_v__3); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 90, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_3); + __pyx_t_5 = __pyx_PyComplex_FromComplex(__pyx_v__4); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 90, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_5); + __pyx_t_6 = PyTuple_New(4); if (unlikely(!__pyx_t_6)) __PYX_ERR(0, 90, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_6); + __Pyx_GIVEREF(__pyx_t_1); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_6, 0, __pyx_t_1) != (0)) __PYX_ERR(0, 90, __pyx_L1_error); + __Pyx_GIVEREF(__pyx_t_2); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_6, 1, __pyx_t_2) != (0)) __PYX_ERR(0, 90, __pyx_L1_error); + __Pyx_GIVEREF(__pyx_t_3); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_6, 2, __pyx_t_3) != (0)) __PYX_ERR(0, 90, __pyx_L1_error); + __Pyx_GIVEREF(__pyx_t_5); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_6, 3, __pyx_t_5) != (0)) __PYX_ERR(0, 90, __pyx_L1_error); + __pyx_t_1 = 0; + __pyx_t_2 = 0; + __pyx_t_3 = 0; + __pyx_t_5 = 0; + { + PyObject *__pyx_temp; + { + __pyx_temp = __pyx_r; + __pyx_r = __pyx_t_6; + } + __Pyx_XDECREF(__pyx_temp); + } + __pyx_t_6 = 0; + goto __pyx_L0; + + /* "fontTools/cu2qu/cu2qu.py":79 + * + * + * @cython.cfunc # <<<<<<<<<<<<<< + * @cython.inline + * @cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex) +*/ + + /* function exit code */ + __pyx_L1_error:; + __Pyx_XDECREF(__pyx_t_1); + __Pyx_XDECREF(__pyx_t_2); + __Pyx_XDECREF(__pyx_t_3); + __Pyx_XDECREF(__pyx_t_5); + __Pyx_XDECREF(__pyx_t_6); + __Pyx_AddTraceback("fontTools.cu2qu.cu2qu.calc_cubic_points", __pyx_clineno, __pyx_lineno, __pyx_filename); + __pyx_r = 0; + __pyx_L0:; + + + + + __Pyx_XGIVEREF(__pyx_r); + __Pyx_RefNannyFinishContext(); + return __pyx_r; +} + +/* "fontTools/cu2qu/cu2qu.py":93 + * + * + * @cython.cfunc # <<<<<<<<<<<<<< + * @cython.inline + * @cython.locals( +*/ + +static CYTHON_INLINE PyObject *__pyx_f_9fontTools_5cu2qu_5cu2qu_calc_cubic_parameters(__pyx_t_double_complex __pyx_v_p0, __pyx_t_double_complex __pyx_v_p1, __pyx_t_double_complex __pyx_v_p2, __pyx_t_double_complex __pyx_v_p3) { + __pyx_t_double_complex __pyx_v_a; + __pyx_t_double_complex __pyx_v_b; + __pyx_t_double_complex __pyx_v_c; + __pyx_t_double_complex __pyx_v_d; + PyObject *__pyx_r = NULL; + __Pyx_RefNannyDeclarations + PyObject *__pyx_t_1 = NULL; + PyObject *__pyx_t_2 = NULL; + PyObject *__pyx_t_3 = NULL; + PyObject *__pyx_t_4 = NULL; + PyObject *__pyx_t_5 = NULL; + int __pyx_lineno = 0; + const char *__pyx_filename = NULL; + int __pyx_clineno = 0; + __Pyx_RefNannySetupContext("calc_cubic_parameters", 0); + + /* "fontTools/cu2qu/cu2qu.py":100 + * @cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex) + * def calc_cubic_parameters(p0, p1, p2, p3): + * c = (p1 - p0) * 3.0 # <<<<<<<<<<<<<< + * b = (p2 - p1) * 3.0 - c + * d = p0 +*/ + __pyx_v_c = __Pyx_c_prod_double(__Pyx_c_diff_double(__pyx_v_p1, __pyx_v_p0), __pyx_t_double_complex_from_parts(3.0, 0)); + + /* "fontTools/cu2qu/cu2qu.py":101 + * def calc_cubic_parameters(p0, p1, p2, p3): + * c = (p1 - p0) * 3.0 + * b = (p2 - p1) * 3.0 - c # <<<<<<<<<<<<<< + * d = p0 + * a = p3 - d - c - b +*/ + __pyx_v_b = __Pyx_c_diff_double(__Pyx_c_prod_double(__Pyx_c_diff_double(__pyx_v_p2, __pyx_v_p1), __pyx_t_double_complex_from_parts(3.0, 0)), __pyx_v_c); + + /* "fontTools/cu2qu/cu2qu.py":102 + * c = (p1 - p0) * 3.0 + * b = (p2 - p1) * 3.0 - c + * d = p0 # <<<<<<<<<<<<<< + * a = p3 - d - c - b + * return a, b, c, d +*/ + __pyx_v_d = __pyx_v_p0; + + /* "fontTools/cu2qu/cu2qu.py":103 + * b = (p2 - p1) * 3.0 - c + * d = p0 + * a = p3 - d - c - b # <<<<<<<<<<<<<< + * return a, b, c, d + * +*/ + __pyx_v_a = __Pyx_c_diff_double(__Pyx_c_diff_double(__Pyx_c_diff_double(__pyx_v_p3, __pyx_v_d), __pyx_v_c), __pyx_v_b); + + /* "fontTools/cu2qu/cu2qu.py":104 + * d = p0 + * a = p3 - d - c - b + * return a, b, c, d # <<<<<<<<<<<<<< + * + * +*/ + __pyx_t_1 = __pyx_PyComplex_FromComplex(__pyx_v_a); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 104, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_1); + __pyx_t_2 = __pyx_PyComplex_FromComplex(__pyx_v_b); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 104, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + __pyx_t_3 = __pyx_PyComplex_FromComplex(__pyx_v_c); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 104, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_3); + __pyx_t_4 = __pyx_PyComplex_FromComplex(__pyx_v_d); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 104, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_4); + __pyx_t_5 = PyTuple_New(4); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 104, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_5); + __Pyx_GIVEREF(__pyx_t_1); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_5, 0, __pyx_t_1) != (0)) __PYX_ERR(0, 104, __pyx_L1_error); + __Pyx_GIVEREF(__pyx_t_2); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_5, 1, __pyx_t_2) != (0)) __PYX_ERR(0, 104, __pyx_L1_error); + __Pyx_GIVEREF(__pyx_t_3); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_5, 2, __pyx_t_3) != (0)) __PYX_ERR(0, 104, __pyx_L1_error); + __Pyx_GIVEREF(__pyx_t_4); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_5, 3, __pyx_t_4) != (0)) __PYX_ERR(0, 104, __pyx_L1_error); + __pyx_t_1 = 0; + __pyx_t_2 = 0; + __pyx_t_3 = 0; + __pyx_t_4 = 0; + { + PyObject *__pyx_temp; + { + __pyx_temp = __pyx_r; + __pyx_r = __pyx_t_5; + } + __Pyx_XDECREF(__pyx_temp); + } + __pyx_t_5 = 0; + goto __pyx_L0; + + /* "fontTools/cu2qu/cu2qu.py":93 + * + * + * @cython.cfunc # <<<<<<<<<<<<<< + * @cython.inline + * @cython.locals( +*/ + + /* function exit code */ + __pyx_L1_error:; + __Pyx_XDECREF(__pyx_t_1); + __Pyx_XDECREF(__pyx_t_2); + __Pyx_XDECREF(__pyx_t_3); + __Pyx_XDECREF(__pyx_t_4); + __Pyx_XDECREF(__pyx_t_5); + __Pyx_AddTraceback("fontTools.cu2qu.cu2qu.calc_cubic_parameters", __pyx_clineno, __pyx_lineno, __pyx_filename); + __pyx_r = 0; + __pyx_L0:; + + + + + __Pyx_XGIVEREF(__pyx_r); + __Pyx_RefNannyFinishContext(); + return __pyx_r; +} + +/* "fontTools/cu2qu/cu2qu.py":107 + * + * + * @cython.cfunc # <<<<<<<<<<<<<< + * @cython.inline + * @cython.locals( +*/ + +static CYTHON_INLINE PyObject *__pyx_f_9fontTools_5cu2qu_5cu2qu_split_cubic_into_n_iter(__pyx_t_double_complex __pyx_v_p0, __pyx_t_double_complex __pyx_v_p1, __pyx_t_double_complex __pyx_v_p2, __pyx_t_double_complex __pyx_v_p3, PyObject *__pyx_v_n) { + PyObject *__pyx_v_a = NULL; + PyObject *__pyx_v_b = NULL; + PyObject *__pyx_r = NULL; + __Pyx_RefNannyDeclarations + int __pyx_t_1; + PyObject *__pyx_t_2 = NULL; + PyObject *__pyx_t_3 = NULL; + PyObject *__pyx_t_4 = NULL; + PyObject *__pyx_t_5 = NULL; + PyObject *(*__pyx_t_6)(PyObject *); + __pyx_t_double_complex __pyx_t_7; + __pyx_t_double_complex __pyx_t_8; + __pyx_t_double_complex __pyx_t_9; + __pyx_t_double_complex __pyx_t_10; + PyObject *__pyx_t_11 = NULL; + PyObject *__pyx_t_12 = NULL; + PyObject *__pyx_t_13 = NULL; + size_t __pyx_t_14; + int __pyx_lineno = 0; + const char *__pyx_filename = NULL; + int __pyx_clineno = 0; + __Pyx_RefNannySetupContext("split_cubic_into_n_iter", 0); + + /* "fontTools/cu2qu/cu2qu.py":129 + * """ + * # Hand-coded special-cases + * if n == 2: # <<<<<<<<<<<<<< + * return iter(split_cubic_into_two(p0, p1, p2, p3)) + * if n == 3: +*/ + __pyx_t_1 = (__Pyx_PyLong_BoolEqObjC(__pyx_v_n, __pyx_mstate_global->__pyx_int_2, 2, 0)); if (unlikely((__pyx_t_1 < 0))) __PYX_ERR(0, 129, __pyx_L1_error) + if (__pyx_t_1) { + + + /* "fontTools/cu2qu/cu2qu.py":130 + * # Hand-coded special-cases + * if n == 2: + * return iter(split_cubic_into_two(p0, p1, p2, p3)) # <<<<<<<<<<<<<< + * if n == 3: + * return iter(split_cubic_into_three(p0, p1, p2, p3)) +*/ + __pyx_t_2 = __pyx_f_9fontTools_5cu2qu_5cu2qu_split_cubic_into_two(__pyx_v_p0, __pyx_v_p1, __pyx_v_p2, __pyx_v_p3); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 130, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + __pyx_t_3 = PyObject_GetIter(__pyx_t_2); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 130, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_3); + __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0; + { + PyObject *__pyx_temp; + { + __pyx_temp = __pyx_r; + __pyx_r = __pyx_t_3; + } + __Pyx_XDECREF(__pyx_temp); + } + __pyx_t_3 = 0; + goto __pyx_L0; + + /* "fontTools/cu2qu/cu2qu.py":129 + * """ + * # Hand-coded special-cases + * if n == 2: # <<<<<<<<<<<<<< + * return iter(split_cubic_into_two(p0, p1, p2, p3)) + * if n == 3: +*/ + } + + /* "fontTools/cu2qu/cu2qu.py":131 + * if n == 2: + * return iter(split_cubic_into_two(p0, p1, p2, p3)) + * if n == 3: # <<<<<<<<<<<<<< + * return iter(split_cubic_into_three(p0, p1, p2, p3)) + * if n == 4: +*/ + __pyx_t_1 = (__Pyx_PyLong_BoolEqObjC(__pyx_v_n, __pyx_mstate_global->__pyx_int_3, 3, 0)); if (unlikely((__pyx_t_1 < 0))) __PYX_ERR(0, 131, __pyx_L1_error) + if (__pyx_t_1) { + + + /* "fontTools/cu2qu/cu2qu.py":132 + * return iter(split_cubic_into_two(p0, p1, p2, p3)) + * if n == 3: + * return iter(split_cubic_into_three(p0, p1, p2, p3)) # <<<<<<<<<<<<<< + * if n == 4: + * a, b = split_cubic_into_two(p0, p1, p2, p3) +*/ + __pyx_t_3 = __pyx_f_9fontTools_5cu2qu_5cu2qu_split_cubic_into_three(__pyx_v_p0, __pyx_v_p1, __pyx_v_p2, __pyx_v_p3); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 132, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_3); + __pyx_t_2 = PyObject_GetIter(__pyx_t_3); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 132, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + __Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0; + { + PyObject *__pyx_temp; + { + __pyx_temp = __pyx_r; + __pyx_r = __pyx_t_2; + } + __Pyx_XDECREF(__pyx_temp); + } + __pyx_t_2 = 0; + goto __pyx_L0; + + /* "fontTools/cu2qu/cu2qu.py":131 + * if n == 2: + * return iter(split_cubic_into_two(p0, p1, p2, p3)) + * if n == 3: # <<<<<<<<<<<<<< + * return iter(split_cubic_into_three(p0, p1, p2, p3)) + * if n == 4: +*/ + } + + /* "fontTools/cu2qu/cu2qu.py":133 + * if n == 3: + * return iter(split_cubic_into_three(p0, p1, p2, p3)) + * if n == 4: # <<<<<<<<<<<<<< + * a, b = split_cubic_into_two(p0, p1, p2, p3) + * return iter( +*/ + __pyx_t_1 = (__Pyx_PyLong_BoolEqObjC(__pyx_v_n, __pyx_mstate_global->__pyx_int_4, 4, 0)); if (unlikely((__pyx_t_1 < 0))) __PYX_ERR(0, 133, __pyx_L1_error) + if (__pyx_t_1) { + + + /* "fontTools/cu2qu/cu2qu.py":134 + * return iter(split_cubic_into_three(p0, p1, p2, p3)) + * if n == 4: + * a, b = split_cubic_into_two(p0, p1, p2, p3) # <<<<<<<<<<<<<< + * return iter( + * split_cubic_into_two(a[0], a[1], a[2], a[3]) +*/ + __pyx_t_2 = __pyx_f_9fontTools_5cu2qu_5cu2qu_split_cubic_into_two(__pyx_v_p0, __pyx_v_p1, __pyx_v_p2, __pyx_v_p3); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 134, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + if ((likely(PyTuple_CheckExact(__pyx_t_2))) || (PyList_CheckExact(__pyx_t_2))) { + PyObject* sequence = __pyx_t_2; + Py_ssize_t size = __Pyx_PySequence_SIZE(sequence); + if (unlikely(size != 2)) { + if (size > 2) __Pyx_RaiseTooManyValuesError(2); + else if (size >= 0) __Pyx_RaiseNeedMoreValuesError(size); + __PYX_ERR(0, 134, __pyx_L1_error) + } + #if CYTHON_ASSUME_SAFE_MACROS && !CYTHON_AVOID_BORROWED_REFS + if (likely(PyTuple_CheckExact(sequence))) { + __pyx_t_3 = PyTuple_GET_ITEM(sequence, 0); + __Pyx_INCREF(__pyx_t_3); + __pyx_t_4 = PyTuple_GET_ITEM(sequence, 1); + __Pyx_INCREF(__pyx_t_4); + } else { + __pyx_t_3 = __Pyx_PyList_GET_ITEM_REF(sequence, 0, __Pyx_ReferenceSharing_SharedReference); + if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 134, __pyx_L1_error) + __Pyx_XGOTREF(__pyx_t_3); + __pyx_t_4 = __Pyx_PyList_GET_ITEM_REF(sequence, 1, __Pyx_ReferenceSharing_SharedReference); + if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 134, __pyx_L1_error) + __Pyx_XGOTREF(__pyx_t_4); + } + #else + __pyx_t_3 = __Pyx_PySequence_ITEM(sequence, 0); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 134, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_3); + __pyx_t_4 = __Pyx_PySequence_ITEM(sequence, 1); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 134, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_4); + #endif + __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0; + } else { + Py_ssize_t index = -1; + __pyx_t_5 = PyObject_GetIter(__pyx_t_2); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 134, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_5); + __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0; + __pyx_t_6 = (CYTHON_COMPILING_IN_LIMITED_API) ? PyIter_Next : __Pyx_PyObject_GetIterNextFunc(__pyx_t_5); + index = 0; __pyx_t_3 = __pyx_t_6(__pyx_t_5); if (unlikely(!__pyx_t_3)) goto __pyx_L6_unpacking_failed; + __Pyx_GOTREF(__pyx_t_3); + index = 1; __pyx_t_4 = __pyx_t_6(__pyx_t_5); if (unlikely(!__pyx_t_4)) goto __pyx_L6_unpacking_failed; + __Pyx_GOTREF(__pyx_t_4); + if (__Pyx_IternextUnpackEndCheck(__pyx_t_6(__pyx_t_5), 2) < (0)) __PYX_ERR(0, 134, __pyx_L1_error) + __pyx_t_6 = NULL; + __Pyx_DECREF(__pyx_t_5); __pyx_t_5 = 0; + goto __pyx_L7_unpacking_done; + __pyx_L6_unpacking_failed:; + __Pyx_DECREF(__pyx_t_5); __pyx_t_5 = 0; + __pyx_t_6 = NULL; + if (__Pyx_IterFinish() == 0) __Pyx_RaiseNeedMoreValuesError(index); + __PYX_ERR(0, 134, __pyx_L1_error) + __pyx_L7_unpacking_done:; + } + __pyx_v_a = __pyx_t_3; + __pyx_t_3 = 0; + __pyx_v_b = __pyx_t_4; + __pyx_t_4 = 0; + + /* "fontTools/cu2qu/cu2qu.py":136 + * a, b = split_cubic_into_two(p0, p1, p2, p3) + * return iter( + * split_cubic_into_two(a[0], a[1], a[2], a[3]) # <<<<<<<<<<<<<< + * + split_cubic_into_two(b[0], b[1], b[2], b[3]) + * ) +*/ + __pyx_t_2 = __Pyx_GetItemInt(__pyx_v_a, 0, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_OwnStrongReference); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 136, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + __pyx_t_7 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_2); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 136, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0; + __pyx_t_2 = __Pyx_GetItemInt(__pyx_v_a, 1, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_OwnStrongReference); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 136, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + __pyx_t_8 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_2); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 136, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0; + __pyx_t_2 = __Pyx_GetItemInt(__pyx_v_a, 2, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_OwnStrongReference); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 136, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + __pyx_t_9 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_2); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 136, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0; + __pyx_t_2 = __Pyx_GetItemInt(__pyx_v_a, 3, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_OwnStrongReference); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 136, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + __pyx_t_10 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_2); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 136, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0; + __pyx_t_2 = __pyx_f_9fontTools_5cu2qu_5cu2qu_split_cubic_into_two(__pyx_t_7, __pyx_t_8, __pyx_t_9, __pyx_t_10); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 136, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + + + + + + /* "fontTools/cu2qu/cu2qu.py":137 + * return iter( + * split_cubic_into_two(a[0], a[1], a[2], a[3]) + * + split_cubic_into_two(b[0], b[1], b[2], b[3]) # <<<<<<<<<<<<<< + * ) + * if n == 6: +*/ + __pyx_t_4 = __Pyx_GetItemInt(__pyx_v_b, 0, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_OwnStrongReference); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 137, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_4); + __pyx_t_10 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_4); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 137, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0; + __pyx_t_4 = __Pyx_GetItemInt(__pyx_v_b, 1, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_OwnStrongReference); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 137, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_4); + __pyx_t_9 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_4); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 137, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0; + __pyx_t_4 = __Pyx_GetItemInt(__pyx_v_b, 2, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_OwnStrongReference); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 137, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_4); + __pyx_t_8 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_4); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 137, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0; + __pyx_t_4 = __Pyx_GetItemInt(__pyx_v_b, 3, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_OwnStrongReference); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 137, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_4); + __pyx_t_7 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_4); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 137, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0; + __pyx_t_4 = __pyx_f_9fontTools_5cu2qu_5cu2qu_split_cubic_into_two(__pyx_t_10, __pyx_t_9, __pyx_t_8, __pyx_t_7); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 137, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_4); + + + + + __pyx_t_3 = __Pyx_PyNumber_Add_object_object(__pyx_t_2, __pyx_t_4); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 137, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_3); + __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0; + __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0; + + /* "fontTools/cu2qu/cu2qu.py":135 + * if n == 4: + * a, b = split_cubic_into_two(p0, p1, p2, p3) + * return iter( # <<<<<<<<<<<<<< + * split_cubic_into_two(a[0], a[1], a[2], a[3]) + * + split_cubic_into_two(b[0], b[1], b[2], b[3]) +*/ + __pyx_t_4 = PyObject_GetIter(__pyx_t_3); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 135, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_4); + __Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0; + { + PyObject *__pyx_temp; + { + __pyx_temp = __pyx_r; + __pyx_r = __pyx_t_4; + } + __Pyx_XDECREF(__pyx_temp); + } + __pyx_t_4 = 0; + goto __pyx_L0; + + /* "fontTools/cu2qu/cu2qu.py":133 + * if n == 3: + * return iter(split_cubic_into_three(p0, p1, p2, p3)) + * if n == 4: # <<<<<<<<<<<<<< + * a, b = split_cubic_into_two(p0, p1, p2, p3) + * return iter( +*/ + } + + /* "fontTools/cu2qu/cu2qu.py":139 + * + split_cubic_into_two(b[0], b[1], b[2], b[3]) + * ) + * if n == 6: # <<<<<<<<<<<<<< + * a, b = split_cubic_into_two(p0, p1, p2, p3) + * return iter( +*/ + __pyx_t_1 = (__Pyx_PyLong_BoolEqObjC(__pyx_v_n, __pyx_mstate_global->__pyx_int_6, 6, 0)); if (unlikely((__pyx_t_1 < 0))) __PYX_ERR(0, 139, __pyx_L1_error) + if (__pyx_t_1) { + + + /* "fontTools/cu2qu/cu2qu.py":140 + * ) + * if n == 6: + * a, b = split_cubic_into_two(p0, p1, p2, p3) # <<<<<<<<<<<<<< + * return iter( + * split_cubic_into_three(a[0], a[1], a[2], a[3]) +*/ + __pyx_t_4 = __pyx_f_9fontTools_5cu2qu_5cu2qu_split_cubic_into_two(__pyx_v_p0, __pyx_v_p1, __pyx_v_p2, __pyx_v_p3); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 140, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_4); + if ((likely(PyTuple_CheckExact(__pyx_t_4))) || (PyList_CheckExact(__pyx_t_4))) { + PyObject* sequence = __pyx_t_4; + Py_ssize_t size = __Pyx_PySequence_SIZE(sequence); + if (unlikely(size != 2)) { + if (size > 2) __Pyx_RaiseTooManyValuesError(2); + else if (size >= 0) __Pyx_RaiseNeedMoreValuesError(size); + __PYX_ERR(0, 140, __pyx_L1_error) + } + #if CYTHON_ASSUME_SAFE_MACROS && !CYTHON_AVOID_BORROWED_REFS + if (likely(PyTuple_CheckExact(sequence))) { + __pyx_t_3 = PyTuple_GET_ITEM(sequence, 0); + __Pyx_INCREF(__pyx_t_3); + __pyx_t_2 = PyTuple_GET_ITEM(sequence, 1); + __Pyx_INCREF(__pyx_t_2); + } else { + __pyx_t_3 = __Pyx_PyList_GET_ITEM_REF(sequence, 0, __Pyx_ReferenceSharing_SharedReference); + if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 140, __pyx_L1_error) + __Pyx_XGOTREF(__pyx_t_3); + __pyx_t_2 = __Pyx_PyList_GET_ITEM_REF(sequence, 1, __Pyx_ReferenceSharing_SharedReference); + if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 140, __pyx_L1_error) + __Pyx_XGOTREF(__pyx_t_2); + } + #else + __pyx_t_3 = __Pyx_PySequence_ITEM(sequence, 0); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 140, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_3); + __pyx_t_2 = __Pyx_PySequence_ITEM(sequence, 1); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 140, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + #endif + __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0; + } else { + Py_ssize_t index = -1; + __pyx_t_5 = PyObject_GetIter(__pyx_t_4); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 140, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_5); + __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0; + __pyx_t_6 = (CYTHON_COMPILING_IN_LIMITED_API) ? PyIter_Next : __Pyx_PyObject_GetIterNextFunc(__pyx_t_5); + index = 0; __pyx_t_3 = __pyx_t_6(__pyx_t_5); if (unlikely(!__pyx_t_3)) goto __pyx_L9_unpacking_failed; + __Pyx_GOTREF(__pyx_t_3); + index = 1; __pyx_t_2 = __pyx_t_6(__pyx_t_5); if (unlikely(!__pyx_t_2)) goto __pyx_L9_unpacking_failed; + __Pyx_GOTREF(__pyx_t_2); + if (__Pyx_IternextUnpackEndCheck(__pyx_t_6(__pyx_t_5), 2) < (0)) __PYX_ERR(0, 140, __pyx_L1_error) + __pyx_t_6 = NULL; + __Pyx_DECREF(__pyx_t_5); __pyx_t_5 = 0; + goto __pyx_L10_unpacking_done; + __pyx_L9_unpacking_failed:; + __Pyx_DECREF(__pyx_t_5); __pyx_t_5 = 0; + __pyx_t_6 = NULL; + if (__Pyx_IterFinish() == 0) __Pyx_RaiseNeedMoreValuesError(index); + __PYX_ERR(0, 140, __pyx_L1_error) + __pyx_L10_unpacking_done:; + } + __pyx_v_a = __pyx_t_3; + __pyx_t_3 = 0; + __pyx_v_b = __pyx_t_2; + __pyx_t_2 = 0; + + /* "fontTools/cu2qu/cu2qu.py":142 + * a, b = split_cubic_into_two(p0, p1, p2, p3) + * return iter( + * split_cubic_into_three(a[0], a[1], a[2], a[3]) # <<<<<<<<<<<<<< + * + split_cubic_into_three(b[0], b[1], b[2], b[3]) + * ) +*/ + __pyx_t_4 = __Pyx_GetItemInt(__pyx_v_a, 0, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_OwnStrongReference); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 142, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_4); + __pyx_t_7 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_4); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 142, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0; + __pyx_t_4 = __Pyx_GetItemInt(__pyx_v_a, 1, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_OwnStrongReference); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 142, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_4); + __pyx_t_8 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_4); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 142, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0; + __pyx_t_4 = __Pyx_GetItemInt(__pyx_v_a, 2, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_OwnStrongReference); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 142, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_4); + __pyx_t_9 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_4); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 142, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0; + __pyx_t_4 = __Pyx_GetItemInt(__pyx_v_a, 3, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_OwnStrongReference); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 142, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_4); + __pyx_t_10 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_4); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 142, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0; + __pyx_t_4 = __pyx_f_9fontTools_5cu2qu_5cu2qu_split_cubic_into_three(__pyx_t_7, __pyx_t_8, __pyx_t_9, __pyx_t_10); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 142, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_4); + + + + + + /* "fontTools/cu2qu/cu2qu.py":143 + * return iter( + * split_cubic_into_three(a[0], a[1], a[2], a[3]) + * + split_cubic_into_three(b[0], b[1], b[2], b[3]) # <<<<<<<<<<<<<< + * ) + * +*/ + __pyx_t_2 = __Pyx_GetItemInt(__pyx_v_b, 0, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_OwnStrongReference); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 143, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + __pyx_t_10 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_2); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 143, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0; + __pyx_t_2 = __Pyx_GetItemInt(__pyx_v_b, 1, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_OwnStrongReference); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 143, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + __pyx_t_9 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_2); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 143, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0; + __pyx_t_2 = __Pyx_GetItemInt(__pyx_v_b, 2, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_OwnStrongReference); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 143, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + __pyx_t_8 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_2); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 143, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0; + __pyx_t_2 = __Pyx_GetItemInt(__pyx_v_b, 3, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_OwnStrongReference); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 143, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + __pyx_t_7 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_2); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 143, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0; + __pyx_t_2 = __pyx_f_9fontTools_5cu2qu_5cu2qu_split_cubic_into_three(__pyx_t_10, __pyx_t_9, __pyx_t_8, __pyx_t_7); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 143, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + + + + + __pyx_t_3 = __Pyx_PyNumber_Add_object_object(__pyx_t_4, __pyx_t_2); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 143, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_3); + __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0; + __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0; + + /* "fontTools/cu2qu/cu2qu.py":141 + * if n == 6: + * a, b = split_cubic_into_two(p0, p1, p2, p3) + * return iter( # <<<<<<<<<<<<<< + * split_cubic_into_three(a[0], a[1], a[2], a[3]) + * + split_cubic_into_three(b[0], b[1], b[2], b[3]) +*/ + __pyx_t_2 = PyObject_GetIter(__pyx_t_3); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 141, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + __Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0; + { + PyObject *__pyx_temp; + { + __pyx_temp = __pyx_r; + __pyx_r = __pyx_t_2; + } + __Pyx_XDECREF(__pyx_temp); + } + __pyx_t_2 = 0; + goto __pyx_L0; + + /* "fontTools/cu2qu/cu2qu.py":139 + * + split_cubic_into_two(b[0], b[1], b[2], b[3]) + * ) + * if n == 6: # <<<<<<<<<<<<<< + * a, b = split_cubic_into_two(p0, p1, p2, p3) + * return iter( +*/ + } + + /* "fontTools/cu2qu/cu2qu.py":146 + * ) + * + * return _split_cubic_into_n_gen(p0, p1, p2, p3, n) # <<<<<<<<<<<<<< + * + * +*/ + __pyx_t_3 = NULL; + __Pyx_GetModuleGlobalName(__pyx_t_4, __pyx_mstate_global->__pyx_n_u_split_cubic_into_n_gen); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 146, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_4); + __pyx_t_5 = __pyx_PyComplex_FromComplex(__pyx_v_p0); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 146, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_5); + __pyx_t_11 = __pyx_PyComplex_FromComplex(__pyx_v_p1); if (unlikely(!__pyx_t_11)) __PYX_ERR(0, 146, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_11); + __pyx_t_12 = __pyx_PyComplex_FromComplex(__pyx_v_p2); if (unlikely(!__pyx_t_12)) __PYX_ERR(0, 146, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_12); + __pyx_t_13 = __pyx_PyComplex_FromComplex(__pyx_v_p3); if (unlikely(!__pyx_t_13)) __PYX_ERR(0, 146, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_13); + __pyx_t_14 = 1; + #if CYTHON_UNPACK_METHODS + if (unlikely(PyMethod_Check(__pyx_t_4))) { + __pyx_t_3 = PyMethod_GET_SELF(__pyx_t_4); + assert(__pyx_t_3); + PyObject* __pyx__function = PyMethod_GET_FUNCTION(__pyx_t_4); + __Pyx_INCREF(__pyx_t_3); + __Pyx_INCREF(__pyx__function); + __Pyx_DECREF_SET(__pyx_t_4, __pyx__function); + __pyx_t_14 = 0; + } + #endif + { + PyObject *__pyx_callargs[6] = {__pyx_t_3, __pyx_t_5, __pyx_t_11, __pyx_t_12, __pyx_t_13, __pyx_v_n}; + __pyx_t_2 = __Pyx_PyObject_FastCall((PyObject*)__pyx_t_4, __pyx_callargs+__pyx_t_14, (6-__pyx_t_14) | (__pyx_t_14*__Pyx_PY_VECTORCALL_ARGUMENTS_OFFSET)); + __Pyx_XDECREF(__pyx_t_3); __pyx_t_3 = 0; + __Pyx_DECREF(__pyx_t_5); __pyx_t_5 = 0; + __Pyx_DECREF(__pyx_t_11); __pyx_t_11 = 0; + __Pyx_DECREF(__pyx_t_12); __pyx_t_12 = 0; + __Pyx_DECREF(__pyx_t_13); __pyx_t_13 = 0; + __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0; + if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 146, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + } + { + PyObject *__pyx_temp; + { + __pyx_temp = __pyx_r; + __pyx_r = __pyx_t_2; + } + __Pyx_XDECREF(__pyx_temp); + } + __pyx_t_2 = 0; + goto __pyx_L0; + + /* "fontTools/cu2qu/cu2qu.py":107 + * + * + * @cython.cfunc # <<<<<<<<<<<<<< + * @cython.inline + * @cython.locals( +*/ + + /* function exit code */ + __pyx_L1_error:; + __Pyx_XDECREF(__pyx_t_2); + __Pyx_XDECREF(__pyx_t_3); + __Pyx_XDECREF(__pyx_t_4); + __Pyx_XDECREF(__pyx_t_5); + __Pyx_XDECREF(__pyx_t_11); + __Pyx_XDECREF(__pyx_t_12); + __Pyx_XDECREF(__pyx_t_13); + __Pyx_AddTraceback("fontTools.cu2qu.cu2qu.split_cubic_into_n_iter", __pyx_clineno, __pyx_lineno, __pyx_filename); + __pyx_r = 0; + __pyx_L0:; + __Pyx_XDECREF(__pyx_v_a); + __Pyx_XDECREF(__pyx_v_b); + __Pyx_XGIVEREF(__pyx_r); + __Pyx_RefNannyFinishContext(); + return __pyx_r; +} +static PyObject *__pyx_gb_9fontTools_5cu2qu_5cu2qu_2generator(__pyx_CoroutineObject *__pyx_generator, CYTHON_UNUSED PyThreadState *__pyx_tstate, PyObject *__pyx_sent_value); /* proto */ + +/* "fontTools/cu2qu/cu2qu.py":149 + * + * + * @cython.locals( # <<<<<<<<<<<<<< + * p0=cython.complex, + * p1=cython.complex, +*/ + +/* Python wrapper */ +static PyObject *__pyx_pw_9fontTools_5cu2qu_5cu2qu_1_split_cubic_into_n_gen(PyObject *__pyx_self, +#if CYTHON_VECTORCALL +PyObject *const *__pyx_args, Py_ssize_t __pyx_nargs, PyObject *__pyx_kwds +#else +PyObject *__pyx_args, PyObject *__pyx_kwds +#endif +); /*proto*/ +PyDoc_STRVAR(__pyx_doc_9fontTools_5cu2qu_5cu2qu__split_cubic_into_n_gen, "_split_cubic_into_n_gen(double complex p0, double complex p1, double complex p2, double complex p3, int n)"); +static PyMethodDef __pyx_mdef_9fontTools_5cu2qu_5cu2qu_1_split_cubic_into_n_gen = {"_split_cubic_into_n_gen", (PyCFunction)(void(*)(void))(__Pyx_PyCFunction_FastCallWithKeywords)__pyx_pw_9fontTools_5cu2qu_5cu2qu_1_split_cubic_into_n_gen, __Pyx_METH_FASTCALL|METH_KEYWORDS, __pyx_doc_9fontTools_5cu2qu_5cu2qu__split_cubic_into_n_gen}; +static PyObject *__pyx_pw_9fontTools_5cu2qu_5cu2qu_1_split_cubic_into_n_gen(PyObject *__pyx_self, +#if CYTHON_VECTORCALL +PyObject *const *__pyx_args, Py_ssize_t __pyx_nargs, PyObject *__pyx_kwds +#else +PyObject *__pyx_args, PyObject *__pyx_kwds +#endif +) { + __pyx_t_double_complex __pyx_v_p0; + __pyx_t_double_complex __pyx_v_p1; + __pyx_t_double_complex __pyx_v_p2; + __pyx_t_double_complex __pyx_v_p3; + int __pyx_v_n; + #if !CYTHON_VECTORCALL + CYTHON_UNUSED Py_ssize_t __pyx_nargs; + #endif + CYTHON_UNUSED PyObject *const *__pyx_kwvalues; + PyObject* values[5] = {0,0,0,0,0}; + int __pyx_lineno = 0; + const char *__pyx_filename = NULL; + int __pyx_clineno = 0; + PyObject *__pyx_r = 0; + __Pyx_RefNannyDeclarations + __Pyx_RefNannySetupContext("_split_cubic_into_n_gen (wrapper)", 0); + #if !CYTHON_VECTORCALL + #if CYTHON_ASSUME_SAFE_SIZE + __pyx_nargs = PyTuple_GET_SIZE(__pyx_args); + #else + __pyx_nargs = PyTuple_Size(__pyx_args); if (unlikely(__pyx_nargs < 0)) return NULL; + #endif + #endif + __pyx_kwvalues = __Pyx_KwValues_FASTCALL(__pyx_args, __pyx_nargs); + { + PyObject ** const __pyx_pyargnames[] = {&__pyx_mstate_global->__pyx_n_u_p0,&__pyx_mstate_global->__pyx_n_u_p1,&__pyx_mstate_global->__pyx_n_u_p2,&__pyx_mstate_global->__pyx_n_u_p3,&__pyx_mstate_global->__pyx_n_u_n,0}; + const Py_ssize_t __pyx_kwds_len = (__pyx_kwds) ? __Pyx_NumKwargs_FASTCALL(__pyx_kwds) : 0; + if (unlikely(__pyx_kwds_len < 0)) __PYX_ERR(0, 149, __pyx_L3_error) + if (__pyx_kwds_len > 0) { + switch (__pyx_nargs) { + case 5: + values[4] = __Pyx_ArgRef_FASTCALL(__pyx_args, 4); + if (!CYTHON_ASSUME_SAFE_MACROS && unlikely(!values[4])) __PYX_ERR(0, 149, __pyx_L3_error) + CYTHON_FALLTHROUGH; + case 4: + values[3] = __Pyx_ArgRef_FASTCALL(__pyx_args, 3); + if (!CYTHON_ASSUME_SAFE_MACROS && unlikely(!values[3])) __PYX_ERR(0, 149, __pyx_L3_error) + CYTHON_FALLTHROUGH; + case 3: + values[2] = __Pyx_ArgRef_FASTCALL(__pyx_args, 2); + if (!CYTHON_ASSUME_SAFE_MACROS && unlikely(!values[2])) __PYX_ERR(0, 149, __pyx_L3_error) + CYTHON_FALLTHROUGH; + case 2: + values[1] = __Pyx_ArgRef_FASTCALL(__pyx_args, 1); + if (!CYTHON_ASSUME_SAFE_MACROS && unlikely(!values[1])) __PYX_ERR(0, 149, __pyx_L3_error) + CYTHON_FALLTHROUGH; + case 1: + values[0] = __Pyx_ArgRef_FASTCALL(__pyx_args, 0); + if (!CYTHON_ASSUME_SAFE_MACROS && unlikely(!values[0])) __PYX_ERR(0, 149, __pyx_L3_error) + CYTHON_FALLTHROUGH; + case 0: break; + default: goto __pyx_L5_argtuple_error; + } + const Py_ssize_t kwd_pos_args = __pyx_nargs; + if (__Pyx_ParseKeywords(__pyx_kwds, __pyx_kwvalues, __pyx_pyargnames, 0, values, kwd_pos_args, __pyx_kwds_len, "_split_cubic_into_n_gen", 0) < (0)) __PYX_ERR(0, 149, __pyx_L3_error) + for (Py_ssize_t i = __pyx_nargs; i < 5; i++) { + if (unlikely(!values[i])) { __Pyx_RaiseArgtupleInvalid("_split_cubic_into_n_gen", 1, 5, 5, i); __PYX_ERR(0, 149, __pyx_L3_error) } + } + } else if (unlikely(__pyx_nargs != 5)) { + goto __pyx_L5_argtuple_error; + } else { + values[0] = __Pyx_ArgRef_FASTCALL(__pyx_args, 0); + if (!CYTHON_ASSUME_SAFE_MACROS && unlikely(!values[0])) __PYX_ERR(0, 149, __pyx_L3_error) + values[1] = __Pyx_ArgRef_FASTCALL(__pyx_args, 1); + if (!CYTHON_ASSUME_SAFE_MACROS && unlikely(!values[1])) __PYX_ERR(0, 149, __pyx_L3_error) + values[2] = __Pyx_ArgRef_FASTCALL(__pyx_args, 2); + if (!CYTHON_ASSUME_SAFE_MACROS && unlikely(!values[2])) __PYX_ERR(0, 149, __pyx_L3_error) + values[3] = __Pyx_ArgRef_FASTCALL(__pyx_args, 3); + if (!CYTHON_ASSUME_SAFE_MACROS && unlikely(!values[3])) __PYX_ERR(0, 149, __pyx_L3_error) + values[4] = __Pyx_ArgRef_FASTCALL(__pyx_args, 4); + if (!CYTHON_ASSUME_SAFE_MACROS && unlikely(!values[4])) __PYX_ERR(0, 149, __pyx_L3_error) + } + __pyx_v_p0 = __Pyx_PyComplex_As___pyx_t_double_complex(values[0]); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 163, __pyx_L3_error) + __pyx_v_p1 = __Pyx_PyComplex_As___pyx_t_double_complex(values[1]); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 163, __pyx_L3_error) + __pyx_v_p2 = __Pyx_PyComplex_As___pyx_t_double_complex(values[2]); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 163, __pyx_L3_error) + __pyx_v_p3 = __Pyx_PyComplex_As___pyx_t_double_complex(values[3]); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 163, __pyx_L3_error) + __pyx_v_n = __Pyx_PyLong_As_int(values[4]); if (unlikely((__pyx_v_n == (int)-1) && PyErr_Occurred())) __PYX_ERR(0, 163, __pyx_L3_error) + } + goto __pyx_L6_skip; + __pyx_L5_argtuple_error:; + __Pyx_RaiseArgtupleInvalid("_split_cubic_into_n_gen", 1, 5, 5, __pyx_nargs); __PYX_ERR(0, 149, __pyx_L3_error) + __pyx_L6_skip:; + goto __pyx_L4_argument_unpacking_done; + __pyx_L3_error:; + for (Py_ssize_t __pyx_temp=0; __pyx_temp < (Py_ssize_t)(sizeof(values)/sizeof(values[0])); ++__pyx_temp) { + Py_XDECREF(values[__pyx_temp]); + } + __Pyx_AddTraceback("fontTools.cu2qu.cu2qu._split_cubic_into_n_gen", __pyx_clineno, __pyx_lineno, __pyx_filename); + __Pyx_RefNannyFinishContext(); + return NULL; + __pyx_L4_argument_unpacking_done:; + __pyx_r = __pyx_pf_9fontTools_5cu2qu_5cu2qu__split_cubic_into_n_gen(__pyx_self, __pyx_v_p0, __pyx_v_p1, __pyx_v_p2, __pyx_v_p3, __pyx_v_n); + + /* function exit code */ + for (Py_ssize_t __pyx_temp=0; __pyx_temp < (Py_ssize_t)(sizeof(values)/sizeof(values[0])); ++__pyx_temp) { + Py_XDECREF(values[__pyx_temp]); + } + + + + + + __Pyx_RefNannyFinishContext(); + return __pyx_r; +} + +static PyObject *__pyx_pf_9fontTools_5cu2qu_5cu2qu__split_cubic_into_n_gen(CYTHON_UNUSED PyObject *__pyx_self, __pyx_t_double_complex __pyx_v_p0, __pyx_t_double_complex __pyx_v_p1, __pyx_t_double_complex __pyx_v_p2, __pyx_t_double_complex __pyx_v_p3, int __pyx_v_n) { + struct __pyx_obj_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen *__pyx_cur_scope; + PyObject *__pyx_r = NULL; + __Pyx_RefNannyDeclarations + int __pyx_lineno = 0; + const char *__pyx_filename = NULL; + int __pyx_clineno = 0; + __Pyx_RefNannySetupContext("_split_cubic_into_n_gen", 0); + __pyx_cur_scope = (struct __pyx_obj_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen *)__pyx_tp_new_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen(__pyx_mstate_global->__pyx_ptype_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen, __pyx_mstate_global->__pyx_empty_tuple, NULL); + if (unlikely(!__pyx_cur_scope)) { + __pyx_cur_scope = ((struct __pyx_obj_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen *)Py_None); + __Pyx_INCREF(Py_None); + __PYX_ERR(0, 149, __pyx_L1_error) + } else { + __Pyx_GOTREF((PyObject *)__pyx_cur_scope); + } + __pyx_cur_scope->__pyx_v_p0 = __pyx_v_p0; + + + __pyx_cur_scope->__pyx_v_p1 = __pyx_v_p1; + + + __pyx_cur_scope->__pyx_v_p2 = __pyx_v_p2; + + + __pyx_cur_scope->__pyx_v_p3 = __pyx_v_p3; + + + __pyx_cur_scope->__pyx_v_n = __pyx_v_n; + + + { + __pyx_CoroutineObject *gen = __Pyx_Generator_New((__pyx_coroutine_body_t) __pyx_gb_9fontTools_5cu2qu_5cu2qu_2generator, ((PyObject *)__pyx_mstate_global->__pyx_codeobj_tab[0]), (PyObject *) __pyx_cur_scope, __pyx_mstate_global->__pyx_n_u_split_cubic_into_n_gen, __pyx_mstate_global->__pyx_n_u_split_cubic_into_n_gen, __pyx_mstate_global->__pyx_n_u_fontTools_cu2qu_cu2qu); if (unlikely(!gen)) __PYX_ERR(0, 149, __pyx_L1_error) + __Pyx_DECREF(__pyx_cur_scope); + __Pyx_RefNannyFinishContext(); + return (PyObject *) gen; + } + + /* function exit code */ + __pyx_L1_error:; + __Pyx_AddTraceback("fontTools.cu2qu.cu2qu._split_cubic_into_n_gen", __pyx_clineno, __pyx_lineno, __pyx_filename); + __pyx_r = NULL; + __Pyx_DECREF((PyObject *)__pyx_cur_scope); + __Pyx_XGIVEREF(__pyx_r); + __Pyx_RefNannyFinishContext(); + return __pyx_r; +} + +static PyObject *__pyx_gb_9fontTools_5cu2qu_5cu2qu_2generator(__pyx_CoroutineObject *__pyx_generator, CYTHON_UNUSED PyThreadState *__pyx_tstate, PyObject *__pyx_sent_value) /* generator body */ +{ + struct __pyx_obj_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen *__pyx_cur_scope = ((struct __pyx_obj_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen *)__pyx_generator->closure); + PyObject *__pyx_r = NULL; + PyObject *__pyx_t_1 = NULL; + PyObject *__pyx_t_2 = NULL; + PyObject *__pyx_t_3 = NULL; + PyObject *__pyx_t_4 = NULL; + PyObject *__pyx_t_5 = NULL; + PyObject *__pyx_t_6 = NULL; + PyObject *(*__pyx_t_7)(PyObject *); + __pyx_t_double_complex __pyx_t_8; + __pyx_t_double_complex __pyx_t_9; + __pyx_t_double_complex __pyx_t_10; + __pyx_t_double_complex __pyx_t_11; + int __pyx_t_12; + int __pyx_t_13; + int __pyx_t_14; + int __pyx_lineno = 0; + const char *__pyx_filename = NULL; + int __pyx_clineno = 0; + __Pyx_RefNannyDeclarations + __Pyx_RefNannySetupContext("_split_cubic_into_n_gen", 0); + switch (__pyx_generator->resume_label) { + case 0: goto __pyx_L3_first_run; + case 1: goto __pyx_L8_resume_from_yield; + default: /* CPython raises the right error here */ + __Pyx_RefNannyFinishContext(); + return NULL; + } + __pyx_L3_first_run:; + if (unlikely(__pyx_sent_value != Py_None)) { + if (unlikely(__pyx_sent_value)) PyErr_SetString(PyExc_TypeError, "can't send non-None value to a just-started generator"); + __PYX_ERR(0, 149, __pyx_L1_error) + } + + /* "fontTools/cu2qu/cu2qu.py":164 + * ) + * def _split_cubic_into_n_gen(p0, p1, p2, p3, n): + * a, b, c, d = calc_cubic_parameters(p0, p1, p2, p3) # <<<<<<<<<<<<<< + * dt = 1 / n + * delta_2 = dt * dt +*/ + __pyx_t_1 = __pyx_f_9fontTools_5cu2qu_5cu2qu_calc_cubic_parameters(__pyx_cur_scope->__pyx_v_p0, __pyx_cur_scope->__pyx_v_p1, __pyx_cur_scope->__pyx_v_p2, __pyx_cur_scope->__pyx_v_p3); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 164, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_1); + if ((likely(PyTuple_CheckExact(__pyx_t_1))) || (PyList_CheckExact(__pyx_t_1))) { + PyObject* sequence = __pyx_t_1; + Py_ssize_t size = __Pyx_PySequence_SIZE(sequence); + if (unlikely(size != 4)) { + if (size > 4) __Pyx_RaiseTooManyValuesError(4); + else if (size >= 0) __Pyx_RaiseNeedMoreValuesError(size); + __PYX_ERR(0, 164, __pyx_L1_error) + } + #if CYTHON_ASSUME_SAFE_MACROS && !CYTHON_AVOID_BORROWED_REFS + if (likely(PyTuple_CheckExact(sequence))) { + __pyx_t_2 = PyTuple_GET_ITEM(sequence, 0); + __Pyx_INCREF(__pyx_t_2); + __pyx_t_3 = PyTuple_GET_ITEM(sequence, 1); + __Pyx_INCREF(__pyx_t_3); + __pyx_t_4 = PyTuple_GET_ITEM(sequence, 2); + __Pyx_INCREF(__pyx_t_4); + __pyx_t_5 = PyTuple_GET_ITEM(sequence, 3); + __Pyx_INCREF(__pyx_t_5); + } else { + __pyx_t_2 = __Pyx_PyList_GET_ITEM_REF(sequence, 0, __Pyx_ReferenceSharing_SharedReference); + if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 164, __pyx_L1_error) + __Pyx_XGOTREF(__pyx_t_2); + __pyx_t_3 = __Pyx_PyList_GET_ITEM_REF(sequence, 1, __Pyx_ReferenceSharing_SharedReference); + if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 164, __pyx_L1_error) + __Pyx_XGOTREF(__pyx_t_3); + __pyx_t_4 = __Pyx_PyList_GET_ITEM_REF(sequence, 2, __Pyx_ReferenceSharing_SharedReference); + if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 164, __pyx_L1_error) + __Pyx_XGOTREF(__pyx_t_4); + __pyx_t_5 = __Pyx_PyList_GET_ITEM_REF(sequence, 3, __Pyx_ReferenceSharing_SharedReference); + if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 164, __pyx_L1_error) + __Pyx_XGOTREF(__pyx_t_5); + } + #else + { + Py_ssize_t i; + PyObject** temps[4] = {&__pyx_t_2,&__pyx_t_3,&__pyx_t_4,&__pyx_t_5}; + for (i=0; i < 4; i++) { + PyObject* item = __Pyx_PySequence_ITEM(sequence, i); if (unlikely(!item)) __PYX_ERR(0, 164, __pyx_L1_error) + __Pyx_GOTREF(item); + *(temps[i]) = item; + } + } + #endif + __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0; + } else { + Py_ssize_t index = -1; + PyObject** temps[4] = {&__pyx_t_2,&__pyx_t_3,&__pyx_t_4,&__pyx_t_5}; + __pyx_t_6 = PyObject_GetIter(__pyx_t_1); if (unlikely(!__pyx_t_6)) __PYX_ERR(0, 164, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_6); + __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0; + __pyx_t_7 = (CYTHON_COMPILING_IN_LIMITED_API) ? PyIter_Next : __Pyx_PyObject_GetIterNextFunc(__pyx_t_6); + for (index=0; index < 4; index++) { + PyObject* item = __pyx_t_7(__pyx_t_6); if (unlikely(!item)) goto __pyx_L4_unpacking_failed; + __Pyx_GOTREF(item); + *(temps[index]) = item; + } + if (__Pyx_IternextUnpackEndCheck(__pyx_t_7(__pyx_t_6), 4) < (0)) __PYX_ERR(0, 164, __pyx_L1_error) + __pyx_t_7 = NULL; + __Pyx_DECREF(__pyx_t_6); __pyx_t_6 = 0; + goto __pyx_L5_unpacking_done; + __pyx_L4_unpacking_failed:; + __Pyx_DECREF(__pyx_t_6); __pyx_t_6 = 0; + __pyx_t_7 = NULL; + if (__Pyx_IterFinish() == 0) __Pyx_RaiseNeedMoreValuesError(index); + __PYX_ERR(0, 164, __pyx_L1_error) + __pyx_L5_unpacking_done:; + } + __pyx_t_8 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_2); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 164, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0; + __pyx_t_9 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_3); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 164, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0; + __pyx_t_10 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_4); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 164, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0; + __pyx_t_11 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_5); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 164, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_5); __pyx_t_5 = 0; + __pyx_cur_scope->__pyx_v_a = __pyx_t_8; + __pyx_cur_scope->__pyx_v_b = __pyx_t_9; + __pyx_cur_scope->__pyx_v_c = __pyx_t_10; + __pyx_cur_scope->__pyx_v_d = __pyx_t_11; + + /* "fontTools/cu2qu/cu2qu.py":165 + * def _split_cubic_into_n_gen(p0, p1, p2, p3, n): + * a, b, c, d = calc_cubic_parameters(p0, p1, p2, p3) + * dt = 1 / n # <<<<<<<<<<<<<< + * delta_2 = dt * dt + * delta_3 = dt * delta_2 +*/ + if (unlikely(__pyx_cur_scope->__pyx_v_n == 0)) { + PyErr_SetString(PyExc_ZeroDivisionError, "float division"); + __PYX_ERR(0, 165, __pyx_L1_error) + } + __pyx_cur_scope->__pyx_v_dt = (1.0 / ((double)__pyx_cur_scope->__pyx_v_n)); + + /* "fontTools/cu2qu/cu2qu.py":166 + * a, b, c, d = calc_cubic_parameters(p0, p1, p2, p3) + * dt = 1 / n + * delta_2 = dt * dt # <<<<<<<<<<<<<< + * delta_3 = dt * delta_2 + * for i in range(n): +*/ + __pyx_cur_scope->__pyx_v_delta_2 = (__pyx_cur_scope->__pyx_v_dt * __pyx_cur_scope->__pyx_v_dt); + + /* "fontTools/cu2qu/cu2qu.py":167 + * dt = 1 / n + * delta_2 = dt * dt + * delta_3 = dt * delta_2 # <<<<<<<<<<<<<< + * for i in range(n): + * t1 = i * dt +*/ + __pyx_cur_scope->__pyx_v_delta_3 = (__pyx_cur_scope->__pyx_v_dt * __pyx_cur_scope->__pyx_v_delta_2); + + /* "fontTools/cu2qu/cu2qu.py":168 + * delta_2 = dt * dt + * delta_3 = dt * delta_2 + * for i in range(n): # <<<<<<<<<<<<<< + * t1 = i * dt + * t1_2 = t1 * t1 +*/ + + __pyx_t_12 = __pyx_cur_scope->__pyx_v_n; + __pyx_t_13 = __pyx_t_12; + + for (__pyx_t_14 = 0; __pyx_t_14 < __pyx_t_13; __pyx_t_14+=1) { + __pyx_cur_scope->__pyx_v_i = __pyx_t_14; + + /* "fontTools/cu2qu/cu2qu.py":169 + * delta_3 = dt * delta_2 + * for i in range(n): + * t1 = i * dt # <<<<<<<<<<<<<< + * t1_2 = t1 * t1 + * # calc new a, b, c and d +*/ + __pyx_cur_scope->__pyx_v_t1 = (__pyx_cur_scope->__pyx_v_i * __pyx_cur_scope->__pyx_v_dt); + + /* "fontTools/cu2qu/cu2qu.py":170 + * for i in range(n): + * t1 = i * dt + * t1_2 = t1 * t1 # <<<<<<<<<<<<<< + * # calc new a, b, c and d + * a1 = a * delta_3 +*/ + __pyx_cur_scope->__pyx_v_t1_2 = (__pyx_cur_scope->__pyx_v_t1 * __pyx_cur_scope->__pyx_v_t1); + + /* "fontTools/cu2qu/cu2qu.py":172 + * t1_2 = t1 * t1 + * # calc new a, b, c and d + * a1 = a * delta_3 # <<<<<<<<<<<<<< + * b1 = (3 * a * t1 + b) * delta_2 + * c1 = (2 * b * t1 + c + 3 * a * t1_2) * dt +*/ + __pyx_cur_scope->__pyx_v_a1 = __Pyx_c_prod_double(__pyx_cur_scope->__pyx_v_a, __pyx_t_double_complex_from_parts(__pyx_cur_scope->__pyx_v_delta_3, 0)); + + /* "fontTools/cu2qu/cu2qu.py":173 + * # calc new a, b, c and d + * a1 = a * delta_3 + * b1 = (3 * a * t1 + b) * delta_2 # <<<<<<<<<<<<<< + * c1 = (2 * b * t1 + c + 3 * a * t1_2) * dt + * d1 = a * t1 * t1_2 + b * t1_2 + c * t1 + d +*/ + __pyx_cur_scope->__pyx_v_b1 = __Pyx_c_prod_double(__Pyx_c_sum_double(__Pyx_c_prod_double(__Pyx_c_prod_double(__pyx_t_double_complex_from_parts(3, 0), __pyx_cur_scope->__pyx_v_a), __pyx_t_double_complex_from_parts(__pyx_cur_scope->__pyx_v_t1, 0)), __pyx_cur_scope->__pyx_v_b), __pyx_t_double_complex_from_parts(__pyx_cur_scope->__pyx_v_delta_2, 0)); + + /* "fontTools/cu2qu/cu2qu.py":174 + * a1 = a * delta_3 + * b1 = (3 * a * t1 + b) * delta_2 + * c1 = (2 * b * t1 + c + 3 * a * t1_2) * dt # <<<<<<<<<<<<<< + * d1 = a * t1 * t1_2 + b * t1_2 + c * t1 + d + * yield calc_cubic_points(a1, b1, c1, d1) +*/ + __pyx_cur_scope->__pyx_v_c1 = __Pyx_c_prod_double(__Pyx_c_sum_double(__Pyx_c_sum_double(__Pyx_c_prod_double(__Pyx_c_prod_double(__pyx_t_double_complex_from_parts(2, 0), __pyx_cur_scope->__pyx_v_b), __pyx_t_double_complex_from_parts(__pyx_cur_scope->__pyx_v_t1, 0)), __pyx_cur_scope->__pyx_v_c), __Pyx_c_prod_double(__Pyx_c_prod_double(__pyx_t_double_complex_from_parts(3, 0), __pyx_cur_scope->__pyx_v_a), __pyx_t_double_complex_from_parts(__pyx_cur_scope->__pyx_v_t1_2, 0))), __pyx_t_double_complex_from_parts(__pyx_cur_scope->__pyx_v_dt, 0)); + + /* "fontTools/cu2qu/cu2qu.py":175 + * b1 = (3 * a * t1 + b) * delta_2 + * c1 = (2 * b * t1 + c + 3 * a * t1_2) * dt + * d1 = a * t1 * t1_2 + b * t1_2 + c * t1 + d # <<<<<<<<<<<<<< + * yield calc_cubic_points(a1, b1, c1, d1) + * +*/ + __pyx_cur_scope->__pyx_v_d1 = __Pyx_c_sum_double(__Pyx_c_sum_double(__Pyx_c_sum_double(__Pyx_c_prod_double(__Pyx_c_prod_double(__pyx_cur_scope->__pyx_v_a, __pyx_t_double_complex_from_parts(__pyx_cur_scope->__pyx_v_t1, 0)), __pyx_t_double_complex_from_parts(__pyx_cur_scope->__pyx_v_t1_2, 0)), __Pyx_c_prod_double(__pyx_cur_scope->__pyx_v_b, __pyx_t_double_complex_from_parts(__pyx_cur_scope->__pyx_v_t1_2, 0))), __Pyx_c_prod_double(__pyx_cur_scope->__pyx_v_c, __pyx_t_double_complex_from_parts(__pyx_cur_scope->__pyx_v_t1, 0))), __pyx_cur_scope->__pyx_v_d); + + /* "fontTools/cu2qu/cu2qu.py":176 + * c1 = (2 * b * t1 + c + 3 * a * t1_2) * dt + * d1 = a * t1 * t1_2 + b * t1_2 + c * t1 + d + * yield calc_cubic_points(a1, b1, c1, d1) # <<<<<<<<<<<<<< + * + * +*/ + __pyx_t_1 = __pyx_f_9fontTools_5cu2qu_5cu2qu_calc_cubic_points(__pyx_cur_scope->__pyx_v_a1, __pyx_cur_scope->__pyx_v_b1, __pyx_cur_scope->__pyx_v_c1, __pyx_cur_scope->__pyx_v_d1); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 176, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_1); + __pyx_r = __pyx_t_1; + __pyx_t_1 = 0; + + __pyx_cur_scope->__pyx_t_0 = __pyx_t_12; + + __pyx_cur_scope->__pyx_t_1 = __pyx_t_13; + + __pyx_cur_scope->__pyx_t_2 = __pyx_t_14; + __Pyx_XGIVEREF(__pyx_r); + __Pyx_RefNannyFinishContext(); + __Pyx_Coroutine_ResetAndClearException(__pyx_generator); + /* return from generator, yielding value */ + __pyx_generator->resume_label = 1; + return __pyx_r; + __pyx_L8_resume_from_yield:; + __pyx_t_12 = __pyx_cur_scope->__pyx_t_0; + __pyx_t_13 = __pyx_cur_scope->__pyx_t_1; + __pyx_t_14 = __pyx_cur_scope->__pyx_t_2; + if (unlikely(!__pyx_sent_value)) __PYX_ERR(0, 176, __pyx_L1_error) + } + + CYTHON_MAYBE_UNUSED_VAR(__pyx_cur_scope); + + /* "fontTools/cu2qu/cu2qu.py":149 + * + * + * @cython.locals( # <<<<<<<<<<<<<< + * p0=cython.complex, + * p1=cython.complex, +*/ + + /* function exit code */ + __pyx_r = Py_None; __Pyx_INCREF(Py_None); + goto __pyx_L0; + __pyx_L1_error:; + __Pyx_XDECREF(__pyx_t_1); + __Pyx_XDECREF(__pyx_t_2); + __Pyx_XDECREF(__pyx_t_3); + __Pyx_XDECREF(__pyx_t_4); + __Pyx_XDECREF(__pyx_t_5); + __Pyx_XDECREF(__pyx_t_6); + if (__Pyx_PyErr_Occurred()) { + __Pyx_Generator_Replace_StopIteration(0); + __Pyx_AddTraceback("_split_cubic_into_n_gen", __pyx_clineno, __pyx_lineno, __pyx_filename); + } + __pyx_L0:; + __Pyx_XGIVEREF(__pyx_r); + #if !CYTHON_USE_EXC_INFO_STACK + __Pyx_Coroutine_ResetAndClearException(__pyx_generator); + #endif + __pyx_generator->resume_label = -1; + __Pyx_Coroutine_clear((PyObject*)__pyx_generator); + __Pyx_RefNannyFinishContext(); + return __pyx_r; +} + +/* "fontTools/cu2qu/cu2qu.py":179 + * + * + * @cython.cfunc # <<<<<<<<<<<<<< + * @cython.inline + * @cython.locals( +*/ + +static CYTHON_INLINE PyObject *__pyx_f_9fontTools_5cu2qu_5cu2qu_split_cubic_into_two(__pyx_t_double_complex __pyx_v_p0, __pyx_t_double_complex __pyx_v_p1, __pyx_t_double_complex __pyx_v_p2, __pyx_t_double_complex __pyx_v_p3) { + __pyx_t_double_complex __pyx_v_mid; + __pyx_t_double_complex __pyx_v_deriv3; + PyObject *__pyx_r = NULL; + __Pyx_RefNannyDeclarations + PyObject *__pyx_t_1 = NULL; + __pyx_t_double_complex __pyx_t_2; + PyObject *__pyx_t_3 = NULL; + PyObject *__pyx_t_4 = NULL; + PyObject *__pyx_t_5 = NULL; + PyObject *__pyx_t_6 = NULL; + PyObject *__pyx_t_7 = NULL; + int __pyx_lineno = 0; + const char *__pyx_filename = NULL; + int __pyx_clineno = 0; + __Pyx_RefNannySetupContext("split_cubic_into_two", 0); + + /* "fontTools/cu2qu/cu2qu.py":200 + * values). + * """ + * mid = (p0 + 3 * (p1 + p2) + p3) * 0.125 # <<<<<<<<<<<<<< + * deriv3 = (p3 + p2 - p1 - p0) * 0.125 + * return ( +*/ + __pyx_v_mid = __Pyx_c_prod_double(__Pyx_c_sum_double(__Pyx_c_sum_double(__pyx_v_p0, __Pyx_c_prod_double(__pyx_t_double_complex_from_parts(3, 0), __Pyx_c_sum_double(__pyx_v_p1, __pyx_v_p2))), __pyx_v_p3), __pyx_t_double_complex_from_parts(0.125, 0)); + + /* "fontTools/cu2qu/cu2qu.py":201 + * """ + * mid = (p0 + 3 * (p1 + p2) + p3) * 0.125 + * deriv3 = (p3 + p2 - p1 - p0) * 0.125 # <<<<<<<<<<<<<< + * return ( + * (p0, (p0 + p1) * 0.5, mid - deriv3, mid), +*/ + __pyx_v_deriv3 = __Pyx_c_prod_double(__Pyx_c_diff_double(__Pyx_c_diff_double(__Pyx_c_sum_double(__pyx_v_p3, __pyx_v_p2), __pyx_v_p1), __pyx_v_p0), __pyx_t_double_complex_from_parts(0.125, 0)); + + /* "fontTools/cu2qu/cu2qu.py":203 + * deriv3 = (p3 + p2 - p1 - p0) * 0.125 + * return ( + * (p0, (p0 + p1) * 0.5, mid - deriv3, mid), # <<<<<<<<<<<<<< + * (mid, mid + deriv3, (p2 + p3) * 0.5, p3), + * ) +*/ + __pyx_t_1 = __pyx_PyComplex_FromComplex(__pyx_v_p0); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 203, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_1); + __pyx_t_2 = __Pyx_c_prod_double(__Pyx_c_sum_double(__pyx_v_p0, __pyx_v_p1), __pyx_t_double_complex_from_parts(0.5, 0)); + + __pyx_t_3 = __pyx_PyComplex_FromComplex(__pyx_t_2); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 203, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_3); + + __pyx_t_2 = __Pyx_c_diff_double(__pyx_v_mid, __pyx_v_deriv3); + + __pyx_t_4 = __pyx_PyComplex_FromComplex(__pyx_t_2); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 203, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_4); + + __pyx_t_5 = __pyx_PyComplex_FromComplex(__pyx_v_mid); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 203, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_5); + __pyx_t_6 = PyTuple_New(4); if (unlikely(!__pyx_t_6)) __PYX_ERR(0, 203, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_6); + __Pyx_GIVEREF(__pyx_t_1); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_6, 0, __pyx_t_1) != (0)) __PYX_ERR(0, 203, __pyx_L1_error); + __Pyx_GIVEREF(__pyx_t_3); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_6, 1, __pyx_t_3) != (0)) __PYX_ERR(0, 203, __pyx_L1_error); + __Pyx_GIVEREF(__pyx_t_4); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_6, 2, __pyx_t_4) != (0)) __PYX_ERR(0, 203, __pyx_L1_error); + __Pyx_GIVEREF(__pyx_t_5); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_6, 3, __pyx_t_5) != (0)) __PYX_ERR(0, 203, __pyx_L1_error); + __pyx_t_1 = 0; + __pyx_t_3 = 0; + __pyx_t_4 = 0; + __pyx_t_5 = 0; + + /* "fontTools/cu2qu/cu2qu.py":204 + * return ( + * (p0, (p0 + p1) * 0.5, mid - deriv3, mid), + * (mid, mid + deriv3, (p2 + p3) * 0.5, p3), # <<<<<<<<<<<<<< + * ) + * +*/ + __pyx_t_5 = __pyx_PyComplex_FromComplex(__pyx_v_mid); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 204, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_5); + __pyx_t_2 = __Pyx_c_sum_double(__pyx_v_mid, __pyx_v_deriv3); + + __pyx_t_4 = __pyx_PyComplex_FromComplex(__pyx_t_2); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 204, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_4); + + __pyx_t_2 = __Pyx_c_prod_double(__Pyx_c_sum_double(__pyx_v_p2, __pyx_v_p3), __pyx_t_double_complex_from_parts(0.5, 0)); + + __pyx_t_3 = __pyx_PyComplex_FromComplex(__pyx_t_2); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 204, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_3); + + __pyx_t_1 = __pyx_PyComplex_FromComplex(__pyx_v_p3); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 204, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_1); + __pyx_t_7 = PyTuple_New(4); if (unlikely(!__pyx_t_7)) __PYX_ERR(0, 204, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_7); + __Pyx_GIVEREF(__pyx_t_5); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_7, 0, __pyx_t_5) != (0)) __PYX_ERR(0, 204, __pyx_L1_error); + __Pyx_GIVEREF(__pyx_t_4); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_7, 1, __pyx_t_4) != (0)) __PYX_ERR(0, 204, __pyx_L1_error); + __Pyx_GIVEREF(__pyx_t_3); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_7, 2, __pyx_t_3) != (0)) __PYX_ERR(0, 204, __pyx_L1_error); + __Pyx_GIVEREF(__pyx_t_1); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_7, 3, __pyx_t_1) != (0)) __PYX_ERR(0, 204, __pyx_L1_error); + __pyx_t_5 = 0; + __pyx_t_4 = 0; + __pyx_t_3 = 0; + __pyx_t_1 = 0; + + /* "fontTools/cu2qu/cu2qu.py":203 + * deriv3 = (p3 + p2 - p1 - p0) * 0.125 + * return ( + * (p0, (p0 + p1) * 0.5, mid - deriv3, mid), # <<<<<<<<<<<<<< + * (mid, mid + deriv3, (p2 + p3) * 0.5, p3), + * ) +*/ + __pyx_t_1 = PyTuple_New(2); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 203, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_1); + __Pyx_GIVEREF(__pyx_t_6); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_1, 0, __pyx_t_6) != (0)) __PYX_ERR(0, 203, __pyx_L1_error); + __Pyx_GIVEREF(__pyx_t_7); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_1, 1, __pyx_t_7) != (0)) __PYX_ERR(0, 203, __pyx_L1_error); + __pyx_t_6 = 0; + __pyx_t_7 = 0; + { + PyObject *__pyx_temp; + { + __pyx_temp = __pyx_r; + __pyx_r = __pyx_t_1; + } + __Pyx_XDECREF(__pyx_temp); + } + __pyx_t_1 = 0; + goto __pyx_L0; + + /* "fontTools/cu2qu/cu2qu.py":179 + * + * + * @cython.cfunc # <<<<<<<<<<<<<< + * @cython.inline + * @cython.locals( +*/ + + /* function exit code */ + __pyx_L1_error:; + __Pyx_XDECREF(__pyx_t_1); + __Pyx_XDECREF(__pyx_t_3); + __Pyx_XDECREF(__pyx_t_4); + __Pyx_XDECREF(__pyx_t_5); + __Pyx_XDECREF(__pyx_t_6); + __Pyx_XDECREF(__pyx_t_7); + __Pyx_AddTraceback("fontTools.cu2qu.cu2qu.split_cubic_into_two", __pyx_clineno, __pyx_lineno, __pyx_filename); + __pyx_r = 0; + __pyx_L0:; + + + __Pyx_XGIVEREF(__pyx_r); + __Pyx_RefNannyFinishContext(); + return __pyx_r; +} + +/* "fontTools/cu2qu/cu2qu.py":208 + * + * + * @cython.cfunc # <<<<<<<<<<<<<< + * @cython.inline + * @cython.locals( +*/ + +static CYTHON_INLINE PyObject *__pyx_f_9fontTools_5cu2qu_5cu2qu_split_cubic_into_three(__pyx_t_double_complex __pyx_v_p0, __pyx_t_double_complex __pyx_v_p1, __pyx_t_double_complex __pyx_v_p2, __pyx_t_double_complex __pyx_v_p3) { + __pyx_t_double_complex __pyx_v_mid1; + __pyx_t_double_complex __pyx_v_deriv1; + __pyx_t_double_complex __pyx_v_mid2; + __pyx_t_double_complex __pyx_v_deriv2; + PyObject *__pyx_r = NULL; + __Pyx_RefNannyDeclarations + PyObject *__pyx_t_1 = NULL; + PyObject *__pyx_t_2 = NULL; + __pyx_t_double_complex __pyx_t_3; + PyObject *__pyx_t_4 = NULL; + PyObject *__pyx_t_5 = NULL; + PyObject *__pyx_t_6 = NULL; + PyObject *__pyx_t_7 = NULL; + PyObject *__pyx_t_8 = NULL; + int __pyx_lineno = 0; + const char *__pyx_filename = NULL; + int __pyx_clineno = 0; + __Pyx_RefNannySetupContext("split_cubic_into_three", 0); + + /* "fontTools/cu2qu/cu2qu.py":237 + * values). + * """ + * mid1 = (8 * p0 + 12 * p1 + 6 * p2 + p3) * (1 / 27) # <<<<<<<<<<<<<< + * deriv1 = (p3 + 3 * p2 - 4 * p0) * (1 / 27) + * mid2 = (p0 + 6 * p1 + 12 * p2 + 8 * p3) * (1 / 27) +*/ + __pyx_v_mid1 = __Pyx_c_prod_double(__Pyx_c_sum_double(__Pyx_c_sum_double(__Pyx_c_sum_double(__Pyx_c_prod_double(__pyx_t_double_complex_from_parts(8, 0), __pyx_v_p0), __Pyx_c_prod_double(__pyx_t_double_complex_from_parts(12, 0), __pyx_v_p1)), __Pyx_c_prod_double(__pyx_t_double_complex_from_parts(6, 0), __pyx_v_p2)), __pyx_v_p3), __pyx_t_double_complex_from_parts((1.0 / 27.0), 0)); + + /* "fontTools/cu2qu/cu2qu.py":238 + * """ + * mid1 = (8 * p0 + 12 * p1 + 6 * p2 + p3) * (1 / 27) + * deriv1 = (p3 + 3 * p2 - 4 * p0) * (1 / 27) # <<<<<<<<<<<<<< + * mid2 = (p0 + 6 * p1 + 12 * p2 + 8 * p3) * (1 / 27) + * deriv2 = (4 * p3 - 3 * p1 - p0) * (1 / 27) +*/ + __pyx_v_deriv1 = __Pyx_c_prod_double(__Pyx_c_diff_double(__Pyx_c_sum_double(__pyx_v_p3, __Pyx_c_prod_double(__pyx_t_double_complex_from_parts(3, 0), __pyx_v_p2)), __Pyx_c_prod_double(__pyx_t_double_complex_from_parts(4, 0), __pyx_v_p0)), __pyx_t_double_complex_from_parts((1.0 / 27.0), 0)); + + /* "fontTools/cu2qu/cu2qu.py":239 + * mid1 = (8 * p0 + 12 * p1 + 6 * p2 + p3) * (1 / 27) + * deriv1 = (p3 + 3 * p2 - 4 * p0) * (1 / 27) + * mid2 = (p0 + 6 * p1 + 12 * p2 + 8 * p3) * (1 / 27) # <<<<<<<<<<<<<< + * deriv2 = (4 * p3 - 3 * p1 - p0) * (1 / 27) + * return ( +*/ + __pyx_v_mid2 = __Pyx_c_prod_double(__Pyx_c_sum_double(__Pyx_c_sum_double(__Pyx_c_sum_double(__pyx_v_p0, __Pyx_c_prod_double(__pyx_t_double_complex_from_parts(6, 0), __pyx_v_p1)), __Pyx_c_prod_double(__pyx_t_double_complex_from_parts(12, 0), __pyx_v_p2)), __Pyx_c_prod_double(__pyx_t_double_complex_from_parts(8, 0), __pyx_v_p3)), __pyx_t_double_complex_from_parts((1.0 / 27.0), 0)); + + /* "fontTools/cu2qu/cu2qu.py":240 + * deriv1 = (p3 + 3 * p2 - 4 * p0) * (1 / 27) + * mid2 = (p0 + 6 * p1 + 12 * p2 + 8 * p3) * (1 / 27) + * deriv2 = (4 * p3 - 3 * p1 - p0) * (1 / 27) # <<<<<<<<<<<<<< + * return ( + * (p0, _complex_div_by_real(2 * p0 + p1, 3.0), mid1 - deriv1, mid1), +*/ + __pyx_v_deriv2 = __Pyx_c_prod_double(__Pyx_c_diff_double(__Pyx_c_diff_double(__Pyx_c_prod_double(__pyx_t_double_complex_from_parts(4, 0), __pyx_v_p3), __Pyx_c_prod_double(__pyx_t_double_complex_from_parts(3, 0), __pyx_v_p1)), __pyx_v_p0), __pyx_t_double_complex_from_parts((1.0 / 27.0), 0)); + + /* "fontTools/cu2qu/cu2qu.py":242 + * deriv2 = (4 * p3 - 3 * p1 - p0) * (1 / 27) + * return ( + * (p0, _complex_div_by_real(2 * p0 + p1, 3.0), mid1 - deriv1, mid1), # <<<<<<<<<<<<<< + * (mid1, mid1 + deriv1, mid2 - deriv2, mid2), + * (mid2, mid2 + deriv2, _complex_div_by_real(p2 + 2 * p3, 3.0), p3), +*/ + __pyx_t_1 = __pyx_PyComplex_FromComplex(__pyx_v_p0); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 242, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_1); + __pyx_t_2 = __pyx_f_9fontTools_5cu2qu_5cu2qu__complex_div_by_real(__Pyx_c_sum_double(__Pyx_c_prod_double(__pyx_t_double_complex_from_parts(2, 0), __pyx_v_p0), __pyx_v_p1), 3.0); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 242, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + __pyx_t_3 = __Pyx_c_diff_double(__pyx_v_mid1, __pyx_v_deriv1); + + __pyx_t_4 = __pyx_PyComplex_FromComplex(__pyx_t_3); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 242, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_4); + + __pyx_t_5 = __pyx_PyComplex_FromComplex(__pyx_v_mid1); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 242, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_5); + __pyx_t_6 = PyTuple_New(4); if (unlikely(!__pyx_t_6)) __PYX_ERR(0, 242, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_6); + __Pyx_GIVEREF(__pyx_t_1); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_6, 0, __pyx_t_1) != (0)) __PYX_ERR(0, 242, __pyx_L1_error); + __Pyx_GIVEREF(__pyx_t_2); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_6, 1, __pyx_t_2) != (0)) __PYX_ERR(0, 242, __pyx_L1_error); + __Pyx_GIVEREF(__pyx_t_4); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_6, 2, __pyx_t_4) != (0)) __PYX_ERR(0, 242, __pyx_L1_error); + __Pyx_GIVEREF(__pyx_t_5); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_6, 3, __pyx_t_5) != (0)) __PYX_ERR(0, 242, __pyx_L1_error); + __pyx_t_1 = 0; + __pyx_t_2 = 0; + __pyx_t_4 = 0; + __pyx_t_5 = 0; + + /* "fontTools/cu2qu/cu2qu.py":243 + * return ( + * (p0, _complex_div_by_real(2 * p0 + p1, 3.0), mid1 - deriv1, mid1), + * (mid1, mid1 + deriv1, mid2 - deriv2, mid2), # <<<<<<<<<<<<<< + * (mid2, mid2 + deriv2, _complex_div_by_real(p2 + 2 * p3, 3.0), p3), + * ) +*/ + __pyx_t_5 = __pyx_PyComplex_FromComplex(__pyx_v_mid1); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 243, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_5); + __pyx_t_3 = __Pyx_c_sum_double(__pyx_v_mid1, __pyx_v_deriv1); + + __pyx_t_4 = __pyx_PyComplex_FromComplex(__pyx_t_3); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 243, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_4); + + __pyx_t_3 = __Pyx_c_diff_double(__pyx_v_mid2, __pyx_v_deriv2); + + __pyx_t_2 = __pyx_PyComplex_FromComplex(__pyx_t_3); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 243, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + + __pyx_t_1 = __pyx_PyComplex_FromComplex(__pyx_v_mid2); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 243, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_1); + __pyx_t_7 = PyTuple_New(4); if (unlikely(!__pyx_t_7)) __PYX_ERR(0, 243, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_7); + __Pyx_GIVEREF(__pyx_t_5); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_7, 0, __pyx_t_5) != (0)) __PYX_ERR(0, 243, __pyx_L1_error); + __Pyx_GIVEREF(__pyx_t_4); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_7, 1, __pyx_t_4) != (0)) __PYX_ERR(0, 243, __pyx_L1_error); + __Pyx_GIVEREF(__pyx_t_2); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_7, 2, __pyx_t_2) != (0)) __PYX_ERR(0, 243, __pyx_L1_error); + __Pyx_GIVEREF(__pyx_t_1); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_7, 3, __pyx_t_1) != (0)) __PYX_ERR(0, 243, __pyx_L1_error); + __pyx_t_5 = 0; + __pyx_t_4 = 0; + __pyx_t_2 = 0; + __pyx_t_1 = 0; + + /* "fontTools/cu2qu/cu2qu.py":244 + * (p0, _complex_div_by_real(2 * p0 + p1, 3.0), mid1 - deriv1, mid1), + * (mid1, mid1 + deriv1, mid2 - deriv2, mid2), + * (mid2, mid2 + deriv2, _complex_div_by_real(p2 + 2 * p3, 3.0), p3), # <<<<<<<<<<<<<< + * ) + * +*/ + __pyx_t_1 = __pyx_PyComplex_FromComplex(__pyx_v_mid2); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 244, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_1); + __pyx_t_3 = __Pyx_c_sum_double(__pyx_v_mid2, __pyx_v_deriv2); + + __pyx_t_2 = __pyx_PyComplex_FromComplex(__pyx_t_3); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 244, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + + __pyx_t_4 = __pyx_f_9fontTools_5cu2qu_5cu2qu__complex_div_by_real(__Pyx_c_sum_double(__pyx_v_p2, __Pyx_c_prod_double(__pyx_t_double_complex_from_parts(2, 0), __pyx_v_p3)), 3.0); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 244, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_4); + __pyx_t_5 = __pyx_PyComplex_FromComplex(__pyx_v_p3); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 244, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_5); + __pyx_t_8 = PyTuple_New(4); if (unlikely(!__pyx_t_8)) __PYX_ERR(0, 244, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_8); + __Pyx_GIVEREF(__pyx_t_1); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_8, 0, __pyx_t_1) != (0)) __PYX_ERR(0, 244, __pyx_L1_error); + __Pyx_GIVEREF(__pyx_t_2); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_8, 1, __pyx_t_2) != (0)) __PYX_ERR(0, 244, __pyx_L1_error); + __Pyx_GIVEREF(__pyx_t_4); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_8, 2, __pyx_t_4) != (0)) __PYX_ERR(0, 244, __pyx_L1_error); + __Pyx_GIVEREF(__pyx_t_5); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_8, 3, __pyx_t_5) != (0)) __PYX_ERR(0, 244, __pyx_L1_error); + __pyx_t_1 = 0; + __pyx_t_2 = 0; + __pyx_t_4 = 0; + __pyx_t_5 = 0; + + /* "fontTools/cu2qu/cu2qu.py":242 + * deriv2 = (4 * p3 - 3 * p1 - p0) * (1 / 27) + * return ( + * (p0, _complex_div_by_real(2 * p0 + p1, 3.0), mid1 - deriv1, mid1), # <<<<<<<<<<<<<< + * (mid1, mid1 + deriv1, mid2 - deriv2, mid2), + * (mid2, mid2 + deriv2, _complex_div_by_real(p2 + 2 * p3, 3.0), p3), +*/ + __pyx_t_5 = PyTuple_New(3); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 242, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_5); + __Pyx_GIVEREF(__pyx_t_6); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_5, 0, __pyx_t_6) != (0)) __PYX_ERR(0, 242, __pyx_L1_error); + __Pyx_GIVEREF(__pyx_t_7); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_5, 1, __pyx_t_7) != (0)) __PYX_ERR(0, 242, __pyx_L1_error); + __Pyx_GIVEREF(__pyx_t_8); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_5, 2, __pyx_t_8) != (0)) __PYX_ERR(0, 242, __pyx_L1_error); + __pyx_t_6 = 0; + __pyx_t_7 = 0; + __pyx_t_8 = 0; + { + PyObject *__pyx_temp; + { + __pyx_temp = __pyx_r; + __pyx_r = __pyx_t_5; + } + __Pyx_XDECREF(__pyx_temp); + } + __pyx_t_5 = 0; + goto __pyx_L0; + + /* "fontTools/cu2qu/cu2qu.py":208 + * + * + * @cython.cfunc # <<<<<<<<<<<<<< + * @cython.inline + * @cython.locals( +*/ + + /* function exit code */ + __pyx_L1_error:; + __Pyx_XDECREF(__pyx_t_1); + __Pyx_XDECREF(__pyx_t_2); + __Pyx_XDECREF(__pyx_t_4); + __Pyx_XDECREF(__pyx_t_5); + __Pyx_XDECREF(__pyx_t_6); + __Pyx_XDECREF(__pyx_t_7); + __Pyx_XDECREF(__pyx_t_8); + __Pyx_AddTraceback("fontTools.cu2qu.cu2qu.split_cubic_into_three", __pyx_clineno, __pyx_lineno, __pyx_filename); + __pyx_r = 0; + __pyx_L0:; + + + + + __Pyx_XGIVEREF(__pyx_r); + __Pyx_RefNannyFinishContext(); + return __pyx_r; +} + +/* "fontTools/cu2qu/cu2qu.py":248 + * + * + * @cython.cfunc # <<<<<<<<<<<<<< + * @cython.inline + * @cython.returns(cython.complex) +*/ + +static CYTHON_INLINE __pyx_t_double_complex __pyx_f_9fontTools_5cu2qu_5cu2qu_cubic_approx_control(double __pyx_v_t, __pyx_t_double_complex __pyx_v_p0, __pyx_t_double_complex __pyx_v_p1, __pyx_t_double_complex __pyx_v_p2, __pyx_t_double_complex __pyx_v_p3) { + __pyx_t_double_complex __pyx_v__p1; + __pyx_t_double_complex __pyx_v__p2; + __pyx_t_double_complex __pyx_r; + + /* "fontTools/cu2qu/cu2qu.py":272 + * complex: Location of candidate control point on quadratic curve. + * """ + * _p1 = p0 + (p1 - p0) * 1.5 # <<<<<<<<<<<<<< + * _p2 = p3 + (p2 - p3) * 1.5 + * return _p1 + (_p2 - _p1) * t +*/ + __pyx_v__p1 = __Pyx_c_sum_double(__pyx_v_p0, __Pyx_c_prod_double(__Pyx_c_diff_double(__pyx_v_p1, __pyx_v_p0), __pyx_t_double_complex_from_parts(1.5, 0))); + + /* "fontTools/cu2qu/cu2qu.py":273 + * """ + * _p1 = p0 + (p1 - p0) * 1.5 + * _p2 = p3 + (p2 - p3) * 1.5 # <<<<<<<<<<<<<< + * return _p1 + (_p2 - _p1) * t + * +*/ + __pyx_v__p2 = __Pyx_c_sum_double(__pyx_v_p3, __Pyx_c_prod_double(__Pyx_c_diff_double(__pyx_v_p2, __pyx_v_p3), __pyx_t_double_complex_from_parts(1.5, 0))); + + /* "fontTools/cu2qu/cu2qu.py":274 + * _p1 = p0 + (p1 - p0) * 1.5 + * _p2 = p3 + (p2 - p3) * 1.5 + * return _p1 + (_p2 - _p1) * t # <<<<<<<<<<<<<< + * + * +*/ + { + + __pyx_r = __Pyx_c_sum_double(__pyx_v__p1, __Pyx_c_prod_double(__Pyx_c_diff_double(__pyx_v__p2, __pyx_v__p1), __pyx_t_double_complex_from_parts(__pyx_v_t, 0))); + } + goto __pyx_L0; + + /* "fontTools/cu2qu/cu2qu.py":248 + * + * + * @cython.cfunc # <<<<<<<<<<<<<< + * @cython.inline + * @cython.returns(cython.complex) +*/ + + /* function exit code */ + __pyx_L0:; + + + + return __pyx_r; +} + +/* "fontTools/cu2qu/cu2qu.py":277 + * + * + * @cython.cfunc # <<<<<<<<<<<<<< + * @cython.inline + * @cython.returns(cython.complex) +*/ + +static CYTHON_INLINE __pyx_t_double_complex __pyx_f_9fontTools_5cu2qu_5cu2qu_calc_intersect(__pyx_t_double_complex __pyx_v_a, __pyx_t_double_complex __pyx_v_b, __pyx_t_double_complex __pyx_v_c, __pyx_t_double_complex __pyx_v_d) { + __pyx_t_double_complex __pyx_v_ab; + __pyx_t_double_complex __pyx_v_cd; + __pyx_t_double_complex __pyx_v_p; + double __pyx_v_h; + __pyx_t_double_complex __pyx_r; + __Pyx_RefNannyDeclarations + PyObject *__pyx_t_1 = NULL; + PyObject *__pyx_t_2 = NULL; + PyObject *__pyx_t_3 = NULL; + double __pyx_t_4; + double __pyx_t_5; + int __pyx_t_6; + PyObject *__pyx_t_7 = NULL; + PyObject *__pyx_t_8 = NULL; + PyObject *__pyx_t_9 = NULL; + int __pyx_t_10; + int __pyx_t_11; + PyObject *__pyx_t_12 = NULL; + PyObject *__pyx_t_13 = NULL; + PyObject *__pyx_t_14 = NULL; + PyObject *__pyx_t_15 = NULL; + size_t __pyx_t_16; + __pyx_t_double_complex __pyx_t_17; + int __pyx_lineno = 0; + const char *__pyx_filename = NULL; + int __pyx_clineno = 0; + __Pyx_RefNannySetupContext("calc_intersect", 0); + + /* "fontTools/cu2qu/cu2qu.py":295 + * if no intersection was found. + * """ + * ab = b - a # <<<<<<<<<<<<<< + * cd = d - c + * p = ab * 1j +*/ + __pyx_v_ab = __Pyx_c_diff_double(__pyx_v_b, __pyx_v_a); + + /* "fontTools/cu2qu/cu2qu.py":296 + * """ + * ab = b - a + * cd = d - c # <<<<<<<<<<<<<< + * p = ab * 1j + * try: +*/ + __pyx_v_cd = __Pyx_c_diff_double(__pyx_v_d, __pyx_v_c); + + /* "fontTools/cu2qu/cu2qu.py":297 + * ab = b - a + * cd = d - c + * p = ab * 1j # <<<<<<<<<<<<<< + * try: + * h = dot(p, a - c) / dot(p, cd) +*/ + __pyx_v_p = __Pyx_c_prod_double(__pyx_v_ab, __pyx_t_double_complex_from_parts(0, 1.0)); + + /* "fontTools/cu2qu/cu2qu.py":298 + * cd = d - c + * p = ab * 1j + * try: # <<<<<<<<<<<<<< + * h = dot(p, a - c) / dot(p, cd) + * except ZeroDivisionError: +*/ + { + __Pyx_PyThreadState_declare + __Pyx_PyThreadState_assign + __Pyx_ExceptionSave(&__pyx_t_1, &__pyx_t_2, &__pyx_t_3); + __Pyx_XGOTREF(__pyx_t_1); + __Pyx_XGOTREF(__pyx_t_2); + __Pyx_XGOTREF(__pyx_t_3); + /*try:*/ { + + /* "fontTools/cu2qu/cu2qu.py":299 + * p = ab * 1j + * try: + * h = dot(p, a - c) / dot(p, cd) # <<<<<<<<<<<<<< + * except ZeroDivisionError: + * # if 3 or 4 points are equal, we do have an intersection despite the zero-div: +*/ + __pyx_t_4 = __pyx_f_9fontTools_5cu2qu_5cu2qu_dot(__pyx_v_p, __Pyx_c_diff_double(__pyx_v_a, __pyx_v_c)); if (unlikely(__pyx_t_4 == ((double)-1) && PyErr_Occurred())) __PYX_ERR(0, 299, __pyx_L3_error) + __pyx_t_5 = __pyx_f_9fontTools_5cu2qu_5cu2qu_dot(__pyx_v_p, __pyx_v_cd); if (unlikely(__pyx_t_5 == ((double)-1) && PyErr_Occurred())) __PYX_ERR(0, 299, __pyx_L3_error) + if (unlikely(__pyx_t_5 == 0)) { + PyErr_SetString(PyExc_ZeroDivisionError, "float division"); + __PYX_ERR(0, 299, __pyx_L3_error) + } + __pyx_v_h = (__pyx_t_4 / __pyx_t_5); + + + + /* "fontTools/cu2qu/cu2qu.py":298 + * cd = d - c + * p = ab * 1j + * try: # <<<<<<<<<<<<<< + * h = dot(p, a - c) / dot(p, cd) + * except ZeroDivisionError: +*/ + } + __Pyx_XDECREF(__pyx_t_1); __pyx_t_1 = 0; + __Pyx_XDECREF(__pyx_t_2); __pyx_t_2 = 0; + __Pyx_XDECREF(__pyx_t_3); __pyx_t_3 = 0; + goto __pyx_L8_try_end; + __pyx_L3_error:; + + /* "fontTools/cu2qu/cu2qu.py":300 + * try: + * h = dot(p, a - c) / dot(p, cd) + * except ZeroDivisionError: # <<<<<<<<<<<<<< + * # if 3 or 4 points are equal, we do have an intersection despite the zero-div: + * # return one of the off-curves so that the algorithm can attempt a one-curve +*/ + __pyx_t_6 = __Pyx_PyErr_ExceptionMatches(((PyObject *)(((PyTypeObject*)PyExc_ZeroDivisionError)))); + if (__pyx_t_6) { + __Pyx_AddTraceback("fontTools.cu2qu.cu2qu.calc_intersect", __pyx_clineno, __pyx_lineno, __pyx_filename); + if (__Pyx_GetException(&__pyx_t_7, &__pyx_t_8, &__pyx_t_9) < 0) __PYX_ERR(0, 300, __pyx_L5_except_error) + __Pyx_XGOTREF(__pyx_t_7); + __Pyx_XGOTREF(__pyx_t_8); + __Pyx_XGOTREF(__pyx_t_9); + + /* "fontTools/cu2qu/cu2qu.py":305 + * # solution if it's within tolerance: + * # https://github.com/linebender/kurbo/pull/484 + * if b == c and (a == b or c == d): # <<<<<<<<<<<<<< + * return b + * return complex(NAN, NAN) +*/ + __pyx_t_11 = (__Pyx_c_eq_double(__pyx_v_b, __pyx_v_c)); + + if (__pyx_t_11) { + + } else { + + __pyx_t_10 = __pyx_t_11; + + goto __pyx_L12_bool_binop_done; + } + __pyx_t_11 = (__Pyx_c_eq_double(__pyx_v_a, __pyx_v_b)); + + if (!__pyx_t_11) { + + } else { + + __pyx_t_10 = __pyx_t_11; + + goto __pyx_L12_bool_binop_done; + } + __pyx_t_11 = (__Pyx_c_eq_double(__pyx_v_c, __pyx_v_d)); + + + __pyx_t_10 = __pyx_t_11; + + __pyx_L12_bool_binop_done:; + if (__pyx_t_10) { + + + /* "fontTools/cu2qu/cu2qu.py":306 + * # https://github.com/linebender/kurbo/pull/484 + * if b == c and (a == b or c == d): + * return b # <<<<<<<<<<<<<< + * return complex(NAN, NAN) + * return c + cd * h +*/ + { + + __pyx_r = __pyx_v_b; + } + __Pyx_DECREF(__pyx_t_7); __pyx_t_7 = 0; + __Pyx_DECREF(__pyx_t_8); __pyx_t_8 = 0; + __Pyx_DECREF(__pyx_t_9); __pyx_t_9 = 0; + goto __pyx_L6_except_return; + + /* "fontTools/cu2qu/cu2qu.py":305 + * # solution if it's within tolerance: + * # https://github.com/linebender/kurbo/pull/484 + * if b == c and (a == b or c == d): # <<<<<<<<<<<<<< + * return b + * return complex(NAN, NAN) +*/ + } + + /* "fontTools/cu2qu/cu2qu.py":307 + * if b == c and (a == b or c == d): + * return b + * return complex(NAN, NAN) # <<<<<<<<<<<<<< + * return c + cd * h + * +*/ + __pyx_t_13 = NULL; + __Pyx_GetModuleGlobalName(__pyx_t_14, __pyx_mstate_global->__pyx_n_u_NAN); if (unlikely(!__pyx_t_14)) __PYX_ERR(0, 307, __pyx_L5_except_error) + __Pyx_GOTREF(__pyx_t_14); + __Pyx_GetModuleGlobalName(__pyx_t_15, __pyx_mstate_global->__pyx_n_u_NAN); if (unlikely(!__pyx_t_15)) __PYX_ERR(0, 307, __pyx_L5_except_error) + __Pyx_GOTREF(__pyx_t_15); + __pyx_t_16 = 1; + { + PyObject *__pyx_callargs[3] = {__pyx_t_13, __pyx_t_14, __pyx_t_15}; + __pyx_t_12 = __Pyx_PyObject_FastCall((PyObject*)(&PyComplex_Type), __pyx_callargs+__pyx_t_16, (3-__pyx_t_16) | (__pyx_t_16*__Pyx_PY_VECTORCALL_ARGUMENTS_OFFSET)); + __Pyx_XDECREF(__pyx_t_13); __pyx_t_13 = 0; + __Pyx_DECREF(__pyx_t_14); __pyx_t_14 = 0; + __Pyx_DECREF(__pyx_t_15); __pyx_t_15 = 0; + if (unlikely(!__pyx_t_12)) __PYX_ERR(0, 307, __pyx_L5_except_error) + __Pyx_GOTREF(__pyx_t_12); + } + __pyx_t_17 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_12); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 307, __pyx_L5_except_error) + __Pyx_DECREF(__pyx_t_12); __pyx_t_12 = 0; + { + __pyx_r = __pyx_t_17; + } + __Pyx_DECREF(__pyx_t_7); __pyx_t_7 = 0; + __Pyx_DECREF(__pyx_t_8); __pyx_t_8 = 0; + __Pyx_DECREF(__pyx_t_9); __pyx_t_9 = 0; + goto __pyx_L6_except_return; + } + goto __pyx_L5_except_error; + + /* "fontTools/cu2qu/cu2qu.py":298 + * cd = d - c + * p = ab * 1j + * try: # <<<<<<<<<<<<<< + * h = dot(p, a - c) / dot(p, cd) + * except ZeroDivisionError: +*/ + __pyx_L5_except_error:; + __Pyx_XGIVEREF(__pyx_t_1); + __Pyx_XGIVEREF(__pyx_t_2); + __Pyx_XGIVEREF(__pyx_t_3); + __Pyx_ExceptionReset(__pyx_t_1, __pyx_t_2, __pyx_t_3); + goto __pyx_L1_error; + __pyx_L6_except_return:; + __Pyx_XGIVEREF(__pyx_t_1); + __Pyx_XGIVEREF(__pyx_t_2); + __Pyx_XGIVEREF(__pyx_t_3); + __Pyx_ExceptionReset(__pyx_t_1, __pyx_t_2, __pyx_t_3); + goto __pyx_L0; + __pyx_L8_try_end:; + } + + /* "fontTools/cu2qu/cu2qu.py":308 + * return b + * return complex(NAN, NAN) + * return c + cd * h # <<<<<<<<<<<<<< + * + * +*/ + { + + __pyx_r = __Pyx_c_sum_double(__pyx_v_c, __Pyx_c_prod_double(__pyx_v_cd, __pyx_t_double_complex_from_parts(__pyx_v_h, 0))); + } + goto __pyx_L0; + + /* "fontTools/cu2qu/cu2qu.py":277 + * + * + * @cython.cfunc # <<<<<<<<<<<<<< + * @cython.inline + * @cython.returns(cython.complex) +*/ + + /* function exit code */ + __pyx_L1_error:; + __Pyx_XDECREF(__pyx_t_7); + __Pyx_XDECREF(__pyx_t_8); + __Pyx_XDECREF(__pyx_t_9); + __Pyx_XDECREF(__pyx_t_12); + __Pyx_XDECREF(__pyx_t_13); + __Pyx_XDECREF(__pyx_t_14); + __Pyx_XDECREF(__pyx_t_15); + __Pyx_AddTraceback("fontTools.cu2qu.cu2qu.calc_intersect", __pyx_clineno, __pyx_lineno, __pyx_filename); + __pyx_r = __pyx_t_double_complex_from_parts(0, 0); + __pyx_L0:; + + + + + + __Pyx_RefNannyFinishContext(); + return __pyx_r; +} + +/* "fontTools/cu2qu/cu2qu.py":311 + * + * + * @cython.cfunc # <<<<<<<<<<<<<< + * @cython.returns(cython.int) + * @cython.locals( +*/ + +static int __pyx_f_9fontTools_5cu2qu_5cu2qu_cubic_farthest_fit_inside(__pyx_t_double_complex __pyx_v_p0, __pyx_t_double_complex __pyx_v_p1, __pyx_t_double_complex __pyx_v_p2, __pyx_t_double_complex __pyx_v_p3, double __pyx_v_tolerance) { + __pyx_t_double_complex __pyx_v_mid; + __pyx_t_double_complex __pyx_v_deriv3; + int __pyx_r; + int __pyx_t_1; + int __pyx_t_2; + int __pyx_t_3; + int __pyx_t_4; + int __pyx_lineno = 0; + const char *__pyx_filename = NULL; + int __pyx_clineno = 0; + + /* "fontTools/cu2qu/cu2qu.py":340 + * """ + * # First check p2 then p1, as p2 has higher error early on. + * if abs(p2) <= tolerance and abs(p1) <= tolerance: # <<<<<<<<<<<<<< + * return True + * +*/ + __pyx_t_2 = (__Pyx_c_abs_double(__pyx_v_p2) <= __pyx_v_tolerance); + + if (__pyx_t_2) { + + } else { + + __pyx_t_1 = __pyx_t_2; + + goto __pyx_L4_bool_binop_done; + } + __pyx_t_2 = (__Pyx_c_abs_double(__pyx_v_p1) <= __pyx_v_tolerance); + + + __pyx_t_1 = __pyx_t_2; + + __pyx_L4_bool_binop_done:; + if (__pyx_t_1) { + + + /* "fontTools/cu2qu/cu2qu.py":341 + * # First check p2 then p1, as p2 has higher error early on. + * if abs(p2) <= tolerance and abs(p1) <= tolerance: + * return True # <<<<<<<<<<<<<< + * + * # Split. +*/ + { + + __pyx_r = 1; + } + goto __pyx_L0; + + /* "fontTools/cu2qu/cu2qu.py":340 + * """ + * # First check p2 then p1, as p2 has higher error early on. + * if abs(p2) <= tolerance and abs(p1) <= tolerance: # <<<<<<<<<<<<<< + * return True + * +*/ + } + + /* "fontTools/cu2qu/cu2qu.py":344 + * + * # Split. + * mid = (p0 + 3 * (p1 + p2) + p3) * 0.125 # <<<<<<<<<<<<<< + * if abs(mid) > tolerance: + * return False +*/ + __pyx_v_mid = __Pyx_c_prod_double(__Pyx_c_sum_double(__Pyx_c_sum_double(__pyx_v_p0, __Pyx_c_prod_double(__pyx_t_double_complex_from_parts(3, 0), __Pyx_c_sum_double(__pyx_v_p1, __pyx_v_p2))), __pyx_v_p3), __pyx_t_double_complex_from_parts(0.125, 0)); + + /* "fontTools/cu2qu/cu2qu.py":345 + * # Split. + * mid = (p0 + 3 * (p1 + p2) + p3) * 0.125 + * if abs(mid) > tolerance: # <<<<<<<<<<<<<< + * return False + * deriv3 = (p3 + p2 - p1 - p0) * 0.125 +*/ + __pyx_t_1 = (__Pyx_c_abs_double(__pyx_v_mid) > __pyx_v_tolerance); + + if (__pyx_t_1) { + + + /* "fontTools/cu2qu/cu2qu.py":346 + * mid = (p0 + 3 * (p1 + p2) + p3) * 0.125 + * if abs(mid) > tolerance: + * return False # <<<<<<<<<<<<<< + * deriv3 = (p3 + p2 - p1 - p0) * 0.125 + * return cubic_farthest_fit_inside( +*/ + { + + __pyx_r = 0; + } + goto __pyx_L0; + + /* "fontTools/cu2qu/cu2qu.py":345 + * # Split. + * mid = (p0 + 3 * (p1 + p2) + p3) * 0.125 + * if abs(mid) > tolerance: # <<<<<<<<<<<<<< + * return False + * deriv3 = (p3 + p2 - p1 - p0) * 0.125 +*/ + } + + /* "fontTools/cu2qu/cu2qu.py":347 + * if abs(mid) > tolerance: + * return False + * deriv3 = (p3 + p2 - p1 - p0) * 0.125 # <<<<<<<<<<<<<< + * return cubic_farthest_fit_inside( + * p0, (p0 + p1) * 0.5, mid - deriv3, mid, tolerance +*/ + __pyx_v_deriv3 = __Pyx_c_prod_double(__Pyx_c_diff_double(__Pyx_c_diff_double(__Pyx_c_sum_double(__pyx_v_p3, __pyx_v_p2), __pyx_v_p1), __pyx_v_p0), __pyx_t_double_complex_from_parts(0.125, 0)); + + /* "fontTools/cu2qu/cu2qu.py":348 + * return False + * deriv3 = (p3 + p2 - p1 - p0) * 0.125 + * return cubic_farthest_fit_inside( # <<<<<<<<<<<<<< + * p0, (p0 + p1) * 0.5, mid - deriv3, mid, tolerance + * ) and cubic_farthest_fit_inside(mid, mid + deriv3, (p2 + p3) * 0.5, p3, tolerance) +*/ + __pyx_t_4 = __pyx_f_9fontTools_5cu2qu_5cu2qu_cubic_farthest_fit_inside(__pyx_v_p0, __Pyx_c_prod_double(__Pyx_c_sum_double(__pyx_v_p0, __pyx_v_p1), __pyx_t_double_complex_from_parts(0.5, 0)), __Pyx_c_diff_double(__pyx_v_mid, __pyx_v_deriv3), __pyx_v_mid, __pyx_v_tolerance); if (unlikely(__pyx_t_4 == ((int)-1) && PyErr_Occurred())) __PYX_ERR(0, 348, __pyx_L1_error) + if (__pyx_t_4) { + + } else { + + __pyx_t_3 = __pyx_t_4; + + goto __pyx_L7_bool_binop_done; + } + + /* "fontTools/cu2qu/cu2qu.py":350 + * return cubic_farthest_fit_inside( + * p0, (p0 + p1) * 0.5, mid - deriv3, mid, tolerance + * ) and cubic_farthest_fit_inside(mid, mid + deriv3, (p2 + p3) * 0.5, p3, tolerance) # <<<<<<<<<<<<<< + * + * +*/ + __pyx_t_4 = __pyx_f_9fontTools_5cu2qu_5cu2qu_cubic_farthest_fit_inside(__pyx_v_mid, __Pyx_c_sum_double(__pyx_v_mid, __pyx_v_deriv3), __Pyx_c_prod_double(__Pyx_c_sum_double(__pyx_v_p2, __pyx_v_p3), __pyx_t_double_complex_from_parts(0.5, 0)), __pyx_v_p3, __pyx_v_tolerance); if (unlikely(__pyx_t_4 == ((int)-1) && PyErr_Occurred())) __PYX_ERR(0, 350, __pyx_L1_error) + + __pyx_t_3 = __pyx_t_4; + + __pyx_L7_bool_binop_done:; + { + __pyx_r = __pyx_t_3; + } + goto __pyx_L0; + + /* "fontTools/cu2qu/cu2qu.py":311 + * + * + * @cython.cfunc # <<<<<<<<<<<<<< + * @cython.returns(cython.int) + * @cython.locals( +*/ + + /* function exit code */ + __pyx_L1_error:; + __Pyx_AddTraceback("fontTools.cu2qu.cu2qu.cubic_farthest_fit_inside", __pyx_clineno, __pyx_lineno, __pyx_filename); + __pyx_r = -1; + __pyx_L0:; + + + + return __pyx_r; +} + +/* "fontTools/cu2qu/cu2qu.py":353 + * + * + * @cython.cfunc # <<<<<<<<<<<<<< + * @cython.inline + * @cython.locals(tolerance=cython.double) +*/ + +static CYTHON_INLINE PyObject *__pyx_f_9fontTools_5cu2qu_5cu2qu_cubic_approx_quadratic(PyObject *__pyx_v_cubic, double __pyx_v_tolerance) { + __pyx_t_double_complex __pyx_v_q1; + __pyx_t_double_complex __pyx_v_c0; + __pyx_t_double_complex __pyx_v_c1; + __pyx_t_double_complex __pyx_v_c2; + __pyx_t_double_complex __pyx_v_c3; + PyObject *__pyx_r = NULL; + __Pyx_RefNannyDeclarations + PyObject *__pyx_t_1 = NULL; + __pyx_t_double_complex __pyx_t_2; + __pyx_t_double_complex __pyx_t_3; + __pyx_t_double_complex __pyx_t_4; + __pyx_t_double_complex __pyx_t_5; + __pyx_t_double_complex __pyx_t_6; + PyObject *__pyx_t_7 = NULL; + PyObject *__pyx_t_8 = NULL; + PyObject *__pyx_t_9 = NULL; + size_t __pyx_t_10; + int __pyx_t_11; + int __pyx_t_12; + int __pyx_lineno = 0; + const char *__pyx_filename = NULL; + int __pyx_clineno = 0; + __Pyx_RefNannySetupContext("cubic_approx_quadratic", 0); + + /* "fontTools/cu2qu/cu2qu.py":377 + * """ + * + * q1 = calc_intersect(cubic[0], cubic[1], cubic[2], cubic[3]) # <<<<<<<<<<<<<< + * if math.isnan(q1.imag): + * return None +*/ + __pyx_t_1 = __Pyx_GetItemInt(__pyx_v_cubic, 0, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_FunctionArgument); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 377, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_1); + __pyx_t_2 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_1); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 377, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0; + __pyx_t_1 = __Pyx_GetItemInt(__pyx_v_cubic, 1, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_FunctionArgument); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 377, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_1); + __pyx_t_3 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_1); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 377, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0; + __pyx_t_1 = __Pyx_GetItemInt(__pyx_v_cubic, 2, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_FunctionArgument); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 377, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_1); + __pyx_t_4 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_1); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 377, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0; + __pyx_t_1 = __Pyx_GetItemInt(__pyx_v_cubic, 3, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_FunctionArgument); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 377, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_1); + __pyx_t_5 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_1); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 377, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0; + __pyx_t_6 = __pyx_f_9fontTools_5cu2qu_5cu2qu_calc_intersect(__pyx_t_2, __pyx_t_3, __pyx_t_4, __pyx_t_5); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 377, __pyx_L1_error) + + + + + __pyx_v_q1 = __pyx_t_6; + + /* "fontTools/cu2qu/cu2qu.py":378 + * + * q1 = calc_intersect(cubic[0], cubic[1], cubic[2], cubic[3]) + * if math.isnan(q1.imag): # <<<<<<<<<<<<<< + * return None + * c0 = cubic[0] +*/ + __pyx_t_7 = NULL; + __Pyx_GetModuleGlobalName(__pyx_t_8, __pyx_mstate_global->__pyx_n_u_math); if (unlikely(!__pyx_t_8)) __PYX_ERR(0, 378, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_8); + __pyx_t_9 = __Pyx_PyObject_GetAttrStr(__pyx_t_8, __pyx_mstate_global->__pyx_n_u_isnan); if (unlikely(!__pyx_t_9)) __PYX_ERR(0, 378, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_9); + __Pyx_DECREF(__pyx_t_8); __pyx_t_8 = 0; + __pyx_t_8 = PyFloat_FromDouble(__Pyx_CIMAG(__pyx_v_q1)); if (unlikely(!__pyx_t_8)) __PYX_ERR(0, 378, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_8); + __pyx_t_10 = 1; + #if CYTHON_UNPACK_METHODS + if (unlikely(PyMethod_Check(__pyx_t_9))) { + __pyx_t_7 = PyMethod_GET_SELF(__pyx_t_9); + assert(__pyx_t_7); + PyObject* __pyx__function = PyMethod_GET_FUNCTION(__pyx_t_9); + __Pyx_INCREF(__pyx_t_7); + __Pyx_INCREF(__pyx__function); + __Pyx_DECREF_SET(__pyx_t_9, __pyx__function); + __pyx_t_10 = 0; + } + #endif + { + PyObject *__pyx_callargs[2] = {__pyx_t_7, __pyx_t_8}; + __pyx_t_1 = __Pyx_PyObject_FastCall((PyObject*)__pyx_t_9, __pyx_callargs+__pyx_t_10, (2-__pyx_t_10) | (__pyx_t_10*__Pyx_PY_VECTORCALL_ARGUMENTS_OFFSET)); + __Pyx_XDECREF(__pyx_t_7); __pyx_t_7 = 0; + __Pyx_DECREF(__pyx_t_8); __pyx_t_8 = 0; + __Pyx_DECREF(__pyx_t_9); __pyx_t_9 = 0; + if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 378, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_1); + } + __pyx_t_11 = __Pyx_PyObject_IsTrue(__pyx_t_1); if (unlikely((__pyx_t_11 < 0))) __PYX_ERR(0, 378, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0; + if (__pyx_t_11) { + + + /* "fontTools/cu2qu/cu2qu.py":379 + * q1 = calc_intersect(cubic[0], cubic[1], cubic[2], cubic[3]) + * if math.isnan(q1.imag): + * return None # <<<<<<<<<<<<<< + * c0 = cubic[0] + * c3 = cubic[3] +*/ + { + PyObject *__pyx_temp; + { + __pyx_temp = __pyx_r; + __pyx_r = Py_None; __Pyx_INCREF(Py_None); + } + __Pyx_XDECREF(__pyx_temp); + } + goto __pyx_L0; + + /* "fontTools/cu2qu/cu2qu.py":378 + * + * q1 = calc_intersect(cubic[0], cubic[1], cubic[2], cubic[3]) + * if math.isnan(q1.imag): # <<<<<<<<<<<<<< + * return None + * c0 = cubic[0] +*/ + } + + /* "fontTools/cu2qu/cu2qu.py":380 + * if math.isnan(q1.imag): + * return None + * c0 = cubic[0] # <<<<<<<<<<<<<< + * c3 = cubic[3] + * c1 = c0 + (q1 - c0) * (2 / 3) +*/ + __pyx_t_1 = __Pyx_GetItemInt(__pyx_v_cubic, 0, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_FunctionArgument); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 380, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_1); + __pyx_t_6 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_1); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 380, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0; + __pyx_v_c0 = __pyx_t_6; + + /* "fontTools/cu2qu/cu2qu.py":381 + * return None + * c0 = cubic[0] + * c3 = cubic[3] # <<<<<<<<<<<<<< + * c1 = c0 + (q1 - c0) * (2 / 3) + * c2 = c3 + (q1 - c3) * (2 / 3) +*/ + __pyx_t_1 = __Pyx_GetItemInt(__pyx_v_cubic, 3, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_FunctionArgument); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 381, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_1); + __pyx_t_6 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_1); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 381, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0; + __pyx_v_c3 = __pyx_t_6; + + /* "fontTools/cu2qu/cu2qu.py":382 + * c0 = cubic[0] + * c3 = cubic[3] + * c1 = c0 + (q1 - c0) * (2 / 3) # <<<<<<<<<<<<<< + * c2 = c3 + (q1 - c3) * (2 / 3) + * if not cubic_farthest_fit_inside(0, c1 - cubic[1], c2 - cubic[2], 0, tolerance): +*/ + __pyx_v_c1 = __Pyx_c_sum_double(__pyx_v_c0, __Pyx_c_prod_double(__Pyx_c_diff_double(__pyx_v_q1, __pyx_v_c0), __pyx_t_double_complex_from_parts((2.0 / 3.0), 0))); + + /* "fontTools/cu2qu/cu2qu.py":383 + * c3 = cubic[3] + * c1 = c0 + (q1 - c0) * (2 / 3) + * c2 = c3 + (q1 - c3) * (2 / 3) # <<<<<<<<<<<<<< + * if not cubic_farthest_fit_inside(0, c1 - cubic[1], c2 - cubic[2], 0, tolerance): + * return None +*/ + __pyx_v_c2 = __Pyx_c_sum_double(__pyx_v_c3, __Pyx_c_prod_double(__Pyx_c_diff_double(__pyx_v_q1, __pyx_v_c3), __pyx_t_double_complex_from_parts((2.0 / 3.0), 0))); + + /* "fontTools/cu2qu/cu2qu.py":384 + * c1 = c0 + (q1 - c0) * (2 / 3) + * c2 = c3 + (q1 - c3) * (2 / 3) + * if not cubic_farthest_fit_inside(0, c1 - cubic[1], c2 - cubic[2], 0, tolerance): # <<<<<<<<<<<<<< + * return None + * return c0, q1, c3 +*/ + __pyx_t_1 = __pyx_PyComplex_FromComplex(__pyx_v_c1); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 384, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_1); + __pyx_t_9 = __Pyx_GetItemInt(__pyx_v_cubic, 1, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_FunctionArgument); if (unlikely(!__pyx_t_9)) __PYX_ERR(0, 384, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_9); + __pyx_t_8 = PyNumber_Subtract(__pyx_t_1, __pyx_t_9); if (unlikely(!__pyx_t_8)) __PYX_ERR(0, 384, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_8); + __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0; + __Pyx_DECREF(__pyx_t_9); __pyx_t_9 = 0; + __pyx_t_6 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_8); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 384, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_8); __pyx_t_8 = 0; + __pyx_t_8 = __pyx_PyComplex_FromComplex(__pyx_v_c2); if (unlikely(!__pyx_t_8)) __PYX_ERR(0, 384, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_8); + __pyx_t_9 = __Pyx_GetItemInt(__pyx_v_cubic, 2, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_FunctionArgument); if (unlikely(!__pyx_t_9)) __PYX_ERR(0, 384, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_9); + __pyx_t_1 = PyNumber_Subtract(__pyx_t_8, __pyx_t_9); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 384, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_1); + __Pyx_DECREF(__pyx_t_8); __pyx_t_8 = 0; + __Pyx_DECREF(__pyx_t_9); __pyx_t_9 = 0; + __pyx_t_5 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_1); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 384, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0; + __pyx_t_12 = __pyx_f_9fontTools_5cu2qu_5cu2qu_cubic_farthest_fit_inside(__pyx_t_double_complex_from_parts(0, 0), __pyx_t_6, __pyx_t_5, __pyx_t_double_complex_from_parts(0, 0), __pyx_v_tolerance); if (unlikely(__pyx_t_12 == ((int)-1) && PyErr_Occurred())) __PYX_ERR(0, 384, __pyx_L1_error) + + + __pyx_t_11 = (!(__pyx_t_12 != 0)); + + + if (__pyx_t_11) { + + + /* "fontTools/cu2qu/cu2qu.py":385 + * c2 = c3 + (q1 - c3) * (2 / 3) + * if not cubic_farthest_fit_inside(0, c1 - cubic[1], c2 - cubic[2], 0, tolerance): + * return None # <<<<<<<<<<<<<< + * return c0, q1, c3 + * +*/ + { + PyObject *__pyx_temp; + { + __pyx_temp = __pyx_r; + __pyx_r = Py_None; __Pyx_INCREF(Py_None); + } + __Pyx_XDECREF(__pyx_temp); + } + goto __pyx_L0; + + /* "fontTools/cu2qu/cu2qu.py":384 + * c1 = c0 + (q1 - c0) * (2 / 3) + * c2 = c3 + (q1 - c3) * (2 / 3) + * if not cubic_farthest_fit_inside(0, c1 - cubic[1], c2 - cubic[2], 0, tolerance): # <<<<<<<<<<<<<< + * return None + * return c0, q1, c3 +*/ + } + + /* "fontTools/cu2qu/cu2qu.py":386 + * if not cubic_farthest_fit_inside(0, c1 - cubic[1], c2 - cubic[2], 0, tolerance): + * return None + * return c0, q1, c3 # <<<<<<<<<<<<<< + * + * +*/ + __pyx_t_1 = __pyx_PyComplex_FromComplex(__pyx_v_c0); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 386, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_1); + __pyx_t_9 = __pyx_PyComplex_FromComplex(__pyx_v_q1); if (unlikely(!__pyx_t_9)) __PYX_ERR(0, 386, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_9); + __pyx_t_8 = __pyx_PyComplex_FromComplex(__pyx_v_c3); if (unlikely(!__pyx_t_8)) __PYX_ERR(0, 386, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_8); + __pyx_t_7 = PyTuple_New(3); if (unlikely(!__pyx_t_7)) __PYX_ERR(0, 386, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_7); + __Pyx_GIVEREF(__pyx_t_1); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_7, 0, __pyx_t_1) != (0)) __PYX_ERR(0, 386, __pyx_L1_error); + __Pyx_GIVEREF(__pyx_t_9); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_7, 1, __pyx_t_9) != (0)) __PYX_ERR(0, 386, __pyx_L1_error); + __Pyx_GIVEREF(__pyx_t_8); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_7, 2, __pyx_t_8) != (0)) __PYX_ERR(0, 386, __pyx_L1_error); + __pyx_t_1 = 0; + __pyx_t_9 = 0; + __pyx_t_8 = 0; + { + PyObject *__pyx_temp; + { + __pyx_temp = __pyx_r; + __pyx_r = __pyx_t_7; + } + __Pyx_XDECREF(__pyx_temp); + } + __pyx_t_7 = 0; + goto __pyx_L0; + + /* "fontTools/cu2qu/cu2qu.py":353 + * + * + * @cython.cfunc # <<<<<<<<<<<<<< + * @cython.inline + * @cython.locals(tolerance=cython.double) +*/ + + /* function exit code */ + __pyx_L1_error:; + __Pyx_XDECREF(__pyx_t_1); + __Pyx_XDECREF(__pyx_t_7); + __Pyx_XDECREF(__pyx_t_8); + __Pyx_XDECREF(__pyx_t_9); + __Pyx_AddTraceback("fontTools.cu2qu.cu2qu.cubic_approx_quadratic", __pyx_clineno, __pyx_lineno, __pyx_filename); + __pyx_r = 0; + __pyx_L0:; + + + + + + __Pyx_XGIVEREF(__pyx_r); + __Pyx_RefNannyFinishContext(); + return __pyx_r; +} + +/* "fontTools/cu2qu/cu2qu.py":389 + * + * + * @cython.cfunc # <<<<<<<<<<<<<< + * @cython.locals(n=cython.int, tolerance=cython.double) + * @cython.locals(i=cython.int) +*/ + +static PyObject *__pyx_f_9fontTools_5cu2qu_5cu2qu_cubic_approx_spline(PyObject *__pyx_v_cubic, int __pyx_v_n, double __pyx_v_tolerance, int __pyx_v_all_quadratic) { + __pyx_t_double_complex __pyx_v_q0; + __pyx_t_double_complex __pyx_v_q1; + __pyx_t_double_complex __pyx_v_next_q1; + __pyx_t_double_complex __pyx_v_q2; + __pyx_t_double_complex __pyx_v_d1; + CYTHON_UNUSED __pyx_t_double_complex __pyx_v_c0; + __pyx_t_double_complex __pyx_v_c1; + __pyx_t_double_complex __pyx_v_c2; + __pyx_t_double_complex __pyx_v_c3; + int __pyx_v_i; + PyObject *__pyx_v_cubics = NULL; + PyObject *__pyx_v_next_cubic = NULL; + PyObject *__pyx_v_spline = NULL; + __pyx_t_double_complex __pyx_v_d0; + PyObject *__pyx_r = NULL; + __Pyx_RefNannyDeclarations + int __pyx_t_1; + PyObject *__pyx_t_2 = NULL; + int __pyx_t_3; + __pyx_t_double_complex __pyx_t_4; + __pyx_t_double_complex __pyx_t_5; + __pyx_t_double_complex __pyx_t_6; + __pyx_t_double_complex __pyx_t_7; + PyObject *__pyx_t_8 = NULL; + __pyx_t_double_complex __pyx_t_9; + PyObject *__pyx_t_10 = NULL; + long __pyx_t_11; + long __pyx_t_12; + int __pyx_t_13; + PyObject *__pyx_t_14 = NULL; + PyObject *__pyx_t_15 = NULL; + PyObject *(*__pyx_t_16)(PyObject *); + long __pyx_t_17; + int __pyx_t_18; + int __pyx_t_19; + int __pyx_lineno = 0; + const char *__pyx_filename = NULL; + int __pyx_clineno = 0; + __Pyx_RefNannySetupContext("cubic_approx_spline", 0); + + /* "fontTools/cu2qu/cu2qu.py":418 + * """ + * + * if n == 1: # <<<<<<<<<<<<<< + * return cubic_approx_quadratic(cubic, tolerance) + * if n == 2 and all_quadratic == False: +*/ + __pyx_t_1 = (__pyx_v_n == 1); + + if (__pyx_t_1) { + + + /* "fontTools/cu2qu/cu2qu.py":419 + * + * if n == 1: + * return cubic_approx_quadratic(cubic, tolerance) # <<<<<<<<<<<<<< + * if n == 2 and all_quadratic == False: + * return cubic +*/ + __pyx_t_2 = __pyx_f_9fontTools_5cu2qu_5cu2qu_cubic_approx_quadratic(__pyx_v_cubic, __pyx_v_tolerance); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 419, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + { + PyObject *__pyx_temp; + { + __pyx_temp = __pyx_r; + __pyx_r = __pyx_t_2; + } + __Pyx_XDECREF(__pyx_temp); + } + __pyx_t_2 = 0; + goto __pyx_L0; + + /* "fontTools/cu2qu/cu2qu.py":418 + * """ + * + * if n == 1: # <<<<<<<<<<<<<< + * return cubic_approx_quadratic(cubic, tolerance) + * if n == 2 and all_quadratic == False: +*/ + } + + /* "fontTools/cu2qu/cu2qu.py":420 + * if n == 1: + * return cubic_approx_quadratic(cubic, tolerance) + * if n == 2 and all_quadratic == False: # <<<<<<<<<<<<<< + * return cubic + * +*/ + __pyx_t_3 = (__pyx_v_n == 2); + + if (__pyx_t_3) { + + } else { + + __pyx_t_1 = __pyx_t_3; + + goto __pyx_L5_bool_binop_done; + } + __pyx_t_3 = (__pyx_v_all_quadratic == 0); + + + __pyx_t_1 = __pyx_t_3; + + __pyx_L5_bool_binop_done:; + if (__pyx_t_1) { + + + /* "fontTools/cu2qu/cu2qu.py":421 + * return cubic_approx_quadratic(cubic, tolerance) + * if n == 2 and all_quadratic == False: + * return cubic # <<<<<<<<<<<<<< + * + * cubics = split_cubic_into_n_iter(cubic[0], cubic[1], cubic[2], cubic[3], n) +*/ + { + PyObject *__pyx_temp; + { + __pyx_temp = __pyx_r; + __Pyx_INCREF(__pyx_v_cubic); + __pyx_r = __pyx_v_cubic; + } + __Pyx_XDECREF(__pyx_temp); + } + goto __pyx_L0; + + /* "fontTools/cu2qu/cu2qu.py":420 + * if n == 1: + * return cubic_approx_quadratic(cubic, tolerance) + * if n == 2 and all_quadratic == False: # <<<<<<<<<<<<<< + * return cubic + * +*/ + } + + /* "fontTools/cu2qu/cu2qu.py":423 + * return cubic + * + * cubics = split_cubic_into_n_iter(cubic[0], cubic[1], cubic[2], cubic[3], n) # <<<<<<<<<<<<<< + * + * # calculate the spline of quadratics and check errors at the same time. +*/ + __pyx_t_2 = __Pyx_GetItemInt(__pyx_v_cubic, 0, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_FunctionArgument); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 423, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + __pyx_t_4 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_2); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 423, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0; + __pyx_t_2 = __Pyx_GetItemInt(__pyx_v_cubic, 1, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_FunctionArgument); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 423, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + __pyx_t_5 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_2); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 423, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0; + __pyx_t_2 = __Pyx_GetItemInt(__pyx_v_cubic, 2, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_FunctionArgument); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 423, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + __pyx_t_6 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_2); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 423, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0; + __pyx_t_2 = __Pyx_GetItemInt(__pyx_v_cubic, 3, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_FunctionArgument); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 423, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + __pyx_t_7 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_2); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 423, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0; + __pyx_t_2 = __Pyx_PyLong_From_int(__pyx_v_n); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 423, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + __pyx_t_8 = __pyx_f_9fontTools_5cu2qu_5cu2qu_split_cubic_into_n_iter(__pyx_t_4, __pyx_t_5, __pyx_t_6, __pyx_t_7, __pyx_t_2); if (unlikely(!__pyx_t_8)) __PYX_ERR(0, 423, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_8); + + + + + __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0; + __pyx_v_cubics = __pyx_t_8; + __pyx_t_8 = 0; + + /* "fontTools/cu2qu/cu2qu.py":426 + * + * # calculate the spline of quadratics and check errors at the same time. + * next_cubic = next(cubics) # <<<<<<<<<<<<<< + * next_q1 = cubic_approx_control( + * 0, next_cubic[0], next_cubic[1], next_cubic[2], next_cubic[3] +*/ + __pyx_t_8 = __Pyx_PyIter_Next(__pyx_v_cubics); if (unlikely(!__pyx_t_8)) __PYX_ERR(0, 426, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_8); + __pyx_v_next_cubic = __pyx_t_8; + __pyx_t_8 = 0; + + /* "fontTools/cu2qu/cu2qu.py":428 + * next_cubic = next(cubics) + * next_q1 = cubic_approx_control( + * 0, next_cubic[0], next_cubic[1], next_cubic[2], next_cubic[3] # <<<<<<<<<<<<<< + * ) + * q2 = cubic[0] +*/ + __pyx_t_8 = __Pyx_GetItemInt(__pyx_v_next_cubic, 0, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_OwnStrongReference); if (unlikely(!__pyx_t_8)) __PYX_ERR(0, 428, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_8); + __pyx_t_7 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_8); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 428, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_8); __pyx_t_8 = 0; + __pyx_t_8 = __Pyx_GetItemInt(__pyx_v_next_cubic, 1, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_OwnStrongReference); if (unlikely(!__pyx_t_8)) __PYX_ERR(0, 428, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_8); + __pyx_t_6 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_8); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 428, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_8); __pyx_t_8 = 0; + __pyx_t_8 = __Pyx_GetItemInt(__pyx_v_next_cubic, 2, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_OwnStrongReference); if (unlikely(!__pyx_t_8)) __PYX_ERR(0, 428, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_8); + __pyx_t_5 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_8); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 428, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_8); __pyx_t_8 = 0; + __pyx_t_8 = __Pyx_GetItemInt(__pyx_v_next_cubic, 3, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_OwnStrongReference); if (unlikely(!__pyx_t_8)) __PYX_ERR(0, 428, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_8); + __pyx_t_4 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_8); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 428, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_8); __pyx_t_8 = 0; + + /* "fontTools/cu2qu/cu2qu.py":427 + * # calculate the spline of quadratics and check errors at the same time. + * next_cubic = next(cubics) + * next_q1 = cubic_approx_control( # <<<<<<<<<<<<<< + * 0, next_cubic[0], next_cubic[1], next_cubic[2], next_cubic[3] + * ) +*/ + __pyx_t_9 = __pyx_f_9fontTools_5cu2qu_5cu2qu_cubic_approx_control(0.0, __pyx_t_7, __pyx_t_6, __pyx_t_5, __pyx_t_4); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 427, __pyx_L1_error) + + + + + __pyx_v_next_q1 = __pyx_t_9; + + /* "fontTools/cu2qu/cu2qu.py":430 + * 0, next_cubic[0], next_cubic[1], next_cubic[2], next_cubic[3] + * ) + * q2 = cubic[0] # <<<<<<<<<<<<<< + * d1 = 0j + * spline = [cubic[0], next_q1] +*/ + __pyx_t_8 = __Pyx_GetItemInt(__pyx_v_cubic, 0, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_FunctionArgument); if (unlikely(!__pyx_t_8)) __PYX_ERR(0, 430, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_8); + __pyx_t_9 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_8); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 430, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_8); __pyx_t_8 = 0; + __pyx_v_q2 = __pyx_t_9; + + /* "fontTools/cu2qu/cu2qu.py":431 + * ) + * q2 = cubic[0] + * d1 = 0j # <<<<<<<<<<<<<< + * spline = [cubic[0], next_q1] + * for i in range(1, n + 1): +*/ + __pyx_v_d1 = __pyx_t_double_complex_from_parts(0, 0.0); + + /* "fontTools/cu2qu/cu2qu.py":432 + * q2 = cubic[0] + * d1 = 0j + * spline = [cubic[0], next_q1] # <<<<<<<<<<<<<< + * for i in range(1, n + 1): + * # Current cubic to convert +*/ + __pyx_t_8 = __Pyx_GetItemInt(__pyx_v_cubic, 0, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_FunctionArgument); if (unlikely(!__pyx_t_8)) __PYX_ERR(0, 432, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_8); + __pyx_t_2 = __pyx_PyComplex_FromComplex(__pyx_v_next_q1); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 432, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + __pyx_t_10 = PyList_New(2); if (unlikely(!__pyx_t_10)) __PYX_ERR(0, 432, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_10); + __Pyx_GIVEREF(__pyx_t_8); + if (__Pyx_PyList_SET_ITEM(__pyx_t_10, 0, __pyx_t_8) != (0)) __PYX_ERR(0, 432, __pyx_L1_error); + __Pyx_GIVEREF(__pyx_t_2); + if (__Pyx_PyList_SET_ITEM(__pyx_t_10, 1, __pyx_t_2) != (0)) __PYX_ERR(0, 432, __pyx_L1_error); + __pyx_t_8 = 0; + __pyx_t_2 = 0; + __pyx_v_spline = ((PyObject*)__pyx_t_10); + __pyx_t_10 = 0; + + /* "fontTools/cu2qu/cu2qu.py":433 + * d1 = 0j + * spline = [cubic[0], next_q1] + * for i in range(1, n + 1): # <<<<<<<<<<<<<< + * # Current cubic to convert + * c0, c1, c2, c3 = next_cubic +*/ + + __pyx_t_11 = (__pyx_v_n + 1); + __pyx_t_12 = __pyx_t_11; + + for (__pyx_t_13 = 1; __pyx_t_13 < __pyx_t_12; __pyx_t_13+=1) { + __pyx_v_i = __pyx_t_13; + + /* "fontTools/cu2qu/cu2qu.py":435 + * for i in range(1, n + 1): + * # Current cubic to convert + * c0, c1, c2, c3 = next_cubic # <<<<<<<<<<<<<< + * + * # Current quadratic approximation of current cubic +*/ + if ((likely(PyTuple_CheckExact(__pyx_v_next_cubic))) || (PyList_CheckExact(__pyx_v_next_cubic))) { + PyObject* sequence = __pyx_v_next_cubic; + Py_ssize_t size = __Pyx_PySequence_SIZE(sequence); + if (unlikely(size != 4)) { + if (size > 4) __Pyx_RaiseTooManyValuesError(4); + else if (size >= 0) __Pyx_RaiseNeedMoreValuesError(size); + __PYX_ERR(0, 435, __pyx_L1_error) + } + #if CYTHON_ASSUME_SAFE_MACROS && !CYTHON_AVOID_BORROWED_REFS + if (likely(PyTuple_CheckExact(sequence))) { + __pyx_t_10 = PyTuple_GET_ITEM(sequence, 0); + __Pyx_INCREF(__pyx_t_10); + __pyx_t_2 = PyTuple_GET_ITEM(sequence, 1); + __Pyx_INCREF(__pyx_t_2); + __pyx_t_8 = PyTuple_GET_ITEM(sequence, 2); + __Pyx_INCREF(__pyx_t_8); + __pyx_t_14 = PyTuple_GET_ITEM(sequence, 3); + __Pyx_INCREF(__pyx_t_14); + } else { + __pyx_t_10 = __Pyx_PyList_GET_ITEM_REF(sequence, 0, __Pyx_ReferenceSharing_SharedReference); + if (unlikely(!__pyx_t_10)) __PYX_ERR(0, 435, __pyx_L1_error) + __Pyx_XGOTREF(__pyx_t_10); + __pyx_t_2 = __Pyx_PyList_GET_ITEM_REF(sequence, 1, __Pyx_ReferenceSharing_SharedReference); + if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 435, __pyx_L1_error) + __Pyx_XGOTREF(__pyx_t_2); + __pyx_t_8 = __Pyx_PyList_GET_ITEM_REF(sequence, 2, __Pyx_ReferenceSharing_SharedReference); + if (unlikely(!__pyx_t_8)) __PYX_ERR(0, 435, __pyx_L1_error) + __Pyx_XGOTREF(__pyx_t_8); + __pyx_t_14 = __Pyx_PyList_GET_ITEM_REF(sequence, 3, __Pyx_ReferenceSharing_SharedReference); + if (unlikely(!__pyx_t_14)) __PYX_ERR(0, 435, __pyx_L1_error) + __Pyx_XGOTREF(__pyx_t_14); + } + #else + { + Py_ssize_t i; + PyObject** temps[4] = {&__pyx_t_10,&__pyx_t_2,&__pyx_t_8,&__pyx_t_14}; + for (i=0; i < 4; i++) { + PyObject* item = __Pyx_PySequence_ITEM(sequence, i); if (unlikely(!item)) __PYX_ERR(0, 435, __pyx_L1_error) + __Pyx_GOTREF(item); + *(temps[i]) = item; + } + } + #endif + } else { + Py_ssize_t index = -1; + PyObject** temps[4] = {&__pyx_t_10,&__pyx_t_2,&__pyx_t_8,&__pyx_t_14}; + __pyx_t_15 = PyObject_GetIter(__pyx_v_next_cubic); if (unlikely(!__pyx_t_15)) __PYX_ERR(0, 435, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_15); + __pyx_t_16 = (CYTHON_COMPILING_IN_LIMITED_API) ? PyIter_Next : __Pyx_PyObject_GetIterNextFunc(__pyx_t_15); + for (index=0; index < 4; index++) { + PyObject* item = __pyx_t_16(__pyx_t_15); if (unlikely(!item)) goto __pyx_L9_unpacking_failed; + __Pyx_GOTREF(item); + *(temps[index]) = item; + } + if (__Pyx_IternextUnpackEndCheck(__pyx_t_16(__pyx_t_15), 4) < (0)) __PYX_ERR(0, 435, __pyx_L1_error) + __pyx_t_16 = NULL; + __Pyx_DECREF(__pyx_t_15); __pyx_t_15 = 0; + goto __pyx_L10_unpacking_done; + __pyx_L9_unpacking_failed:; + __Pyx_DECREF(__pyx_t_15); __pyx_t_15 = 0; + __pyx_t_16 = NULL; + if (__Pyx_IterFinish() == 0) __Pyx_RaiseNeedMoreValuesError(index); + __PYX_ERR(0, 435, __pyx_L1_error) + __pyx_L10_unpacking_done:; + } + __pyx_t_9 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_10); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 435, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_10); __pyx_t_10 = 0; + __pyx_t_4 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_2); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 435, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0; + __pyx_t_5 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_8); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 435, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_8); __pyx_t_8 = 0; + __pyx_t_6 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_14); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 435, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_14); __pyx_t_14 = 0; + __pyx_v_c0 = __pyx_t_9; + __pyx_v_c1 = __pyx_t_4; + __pyx_v_c2 = __pyx_t_5; + __pyx_v_c3 = __pyx_t_6; + + /* "fontTools/cu2qu/cu2qu.py":438 + * + * # Current quadratic approximation of current cubic + * q0 = q2 # <<<<<<<<<<<<<< + * q1 = next_q1 + * if i < n: +*/ + __pyx_v_q0 = __pyx_v_q2; + + /* "fontTools/cu2qu/cu2qu.py":439 + * # Current quadratic approximation of current cubic + * q0 = q2 + * q1 = next_q1 # <<<<<<<<<<<<<< + * if i < n: + * next_cubic = next(cubics) +*/ + __pyx_v_q1 = __pyx_v_next_q1; + + /* "fontTools/cu2qu/cu2qu.py":440 + * q0 = q2 + * q1 = next_q1 + * if i < n: # <<<<<<<<<<<<<< + * next_cubic = next(cubics) + * next_q1 = cubic_approx_control( +*/ + __pyx_t_1 = (__pyx_v_i < __pyx_v_n); + + if (__pyx_t_1) { + + + /* "fontTools/cu2qu/cu2qu.py":441 + * q1 = next_q1 + * if i < n: + * next_cubic = next(cubics) # <<<<<<<<<<<<<< + * next_q1 = cubic_approx_control( + * i / (n - 1), next_cubic[0], next_cubic[1], next_cubic[2], next_cubic[3] +*/ + __pyx_t_14 = __Pyx_PyIter_Next(__pyx_v_cubics); if (unlikely(!__pyx_t_14)) __PYX_ERR(0, 441, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_14); + __Pyx_DECREF_SET(__pyx_v_next_cubic, __pyx_t_14); + __pyx_t_14 = 0; + + /* "fontTools/cu2qu/cu2qu.py":443 + * next_cubic = next(cubics) + * next_q1 = cubic_approx_control( + * i / (n - 1), next_cubic[0], next_cubic[1], next_cubic[2], next_cubic[3] # <<<<<<<<<<<<<< + * ) + * spline.append(next_q1) +*/ + __pyx_t_17 = (__pyx_v_n - 1); + + if (unlikely(__pyx_t_17 == 0)) { + PyErr_SetString(PyExc_ZeroDivisionError, "float division"); + __PYX_ERR(0, 443, __pyx_L1_error) + } + __pyx_t_14 = __Pyx_GetItemInt(__pyx_v_next_cubic, 0, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_OwnStrongReference); if (unlikely(!__pyx_t_14)) __PYX_ERR(0, 443, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_14); + __pyx_t_6 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_14); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 443, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_14); __pyx_t_14 = 0; + __pyx_t_14 = __Pyx_GetItemInt(__pyx_v_next_cubic, 1, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_OwnStrongReference); if (unlikely(!__pyx_t_14)) __PYX_ERR(0, 443, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_14); + __pyx_t_5 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_14); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 443, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_14); __pyx_t_14 = 0; + __pyx_t_14 = __Pyx_GetItemInt(__pyx_v_next_cubic, 2, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_OwnStrongReference); if (unlikely(!__pyx_t_14)) __PYX_ERR(0, 443, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_14); + __pyx_t_4 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_14); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 443, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_14); __pyx_t_14 = 0; + __pyx_t_14 = __Pyx_GetItemInt(__pyx_v_next_cubic, 3, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_OwnStrongReference); if (unlikely(!__pyx_t_14)) __PYX_ERR(0, 443, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_14); + __pyx_t_9 = __Pyx_PyComplex_As___pyx_t_double_complex(__pyx_t_14); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 443, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_14); __pyx_t_14 = 0; + + /* "fontTools/cu2qu/cu2qu.py":442 + * if i < n: + * next_cubic = next(cubics) + * next_q1 = cubic_approx_control( # <<<<<<<<<<<<<< + * i / (n - 1), next_cubic[0], next_cubic[1], next_cubic[2], next_cubic[3] + * ) +*/ + __pyx_t_7 = __pyx_f_9fontTools_5cu2qu_5cu2qu_cubic_approx_control((((double)__pyx_v_i) / ((double)__pyx_t_17)), __pyx_t_6, __pyx_t_5, __pyx_t_4, __pyx_t_9); if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 442, __pyx_L1_error) + + + + + + __pyx_v_next_q1 = __pyx_t_7; + + /* "fontTools/cu2qu/cu2qu.py":445 + * i / (n - 1), next_cubic[0], next_cubic[1], next_cubic[2], next_cubic[3] + * ) + * spline.append(next_q1) # <<<<<<<<<<<<<< + * q2 = (q1 + next_q1) * 0.5 + * else: +*/ + __pyx_t_14 = __pyx_PyComplex_FromComplex(__pyx_v_next_q1); if (unlikely(!__pyx_t_14)) __PYX_ERR(0, 445, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_14); + __pyx_t_18 = __Pyx_PyList_Append(__pyx_v_spline, __pyx_t_14); if (unlikely(__pyx_t_18 == ((int)-1))) __PYX_ERR(0, 445, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_14); __pyx_t_14 = 0; + + + /* "fontTools/cu2qu/cu2qu.py":446 + * ) + * spline.append(next_q1) + * q2 = (q1 + next_q1) * 0.5 # <<<<<<<<<<<<<< + * else: + * q2 = c3 +*/ + __pyx_v_q2 = __Pyx_c_prod_double(__Pyx_c_sum_double(__pyx_v_q1, __pyx_v_next_q1), __pyx_t_double_complex_from_parts(0.5, 0)); + + /* "fontTools/cu2qu/cu2qu.py":440 + * q0 = q2 + * q1 = next_q1 + * if i < n: # <<<<<<<<<<<<<< + * next_cubic = next(cubics) + * next_q1 = cubic_approx_control( +*/ + goto __pyx_L11; + } + + /* "fontTools/cu2qu/cu2qu.py":448 + * q2 = (q1 + next_q1) * 0.5 + * else: + * q2 = c3 # <<<<<<<<<<<<<< + * + * # End-point deltas +*/ + /*else*/ { + __pyx_v_q2 = __pyx_v_c3; + } + __pyx_L11:; + + /* "fontTools/cu2qu/cu2qu.py":451 + * + * # End-point deltas + * d0 = d1 # <<<<<<<<<<<<<< + * d1 = q2 - c3 + * +*/ + __pyx_v_d0 = __pyx_v_d1; + + /* "fontTools/cu2qu/cu2qu.py":452 + * # End-point deltas + * d0 = d1 + * d1 = q2 - c3 # <<<<<<<<<<<<<< + * + * if abs(d1) > tolerance or not cubic_farthest_fit_inside( +*/ + __pyx_v_d1 = __Pyx_c_diff_double(__pyx_v_q2, __pyx_v_c3); + + /* "fontTools/cu2qu/cu2qu.py":454 + * d1 = q2 - c3 + * + * if abs(d1) > tolerance or not cubic_farthest_fit_inside( # <<<<<<<<<<<<<< + * d0, + * q0 + (q1 - q0) * (2 / 3) - c1, +*/ + __pyx_t_3 = (__Pyx_c_abs_double(__pyx_v_d1) > __pyx_v_tolerance); + + if (!__pyx_t_3) { + + } else { + + __pyx_t_1 = __pyx_t_3; + + goto __pyx_L13_bool_binop_done; + } + + /* "fontTools/cu2qu/cu2qu.py":459 + * q2 + (q1 - q2) * (2 / 3) - c2, + * d1, + * tolerance, # <<<<<<<<<<<<<< + * ): + * return None +*/ + __pyx_t_19 = __pyx_f_9fontTools_5cu2qu_5cu2qu_cubic_farthest_fit_inside(__pyx_v_d0, __Pyx_c_diff_double(__Pyx_c_sum_double(__pyx_v_q0, __Pyx_c_prod_double(__Pyx_c_diff_double(__pyx_v_q1, __pyx_v_q0), __pyx_t_double_complex_from_parts((2.0 / 3.0), 0))), __pyx_v_c1), __Pyx_c_diff_double(__Pyx_c_sum_double(__pyx_v_q2, __Pyx_c_prod_double(__Pyx_c_diff_double(__pyx_v_q1, __pyx_v_q2), __pyx_t_double_complex_from_parts((2.0 / 3.0), 0))), __pyx_v_c2), __pyx_v_d1, __pyx_v_tolerance); if (unlikely(__pyx_t_19 == ((int)-1) && PyErr_Occurred())) __PYX_ERR(0, 454, __pyx_L1_error) + + /* "fontTools/cu2qu/cu2qu.py":454 + * d1 = q2 - c3 + * + * if abs(d1) > tolerance or not cubic_farthest_fit_inside( # <<<<<<<<<<<<<< + * d0, + * q0 + (q1 - q0) * (2 / 3) - c1, +*/ + __pyx_t_3 = (!(__pyx_t_19 != 0)); + + + + __pyx_t_1 = __pyx_t_3; + + __pyx_L13_bool_binop_done:; + if (__pyx_t_1) { + + + /* "fontTools/cu2qu/cu2qu.py":461 + * tolerance, + * ): + * return None # <<<<<<<<<<<<<< + * spline.append(cubic[3]) + * +*/ + { + PyObject *__pyx_temp; + { + __pyx_temp = __pyx_r; + __pyx_r = Py_None; __Pyx_INCREF(Py_None); + } + __Pyx_XDECREF(__pyx_temp); + } + goto __pyx_L0; + + /* "fontTools/cu2qu/cu2qu.py":454 + * d1 = q2 - c3 + * + * if abs(d1) > tolerance or not cubic_farthest_fit_inside( # <<<<<<<<<<<<<< + * d0, + * q0 + (q1 - q0) * (2 / 3) - c1, +*/ + } + } + + + /* "fontTools/cu2qu/cu2qu.py":462 + * ): + * return None + * spline.append(cubic[3]) # <<<<<<<<<<<<<< + * + * return spline +*/ + __pyx_t_14 = __Pyx_GetItemInt(__pyx_v_cubic, 3, long, 1, __Pyx_PyLong_From_long, 0, 1, 1, __Pyx_ReferenceSharing_FunctionArgument); if (unlikely(!__pyx_t_14)) __PYX_ERR(0, 462, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_14); + __pyx_t_18 = __Pyx_PyList_Append(__pyx_v_spline, __pyx_t_14); if (unlikely(__pyx_t_18 == ((int)-1))) __PYX_ERR(0, 462, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_14); __pyx_t_14 = 0; + + + /* "fontTools/cu2qu/cu2qu.py":464 + * spline.append(cubic[3]) + * + * return spline # <<<<<<<<<<<<<< + * + * +*/ + { + PyObject *__pyx_temp; + { + __pyx_temp = __pyx_r; + __Pyx_INCREF(__pyx_v_spline); + __pyx_r = __pyx_v_spline; + } + __Pyx_XDECREF(__pyx_temp); + } + goto __pyx_L0; + + /* "fontTools/cu2qu/cu2qu.py":389 + * + * + * @cython.cfunc # <<<<<<<<<<<<<< + * @cython.locals(n=cython.int, tolerance=cython.double) + * @cython.locals(i=cython.int) +*/ + + /* function exit code */ + __pyx_L1_error:; + __Pyx_XDECREF(__pyx_t_2); + __Pyx_XDECREF(__pyx_t_8); + __Pyx_XDECREF(__pyx_t_10); + __Pyx_XDECREF(__pyx_t_14); + __Pyx_XDECREF(__pyx_t_15); + __Pyx_AddTraceback("fontTools.cu2qu.cu2qu.cubic_approx_spline", __pyx_clineno, __pyx_lineno, __pyx_filename); + __pyx_r = 0; + __pyx_L0:; + + + + + + + + + + + __Pyx_XDECREF(__pyx_v_cubics); + __Pyx_XDECREF(__pyx_v_next_cubic); + __Pyx_XDECREF(__pyx_v_spline); + + __Pyx_XGIVEREF(__pyx_r); + __Pyx_RefNannyFinishContext(); + return __pyx_r; +} + +/* "fontTools/cu2qu/cu2qu.py":467 + * + * + * @cython.locals(max_err=cython.double) # <<<<<<<<<<<<<< + * @cython.locals(n=cython.int) + * @cython.locals(all_quadratic=cython.int) +*/ + +/* Python wrapper */ +static PyObject *__pyx_pw_9fontTools_5cu2qu_5cu2qu_4curve_to_quadratic(PyObject *__pyx_self, +#if CYTHON_VECTORCALL +PyObject *const *__pyx_args, Py_ssize_t __pyx_nargs, PyObject *__pyx_kwds +#else +PyObject *__pyx_args, PyObject *__pyx_kwds +#endif +); /*proto*/ +PyDoc_STRVAR(__pyx_doc_9fontTools_5cu2qu_5cu2qu_3curve_to_quadratic, "curve_to_quadratic(curve, double max_err, int all_quadratic=True)\n\nApproximate a cubic Bezier curve with a spline of n quadratics.\n\nArgs:\n cubic (sequence): Four 2D tuples representing control points of\n the cubic Bezier curve.\n max_err (double): Permitted deviation from the original curve.\n all_quadratic (bool): If True (default) returned value is a\n quadratic spline. If False, it\047s either a single quadratic\n curve or a single cubic curve.\n\nReturns:\n If all_quadratic is True: A list of 2D tuples representing\n control points of the quadratic spline.\n\n If all_quadratic is False: Either a quadratic curve (if length\n of output is 3), or a cubic curve (if length of output is 4).\n\nRaises:\n fontTools.cu2qu.errors.ApproxNotFoundError: if no suitable\n approximation can be found with the given parameters."); +static PyMethodDef __pyx_mdef_9fontTools_5cu2qu_5cu2qu_4curve_to_quadratic = {"curve_to_quadratic", (PyCFunction)(void(*)(void))(__Pyx_PyCFunction_FastCallWithKeywords)__pyx_pw_9fontTools_5cu2qu_5cu2qu_4curve_to_quadratic, __Pyx_METH_FASTCALL|METH_KEYWORDS, __pyx_doc_9fontTools_5cu2qu_5cu2qu_3curve_to_quadratic}; +static PyObject *__pyx_pw_9fontTools_5cu2qu_5cu2qu_4curve_to_quadratic(PyObject *__pyx_self, +#if CYTHON_VECTORCALL +PyObject *const *__pyx_args, Py_ssize_t __pyx_nargs, PyObject *__pyx_kwds +#else +PyObject *__pyx_args, PyObject *__pyx_kwds +#endif +) { + PyObject *__pyx_v_curve = 0; + double __pyx_v_max_err; + int __pyx_v_all_quadratic; + #if !CYTHON_VECTORCALL + CYTHON_UNUSED Py_ssize_t __pyx_nargs; + #endif + CYTHON_UNUSED PyObject *const *__pyx_kwvalues; + PyObject* values[3] = {0,0,0}; + int __pyx_lineno = 0; + const char *__pyx_filename = NULL; + int __pyx_clineno = 0; + PyObject *__pyx_r = 0; + __Pyx_RefNannyDeclarations + __Pyx_RefNannySetupContext("curve_to_quadratic (wrapper)", 0); + #if !CYTHON_VECTORCALL + #if CYTHON_ASSUME_SAFE_SIZE + __pyx_nargs = PyTuple_GET_SIZE(__pyx_args); + #else + __pyx_nargs = PyTuple_Size(__pyx_args); if (unlikely(__pyx_nargs < 0)) return NULL; + #endif + #endif + __pyx_kwvalues = __Pyx_KwValues_FASTCALL(__pyx_args, __pyx_nargs); + { + PyObject ** const __pyx_pyargnames[] = {&__pyx_mstate_global->__pyx_n_u_curve,&__pyx_mstate_global->__pyx_n_u_max_err,&__pyx_mstate_global->__pyx_n_u_all_quadratic,0}; + const Py_ssize_t __pyx_kwds_len = (__pyx_kwds) ? __Pyx_NumKwargs_FASTCALL(__pyx_kwds) : 0; + if (unlikely(__pyx_kwds_len < 0)) __PYX_ERR(0, 467, __pyx_L3_error) + if (__pyx_kwds_len > 0) { + switch (__pyx_nargs) { + case 3: + values[2] = __Pyx_ArgRef_FASTCALL(__pyx_args, 2); + if (!CYTHON_ASSUME_SAFE_MACROS && unlikely(!values[2])) __PYX_ERR(0, 467, __pyx_L3_error) + CYTHON_FALLTHROUGH; + case 2: + values[1] = __Pyx_ArgRef_FASTCALL(__pyx_args, 1); + if (!CYTHON_ASSUME_SAFE_MACROS && unlikely(!values[1])) __PYX_ERR(0, 467, __pyx_L3_error) + CYTHON_FALLTHROUGH; + case 1: + values[0] = __Pyx_ArgRef_FASTCALL(__pyx_args, 0); + if (!CYTHON_ASSUME_SAFE_MACROS && unlikely(!values[0])) __PYX_ERR(0, 467, __pyx_L3_error) + CYTHON_FALLTHROUGH; + case 0: break; + default: goto __pyx_L5_argtuple_error; + } + const Py_ssize_t kwd_pos_args = __pyx_nargs; + if (__Pyx_ParseKeywords(__pyx_kwds, __pyx_kwvalues, __pyx_pyargnames, 0, values, kwd_pos_args, __pyx_kwds_len, "curve_to_quadratic", 0) < (0)) __PYX_ERR(0, 467, __pyx_L3_error) + for (Py_ssize_t i = __pyx_nargs; i < 2; i++) { + if (unlikely(!values[i])) { __Pyx_RaiseArgtupleInvalid("curve_to_quadratic", 0, 2, 3, i); __PYX_ERR(0, 467, __pyx_L3_error) } + } + } else { + switch (__pyx_nargs) { + case 3: + values[2] = __Pyx_ArgRef_FASTCALL(__pyx_args, 2); + if (!CYTHON_ASSUME_SAFE_MACROS && unlikely(!values[2])) __PYX_ERR(0, 467, __pyx_L3_error) + CYTHON_FALLTHROUGH; + case 2: + values[1] = __Pyx_ArgRef_FASTCALL(__pyx_args, 1); + if (!CYTHON_ASSUME_SAFE_MACROS && unlikely(!values[1])) __PYX_ERR(0, 467, __pyx_L3_error) + values[0] = __Pyx_ArgRef_FASTCALL(__pyx_args, 0); + if (!CYTHON_ASSUME_SAFE_MACROS && unlikely(!values[0])) __PYX_ERR(0, 467, __pyx_L3_error) + break; + default: goto __pyx_L5_argtuple_error; + } + } + __pyx_v_curve = values[0]; + __pyx_v_max_err = __Pyx_PyFloat_AsDouble(values[1]); if (unlikely((__pyx_v_max_err == (double)-1) && PyErr_Occurred())) __PYX_ERR(0, 470, __pyx_L3_error) + if (values[2]) { + __pyx_v_all_quadratic = __Pyx_PyLong_As_int(values[2]); if (unlikely((__pyx_v_all_quadratic == (int)-1) && PyErr_Occurred())) __PYX_ERR(0, 470, __pyx_L3_error) + } else { + + /* "fontTools/cu2qu/cu2qu.py":470 + * @cython.locals(n=cython.int) + * @cython.locals(all_quadratic=cython.int) + * def curve_to_quadratic(curve, max_err, all_quadratic=True): # <<<<<<<<<<<<<< + * """Approximate a cubic Bezier curve with a spline of n quadratics. + * +*/ + __pyx_v_all_quadratic = ((int)((int)1)); + } + } + goto __pyx_L6_skip; + __pyx_L5_argtuple_error:; + __Pyx_RaiseArgtupleInvalid("curve_to_quadratic", 0, 2, 3, __pyx_nargs); __PYX_ERR(0, 467, __pyx_L3_error) + __pyx_L6_skip:; + goto __pyx_L4_argument_unpacking_done; + __pyx_L3_error:; + for (Py_ssize_t __pyx_temp=0; __pyx_temp < (Py_ssize_t)(sizeof(values)/sizeof(values[0])); ++__pyx_temp) { + Py_XDECREF(values[__pyx_temp]); + } + __Pyx_AddTraceback("fontTools.cu2qu.cu2qu.curve_to_quadratic", __pyx_clineno, __pyx_lineno, __pyx_filename); + __Pyx_RefNannyFinishContext(); + return NULL; + __pyx_L4_argument_unpacking_done:; + __pyx_r = __pyx_pf_9fontTools_5cu2qu_5cu2qu_3curve_to_quadratic(__pyx_self, __pyx_v_curve, __pyx_v_max_err, __pyx_v_all_quadratic); + + /* "fontTools/cu2qu/cu2qu.py":467 + * + * + * @cython.locals(max_err=cython.double) # <<<<<<<<<<<<<< + * @cython.locals(n=cython.int) + * @cython.locals(all_quadratic=cython.int) +*/ + + /* function exit code */ + for (Py_ssize_t __pyx_temp=0; __pyx_temp < (Py_ssize_t)(sizeof(values)/sizeof(values[0])); ++__pyx_temp) { + Py_XDECREF(values[__pyx_temp]); + } + + + __Pyx_RefNannyFinishContext(); + return __pyx_r; +} + +static PyObject *__pyx_pf_9fontTools_5cu2qu_5cu2qu_3curve_to_quadratic(CYTHON_UNUSED PyObject *__pyx_self, PyObject *__pyx_v_curve, double __pyx_v_max_err, int __pyx_v_all_quadratic) { + int __pyx_v_n; + PyObject *__pyx_v_spline = NULL; + PyObject *__pyx_7genexpr__pyx_v_p = NULL; + PyObject *__pyx_8genexpr1__pyx_v_s = NULL; + PyObject *__pyx_r = NULL; + __Pyx_RefNannyDeclarations + PyObject *__pyx_t_1 = NULL; + PyObject *__pyx_t_2 = NULL; + Py_ssize_t __pyx_t_3; + PyObject *(*__pyx_t_4)(PyObject *); + PyObject *__pyx_t_5 = NULL; + PyObject *__pyx_t_6 = NULL; + Py_ssize_t __pyx_t_7; + int __pyx_t_8; + int __pyx_t_9; + Py_ssize_t __pyx_t_10; + PyObject *__pyx_t_11 = NULL; + size_t __pyx_t_12; + int __pyx_lineno = 0; + const char *__pyx_filename = NULL; + int __pyx_clineno = 0; + __Pyx_RefNannySetupContext("curve_to_quadratic", 0); + __Pyx_INCREF(__pyx_v_curve); + + /* "fontTools/cu2qu/cu2qu.py":493 + * """ + * + * curve = [complex(*p) for p in curve] # <<<<<<<<<<<<<< + * + * for n in range(1, MAX_N + 1): +*/ + { /* enter inner scope */ + __pyx_t_1 = PyList_New(0); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 493, __pyx_L5_error) + __Pyx_GOTREF(__pyx_t_1); + if (likely(PyList_CheckExact(__pyx_v_curve)) || PyTuple_CheckExact(__pyx_v_curve)) { + __pyx_t_2 = __pyx_v_curve; __Pyx_INCREF(__pyx_t_2); + __pyx_t_3 = 0; + __pyx_t_4 = NULL; + } else { + __pyx_t_3 = -1; __pyx_t_2 = PyObject_GetIter(__pyx_v_curve); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 493, __pyx_L5_error) + __Pyx_GOTREF(__pyx_t_2); + __pyx_t_4 = (CYTHON_COMPILING_IN_LIMITED_API) ? PyIter_Next : __Pyx_PyObject_GetIterNextFunc(__pyx_t_2); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 493, __pyx_L5_error) + } + for (;;) { + if (likely(!__pyx_t_4)) { + if (likely(PyList_CheckExact(__pyx_t_2))) { + { + Py_ssize_t __pyx_temp = __Pyx_PyList_GET_SIZE(__pyx_t_2); + #if !CYTHON_ASSUME_SAFE_SIZE + if (unlikely((__pyx_temp < 0))) __PYX_ERR(0, 493, __pyx_L5_error) + #endif + if (__pyx_t_3 >= __pyx_temp) break; + } + __pyx_t_5 = __Pyx_PyList_GET_ITEM_REF(__pyx_t_2, __pyx_t_3, __Pyx_ReferenceSharing_OwnStrongReference); + ++__pyx_t_3; + } else { + { + Py_ssize_t __pyx_temp = __Pyx_PyTuple_GET_SIZE(__pyx_t_2); + #if !CYTHON_ASSUME_SAFE_SIZE + if (unlikely((__pyx_temp < 0))) __PYX_ERR(0, 493, __pyx_L5_error) + #endif + if (__pyx_t_3 >= __pyx_temp) break; + } + #if CYTHON_ASSUME_SAFE_MACROS && !CYTHON_AVOID_BORROWED_REFS + __pyx_t_5 = __Pyx_NewRef(PyTuple_GET_ITEM(__pyx_t_2, __pyx_t_3)); + #else + __pyx_t_5 = __Pyx_PySequence_ITEM(__pyx_t_2, __pyx_t_3); + #endif + ++__pyx_t_3; + } + if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 493, __pyx_L5_error) + } else { + __pyx_t_5 = __pyx_t_4(__pyx_t_2); + if (unlikely(!__pyx_t_5)) { + PyObject* exc_type = PyErr_Occurred(); + if (exc_type) { + if (unlikely(!__Pyx_PyErr_GivenExceptionMatches(exc_type, PyExc_StopIteration))) __PYX_ERR(0, 493, __pyx_L5_error) + PyErr_Clear(); + } + break; + } + } + __Pyx_GOTREF(__pyx_t_5); + __Pyx_XDECREF_SET(__pyx_7genexpr__pyx_v_p, __pyx_t_5); + __pyx_t_5 = 0; + __pyx_t_5 = __Pyx_PySequence_Tuple(__pyx_7genexpr__pyx_v_p); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 493, __pyx_L5_error) + __Pyx_GOTREF(__pyx_t_5); + __pyx_t_6 = __Pyx_PyObject_Call(((PyObject *)(&PyComplex_Type)), __pyx_t_5, NULL); if (unlikely(!__pyx_t_6)) __PYX_ERR(0, 493, __pyx_L5_error) + __Pyx_GOTREF(__pyx_t_6); + __Pyx_DECREF(__pyx_t_5); __pyx_t_5 = 0; + __Pyx_GIVEREF(__pyx_t_6); + if (unlikely(__Pyx_ListComp_AppendAndDecref(__pyx_t_1, __pyx_t_6))) __PYX_ERR(0, 493, __pyx_L5_error) + __pyx_t_6 = 0; + } + __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0; + __Pyx_XDECREF(__pyx_7genexpr__pyx_v_p); __pyx_7genexpr__pyx_v_p = 0; + goto __pyx_L9_exit_scope; + __pyx_L5_error:; + __Pyx_XDECREF(__pyx_7genexpr__pyx_v_p); __pyx_7genexpr__pyx_v_p = 0; + goto __pyx_L1_error; + __pyx_L9_exit_scope:; + } /* exit inner scope */ + __Pyx_DECREF_SET(__pyx_v_curve, __pyx_t_1); + __pyx_t_1 = 0; + + /* "fontTools/cu2qu/cu2qu.py":495 + * curve = [complex(*p) for p in curve] + * + * for n in range(1, MAX_N + 1): # <<<<<<<<<<<<<< + * spline = cubic_approx_spline(curve, n, max_err, all_quadratic) + * if spline is not None: +*/ + __Pyx_GetModuleGlobalName(__pyx_t_1, __pyx_mstate_global->__pyx_n_u_MAX_N); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 495, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_1); + __pyx_t_2 = __Pyx_PyLong_AddObjC(__pyx_t_1, __pyx_mstate_global->__pyx_int_1, 1, 0, 0); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 495, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0; + __pyx_t_3 = __Pyx_PyIndex_AsSsize_t(__pyx_t_2); if (unlikely((__pyx_t_3 == (Py_ssize_t)-1) && PyErr_Occurred())) __PYX_ERR(0, 495, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0; + __pyx_t_7 = __pyx_t_3; + + for (__pyx_t_8 = 1; __pyx_t_8 < __pyx_t_7; __pyx_t_8+=1) { + __pyx_v_n = __pyx_t_8; + + /* "fontTools/cu2qu/cu2qu.py":496 + * + * for n in range(1, MAX_N + 1): + * spline = cubic_approx_spline(curve, n, max_err, all_quadratic) # <<<<<<<<<<<<<< + * if spline is not None: + * # done. go home +*/ + __pyx_t_2 = __pyx_f_9fontTools_5cu2qu_5cu2qu_cubic_approx_spline(__pyx_v_curve, __pyx_v_n, __pyx_v_max_err, __pyx_v_all_quadratic); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 496, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + __Pyx_XDECREF_SET(__pyx_v_spline, __pyx_t_2); + __pyx_t_2 = 0; + + /* "fontTools/cu2qu/cu2qu.py":497 + * for n in range(1, MAX_N + 1): + * spline = cubic_approx_spline(curve, n, max_err, all_quadratic) + * if spline is not None: # <<<<<<<<<<<<<< + * # done. go home + * return [(s.real, s.imag) for s in spline] +*/ + __pyx_t_9 = (__pyx_v_spline != Py_None); + if (__pyx_t_9) { + + + /* "fontTools/cu2qu/cu2qu.py":499 + * if spline is not None: + * # done. go home + * return [(s.real, s.imag) for s in spline] # <<<<<<<<<<<<<< + * + * raise ApproxNotFoundError(curve) +*/ + { /* enter inner scope */ + __pyx_t_2 = PyList_New(0); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 499, __pyx_L15_error) + __Pyx_GOTREF(__pyx_t_2); + if (likely(PyList_CheckExact(__pyx_v_spline)) || PyTuple_CheckExact(__pyx_v_spline)) { + __pyx_t_1 = __pyx_v_spline; __Pyx_INCREF(__pyx_t_1); + __pyx_t_10 = 0; + __pyx_t_4 = NULL; + } else { + __pyx_t_10 = -1; __pyx_t_1 = PyObject_GetIter(__pyx_v_spline); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 499, __pyx_L15_error) + __Pyx_GOTREF(__pyx_t_1); + __pyx_t_4 = (CYTHON_COMPILING_IN_LIMITED_API) ? PyIter_Next : __Pyx_PyObject_GetIterNextFunc(__pyx_t_1); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 499, __pyx_L15_error) + } + for (;;) { + if (likely(!__pyx_t_4)) { + if (likely(PyList_CheckExact(__pyx_t_1))) { + { + Py_ssize_t __pyx_temp = __Pyx_PyList_GET_SIZE(__pyx_t_1); + #if !CYTHON_ASSUME_SAFE_SIZE + if (unlikely((__pyx_temp < 0))) __PYX_ERR(0, 499, __pyx_L15_error) + #endif + if (__pyx_t_10 >= __pyx_temp) break; + } + __pyx_t_6 = __Pyx_PyList_GET_ITEM_REF(__pyx_t_1, __pyx_t_10, __Pyx_ReferenceSharing_OwnStrongReference); + ++__pyx_t_10; + } else { + { + Py_ssize_t __pyx_temp = __Pyx_PyTuple_GET_SIZE(__pyx_t_1); + #if !CYTHON_ASSUME_SAFE_SIZE + if (unlikely((__pyx_temp < 0))) __PYX_ERR(0, 499, __pyx_L15_error) + #endif + if (__pyx_t_10 >= __pyx_temp) break; + } + #if CYTHON_ASSUME_SAFE_MACROS && !CYTHON_AVOID_BORROWED_REFS + __pyx_t_6 = __Pyx_NewRef(PyTuple_GET_ITEM(__pyx_t_1, __pyx_t_10)); + #else + __pyx_t_6 = __Pyx_PySequence_ITEM(__pyx_t_1, __pyx_t_10); + #endif + ++__pyx_t_10; + } + if (unlikely(!__pyx_t_6)) __PYX_ERR(0, 499, __pyx_L15_error) + } else { + __pyx_t_6 = __pyx_t_4(__pyx_t_1); + if (unlikely(!__pyx_t_6)) { + PyObject* exc_type = PyErr_Occurred(); + if (exc_type) { + if (unlikely(!__Pyx_PyErr_GivenExceptionMatches(exc_type, PyExc_StopIteration))) __PYX_ERR(0, 499, __pyx_L15_error) + PyErr_Clear(); + } + break; + } + } + __Pyx_GOTREF(__pyx_t_6); + __Pyx_XDECREF_SET(__pyx_8genexpr1__pyx_v_s, __pyx_t_6); + __pyx_t_6 = 0; + __pyx_t_6 = __Pyx_PyObject_GetAttrStr(__pyx_8genexpr1__pyx_v_s, __pyx_mstate_global->__pyx_n_u_real); if (unlikely(!__pyx_t_6)) __PYX_ERR(0, 499, __pyx_L15_error) + __Pyx_GOTREF(__pyx_t_6); + __pyx_t_5 = __Pyx_PyObject_GetAttrStr(__pyx_8genexpr1__pyx_v_s, __pyx_mstate_global->__pyx_n_u_imag); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 499, __pyx_L15_error) + __Pyx_GOTREF(__pyx_t_5); + __pyx_t_11 = PyTuple_New(2); if (unlikely(!__pyx_t_11)) __PYX_ERR(0, 499, __pyx_L15_error) + __Pyx_GOTREF(__pyx_t_11); + __Pyx_GIVEREF(__pyx_t_6); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_11, 0, __pyx_t_6) != (0)) __PYX_ERR(0, 499, __pyx_L15_error); + __Pyx_GIVEREF(__pyx_t_5); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_11, 1, __pyx_t_5) != (0)) __PYX_ERR(0, 499, __pyx_L15_error); + __pyx_t_6 = 0; + __pyx_t_5 = 0; + __Pyx_GIVEREF(__pyx_t_11); + if (unlikely(__Pyx_ListComp_AppendAndDecref(__pyx_t_2, __pyx_t_11))) __PYX_ERR(0, 499, __pyx_L15_error) + __pyx_t_11 = 0; + } + __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0; + __Pyx_XDECREF(__pyx_8genexpr1__pyx_v_s); __pyx_8genexpr1__pyx_v_s = 0; + goto __pyx_L19_exit_scope; + __pyx_L15_error:; + __Pyx_XDECREF(__pyx_8genexpr1__pyx_v_s); __pyx_8genexpr1__pyx_v_s = 0; + goto __pyx_L1_error; + __pyx_L19_exit_scope:; + } /* exit inner scope */ + { + PyObject *__pyx_temp; + { + __pyx_temp = __pyx_r; + __pyx_r = __pyx_t_2; + } + __Pyx_XDECREF(__pyx_temp); + } + __pyx_t_2 = 0; + goto __pyx_L0; + + /* "fontTools/cu2qu/cu2qu.py":497 + * for n in range(1, MAX_N + 1): + * spline = cubic_approx_spline(curve, n, max_err, all_quadratic) + * if spline is not None: # <<<<<<<<<<<<<< + * # done. go home + * return [(s.real, s.imag) for s in spline] +*/ + } + } + + + + /* "fontTools/cu2qu/cu2qu.py":501 + * return [(s.real, s.imag) for s in spline] + * + * raise ApproxNotFoundError(curve) # <<<<<<<<<<<<<< + * + * +*/ + __pyx_t_1 = NULL; + __Pyx_GetModuleGlobalName(__pyx_t_11, __pyx_mstate_global->__pyx_n_u_ApproxNotFoundError); if (unlikely(!__pyx_t_11)) __PYX_ERR(0, 501, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_11); + __pyx_t_12 = 1; + #if CYTHON_UNPACK_METHODS + if (unlikely(PyMethod_Check(__pyx_t_11))) { + __pyx_t_1 = PyMethod_GET_SELF(__pyx_t_11); + assert(__pyx_t_1); + PyObject* __pyx__function = PyMethod_GET_FUNCTION(__pyx_t_11); + __Pyx_INCREF(__pyx_t_1); + __Pyx_INCREF(__pyx__function); + __Pyx_DECREF_SET(__pyx_t_11, __pyx__function); + __pyx_t_12 = 0; + } + #endif + { + PyObject *__pyx_callargs[2] = {__pyx_t_1, __pyx_v_curve}; + __pyx_t_2 = __Pyx_PyObject_FastCall((PyObject*)__pyx_t_11, __pyx_callargs+__pyx_t_12, (2-__pyx_t_12) | (__pyx_t_12*__Pyx_PY_VECTORCALL_ARGUMENTS_OFFSET)); + __Pyx_XDECREF(__pyx_t_1); __pyx_t_1 = 0; + __Pyx_DECREF(__pyx_t_11); __pyx_t_11 = 0; + if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 501, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + } + __Pyx_Raise(__pyx_t_2, 0, 0, 0); + __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0; + __PYX_ERR(0, 501, __pyx_L1_error) + + /* "fontTools/cu2qu/cu2qu.py":467 + * + * + * @cython.locals(max_err=cython.double) # <<<<<<<<<<<<<< + * @cython.locals(n=cython.int) + * @cython.locals(all_quadratic=cython.int) +*/ + + /* function exit code */ + __pyx_L1_error:; + __Pyx_XDECREF(__pyx_t_1); + __Pyx_XDECREF(__pyx_t_2); + __Pyx_XDECREF(__pyx_t_5); + __Pyx_XDECREF(__pyx_t_6); + __Pyx_XDECREF(__pyx_t_11); + __Pyx_AddTraceback("fontTools.cu2qu.cu2qu.curve_to_quadratic", __pyx_clineno, __pyx_lineno, __pyx_filename); + __pyx_r = NULL; + __pyx_L0:; + + __Pyx_XDECREF(__pyx_v_spline); + __Pyx_XDECREF(__pyx_7genexpr__pyx_v_p); + __Pyx_XDECREF(__pyx_8genexpr1__pyx_v_s); + __Pyx_XDECREF(__pyx_v_curve); + __Pyx_XGIVEREF(__pyx_r); + __Pyx_RefNannyFinishContext(); + return __pyx_r; +} + +/* "fontTools/cu2qu/cu2qu.py":504 + * + * + * @cython.locals(l=cython.int, last_i=cython.int, i=cython.int) # <<<<<<<<<<<<<< + * @cython.locals(all_quadratic=cython.int) + * def curves_to_quadratic(curves, max_errors, all_quadratic=True): +*/ + +/* Python wrapper */ +static PyObject *__pyx_pw_9fontTools_5cu2qu_5cu2qu_6curves_to_quadratic(PyObject *__pyx_self, +#if CYTHON_VECTORCALL +PyObject *const *__pyx_args, Py_ssize_t __pyx_nargs, PyObject *__pyx_kwds +#else +PyObject *__pyx_args, PyObject *__pyx_kwds +#endif +); /*proto*/ +PyDoc_STRVAR(__pyx_doc_9fontTools_5cu2qu_5cu2qu_5curves_to_quadratic, "curves_to_quadratic(curves, max_errors, int all_quadratic=True)\n\nReturn quadratic Bezier splines approximating the input cubic Beziers.\n\nArgs:\n curves: A sequence of *n* curves, each curve being a sequence of four\n 2D tuples.\n max_errors: A sequence of *n* floats representing the maximum permissible\n deviation from each of the cubic Bezier curves.\n all_quadratic (bool): If True (default) returned values are a\n quadratic spline. If False, they are either a single quadratic\n curve or a single cubic curve.\n\nExample::\n\n >>> curves_to_quadratic( [\n ... [ (50,50), (100,100), (150,100), (200,50) ],\n ... [ (75,50), (120,100), (150,75), (200,60) ]\n ... ], [1,1] )\n [[(50.0, 50.0), (75.0, 75.0), (125.0, 91.66666666666666), (175.0, 75.0), (200.0, 50.0)], [(75.0, 50.0), (97.5, 75.0), (135.41666666666666, 82.08333333333333), (175.0, 67.5), (200.0, 60.0)]]\n\nThe returned splines have \"implied oncurve points\" suitable for use in\nTrueType ``glif`` outlines - i.e. in the first spline returned above,\nthe first quadratic segment runs from (50,50) to\n( (75 + 125)/2 , (120 + 91.666..)/2 ) = (100, 83.333...).\n\nReturns:\n If all_quadratic is True, a list of splines, each spline being a list\n of 2D tuples. If ``curves`` is empty, returns an empty list.\n\n If all_quadratic is False, a list of curves, each curve being a quadratic\n (length 3), or cubic (length 4).\n\nRaises:\n ValueError: if ``max_errors`` does not match the number of curves.\n fontTools.cu2qu.errors.ApproxNotFoundError: if no suitable approximation\n can be found for all curves with the given parameters."); +static PyMethodDef __pyx_mdef_9fontTools_5cu2qu_5cu2qu_6curves_to_quadratic = {"curves_to_quadratic", (PyCFunction)(void(*)(void))(__Pyx_PyCFunction_FastCallWithKeywords)__pyx_pw_9fontTools_5cu2qu_5cu2qu_6curves_to_quadratic, __Pyx_METH_FASTCALL|METH_KEYWORDS, __pyx_doc_9fontTools_5cu2qu_5cu2qu_5curves_to_quadratic}; +static PyObject *__pyx_pw_9fontTools_5cu2qu_5cu2qu_6curves_to_quadratic(PyObject *__pyx_self, +#if CYTHON_VECTORCALL +PyObject *const *__pyx_args, Py_ssize_t __pyx_nargs, PyObject *__pyx_kwds +#else +PyObject *__pyx_args, PyObject *__pyx_kwds +#endif +) { + PyObject *__pyx_v_curves = 0; + PyObject *__pyx_v_max_errors = 0; + int __pyx_v_all_quadratic; + #if !CYTHON_VECTORCALL + CYTHON_UNUSED Py_ssize_t __pyx_nargs; + #endif + CYTHON_UNUSED PyObject *const *__pyx_kwvalues; + PyObject* values[3] = {0,0,0}; + int __pyx_lineno = 0; + const char *__pyx_filename = NULL; + int __pyx_clineno = 0; + PyObject *__pyx_r = 0; + __Pyx_RefNannyDeclarations + __Pyx_RefNannySetupContext("curves_to_quadratic (wrapper)", 0); + #if !CYTHON_VECTORCALL + #if CYTHON_ASSUME_SAFE_SIZE + __pyx_nargs = PyTuple_GET_SIZE(__pyx_args); + #else + __pyx_nargs = PyTuple_Size(__pyx_args); if (unlikely(__pyx_nargs < 0)) return NULL; + #endif + #endif + __pyx_kwvalues = __Pyx_KwValues_FASTCALL(__pyx_args, __pyx_nargs); + { + PyObject ** const __pyx_pyargnames[] = {&__pyx_mstate_global->__pyx_n_u_curves,&__pyx_mstate_global->__pyx_n_u_max_errors,&__pyx_mstate_global->__pyx_n_u_all_quadratic,0}; + const Py_ssize_t __pyx_kwds_len = (__pyx_kwds) ? __Pyx_NumKwargs_FASTCALL(__pyx_kwds) : 0; + if (unlikely(__pyx_kwds_len < 0)) __PYX_ERR(0, 504, __pyx_L3_error) + if (__pyx_kwds_len > 0) { + switch (__pyx_nargs) { + case 3: + values[2] = __Pyx_ArgRef_FASTCALL(__pyx_args, 2); + if (!CYTHON_ASSUME_SAFE_MACROS && unlikely(!values[2])) __PYX_ERR(0, 504, __pyx_L3_error) + CYTHON_FALLTHROUGH; + case 2: + values[1] = __Pyx_ArgRef_FASTCALL(__pyx_args, 1); + if (!CYTHON_ASSUME_SAFE_MACROS && unlikely(!values[1])) __PYX_ERR(0, 504, __pyx_L3_error) + CYTHON_FALLTHROUGH; + case 1: + values[0] = __Pyx_ArgRef_FASTCALL(__pyx_args, 0); + if (!CYTHON_ASSUME_SAFE_MACROS && unlikely(!values[0])) __PYX_ERR(0, 504, __pyx_L3_error) + CYTHON_FALLTHROUGH; + case 0: break; + default: goto __pyx_L5_argtuple_error; + } + const Py_ssize_t kwd_pos_args = __pyx_nargs; + if (__Pyx_ParseKeywords(__pyx_kwds, __pyx_kwvalues, __pyx_pyargnames, 0, values, kwd_pos_args, __pyx_kwds_len, "curves_to_quadratic", 0) < (0)) __PYX_ERR(0, 504, __pyx_L3_error) + for (Py_ssize_t i = __pyx_nargs; i < 2; i++) { + if (unlikely(!values[i])) { __Pyx_RaiseArgtupleInvalid("curves_to_quadratic", 0, 2, 3, i); __PYX_ERR(0, 504, __pyx_L3_error) } + } + } else { + switch (__pyx_nargs) { + case 3: + values[2] = __Pyx_ArgRef_FASTCALL(__pyx_args, 2); + if (!CYTHON_ASSUME_SAFE_MACROS && unlikely(!values[2])) __PYX_ERR(0, 504, __pyx_L3_error) + CYTHON_FALLTHROUGH; + case 2: + values[1] = __Pyx_ArgRef_FASTCALL(__pyx_args, 1); + if (!CYTHON_ASSUME_SAFE_MACROS && unlikely(!values[1])) __PYX_ERR(0, 504, __pyx_L3_error) + values[0] = __Pyx_ArgRef_FASTCALL(__pyx_args, 0); + if (!CYTHON_ASSUME_SAFE_MACROS && unlikely(!values[0])) __PYX_ERR(0, 504, __pyx_L3_error) + break; + default: goto __pyx_L5_argtuple_error; + } + } + __pyx_v_curves = values[0]; + __pyx_v_max_errors = values[1]; + if (values[2]) { + __pyx_v_all_quadratic = __Pyx_PyLong_As_int(values[2]); if (unlikely((__pyx_v_all_quadratic == (int)-1) && PyErr_Occurred())) __PYX_ERR(0, 506, __pyx_L3_error) + } else { + + /* "fontTools/cu2qu/cu2qu.py":506 + * @cython.locals(l=cython.int, last_i=cython.int, i=cython.int) + * @cython.locals(all_quadratic=cython.int) + * def curves_to_quadratic(curves, max_errors, all_quadratic=True): # <<<<<<<<<<<<<< + * """Return quadratic Bezier splines approximating the input cubic Beziers. + * +*/ + __pyx_v_all_quadratic = ((int)((int)1)); + } + } + goto __pyx_L6_skip; + __pyx_L5_argtuple_error:; + __Pyx_RaiseArgtupleInvalid("curves_to_quadratic", 0, 2, 3, __pyx_nargs); __PYX_ERR(0, 504, __pyx_L3_error) + __pyx_L6_skip:; + goto __pyx_L4_argument_unpacking_done; + __pyx_L3_error:; + for (Py_ssize_t __pyx_temp=0; __pyx_temp < (Py_ssize_t)(sizeof(values)/sizeof(values[0])); ++__pyx_temp) { + Py_XDECREF(values[__pyx_temp]); + } + __Pyx_AddTraceback("fontTools.cu2qu.cu2qu.curves_to_quadratic", __pyx_clineno, __pyx_lineno, __pyx_filename); + __Pyx_RefNannyFinishContext(); + return NULL; + __pyx_L4_argument_unpacking_done:; + __pyx_r = __pyx_pf_9fontTools_5cu2qu_5cu2qu_5curves_to_quadratic(__pyx_self, __pyx_v_curves, __pyx_v_max_errors, __pyx_v_all_quadratic); + + /* "fontTools/cu2qu/cu2qu.py":504 + * + * + * @cython.locals(l=cython.int, last_i=cython.int, i=cython.int) # <<<<<<<<<<<<<< + * @cython.locals(all_quadratic=cython.int) + * def curves_to_quadratic(curves, max_errors, all_quadratic=True): +*/ + + /* function exit code */ + for (Py_ssize_t __pyx_temp=0; __pyx_temp < (Py_ssize_t)(sizeof(values)/sizeof(values[0])); ++__pyx_temp) { + Py_XDECREF(values[__pyx_temp]); + } + + __Pyx_RefNannyFinishContext(); + return __pyx_r; +} + +static PyObject *__pyx_pf_9fontTools_5cu2qu_5cu2qu_5curves_to_quadratic(CYTHON_UNUSED PyObject *__pyx_self, PyObject *__pyx_v_curves, PyObject *__pyx_v_max_errors, int __pyx_v_all_quadratic) { + int __pyx_v_l; + int __pyx_v_last_i; + int __pyx_v_i; + PyObject *__pyx_v_splines = NULL; + PyObject *__pyx_v_n = NULL; + PyObject *__pyx_v_spline = NULL; + PyObject *__pyx_8genexpr2__pyx_v_curve = NULL; + PyObject *__pyx_8genexpr3__pyx_v_p = NULL; + PyObject *__pyx_8genexpr4__pyx_v_spline = NULL; + PyObject *__pyx_8genexpr5__pyx_v_s = NULL; + PyObject *__pyx_r = NULL; + __Pyx_RefNannyDeclarations + PyObject *__pyx_t_1 = NULL; + PyObject *__pyx_t_2 = NULL; + Py_ssize_t __pyx_t_3; + PyObject *(*__pyx_t_4)(PyObject *); + PyObject *__pyx_t_5 = NULL; + PyObject *__pyx_t_6 = NULL; + Py_ssize_t __pyx_t_7; + PyObject *(*__pyx_t_8)(PyObject *); + PyObject *__pyx_t_9 = NULL; + PyObject *__pyx_t_10 = NULL; + int __pyx_t_11; + size_t __pyx_t_12; + int __pyx_t_13; + int __pyx_t_14; + double __pyx_t_15; + long __pyx_t_16; + PyObject *__pyx_t_17 = NULL; + int __pyx_lineno = 0; + const char *__pyx_filename = NULL; + int __pyx_clineno = 0; + __Pyx_RefNannySetupContext("curves_to_quadratic", 0); + __Pyx_INCREF(__pyx_v_curves); + + /* "fontTools/cu2qu/cu2qu.py":544 + * """ + * + * curves = [[complex(*p) for p in curve] for curve in curves] # <<<<<<<<<<<<<< + * if len(max_errors) != len(curves): + * raise ValueError("max_errors must match the number of curves") +*/ + { /* enter inner scope */ + __pyx_t_1 = PyList_New(0); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 544, __pyx_L5_error) + __Pyx_GOTREF(__pyx_t_1); + if (likely(PyList_CheckExact(__pyx_v_curves)) || PyTuple_CheckExact(__pyx_v_curves)) { + __pyx_t_2 = __pyx_v_curves; __Pyx_INCREF(__pyx_t_2); + __pyx_t_3 = 0; + __pyx_t_4 = NULL; + } else { + __pyx_t_3 = -1; __pyx_t_2 = PyObject_GetIter(__pyx_v_curves); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 544, __pyx_L5_error) + __Pyx_GOTREF(__pyx_t_2); + __pyx_t_4 = (CYTHON_COMPILING_IN_LIMITED_API) ? PyIter_Next : __Pyx_PyObject_GetIterNextFunc(__pyx_t_2); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 544, __pyx_L5_error) + } + for (;;) { + if (likely(!__pyx_t_4)) { + if (likely(PyList_CheckExact(__pyx_t_2))) { + { + Py_ssize_t __pyx_temp = __Pyx_PyList_GET_SIZE(__pyx_t_2); + #if !CYTHON_ASSUME_SAFE_SIZE + if (unlikely((__pyx_temp < 0))) __PYX_ERR(0, 544, __pyx_L5_error) + #endif + if (__pyx_t_3 >= __pyx_temp) break; + } + __pyx_t_5 = __Pyx_PyList_GET_ITEM_REF(__pyx_t_2, __pyx_t_3, __Pyx_ReferenceSharing_OwnStrongReference); + ++__pyx_t_3; + } else { + { + Py_ssize_t __pyx_temp = __Pyx_PyTuple_GET_SIZE(__pyx_t_2); + #if !CYTHON_ASSUME_SAFE_SIZE + if (unlikely((__pyx_temp < 0))) __PYX_ERR(0, 544, __pyx_L5_error) + #endif + if (__pyx_t_3 >= __pyx_temp) break; + } + #if CYTHON_ASSUME_SAFE_MACROS && !CYTHON_AVOID_BORROWED_REFS + __pyx_t_5 = __Pyx_NewRef(PyTuple_GET_ITEM(__pyx_t_2, __pyx_t_3)); + #else + __pyx_t_5 = __Pyx_PySequence_ITEM(__pyx_t_2, __pyx_t_3); + #endif + ++__pyx_t_3; + } + if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 544, __pyx_L5_error) + } else { + __pyx_t_5 = __pyx_t_4(__pyx_t_2); + if (unlikely(!__pyx_t_5)) { + PyObject* exc_type = PyErr_Occurred(); + if (exc_type) { + if (unlikely(!__Pyx_PyErr_GivenExceptionMatches(exc_type, PyExc_StopIteration))) __PYX_ERR(0, 544, __pyx_L5_error) + PyErr_Clear(); + } + break; + } + } + __Pyx_GOTREF(__pyx_t_5); + __Pyx_XDECREF_SET(__pyx_8genexpr2__pyx_v_curve, __pyx_t_5); + __pyx_t_5 = 0; + { /* enter inner scope */ + __pyx_t_5 = PyList_New(0); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 544, __pyx_L10_error) + __Pyx_GOTREF(__pyx_t_5); + if (likely(PyList_CheckExact(__pyx_8genexpr2__pyx_v_curve)) || PyTuple_CheckExact(__pyx_8genexpr2__pyx_v_curve)) { + __pyx_t_6 = __pyx_8genexpr2__pyx_v_curve; __Pyx_INCREF(__pyx_t_6); + __pyx_t_7 = 0; + __pyx_t_8 = NULL; + } else { + __pyx_t_7 = -1; __pyx_t_6 = PyObject_GetIter(__pyx_8genexpr2__pyx_v_curve); if (unlikely(!__pyx_t_6)) __PYX_ERR(0, 544, __pyx_L10_error) + __Pyx_GOTREF(__pyx_t_6); + __pyx_t_8 = (CYTHON_COMPILING_IN_LIMITED_API) ? PyIter_Next : __Pyx_PyObject_GetIterNextFunc(__pyx_t_6); if (unlikely(!__pyx_t_8)) __PYX_ERR(0, 544, __pyx_L10_error) + } + for (;;) { + if (likely(!__pyx_t_8)) { + if (likely(PyList_CheckExact(__pyx_t_6))) { + { + Py_ssize_t __pyx_temp = __Pyx_PyList_GET_SIZE(__pyx_t_6); + #if !CYTHON_ASSUME_SAFE_SIZE + if (unlikely((__pyx_temp < 0))) __PYX_ERR(0, 544, __pyx_L10_error) + #endif + if (__pyx_t_7 >= __pyx_temp) break; + } + __pyx_t_9 = __Pyx_PyList_GET_ITEM_REF(__pyx_t_6, __pyx_t_7, __Pyx_ReferenceSharing_OwnStrongReference); + ++__pyx_t_7; + } else { + { + Py_ssize_t __pyx_temp = __Pyx_PyTuple_GET_SIZE(__pyx_t_6); + #if !CYTHON_ASSUME_SAFE_SIZE + if (unlikely((__pyx_temp < 0))) __PYX_ERR(0, 544, __pyx_L10_error) + #endif + if (__pyx_t_7 >= __pyx_temp) break; + } + #if CYTHON_ASSUME_SAFE_MACROS && !CYTHON_AVOID_BORROWED_REFS + __pyx_t_9 = __Pyx_NewRef(PyTuple_GET_ITEM(__pyx_t_6, __pyx_t_7)); + #else + __pyx_t_9 = __Pyx_PySequence_ITEM(__pyx_t_6, __pyx_t_7); + #endif + ++__pyx_t_7; + } + if (unlikely(!__pyx_t_9)) __PYX_ERR(0, 544, __pyx_L10_error) + } else { + __pyx_t_9 = __pyx_t_8(__pyx_t_6); + if (unlikely(!__pyx_t_9)) { + PyObject* exc_type = PyErr_Occurred(); + if (exc_type) { + if (unlikely(!__Pyx_PyErr_GivenExceptionMatches(exc_type, PyExc_StopIteration))) __PYX_ERR(0, 544, __pyx_L10_error) + PyErr_Clear(); + } + break; + } + } + __Pyx_GOTREF(__pyx_t_9); + __Pyx_XDECREF_SET(__pyx_8genexpr3__pyx_v_p, __pyx_t_9); + __pyx_t_9 = 0; + __pyx_t_9 = __Pyx_PySequence_Tuple(__pyx_8genexpr3__pyx_v_p); if (unlikely(!__pyx_t_9)) __PYX_ERR(0, 544, __pyx_L10_error) + __Pyx_GOTREF(__pyx_t_9); + __pyx_t_10 = __Pyx_PyObject_Call(((PyObject *)(&PyComplex_Type)), __pyx_t_9, NULL); if (unlikely(!__pyx_t_10)) __PYX_ERR(0, 544, __pyx_L10_error) + __Pyx_GOTREF(__pyx_t_10); + __Pyx_DECREF(__pyx_t_9); __pyx_t_9 = 0; + __Pyx_GIVEREF(__pyx_t_10); + if (unlikely(__Pyx_ListComp_AppendAndDecref(__pyx_t_5, __pyx_t_10))) __PYX_ERR(0, 544, __pyx_L10_error) + __pyx_t_10 = 0; + } + __Pyx_DECREF(__pyx_t_6); __pyx_t_6 = 0; + __Pyx_XDECREF(__pyx_8genexpr3__pyx_v_p); __pyx_8genexpr3__pyx_v_p = 0; + goto __pyx_L14_exit_scope; + __pyx_L10_error:; + __Pyx_XDECREF(__pyx_8genexpr3__pyx_v_p); __pyx_8genexpr3__pyx_v_p = 0; + goto __pyx_L5_error; + __pyx_L14_exit_scope:; + } /* exit inner scope */ + __Pyx_GIVEREF(__pyx_t_5); + if (unlikely(__Pyx_ListComp_AppendAndDecref(__pyx_t_1, __pyx_t_5))) __PYX_ERR(0, 544, __pyx_L5_error) + __pyx_t_5 = 0; + } + __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0; + __Pyx_XDECREF(__pyx_8genexpr2__pyx_v_curve); __pyx_8genexpr2__pyx_v_curve = 0; + goto __pyx_L16_exit_scope; + __pyx_L5_error:; + __Pyx_XDECREF(__pyx_8genexpr2__pyx_v_curve); __pyx_8genexpr2__pyx_v_curve = 0; + goto __pyx_L1_error; + __pyx_L16_exit_scope:; + } /* exit inner scope */ + __Pyx_DECREF_SET(__pyx_v_curves, __pyx_t_1); + __pyx_t_1 = 0; + + /* "fontTools/cu2qu/cu2qu.py":545 + * + * curves = [[complex(*p) for p in curve] for curve in curves] + * if len(max_errors) != len(curves): # <<<<<<<<<<<<<< + * raise ValueError("max_errors must match the number of curves") + * if not curves: +*/ + __pyx_t_3 = PyObject_Length(__pyx_v_max_errors); if (unlikely(__pyx_t_3 == ((Py_ssize_t)-1))) __PYX_ERR(0, 545, __pyx_L1_error) + __pyx_t_7 = PyObject_Length(__pyx_v_curves); if (unlikely(__pyx_t_7 == ((Py_ssize_t)-1))) __PYX_ERR(0, 545, __pyx_L1_error) + __pyx_t_11 = (__pyx_t_3 != __pyx_t_7); + + + + if (unlikely(__pyx_t_11)) { + + + /* "fontTools/cu2qu/cu2qu.py":546 + * curves = [[complex(*p) for p in curve] for curve in curves] + * if len(max_errors) != len(curves): + * raise ValueError("max_errors must match the number of curves") # <<<<<<<<<<<<<< + * if not curves: + * return [] +*/ + __pyx_t_2 = NULL; + __pyx_t_12 = 1; + { + PyObject *__pyx_callargs[2] = {__pyx_t_2, __pyx_mstate_global->__pyx_kp_u_max_errors_must_match_the_number}; + __pyx_t_1 = __Pyx_PyObject_FastCall((PyObject*)(((PyTypeObject*)PyExc_ValueError)), __pyx_callargs+__pyx_t_12, (2-__pyx_t_12) | (__pyx_t_12*__Pyx_PY_VECTORCALL_ARGUMENTS_OFFSET)); + __Pyx_XDECREF(__pyx_t_2); __pyx_t_2 = 0; + if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 546, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_1); + } + __Pyx_Raise(__pyx_t_1, 0, 0, 0); + __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0; + __PYX_ERR(0, 546, __pyx_L1_error) + + /* "fontTools/cu2qu/cu2qu.py":545 + * + * curves = [[complex(*p) for p in curve] for curve in curves] + * if len(max_errors) != len(curves): # <<<<<<<<<<<<<< + * raise ValueError("max_errors must match the number of curves") + * if not curves: +*/ + } + + /* "fontTools/cu2qu/cu2qu.py":547 + * if len(max_errors) != len(curves): + * raise ValueError("max_errors must match the number of curves") + * if not curves: # <<<<<<<<<<<<<< + * return [] + * +*/ + __pyx_t_11 = __Pyx_PyObject_IsTrue(__pyx_v_curves); if (unlikely((__pyx_t_11 < 0))) __PYX_ERR(0, 547, __pyx_L1_error) + __pyx_t_13 = (!__pyx_t_11); + + + if (__pyx_t_13) { + + + /* "fontTools/cu2qu/cu2qu.py":548 + * raise ValueError("max_errors must match the number of curves") + * if not curves: + * return [] # <<<<<<<<<<<<<< + * + * l = len(curves) +*/ + __pyx_t_1 = PyList_New(0); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 548, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_1); + { + PyObject *__pyx_temp; + { + __pyx_temp = __pyx_r; + __pyx_r = __pyx_t_1; + } + __Pyx_XDECREF(__pyx_temp); + } + __pyx_t_1 = 0; + goto __pyx_L0; + + /* "fontTools/cu2qu/cu2qu.py":547 + * if len(max_errors) != len(curves): + * raise ValueError("max_errors must match the number of curves") + * if not curves: # <<<<<<<<<<<<<< + * return [] + * +*/ + } + + /* "fontTools/cu2qu/cu2qu.py":550 + * return [] + * + * l = len(curves) # <<<<<<<<<<<<<< + * splines = [None] * l + * last_i = i = 0 +*/ + __pyx_t_7 = PyObject_Length(__pyx_v_curves); if (unlikely(__pyx_t_7 == ((Py_ssize_t)-1))) __PYX_ERR(0, 550, __pyx_L1_error) + __pyx_v_l = __pyx_t_7; + + /* "fontTools/cu2qu/cu2qu.py":551 + * + * l = len(curves) + * splines = [None] * l # <<<<<<<<<<<<<< + * last_i = i = 0 + * n = 1 +*/ + __pyx_t_1 = PyList_New(1 * ((__pyx_v_l<0) ? 0:__pyx_v_l)); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 551, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_1); + { Py_ssize_t __pyx_temp; + for (__pyx_temp=0; __pyx_temp < __pyx_v_l; __pyx_temp++) { + __Pyx_INCREF(Py_None); + __Pyx_GIVEREF(Py_None); + if (__Pyx_PyList_SET_ITEM(__pyx_t_1, __pyx_temp, Py_None) != (0)) __PYX_ERR(0, 551, __pyx_L1_error); + } + } + __pyx_v_splines = ((PyObject*)__pyx_t_1); + __pyx_t_1 = 0; + + /* "fontTools/cu2qu/cu2qu.py":552 + * l = len(curves) + * splines = [None] * l + * last_i = i = 0 # <<<<<<<<<<<<<< + * n = 1 + * while True: +*/ + __pyx_v_last_i = 0; + __pyx_v_i = 0; + + /* "fontTools/cu2qu/cu2qu.py":553 + * splines = [None] * l + * last_i = i = 0 + * n = 1 # <<<<<<<<<<<<<< + * while True: + * spline = cubic_approx_spline(curves[i], n, max_errors[i], all_quadratic) +*/ + __Pyx_INCREF(__pyx_mstate_global->__pyx_int_1); + __pyx_v_n = __pyx_mstate_global->__pyx_int_1; + + /* "fontTools/cu2qu/cu2qu.py":554 + * last_i = i = 0 + * n = 1 + * while True: # <<<<<<<<<<<<<< + * spline = cubic_approx_spline(curves[i], n, max_errors[i], all_quadratic) + * if spline is None: +*/ + while (1) { + + /* "fontTools/cu2qu/cu2qu.py":555 + * n = 1 + * while True: + * spline = cubic_approx_spline(curves[i], n, max_errors[i], all_quadratic) # <<<<<<<<<<<<<< + * if spline is None: + * if n == MAX_N: +*/ + __pyx_t_1 = __Pyx_GetItemInt(__pyx_v_curves, __pyx_v_i, int, 1, __Pyx_PyLong_From_int, 1, 1, 1, __Pyx_ReferenceSharing_FunctionArgument); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 555, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_1); + __pyx_t_14 = __Pyx_PyLong_As_int(__pyx_v_n); if (unlikely((__pyx_t_14 == (int)-1) && PyErr_Occurred())) __PYX_ERR(0, 555, __pyx_L1_error) + __pyx_t_2 = __Pyx_GetItemInt(__pyx_v_max_errors, __pyx_v_i, int, 1, __Pyx_PyLong_From_int, 1, 1, 1, __Pyx_ReferenceSharing_FunctionArgument); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 555, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + __pyx_t_15 = __Pyx_PyFloat_AsDouble(__pyx_t_2); if (unlikely((__pyx_t_15 == (double)-1) && PyErr_Occurred())) __PYX_ERR(0, 555, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0; + __pyx_t_2 = __pyx_f_9fontTools_5cu2qu_5cu2qu_cubic_approx_spline(__pyx_t_1, __pyx_t_14, __pyx_t_15, __pyx_v_all_quadratic); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 555, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0; + + + __Pyx_XDECREF_SET(__pyx_v_spline, __pyx_t_2); + __pyx_t_2 = 0; + + /* "fontTools/cu2qu/cu2qu.py":556 + * while True: + * spline = cubic_approx_spline(curves[i], n, max_errors[i], all_quadratic) + * if spline is None: # <<<<<<<<<<<<<< + * if n == MAX_N: + * break +*/ + __pyx_t_13 = (__pyx_v_spline == Py_None); + if (__pyx_t_13) { + + + /* "fontTools/cu2qu/cu2qu.py":557 + * spline = cubic_approx_spline(curves[i], n, max_errors[i], all_quadratic) + * if spline is None: + * if n == MAX_N: # <<<<<<<<<<<<<< + * break + * n += 1 +*/ + __Pyx_GetModuleGlobalName(__pyx_t_2, __pyx_mstate_global->__pyx_n_u_MAX_N); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 557, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + __pyx_t_13 = __Pyx_PyObject_CompareBoolEq_int_object(__pyx_v_n, __pyx_t_2, Py_EQ); if (unlikely((__pyx_t_13 < 0))) __PYX_ERR(0, 557, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0; + if (__pyx_t_13) { + + + /* "fontTools/cu2qu/cu2qu.py":558 + * if spline is None: + * if n == MAX_N: + * break # <<<<<<<<<<<<<< + * n += 1 + * last_i = i +*/ + goto __pyx_L20_break; + + /* "fontTools/cu2qu/cu2qu.py":557 + * spline = cubic_approx_spline(curves[i], n, max_errors[i], all_quadratic) + * if spline is None: + * if n == MAX_N: # <<<<<<<<<<<<<< + * break + * n += 1 +*/ + } + + /* "fontTools/cu2qu/cu2qu.py":559 + * if n == MAX_N: + * break + * n += 1 # <<<<<<<<<<<<<< + * last_i = i + * continue +*/ + __pyx_t_2 = __Pyx_PyLong_AddObjC(__pyx_v_n, __pyx_mstate_global->__pyx_int_1, 1, 1, 0); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 559, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + __Pyx_DECREF_SET(__pyx_v_n, ((PyObject*)__pyx_t_2)); + __pyx_t_2 = 0; + + /* "fontTools/cu2qu/cu2qu.py":560 + * break + * n += 1 + * last_i = i # <<<<<<<<<<<<<< + * continue + * splines[i] = spline +*/ + __pyx_v_last_i = __pyx_v_i; + + /* "fontTools/cu2qu/cu2qu.py":561 + * n += 1 + * last_i = i + * continue # <<<<<<<<<<<<<< + * splines[i] = spline + * i = (i + 1) % l +*/ + goto __pyx_L19_continue; + + /* "fontTools/cu2qu/cu2qu.py":556 + * while True: + * spline = cubic_approx_spline(curves[i], n, max_errors[i], all_quadratic) + * if spline is None: # <<<<<<<<<<<<<< + * if n == MAX_N: + * break +*/ + } + + /* "fontTools/cu2qu/cu2qu.py":562 + * last_i = i + * continue + * splines[i] = spline # <<<<<<<<<<<<<< + * i = (i + 1) % l + * if i == last_i: +*/ + if (unlikely((__Pyx_SetItemInt(__pyx_v_splines, __pyx_v_i, __pyx_v_spline, int, 1, __Pyx_PyLong_From_int, 1, 1, 1, __Pyx_ReferenceSharing_OwnStrongReference) < 0))) __PYX_ERR(0, 562, __pyx_L1_error) + + /* "fontTools/cu2qu/cu2qu.py":563 + * continue + * splines[i] = spline + * i = (i + 1) % l # <<<<<<<<<<<<<< + * if i == last_i: + * # done. go home +*/ + __pyx_t_16 = (__pyx_v_i + 1); + + if (unlikely(__pyx_v_l == 0)) { + PyErr_SetString(PyExc_ZeroDivisionError, "integer division or modulo by zero"); + __PYX_ERR(0, 563, __pyx_L1_error) + } + __pyx_v_i = __Pyx_mod_long(__pyx_t_16, __pyx_v_l, 0); + + + /* "fontTools/cu2qu/cu2qu.py":564 + * splines[i] = spline + * i = (i + 1) % l + * if i == last_i: # <<<<<<<<<<<<<< + * # done. go home + * return [[(s.real, s.imag) for s in spline] for spline in splines] +*/ + __pyx_t_13 = (__pyx_v_i == __pyx_v_last_i); + + if (__pyx_t_13) { + + + /* "fontTools/cu2qu/cu2qu.py":566 + * if i == last_i: + * # done. go home + * return [[(s.real, s.imag) for s in spline] for spline in splines] # <<<<<<<<<<<<<< + * + * raise ApproxNotFoundError(curves) +*/ + { /* enter inner scope */ + __pyx_t_2 = PyList_New(0); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 566, __pyx_L26_error) + __Pyx_GOTREF(__pyx_t_2); + __pyx_t_1 = __pyx_v_splines; __Pyx_INCREF(__pyx_t_1); + __pyx_t_7 = 0; + for (;;) { + { + Py_ssize_t __pyx_temp = __Pyx_PyList_GET_SIZE(__pyx_t_1); + #if !CYTHON_ASSUME_SAFE_SIZE + if (unlikely((__pyx_temp < 0))) __PYX_ERR(0, 566, __pyx_L26_error) + #endif + if (__pyx_t_7 >= __pyx_temp) break; + } + __pyx_t_5 = __Pyx_PyList_GET_ITEM_REF(__pyx_t_1, __pyx_t_7, __Pyx_ReferenceSharing_OwnStrongReference); + ++__pyx_t_7; + if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 566, __pyx_L26_error) + __Pyx_GOTREF(__pyx_t_5); + __Pyx_XDECREF_SET(__pyx_8genexpr4__pyx_v_spline, __pyx_t_5); + __pyx_t_5 = 0; + { /* enter inner scope */ + __pyx_t_5 = PyList_New(0); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 566, __pyx_L31_error) + __Pyx_GOTREF(__pyx_t_5); + if (likely(PyList_CheckExact(__pyx_8genexpr4__pyx_v_spline)) || PyTuple_CheckExact(__pyx_8genexpr4__pyx_v_spline)) { + __pyx_t_6 = __pyx_8genexpr4__pyx_v_spline; __Pyx_INCREF(__pyx_t_6); + __pyx_t_3 = 0; + __pyx_t_4 = NULL; + } else { + __pyx_t_3 = -1; __pyx_t_6 = PyObject_GetIter(__pyx_8genexpr4__pyx_v_spline); if (unlikely(!__pyx_t_6)) __PYX_ERR(0, 566, __pyx_L31_error) + __Pyx_GOTREF(__pyx_t_6); + __pyx_t_4 = (CYTHON_COMPILING_IN_LIMITED_API) ? PyIter_Next : __Pyx_PyObject_GetIterNextFunc(__pyx_t_6); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 566, __pyx_L31_error) + } + for (;;) { + if (likely(!__pyx_t_4)) { + if (likely(PyList_CheckExact(__pyx_t_6))) { + { + Py_ssize_t __pyx_temp = __Pyx_PyList_GET_SIZE(__pyx_t_6); + #if !CYTHON_ASSUME_SAFE_SIZE + if (unlikely((__pyx_temp < 0))) __PYX_ERR(0, 566, __pyx_L31_error) + #endif + if (__pyx_t_3 >= __pyx_temp) break; + } + __pyx_t_10 = __Pyx_PyList_GET_ITEM_REF(__pyx_t_6, __pyx_t_3, __Pyx_ReferenceSharing_OwnStrongReference); + ++__pyx_t_3; + } else { + { + Py_ssize_t __pyx_temp = __Pyx_PyTuple_GET_SIZE(__pyx_t_6); + #if !CYTHON_ASSUME_SAFE_SIZE + if (unlikely((__pyx_temp < 0))) __PYX_ERR(0, 566, __pyx_L31_error) + #endif + if (__pyx_t_3 >= __pyx_temp) break; + } + #if CYTHON_ASSUME_SAFE_MACROS && !CYTHON_AVOID_BORROWED_REFS + __pyx_t_10 = __Pyx_NewRef(PyTuple_GET_ITEM(__pyx_t_6, __pyx_t_3)); + #else + __pyx_t_10 = __Pyx_PySequence_ITEM(__pyx_t_6, __pyx_t_3); + #endif + ++__pyx_t_3; + } + if (unlikely(!__pyx_t_10)) __PYX_ERR(0, 566, __pyx_L31_error) + } else { + __pyx_t_10 = __pyx_t_4(__pyx_t_6); + if (unlikely(!__pyx_t_10)) { + PyObject* exc_type = PyErr_Occurred(); + if (exc_type) { + if (unlikely(!__Pyx_PyErr_GivenExceptionMatches(exc_type, PyExc_StopIteration))) __PYX_ERR(0, 566, __pyx_L31_error) + PyErr_Clear(); + } + break; + } + } + __Pyx_GOTREF(__pyx_t_10); + __Pyx_XDECREF_SET(__pyx_8genexpr5__pyx_v_s, __pyx_t_10); + __pyx_t_10 = 0; + __pyx_t_10 = __Pyx_PyObject_GetAttrStr(__pyx_8genexpr5__pyx_v_s, __pyx_mstate_global->__pyx_n_u_real); if (unlikely(!__pyx_t_10)) __PYX_ERR(0, 566, __pyx_L31_error) + __Pyx_GOTREF(__pyx_t_10); + __pyx_t_9 = __Pyx_PyObject_GetAttrStr(__pyx_8genexpr5__pyx_v_s, __pyx_mstate_global->__pyx_n_u_imag); if (unlikely(!__pyx_t_9)) __PYX_ERR(0, 566, __pyx_L31_error) + __Pyx_GOTREF(__pyx_t_9); + __pyx_t_17 = PyTuple_New(2); if (unlikely(!__pyx_t_17)) __PYX_ERR(0, 566, __pyx_L31_error) + __Pyx_GOTREF(__pyx_t_17); + __Pyx_GIVEREF(__pyx_t_10); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_17, 0, __pyx_t_10) != (0)) __PYX_ERR(0, 566, __pyx_L31_error); + __Pyx_GIVEREF(__pyx_t_9); + if (__Pyx_PyTuple_SET_ITEM(__pyx_t_17, 1, __pyx_t_9) != (0)) __PYX_ERR(0, 566, __pyx_L31_error); + __pyx_t_10 = 0; + __pyx_t_9 = 0; + __Pyx_GIVEREF(__pyx_t_17); + if (unlikely(__Pyx_ListComp_AppendAndDecref(__pyx_t_5, __pyx_t_17))) __PYX_ERR(0, 566, __pyx_L31_error) + __pyx_t_17 = 0; + } + __Pyx_DECREF(__pyx_t_6); __pyx_t_6 = 0; + __Pyx_XDECREF(__pyx_8genexpr5__pyx_v_s); __pyx_8genexpr5__pyx_v_s = 0; + goto __pyx_L35_exit_scope; + __pyx_L31_error:; + __Pyx_XDECREF(__pyx_8genexpr5__pyx_v_s); __pyx_8genexpr5__pyx_v_s = 0; + goto __pyx_L26_error; + __pyx_L35_exit_scope:; + } /* exit inner scope */ + __Pyx_GIVEREF(__pyx_t_5); + if (unlikely(__Pyx_ListComp_AppendAndDecref(__pyx_t_2, __pyx_t_5))) __PYX_ERR(0, 566, __pyx_L26_error) + __pyx_t_5 = 0; + } + __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0; + __Pyx_XDECREF(__pyx_8genexpr4__pyx_v_spline); __pyx_8genexpr4__pyx_v_spline = 0; + goto __pyx_L37_exit_scope; + __pyx_L26_error:; + __Pyx_XDECREF(__pyx_8genexpr4__pyx_v_spline); __pyx_8genexpr4__pyx_v_spline = 0; + goto __pyx_L1_error; + __pyx_L37_exit_scope:; + } /* exit inner scope */ + { + PyObject *__pyx_temp; + { + __pyx_temp = __pyx_r; + __pyx_r = __pyx_t_2; + } + __Pyx_XDECREF(__pyx_temp); + } + __pyx_t_2 = 0; + goto __pyx_L0; + + /* "fontTools/cu2qu/cu2qu.py":564 + * splines[i] = spline + * i = (i + 1) % l + * if i == last_i: # <<<<<<<<<<<<<< + * # done. go home + * return [[(s.real, s.imag) for s in spline] for spline in splines] +*/ + } + __pyx_L19_continue:; + } + __pyx_L20_break:; + + /* "fontTools/cu2qu/cu2qu.py":568 + * return [[(s.real, s.imag) for s in spline] for spline in splines] + * + * raise ApproxNotFoundError(curves) # <<<<<<<<<<<<<< +*/ + __pyx_t_1 = NULL; + __Pyx_GetModuleGlobalName(__pyx_t_5, __pyx_mstate_global->__pyx_n_u_ApproxNotFoundError); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 568, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_5); + __pyx_t_12 = 1; + #if CYTHON_UNPACK_METHODS + if (unlikely(PyMethod_Check(__pyx_t_5))) { + __pyx_t_1 = PyMethod_GET_SELF(__pyx_t_5); + assert(__pyx_t_1); + PyObject* __pyx__function = PyMethod_GET_FUNCTION(__pyx_t_5); + __Pyx_INCREF(__pyx_t_1); + __Pyx_INCREF(__pyx__function); + __Pyx_DECREF_SET(__pyx_t_5, __pyx__function); + __pyx_t_12 = 0; + } + #endif + { + PyObject *__pyx_callargs[2] = {__pyx_t_1, __pyx_v_curves}; + __pyx_t_2 = __Pyx_PyObject_FastCall((PyObject*)__pyx_t_5, __pyx_callargs+__pyx_t_12, (2-__pyx_t_12) | (__pyx_t_12*__Pyx_PY_VECTORCALL_ARGUMENTS_OFFSET)); + __Pyx_XDECREF(__pyx_t_1); __pyx_t_1 = 0; + __Pyx_DECREF(__pyx_t_5); __pyx_t_5 = 0; + if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 568, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_2); + } + __Pyx_Raise(__pyx_t_2, 0, 0, 0); + __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0; + __PYX_ERR(0, 568, __pyx_L1_error) + + /* "fontTools/cu2qu/cu2qu.py":504 + * + * + * @cython.locals(l=cython.int, last_i=cython.int, i=cython.int) # <<<<<<<<<<<<<< + * @cython.locals(all_quadratic=cython.int) + * def curves_to_quadratic(curves, max_errors, all_quadratic=True): +*/ + + /* function exit code */ + __pyx_L1_error:; + __Pyx_XDECREF(__pyx_t_1); + __Pyx_XDECREF(__pyx_t_2); + __Pyx_XDECREF(__pyx_t_5); + __Pyx_XDECREF(__pyx_t_6); + __Pyx_XDECREF(__pyx_t_9); + __Pyx_XDECREF(__pyx_t_10); + __Pyx_XDECREF(__pyx_t_17); + __Pyx_AddTraceback("fontTools.cu2qu.cu2qu.curves_to_quadratic", __pyx_clineno, __pyx_lineno, __pyx_filename); + __pyx_r = NULL; + __pyx_L0:; + + + + __Pyx_XDECREF(__pyx_v_splines); + __Pyx_XDECREF(__pyx_v_n); + __Pyx_XDECREF(__pyx_v_spline); + __Pyx_XDECREF(__pyx_8genexpr2__pyx_v_curve); + __Pyx_XDECREF(__pyx_8genexpr3__pyx_v_p); + __Pyx_XDECREF(__pyx_8genexpr4__pyx_v_spline); + __Pyx_XDECREF(__pyx_8genexpr5__pyx_v_s); + __Pyx_XDECREF(__pyx_v_curves); + __Pyx_XGIVEREF(__pyx_r); + __Pyx_RefNannyFinishContext(); + return __pyx_r; +} +/* #### Code section: module_exttypes ### */ + +static PyObject *__pyx_tp_new__initialisation_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen(PyObject *o, +#if CYTHON_VECTORCALL_TPNEW + CYTHON_UNUSED PyObject *const *args, CYTHON_UNUSED Py_ssize_t nargs, CYTHON_UNUSED PyObject *kwnames +#else + CYTHON_UNUSED PyObject *a, CYTHON_UNUSED PyObject *k +#endif +) { + return o; +} + +static PyObject *__pyx_tp_new_vectorcall_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen(PyTypeObject *t, +#if CYTHON_VECTORCALL_TPNEW + PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames +#else + PyObject *a, PyObject *k +#endif +) { + PyObject *o; + #if CYTHON_USE_FREELISTS + if (likely((int)(__pyx_mstate_global->__pyx_freecount_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen > 0) & __PYX_CHECK_FINAL_TYPE_FOR_FREELISTS(t, __pyx_mstate_global->__pyx_ptype_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen, sizeof(struct __pyx_obj_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen)))) + { + o = (PyObject*)__pyx_mstate_global->__pyx_freelist_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen[--__pyx_mstate_global->__pyx_freecount_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen]; + #if CYTHON_USE_TYPE_SPECS + Py_DECREF(Py_TYPE(o)); + #endif + memset(o, 0, sizeof(struct __pyx_obj_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen)); + #if CYTHON_COMPILING_IN_LIMITED_API + (void) PyObject_Init(o, t); + #else + (void) PyObject_INIT(o, t); + #endif + } else + #endif + { + o = __Pyx_AllocateExtensionType(t, 1); + } + if (unlikely(!o)) return 0; + return __pyx_tp_new__initialisation_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen(o, +#if CYTHON_VECTORCALL_TPNEW + args, nargs, kwnames +#else + a, k +#endif +); +} + +#if CYTHON_VECTORCALL_TPNEW +static PyObject *__pyx_tp_new_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen(PyTypeObject *t, PyObject *a, PyObject *k) { + return __Pyx_CallTpnewAsVectorcall(__pyx_tp_new_vectorcall_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen, t, a, k); +} +#endif + +#if CYTHON_VECTORCALL_TPNEW +static PyObject *__pyx_tp_vectorcall_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen(PyObject *t, PyObject *const *args, size_t nargsf, PyObject *kwnames) { + if (unlikely((PyTypeObject*)t != __pyx_mstate_global->__pyx_ptype_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen || __Pyx_PyType_HasFeature((PyTypeObject*)t, Py_TPFLAGS_IS_ABSTRACT))) { + return __Pyx_CallNewInitFromVectorcall((PyTypeObject*)t, args, nargsf, kwnames); + } + Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); + PyObject *o = __pyx_tp_new_vectorcall_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen((PyTypeObject*)t, args, nargs, kwnames); + return o; +} +#endif + +static void __pyx_tp_dealloc_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen(PyObject *o) { + #if CYTHON_USE_TP_FINALIZE + if (unlikely(__Pyx_PyObject_GetSlot(o, tp_finalize, destructor)) && (!PyType_IS_GC(Py_TYPE(o)) || !__Pyx_PyObject_GC_IsFinalized(o))) { + if (__Pyx_PyObject_GetSlot(o, tp_dealloc, destructor) == __pyx_tp_dealloc_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen) { + if (PyObject_CallFinalizerFromDealloc(o)) return; + } + } + #endif + #if CYTHON_USE_FREELISTS + if (likely((int)(__pyx_mstate_global->__pyx_freecount_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen < 8) & __PYX_CHECK_FINAL_TYPE_FOR_FREELISTS(Py_TYPE(o), __pyx_mstate_global->__pyx_ptype_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen, sizeof(struct __pyx_obj_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen)))) + { + __pyx_mstate_global->__pyx_freelist_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen[__pyx_mstate_global->__pyx_freecount_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen++] = ((struct __pyx_obj_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen *)o); + } else + #endif + { + PyTypeObject *tp = Py_TYPE(o); + #if CYTHON_USE_TYPE_SLOTS + (*tp->tp_free)(o); + #else + { + freefunc tp_free = (freefunc)PyType_GetSlot(tp, Py_tp_free); + if (tp_free) tp_free(o); + } + #endif + #if CYTHON_USE_TYPE_SPECS + Py_DECREF(tp); + #endif + } +} +#if CYTHON_USE_TYPE_SPECS +static PyType_Slot __pyx_type_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen_slots[] = { + {Py_tp_dealloc, (void *)__pyx_tp_dealloc_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen}, + {Py_tp_new, (void *)__pyx_tp_new_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen}, + #if (!CYTHON_COMPILING_IN_PYPY || PYPY_VERSION_NUM >= 0x07030800) && (!CYTHON_COMPILING_IN_LIMITED_API || __PYX_LIMITED_VERSION_HEX >= 0x030E0000) + #if CYTHON_VECTORCALL_TPNEW + {Py_tp_vectorcall, (void *)__pyx_tp_vectorcall_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen}, + #endif + #endif + {0, 0}, +}; +static PyType_Spec __pyx_type_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen_spec = { + "fontTools.cu2qu.cu2qu.__pyx_scope_struct___split_cubic_into_n_gen", + sizeof(struct __pyx_obj_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen), + 0, + Py_TPFLAGS_DEFAULT|Py_TPFLAGS_HAVE_VERSION_TAG, + __pyx_type_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen_slots, +}; +#else + +static PyTypeObject __pyx_type_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen = { + PyVarObject_HEAD_INIT(0, 0) + "fontTools.cu2qu.cu2qu.""__pyx_scope_struct___split_cubic_into_n_gen", /*tp_name*/ + sizeof(struct __pyx_obj_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen), /*tp_basicsize*/ + 0, /*tp_itemsize*/ + __pyx_tp_dealloc_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen, /*tp_dealloc*/ + 0, /*tp_vectorcall_offset*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_as_async*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT|Py_TPFLAGS_HAVE_VERSION_TAG, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + 0, /*tp_methods*/ + 0, /*tp_members*/ + 0, /*tp_getset*/ + 0, /*tp_base*/ + 0, /*tp_dict*/ + 0, /*tp_descr_get*/ + 0, /*tp_descr_set*/ + #if !CYTHON_USE_TYPE_SPECS + 0, /*tp_dictoffset*/ + #endif + 0, /*tp_init*/ + 0, /*tp_alloc*/ + __pyx_tp_new_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen, /*tp_new*/ + 0, /*tp_free*/ + 0, /*tp_is_gc*/ + 0, /*tp_bases*/ + 0, /*tp_mro*/ + 0, /*tp_cache*/ + 0, /*tp_subclasses*/ + 0, /*tp_weaklist*/ + 0, /*tp_del*/ + 0, /*tp_version_tag*/ + #if CYTHON_USE_TP_FINALIZE + 0, /*tp_finalize*/ + #else + NULL, /*tp_finalize*/ + #endif + #if (!CYTHON_COMPILING_IN_PYPY || PYPY_VERSION_NUM >= 0x07030800) && (!CYTHON_COMPILING_IN_LIMITED_API || __PYX_LIMITED_VERSION_HEX >= 0x030E0000) + #if CYTHON_VECTORCALL_TPNEW + __pyx_tp_vectorcall_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen, /*tp_vectorcall*/ + #else + NULL, /*tp_vectorcall*/ + #endif + #endif + #if __PYX_NEED_TP_PRINT_SLOT == 1 + 0, /*tp_print*/ + #endif + #if PY_VERSION_HEX >= 0x030C0000 + 0, /*tp_watched*/ + #endif + #if PY_VERSION_HEX >= 0x030d00A4 + 0, /*tp_versions_used*/ + #endif + #if CYTHON_COMPILING_IN_PYPY && PY_VERSION_HEX < 0x030a0000 + 0, /*tp_pypy_flags*/ + #endif +}; +#endif + +static PyMethodDef __pyx_methods[] = { + {0, 0, 0, 0} +}; +/* #### Code section: initfunc_declarations ### */ +static CYTHON_SMALL_CODE int __Pyx_InitCachedBuiltins(__pyx_mstatetype *__pyx_mstate); /*proto*/ +static CYTHON_SMALL_CODE int __Pyx_InitCachedConstants(__pyx_mstatetype *__pyx_mstate); /*proto*/ +static CYTHON_SMALL_CODE int __Pyx_InitGlobals(void); /*proto*/ +static CYTHON_SMALL_CODE int __Pyx_InitAfterSharedUtility(void); /*proto*/ +static CYTHON_SMALL_CODE int __Pyx_InitConstants(__pyx_mstatetype *__pyx_mstate); /*proto*/ +static CYTHON_SMALL_CODE int __Pyx_modinit_Global_init_code(__pyx_mstatetype *__pyx_mstate); /*proto*/ +static CYTHON_SMALL_CODE int __Pyx_modinit_Variable_export_code(__pyx_mstatetype *__pyx_mstate); /*proto*/ +static CYTHON_SMALL_CODE int __Pyx_modinit_Function_export_code(__pyx_mstatetype *__pyx_mstate); /*proto*/ +static CYTHON_SMALL_CODE int __Pyx_modinit_Exttype___pyx_obj_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen(__pyx_mstatetype *__pyx_mstate); /*proto*/ +static CYTHON_SMALL_CODE int __Pyx_modinit_Type_import_code(__pyx_mstatetype *__pyx_mstate); /*proto*/ +static CYTHON_SMALL_CODE int __Pyx_modinit_Variable_import_code(__pyx_mstatetype *__pyx_mstate); /*proto*/ +static CYTHON_SMALL_CODE int __Pyx_modinit_Function_import_code(__pyx_mstatetype *__pyx_mstate); /*proto*/ +static CYTHON_SMALL_CODE int __Pyx_CreateCodeObjects(__pyx_mstatetype *__pyx_mstate); /*proto*/ +/* #### Code section: init_module ### */ + +static int __Pyx_modinit_Global_init_code(__pyx_mstatetype *__pyx_mstate) { + __Pyx_RefNannyDeclarations + CYTHON_UNUSED_VAR(__pyx_mstate); + __Pyx_RefNannySetupContext("__Pyx_modinit_Global_init_code", 0); + /*--- Global init code ---*/ + __Pyx_RefNannyFinishContext(); + return 0; +} + +static int __Pyx_modinit_Variable_export_code(__pyx_mstatetype *__pyx_mstate) { + __Pyx_RefNannyDeclarations + CYTHON_UNUSED_VAR(__pyx_mstate); + __Pyx_RefNannySetupContext("__Pyx_modinit_Variable_export_code", 0); + /*--- Variable export code ---*/ + __Pyx_RefNannyFinishContext(); + return 0; +} + +static int __Pyx_modinit_Function_export_code(__pyx_mstatetype *__pyx_mstate) { + __Pyx_RefNannyDeclarations + CYTHON_UNUSED_VAR(__pyx_mstate); + __Pyx_RefNannySetupContext("__Pyx_modinit_Function_export_code", 0); + /*--- Function export code ---*/ + __Pyx_RefNannyFinishContext(); + return 0; +} + +static int __Pyx_modinit_Exttype___pyx_obj_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen(__pyx_mstatetype *__pyx_mstate) { + __Pyx_RefNannyDeclarations + int __pyx_lineno = 0; + const char *__pyx_filename = NULL; + int __pyx_clineno = 0; + CYTHON_UNUSED_VAR(__pyx_mstate); + __Pyx_RefNannySetupContext("__Pyx_modinit_Exttype___pyx_obj_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen", 0); + /*--- Exttype __pyx_obj_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen ---*/ + #if CYTHON_USE_TYPE_SPECS + __pyx_mstate->__pyx_ptype_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen = (PyTypeObject *) __Pyx_PyType_FromModuleAndSpec(__pyx_m, &__pyx_type_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen_spec, NULL); if (unlikely(!__pyx_mstate->__pyx_ptype_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen)) __PYX_ERR(0, 149, __pyx_L1_error) + #else + __pyx_mstate->__pyx_ptype_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen = &__pyx_type_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen; + #endif + #if !CYTHON_COMPILING_IN_LIMITED_API + #endif + #if !CYTHON_USE_TYPE_SPECS + if (__Pyx_PyType_Ready(__pyx_mstate->__pyx_ptype_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen) < (0)) __PYX_ERR(0, 149, __pyx_L1_error) + #endif + #if CYTHON_COMPILING_IN_CPYTHON && PY_VERSION_HEX >= 0x030E0000 + PyUnstable_Object_EnableDeferredRefcount((PyObject*)__pyx_mstate->__pyx_ptype_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen); + #endif + #if !CYTHON_COMPILING_IN_LIMITED_API + if ((CYTHON_USE_TYPE_SLOTS && CYTHON_USE_PYTYPE_LOOKUP) && likely(!__pyx_mstate->__pyx_ptype_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen->tp_dictoffset && __pyx_mstate->__pyx_ptype_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen->tp_getattro == PyObject_GenericGetAttr)) { + __pyx_mstate->__pyx_ptype_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen->tp_getattro = PyObject_GenericGetAttr; + } + #endif + __Pyx_RefNannyFinishContext(); + return 0; + __pyx_L1_error:; + __Pyx_RefNannyFinishContext(); + return -1; +} + +static int __Pyx_modinit_Type_import_code(__pyx_mstatetype *__pyx_mstate) { + __Pyx_RefNannyDeclarations + CYTHON_UNUSED_VAR(__pyx_mstate); + __Pyx_RefNannySetupContext("__Pyx_modinit_Type_import_code", 0); + /*--- Type import code ---*/ + __Pyx_RefNannyFinishContext(); + return 0; +} + +static int __Pyx_modinit_Variable_import_code(__pyx_mstatetype *__pyx_mstate) { + __Pyx_RefNannyDeclarations + CYTHON_UNUSED_VAR(__pyx_mstate); + __Pyx_RefNannySetupContext("__Pyx_modinit_Variable_import_code", 0); + /*--- Variable import code ---*/ + __Pyx_RefNannyFinishContext(); + return 0; +} + +static int __Pyx_modinit_Function_import_code(__pyx_mstatetype *__pyx_mstate) { + __Pyx_RefNannyDeclarations + CYTHON_UNUSED_VAR(__pyx_mstate); + __Pyx_RefNannySetupContext("__Pyx_modinit_Function_import_code", 0); + /*--- Function import code ---*/ + __Pyx_RefNannyFinishContext(); + return 0; +} + +#if CYTHON_PEP489_MULTI_PHASE_INIT +static PyObject* __pyx_pymod_create(PyObject *spec, PyModuleDef *def); /*proto*/ +static int __pyx_pymod_exec_cu2qu(PyObject* module); /*proto*/ +static PyModuleDef_Slot __pyx_moduledef_slots[] = { + {Py_mod_create, (void*)__pyx_pymod_create}, + {Py_mod_exec, (void*)__pyx_pymod_exec_cu2qu}, + #if CYTHON_COMPILING_IN_CPYTHON_FREETHREADING + {Py_mod_gil, __Pyx_FREETHREADING_COMPATIBLE}, + #endif + #if PY_VERSION_HEX >= 0x030C0000 && CYTHON_USE_MODULE_STATE + {Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED}, + #endif + {0, NULL} +}; +#endif + +#ifdef __cplusplus +namespace { + struct PyModuleDef __pyx_moduledef = + #else + static struct PyModuleDef __pyx_moduledef = + #endif + { + PyModuleDef_HEAD_INIT, + "cu2qu", + 0, /* m_doc */ + #if CYTHON_USE_MODULE_STATE + sizeof(__pyx_mstatetype), /* m_size */ + #else + (CYTHON_PEP489_MULTI_PHASE_INIT) ? 0 : -1, /* m_size */ + #endif + __pyx_methods /* m_methods */, + #if CYTHON_PEP489_MULTI_PHASE_INIT + __pyx_moduledef_slots, /* m_slots */ + #else + NULL, /* m_reload */ + #endif + #if CYTHON_USE_MODULE_STATE + __pyx_m_traverse, /* m_traverse */ + __pyx_m_clear, /* m_clear */ + NULL /* m_free */ + #else + NULL, /* m_traverse */ + NULL, /* m_clear */ + NULL /* m_free */ + #endif + }; + #ifdef __cplusplus +} /* anonymous namespace */ +#endif + +/* PyModInitFuncType */ +#ifndef CYTHON_NO_PYINIT_EXPORT + #define __Pyx_PyMODINIT_FUNC PyMODINIT_FUNC +#else + #ifdef __cplusplus + #define __Pyx_PyMODINIT_FUNC extern "C" PyObject * + #else + #define __Pyx_PyMODINIT_FUNC PyObject * + #endif +#endif + +__Pyx_PyMODINIT_FUNC PyInit_cu2qu(void) CYTHON_SMALL_CODE; /*proto*/ +__Pyx_PyMODINIT_FUNC PyInit_cu2qu(void) +#if CYTHON_PEP489_MULTI_PHASE_INIT +{ + return PyModuleDef_Init(&__pyx_moduledef); +} +/* ModuleCreationPEP489 */ +#if CYTHON_COMPILING_IN_LIMITED_API && (__PYX_LIMITED_VERSION_HEX < 0x030A0000 &&\ + (defined(_WIN32) || defined(WIN32) || defined(MS_WINDOWS))) +static PY_INT64_T __Pyx_GetCurrentInterpreterId(void) { + { + PyObject *module = PyImport_ImportModule("_interpreters"); // 3.13+ I think + if (!module) { + if (!PyErr_ExceptionMatches(PyExc_Exception)) { + goto bad; // BaseException + } + PyErr_Clear(); // just try the 3.8-3.12 version + module = PyImport_ImportModule("_xxsubinterpreters"); + if (!module) goto bad; + } + PyObject *current = PyObject_CallMethod(module, "get_current", NULL); + Py_DECREF(module); + if (!current) goto bad; + if (PyTuple_Check(current)) { + PyObject *new_current = PySequence_GetItem(current, 0); + Py_DECREF(current); + current = new_current; + if (!new_current) goto bad; + } + long long as_c_int = PyLong_AsLongLong(current); + Py_DECREF(current); + return as_c_int; + } + bad: + PySys_WriteStderr("__Pyx_GetCurrentInterpreterId failed. Try setting the C define CYTHON_PEP489_MULTI_PHASE_INIT=0\n"); + return -1; +} +#endif +#if !CYTHON_USE_MODULE_STATE +static CYTHON_SMALL_CODE int __Pyx_check_single_interpreter(void) { + static PY_INT64_T main_interpreter_id = -1; +#if CYTHON_COMPILING_IN_GRAAL && defined(GRAALPY_VERSION_NUM) && GRAALPY_VERSION_NUM > 0x19000000 + PY_INT64_T current_id = GraalPyInterpreterState_GetIDFromThreadState(PyThreadState_Get()); +#elif CYTHON_COMPILING_IN_GRAAL + PY_INT64_T current_id = PyInterpreterState_GetIDFromThreadState(PyThreadState_Get()); +#elif CYTHON_COMPILING_IN_LIMITED_API && (__PYX_LIMITED_VERSION_HEX < 0x030A0000 &&\ + (defined(_WIN32) || defined(WIN32) || defined(MS_WINDOWS))) + PY_INT64_T current_id = __Pyx_GetCurrentInterpreterId(); +#elif CYTHON_COMPILING_IN_LIMITED_API + PY_INT64_T current_id = PyInterpreterState_GetID(PyInterpreterState_Get()); +#else + PY_INT64_T current_id = PyInterpreterState_GetID(PyThreadState_Get()->interp); +#endif + if (unlikely(current_id == -1)) { + return -1; + } + if (main_interpreter_id == -1) { + main_interpreter_id = current_id; + return 0; + } else if (unlikely(main_interpreter_id != current_id)) { + PyErr_SetString( + PyExc_ImportError, + "Interpreter change detected - this module can only be loaded into one interpreter per process."); + return -1; + } + return 0; +} +#endif +static CYTHON_SMALL_CODE int __Pyx_copy_spec_to_module(PyObject *spec, PyObject *moddict, const char* from_name, const char* to_name, int allow_none) +{ + PyObject *value = PyObject_GetAttrString(spec, from_name); + int result = 0; + if (likely(value)) { + if (allow_none || value != Py_None) { + result = PyDict_SetItemString(moddict, to_name, value); + } + Py_DECREF(value); + } else if (PyErr_ExceptionMatches(PyExc_AttributeError)) { + PyErr_Clear(); + } else { + result = -1; + } + return result; +} +static CYTHON_SMALL_CODE PyObject* __pyx_pymod_create(PyObject *spec, PyModuleDef *def) { + PyObject *module = NULL, *moddict, *modname; + CYTHON_UNUSED_VAR(def); + #if !CYTHON_USE_MODULE_STATE + if (__Pyx_check_single_interpreter()) + return NULL; + #endif + if (__pyx_m) + return __Pyx_NewRef(__pyx_m); + modname = PyObject_GetAttrString(spec, "name"); + if (unlikely(!modname)) goto bad; + module = PyModule_NewObject(modname); + Py_DECREF(modname); + if (unlikely(!module)) goto bad; + moddict = PyModule_GetDict(module); + if (unlikely(!moddict)) goto bad; + if (unlikely(__Pyx_copy_spec_to_module(spec, moddict, "loader", "__loader__", 1) < 0)) goto bad; + if (unlikely(__Pyx_copy_spec_to_module(spec, moddict, "origin", "__file__", 1) < 0)) goto bad; + if (unlikely(__Pyx_copy_spec_to_module(spec, moddict, "parent", "__package__", 1) < 0)) goto bad; + if (unlikely(__Pyx_copy_spec_to_module(spec, moddict, "submodule_search_locations", "__path__", 0) < 0)) goto bad; + return module; +bad: + Py_XDECREF(module); + return NULL; +} + + +static CYTHON_SMALL_CODE int __pyx_pymod_exec_cu2qu(PyObject *__pyx_pyinit_module) +#endif +{ + int stringtab_initialized = 0; + #if CYTHON_USE_MODULE_STATE + int pystate_addmodule_run = 0; + #endif + __pyx_mstatetype *__pyx_mstate = NULL; + PyObject *__pyx_t_1 = NULL; + PyObject *__pyx_t_2 = NULL; + PyObject *__pyx_t_3 = NULL; + PyObject *__pyx_t_4 = NULL; + Py_ssize_t __pyx_t_5; + PyObject *__pyx_t_6 = NULL; + double __pyx_t_7; + int __pyx_lineno = 0; + const char *__pyx_filename = NULL; + int __pyx_clineno = 0; + __Pyx_RefNannyDeclarations + #if CYTHON_PEP489_MULTI_PHASE_INIT + if (__pyx_m) { + if (__pyx_m == __pyx_pyinit_module) return 0; + PyErr_SetString(PyExc_RuntimeError, "Module 'cu2qu' has already been imported. Re-initialisation is not supported."); + return -1; + } + #else + if (__pyx_m) return __Pyx_NewRef(__pyx_m); + #endif + /*--- Module creation code ---*/ + #if CYTHON_PEP489_MULTI_PHASE_INIT + __pyx_t_1 = __pyx_pyinit_module; + Py_INCREF(__pyx_t_1); + #else + __pyx_t_1 = PyModule_Create(&__pyx_moduledef); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 1, __pyx_L1_error) + #endif + #if CYTHON_USE_MODULE_STATE + { + int add_module_result = __Pyx_State_AddModule(__pyx_t_1, &__pyx_moduledef); + __pyx_t_1 = 0; /* transfer ownership from __pyx_t_1 to "cu2qu" pseudovariable */ + if (unlikely((add_module_result < 0))) __PYX_ERR(0, 1, __pyx_L1_error) + pystate_addmodule_run = 1; + } + #else + __pyx_m = __pyx_t_1; + #endif + #if CYTHON_COMPILING_IN_CPYTHON_FREETHREADING + PyUnstable_Module_SetGIL(__pyx_m, Py_MOD_GIL_USED); + #endif + __pyx_mstate = __pyx_mstate_global; + CYTHON_UNUSED_VAR(__pyx_t_1); + __pyx_mstate->__pyx_d = PyModule_GetDict(__pyx_m); if (unlikely(!__pyx_mstate->__pyx_d)) __PYX_ERR(0, 1, __pyx_L1_error) + Py_INCREF(__pyx_mstate->__pyx_d); + __pyx_mstate->__pyx_b = __Pyx_PyImport_AddModuleRef(__Pyx_BUILTIN_MODULE_NAME); if (unlikely(!__pyx_mstate->__pyx_b)) __PYX_ERR(0, 1, __pyx_L1_error) + __pyx_mstate->__pyx_cython_runtime = __Pyx_PyImport_AddModuleRef("cython_runtime"); if (unlikely(!__pyx_mstate->__pyx_cython_runtime)) __PYX_ERR(0, 1, __pyx_L1_error) + if (PyObject_SetAttrString(__pyx_m, "__builtins__", __pyx_mstate->__pyx_b) < 0) __PYX_ERR(0, 1, __pyx_L1_error) + /* ImportRefnannyAPI */ + #if CYTHON_REFNANNY + __Pyx_RefNanny = __Pyx_RefNannyImportAPI("refnanny"); + if (!__Pyx_RefNanny) { + if (PyErr_ExceptionMatches(PyExc_Exception)) { + PyErr_Clear(); + __Pyx_RefNanny = __Pyx_RefNannyImportAPI("Cython.Runtime.refnanny"); + } + if (!__Pyx_RefNanny) + Py_FatalError("failed to import 'refnanny' module"); + } + #endif + +__Pyx_RefNannySetupContext("PyInit_cu2qu", 0); + __Pyx_init_runtime_version(); + if (__Pyx_check_binary_version(__PYX_LIMITED_VERSION_HEX, __Pyx_get_runtime_version(), CYTHON_COMPILING_IN_LIMITED_API) < (0)) __PYX_ERR(0, 1, __pyx_L1_error) + __pyx_mstate->__pyx_empty_tuple = PyTuple_New(0); if (unlikely(!__pyx_mstate->__pyx_empty_tuple)) __PYX_ERR(0, 1, __pyx_L1_error) + __pyx_mstate->__pyx_empty_bytes = PyBytes_FromStringAndSize("", 0); if (unlikely(!__pyx_mstate->__pyx_empty_bytes)) __PYX_ERR(0, 1, __pyx_L1_error) + __pyx_mstate->__pyx_empty_unicode = PyUnicode_FromStringAndSize("", 0); if (unlikely(!__pyx_mstate->__pyx_empty_unicode)) __PYX_ERR(0, 1, __pyx_L1_error) + /*--- Library function declarations ---*/ + /*--- Initialize various global constants etc. ---*/ + if (__Pyx_InitConstants(__pyx_mstate) < (0)) __PYX_ERR(0, 1, __pyx_L1_error) + stringtab_initialized = 1; + if (__Pyx_InitGlobals() < (0)) __PYX_ERR(0, 1, __pyx_L1_error) + if (__pyx_module_is_main_fontTools__cu2qu__cu2qu) { + if (PyObject_SetAttr(__pyx_m, __pyx_mstate_global->__pyx_n_u_name, __pyx_mstate_global->__pyx_n_u_main) < (0)) __PYX_ERR(0, 1, __pyx_L1_error) + } + { + PyObject *modules = PyImport_GetModuleDict(); if (unlikely(!modules)) __PYX_ERR(0, 1, __pyx_L1_error) + if (!PyDict_GetItemString(modules, "fontTools.cu2qu.cu2qu")) { + if (unlikely((PyDict_SetItemString(modules, "fontTools.cu2qu.cu2qu", __pyx_m) < 0))) __PYX_ERR(0, 1, __pyx_L1_error) + } + } + /*--- Builtin init code ---*/ + if (__Pyx_InitCachedBuiltins(__pyx_mstate) < (0)) __PYX_ERR(0, 1, __pyx_L1_error) + /*--- Constants init code ---*/ + if (__Pyx_InitCachedConstants(__pyx_mstate) < (0)) __PYX_ERR(0, 1, __pyx_L1_error) + if (__Pyx_CreateCodeObjects(__pyx_mstate) < (0)) __PYX_ERR(0, 1, __pyx_L1_error) + /*--- Global type/function init code ---*/ + (void)__Pyx_modinit_Global_init_code(__pyx_mstate); + (void)__Pyx_modinit_Variable_export_code(__pyx_mstate); + (void)__Pyx_modinit_Function_export_code(__pyx_mstate); + /*--- Type init code ---*/ + if (unlikely((__Pyx_modinit_Exttype___pyx_obj_9fontTools_5cu2qu_5cu2qu___pyx_scope_struct___split_cubic_into_n_gen(__pyx_mstate) < 0))) __PYX_ERR(0, 1, __pyx_L1_error) + (void)__Pyx_modinit_Type_import_code(__pyx_mstate); + (void)__Pyx_modinit_Variable_import_code(__pyx_mstate); + (void)__Pyx_modinit_Function_import_code(__pyx_mstate); + if (__Pyx_InitAfterSharedUtility() < (0)) __PYX_ERR(0, 1, __pyx_L1_error) + /*--- Execution code ---*/ + + /* "fontTools/cu2qu/cu2qu.py":18 + * # limitations under the License. + * + * try: # <<<<<<<<<<<<<< + * import cython + * except (AttributeError, ImportError): +*/ + { + (void)__pyx_t_1; (void)__pyx_t_2; (void)__pyx_t_3; /* mark used */ + /*try:*/ { + + /* "fontTools/cu2qu/cu2qu.py":19 + * + * try: + * import cython # <<<<<<<<<<<<<< + * except (AttributeError, ImportError): + * # if cython not installed, use mock module with no-op decorators and types +*/ + } + } + + /* "fontTools/cu2qu/cu2qu.py":23 + * # if cython not installed, use mock module with no-op decorators and types + * from fontTools.misc import cython + * COMPILED = cython.compiled # <<<<<<<<<<<<<< + * + * import math +*/ + if (PyDict_SetItem(__pyx_mstate_global->__pyx_d, __pyx_mstate_global->__pyx_n_u_COMPILED, Py_True) < (0)) __PYX_ERR(0, 23, __pyx_L1_error) + + /* "fontTools/cu2qu/cu2qu.py":25 + * COMPILED = cython.compiled + * + * import math # <<<<<<<<<<<<<< + * + * from .errors import Error as Cu2QuError, ApproxNotFoundError +*/ + __pyx_t_3 = __Pyx_Import(__pyx_mstate_global->__pyx_n_u_math, 0, 0, NULL, 0); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 25, __pyx_L1_error) + __pyx_t_4 = __pyx_t_3; + __Pyx_GOTREF(__pyx_t_4); + if (PyDict_SetItem(__pyx_mstate_global->__pyx_d, __pyx_mstate_global->__pyx_n_u_math, __pyx_t_4) < (0)) __PYX_ERR(0, 25, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0; + + /* "fontTools/cu2qu/cu2qu.py":27 + * import math + * + * from .errors import Error as Cu2QuError, ApproxNotFoundError # <<<<<<<<<<<<<< + * + * __all__ = ["curve_to_quadratic", "curves_to_quadratic"] +*/ + { + PyObject* const __pyx_imported_names[] = {__pyx_mstate_global->__pyx_n_u_Error,__pyx_mstate_global->__pyx_n_u_ApproxNotFoundError}; + __pyx_t_3 = __Pyx_Import(__pyx_mstate_global->__pyx_n_u_errors, __pyx_imported_names, 2, __pyx_mstate_global->__pyx_kp_u_fontTools_cu2qu_errors, 1); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 27, __pyx_L1_error) + } + __pyx_t_4 = __pyx_t_3; + __Pyx_GOTREF(__pyx_t_4); + { + PyObject* const __pyx_imported_names[] = {__pyx_mstate_global->__pyx_n_u_Error,__pyx_mstate_global->__pyx_n_u_ApproxNotFoundError}; + for (__pyx_t_5=0; __pyx_t_5 < 2; __pyx_t_5++) { + __pyx_t_6 = __Pyx_ImportFrom(__pyx_t_4, __pyx_imported_names[__pyx_t_5]); if (unlikely(!__pyx_t_6)) __PYX_ERR(0, 27, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_6); + switch (__pyx_t_5) { + case 0: + if (PyDict_SetItem(__pyx_mstate_global->__pyx_d, __pyx_mstate_global->__pyx_n_u_Cu2QuError, __pyx_t_6) < (0)) __PYX_ERR(0, 27, __pyx_L1_error) + break; + default:; + if (PyDict_SetItem(__pyx_mstate_global->__pyx_d, __pyx_imported_names[__pyx_t_5], __pyx_t_6) < (0)) __PYX_ERR(0, 27, __pyx_L1_error) + } + __Pyx_DECREF(__pyx_t_6); __pyx_t_6 = 0; + } + } + __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0; + + /* "fontTools/cu2qu/cu2qu.py":29 + * from .errors import Error as Cu2QuError, ApproxNotFoundError + * + * __all__ = ["curve_to_quadratic", "curves_to_quadratic"] # <<<<<<<<<<<<<< + * + * MAX_N = 100 +*/ + { + PyObject* __pyx_temp[2] = {__pyx_mstate_global->__pyx_n_u_curve_to_quadratic, __pyx_mstate_global->__pyx_n_u_curves_to_quadratic}; + __pyx_t_4 = __Pyx_PyList_FromArray(__pyx_temp, 2); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 29, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_4); + } + if (PyDict_SetItem(__pyx_mstate_global->__pyx_d, __pyx_mstate_global->__pyx_n_u_all, __pyx_t_4) < (0)) __PYX_ERR(0, 29, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0; + + /* "fontTools/cu2qu/cu2qu.py":31 + * __all__ = ["curve_to_quadratic", "curves_to_quadratic"] + * + * MAX_N = 100 # <<<<<<<<<<<<<< + * + * NAN = float("NaN") +*/ + if (PyDict_SetItem(__pyx_mstate_global->__pyx_d, __pyx_mstate_global->__pyx_n_u_MAX_N, __pyx_mstate_global->__pyx_int_100) < (0)) __PYX_ERR(0, 31, __pyx_L1_error) + + /* "fontTools/cu2qu/cu2qu.py":33 + * MAX_N = 100 + * + * NAN = float("NaN") # <<<<<<<<<<<<<< + * + * +*/ + __pyx_t_7 = __Pyx_PyUnicode_AsDouble(__pyx_mstate_global->__pyx_n_u_NaN); if (unlikely(__PYX_CHECK_FLOAT_EXCEPTION(__pyx_t_7, ((double)((double)-1))) && PyErr_Occurred())) __PYX_ERR(0, 33, __pyx_L1_error) + __pyx_t_4 = PyFloat_FromDouble(__pyx_t_7); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 33, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_4); + + if (PyDict_SetItem(__pyx_mstate_global->__pyx_d, __pyx_mstate_global->__pyx_n_u_NAN, __pyx_t_4) < (0)) __PYX_ERR(0, 33, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0; + + /* "fontTools/cu2qu/cu2qu.py":149 + * + * + * @cython.locals( # <<<<<<<<<<<<<< + * p0=cython.complex, + * p1=cython.complex, +*/ + __pyx_t_4 = __Pyx_CyFunction_New(&__pyx_mdef_9fontTools_5cu2qu_5cu2qu_1_split_cubic_into_n_gen, 0, __pyx_mstate_global->__pyx_n_u_split_cubic_into_n_gen, NULL, __pyx_mstate_global->__pyx_n_u_fontTools_cu2qu_cu2qu, __pyx_mstate_global->__pyx_d, ((PyObject *)__pyx_mstate_global->__pyx_codeobj_tab[0])); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 149, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_4); + #if CYTHON_COMPILING_IN_CPYTHON && PY_VERSION_HEX >= 0x030E0000 + PyUnstable_Object_EnableDeferredRefcount(__pyx_t_4); + #endif + if (PyDict_SetItem(__pyx_mstate_global->__pyx_d, __pyx_mstate_global->__pyx_n_u_split_cubic_into_n_gen, __pyx_t_4) < (0)) __PYX_ERR(0, 149, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0; + + /* "fontTools/cu2qu/cu2qu.py":470 + * @cython.locals(n=cython.int) + * @cython.locals(all_quadratic=cython.int) + * def curve_to_quadratic(curve, max_err, all_quadratic=True): # <<<<<<<<<<<<<< + * """Approximate a cubic Bezier curve with a spline of n quadratics. + * +*/ + __pyx_t_4 = __Pyx_PyBool_FromLong(((int)1)); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 470, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_4); + + /* "fontTools/cu2qu/cu2qu.py":467 + * + * + * @cython.locals(max_err=cython.double) # <<<<<<<<<<<<<< + * @cython.locals(n=cython.int) + * @cython.locals(all_quadratic=cython.int) +*/ + { + PyObject* __pyx_temp[1] = {__pyx_t_4}; + __pyx_t_6 = __Pyx_PyTuple_FromArray(__pyx_temp, 1); if (unlikely(!__pyx_t_6)) __PYX_ERR(0, 467, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_6); + } + __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0; + __pyx_t_4 = __Pyx_CyFunction_New(&__pyx_mdef_9fontTools_5cu2qu_5cu2qu_4curve_to_quadratic, 0, __pyx_mstate_global->__pyx_n_u_curve_to_quadratic, NULL, __pyx_mstate_global->__pyx_n_u_fontTools_cu2qu_cu2qu, __pyx_mstate_global->__pyx_d, ((PyObject *)__pyx_mstate_global->__pyx_codeobj_tab[1])); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 467, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_4); + #if CYTHON_COMPILING_IN_CPYTHON && PY_VERSION_HEX >= 0x030E0000 + PyUnstable_Object_EnableDeferredRefcount(__pyx_t_4); + #endif + __Pyx_CyFunction_SetDefaultsTuple(__pyx_t_4, __pyx_t_6); + __Pyx_DECREF(__pyx_t_6); __pyx_t_6 = 0; + if (PyDict_SetItem(__pyx_mstate_global->__pyx_d, __pyx_mstate_global->__pyx_n_u_curve_to_quadratic, __pyx_t_4) < (0)) __PYX_ERR(0, 467, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0; + + /* "fontTools/cu2qu/cu2qu.py":506 + * @cython.locals(l=cython.int, last_i=cython.int, i=cython.int) + * @cython.locals(all_quadratic=cython.int) + * def curves_to_quadratic(curves, max_errors, all_quadratic=True): # <<<<<<<<<<<<<< + * """Return quadratic Bezier splines approximating the input cubic Beziers. + * +*/ + __pyx_t_4 = __Pyx_PyBool_FromLong(((int)1)); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 506, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_4); + + /* "fontTools/cu2qu/cu2qu.py":504 + * + * + * @cython.locals(l=cython.int, last_i=cython.int, i=cython.int) # <<<<<<<<<<<<<< + * @cython.locals(all_quadratic=cython.int) + * def curves_to_quadratic(curves, max_errors, all_quadratic=True): +*/ + { + PyObject* __pyx_temp[1] = {__pyx_t_4}; + __pyx_t_6 = __Pyx_PyTuple_FromArray(__pyx_temp, 1); if (unlikely(!__pyx_t_6)) __PYX_ERR(0, 504, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_6); + } + __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0; + __pyx_t_4 = __Pyx_CyFunction_New(&__pyx_mdef_9fontTools_5cu2qu_5cu2qu_6curves_to_quadratic, 0, __pyx_mstate_global->__pyx_n_u_curves_to_quadratic, NULL, __pyx_mstate_global->__pyx_n_u_fontTools_cu2qu_cu2qu, __pyx_mstate_global->__pyx_d, ((PyObject *)__pyx_mstate_global->__pyx_codeobj_tab[2])); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 504, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_4); + #if CYTHON_COMPILING_IN_CPYTHON && PY_VERSION_HEX >= 0x030E0000 + PyUnstable_Object_EnableDeferredRefcount(__pyx_t_4); + #endif + __Pyx_CyFunction_SetDefaultsTuple(__pyx_t_4, __pyx_t_6); + __Pyx_DECREF(__pyx_t_6); __pyx_t_6 = 0; + if (PyDict_SetItem(__pyx_mstate_global->__pyx_d, __pyx_mstate_global->__pyx_n_u_curves_to_quadratic, __pyx_t_4) < (0)) __PYX_ERR(0, 504, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0; + + /* "fontTools/cu2qu/cu2qu.py":1 + * # cython: language_level=3 # <<<<<<<<<<<<<< + * # distutils: define_macros=CYTHON_TRACE_NOGIL=1 + * +*/ + __pyx_t_4 = __Pyx_PyDict_NewPresized(1); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 1, __pyx_L1_error) + __Pyx_GOTREF(__pyx_t_4); + if (PyDict_SetItem(__pyx_t_4, __pyx_mstate_global->__pyx_kp_u_curves_to_quadratic_line_504, __pyx_mstate_global->__pyx_kp_u_Return_quadratic_Bezier_splines) < (0)) __PYX_ERR(0, 1, __pyx_L1_error) + if (PyDict_SetItem(__pyx_mstate_global->__pyx_d, __pyx_mstate_global->__pyx_n_u_test, __pyx_t_4) < (0)) __PYX_ERR(0, 1, __pyx_L1_error) + __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0; + + /*--- Wrapped vars code ---*/ + + goto __pyx_L0; + __pyx_L1_error:; + __Pyx_XDECREF(__pyx_t_4); + __Pyx_XDECREF(__pyx_t_6); + if (__pyx_m) { + if (__pyx_mstate->__pyx_d && stringtab_initialized) { + __Pyx_AddTraceback("init fontTools.cu2qu.cu2qu", __pyx_clineno, __pyx_lineno, __pyx_filename); + } + #if !CYTHON_USE_MODULE_STATE + Py_CLEAR(__pyx_m); + #else + Py_DECREF(__pyx_m); + if (pystate_addmodule_run) { + PyObject *tp, *value, *tb; + __Pyx_PyErr_FetchException(&tp, &value, &tb); + __Pyx_State_RemoveModule(&__pyx_moduledef); + __Pyx_PyErr_RestoreException(tp, value, tb); + } + #endif + } else if (!PyErr_Occurred()) { + PyErr_SetString(PyExc_ImportError, "init fontTools.cu2qu.cu2qu"); + } + __pyx_L0:; + __Pyx_RefNannyFinishContext(); + #if CYTHON_PEP489_MULTI_PHASE_INIT + return (__pyx_m != NULL) ? 0 : -1; + #else + return __pyx_m; + #endif +} +/* #### Code section: pystring_table ### */ +/* #### Code section: cached_builtins ### */ + +static int __Pyx_InitCachedBuiltins(__pyx_mstatetype *__pyx_mstate) { + CYTHON_UNUSED_VAR(__pyx_mstate); + + /* Cached unbound methods */ + __pyx_mstate->__pyx_umethod_PyDict_Type_items.type = (PyObject*)&PyDict_Type; + __pyx_mstate->__pyx_umethod_PyDict_Type_items.method_name = &__pyx_mstate->__pyx_n_u_items; + __pyx_mstate->__pyx_umethod_PyDict_Type_pop.type = (PyObject*)&PyDict_Type; + __pyx_mstate->__pyx_umethod_PyDict_Type_pop.method_name = &__pyx_mstate->__pyx_n_u_pop; + __pyx_mstate->__pyx_umethod_PyDict_Type_values.type = (PyObject*)&PyDict_Type; + __pyx_mstate->__pyx_umethod_PyDict_Type_values.method_name = &__pyx_mstate->__pyx_n_u_values; + return 0; +} +/* #### Code section: cached_constants ### */ + +static int __Pyx_InitCachedConstants(__pyx_mstatetype *__pyx_mstate) { + __Pyx_RefNannyDeclarations + CYTHON_UNUSED_VAR(__pyx_mstate); + __Pyx_RefNannySetupContext("__Pyx_InitCachedConstants", 0); + __Pyx_RefNannyFinishContext(); + return 0; +} +/* #### Code section: init_constants ### */ + +static int __Pyx_InitConstants(__pyx_mstatetype *__pyx_mstate) { + int __pyx_lineno = 0; + const char *__pyx_filename = NULL; + int __pyx_clineno = 0; + CYTHON_UNUSED_VAR(__pyx_mstate); + { + const struct { const unsigned int length: 11; } str_length_index[] = {{1},{1},{28},{1688},{30},{7},{6},{22},{2},{9},{42},{19},{8},{10},{5},{5},{3},{3},{20},{7},{12},{8},{8},{10},{8},{12},{8},{13},{23},{1},{2},{13},{18},{1},{2},{1},{2},{18},{5},{5},{18},{6},{19},{1},{2},{7},{7},{2},{6},{21},{1},{4},{5},{5},{1},{6},{4},{7},{10},{1},{4},{1},{2},{2},{2},{2},{3},{4},{1},{4},{10},{6},{7},{2},{4},{5},{5},{6}}; + const struct { const unsigned int length: 8; } bytes_length_index[] = {{2},{232},{97}}; + #ifndef CYTHON_COMPRESS_STRINGS + #define CYTHON_COMPRESS_STRINGS 90 + #endif + #if (CYTHON_COMPRESS_STRINGS) == 1 /* compression: zlib (1341 bytes) */ +static const char cstring[] = "x\332}UOo\3336\024\257\321d3\262\244]\320\3658\200i7\314\356T\325r\342\2445\266\016n\323\016\005Z\257\316\322v@\020(\264D;\334$R\021\251$\336)G\035u\3241G\037{\314\261\037\241G\037\363\021\372\021\366H\311\222\275\006\025 \212\"\371\376\375\336\357=\232\277\275\244\375\007\003\316\344.\347\236x\340D\315\243(\033\315`\264Cd\0242t\024a7\304\222:\350\t\371\227\222\020\211\300\243\214\010\204\203 \344\247\324\207=6D\362\220 \312\202H\"\047\352\027\207\205\271\264\204\340\351\204C\321\3263\3658QxLD\033u\220 G\021a\016A|\200\356\261{\371\216\201\010v\016\263\037\324\047J=\236;:\340QXhSOs\033\311(\360\010\330\233.\371\370\324&a\310\303+\r\r<\216\245@!\tB\"\010+B\000)\352G>\nH\350S!h\337#s\206\\rL!`\316\320 \344~\346\047\250T\242\263q\347\201\224\336`\317\263K$k}\300\273\336F/\006h7\214\010\252\271d\200#O\326\301!\005:q\3211\366\"\005rH\020\236\363\240\324\222%\302TZ\236cO\020C\2711\322\"\204\3024T\250A`\036)\205\346Te\000\363\231sY\014z=\317\334\263S\354\003\260\355\366R!\372\370\361\343<>[\3622\252\032\332+\216\230\246\t\343\036\252\265\032F\253Q7P\315j4\014x\365\264UL\233\r\275\217\366\215\317E\267ZS\321\346\234\350V\013f\231\350\246\022\235\223\3347\320\236eX\373\250^,\357\355\201\027f\303@jTJ\266Z\352O\215\231v\375\373\31027\347\036\2757\177\024L\026z\224\241\\\323T\357\243-\2635\243w\275enX\263\032\r\364\260i6\036\256\317<3F6Az\306\310\2466\262\237\241\276\013\364*\2301\255\277C\014\311\273C!;\024V9\313\262\031p\312\244\270\203DD%\006\366B\255\204(\022\25283U\300\267\335Q@\320\301\301\320\243\203\203\003\304#\231)\274\217\250\tt\242L\323y@C!sc\245q\334\347\307$KVyh\206\222d\350C5\2410b\"\253\220\234\001Hr-TS\360\243\237\021\240^\177\320DYv\341?\203\3374\325b\035\375\232\361\005=\\7\001$\310k=gc\326\223fZ\tp\177\276\264\250\320!\032\300i\217\202oP\2349`yW\311#\232\266\025u\250\320\006\207\313N\242t\037\034dD\007\230@1\361\00392r0\2406Y\266\242u\230K_\364)/\320\322\251/t\272\317\213\265\346\0216\224\207H\361\205\207y\225N\0277\np0\025d\006\233\267\252\205\336\030\217.*\223\205\257\317D\274\026\377\222\210\364n\332K\361\244z#\376;\255\244\253jG\306\326\244z3>\272\\\250\306\327c+\356L\026n\200\226\343d\0479\232,\254\304\273\t\034\253\306\240eqR]\375\270\372\303y\347\374\355\330\032o\277\277\376\276}\321\273p?\254M\252\337\304[\311\335\2447Y\276\031\207\311\255\2443\371vu\262\274\n\222\313\267\325\352\212\266\220,\302\274\272\022\357\3503O\022\254\344\232\361\237I\345r\371Vb\251\225\344$\305\351\311\271;\376q|\372^^\264?\364.\027\226>.}\257\274>\253|\372\352Z\255\376\311\274\266\270\022w\342wp\336I\277K\337\234\257)\327\027\3437\311Z\262\236\014\322\047*\300\334\321w\343\247\032\001\355\340O\351\2522\324Lz\311\020\024\016\317w\307\267\307\2704\360\037I\272TY"; + PyObject *data = __Pyx_DecompressString(cstring, 1341, 1); + #define __Pyx_DecompressString_LZSS_UNUSED + if (unlikely(!data)) __PYX_ERR(0, 1, __pyx_L1_error) + const char* const bytes = __Pyx_PyBytes_AsString(data); + #if !CYTHON_ASSUME_SAFE_MACROS + if (likely(bytes)); else { Py_DECREF(data); __PYX_ERR(0, 1, __pyx_L1_error) } + #endif + #elif (CYTHON_COMPRESS_STRINGS) > 0 && (CYTHON_COMPRESS_STRINGS) <= 90 /* compression: lzss (1758 bytes) */ +static const char cstring[] = "\377.?Lib/fo\377ntTools/\337cu2qu\000\003.p\377yReturn \377quadrati\377c Bezier\377 splines\377 approxi\375m\031\000ng the\377 input c\373ub$\006s.\n\n \377 Args:\374\005\002\n\001curves\377: A sequ\377ence of \357*n* \024\003, e\367ach\007\003 beiz\\\000a\036\nfourE\006\376\\\0012D tupl\361em\000k\001o\001max_\337errorT\022fl\377oats rep\177resenti\274\004\3763\000imum pe\377rmissibl\275e\\\ndevi\212 o?n from\242\003\271\000\350\372\001\354\t\302\004.\352\006all\355_\300\047(b\355 ): \377If True \377(default\371)\216\000\361!ed va{lu\341!re a\336\n\364\206G\203C.B\001Fals\273e,\201Ay a0\000e\315i\215@r \263 \272 le\030\273G\251*\203B o\035\010\266C\233B\336\263DExam\326 ::\335\n\266F>>>\242D_t\335o\314\007( [\331F..\375.\364@[ (50,\37750), (10\3230,\001\000\006\0025\002\00620n\033\002 ],)\017753\005m2.\n75M\000 (7\001\33760) ]h\n],\377 [1,1] )\366\345f[[\203\000.0, 750.\206\00275\n\001\002\001\372\224\0022\n\00291.66\0316\000\000\000\00366\260\002\035\013\253\000\344?\006e\001(D\003P\00597.\3675, O\00635.4\3751H\t, 82.0\2178333\000\000\000\003Z\0106h;\000\245!^\0046\251\001]]\271\205\003\347The\220g\370\205\005hav\257e \"i\233@i\255`o\375n\310Cpoints\377\" suitab\032\370@f\335@us\231\206\001\205\206\002\354a\377Type ``g\377lif`` ou\375t\313\206\003- i.e.\367 in\310\206\002firs\371t\346\206\004\215\204\007above\360\247C\356\206\001\037\003\210\204\010egme\177nt runs\234\205\003n\373D to\204\207\002( \262 \377 + 125)/\2332 \225a20\r\000\371#.\335.\021\001) =\252c 8\0233.\253 \312`)\301\207\004\216\210\003\300\207\010\267If \315\205\013is\316\205\002,\337 a li\276\000of\300\251\210\005\317\207\004\271\210\003\316\207\006!\001\220\210\006of\031 \300\207\007\232\206\001``\246\210\003\244 V\000\277empty,\235\206\004sO an \r\002d\002.\226\205\007\300\200\021\232\206\004\213\007\317\210\030\351\211\006\250\211\006(l\377ength 3)\321,\253\206\001\334\211\003\r\0054\224&ai\253se\334\211\010V\277\207\001E\377\210\001:\307 if\356@\211\211\007\364@do\377es not m\357atch\273""\212\002num\207ber\210\212\001\234\212\003\265\210\007\233\213\006.\332\237\213\002.\323\211\003.A\376\212\002No\277tFounda\007n\231o\366g\234\213\010on\371\212\007\272 b\047e f4\001\003\000r\302A\367\212\004\357 wit\212\003giv\377en param\217eter\326\212\004\270\213\003\356\207\n \375(\217\214\001 504)d\233is\363\204\001en\371\204\001\241\023g\351c\245 \033\002d\233\213\007 mu\371s\360\031\330\020COMPI\377LEDCu2Qu\374\343\"\350\"MAX_NN\377ANNaN__P\373yx\001\000Dict_\377NextRef_\325_\210\213\001_\004\000n\373 at}e\010\000_func\004\001\357main\003\002odu\345l\025\002n\371\000\036\001qua\275l\004\005test>\000i\377s_coroutoine_\222\216\001t_\367\215\002\377_into_n_\355g\202 a1\355\213\nasy\337ncio./\006sb?b1cc1c\317\216\0015\000\377_traceba\177ckclose\244\216\002\360\251\216\002\335\212\n\272\216\003\3650dd1d\277elta_2\001\0033\343dt\375\215\003\260m\345\217\002iim\377agisnani\177temslla\324\000\262\314\217\001h\265\216\004\271\216\007nn\272 p\377p0p1p2p3\377popreals\177sendset\266\215\004\374\221\220\004\230\220\003t1t1_2\237throw\305\215\002\311\215\003\200\377\001\200\001\340,-\360L\377\001\000\005\016\210Q\210a\377\210w\220b\230\003\2304\377\230u\240G\2504\250y\377\270\001\330\004\007\200s\210\377!\210<\220s\230#\230\377Q\230a\330\010\016\210j\357\230\001\230\021\026\001t\2101\377\330\010\017\210q\340\004\010\377\210\003\2101\210A\330\004\375\016C\000v\220R\220q\330\337\004\r\210T\220$\000\010\210\376C\000\005\330\010\021\320\021$\377\240A\240V\2501\250D\377\260\003\260:\270Q\270d\377\300!\330\010\013\2107\220\377#\220Q\330\014\017\210r\377\220\023\220A\330\020\021\330[\014\0217\000\014\025\022\001\r]\002{\220\005\037\000\010\r\210R\037\002\373B\220\205\000\013\2102\210S\377\220\001\340\014\023\2201\220\376\017\000\220w\230a\230w\240\377d\250%\250x\260t\270\377:\300Q\340\004\n\320\n\375\035\262\001\200\001\360\006\000(\377)\360.\000\005\r\210A\373\210W-\000c\230\024\230U\373\240!\267\001\005\210U\220!?\2203\220f\230B\335\001\242\004\347W\250C\375\002\234\002\047""\230\021\276e\0012\220Q\220g\204 g?\240T\250\025\250aW\007"; + PyObject *data = __Pyx_DecompressString_LZSS(cstring, 1758, 2620); + #define __Pyx_DecompressString_UNUSED + if (unlikely(!data)) __PYX_ERR(0, 1, __pyx_L1_error) + const char* const bytes = __Pyx_PyBytes_AsString(data); + #if !CYTHON_ASSUME_SAFE_MACROS + if (likely(bytes)); else { Py_DECREF(data); __PYX_ERR(0, 1, __pyx_L1_error) } + #endif + #else /* compression: none (2620 bytes) */ +static const char bytes[] = ".?Lib/fontTools/cu2qu/cu2qu.pyReturn quadratic Bezier splines approximating the input cubic Beziers.\n\n Args:\n curves: A sequence of *n* curves, each curve being a sequence of four\n 2D tuples.\n max_errors: A sequence of *n* floats representing the maximum permissible\n deviation from each of the cubic Bezier curves.\n all_quadratic (bool): If True (default) returned values are a\n quadratic spline. If False, they are either a single quadratic\n curve or a single cubic curve.\n\n Example::\n\n >>> curves_to_quadratic( [\n ... [ (50,50), (100,100), (150,100), (200,50) ],\n ... [ (75,50), (120,100), (150,75), (200,60) ]\n ... ], [1,1] )\n [[(50.0, 50.0), (75.0, 75.0), (125.0, 91.66666666666666), (175.0, 75.0), (200.0, 50.0)], [(75.0, 50.0), (97.5, 75.0), (135.41666666666666, 82.08333333333333), (175.0, 67.5), (200.0, 60.0)]]\n\n The returned splines have \"implied oncurve points\" suitable for use in\n TrueType ``glif`` outlines - i.e. in the first spline returned above,\n the first quadratic segment runs from (50,50) to\n ( (75 + 125)/2 , (120 + 91.666..)/2 ) = (100, 83.333...).\n\n Returns:\n If all_quadratic is True, a list of splines, each spline being a list\n of 2D tuples. If ``curves`` is empty, returns an empty list.\n\n If all_quadratic is False, a list of curves, each curve being a quadratic\n (length 3), or cubic (length 4).\n\n Raises:\n ValueError: if ``max_errors`` does not match the number of curves.\n fontTools.cu2qu.errors.ApproxNotFoundError: if no suitable approximation\n can be found for all curves with the given parameters.\n curves_to_quadratic (line 504)disableenablefontTools.cu2qu.errorsgcisenabledmax_errors must match the number of curvesApproxNotFoundErrorCOMPILEDCu2QuErrorErrorMAX_NNANNaN__Pyx_PyDict_NextRef__all____annotate____func____main____module____name___""_qualname____test___is_coroutine_split_cubic_into_n_genaa1all_quadraticasyncio.coroutinesbb1cc1cline_in_tracebackclosecurvecurve_to_quadraticcurvescurves_to_quadraticdd1delta_2delta_3dterrorsfontTools.cu2qu.cu2quiimagisnanitemsllast_imathmax_errmax_errorsnnextpp0p1p2p3poprealssendsetdefaultsplinesplinest1t1_2throwvaluevalues\200\001\200\001\340,-\360L\001\000\005\016\210Q\210a\210w\220b\230\003\2304\230u\240G\2504\250y\270\001\330\004\007\200s\210!\210<\220s\230#\230Q\230a\330\010\016\210j\230\001\230\021\330\004\007\200t\2101\330\010\017\210q\340\004\010\210\003\2101\210A\330\004\016\210a\210v\220R\220q\330\004\r\210T\220\021\330\004\010\210\001\330\004\005\330\010\021\320\021$\240A\240V\2501\250D\260\003\260:\270Q\270d\300!\330\010\013\2107\220#\220Q\330\014\017\210r\220\023\220A\330\020\021\330\014\021\220\021\330\014\025\220Q\330\014\r\330\010\017\210q\220\005\220Q\330\010\r\210R\210r\220\023\220B\220a\330\010\013\2102\210S\220\001\340\014\023\2201\220B\220a\220w\230a\230w\240d\250%\250x\260t\270:\300Q\340\004\n\320\n\035\230Q\230a\200\001\360\006\000()\360.\000\005\r\210A\210W\220B\220c\230\024\230U\240!\340\004\010\210\005\210U\220!\2203\220f\230B\230a\330\010\021\320\021$\240A\240W\250C\250y\270\001\330\010\013\2107\220\047\230\021\340\014\023\2202\220Q\220g\230Q\230g\240T\250\025\250a\340\004\n\320\n\035\230Q\230a"; + PyObject *data = NULL; + #define __Pyx_DecompressString_UNUSED + #define __Pyx_DecompressString_LZSS_UNUSED + #endif + PyObject **stringtab = __pyx_mstate->__pyx_string_tab; + Py_ssize_t pos = 0; + for (int i = 0; i < 78; i++) { + Py_ssize_t bytes_length = str_length_index[i].length; + PyObject *string = PyUnicode_DecodeUTF8(bytes + pos, bytes_length, NULL); + if (likely(string) && i >= 11) PyUnicode_InternInPlace(&string); + if (unlikely(!string)) { + Py_XDECREF(data); + __PYX_ERR(0, 1, __pyx_L1_error) + } + stringtab[i] = string; + pos += bytes_length; + } + for (int i = 78; i < 81; i++) { + Py_ssize_t bytes_length = bytes_length_index[i-78].length; + PyObject *string = PyBytes_FromStringAndSize(bytes + pos, bytes_length); + stringtab[i] = string; + pos += bytes_length; + if (unlikely(!string)) { + Py_XDECREF(data); + __PYX_ERR(0, 1, __pyx_L1_error) + } + } + Py_XDECREF(data); + for (Py_ssize_t i = 0; i < 81; i++) { + if (unlikely(PyObject_Hash(stringtab[i]) == -1)) { + __PYX_ERR(0, 1, __pyx_L1_error) + } + } + #if CYTHON_IMMORTAL_CONSTANTS + { + PyObject **table = stringtab + 78; + for (Py_ssize_t i=0; i<3; ++i) { + #if PY_VERSION_HEX >= 0x030F0000 + PyUnstable_SetImmortal(table[i]); + #elif CYTHON_COMPILING_IN_CPYTHON_FREETHREADING + if ((PY_SSIZE_T_MAX <= _Py_IMMORTAL_REFCNT_LOCAL)) break; + #if PY_VERSION_HEX < 0x030E0000 + if (_Py_IsOwnedByCurrentThread(table[i]) && Py_REFCNT(table[i]) == 1) + #else + if (PyUnstable_Object_IsUniquelyReferenced(table[i])) + #endif + { + Py_SET_REFCNT(table[i], ((Py_ssize_t)_Py_IMMORTAL_REFCNT_LOCAL + 1)); + } + #else + if ((PY_SSIZE_T_MAX < _Py_IMMORTAL_INITIAL_REFCNT)) break; + Py_SET_REFCNT(table[i], _Py_IMMORTAL_INITIAL_REFCNT); + #endif + } + } + #endif + } + { + PyObject **numbertab = __pyx_mstate->__pyx_number_tab + 0; + int8_t const cint_constants_1[] = {1,2,3,4,6,100}; + for (int i = 0; i < 6; i++) { + numbertab[i] = PyLong_FromLong(cint_constants_1[i - 0]); + if (unlikely(!numbertab[i])) __PYX_ERR(0, 1, __pyx_L1_error) + } + } + #if CYTHON_IMMORTAL_CONSTANTS + { + PyObject **table = __pyx_mstate->__pyx_number_tab; + for (Py_ssize_t i=0; i<6; ++i) { + #if PY_VERSION_HEX >= 0x030F0000 + PyUnstable_SetImmortal(table[i]); + #elif CYTHON_COMPILING_IN_CPYTHON_FREETHREADING + if ((PY_SSIZE_T_MAX <= _Py_IMMORTAL_REFCNT_LOCAL)) break; + #if PY_VERSION_HEX < 0x030E0000 + if (_Py_IsOwnedByCurrentThread(table[i]) && Py_REFCNT(table[i]) == 1) + #else + if (PyUnstable_Object_IsUniquelyReferenced(table[i])) + #endif + { + Py_SET_REFCNT(table[i], ((Py_ssize_t)_Py_IMMORTAL_REFCNT_LOCAL + 1)); + } + #else + if ((PY_SSIZE_T_MAX < _Py_IMMORTAL_INITIAL_REFCNT)) break; + Py_SET_REFCNT(table[i], _Py_IMMORTAL_INITIAL_REFCNT); + #endif + } + } + #endif + return 0; + __pyx_L1_error:; + return -1; +} +/* #### Code section: init_codeobjects ### */ +#ifdef __cplusplus +namespace { +#endif +typedef struct { + unsigned int argcount : 3; + unsigned int num_posonly_args : 1; + unsigned int num_kwonly_args : 1; + unsigned int nlocals : 5; + unsigned int flags : 10; + unsigned int first_line : 9; +} __Pyx_PyCode_New_function_description; +#ifdef __cplusplus +} /* anonymous namespace */ +#endif +/* NewCodeObj.proto */ +static PyObject* __Pyx_PyCode_New( + const __Pyx_PyCode_New_function_description descr, + PyObject * const *varnames, + PyObject *filename, + PyObject *funcname, + PyObject *line_table, + PyObject *tuple_dedup_map +); + + +static int __Pyx_CreateCodeObjects(__pyx_mstatetype *__pyx_mstate) { + PyObject* tuple_dedup_map = PyDict_New(); + if (unlikely(!tuple_dedup_map)) return -1; + { + const __Pyx_PyCode_New_function_description descr = {5, 0, 0, 19, (unsigned int)(CO_OPTIMIZED|CO_NEWLOCALS|CO_GENERATOR), 149}; + PyObject* const varnames[] = {__pyx_mstate->__pyx_n_u_p0, __pyx_mstate->__pyx_n_u_p1, __pyx_mstate->__pyx_n_u_p2, __pyx_mstate->__pyx_n_u_p3, __pyx_mstate->__pyx_n_u_n, __pyx_mstate->__pyx_n_u_a1, __pyx_mstate->__pyx_n_u_b1, __pyx_mstate->__pyx_n_u_c1, __pyx_mstate->__pyx_n_u_d1, __pyx_mstate->__pyx_n_u_dt, __pyx_mstate->__pyx_n_u_delta_2, __pyx_mstate->__pyx_n_u_delta_3, __pyx_mstate->__pyx_n_u_i, __pyx_mstate->__pyx_n_u_a, __pyx_mstate->__pyx_n_u_b, __pyx_mstate->__pyx_n_u_c, __pyx_mstate->__pyx_n_u_d, __pyx_mstate->__pyx_n_u_t1, __pyx_mstate->__pyx_n_u_t1_2}; + __pyx_mstate_global->__pyx_codeobj_tab[0] = __Pyx_PyCode_New(descr, varnames, __pyx_mstate->__pyx_kp_u_Lib_fontTools_cu2qu_cu2qu_py, __pyx_mstate->__pyx_n_u_split_cubic_into_n_gen, __pyx_mstate->__pyx_kp_b_iso88591__3, tuple_dedup_map); if (unlikely(!__pyx_mstate_global->__pyx_codeobj_tab[0])) goto bad; + } + { + const __Pyx_PyCode_New_function_description descr = {3, 0, 0, 7, (unsigned int)(CO_OPTIMIZED|CO_NEWLOCALS), 467}; + PyObject* const varnames[] = {__pyx_mstate->__pyx_n_u_curve, __pyx_mstate->__pyx_n_u_max_err, __pyx_mstate->__pyx_n_u_all_quadratic, __pyx_mstate->__pyx_n_u_n, __pyx_mstate->__pyx_n_u_spline, __pyx_mstate->__pyx_n_u_p, __pyx_mstate->__pyx_n_u_s}; + __pyx_mstate_global->__pyx_codeobj_tab[1] = __Pyx_PyCode_New(descr, varnames, __pyx_mstate->__pyx_kp_u_Lib_fontTools_cu2qu_cu2qu_py, __pyx_mstate->__pyx_n_u_curve_to_quadratic, __pyx_mstate->__pyx_kp_b_iso88591_AWBc_U_U_3fBa_AWCy_7_2QgQgT_a_Q, tuple_dedup_map); if (unlikely(!__pyx_mstate_global->__pyx_codeobj_tab[1])) goto bad; + } + { + const __Pyx_PyCode_New_function_description descr = {3, 0, 0, 13, (unsigned int)(CO_OPTIMIZED|CO_NEWLOCALS), 504}; + PyObject* const varnames[] = {__pyx_mstate->__pyx_n_u_curves, __pyx_mstate->__pyx_n_u_max_errors, __pyx_mstate->__pyx_n_u_all_quadratic, __pyx_mstate->__pyx_n_u_l, __pyx_mstate->__pyx_n_u_last_i, __pyx_mstate->__pyx_n_u_i, __pyx_mstate->__pyx_n_u_splines, __pyx_mstate->__pyx_n_u_n, __pyx_mstate->__pyx_n_u_spline, __pyx_mstate->__pyx_n_u_curve, __pyx_mstate->__pyx_n_u_p, __pyx_mstate->__pyx_n_u_spline, __pyx_mstate->__pyx_n_u_s}; + __pyx_mstate_global->__pyx_codeobj_tab[2] = __Pyx_PyCode_New(descr, varnames, __pyx_mstate->__pyx_kp_u_Lib_fontTools_cu2qu_cu2qu_py, __pyx_mstate->__pyx_n_u_curves_to_quadratic, __pyx_mstate->__pyx_kp_b_iso88591_L_Qawb_4uG4y_s_s_Qa_j_t1_q_1A_a, tuple_dedup_map); if (unlikely(!__pyx_mstate_global->__pyx_codeobj_tab[2])) goto bad; + } + Py_DECREF(tuple_dedup_map); + return 0; + bad: + Py_DECREF(tuple_dedup_map); + return -1; +} +/* #### Code section: init_globals ### */ + +static int __Pyx_InitGlobals(void) { + int __pyx_lineno = 0; + const char *__pyx_filename = NULL; + int __pyx_clineno = 0; + /* PythonCompatibility.init */ + if (likely(__Pyx_init_co_variables() == 0 && __Pyx_init_tpflags_variables() == 0)); else + + if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 1, __pyx_L1_error) + + /* PyFrozenDict.init */ + #if CYTHON_COMPILING_IN_LIMITED_API + { + PyObject *builtins = PyEval_GetBuiltins(); // borrowed + if (likely(builtins)) { + PyObject *type_name = PyUnicode_FromStringAndSize("frozendict", sizeof("frozendict")-1); + if (likely(type_name)) { + PyObject *frozendict_type = PyObject_GetItem(builtins, type_name); + Py_DECREF(type_name); + if (!frozendict_type && PyErr_ExceptionMatches(PyExc_KeyError)) { + PyErr_Clear(); + frozendict_type = (PyObject*) &PyDict_Type; + Py_INCREF(frozendict_type); + } + __pyx_mstate_global->__Pyx_PyFrozenDictType = frozendict_type; + } + } + } // error handling follows + #endif + + if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 1, __pyx_L1_error) + + /* CommonTypesMetaclass.init */ + if (likely(__pyx_CommonTypesMetaclass_init(__pyx_m) == 0)); else + + if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 1, __pyx_L1_error) + + /* CachedMethodType.init */ + #if CYTHON_COMPILING_IN_LIMITED_API + { + PyObject *typesModule=NULL; + typesModule = PyImport_ImportModule("types"); + if (typesModule) { + __pyx_mstate_global->__Pyx_CachedMethodType = PyObject_GetAttrString(typesModule, "MethodType"); + Py_DECREF(typesModule); + } + } // error handling follows + #endif + + if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 1, __pyx_L1_error) + + /* Generator.init */ + if (likely(__pyx_Generator_init(__pyx_m) == 0)); else + + if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 1, __pyx_L1_error) + + return 0; + __pyx_L1_error:; + return -1; +} +/* #### Code section: init_after_shared_utility ### */ + +static int __Pyx_InitAfterSharedUtility(void) { + int __pyx_lineno = 0; + const char *__pyx_filename = NULL; + int __pyx_clineno = 0; + /* CythonFunctionPerModule.init */ + if (likely(__pyx_CyFunction_init(__pyx_m) == 0)); else + + if (unlikely(PyErr_Occurred())) __PYX_ERR(0, 1, __pyx_L1_error) + + return 0; + __pyx_L1_error:; + return -1; +} +/* #### Code section: cleanup_globals ### */ +/* #### Code section: cleanup_module ### */ +/* #### Code section: main_method ### */ +/* #### Code section: utility_code_pragmas ### */ +#ifdef _MSC_VER +#pragma warning( push ) +/* Warning 4127: conditional expression is constant + * Cython uses constant conditional expressions to allow in inline functions to be optimized at + * compile-time, so this warning is not useful + */ +#pragma warning( disable : 4127 ) +#endif + + + +/* #### Code section: utility_code_def ### */ + +/* --- Runtime support code --- */ +/* Refnanny */ +#if CYTHON_REFNANNY +static __Pyx_RefNannyAPIStruct *__Pyx_RefNannyImportAPI(const char *modname) { + PyObject *m = NULL, *p = NULL; + void *r = NULL; + m = PyImport_ImportModule(modname); + if (!m) goto end; + p = PyObject_GetAttrString(m, "RefNannyAPI"); + if (!p) goto end; + r = PyLong_AsVoidPtr(p); +end: + Py_XDECREF(p); + Py_XDECREF(m); + return (__Pyx_RefNannyAPIStruct *)r; +} +#endif + +/* PyObjectCall (used by PyObjectFastCall) */ +#if CYTHON_COMPILING_IN_CPYTHON +static CYTHON_INLINE PyObject* __Pyx_PyObject_Call(PyObject *func, PyObject *arg, PyObject *kw) { + PyObject *result; + ternaryfunc call = Py_TYPE(func)->tp_call; + if (unlikely(!call)) + return PyObject_Call(func, arg, kw); + if (unlikely(Py_EnterRecursiveCall(" while calling a Python object"))) + return NULL; + result = (*call)(func, arg, kw); + Py_LeaveRecursiveCall(); + if (unlikely(!result) && unlikely(!PyErr_Occurred())) { + PyErr_SetString( + PyExc_SystemError, + "NULL result without error in PyObject_Call"); + } + return result; +} +#endif + +/* PyObjectCallMethO (used by PyObjectFastCall) */ +#if CYTHON_COMPILING_IN_CPYTHON +static CYTHON_INLINE PyObject* __Pyx_PyObject_CallMethO(PyObject *func, PyObject *arg) { + PyObject *self, *result; + PyCFunction cfunc; + cfunc = __Pyx_CyOrPyCFunction_GET_FUNCTION(func); + self = __Pyx_CyOrPyCFunction_GET_SELF(func); + if (unlikely(Py_EnterRecursiveCall(" while calling a Python object"))) + return NULL; + result = cfunc(self, arg); + Py_LeaveRecursiveCall(); + if (unlikely(!result) && unlikely(!PyErr_Occurred())) { + PyErr_SetString( + PyExc_SystemError, + "NULL result without error in PyObject_Call"); + } + return result; +} +#endif + +/* PyObjectFastCall */ +#if CYTHON_COMPILING_IN_LIMITED_API +static PyObject* __Pyx_PyObject_FastCall_fallback(PyObject *func, PyObject * const*args, size_t nargs, PyObject *kwargs) { + PyObject *argstuple; + PyObject *result = 0; + size_t i; + argstuple = PyTuple_New((Py_ssize_t)nargs); + if (unlikely(!argstuple)) return NULL; + for (i = 0; i < nargs; i++) { + Py_INCREF(args[i]); + if (__Pyx_PyTuple_SET_ITEM(argstuple, (Py_ssize_t)i, args[i]) != (0)) goto bad; + } + result = __Pyx_PyObject_Call(func, argstuple, kwargs); + bad: + Py_DECREF(argstuple); + return result; +} +#endif +#if CYTHON_VECTORCALL && !(CYTHON_COMPILING_IN_LIMITED_API || CYTHON_COMPILING_IN_PYPY) + #if CYTHON_COMPILING_IN_CPYTHON +static CYTHON_INLINE vectorcallfunc __Pyx_PyVectorcall_Function(PyObject *callable) { + PyTypeObject *tp = Py_TYPE(callable); + #if defined(__Pyx_CyFunction_USED) + if (__Pyx_CyFunction_CheckExact(callable)) { + return __Pyx_CyFunction_func_vectorcall(callable); + } + #endif + if (!PyType_HasFeature(tp, Py_TPFLAGS_HAVE_VECTORCALL)) { + return NULL; + } + assert(PyCallable_Check(callable)); + Py_ssize_t offset = tp->tp_vectorcall_offset; + assert(offset > 0); + vectorcallfunc ptr; + memcpy(&ptr, (char *) callable + offset, sizeof(ptr)); + return ptr; +} + #else + #define __Pyx_PyVectorcall_Function(callable) PyVectorcall_Function(callable) + #endif +#endif +static CYTHON_INLINE PyObject* __Pyx_PyObject_FastCallDict(PyObject *func, PyObject *const *args, size_t nargsf, PyObject *kwargs) { + Py_ssize_t nargs = __Pyx_PyVectorcall_NARGS(nargsf); +#if CYTHON_COMPILING_IN_CPYTHON + if (nargs == 0 && kwargs == NULL) { + if (__Pyx_CyOrPyCFunction_Check(func) && likely( __Pyx_CyOrPyCFunction_GET_FLAGS(func) & METH_NOARGS)) + return __Pyx_PyObject_CallMethO(func, NULL); + } + else if (nargs == 1 && kwargs == NULL) { + if (__Pyx_CyOrPyCFunction_Check(func) && likely( __Pyx_CyOrPyCFunction_GET_FLAGS(func) & METH_O)) + return __Pyx_PyObject_CallMethO(func, args[0]); + } +#endif + if (kwargs == NULL) { + #if CYTHON_VECTORCALL + #if CYTHON_COMPILING_IN_LIMITED_API || CYTHON_COMPILING_IN_PYPY + return PyObject_Vectorcall(func, args, nargsf, NULL); + #else + vectorcallfunc f = __Pyx_PyVectorcall_Function(func); + if (f) { + return f(func, args, nargsf, NULL); + } + #endif + #endif + } + if (nargs == 0) { + return __Pyx_PyObject_Call(func, __pyx_mstate_global->__pyx_empty_tuple, kwargs); + } + #if CYTHON_COMPILING_IN_LIMITED_API + return __Pyx_PyObject_FastCall_fallback(func, args, (size_t)nargs, kwargs); + #elif CYTHON_COMPILING_IN_PYPY + return PyObject_VectorcallDict(func, args, (size_t) nargs, kwargs); + #else + return PyObject_VectorcallDict(func, args, nargsf, kwargs); + #endif +} + +/* PyLongCompare */ +static CYTHON_INLINE int __Pyx_PyLong_BoolEqObjC(PyObject *op1, PyObject *op2, long intval, long inplace) { + CYTHON_MAYBE_UNUSED_VAR(intval); + CYTHON_UNUSED_VAR(inplace); + if (op1 == op2) { + return 1; + } + #if CYTHON_USE_PYLONG_INTERNALS + if (likely(PyLong_CheckExact(op1))) { + int unequal; + unsigned long uintval; + Py_ssize_t size = __Pyx_PyLong_DigitCount(op1); + const digit* digits = __Pyx_PyLong_Digits(op1); + if (intval == 0) { + return (__Pyx_PyLong_IsZero(op1) == 1); + } else if (intval < 0) { + if (__Pyx_PyLong_IsNonNeg(op1)) + return 0; + intval = -intval; + } else { + if (__Pyx_PyLong_IsNeg(op1)) + return 0; + } + uintval = (unsigned long) intval; +#if PyLong_SHIFT * 4 < SIZEOF_LONG*8 + if (uintval >> (PyLong_SHIFT * 4)) { + unequal = (size != 5) || (digits[0] != (uintval & (unsigned long) PyLong_MASK)) + | (digits[1] != ((uintval >> (1 * PyLong_SHIFT)) & (unsigned long) PyLong_MASK)) | (digits[2] != ((uintval >> (2 * PyLong_SHIFT)) & (unsigned long) PyLong_MASK)) | (digits[3] != ((uintval >> (3 * PyLong_SHIFT)) & (unsigned long) PyLong_MASK)) | (digits[4] != ((uintval >> (4 * PyLong_SHIFT)) & (unsigned long) PyLong_MASK)); + } else +#endif +#if PyLong_SHIFT * 3 < SIZEOF_LONG*8 + if (uintval >> (PyLong_SHIFT * 3)) { + unequal = (size != 4) || (digits[0] != (uintval & (unsigned long) PyLong_MASK)) + | (digits[1] != ((uintval >> (1 * PyLong_SHIFT)) & (unsigned long) PyLong_MASK)) | (digits[2] != ((uintval >> (2 * PyLong_SHIFT)) & (unsigned long) PyLong_MASK)) | (digits[3] != ((uintval >> (3 * PyLong_SHIFT)) & (unsigned long) PyLong_MASK)); + } else +#endif +#if PyLong_SHIFT * 2 < SIZEOF_LONG*8 + if (uintval >> (PyLong_SHIFT * 2)) { + unequal = (size != 3) || (digits[0] != (uintval & (unsigned long) PyLong_MASK)) + | (digits[1] != ((uintval >> (1 * PyLong_SHIFT)) & (unsigned long) PyLong_MASK)) | (digits[2] != ((uintval >> (2 * PyLong_SHIFT)) & (unsigned long) PyLong_MASK)); + } else +#endif +#if PyLong_SHIFT * 1 < SIZEOF_LONG*8 + if (uintval >> (PyLong_SHIFT * 1)) { + unequal = (size != 2) || (digits[0] != (uintval & (unsigned long) PyLong_MASK)) + | (digits[1] != ((uintval >> (1 * PyLong_SHIFT)) & (unsigned long) PyLong_MASK)); + } else +#endif + unequal = (size != 1) || (((unsigned long) digits[0]) != (uintval & (unsigned long) PyLong_MASK)); + return (unequal == 0); + } + #endif + if (PyFloat_CheckExact(op1)) { + const long b = intval; + double a = __Pyx_PyFloat_AS_DOUBLE(op1); + return ((double)a == (double)b); + } + return __Pyx_PyObject_RichCompareBool(op1, op2, Py_EQ); +} + +/* RaiseTooManyValuesToUnpack */ +static CYTHON_INLINE void __Pyx_RaiseTooManyValuesError(Py_ssize_t expected) { + PyErr_Format(PyExc_ValueError, + "too many values to unpack (expected %" CYTHON_FORMAT_SSIZE_T "d)", expected); +} + +/* RaiseNeedMoreValuesToUnpack */ +static CYTHON_INLINE void __Pyx_RaiseNeedMoreValuesError(Py_ssize_t index) { + PyErr_Format(PyExc_ValueError, + "need more than %" CYTHON_FORMAT_SSIZE_T "d value%.1s to unpack", + index, (index == 1) ? "" : "s"); +} + +/* FastTypeChecks (used by GivenExceptionMatches) */ +#if CYTHON_COMPILING_IN_CPYTHON +static int __Pyx_InBases(PyTypeObject *a, PyTypeObject *b) { + while (a) { + a = __Pyx_PyType_GetSlot(a, tp_base, PyTypeObject*); + if (a == b) + return 1; + } + return b == &PyBaseObject_Type; +} +static CYTHON_INLINE int __Pyx_IsSubtype(PyTypeObject *a, PyTypeObject *b) { + PyObject *mro; + if (a == b) return 1; + mro = a->tp_mro; + if (likely(mro)) { + Py_ssize_t i, n; + n = PyTuple_GET_SIZE(mro); + for (i = 0; i < n; i++) { + if (PyTuple_GET_ITEM(mro, i) == (PyObject *)b) + return 1; + } + return 0; + } + return __Pyx_InBases(a, b); +} +static CYTHON_INLINE int __Pyx_IsAnySubtype2(PyTypeObject *cls, PyTypeObject *a, PyTypeObject *b) { + PyObject *mro; + if (cls == a || cls == b) return 1; + mro = cls->tp_mro; + if (likely(mro)) { + Py_ssize_t i, n; + n = PyTuple_GET_SIZE(mro); + for (i = 0; i < n; i++) { + PyObject *base = PyTuple_GET_ITEM(mro, i); + if (base == (PyObject *)a || base == (PyObject *)b) + return 1; + } + return 0; + } + return __Pyx_InBases(cls, a) || __Pyx_InBases(cls, b); +} +#endif + +/* PyErrFetchRestore (used by GivenExceptionMatches) */ +#if CYTHON_FAST_THREAD_STATE +static CYTHON_INLINE void __Pyx_ErrRestoreInState(PyThreadState *tstate, PyObject *type, PyObject *value, PyObject *tb) { +#if PY_VERSION_HEX >= 0x030C00A6 + PyObject *tmp_value; + assert(type == NULL || (value != NULL && type == (PyObject*) Py_TYPE(value))); + if (value) { + #if CYTHON_COMPILING_IN_CPYTHON + if (unlikely(((PyBaseExceptionObject*) value)->traceback != tb)) + #endif + PyException_SetTraceback(value, tb); + } + tmp_value = tstate->current_exception; + tstate->current_exception = value; + Py_XDECREF(tmp_value); + Py_XDECREF(type); + Py_XDECREF(tb); +#else + PyObject *tmp_type, *tmp_value, *tmp_tb; + tmp_type = tstate->curexc_type; + tmp_value = tstate->curexc_value; + tmp_tb = tstate->curexc_traceback; + tstate->curexc_type = type; + tstate->curexc_value = value; + tstate->curexc_traceback = tb; + Py_XDECREF(tmp_type); + Py_XDECREF(tmp_value); + Py_XDECREF(tmp_tb); +#endif +} +static CYTHON_INLINE void __Pyx_ErrFetchInState(PyThreadState *tstate, PyObject **type, PyObject **value, PyObject **tb) { +#if PY_VERSION_HEX >= 0x030C00A6 + PyObject* exc_value; + exc_value = tstate->current_exception; + tstate->current_exception = 0; + *value = exc_value; + *type = NULL; + *tb = NULL; + if (exc_value) { + *type = (PyObject*) Py_TYPE(exc_value); + Py_INCREF(*type); + #if CYTHON_COMPILING_IN_CPYTHON + *tb = ((PyBaseExceptionObject*) exc_value)->traceback; + Py_XINCREF(*tb); + #else + *tb = PyException_GetTraceback(exc_value); + #endif + } +#else + *type = tstate->curexc_type; + *value = tstate->curexc_value; + *tb = tstate->curexc_traceback; + tstate->curexc_type = 0; + tstate->curexc_value = 0; + tstate->curexc_traceback = 0; +#endif +} +#endif + +/* GivenExceptionMatches (used by IterFinish) */ +#if CYTHON_COMPILING_IN_CPYTHON +static CYTHON_INLINE int __Pyx_inner_PyErr_GivenExceptionMatches2(PyObject *err, PyObject* exc_type1, PyObject *exc_type2) { + if (exc_type1) { + return __Pyx_IsAnySubtype2((PyTypeObject*)err, (PyTypeObject*)exc_type1, (PyTypeObject*)exc_type2); + } else { + return __Pyx_IsSubtype((PyTypeObject*)err, (PyTypeObject*)exc_type2); + } +} +static int __Pyx_PyErr_GivenExceptionMatchesTuple(PyObject *exc_type, PyObject *tuple) { + Py_ssize_t i, n; + assert(PyExceptionClass_Check(exc_type)); + n = PyTuple_GET_SIZE(tuple); + for (i=0; isq_length)) { + Py_ssize_t l = sm->sq_length(o); + if (likely(l >= 0)) { + *i += l; + } else { + if (!PyErr_ExceptionMatches(PyExc_OverflowError)) + return -1; + PyErr_Clear(); + } + } + return 0; +} +#endif + +/* GetItemInt */ +static PyObject *__Pyx_GetItemInt_Generic(PyObject *o, PyObject* j) { + PyObject *r; + if (unlikely(!j)) return NULL; + r = PyObject_GetItem(o, j); + Py_DECREF(j); + return r; +} +static PyObject *__Pyx_GetItemInt_Generic_size(PyObject *o, Py_ssize_t i) { + return __Pyx_GetItemInt_Generic(o, PyLong_FromSsize_t(i)); +} +static CYTHON_INLINE PyObject *__Pyx_GetItemInt_List_Fast(PyObject *o, Py_ssize_t i, + int wraparound, int boundscheck, int unsafe_shared) { + CYTHON_MAYBE_UNUSED_VAR(unsafe_shared); +#if CYTHON_AVOID_BORROWED_REFS + CYTHON_UNUSED_VAR(boundscheck); + Py_ssize_t wrapped_i = i; + if (wraparound & unlikely(i < 0)) { + Py_ssize_t size = __Pyx_PyList_GET_SIZE(o); + #if !CYTHON_ASSUME_SAFE_SIZE + if (unlikely(size < 0)) return NULL; + #endif + wrapped_i += size; + } + return __Pyx_PyList_GetItemRef(o, wrapped_i); +#elif CYTHON_ASSUME_SAFE_SIZE && CYTHON_ASSUME_SAFE_MACROS + Py_ssize_t wrapped_i = i; + Py_ssize_t size = (wraparound | boundscheck) ? PyList_GET_SIZE(o) : -1; + if (wraparound & unlikely(i < 0)) { + wrapped_i += size; + } + if ((!boundscheck) || likely(__Pyx_is_valid_index(wrapped_i, size))) { + return __Pyx_PyList_GET_ITEM_REF(o, wrapped_i, unsafe_shared); + } + return __Pyx_GetItemInt_Generic_size(o, i); +#else + (void)wraparound; + (void)boundscheck; + return PySequence_GetItem(o, i); +#endif +} +static CYTHON_INLINE PyObject *__Pyx_GetItemInt_Tuple_Fast(PyObject *o, Py_ssize_t i, + int wraparound, int boundscheck, int unsafe_shared) { + CYTHON_MAYBE_UNUSED_VAR(unsafe_shared); +#if CYTHON_AVOID_BORROWED_REFS + CYTHON_UNUSED_VAR(boundscheck); + Py_ssize_t wrapped_i = i; + if (wraparound & unlikely(i < 0)) { + Py_ssize_t size = __Pyx_PyTuple_GET_SIZE(o); + #if !CYTHON_ASSUME_SAFE_SIZE + if (unlikely(size < 0)) return NULL; + #endif + wrapped_i += size; + } + #if CYTHON_ASSUME_SAFE_MACROS && !CYTHON_COMPILING_IN_LIMITED_API + return PySequence_ITEM(o, wrapped_i); + #else + if (unlikely(wrapped_i < 0)) { + PyErr_SetString(PyExc_IndexError, "tuple index out of range"); + return NULL; + } + return PySequence_GetItem(o, wrapped_i); + #endif +#elif CYTHON_ASSUME_SAFE_SIZE && CYTHON_ASSUME_SAFE_MACROS + Py_ssize_t wrapped_i = i; + Py_ssize_t size = (wraparound | boundscheck) ? PyTuple_GET_SIZE(o) : -1; + if (wraparound & unlikely(i < 0)) { + wrapped_i += size; + } + if ((!boundscheck) || likely(__Pyx_is_valid_index(wrapped_i, size))) { + return __Pyx_NewRef(__Pyx_PyTuple_GET_ITEM(o, wrapped_i)); + } + return __Pyx_GetItemInt_Generic_size(o, i); +#else + (void)wraparound; + (void)boundscheck; + return PySequence_GetItem(o, i); +#endif +} +#if CYTHON_USE_TYPE_SLOTS && !CYTHON_COMPILING_IN_PYPY +static CYTHON_INLINE PyObject *__Pyx_GetItemInt_Fast_mapping(PyObject *o, binaryfunc getitem, Py_ssize_t i) { + PyObject *r, *key = PyLong_FromSsize_t(i); + if (unlikely(!key)) return NULL; + r = getitem(o, key); + Py_DECREF(key); + return r; +} +#endif +static CYTHON_INLINE PyObject *__Pyx_GetItemInt_Fast(PyObject *o, Py_ssize_t i, + int wraparound, int boundscheck, int unsafe_shared) { + CYTHON_MAYBE_UNUSED_VAR(unsafe_shared); +#if CYTHON_ASSUME_SAFE_SIZE + if (PyList_CheckExact(o)) { + return __Pyx_GetItemInt_List_Fast(o, i, wraparound, boundscheck, unsafe_shared); + } else + #if CYTHON_ASSUME_SAFE_MACROS && !CYTHON_AVOID_BORROWED_REFS + if (PyTuple_CheckExact(o)) { + return __Pyx_GetItemInt_Tuple_Fast(o, i, wraparound, boundscheck, unsafe_shared); + } else + #endif +#else + if ((!wraparound || i >= 0) & PyList_CheckExact(o)) { + return boundscheck ? __Pyx_PyList_GetItemRef(o, i) : __Pyx_PyList_GET_ITEM_REF(o, i, unsafe_shared); + } else +#endif +#if CYTHON_USE_TYPE_SLOTS && !CYTHON_COMPILING_IN_PYPY + if (PyDict_CheckExact(o)) { + return __Pyx_GetItemInt_Fast_mapping(o, PyDict_Type.tp_as_mapping->mp_subscript, i); + #if defined(PyFrozenDict_CheckExact) + } else if (PyFrozenDict_CheckExact(o)) { + return __Pyx_GetItemInt_Fast_mapping(o, PyFrozenDict_Type.tp_as_mapping->mp_subscript, i); + #endif + } else + { + PyTypeObject *obj_type = Py_TYPE(o); + int seq_or_mapping = __Pyx_PyType_GetFlags(obj_type) & (Py_TPFLAGS_SEQUENCE|Py_TPFLAGS_MAPPING); + if (seq_or_mapping != Py_TPFLAGS_SEQUENCE) { + PyMappingMethods *mm = obj_type->tp_as_mapping; + if (mm && mm->mp_subscript) + return __Pyx_GetItemInt_Fast_mapping(o, mm->mp_subscript, i); + } + PySequenceMethods *sm = obj_type->tp_as_sequence; + if (likely(sm && sm->sq_item)) { + if (wraparound && (i < 0) && unlikely(__Pyx_GetItemInt_wraparound(o, sm, &i) == -1)) + return NULL; + return sm->sq_item(o, i); + } + if (seq_or_mapping == Py_TPFLAGS_SEQUENCE) { + PyMappingMethods *mm = obj_type->tp_as_mapping; + if (likely(mm && mm->mp_subscript)) + return __Pyx_GetItemInt_Fast_mapping(o, mm->mp_subscript, i); + } + } +#else + if (!PyMapping_Check(o)) { + return PySequence_GetItem(o, i); + } +#endif + (void)wraparound; + (void)boundscheck; + return __Pyx_GetItemInt_Generic_size(o, i); +} + +/* FormatTypeName (used by RaiseErrorWithObjectTypes) */ +#if CYTHON_COMPILING_IN_LIMITED_API && __PYX_LIMITED_VERSION_HEX < 0x030d0000 +static __Pyx_TypeName +__Pyx_PyType_GetFullyQualifiedName(PyTypeObject* tp) +{ + PyObject *module = NULL, *name = NULL, *result = NULL; + #if __PYX_LIMITED_VERSION_HEX < 0x030b0000 + name = __Pyx_PyObject_GetAttrStr((PyObject *)tp, + __pyx_mstate_global->__pyx_n_u_qualname); + #else + name = PyType_GetQualName(tp); + #endif + if (unlikely(name == NULL) || unlikely(!PyUnicode_Check(name))) goto bad; + module = __Pyx_PyObject_GetAttrStr((PyObject *)tp, + __pyx_mstate_global->__pyx_n_u_module); + if (unlikely(module == NULL) || unlikely(!PyUnicode_Check(module))) goto bad; + if (PyUnicode_CompareWithASCIIString(module, "builtins") == 0) { + result = name; + name = NULL; + goto done; + } + result = PyUnicode_FromFormat("%U.%U", module, name); + if (unlikely(result == NULL)) goto bad; + done: + Py_XDECREF(name); + Py_XDECREF(module); + return result; + bad: + PyErr_Clear(); + if (name) { + result = name; + name = NULL; + } else { + result = __Pyx_NewRef(__pyx_mstate_global->__pyx_kp_u_); + } + goto done; +} +#endif + +/* RaiseErrorWithObjectTypes (used by PyNumberBinop) */ +static void __Pyx_RaiseErrorWithTypes1(PyObject* exc_type, const char *message, const char *arg, PyTypeObject *type_obj1, PyTypeObject *type_obj2) { + __Pyx_TypeName type_name1 = __Pyx_PyType_GetFullyQualifiedName(type_obj1); + #if CYTHON_COMPILING_IN_LIMITED_API && __PYX_LIMITED_VERSION_HEX < 0x030d0000 + if (unlikely(!type_name1)) return; + #endif + __Pyx_TypeName type_name2 = __Pyx_PyType_GetFullyQualifiedName(type_obj2); + #if CYTHON_COMPILING_IN_LIMITED_API && __PYX_LIMITED_VERSION_HEX < 0x030d0000 + if (unlikely(!type_name2)) goto type2_failed; + #endif + PyErr_Format(exc_type, message, arg, type_name1, type_name2); + __Pyx_DECREF_TypeName(type_name2); +#if CYTHON_COMPILING_IN_LIMITED_API && __PYX_LIMITED_VERSION_HEX < 0x030d0000 +type2_failed: +#endif + __Pyx_DECREF_TypeName(type_name1); +} + +/* PyNumberBinop */ +#if !(CYTHON_COMPILING_IN_PYPY || CYTHON_COMPILING_IN_GRAAL || CYTHON_COMPILING_IN_LIMITED_API) +#if CYTHON_USE_TYPE_SLOTS || __PYX_LIMITED_VERSION_HEX >= 0x030A0000 +#ifndef __Pyx_DEFINED_BinopTypeError +#define __Pyx_DEFINED_BinopTypeError +static void __Pyx_BinopTypeError(PyObject *op1, PyObject *op2, const char* op, int inplace) { + char opname[4] = {op[0], op[1], 0, 0}; + if (inplace) { + opname[op[1] ? 2 : 1] = '='; + } + __Pyx_RaiseErrorWithObjectTypes1( + PyExc_TypeError, + "unsupported operand type(s) for %.3s: '" __Pyx_FMT_TYPENAME "' and '" __Pyx_FMT_TYPENAME "'", + opname, op1, op2); +} +#endif +#endif +#ifndef __Pyx_DEFINED_PyNumber_Add_xfloat_object +#define __Pyx_DEFINED_PyNumber_Add_xfloat_object +static PyObject* __Pyx_PyNumber_Add_xfloat_object(PyObject *op1, PyObject *op2, int inplace) { + if (PyLong_CheckExact(op2)) { + double int_op2; + #if CYTHON_USE_PYLONG_INTERNALS + if (__Pyx_PyLong_IsCompact(op2)) { + Py_ssize_t compact_op2 = __Pyx_PyLong_CompactValue(op2); + if (compact_op2 == 0) return __Pyx_NewRef(op1); + int_op2 = (double) compact_op2; + } else + #endif + { + int_op2 = PyLong_AsDouble(op2); + if (unlikely((int_op2 == -1.) && PyErr_Occurred())) return NULL; + #if !CYTHON_USE_PYLONG_INTERNALS + if (int_op2 == 0.) return __Pyx_NewRef(op1); + #endif + } + double float_op1 = __Pyx_PyFloat_AS_DOUBLE(op1); + #if !CYTHON_ASSUME_SAFE_MACROS + if (unlikely((float_op1 == -1.) && PyErr_Occurred())) return NULL; + #endif + return PyFloat_FromDouble(float_op1 + int_op2); + } + if (PyLong_Check(op2)) { + binaryfunc slot_func = __Pyx_PyType_GetSubSlot(&PyFloat_Type, tp_as_number, nb_add, binaryfunc); + if (likely(slot_func)) { + return slot_func(op1, op2); + } + } + #if CYTHON_USE_TYPE_SLOTS || __PYX_LIMITED_VERSION_HEX >= 0x030A0000 + { + PyTypeObject *type_op2 = Py_TYPE(op2); + binaryfunc slot_func = __Pyx_PyType_GetSubSlot(type_op2, tp_as_number, nb_add, binaryfunc); + if (likely(slot_func)) { + PyObject *result = slot_func(op1, op2); + if (likely(result != Py_NotImplemented)) { + return result; + } + Py_DECREF(result); + } + __Pyx_BinopTypeError(op1, op2, "+", inplace); + return NULL; + } + #else + return (inplace) ? PyNumber_InPlaceAdd(op1, op2) : PyNumber_Add(op1, op2); + #endif +} +#endif +#ifndef __Pyx_DEFINED_PyNumber_Add_xint_object +#define __Pyx_DEFINED_PyNumber_Add_xint_object +static PyObject* __Pyx_PyNumber_Add_xint_object(PyObject *op1, PyObject *op2, int inplace) { + if (PyFloat_CheckExact(op2)) { + double int_op1; + #if CYTHON_USE_PYLONG_INTERNALS + if (__Pyx_PyLong_IsCompact(op1)) { + Py_ssize_t compact_op1 = __Pyx_PyLong_CompactValue(op1); + if (compact_op1 == 0) return __Pyx_NewRef(op2); + int_op1 = (double) compact_op1; + } else + #endif + { + int_op1 = PyLong_AsDouble(op1); + if (unlikely((int_op1 == -1.) && PyErr_Occurred())) return NULL; + #if !CYTHON_USE_PYLONG_INTERNALS + if (int_op1 == 0.) return __Pyx_NewRef(op2); + #endif + } + double float_op2 = __Pyx_PyFloat_AS_DOUBLE(op2); + #if !CYTHON_ASSUME_SAFE_MACROS + if (unlikely((float_op2 == -1.) && PyErr_Occurred())) return NULL; + #endif + return PyFloat_FromDouble(int_op1 + float_op2); + } + #if CYTHON_USE_TYPE_SLOTS || __PYX_LIMITED_VERSION_HEX >= 0x030A0000 + { + PyTypeObject *type_op2 = Py_TYPE(op2); + binaryfunc slot_func = __Pyx_PyType_GetSubSlot(type_op2, tp_as_number, nb_add, binaryfunc); + if (likely(slot_func)) { + PyObject *result = slot_func(op1, op2); + if (likely(result != Py_NotImplemented)) { + return result; + } + Py_DECREF(result); + } + __Pyx_BinopTypeError(op1, op2, "+", inplace); + return NULL; + } + #else + return (inplace) ? PyNumber_InPlaceAdd(op1, op2) : PyNumber_Add(op1, op2); + #endif +} +#endif +static CYTHON_INLINE PyObject* __Pyx__PyNumber_Add_object_object(PyObject *op1, PyObject *op2, int inplace) { + if (PyFloat_CheckExact(op1)) { + if (PyFloat_CheckExact(op2)) { + double float_op2 = __Pyx_PyFloat_AS_DOUBLE(op2); + #if !CYTHON_ASSUME_SAFE_MACROS + if (unlikely((float_op2 == -1.) && PyErr_Occurred())) return NULL; + #endif + double float_op1 = __Pyx_PyFloat_AS_DOUBLE(op1); + #if !CYTHON_ASSUME_SAFE_MACROS + if (unlikely((float_op1 == -1.) && PyErr_Occurred())) return NULL; + #endif + return PyFloat_FromDouble(float_op1 + float_op2); + } + return __Pyx_PyNumber_Add_xfloat_object(op1, op2, inplace); + } + if (PyLong_CheckExact(op1)) { + if (PyLong_CheckExact(op2)) { + #if CYTHON_USE_PYLONG_INTERNALS + if (__Pyx_PyLong_IsCompact(op1)) { + Py_ssize_t int_op1 = __Pyx_PyLong_CompactValue(op1); + if (int_op1 == 0) return __Pyx_NewRef(op2); + if (__Pyx_PyLong_IsCompact(op2)) { + Py_ssize_t int_op2 = __Pyx_PyLong_CompactValue(op2); + if (int_op2 == 0) return __Pyx_NewRef(op1); + return PyLong_FromSsize_t(int_op1 + int_op2); + } + } + else if (__Pyx_PyLong_IsZero(op2)) return __Pyx_NewRef(op1); + #endif + binaryfunc slot_func = __Pyx_PyType_GetSubSlot(&PyLong_Type, tp_as_number, nb_add, binaryfunc); + if (likely(slot_func)) { + return slot_func(op1, op2); + } + } + return __Pyx_PyNumber_Add_xint_object(op1, op2, inplace); + } + return (inplace) ? PyNumber_InPlaceAdd(op1, op2) : PyNumber_Add(op1, op2); +} +#endif + +/* IgnoreException (used by GetModuleGlobalName) */ +static CYTHON_INLINE int __Pyx_IgnoreGivenException(PyObject *given_exception, PyObject *ignorable_exception) { + if (PyErr_GivenExceptionMatches(given_exception ? given_exception : PyErr_Occurred(), ignorable_exception)) { + PyErr_Clear(); + return 1; + } + return 0; +} + +/* PyErrExceptionMatches (used by PyObjectGetAttrStrNoError) */ +#if CYTHON_FAST_THREAD_STATE +static int __Pyx_PyErr_ExceptionMatchesTuple(PyObject *exc_type, PyObject *tuple) { + Py_ssize_t i, n; + n = PyTuple_GET_SIZE(tuple); + for (i=0; i= 0x030C00A6 + PyObject *current_exception = tstate->current_exception; + if (unlikely(!current_exception)) return 0; + exc_type = (PyObject*) Py_TYPE(current_exception); + if (exc_type == err) return 1; +#else + exc_type = tstate->curexc_type; + if (exc_type == err) return 1; + if (unlikely(!exc_type)) return 0; +#endif + #if CYTHON_AVOID_BORROWED_REFS + Py_INCREF(exc_type); + #endif + if (unlikely(PyTuple_Check(err))) { + result = __Pyx_PyErr_ExceptionMatchesTuple(exc_type, err); + } else { + result = __Pyx_PyErr_GivenExceptionMatches(exc_type, err); + } + #if CYTHON_AVOID_BORROWED_REFS + Py_DECREF(exc_type); + #endif + return result; +} +#endif + +/* PyObjectGetAttrStr (used by PyObjectGetAttrStrNoError) */ +#if CYTHON_USE_TYPE_SLOTS +static CYTHON_INLINE PyObject* __Pyx_PyObject_GetAttrStr(PyObject* obj, PyObject* attr_name) { + PyTypeObject* tp = Py_TYPE(obj); + if (likely(tp->tp_getattro)) + return tp->tp_getattro(obj, attr_name); + return PyObject_GetAttr(obj, attr_name); +} +#endif + +/* PyObjectGetAttrStrNoError (used by GetBuiltinName) */ +#if __PYX_LIMITED_VERSION_HEX < 0x030d0000 +static void __Pyx_PyObject_GetAttrStr_ClearAttributeError(void) { + __Pyx_PyThreadState_declare + __Pyx_PyThreadState_assign + if (likely(__Pyx_PyErr_ExceptionMatches(PyExc_AttributeError))) + __Pyx_PyErr_Clear(); +} +#endif +static CYTHON_INLINE PyObject* __Pyx_PyObject_GetAttrStrNoError(PyObject* obj, PyObject* attr_name) { + PyObject *result; +#if __PYX_LIMITED_VERSION_HEX >= 0x030d0000 + (void) PyObject_GetOptionalAttr(obj, attr_name, &result); + return result; +#else +#if CYTHON_COMPILING_IN_CPYTHON && CYTHON_USE_TYPE_SLOTS + PyTypeObject* tp = Py_TYPE(obj); + if (likely(tp->tp_getattro == PyObject_GenericGetAttr)) { + return _PyObject_GenericGetAttrWithDict(obj, attr_name, NULL, 1); + } +#endif + result = __Pyx_PyObject_GetAttrStr(obj, attr_name); + if (unlikely(!result)) { + __Pyx_PyObject_GetAttrStr_ClearAttributeError(); + } + return result; +#endif +} + +/* GetBuiltinName (used by GetModuleGlobalName) */ +static PyObject *__Pyx_GetBuiltinName(PyObject *name) { + PyObject* result = __Pyx_PyObject_GetAttrStrNoError(__pyx_mstate_global->__pyx_b, name); + if (unlikely(!result) && !PyErr_Occurred()) { + PyErr_Format(PyExc_NameError, + "name '%U' is not defined", name); + } + return result; +} + +/* PyDictVersioning (used by GetModuleGlobalName) */ +#if CYTHON_USE_DICT_VERSIONS && CYTHON_USE_TYPE_SLOTS +static CYTHON_INLINE PY_UINT64_T __Pyx_get_tp_dict_version(PyObject *obj) { + PyObject *dict = Py_TYPE(obj)->tp_dict; + return likely(dict) ? __PYX_GET_DICT_VERSION(dict) : 0; +} +static CYTHON_INLINE PY_UINT64_T __Pyx_get_object_dict_version(PyObject *obj) { + PyObject **dictptr = NULL; + Py_ssize_t offset = Py_TYPE(obj)->tp_dictoffset; + if (offset) { +#if CYTHON_COMPILING_IN_CPYTHON + dictptr = (likely(offset > 0)) ? (PyObject **) ((char *)obj + offset) : _PyObject_GetDictPtr(obj); +#else + dictptr = _PyObject_GetDictPtr(obj); +#endif + } + return (dictptr && *dictptr) ? __PYX_GET_DICT_VERSION(*dictptr) : 0; +} +static CYTHON_INLINE int __Pyx_object_dict_version_matches(PyObject* obj, PY_UINT64_T tp_dict_version, PY_UINT64_T obj_dict_version) { + PyObject *dict = Py_TYPE(obj)->tp_dict; + if (unlikely(!dict) || unlikely(tp_dict_version != __PYX_GET_DICT_VERSION(dict))) + return 0; + return obj_dict_version == __Pyx_get_object_dict_version(obj); +} +#endif + +/* GetModuleGlobalName */ +#if CYTHON_USE_DICT_VERSIONS +static PyObject *__Pyx__GetModuleGlobalName(PyObject *name, PY_UINT64_T *dict_version, PyObject **dict_cached_value) +#else +static CYTHON_INLINE PyObject *__Pyx__GetModuleGlobalName(PyObject *name) +#endif +{ + PyObject *result; +#if CYTHON_COMPILING_IN_LIMITED_API + if (unlikely(!__pyx_m)) { + if (!PyErr_Occurred()) + PyErr_SetNone(PyExc_NameError); + return NULL; + } + result = PyObject_GetAttr(__pyx_m, name); + if (likely(result)) { + return result; + } + if (!__Pyx_IgnoreException(PyExc_Exception)) { + return NULL; // BaseException + } +#elif CYTHON_AVOID_BORROWED_REFS || CYTHON_AVOID_THREAD_UNSAFE_BORROWED_REFS + if (unlikely(__Pyx_PyDict_GetItemRef(__pyx_mstate_global->__pyx_d, name, &result) == -1)) { + if (!__Pyx_IgnoreException(PyExc_Exception)) { + return NULL; // BaseException + } + } + __PYX_UPDATE_DICT_CACHE(__pyx_mstate_global->__pyx_d, result, *dict_cached_value, *dict_version) + if (likely(result)) { + return result; + } +#else + result = _PyDict_GetItem_KnownHash(__pyx_mstate_global->__pyx_d, name, ((PyASCIIObject *) name)->hash); + __PYX_UPDATE_DICT_CACHE(__pyx_mstate_global->__pyx_d, result, *dict_cached_value, *dict_version) + if (likely(result)) { + return __Pyx_NewRef(result); + } + PyObject *exc = PyErr_Occurred(); + if (unlikely(exc) && !__Pyx_IgnoreGivenException(exc, PyExc_Exception)) { + return NULL; // BaseException + } +#endif + return __Pyx_GetBuiltinName(name); +} + +/* CopyObjectArray (used by TupleOrListFromArrayImpl) */ +#if CYTHON_COMPILING_IN_CPYTHON +static CYTHON_INLINE void __Pyx_copy_object_array(PyObject *const *CYTHON_RESTRICT src, PyObject** CYTHON_RESTRICT dest, Py_ssize_t length) { + PyObject *v; + Py_ssize_t i; + for (i = 0; i < length; i++) { + v = dest[i] = src[i]; + Py_INCREF(v); + } +} +#endif + +/* TupleOrListFromArrayImpl (used by TupleFromArray) */ +#if !(PY_VERSION_HEX >= 0x030F0000 && !CYTHON_COMPILING_IN_LIMITED_API) +CYTHON_UNUSED static PyObject * +__Pyx_PyTuple_FromArray(PyObject *const *src, Py_ssize_t n) { + if (n <= 0) { + return __Pyx_NewRef(__pyx_mstate_global->__pyx_empty_tuple); + } + PyObject *res = PyTuple_New(n); + if (unlikely(res == NULL)) return NULL; + #if CYTHON_COMPILING_IN_CPYTHON + __Pyx_copy_object_array(src, ((PyTupleObject*)res)->ob_item, n); + #else + Py_ssize_t i; + for (i = 0; i < n; i++) { + Py_INCREF(src[i]); + if (unlikely(__Pyx_PyTuple_SET_ITEM(res, i, src[i]) < (0))) { + Py_DECREF(res); + return NULL; + } + } + #endif + return res; +} +#endif + +/* PyObjectCompare (used by UnicodeEquals) */ +#ifndef __Pyx_DEFINED_PyObject_CompareStrStrBoolEq +#define __Pyx_DEFINED_PyObject_CompareStrStrBoolEq +static CYTHON_INLINE int __Pyx_PyObject_CompareStrStrBoolEq(PyObject* s1, PyObject* s2) { + #if __PYX_LIMITED_VERSION_HEX >= 0x030e0000 + int result = PyUnicode_Equal(s1, s2); + #if !CYTHON_COMPILING_IN_CPYTHON + if (unlikely(result == -1)) return -1; + #endif + if (result == 0) goto __pyx_return_false; else goto __pyx_return_true; + #else + int result = PyUnicode_Compare(s1, s2); + if (unlikely((result == -1) && PyErr_Occurred())) return -1; + if (result == 0) goto __pyx_return_true; else goto __pyx_return_false; + #endif +__pyx_return_true: + return 1; +__pyx_return_false: + return 0; +} +#endif +static CYTHON_INLINE int __Pyx_PyObject_CompareBoolEq_str_str(PyObject *op1, PyObject *op2, int pyop) { + CYTHON_UNUSED_VAR(pyop); + if (unlikely(op1 == Py_None)) { + if (op2 == Py_None) goto __pyx_return_true; else goto __pyx_return_false; + } + if (unlikely(op2 == Py_None)) { + if (op1 == Py_None) goto __pyx_return_true; else goto __pyx_return_false; + } + + if (likely(op1 != Py_None)) { + if (op1 == op2) goto __pyx_return_true; + if (likely(op2 != Py_None)) { + return __Pyx_PyObject_CompareStrStrBoolEq(op1, op2); + } + goto __pyx_richcmp; + } + + if ((0)) goto __pyx_richcmp; + if ((0)) goto __pyx_return_true; + if ((0)) goto __pyx_return_false; +__pyx_richcmp: + return __Pyx_PyObject_RichCompareBool(op1, op2, Py_EQ); +__pyx_return_true: + return 1; +__pyx_return_false: + return 0; +} + +/* fastcall */ +#if CYTHON_VECTORCALL +static CYTHON_INLINE PyObject * __Pyx_GetKwValue_FASTCALL(PyObject *kwnames, PyObject *const *kwvalues, PyObject *s) +{ + Py_ssize_t i, n = __Pyx_PyTuple_GET_SIZE(kwnames); + #if !CYTHON_ASSUME_SAFE_SIZE + if (unlikely(n == -1)) return NULL; + #endif + for (i = 0; i < n; i++) + { + PyObject *namei = __Pyx_PyTuple_GET_ITEM(kwnames, i); + #if !CYTHON_ASSUME_SAFE_MACROS + if (unlikely(!namei)) return NULL; + #endif + if (s == namei) return kwvalues[i]; + } + for (i = 0; i < n; i++) + { + PyObject *namei = __Pyx_PyTuple_GET_ITEM(kwnames, i); + #if !CYTHON_ASSUME_SAFE_MACROS + if (unlikely(!namei)) return NULL; + #endif + int eq = __Pyx_PyUnicode_Equals(s, namei); + if (unlikely(eq != 0)) { + if (unlikely(eq < 0)) return NULL; + return kwvalues[i]; + } + } + return NULL; +} +#if CYTHON_COMPILING_IN_CPYTHON && PY_VERSION_HEX >= 0x030d0000 || CYTHON_COMPILING_IN_LIMITED_API || CYTHON_COMPILING_IN_PYPY || CYTHON_COMPILING_IN_GRAAL +CYTHON_UNUSED static PyObject *__Pyx_KwargsAsDict_FASTCALL(PyObject *kwnames, PyObject *const *kwvalues) { + Py_ssize_t i, nkwargs; + PyObject *dict; +#if !CYTHON_ASSUME_SAFE_SIZE + nkwargs = PyTuple_Size(kwnames); + if (unlikely(nkwargs < 0)) return NULL; +#else + nkwargs = PyTuple_GET_SIZE(kwnames); +#endif + dict = PyDict_New(); + if (unlikely(!dict)) + return NULL; + for (i=0; itype, *target->method_name); + if (unlikely(!method)) + return -1; + result = method; +#if CYTHON_COMPILING_IN_CPYTHON + if (likely(__Pyx_TypeCheck(method, &PyMethodDescr_Type))) + { + PyMethodDescrObject *descr = (PyMethodDescrObject*) method; + target->func = descr->d_method->ml_meth; + target->flag = descr->d_method->ml_flags & ~(METH_CLASS | METH_STATIC | METH_COEXIST); + } else +#endif +#if CYTHON_COMPILING_IN_PYPY +#else + if (PyCFunction_Check(method)) +#endif + { + PyObject *self; + int self_found; +#if CYTHON_COMPILING_IN_LIMITED_API || CYTHON_COMPILING_IN_PYPY + self = PyObject_GetAttrString(method, "__self__"); + if (!self && !__Pyx_IgnoreException(PyExc_AttributeError)) { + return -1; + } +#else + self = PyCFunction_GET_SELF(method); +#endif + self_found = (self && self != Py_None); +#if CYTHON_COMPILING_IN_LIMITED_API || CYTHON_COMPILING_IN_PYPY + Py_XDECREF(self); +#endif + if (self_found) { + PyObject *unbound_method = PyCFunction_New(&__Pyx_UnboundCMethod_Def, method); + if (unlikely(!unbound_method)) return -1; + Py_DECREF(method); + result = unbound_method; + } + } +#if !CYTHON_COMPILING_IN_CPYTHON_FREETHREADING + if (unlikely(target->method)) { + Py_DECREF(result); + } else +#endif + target->method = result; + return 0; +} + +/* CallUnboundCMethod0 */ +#if CYTHON_COMPILING_IN_CPYTHON +static CYTHON_INLINE PyObject* __Pyx_CallUnboundCMethod0(__Pyx_CachedCFunction* cfunc, PyObject* self) { + int was_initialized = __Pyx_CachedCFunction_GetAndSetInitializing(cfunc); + if (likely(was_initialized == 2 && cfunc->func)) { + if (likely(cfunc->flag == METH_NOARGS)) + return __Pyx_CallCFunction(cfunc, self, NULL); + if (likely(cfunc->flag == METH_FASTCALL)) + return __Pyx_CallCFunctionFast(cfunc, self, NULL, 0); + if (cfunc->flag == (METH_FASTCALL | METH_KEYWORDS)) + return __Pyx_CallCFunctionFastWithKeywords(cfunc, self, NULL, 0, NULL); + if (likely(cfunc->flag == (METH_VARARGS | METH_KEYWORDS))) + return __Pyx_CallCFunctionWithKeywords(cfunc, self, __pyx_mstate_global->__pyx_empty_tuple, NULL); + if (cfunc->flag == METH_VARARGS) + return __Pyx_CallCFunction(cfunc, self, __pyx_mstate_global->__pyx_empty_tuple); + return __Pyx__CallUnboundCMethod0(cfunc, self); + } +#if CYTHON_COMPILING_IN_CPYTHON_FREETHREADING + else if (unlikely(was_initialized == 1)) { + __Pyx_CachedCFunction tmp_cfunc = { +#ifndef __cplusplus + 0 +#endif + }; + tmp_cfunc.type = cfunc->type; + tmp_cfunc.method_name = cfunc->method_name; + return __Pyx__CallUnboundCMethod0(&tmp_cfunc, self); + } +#endif + PyObject *result = __Pyx__CallUnboundCMethod0(cfunc, self); + __Pyx_CachedCFunction_SetFinishedInitializing(cfunc); + return result; +} +#endif +static PyObject* __Pyx__CallUnboundCMethod0(__Pyx_CachedCFunction* cfunc, PyObject* self) { + PyObject *result; + if (unlikely(!cfunc->method) && unlikely(__Pyx_TryUnpackUnboundCMethod(cfunc) < 0)) return NULL; + result = __Pyx_PyObject_CallOneArg(cfunc->method, self); + return result; +} + +/* py_dict_items (used by OwnedDictNext) */ +static CYTHON_INLINE PyObject* __Pyx_PyDict_Items(PyObject* d) { + return __Pyx_CallUnboundCMethod0(&__pyx_mstate_global->__pyx_umethod_PyDict_Type_items, d); +} + +/* py_dict_values (used by OwnedDictNext) */ +static CYTHON_INLINE PyObject* __Pyx_PyDict_Values(PyObject* d) { + return __Pyx_CallUnboundCMethod0(&__pyx_mstate_global->__pyx_umethod_PyDict_Type_values, d); +} + +/* OwnedDictNext (used by ParseKeywordsImpl) */ +#if CYTHON_AVOID_BORROWED_REFS +static int __Pyx_PyDict_NextRef(PyObject *p, PyObject **ppos, PyObject **pkey, PyObject **pvalue) { + PyObject *next = NULL; + if (!*ppos) { + if (pvalue) { + PyObject *dictview = pkey ? __Pyx_PyDict_Items(p) : __Pyx_PyDict_Values(p); + if (unlikely(!dictview)) goto bad; + *ppos = PyObject_GetIter(dictview); + Py_DECREF(dictview); + } else { + *ppos = PyObject_GetIter(p); + } + if (unlikely(!*ppos)) goto bad; + } + next = PyIter_Next(*ppos); + if (!next) { + if (PyErr_Occurred()) goto bad; + return 0; + } + if (pkey && pvalue) { + *pkey = __Pyx_PySequence_ITEM(next, 0); + if (unlikely(*pkey)) goto bad; + *pvalue = __Pyx_PySequence_ITEM(next, 1); + if (unlikely(*pvalue)) goto bad; + Py_DECREF(next); + } else if (pkey) { + *pkey = next; + } else { + assert(pvalue); + *pvalue = next; + } + return 1; + bad: + Py_XDECREF(next); +#if !CYTHON_COMPILING_IN_LIMITED_API && PY_VERSION_HEX >= 0x030d0000 + PyErr_FormatUnraisable("Exception ignored in __Pyx_PyDict_NextRef"); +#else + PyErr_WriteUnraisable(__pyx_mstate_global->__pyx_n_u_Pyx_PyDict_NextRef); +#endif + if (pkey) *pkey = NULL; + if (pvalue) *pvalue = NULL; + return 0; +} +#else // !CYTHON_AVOID_BORROWED_REFS +static int __Pyx_PyDict_NextRef(PyObject *p, Py_ssize_t *ppos, PyObject **pkey, PyObject **pvalue) { + int result = PyDict_Next(p, ppos, pkey, pvalue); + if (likely(result == 1)) { + if (pkey) Py_INCREF(*pkey); + if (pvalue) Py_INCREF(*pvalue); + } + return result; +} +#endif + +/* RaiseDoubleKeywords (used by ParseKeywordsImpl) */ +static void __Pyx_RaiseDoubleKeywordsError( + const char* func_name, + PyObject* kw_name) +{ + PyErr_Format(PyExc_TypeError, + "%s() got multiple values for keyword argument '%U'", func_name, kw_name); +} + +/* CallUnboundCMethod2 */ +#if CYTHON_COMPILING_IN_CPYTHON +static CYTHON_INLINE PyObject *__Pyx_CallUnboundCMethod2(__Pyx_CachedCFunction *cfunc, PyObject *self, PyObject *arg1, PyObject *arg2) { + int was_initialized = __Pyx_CachedCFunction_GetAndSetInitializing(cfunc); + if (likely(was_initialized == 2 && cfunc->func)) { + PyObject *args[2] = {arg1, arg2}; + if (cfunc->flag == METH_FASTCALL) { + return __Pyx_CallCFunctionFast(cfunc, self, args, 2); + } + if (cfunc->flag == (METH_FASTCALL | METH_KEYWORDS)) + return __Pyx_CallCFunctionFastWithKeywords(cfunc, self, args, 2, NULL); + } +#if CYTHON_COMPILING_IN_CPYTHON_FREETHREADING + else if (unlikely(was_initialized == 1)) { + __Pyx_CachedCFunction tmp_cfunc = { +#ifndef __cplusplus + 0 +#endif + }; + tmp_cfunc.type = cfunc->type; + tmp_cfunc.method_name = cfunc->method_name; + return __Pyx__CallUnboundCMethod2(&tmp_cfunc, self, arg1, arg2); + } +#endif + PyObject *result = __Pyx__CallUnboundCMethod2(cfunc, self, arg1, arg2); + __Pyx_CachedCFunction_SetFinishedInitializing(cfunc); + return result; +} +#endif +static PyObject* __Pyx__CallUnboundCMethod2(__Pyx_CachedCFunction* cfunc, PyObject* self, PyObject* arg1, PyObject* arg2){ + if (unlikely(!cfunc->func && !cfunc->method) && unlikely(__Pyx_TryUnpackUnboundCMethod(cfunc) < 0)) return NULL; +#if CYTHON_COMPILING_IN_CPYTHON + if (cfunc->func && (cfunc->flag & METH_VARARGS)) { + PyObject *result = NULL; + PyObject *args = PyTuple_New(2); + if (unlikely(!args)) return NULL; + Py_INCREF(arg1); + PyTuple_SET_ITEM(args, 0, arg1); + Py_INCREF(arg2); + PyTuple_SET_ITEM(args, 1, arg2); + if (cfunc->flag & METH_KEYWORDS) + result = __Pyx_CallCFunctionWithKeywords(cfunc, self, args, NULL); + else + result = __Pyx_CallCFunction(cfunc, self, args); + Py_DECREF(args); + return result; + } +#endif + { + PyObject *args[4] = {NULL, self, arg1, arg2}; + return __Pyx_PyObject_FastCall(cfunc->method, args+1, 3 | __Pyx_PY_VECTORCALL_ARGUMENTS_OFFSET); + } +} + +/* ParseKeywordsImpl (used by ParseKeywords) */ +static int __Pyx_ValidateDuplicatePosArgs( + PyObject *kwds, + PyObject ** const argnames[], + PyObject ** const *first_kw_arg, + const char* function_name) +{ + PyObject ** const *name = argnames; + while (name != first_kw_arg) { + PyObject *key = **name; + int found = PyDict_Contains(kwds, key); + if (unlikely(found)) { + if (found == 1) __Pyx_RaiseDoubleKeywordsError(function_name, key); + goto bad; + } + name++; + } + return 0; +bad: + return -1; +} +#if CYTHON_USE_UNICODE_INTERNALS +static CYTHON_INLINE int __Pyx_UnicodeKeywordsEqual(PyObject *s1, PyObject *s2) { + int kind; + Py_ssize_t len = PyUnicode_GET_LENGTH(s1); + if (len != PyUnicode_GET_LENGTH(s2)) return 0; + kind = PyUnicode_KIND(s1); + if (kind != PyUnicode_KIND(s2)) return 0; + const void *data1 = PyUnicode_DATA(s1); + const void *data2 = PyUnicode_DATA(s2); + return (memcmp(data1, data2, (size_t) len * (size_t) kind) == 0); +} +#endif +static int __Pyx_MatchKeywordArg_str( + PyObject *key, + PyObject ** const argnames[], + PyObject ** const *first_kw_arg, + size_t *index_found, + const char *function_name) +{ + PyObject ** const *name; + #if CYTHON_USE_UNICODE_INTERNALS + Py_hash_t key_hash = ((PyASCIIObject*)key)->hash; + if (unlikely(key_hash == -1)) { + key_hash = PyObject_Hash(key); + if (unlikely(key_hash == -1)) + goto bad; + } + #endif + name = first_kw_arg; + while (*name) { + PyObject *name_str = **name; + #if CYTHON_USE_UNICODE_INTERNALS + if (key_hash == ((PyASCIIObject*)name_str)->hash && __Pyx_UnicodeKeywordsEqual(name_str, key)) { + *index_found = (size_t) (name - argnames); + return 1; + } + #else + #if CYTHON_ASSUME_SAFE_SIZE + if (PyUnicode_GET_LENGTH(name_str) == PyUnicode_GET_LENGTH(key)) + #endif + { + int cmp = PyUnicode_Compare(name_str, key); + if (cmp < 0 && unlikely(PyErr_Occurred())) goto bad; + if (cmp == 0) { + *index_found = (size_t) (name - argnames); + return 1; + } + } + #endif + name++; + } + name = argnames; + while (name != first_kw_arg) { + PyObject *name_str = **name; + #if CYTHON_USE_UNICODE_INTERNALS + if (unlikely(key_hash == ((PyASCIIObject*)name_str)->hash)) { + if (__Pyx_UnicodeKeywordsEqual(name_str, key)) + goto arg_passed_twice; + } + #else + #if CYTHON_ASSUME_SAFE_SIZE + if (PyUnicode_GET_LENGTH(name_str) == PyUnicode_GET_LENGTH(key)) + #endif + { + if (unlikely(name_str == key)) goto arg_passed_twice; + int cmp = PyUnicode_Compare(name_str, key); + if (cmp < 0 && unlikely(PyErr_Occurred())) goto bad; + if (cmp == 0) goto arg_passed_twice; + } + #endif + name++; + } + return 0; +arg_passed_twice: + __Pyx_RaiseDoubleKeywordsError(function_name, key); + goto bad; +bad: + return -1; +} +static int __Pyx_MatchKeywordArg_nostr( + PyObject *key, + PyObject ** const argnames[], + PyObject ** const *first_kw_arg, + size_t *index_found, + const char *function_name) +{ + PyObject ** const *name; + if (unlikely(!PyUnicode_Check(key))) goto invalid_keyword_type; + name = first_kw_arg; + while (*name) { + int cmp = PyObject_RichCompareBool(**name, key, Py_EQ); + if (cmp == 1) { + *index_found = (size_t) (name - argnames); + return 1; + } + if (unlikely(cmp == -1)) goto bad; + name++; + } + name = argnames; + while (name != first_kw_arg) { + int cmp = PyObject_RichCompareBool(**name, key, Py_EQ); + if (unlikely(cmp != 0)) { + if (cmp == 1) goto arg_passed_twice; + else goto bad; + } + name++; + } + return 0; +arg_passed_twice: + __Pyx_RaiseDoubleKeywordsError(function_name, key); + goto bad; +invalid_keyword_type: + PyErr_Format(PyExc_TypeError, + "%.200s() keywords must be strings", function_name); + goto bad; +bad: + return -1; +} +static CYTHON_INLINE int __Pyx_MatchKeywordArg( + PyObject *key, + PyObject ** const argnames[], + PyObject ** const *first_kw_arg, + size_t *index_found, + const char *function_name) +{ + return likely(PyUnicode_CheckExact(key)) ? + __Pyx_MatchKeywordArg_str(key, argnames, first_kw_arg, index_found, function_name) : + __Pyx_MatchKeywordArg_nostr(key, argnames, first_kw_arg, index_found, function_name); +} +static void __Pyx_RejectUnknownKeyword( + PyObject *kwds, + PyObject ** const argnames[], + PyObject ** const *first_kw_arg, + const char *function_name) +{ + #if CYTHON_AVOID_BORROWED_REFS + PyObject *pos = NULL; + #else + Py_ssize_t pos = 0; + #endif + PyObject *key = NULL; + __Pyx_BEGIN_CRITICAL_SECTION(kwds); + #if CYTHON_AVOID_BORROWED_REFS + while (__Pyx_PyDict_NextRef(kwds, &pos, &key, NULL)) + #else + (void) __Pyx_PyDict_NextRef; + while (PyDict_Next(kwds, &pos, &key, NULL)) + #endif + { + PyObject** const *name = first_kw_arg; + while (*name && (**name != key)) name++; + if (!*name) { + size_t index_found = 0; + int cmp = __Pyx_MatchKeywordArg(key, argnames, first_kw_arg, &index_found, function_name); + if (cmp != 1) { + if (cmp == 0) { + PyErr_Format(PyExc_TypeError, + "%s() got an unexpected keyword argument '%U'", + function_name, key); + } + #if CYTHON_AVOID_BORROWED_REFS + Py_DECREF(key); + #endif + break; + } + } + #if CYTHON_AVOID_BORROWED_REFS + Py_DECREF(key); + #endif + } + __Pyx_END_CRITICAL_SECTION(); + #if CYTHON_AVOID_BORROWED_REFS + Py_XDECREF(pos); + #endif + assert(PyErr_Occurred()); +} +static int __Pyx_ParseKeywordDict( + PyObject *kwds, + PyObject ** const argnames[], + PyObject *values[], + Py_ssize_t num_pos_args, + Py_ssize_t num_kwargs, + const char* function_name, + int ignore_unknown_kwargs) +{ + PyObject** const *name; + PyObject** const *first_kw_arg = argnames + num_pos_args; + Py_ssize_t extracted = 0; +#if !CYTHON_COMPILING_IN_PYPY || defined(PyArg_ValidateKeywordArguments) + if (unlikely(!PyArg_ValidateKeywordArguments(kwds))) return -1; +#endif + name = first_kw_arg; + while (*name && num_kwargs > extracted) { + PyObject * key = **name; + PyObject *value; + int found = 0; + #if __PYX_LIMITED_VERSION_HEX >= 0x030d0000 + found = PyDict_GetItemRef(kwds, key, &value); + #else + value = PyDict_GetItemWithError(kwds, key); + if (value) { + Py_INCREF(value); + found = 1; + } else { + if (unlikely(PyErr_Occurred())) goto bad; + } + #endif + if (found) { + if (unlikely(found < 0)) goto bad; + values[name-argnames] = value; + extracted++; + } + name++; + } + if (num_kwargs > extracted) { + if (ignore_unknown_kwargs) { + if (unlikely(__Pyx_ValidateDuplicatePosArgs(kwds, argnames, first_kw_arg, function_name) == -1)) + goto bad; + } else { + __Pyx_RejectUnknownKeyword(kwds, argnames, first_kw_arg, function_name); + goto bad; + } + } + return 0; +bad: + return -1; +} +static int __Pyx_ParseKeywordDictToDict( + PyObject *kwds, + PyObject ** const argnames[], + PyObject *kwds2, + PyObject *values[], + Py_ssize_t num_pos_args, + const char* function_name) +{ + PyObject** const *name; + PyObject** const *first_kw_arg = argnames + num_pos_args; + Py_ssize_t len; +#if !CYTHON_COMPILING_IN_PYPY || defined(PyArg_ValidateKeywordArguments) + if (unlikely(!PyArg_ValidateKeywordArguments(kwds))) return -1; +#endif + if (PyDict_Update(kwds2, kwds) < 0) goto bad; + name = first_kw_arg; + while (*name) { + PyObject *key = **name; + PyObject *value; +#if !CYTHON_COMPILING_IN_LIMITED_API && (PY_VERSION_HEX >= 0x030d00A2 || defined(PyDict_Pop)) + int found = PyDict_Pop(kwds2, key, &value); + if (found) { + if (unlikely(found < 0)) goto bad; + values[name-argnames] = value; + } +#elif __PYX_LIMITED_VERSION_HEX >= 0x030d0000 + int found = PyDict_GetItemRef(kwds2, key, &value); + if (found) { + if (unlikely(found < 0)) goto bad; + values[name-argnames] = value; + if (unlikely(PyDict_DelItem(kwds2, key) < 0)) goto bad; + } +#else + #if CYTHON_COMPILING_IN_CPYTHON + value = _PyDict_Pop(kwds2, key, kwds2); + #else + value = __Pyx_CallUnboundCMethod2(&__pyx_mstate_global->__pyx_umethod_PyDict_Type_pop, kwds2, key, kwds2); + #endif + if (value == kwds2) { + Py_DECREF(value); + } else { + if (unlikely(!value)) goto bad; + values[name-argnames] = value; + } +#endif + name++; + } + len = PyDict_Size(kwds2); + if (len > 0) { + return __Pyx_ValidateDuplicatePosArgs(kwds, argnames, first_kw_arg, function_name); + } else if (unlikely(len == -1)) { + goto bad; + } + return 0; +bad: + return -1; +} +static int __Pyx_ParseKeywordsTuple( + PyObject *kwds, + PyObject * const *kwvalues, + PyObject ** const argnames[], + PyObject *kwds2, + PyObject *values[], + Py_ssize_t num_pos_args, + Py_ssize_t num_kwargs, + const char* function_name, + int ignore_unknown_kwargs) +{ + PyObject *key = NULL; + PyObject** const * name; + PyObject** const *first_kw_arg = argnames + num_pos_args; + for (Py_ssize_t pos = 0; pos < num_kwargs; pos++) { +#if CYTHON_AVOID_BORROWED_REFS + key = __Pyx_PySequence_ITEM(kwds, pos); +#else + key = __Pyx_PyTuple_GET_ITEM(kwds, pos); +#endif +#if !CYTHON_ASSUME_SAFE_MACROS + if (unlikely(!key)) goto bad; +#endif + name = first_kw_arg; + while (*name && (**name != key)) name++; + if (*name) { + PyObject *value = kwvalues[pos]; + values[name-argnames] = __Pyx_NewRef(value); + } else { + size_t index_found = 0; + int cmp = __Pyx_MatchKeywordArg(key, argnames, first_kw_arg, &index_found, function_name); + if (cmp == 1) { + PyObject *value = kwvalues[pos]; + values[index_found] = __Pyx_NewRef(value); + } else { + if (unlikely(cmp == -1)) goto bad; + if (kwds2) { + PyObject *value = kwvalues[pos]; + if (unlikely(PyDict_SetItem(kwds2, key, value))) goto bad; + } else if (!ignore_unknown_kwargs) { + goto invalid_keyword; + } + } + } + #if CYTHON_AVOID_BORROWED_REFS + Py_DECREF(key); + key = NULL; + #endif + } + return 0; +invalid_keyword: + PyErr_Format(PyExc_TypeError, + "%s() got an unexpected keyword argument '%U'", + function_name, key); + goto bad; +bad: + #if CYTHON_AVOID_BORROWED_REFS + Py_XDECREF(key); + #endif + return -1; +} + +/* ParseKeywords */ +static int __Pyx_ParseKeywords( + PyObject *kwds, + PyObject * const *kwvalues, + PyObject ** const argnames[], + PyObject *kwds2, + PyObject *values[], + Py_ssize_t num_pos_args, + Py_ssize_t num_kwargs, + const char* function_name, + int ignore_unknown_kwargs) +{ + if (CYTHON_VECTORCALL && likely(PyTuple_Check(kwds))) + return __Pyx_ParseKeywordsTuple(kwds, kwvalues, argnames, kwds2, values, num_pos_args, num_kwargs, function_name, ignore_unknown_kwargs); + else if (kwds2) + return __Pyx_ParseKeywordDictToDict(kwds, argnames, kwds2, values, num_pos_args, function_name); + else + return __Pyx_ParseKeywordDict(kwds, argnames, values, num_pos_args, num_kwargs, function_name, ignore_unknown_kwargs); +} + +/* RaiseArgTupleInvalid */ +static void __Pyx_RaiseArgtupleInvalid( + const char* func_name, + int exact, + Py_ssize_t num_min, + Py_ssize_t num_max, + Py_ssize_t num_found) +{ + Py_ssize_t num_expected; + const char *more_or_less; + if (num_found < num_min) { + num_expected = num_min; + more_or_less = "at least"; + } else { + num_expected = num_max; + more_or_less = "at most"; + } + if (exact) { + more_or_less = "exactly"; + } + PyErr_Format(PyExc_TypeError, + "%.200s() takes %.8s %" CYTHON_FORMAT_SSIZE_T "d positional argument%.1s (%" CYTHON_FORMAT_SSIZE_T "d given)", + func_name, more_or_less, num_expected, + (num_expected == 1) ? "" : "s", num_found); +} + +/* GetException (used by pep479) */ +#if CYTHON_FAST_THREAD_STATE +static int __Pyx__GetException(PyThreadState *tstate, PyObject **type, PyObject **value, PyObject **tb) +#else +static int __Pyx_GetException(PyObject **type, PyObject **value, PyObject **tb) +#endif +{ + PyObject *local_type = NULL, *local_value, *local_tb = NULL; +#if CYTHON_FAST_THREAD_STATE + PyObject *tmp_type, *tmp_value, *tmp_tb; + #if PY_VERSION_HEX >= 0x030C0000 + local_value = tstate->current_exception; + tstate->current_exception = 0; + #else + local_type = tstate->curexc_type; + local_value = tstate->curexc_value; + local_tb = tstate->curexc_traceback; + tstate->curexc_type = 0; + tstate->curexc_value = 0; + tstate->curexc_traceback = 0; + #endif +#elif __PYX_LIMITED_VERSION_HEX >= 0x030C0000 + local_value = PyErr_GetRaisedException(); +#else + PyErr_Fetch(&local_type, &local_value, &local_tb); +#endif +#if __PYX_LIMITED_VERSION_HEX >= 0x030C0000 + if (likely(local_value)) { + local_type = (PyObject*) Py_TYPE(local_value); + Py_INCREF(local_type); + local_tb = PyException_GetTraceback(local_value); + } +#else + PyErr_NormalizeException(&local_type, &local_value, &local_tb); +#if CYTHON_FAST_THREAD_STATE + if (unlikely(tstate->curexc_type)) +#else + if (unlikely(PyErr_Occurred())) +#endif + goto bad; + if (local_tb) { + if (unlikely(PyException_SetTraceback(local_value, local_tb) < 0)) + goto bad; + } +#endif // __PYX_LIMITED_VERSION_HEX >= 0x030C0000 + Py_XINCREF(local_tb); + Py_XINCREF(local_type); + Py_XINCREF(local_value); + *type = local_type; + *value = local_value; + *tb = local_tb; +#if CYTHON_FAST_THREAD_STATE + #if CYTHON_USE_EXC_INFO_STACK + { + _PyErr_StackItem *exc_info = tstate->exc_info; + #if PY_VERSION_HEX >= 0x030B00a4 + tmp_value = exc_info->exc_value; + exc_info->exc_value = local_value; + tmp_type = NULL; + tmp_tb = NULL; + Py_XDECREF(local_type); + Py_XDECREF(local_tb); + #else + tmp_type = exc_info->exc_type; + tmp_value = exc_info->exc_value; + tmp_tb = exc_info->exc_traceback; + exc_info->exc_type = local_type; + exc_info->exc_value = local_value; + exc_info->exc_traceback = local_tb; + #endif + } + #else + tmp_type = tstate->exc_type; + tmp_value = tstate->exc_value; + tmp_tb = tstate->exc_traceback; + tstate->exc_type = local_type; + tstate->exc_value = local_value; + tstate->exc_traceback = local_tb; + #endif + Py_XDECREF(tmp_type); + Py_XDECREF(tmp_value); + Py_XDECREF(tmp_tb); +#elif __PYX_LIMITED_VERSION_HEX >= 0x030b0000 + PyErr_SetHandledException(local_value); + Py_XDECREF(local_value); + Py_XDECREF(local_type); + Py_XDECREF(local_tb); +#else + PyErr_SetExcInfo(local_type, local_value, local_tb); +#endif + return 0; +#if __PYX_LIMITED_VERSION_HEX < 0x030C0000 +bad: + *type = 0; + *value = 0; + *tb = 0; + Py_XDECREF(local_type); + Py_XDECREF(local_value); + Py_XDECREF(local_tb); + return -1; +#endif +} + +/* pep479 */ +static void __Pyx_Generator_Replace_StopIteration(int in_async_gen) { + PyObject *exc, *val, *tb, *cur_exc, *new_exc; + __Pyx_PyThreadState_declare + int is_async_stopiteration = 0; + CYTHON_MAYBE_UNUSED_VAR(in_async_gen); + __Pyx_PyThreadState_assign + cur_exc = __Pyx_PyErr_CurrentExceptionType(); + if (likely(!__Pyx_PyErr_GivenExceptionMatches(cur_exc, PyExc_StopIteration))) { + if (in_async_gen && unlikely(__Pyx_PyErr_GivenExceptionMatches(cur_exc, PyExc_StopAsyncIteration))) { + is_async_stopiteration = 1; + } else { + return; + } + } + __Pyx_GetException(&exc, &val, &tb); + Py_XDECREF(exc); + Py_XDECREF(tb); + new_exc = PyObject_CallFunction(PyExc_RuntimeError, "s", + is_async_stopiteration ? "async generator raised StopAsyncIteration" : + in_async_gen ? "async generator raised StopIteration" : + "generator raised StopIteration"); + if (!new_exc) { + Py_XDECREF(val); + return; + } + PyException_SetCause(new_exc, val); // steals ref to val + PyErr_SetObject(PyExc_RuntimeError, new_exc); + Py_DECREF(new_exc); +} + +/* GetTopmostException (used by SaveResetException) */ +#if CYTHON_USE_EXC_INFO_STACK && CYTHON_FAST_THREAD_STATE +static _PyErr_StackItem * +__Pyx_PyErr_GetTopmostException(PyThreadState *tstate) +{ + _PyErr_StackItem *exc_info = tstate->exc_info; + while ((exc_info->exc_value == NULL || exc_info->exc_value == Py_None) && + exc_info->previous_item != NULL) + { + exc_info = exc_info->previous_item; + } + return exc_info; +} +#endif + +/* SaveResetException */ +#if CYTHON_FAST_THREAD_STATE +static CYTHON_INLINE void __Pyx__ExceptionSave(PyThreadState *tstate, PyObject **type, PyObject **value, PyObject **tb) { + #if CYTHON_USE_EXC_INFO_STACK && PY_VERSION_HEX >= 0x030B00a4 + _PyErr_StackItem *exc_info = __Pyx_PyErr_GetTopmostException(tstate); + PyObject *exc_value = exc_info->exc_value; + if (exc_value == NULL || exc_value == Py_None) { + *value = NULL; + *type = NULL; + *tb = NULL; + } else { + *value = exc_value; + Py_INCREF(*value); + *type = (PyObject*) Py_TYPE(exc_value); + Py_INCREF(*type); + *tb = PyException_GetTraceback(exc_value); + } + #elif CYTHON_USE_EXC_INFO_STACK + _PyErr_StackItem *exc_info = __Pyx_PyErr_GetTopmostException(tstate); + *type = exc_info->exc_type; + *value = exc_info->exc_value; + *tb = exc_info->exc_traceback; + Py_XINCREF(*type); + Py_XINCREF(*value); + Py_XINCREF(*tb); + #else + *type = tstate->exc_type; + *value = tstate->exc_value; + *tb = tstate->exc_traceback; + Py_XINCREF(*type); + Py_XINCREF(*value); + Py_XINCREF(*tb); + #endif +} +static CYTHON_INLINE void __Pyx__ExceptionReset(PyThreadState *tstate, PyObject *type, PyObject *value, PyObject *tb) { + #if CYTHON_USE_EXC_INFO_STACK && PY_VERSION_HEX >= 0x030B00a4 + _PyErr_StackItem *exc_info = tstate->exc_info; + PyObject *tmp_value = exc_info->exc_value; + exc_info->exc_value = value; + Py_XDECREF(tmp_value); + Py_XDECREF(type); + Py_XDECREF(tb); + #else + PyObject *tmp_type, *tmp_value, *tmp_tb; + #if CYTHON_USE_EXC_INFO_STACK + _PyErr_StackItem *exc_info = tstate->exc_info; + tmp_type = exc_info->exc_type; + tmp_value = exc_info->exc_value; + tmp_tb = exc_info->exc_traceback; + exc_info->exc_type = type; + exc_info->exc_value = value; + exc_info->exc_traceback = tb; + #else + tmp_type = tstate->exc_type; + tmp_value = tstate->exc_value; + tmp_tb = tstate->exc_traceback; + tstate->exc_type = type; + tstate->exc_value = value; + tstate->exc_traceback = tb; + #endif + Py_XDECREF(tmp_type); + Py_XDECREF(tmp_value); + Py_XDECREF(tmp_tb); + #endif +} +#endif + +/* IterNextPlain (used by IterNext) */ +#if CYTHON_COMPILING_IN_LIMITED_API && __PYX_LIMITED_VERSION_HEX < 0x030A0000 +static PyObject *__Pyx_GetBuiltinNext_LimitedAPI(void) { + if (unlikely(!__pyx_mstate_global->__Pyx_GetBuiltinNext_LimitedAPI_cache)) + __pyx_mstate_global->__Pyx_GetBuiltinNext_LimitedAPI_cache = __Pyx_GetBuiltinName(__pyx_mstate_global->__pyx_n_u_next); + return __pyx_mstate_global->__Pyx_GetBuiltinNext_LimitedAPI_cache; +} +#endif +static CYTHON_INLINE PyObject *__Pyx_PyIter_Next_Plain(PyObject *iterator) { +#if CYTHON_COMPILING_IN_LIMITED_API && __PYX_LIMITED_VERSION_HEX < 0x030A0000 + PyObject *result; + PyObject *next = __Pyx_GetBuiltinNext_LimitedAPI(); + if (unlikely(!next)) return NULL; + result = PyObject_CallFunctionObjArgs(next, iterator, NULL); + return result; +#else + (void)__Pyx_GetBuiltinName; // only for early limited API + iternextfunc iternext = __Pyx_PyObject_GetIterNextFunc(iterator); + assert(iternext); + return iternext(iterator); +#endif +} + +/* RaiseErrorWithObjectType (used by IterNext) */ +static void __Pyx_RaiseErrorWithType(PyObject* exc_type, const char* message, PyTypeObject *type_obj) { + __Pyx_TypeName type_name = __Pyx_PyType_GetFullyQualifiedName(type_obj); + #if CYTHON_COMPILING_IN_LIMITED_API && __PYX_LIMITED_VERSION_HEX < 0x030d0000 + if (unlikely(!type_name)) return; + #endif + PyErr_Format(exc_type, message, type_name); + __Pyx_DECREF_TypeName(type_name); +} + +/* IterNext */ +static PyObject *__Pyx_PyIter_Next2Default(PyObject* defval) { + PyObject* exc_type; + __Pyx_PyThreadState_declare + __Pyx_PyThreadState_assign + exc_type = __Pyx_PyErr_CurrentExceptionType(); + if (unlikely(exc_type)) { + if (!defval || unlikely(!__Pyx_PyErr_GivenExceptionMatches(exc_type, PyExc_StopIteration))) + return NULL; + __Pyx_PyErr_Clear(); + Py_INCREF(defval); + return defval; + } + if (defval) { + Py_INCREF(defval); + return defval; + } + __Pyx_PyErr_SetNone(PyExc_StopIteration); + return NULL; +} +static void __Pyx_PyIter_Next_ErrorNoIterator(PyObject *iterator) { + __Pyx_RaiseTypeErrorWithObjectType( + __Pyx_FMT_TYPENAME " object is not an iterator", iterator); +} +static CYTHON_INLINE PyObject *__Pyx_PyIter_Next2(PyObject* iterator, PyObject* defval) { + PyObject* next; +#if !CYTHON_COMPILING_IN_LIMITED_API + iternextfunc iternext = __Pyx_PyObject_TryGetSlot(iterator, tp_iternext, iternextfunc); + if (likely(iternext)) { + next = iternext(iterator); + if (likely(next)) + return next; + #if CYTHON_COMPILING_IN_CPYTHON && PY_VERSION_HEX < 0x030d0000 + if (unlikely(iternext == &_PyObject_NextNotImplemented)) + return NULL; + #endif + } else if (CYTHON_USE_TYPE_SLOTS) { + __Pyx_PyIter_Next_ErrorNoIterator(iterator); + return NULL; + } else +#endif + if (unlikely(!PyIter_Check(iterator))) { + __Pyx_PyIter_Next_ErrorNoIterator(iterator); + return NULL; + } else { + next = defval ? PyIter_Next(iterator) : __Pyx_PyIter_Next_Plain(iterator); + if (likely(next)) + return next; + } + return __Pyx_PyIter_Next2Default(defval); +} + +/* ListAppendAndDecrefInternal (used by ListAppend) */ +#if CYTHON_USE_PYLIST_INTERNALS && CYTHON_ASSUME_SAFE_MACROS && CYTHON_ASSUME_SAFE_SIZE +static CYTHON_INLINE void __Pyx__ListComp_AppendAndDecref(PyObject* list, Py_ssize_t len, PyObject* x) { + PyList_SET_ITEM(list, len, x); + Py_SET_SIZE(list, len + 1); +} +#endif + +/* ListAppend */ +#if CYTHON_USE_PYLIST_INTERNALS && CYTHON_ASSUME_SAFE_MACROS && CYTHON_ASSUME_SAFE_SIZE +static CYTHON_INLINE int __Pyx_PyList_Append(PyObject* list, PyObject* x) { + PyListObject* L = (PyListObject*) list; + Py_ssize_t len = Py_SIZE(list); + if (likely(L->allocated > len) & likely(len > (L->allocated >> 1))) { + Py_INCREF(x); + __Pyx__ListComp_AppendAndDecref(list, len, x); + return 0; + } + return PyList_Append(list, x); +} +#endif + +/* ListCompAppendAndDecref */ +#if CYTHON_USE_PYLIST_INTERNALS && CYTHON_ASSUME_SAFE_MACROS && CYTHON_ASSUME_SAFE_SIZE +static CYTHON_INLINE int __Pyx_ListComp_AppendAndDecref(PyObject* list, PyObject* x) { + PyListObject* L = (PyListObject*) list; + Py_ssize_t len = Py_SIZE(list); + if (likely(L->allocated > len)) { + __Pyx__ListComp_AppendAndDecref(list, len, x); + return 0; + } + int result = PyList_Append(list, x); + Py_DECREF(x); + return result; +} +#else +static CYTHON_INLINE int __Pyx_ListComp_AppendAndDecref(PyObject* list, PyObject* x) { + int result = PyList_Append(list, x); + Py_DECREF(x); + return result; +} +#endif + +/* PyLongBinop */ +#if !CYTHON_COMPILING_IN_PYPY +static PyObject* __Pyx_Fallback___Pyx_PyLong_AddObjC(PyObject *op1, PyObject *op2, int inplace) { + return (inplace ? PyNumber_InPlaceAdd : PyNumber_Add)(op1, op2); +} +#if CYTHON_USE_PYLONG_INTERNALS +static PyObject* __Pyx_Unpacked___Pyx_PyLong_AddObjC(PyObject *op1, PyObject *op2, long intval, int inplace, int zerodivision_check) { + CYTHON_MAYBE_UNUSED_VAR(inplace); + CYTHON_UNUSED_VAR(zerodivision_check); + const long b = intval; + long a; + const PY_LONG_LONG llb = intval; + PY_LONG_LONG lla; + if (unlikely(__Pyx_PyLong_IsZero(op1))) { + return __Pyx_NewRef(op2); + } + const int is_positive = __Pyx_PyLong_IsPos(op1); + const digit* digits = __Pyx_PyLong_Digits(op1); + const Py_ssize_t size = __Pyx_PyLong_DigitCount(op1); + if (likely(size == 1)) { + a = (long) digits[0]; + if (!is_positive) a *= -1; + } else { + if (size == 2 && 8 * sizeof(long) - 1 > 2 * PyLong_SHIFT) { + a = (long) (((((unsigned long)digits[1]) << PyLong_SHIFT) | (unsigned long)digits[0])); + if (!is_positive) a *= -1; + goto calculate_long; + } else if (size == 2 && 8 * sizeof(PY_LONG_LONG) - 1 > 2 * PyLong_SHIFT) { + lla = (PY_LONG_LONG) (((((unsigned PY_LONG_LONG)digits[1]) << PyLong_SHIFT) | (unsigned PY_LONG_LONG)digits[0])); + if (!is_positive) lla *= -1; + goto calculate_long_long; + } else + if (size == 3 && 8 * sizeof(long) - 1 > 3 * PyLong_SHIFT) { + a = (long) (((((((unsigned long)digits[2]) << PyLong_SHIFT) | (unsigned long)digits[1]) << PyLong_SHIFT) | (unsigned long)digits[0])); + if (!is_positive) a *= -1; + goto calculate_long; + } else if (size == 3 && 8 * sizeof(PY_LONG_LONG) - 1 > 3 * PyLong_SHIFT) { + lla = (PY_LONG_LONG) (((((((unsigned PY_LONG_LONG)digits[2]) << PyLong_SHIFT) | (unsigned PY_LONG_LONG)digits[1]) << PyLong_SHIFT) | (unsigned PY_LONG_LONG)digits[0])); + if (!is_positive) lla *= -1; + goto calculate_long_long; + } else + if (size == 4 && 8 * sizeof(long) - 1 > 4 * PyLong_SHIFT) { + a = (long) (((((((((unsigned long)digits[3]) << PyLong_SHIFT) | (unsigned long)digits[2]) << PyLong_SHIFT) | (unsigned long)digits[1]) << PyLong_SHIFT) | (unsigned long)digits[0])); + if (!is_positive) a *= -1; + goto calculate_long; + } else if (size == 4 && 8 * sizeof(PY_LONG_LONG) - 1 > 4 * PyLong_SHIFT) { + lla = (PY_LONG_LONG) (((((((((unsigned PY_LONG_LONG)digits[3]) << PyLong_SHIFT) | (unsigned PY_LONG_LONG)digits[2]) << PyLong_SHIFT) | (unsigned PY_LONG_LONG)digits[1]) << PyLong_SHIFT) | (unsigned PY_LONG_LONG)digits[0])); + if (!is_positive) lla *= -1; + goto calculate_long_long; + } else + {} + return PyLong_Type.tp_as_number->nb_add(op1, op2); + } + calculate_long: + { + long x; + x = a + b; + return PyLong_FromLong(x); + } + calculate_long_long: + { + PY_LONG_LONG llx; + llx = lla + llb; + return PyLong_FromLongLong(llx); + } + +} +#endif +static PyObject* __Pyx_Float___Pyx_PyLong_AddObjC(PyObject *float_val, long intval, int zerodivision_check) { + CYTHON_UNUSED_VAR(zerodivision_check); + const long b = intval; + double a = __Pyx_PyFloat_AS_DOUBLE(float_val); + double result; + + result = ((double)a) + (double)b; + return PyFloat_FromDouble(result); +} +static CYTHON_INLINE PyObject* __Pyx_PyLong_AddObjC(PyObject *op1, PyObject *op2, long intval, int inplace, int zerodivision_check) { + CYTHON_MAYBE_UNUSED_VAR(intval); + CYTHON_UNUSED_VAR(zerodivision_check); + #if CYTHON_USE_PYLONG_INTERNALS + if (likely(PyLong_CheckExact(op1))) { + return __Pyx_Unpacked___Pyx_PyLong_AddObjC(op1, op2, intval, inplace, zerodivision_check); + } + #endif + if (PyFloat_CheckExact(op1)) { + return __Pyx_Float___Pyx_PyLong_AddObjC(op1, intval, zerodivision_check); + } + return __Pyx_Fallback___Pyx_PyLong_AddObjC(op1, op2, inplace); +} +#endif + +/* RaiseException */ +static void __Pyx_Raise(PyObject *type, PyObject *value, PyObject *tb, PyObject *cause) { + PyObject* owned_instance = NULL; + if (tb == Py_None) { + tb = 0; + } else if (tb && !PyTraceBack_Check(tb)) { + PyErr_SetString(PyExc_TypeError, + "raise: arg 3 must be a traceback or None"); + goto bad; + } + if (value == Py_None) + value = 0; + if (PyExceptionInstance_Check(type)) { + if (value) { + PyErr_SetString(PyExc_TypeError, + "instance exception may not have a separate value"); + goto bad; + } + value = type; + type = (PyObject*) Py_TYPE(value); + } else if (PyExceptionClass_Check(type)) { + PyObject *instance_class = NULL; + if (value && PyExceptionInstance_Check(value)) { + instance_class = (PyObject*) Py_TYPE(value); + if (instance_class != type) { + int is_subclass = PyObject_IsSubclass(instance_class, type); + if (!is_subclass) { + instance_class = NULL; + } else if (unlikely(is_subclass == -1)) { + goto bad; + } else { + type = instance_class; + } + } + } + if (!instance_class) { + PyObject *args; + if (!value) + args = PyTuple_New(0); + else if (PyTuple_Check(value)) { + Py_INCREF(value); + args = value; + } else + args = PyTuple_Pack(1, value); + if (!args) + goto bad; + owned_instance = PyObject_Call(type, args, NULL); + Py_DECREF(args); + if (!owned_instance) + goto bad; + value = owned_instance; + if (!PyExceptionInstance_Check(value)) { + PyErr_Format(PyExc_TypeError, + "calling %R should have returned an instance of " + "BaseException, not %R", + type, Py_TYPE(value)); + goto bad; + } + } + } else { + PyErr_SetString(PyExc_TypeError, + "raise: exception class must be a subclass of BaseException"); + goto bad; + } + if (cause) { + PyObject *fixed_cause; + if (cause == Py_None) { + fixed_cause = NULL; + } else if (PyExceptionClass_Check(cause)) { + fixed_cause = PyObject_CallObject(cause, NULL); + if (fixed_cause == NULL) + goto bad; + } else if (PyExceptionInstance_Check(cause)) { + fixed_cause = cause; + Py_INCREF(fixed_cause); + } else { + PyErr_SetString(PyExc_TypeError, + "exception causes must derive from " + "BaseException"); + goto bad; + } + PyException_SetCause(value, fixed_cause); + } + PyErr_SetObject(type, value); + if (tb) { +#if PY_VERSION_HEX >= 0x030C00A6 + PyException_SetTraceback(value, tb); +#elif CYTHON_FAST_THREAD_STATE + PyThreadState *tstate = __Pyx_PyThreadState_Current; + PyObject* tmp_tb = tstate->curexc_traceback; + if (tb != tmp_tb) { + Py_INCREF(tb); + tstate->curexc_traceback = tb; + Py_XDECREF(tmp_tb); + } +#else + PyObject *tmp_type, *tmp_value, *tmp_tb; + PyErr_Fetch(&tmp_type, &tmp_value, &tmp_tb); + Py_INCREF(tb); + PyErr_Restore(tmp_type, tmp_value, tb); + Py_XDECREF(tmp_tb); +#endif + } +bad: + Py_XDECREF(owned_instance); + return; +} + +/* PyObjectCompare */ +#ifndef __Pyx_DEFINED_PyObject_CompareIntFloatBoolEq +#define __Pyx_DEFINED_PyObject_CompareIntFloatBoolEq +static int __Pyx_PyObject_CompareIntFloatBoolEq(PyObject *op1, PyObject *op2) { + double float_op2 = __Pyx_PyFloat_AS_DOUBLE(op2); + #if !CYTHON_ASSUME_SAFE_MACROS + if (unlikely(float_op2 == -1. && PyErr_Occurred())) return -1; + #endif + #if CYTHON_USE_PYLONG_INTERNALS + if (__Pyx_PyLong_IsCompact(op1)) { + Py_ssize_t iop1 = __Pyx_PyLong_CompactValue(op1); + if (((double)iop1) == float_op2) goto __pyx_return_true; else goto __pyx_return_false; + } + if (unlikely(!isfinite(float_op2))) { + if (0.0 == float_op2) goto __pyx_return_true; else goto __pyx_return_false; + } else { + int sign1 = __Pyx_PyLong_Sign(op1); + if (float_op2 >= 0.) { + if (sign1 < 0) goto __pyx_return_false; + if (float_op2 < (double) (1L << PyLong_SHIFT)) goto __pyx_return_false; + } else { + if (sign1 > 0) goto __pyx_return_false; + if (float_op2 > -(double) (1L << PyLong_SHIFT)) goto __pyx_return_false; + } + } + #else + if (unlikely(!isfinite(float_op2))) { + if (0.0 == float_op2) goto __pyx_return_true; else goto __pyx_return_false; + } else { + int overflow1; + long iop1 = PyLong_AsLongAndOverflow(op1, &overflow1); + if (likely(!overflow1)) { + if ((long long) iop1 >= (1LL << 53)) { + overflow1 = 1; + } else if ((long long) iop1 <= - (1LL << 53)) { + overflow1 = -1; + } else { + if (((double) iop1) == float_op2) goto __pyx_return_true; else goto __pyx_return_false; + } + } + if (overflow1 < 0) { + if (float_op2 > ((double) (1LL << 53))) goto __pyx_return_false; + } else { + if (float_op2 < - ((double) (1LL << 53))) goto __pyx_return_false; + } + } + #endif + return __Pyx_PyObject_RichCompareBool(op1, op2, Py_EQ); +__pyx_return_true: + return 1; +__pyx_return_false: + return 0; +} +#endif +#ifndef __Pyx_DEFINED_PyObject_CompareIntIntBoolEq +#define __Pyx_DEFINED_PyObject_CompareIntIntBoolEq +static int __Pyx_PyObject_CompareIntIntBoolEq(PyObject *op1, PyObject *op2) { +#if CYTHON_USE_PYLONG_INTERNALS + Py_ssize_t cmp = __Pyx_PyLong_CompareSignAndSize(op1, op2); + if (cmp == 0) { + Py_ssize_t size = __Pyx_PyLong_DigitCount(op1); + if (size > 0) { + const digit* digits1 = __Pyx_PyLong_Digits(op1); + const digit* digits2 = __Pyx_PyLong_Digits(op2); + if (size == 1) { + cmp = (Py_ssize_t) digits1[0] - (Py_ssize_t) digits2[0]; + } else if ((size == 2) && (8 * sizeof(Py_ssize_t) >= 2 * PyLong_SHIFT)) { + cmp = (Py_ssize_t) (((((size_t)digits1[1]) << PyLong_SHIFT) | (size_t)digits1[0])) - (Py_ssize_t) (((((size_t)digits2[1]) << PyLong_SHIFT) | (size_t)digits2[0])); + } else { + for (Py_ssize_t i=size-1; i >= 0 && !cmp; --i) { + cmp = (Py_ssize_t) digits1[i] - (Py_ssize_t) digits2[i]; + } + } + } + if (cmp == 0) goto __pyx_return_true; + if (__Pyx_PyLong_IsNeg(op1)) cmp = -cmp; + } + goto __pyx_return_false; +#else + int overflow1, overflow2; + long long iop1 = PyLong_AsLongLongAndOverflow(op1, &overflow1); + long long iop2 = PyLong_AsLongLongAndOverflow(op2, &overflow2); + if (likely(!(overflow1 | overflow2))) { + if (iop1 == iop2) goto __pyx_return_true; else goto __pyx_return_false; + } else if (overflow1 != overflow2) { + if (overflow1 == overflow2) goto __pyx_return_true; else goto __pyx_return_false; + } else { + return __Pyx_PyObject_RichCompareBool(op1, op2, Py_EQ); + } +#endif +__pyx_return_true: + return 1; +__pyx_return_false: + return 0; +} +#endif +static CYTHON_INLINE int __Pyx_PyObject_CompareBoolEq_int_object(PyObject *op1, PyObject *op2, int pyop) { + CYTHON_UNUSED_VAR(pyop); + if (unlikely(op1 == Py_None)) { + if (op2 == Py_None) goto __pyx_return_true; else goto __pyx_richcmp; + } + if (op1 == op2) goto __pyx_return_true; + if (likely(op1 != Py_None)) { + if (op1 == op2) goto __pyx_return_true; + if (likely(PyLong_CheckExact(op2))) { + return __Pyx_PyObject_CompareIntIntBoolEq(op1, op2); + } + if (PyFloat_CheckExact(op2)) { + return __Pyx_PyObject_CompareIntFloatBoolEq(op1, op2); + } + goto __pyx_richcmp; + } + if ((0)) goto __pyx_richcmp; + if ((0)) goto __pyx_return_true; + if ((0)) goto __pyx_return_false; +__pyx_richcmp: + return __Pyx_PyObject_RichCompareBool(op1, op2, Py_EQ); +__pyx_return_true: + return 1; +__pyx_return_false: + return 0; +} + +/* SetItemInt */ +static int __Pyx_SetItemInt_Generic(PyObject *o, PyObject *j, PyObject *v) { + int r; + if (unlikely(!j)) return -1; + r = PyObject_SetItem(o, j, v); + Py_DECREF(j); + return r; +} +#if CYTHON_USE_TYPE_SLOTS && !CYTHON_COMPILING_IN_PYPY +static CYTHON_INLINE int __Pyx_SetItemInt_Fast_mapping(PyObject *o, objobjargproc setitem, Py_ssize_t i, PyObject *value) { + PyObject *key = PyLong_FromSsize_t(i); + if (unlikely(!key)) return -1; + int r = setitem(o, key, value); + Py_DECREF(key); + return r; +} +#endif +static CYTHON_INLINE int __Pyx_SetItemInt_Fast(PyObject *o, Py_ssize_t i, PyObject *v, + int wraparound, int boundscheck, int unsafe_shared) { + CYTHON_MAYBE_UNUSED_VAR(unsafe_shared); +#if CYTHON_ASSUME_SAFE_MACROS && CYTHON_ASSUME_SAFE_SIZE && !CYTHON_AVOID_BORROWED_REFS + if (PyList_CheckExact(o)) { + Py_ssize_t n = (!wraparound) ? i : ((likely(i >= 0)) ? i : i + PyList_GET_SIZE(o)); + if ((CYTHON_AVOID_THREAD_UNSAFE_BORROWED_REFS && !__Pyx_IS_UNIQUELY_REFERENCED(o, unsafe_shared))) { + Py_INCREF(v); + return PyList_SetItem(o, n, v); + } else if ((!boundscheck) || likely(__Pyx_is_valid_index(n, PyList_GET_SIZE(o)))) { + PyObject* old; + Py_INCREF(v); + old = PyList_GET_ITEM(o, n); + PyList_SET_ITEM(o, n, v); + Py_DECREF(old); + return 0; + } + } else +#endif +#if CYTHON_USE_TYPE_SLOTS && !CYTHON_COMPILING_IN_PYPY + if (PyDict_CheckExact(o)) { + return __Pyx_SetItemInt_Fast_mapping(o, PyDict_Type.tp_as_mapping->mp_ass_subscript, i, v); + } else + { + PyTypeObject *obj_type = Py_TYPE(o); + int seq_or_mapping = __Pyx_PyType_GetFlags(obj_type) & (Py_TPFLAGS_SEQUENCE|Py_TPFLAGS_MAPPING); + if (seq_or_mapping != Py_TPFLAGS_SEQUENCE) { + PyMappingMethods *mm = obj_type->tp_as_mapping; + if (mm && mm->mp_ass_subscript) + return __Pyx_SetItemInt_Fast_mapping(o, mm->mp_ass_subscript, i, v); + } + PySequenceMethods *sm = obj_type->tp_as_sequence; + if (likely(sm && sm->sq_ass_item)) { + if (wraparound && (i < 0) && unlikely(__Pyx_GetItemInt_wraparound(o, sm, &i) == -1)) + return -1; + return sm->sq_ass_item(o, i, v); + } + if (seq_or_mapping == Py_TPFLAGS_SEQUENCE) { + PyMappingMethods *mm = obj_type->tp_as_mapping; + if (likely(mm && mm->mp_ass_subscript)) + return __Pyx_SetItemInt_Fast_mapping(o, mm->mp_ass_subscript, i, v); + } + } +#else + if (!PyMapping_Check(o)) { + return PySequence_SetItem(o, i, v); + } +#endif + (void)wraparound; + (void)boundscheck; + return __Pyx_SetItemInt_Generic(o, PyLong_FromSsize_t(i), v); +} + +/* ModInt[long] */ +static CYTHON_INLINE long __Pyx_mod_long(long a, long b, int b_is_constant) { + long r = a % b; + long adapt_python = (b_is_constant ? + ((r != 0) & ((r < 0) ^ (b < 0))) : + ((r != 0) & ((r ^ b) < 0)) + ); + return r + adapt_python * b; +} + +/* AllocateExtensionType */ +static PyObject *__Pyx_AllocateExtensionType(PyTypeObject *t, int is_final) { + if (is_final || likely(!__Pyx_PyType_HasFeature(t, Py_TPFLAGS_IS_ABSTRACT))) { + allocfunc alloc_func = __Pyx_PyType_GetSlot(t, tp_alloc, allocfunc); + return alloc_func(t, 0); + } else { + newfunc tp_new = __Pyx_PyType_TryGetSlot(&PyBaseObject_Type, tp_new, newfunc); + #if CYTHON_COMPILING_IN_LIMITED_API && __PYX_LIMITED_VERSION_HEX < 0x030A0000 + if (!tp_new) { + PyObject *new_str = PyUnicode_FromString("__new__"); + if (likely(new_str)) { + PyObject *o = PyObject_CallMethodObjArgs((PyObject *)&PyBaseObject_Type, new_str, t, NULL); + Py_DECREF(new_str); + return o; + } else + return NULL; + } else + #endif + return tp_new(t, __pyx_mstate_global->__pyx_empty_tuple, 0); + } +} + +/* CallSlotAsVectorcallUnpackDict (used by CallSlotAsVectorcall) */ +static CYTHON_INLINE int __Pyx_CallSlotAsVectorcallUnpackDict(Py_ssize_t args_size, PyObject *keys_dict, PyObject **args, PyObject *kwnames) { + Py_ssize_t pos = 0, i = 0; + PyObject *key, *value; + while (PyDict_Next(keys_dict, &pos, &key, &value)) { + Py_INCREF(value); + args[args_size+i] = value; + Py_INCREF(key); + if (unlikely(__Pyx_PyTuple_SET_ITEM(kwnames, i, key) == -1)) { + return -1; + } + ++i; + } + return 0; +} + +/* CallSlotAsVectorcall */ +#if CYTHON_VECTORCALL_TPNEW +static PyObject * __Pyx_CallTpnewAsVectorcall(__Pyx_tpnewvectorcallfunc f, PyTypeObject* o, PyObject *a, PyObject *k) { + Py_ssize_t k_size = k ? __Pyx_PyDict_GET_SIZE(k) : 0; +#if !CYTHON_ASSUME_SAFE_SIZE + if (unlikely(k_size < 0)) return NULL; +#endif + Py_ssize_t a_size = __Pyx_PyTuple_GET_SIZE(a); +#if !CYTHON_ASSUME_SAFE_SIZE + if (unlikely(a_size < 0)) return NULL; +#endif +#if CYTHON_ASSUME_SAFE_MACROS + if (k_size == 0) { + return f(o, &PyTuple_GET_ITEM(a, 0), a_size, NULL); + } +#else + if (k_size == 0 && a_size == 0) { + return f(o, NULL, 0, NULL); + } +#endif + PyObject *stack_args[5]; + PyObject **args = stack_args; + Py_ssize_t maxnargs = PY_SSIZE_T_MAX / sizeof(PyObject*) - 1; + if (a_size > maxnargs - k_size) { + PyErr_NoMemory(); + return NULL; + } + Py_ssize_t total_size = a_size + k_size; + if (total_size > 5) { + args = (PyObject**)PyMem_Malloc(((size_t)total_size)*sizeof(PyObject*)); + if (unlikely(!args)) { + PyErr_NoMemory(); + return NULL; + } + } + PyObject * result = NULL; + PyObject *kwnames = NULL; + int unpack_dict_result; + Py_ssize_t i = 0; + for (; i < a_size; ++i) { + args[i] = __Pyx_PyTuple_GET_ITEM(a, i); +#if !CYTHON_ASSUME_SAFE_MACROS + if (unlikely(!args[i])) goto cleanup; +#endif + } +#if !CYTHON_ASSUME_SAFE_MACROS + if (k) // There's a specific shortcut earlier for ASSUME_SAFE_MACROS with no keywords +#endif + { + kwnames = PyTuple_New(k_size); + if (unlikely(!kwnames)) goto cleanup; + __Pyx_BEGIN_CRITICAL_SECTION(k); + unpack_dict_result = __Pyx_CallSlotAsVectorcallUnpackDict(a_size, k, args, kwnames); + __Pyx_END_CRITICAL_SECTION(); + if (unlikely(unpack_dict_result == -1)) goto cleanup; + } + result = f(o, args, a_size, kwnames); + cleanup: + Py_XDECREF(kwnames); + for (i=a_size; i__pyx_empty_tuple); + } + result = tp_new(t, args_tuple, kwds_dict); + if (unlikely(!result)) { + goto end; + } + if (PyObject_TypeCheck(result, t)) { + initproc tp_init = __Pyx_PyObject_GetSlot(result, tp_init, initproc); + if (tp_init && unlikely(tp_init(result, args_tuple, kwds_dict) < 0)) { + Py_CLEAR(result); + } + } + end: + Py_XDECREF(args_tuple); + Py_XDECREF(kwds_dict); + return result; +} +#endif + +/* PyObjectCallMethod0 (used by PyType_Ready) */ +static CYTHON_INLINE PyObject* __Pyx_PyObject_CallMethod0(PyObject* obj, PyObject* method_name) { +#if CYTHON_VECTORCALL && (__PYX_LIMITED_VERSION_HEX >= 0x030C0000 || !CYTHON_COMPILING_IN_LIMITED_API) + return PyObject_VectorcallMethod(method_name, &obj, 1 | PY_VECTORCALL_ARGUMENTS_OFFSET, NULL); +#elif CYTHON_COMPILING_IN_LIMITED_API + return PyObject_CallMethodObjArgs(obj, method_name, NULL); +#else + return PyObject_CallMethodNoArgs(obj, method_name); +#endif +} + +/* GetTypeDictOffset (used by ValidateBasesTuple) */ +#if !CYTHON_USE_TYPE_SLOTS +CYTHON_UNUSED static Py_ssize_t __Pyx_GetTypeDictOffset(PyObject *tp, int require_cython_valid_result) { + PyObject *tp_dictoffset_o; + Py_ssize_t tp_dictoffset; + tp_dictoffset_o = PyObject_GetAttrString(tp, "__dictoffset__"); + if (unlikely(!tp_dictoffset_o)) return -1; + tp_dictoffset = PyLong_AsSsize_t(tp_dictoffset_o); + Py_DECREF(tp_dictoffset_o); + if (unlikely(require_cython_valid_result && tp_dictoffset == 0)) { + PyErr_SetString( + PyExc_TypeError, + "'type' doesn't have a dictoffset"); + return -1; + } else if (unlikely(require_cython_valid_result && tp_dictoffset < 0)) { + PyErr_SetString( + PyExc_TypeError, + "'type' has an unexpected negative dictoffset. " + "Please report this as Cython bug"); + return -1; + } + return tp_dictoffset; +} +#endif + +/* RaiseErrorWithObjectType1 (used by ValidateBasesTuple) */ +static void __Pyx_RaiseErrorWithType1(PyObject* exc_type, const char* message, const char *arg, PyTypeObject *type_obj) { + __Pyx_TypeName type_name = __Pyx_PyType_GetFullyQualifiedName(type_obj); + #if CYTHON_COMPILING_IN_LIMITED_API && __PYX_LIMITED_VERSION_HEX < 0x030d0000 + if (unlikely(!type_name)) return; + #endif + PyErr_Format(exc_type, message, arg, type_name); + __Pyx_DECREF_TypeName(type_name); +} + +/* ValidateBasesTuple (used by PyType_Ready) */ +#if CYTHON_COMPILING_IN_CPYTHON || CYTHON_COMPILING_IN_LIMITED_API || CYTHON_USE_TYPE_SPECS +static int __Pyx_validate_bases_tuple(const char *type_name, int has_dictoffset, PyObject *bases) { + Py_ssize_t i, n; +#if CYTHON_ASSUME_SAFE_SIZE + n = PyTuple_GET_SIZE(bases); +#else + n = PyTuple_Size(bases); + if (unlikely(n < 0)) return -1; +#endif + if (!has_dictoffset) { + PyObject *b; +#if CYTHON_AVOID_BORROWED_REFS + b = PySequence_GetItem(bases, 0); + if (!b) return -1; +#elif CYTHON_ASSUME_SAFE_MACROS + b = PyTuple_GET_ITEM(bases, 0); +#else + b = PyTuple_GetItem(bases, 0); + if (!b) return -1; +#endif +#if CYTHON_USE_TYPE_SLOTS + has_dictoffset = ((PyTypeObject*)b)->tp_dictoffset != 0; +#else + Py_ssize_t dictoffset = __Pyx_GetTypeDictOffset(b, 0); + has_dictoffset = dictoffset != 0; +#endif +#if CYTHON_AVOID_BORROWED_REFS + Py_DECREF(b); +#endif +#if !CYTHON_USE_TYPE_SLOTS + if (dictoffset == -1 && PyErr_Occurred()) return -1; +#endif + } + for (i = 1; i < n; i++) + { + PyTypeObject *b; +#if CYTHON_AVOID_BORROWED_REFS + PyObject *b0 = PySequence_GetItem(bases, i); + if (!b0) return -1; +#elif CYTHON_ASSUME_SAFE_MACROS + PyObject *b0 = PyTuple_GET_ITEM(bases, i); +#else + PyObject *b0 = PyTuple_GetItem(bases, i); + if (!b0) return -1; +#endif + b = (PyTypeObject*) b0; + if (!__Pyx_PyType_HasFeature(b, Py_TPFLAGS_HEAPTYPE)) + { + __Pyx_RaiseErrorWithType( + PyExc_TypeError, "base class '" __Pyx_FMT_TYPENAME "' is not a heap type", b); +#if CYTHON_AVOID_BORROWED_REFS + Py_DECREF(b0); +#endif + return -1; + } + if (!has_dictoffset) + { + Py_ssize_t b_dictoffset = 0; +#if CYTHON_USE_TYPE_SLOTS + b_dictoffset = b->tp_dictoffset; +#else + b_dictoffset = __Pyx_GetTypeDictOffset((PyObject*)b, 0); + if (b_dictoffset == -1 && PyErr_Occurred()) goto dictoffset_return; +#endif + if (b_dictoffset) { + __Pyx_RaiseErrorWithType1( + PyExc_TypeError, + "extension type '%.200s' has no __dict__ slot, " + "but base type '" __Pyx_FMT_TYPENAME "' has: " + "either add 'cdef dict __dict__' to the extension type " + "or add '__slots__ = [...]' to the base type", + type_name, b); +#if !CYTHON_USE_TYPE_SLOTS + dictoffset_return: +#endif +#if CYTHON_AVOID_BORROWED_REFS + Py_DECREF(b0); +#endif + return -1; + } + } +#if CYTHON_AVOID_BORROWED_REFS + Py_DECREF(b0); +#endif + } + return 0; +} +#endif + +/* PyType_Ready */ +CYTHON_UNUSED static int __Pyx_PyType_HasMultipleInheritance(PyTypeObject *t) { + while (t) { + PyObject *bases = __Pyx_PyType_GetSlot(t, tp_bases, PyObject*); + if (bases) { + return 1; + } + t = __Pyx_PyType_GetSlot(t, tp_base, PyTypeObject*); + } + return 0; +} +static int __Pyx_PyType_Ready(PyTypeObject *t) { +#if CYTHON_USE_TYPE_SPECS || !CYTHON_COMPILING_IN_CPYTHON + (void)__Pyx_PyObject_CallMethod0; +#if CYTHON_USE_TYPE_SPECS + (void)__Pyx_validate_bases_tuple; +#endif + return PyType_Ready(t); +#else + int r; + if (!__Pyx_PyType_HasMultipleInheritance(t)) { + return PyType_Ready(t); + } + PyObject *bases = __Pyx_PyType_GetSlot(t, tp_bases, PyObject*); + if (bases && unlikely(__Pyx_validate_bases_tuple(t->tp_name, t->tp_dictoffset != 0, bases) == -1)) + return -1; + { + int gc_was_enabled; + #if PY_VERSION_HEX >= 0x030A00b1 + gc_was_enabled = PyGC_Disable(); + (void)__Pyx_PyObject_CallMethod0; + #else + PyObject *ret, *py_status; + PyObject *gc = NULL; + #if (!CYTHON_COMPILING_IN_PYPY || PYPY_VERSION_NUM+0 >= 0x07030400) &&\ + !CYTHON_COMPILING_IN_GRAAL + gc = PyImport_GetModule(__pyx_mstate_global->__pyx_kp_u_gc); + #endif + if (unlikely(!gc)) gc = PyImport_Import(__pyx_mstate_global->__pyx_kp_u_gc); + if (unlikely(!gc)) return -1; + py_status = __Pyx_PyObject_CallMethod0(gc, __pyx_mstate_global->__pyx_kp_u_isenabled); + if (unlikely(!py_status)) { + Py_DECREF(gc); + return -1; + } + gc_was_enabled = __Pyx_PyObject_IsTrue(py_status); + Py_DECREF(py_status); + if (gc_was_enabled > 0) { + ret = __Pyx_PyObject_CallMethod0(gc, __pyx_mstate_global->__pyx_kp_u_disable); + if (unlikely(!ret)) { + Py_DECREF(gc); + return -1; + } + Py_DECREF(ret); + } else if (unlikely(gc_was_enabled == -1)) { + Py_DECREF(gc); + return -1; + } + #endif + t->tp_flags |= Py_TPFLAGS_HEAPTYPE; +#if PY_VERSION_HEX >= 0x030A0000 + t->tp_flags |= Py_TPFLAGS_IMMUTABLETYPE; +#endif + r = PyType_Ready(t); + t->tp_flags &= ~Py_TPFLAGS_HEAPTYPE; + #if PY_VERSION_HEX >= 0x030A00b1 + if (gc_was_enabled) + PyGC_Enable(); + #else + if (gc_was_enabled) { + PyObject *tp, *v, *tb; + PyErr_Fetch(&tp, &v, &tb); + ret = __Pyx_PyObject_CallMethod0(gc, __pyx_mstate_global->__pyx_kp_u_enable); + if (likely(ret || r == -1)) { + Py_XDECREF(ret); + PyErr_Restore(tp, v, tb); + } else { + Py_XDECREF(tp); + Py_XDECREF(v); + Py_XDECREF(tb); + r = -1; + } + } + Py_DECREF(gc); + #endif + } + return r; +#endif +} + +/* HasAttr (used by ImportImpl) */ +#if __PYX_LIMITED_VERSION_HEX < 0x030d0000 +static CYTHON_INLINE int __Pyx_HasAttr(PyObject *o, PyObject *n) { + PyObject *r; + if (unlikely(!PyUnicode_Check(n))) { + PyErr_SetString(PyExc_TypeError, + "hasattr(): attribute name must be string"); + return -1; + } + r = __Pyx_PyObject_GetAttrStrNoError(o, n); + if (!r) { + return (unlikely(PyErr_Occurred())) ? -1 : 0; + } else { + Py_DECREF(r); + return 1; + } +} +#endif + +/* TupleOrListFromArrayImpl (used by ListFromArray) */ +CYTHON_UNUSED static PyObject * +__Pyx_PyList_FromArray(PyObject *const *src, Py_ssize_t n) { + PyObject *res = PyList_New(n); + if (unlikely(res == NULL)) return NULL; + #if CYTHON_COMPILING_IN_CPYTHON + __Pyx_copy_object_array(src, ((PyListObject*)res)->ob_item, n); + #else + Py_ssize_t i; + for (i = 0; i < n; i++) { + Py_INCREF(src[i]); + if (unlikely(__Pyx_PyList_SET_ITEM(res, i, src[i]) < (0))) { + Py_DECREF(res); + return NULL; + } + } + #endif + return res; +} + +/* ImportImpl (used by Import) */ +static int __Pyx__Import_GetModule(PyObject *qualname, PyObject **module) { + PyObject *imported_module = PyImport_GetModule(qualname); + if (unlikely(!imported_module)) { + *module = NULL; + if (PyErr_Occurred()) { + return -1; + } + return 0; + } + *module = imported_module; + return 1; +} +static int __Pyx__Import_Lookup(PyObject *qualname, PyObject *const *imported_names, Py_ssize_t len_imported_names, PyObject **module) { + PyObject *imported_module; + PyObject *top_level_package_name; + Py_ssize_t i; + int status, module_found; + Py_ssize_t dot_index; + module_found = __Pyx__Import_GetModule(qualname, &imported_module); + if (unlikely(!module_found || module_found == -1)) { + *module = NULL; + return module_found; + } + if (imported_names) { + for (i = 0; i < len_imported_names; i++) { + PyObject *imported_name = imported_names[i]; +#if __PYX_LIMITED_VERSION_HEX < 0x030d0000 + int has_imported_attribute = PyObject_HasAttr(imported_module, imported_name); +#else + int has_imported_attribute = PyObject_HasAttrWithError(imported_module, imported_name); + if (unlikely(has_imported_attribute == -1)) goto error; +#endif + if (!has_imported_attribute) { + goto not_found; + } + } + *module = imported_module; + return 1; + } + dot_index = PyUnicode_FindChar(qualname, '.', 0, PY_SSIZE_T_MAX, 1); + if (dot_index == -1) { + *module = imported_module; + return 1; + } + if (unlikely(dot_index == -2)) goto error; + top_level_package_name = PyUnicode_Substring(qualname, 0, dot_index); + if (unlikely(!top_level_package_name)) goto error; + Py_DECREF(imported_module); + status = __Pyx__Import_GetModule(top_level_package_name, module); + Py_DECREF(top_level_package_name); + return status; +error: + Py_DECREF(imported_module); + *module = NULL; + return -1; +not_found: + Py_DECREF(imported_module); + *module = NULL; + return 0; +} +static PyObject *__Pyx__Import(PyObject *name, PyObject *const *imported_names, Py_ssize_t len_imported_names, PyObject *qualname, PyObject *moddict, int level) { + PyObject *module = 0; + PyObject *empty_dict = 0; + PyObject *from_list = 0; + int module_found; + if (!qualname) { + qualname = name; + } + module_found = __Pyx__Import_Lookup(qualname, imported_names, len_imported_names, &module); + if (likely(module_found == 1)) { + return module; + } else if (unlikely(module_found == -1)) { + return NULL; + } + #if !CYTHON_COMPILING_IN_LIMITED_API && PY_VERSION_HEX >= 0x030f00a6 + empty_dict = PyFrozenDict_New(NULL); + #else + empty_dict = PyDict_New(); + #endif + if (unlikely(!empty_dict)) + goto bad; + if (imported_names) { + from_list = __Pyx_PyList_FromArray(imported_names, len_imported_names); + if (unlikely(!from_list)) + goto bad; + } + if (level == -1) { + const char* package_sep = strchr(__Pyx_MODULE_NAME, '.'); + if (package_sep != (0)) { + module = PyImport_ImportModuleLevelObject( + name, moddict, empty_dict, from_list, 1); + if (unlikely(!module)) { + if (unlikely(!PyErr_ExceptionMatches(PyExc_ImportError))) + goto bad; + PyErr_Clear(); + } + } + level = 0; + } + if (!module) { + module = PyImport_ImportModuleLevelObject( + name, moddict, empty_dict, from_list, level); + } +bad: + Py_XDECREF(from_list); + Py_XDECREF(empty_dict); + return module; +} + +/* Import */ +static PyObject *__Pyx_Import(PyObject *name, PyObject *const *imported_names, Py_ssize_t len_imported_names, PyObject *qualname, int level) { + return __Pyx__Import(name, imported_names, len_imported_names, qualname, __pyx_mstate_global->__pyx_d, level); +} + +/* ImportFrom */ +static PyObject* __Pyx_ImportFrom(PyObject* module, PyObject* name) { + PyObject* value = __Pyx_PyObject_GetAttrStr(module, name); + if (unlikely(!value) && PyErr_ExceptionMatches(PyExc_AttributeError)) { + const char* module_name_str = 0; + PyObject* module_name = 0; + PyObject* module_dot = 0; + PyObject* full_name = 0; + PyErr_Clear(); + module_name_str = PyModule_GetName(module); + if (unlikely(!module_name_str)) { goto modbad; } + module_name = PyUnicode_FromString(module_name_str); + if (unlikely(!module_name)) { goto modbad; } + module_dot = PyUnicode_Concat(module_name, __pyx_mstate_global->__pyx_kp_u__2); + if (unlikely(!module_dot)) { goto modbad; } + full_name = PyUnicode_Concat(module_dot, name); + if (unlikely(!full_name)) { goto modbad; } + #if (CYTHON_COMPILING_IN_PYPY && PYPY_VERSION_NUM < 0x07030400) ||\ + CYTHON_COMPILING_IN_GRAAL + { + PyObject *modules = PyImport_GetModuleDict(); + if (unlikely(!modules)) + goto modbad; + value = PyObject_GetItem(modules, full_name); + } + #else + value = PyImport_GetModule(full_name); + #endif + modbad: + Py_XDECREF(full_name); + Py_XDECREF(module_dot); + Py_XDECREF(module_name); + } + if (unlikely(!value)) { + PyErr_Format(PyExc_ImportError, "cannot import name %S", name); + } + return value; +} + +/* pybytes_as_double (used by pyunicode_as_double) */ +static double __Pyx_SlowPyString_AsDouble(PyObject *obj) { + PyObject *float_value = PyFloat_FromString(obj); + if (likely(float_value)) { + double value = __Pyx_PyFloat_AS_DOUBLE(float_value); + Py_DECREF(float_value); + return value; + } + return (double)-1; +} +static const char* __Pyx__PyBytes_AsDouble_Copy(const char* start, char* buffer, Py_ssize_t length) { + int last_was_punctuation = 1; + int parse_error_found = 0; + Py_ssize_t i; + for (i=0; i < length; i++) { + char chr = start[i]; + int is_punctuation = (chr == '_') | (chr == '.') | (chr == 'e') | (chr == 'E'); + *buffer = chr; + buffer += (chr != '_'); + parse_error_found |= last_was_punctuation & is_punctuation; + last_was_punctuation = is_punctuation; + } + parse_error_found |= last_was_punctuation; + *buffer = '\0'; + return unlikely(parse_error_found) ? NULL : buffer; +} +static double __Pyx__PyBytes_AsDouble_inf_nan(const char* start, Py_ssize_t length) { + int matches = 1; + char sign = start[0]; + int is_signed = (sign == '+') | (sign == '-'); + start += is_signed; + length -= is_signed; + switch (start[0]) { + #ifdef Py_NAN + case 'n': + case 'N': + if (unlikely(length != 3)) goto parse_failure; + matches &= (start[1] == 'a' || start[1] == 'A'); + matches &= (start[2] == 'n' || start[2] == 'N'); + if (unlikely(!matches)) goto parse_failure; + return (sign == '-') ? -Py_NAN : Py_NAN; + #endif + case 'i': + case 'I': + if (unlikely(length < 3)) goto parse_failure; + matches &= (start[1] == 'n' || start[1] == 'N'); + matches &= (start[2] == 'f' || start[2] == 'F'); + if (likely(length == 3 && matches)) + return (sign == '-') ? -Py_HUGE_VAL : Py_HUGE_VAL; + if (unlikely(length != 8)) goto parse_failure; + matches &= (start[3] == 'i' || start[3] == 'I'); + matches &= (start[4] == 'n' || start[4] == 'N'); + matches &= (start[5] == 'i' || start[5] == 'I'); + matches &= (start[6] == 't' || start[6] == 'T'); + matches &= (start[7] == 'y' || start[7] == 'Y'); + if (unlikely(!matches)) goto parse_failure; + return (sign == '-') ? -Py_HUGE_VAL : Py_HUGE_VAL; + case '.': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': + break; + default: + goto parse_failure; + } + return 0.0; +parse_failure: + return -1.0; +} +static CYTHON_INLINE int __Pyx__PyBytes_AsDouble_IsSpace(char ch) { + return (ch == 0x20) | !((ch < 0x9) | (ch > 0xd)); +} +CYTHON_UNUSED static double __Pyx__PyBytes_AsDouble(PyObject *obj, const char* start, Py_ssize_t length) { + double value; + Py_ssize_t i, digits; + const char *last = start + length; + char *end; + int valid_parse; + while (__Pyx__PyBytes_AsDouble_IsSpace(*start)) + start++; + while (start < last - 1 && __Pyx__PyBytes_AsDouble_IsSpace(last[-1])) + last--; + length = last - start; + if (unlikely(length <= 0)) goto fallback; + value = __Pyx__PyBytes_AsDouble_inf_nan(start, length); + if (value != 0.0) { + if (unlikely(value == -1.0)) goto fallback; + return value; + } + digits = 0; + for (i=0; i < length; digits += start[i++] != '_'); + if (likely(digits == length)) { + value = PyOS_string_to_double(start, &end, NULL); + valid_parse = (end == last); + } else if (digits < 40) { + char number[40]; + last = __Pyx__PyBytes_AsDouble_Copy(start, number, length); + if (unlikely(!last)) goto fallback; + value = PyOS_string_to_double(number, &end, NULL); + valid_parse = (end == last); + } else { + char *number = (char*) PyMem_Malloc(((size_t) digits + 1) * sizeof(char)); + if (unlikely(!number)) goto fallback; + last = __Pyx__PyBytes_AsDouble_Copy(start, number, length); + if (unlikely(!last)) { + PyMem_Free(number); + goto fallback; + } + value = PyOS_string_to_double(number, &end, NULL); + valid_parse = (end == last); + PyMem_Free(number); + } + if (likely(valid_parse) || (value == (double)-1 && PyErr_Occurred())) { + return value; + } +fallback: + return __Pyx_SlowPyString_AsDouble(obj); +} + +/* dict_setdefault (used by FetchCommonType) */ +static CYTHON_INLINE PyObject *__Pyx_PyDict_SetDefault(PyObject *d, PyObject *key, PyObject *default_value) { + PyObject* value; +#if __PYX_LIMITED_VERSION_HEX >= 0x030F0000 || (!CYTHON_COMPILING_IN_LIMITED_API && PY_VERSION_HEX >= 0x030d00A4) + PyDict_SetDefaultRef(d, key, default_value, &value); +#elif CYTHON_COMPILING_IN_LIMITED_API && __PYX_LIMITED_VERSION_HEX >= 0x030C0000 + PyObject *args[] = {d, key, default_value}; + value = PyObject_VectorcallMethod(__pyx_mstate_global->__pyx_n_u_setdefault, args, 3 | PY_VECTORCALL_ARGUMENTS_OFFSET, NULL); +#elif CYTHON_COMPILING_IN_LIMITED_API + value = PyObject_CallMethodObjArgs(d, __pyx_mstate_global->__pyx_n_u_setdefault, key, default_value, NULL); +#else + value = PyDict_SetDefault(d, key, default_value); + if (unlikely(!value)) return NULL; + Py_INCREF(value); +#endif + return value; +} + +/* AddModuleRef (used by FetchSharedCythonModule) */ +#if CYTHON_COMPILING_IN_CPYTHON_FREETHREADING && PY_VERSION_HEX < 0x030F00a3 + static PyObject *__Pyx_PyImport_AddModuleObjectRef(PyObject *name) { + PyObject *module_dict = PyImport_GetModuleDict(); + PyObject *m; + if (PyMapping_GetOptionalItem(module_dict, name, &m) < 0) { + return NULL; + } + if (m != NULL && PyModule_Check(m)) { + return m; + } + Py_XDECREF(m); + m = PyModule_NewObject(name); + if (m == NULL) + return NULL; + if (PyDict_CheckExact(module_dict)) { + PyObject *new_m; + (void)PyDict_SetDefaultRef(module_dict, name, m, &new_m); + Py_DECREF(m); + return new_m; + } else { + if (PyObject_SetItem(module_dict, name, m) != 0) { + Py_DECREF(m); + return NULL; + } + return m; + } + } + static PyObject *__Pyx_PyImport_AddModuleRef(const char *name) { + if ((PY_VERSION_HEX >= 0x030E0000) && Py_Version >= 0x030E0100) { + return PyImport_AddModuleRef(name); + } + PyObject *py_name = PyUnicode_FromString(name); + if (!py_name) return NULL; + PyObject *module = __Pyx_PyImport_AddModuleObjectRef(py_name); + Py_DECREF(py_name); + return module; + } +#elif __PYX_LIMITED_VERSION_HEX < 0x030d0000 + static PyObject *__Pyx_PyImport_AddModuleRef(const char *name) { + PyObject *module = PyImport_AddModule(name); + Py_XINCREF(module); + return module; + } +#endif + +/* FetchSharedCythonModule (used by FetchCommonType) */ +static PyObject *__Pyx_FetchSharedCythonABIModule(void) { + return __Pyx_PyImport_AddModuleRef(__PYX_ABI_MODULE_NAME); +} + +/* VerifyCachedType (used by FetchCommonType) */ +static int __Pyx_VerifyCachedType(PyObject *cached_type, + const char *name, + Py_ssize_t expected_basicsize) { + Py_ssize_t basicsize; + if (!PyType_Check(cached_type)) { + PyErr_Format(PyExc_TypeError, + "Shared Cython type %.200s is not a type object", name); + return -1; + } + if (expected_basicsize == 0) { + return 0; // size is inherited, nothing useful to check + } +#if CYTHON_COMPILING_IN_LIMITED_API && CYTHON_OPAQUE_OBJECTS + if (expected_basicsize < 0) { + basicsize = PyType_GetTypeDataSize((PyTypeObject*)cached_type); + } else +#endif + { + #if CYTHON_COMPILING_IN_LIMITED_API + PyObject *py_basicsize; + py_basicsize = PyObject_GetAttrString(cached_type, "__basicsize__"); + if (unlikely(!py_basicsize)) return -1; + basicsize = PyLong_AsSsize_t(py_basicsize); + Py_DECREF(py_basicsize); + py_basicsize = NULL; + if (unlikely(basicsize == (Py_ssize_t)-1) && PyErr_Occurred()) return -1; + #else + basicsize = ((PyTypeObject*) cached_type)->tp_basicsize; + #endif + } + if ((expected_basicsize >= 0) ? + (basicsize != expected_basicsize) : + (basicsize < -expected_basicsize)) { + PyErr_Format(PyExc_TypeError, + "Shared Cython type %.200s has the wrong size, try recompiling", + name); + return -1; + } + return 0; +} + +/* FetchCommonType (used by CommonTypesMetaclass) */ +#if __PYX_LIMITED_VERSION_HEX < 0x030C0000 +static PyObject* __Pyx_PyType_FromMetaclass(PyTypeObject *metaclass, PyObject *module, PyType_Spec *spec, PyObject *bases) { + PyObject *result = __Pyx_PyType_FromModuleAndSpec(module, spec, bases); + if (result && metaclass) { + PyObject *old_tp = (PyObject*)Py_TYPE(result); + Py_INCREF((PyObject*)metaclass); + Py_SET_TYPE(result, metaclass); + Py_DECREF(old_tp); + PyType_Modified((PyTypeObject*)result); + } + return result; +} +#else +#define __Pyx_PyType_FromMetaclass(me, mo, s, b) PyType_FromMetaclass(me, mo, s, b) +#endif +static PyTypeObject *__Pyx_FetchCommonTypeFromSpec(PyTypeObject *metaclass, PyObject *module, PyType_Spec *spec, PyObject *bases) { + PyObject *abi_module = NULL, *cached_type = NULL, *abi_module_dict, *new_cached_type, *py_object_name; + int get_item_ref_result; + const char* object_name = strrchr(spec->name, '.'); + object_name = object_name ? object_name+1 : spec->name; + py_object_name = PyUnicode_FromString(object_name); + if (!py_object_name) return NULL; + abi_module = __Pyx_FetchSharedCythonABIModule(); + if (!abi_module) goto done; + abi_module_dict = PyModule_GetDict(abi_module); + if (!abi_module_dict) goto done; + get_item_ref_result = __Pyx_PyDict_GetItemRef(abi_module_dict, py_object_name, &cached_type); + if (get_item_ref_result == 1) { + if (__Pyx_VerifyCachedType( + cached_type, + object_name, + spec->basicsize) < 0) { + goto bad; + } + goto done; + } else if (unlikely(get_item_ref_result == -1)) { + goto bad; + } + cached_type = __Pyx_PyType_FromMetaclass( + metaclass, + CYTHON_USE_MODULE_STATE ? module : abi_module, + spec, bases); + if (unlikely(!cached_type)) goto bad; + new_cached_type = __Pyx_PyDict_SetDefault(abi_module_dict, py_object_name, cached_type); + if (unlikely(new_cached_type != cached_type)) { + if (unlikely(!new_cached_type)) goto bad; + Py_DECREF(cached_type); + cached_type = new_cached_type; + if (__Pyx_VerifyCachedType( + cached_type, + object_name, + spec->basicsize) < 0) { + goto bad; + } + goto done; + } else { + Py_DECREF(new_cached_type); + } +done: + Py_XDECREF(abi_module); + Py_DECREF(py_object_name); + assert(cached_type == NULL || PyType_Check(cached_type)); + return (PyTypeObject *) cached_type; +bad: + Py_XDECREF(cached_type); + cached_type = NULL; + goto done; +} + +/* CommonTypesMetaclass (used by CythonFunctionShared) */ +static PyObject* __pyx_CommonTypesMetaclass_get_module(CYTHON_UNUSED PyObject *self, CYTHON_UNUSED void* context) { + return PyUnicode_FromString(__PYX_ABI_MODULE_NAME); +} +#if __PYX_LIMITED_VERSION_HEX < 0x030A0000 +static PyObject* __pyx_CommonTypesMetaclass_call(CYTHON_UNUSED PyObject *self, CYTHON_UNUSED PyObject *args, CYTHON_UNUSED PyObject *kwds) { + PyErr_SetString(PyExc_TypeError, "Cannot instantiate Cython internal types"); + return NULL; +} +static int __pyx_CommonTypesMetaclass_setattr(CYTHON_UNUSED PyObject *self, CYTHON_UNUSED PyObject *attr, CYTHON_UNUSED PyObject *value) { + PyErr_SetString(PyExc_TypeError, "Cython internal types are immutable"); + return -1; +} +#endif +static PyGetSetDef __pyx_CommonTypesMetaclass_getset[] = { + {"__module__", __pyx_CommonTypesMetaclass_get_module, NULL, NULL, NULL}, + {0, 0, 0, 0, 0} +}; +static PyType_Slot __pyx_CommonTypesMetaclass_slots[] = { + {Py_tp_getset, (void *)__pyx_CommonTypesMetaclass_getset}, + #if __PYX_LIMITED_VERSION_HEX < 0x030A0000 + {Py_tp_call, (void*)__pyx_CommonTypesMetaclass_call}, + {Py_tp_new, (void*)__pyx_CommonTypesMetaclass_call}, + {Py_tp_setattro, (void*)__pyx_CommonTypesMetaclass_setattr}, + #endif + {0, 0} +}; +static PyType_Spec __pyx_CommonTypesMetaclass_spec = { + __PYX_TYPE_MODULE_PREFIX "_common_types_metatype", + 0, + 0, + Py_TPFLAGS_IMMUTABLETYPE | + Py_TPFLAGS_DISALLOW_INSTANTIATION | + Py_TPFLAGS_DEFAULT, + __pyx_CommonTypesMetaclass_slots +}; +static int __pyx_CommonTypesMetaclass_init(PyObject *module) { + __pyx_mstatetype *mstate = __Pyx_PyModule_GetState(module); + PyObject *bases = PyTuple_Pack(1, &PyType_Type); + if (unlikely(!bases)) { + return -1; + } + mstate->__pyx_CommonTypesMetaclassType = __Pyx_FetchCommonTypeFromSpec(NULL, module, &__pyx_CommonTypesMetaclass_spec, bases); + Py_DECREF(bases); + if (unlikely(mstate->__pyx_CommonTypesMetaclassType == NULL)) { + return -1; + } + return 0; +} + +/* CythonFunctionPerModule (used by CythonFunctionShared) */ +#if CYTHON_COMPILING_IN_LIMITED_API +static CYTHON_INLINE int __Pyx__IsSameCyOrCFunctionNoMethod(PyObject *func, void (*cfunc)(void)) { + if (__Pyx_CyFunction_Check(func)) { + return __Pyx_as_CyFunctionObject(func)->func_methoddef->ml_meth == (PyCFunction) cfunc; + } else if (PyCFunction_Check(func)) { + return PyCFunction_GetFunction(func) == (PyCFunction) cfunc; + } + return 0; +} +static CYTHON_INLINE int __Pyx__IsSameCyOrCFunction(PyObject *func, void (*cfunc)(void)) { + if ((PyObject*)Py_TYPE(func) == __pyx_mstate_global->__Pyx_CachedMethodType) { + int result; + PyObject *newFunc = PyObject_GetAttr(func, __pyx_mstate_global->__pyx_n_u_func); + if (unlikely(!newFunc)) { + PyErr_Clear(); // It's only an optimization, so don't throw an error + return 0; + } + result = __Pyx__IsSameCyOrCFunctionNoMethod(newFunc, cfunc); + Py_DECREF(newFunc); + return result; + } + return __Pyx__IsSameCyOrCFunctionNoMethod(func, cfunc); +} +#else +static CYTHON_INLINE int __Pyx__IsSameCyOrCFunction(PyObject *func, void (*cfunc)(void)) { + if (PyMethod_Check(func)) { + func = PyMethod_GET_FUNCTION(func); + } + return __Pyx_CyOrPyCFunction_Check(func) && __Pyx_CyOrPyCFunction_GET_FUNCTION(func) == (PyCFunction) cfunc; +} +#endif +static CYTHON_INLINE void __Pyx__CyFunction_SetClassObj(__pyx_CyFunctionObject* f, PyObject* classobj) { +#if CYTHON_COMPILING_IN_LIMITED_API + __Pyx_Py_XDECREF_SET( + __Pyx__CyFunction_GetClassObj(f), + ((classobj) ? __Pyx_NewRef(classobj) : NULL)); +#else + __Pyx_Py_XDECREF_SET( + ((PyCMethodObject *) (f))->mm_class, + (PyTypeObject*)((classobj) ? __Pyx_NewRef(classobj) : NULL)); +#endif +} +static CYTHON_INLINE PyObject *__Pyx_CyFunction_InitDefaults(PyObject *func, PyTypeObject *defaults_type) { + __pyx_CyFunctionObject *m = __Pyx_as_CyFunctionObject(func); + m->defaults = PyObject_CallObject((PyObject*)defaults_type, NULL); // _PyObject_New(defaults_type); + if (unlikely(!m->defaults)) + return NULL; + return m->defaults; +} +static CYTHON_INLINE void __Pyx_CyFunction_SetDefaultsTuple(PyObject *func, PyObject *tuple) { + __pyx_CyFunctionObject *m = __Pyx_as_CyFunctionObject(func); + m->defaults_tuple = tuple; + Py_INCREF(tuple); +} +static CYTHON_INLINE void __Pyx_CyFunction_SetDefaultsKwDict(PyObject *func, PyObject *dict) { + __pyx_CyFunctionObject *m = __Pyx_as_CyFunctionObject(func); + m->defaults_kwdict = dict; + Py_INCREF(dict); +} +static CYTHON_INLINE void __Pyx_CyFunction_SetAnnotationsDict(PyObject *func, PyObject *dict) { + __pyx_CyFunctionObject *m = __Pyx_as_CyFunctionObject(func); + m->func_annotations = dict; + Py_INCREF(dict); +} + +/* CallTypeTraverse (used by CythonFunctionShared) */ +#if !CYTHON_USE_TYPE_SPECS +#else +static int __Pyx_call_type_traverse(PyObject *o, int always_call, visitproc visit, void *arg) { + if (!always_call) { + PyTypeObject *base = __Pyx_PyObject_GetSlot(o, tp_base, PyTypeObject*); + unsigned long flags = PyType_GetFlags(base); + if (flags & Py_TPFLAGS_HEAPTYPE) { + return 0; + } + } + Py_VISIT((PyObject*)Py_TYPE(o)); + return 0; +} +#endif + +/* PyMethodNew (used by CythonFunctionShared) */ +#if CYTHON_COMPILING_IN_LIMITED_API +static PyObject *__Pyx_PyMethod_New(PyObject *func, PyObject *self, PyObject *typ) { + PyObject *result; + CYTHON_UNUSED_VAR(typ); + if (!self) + return __Pyx_NewRef(func); + #if __PYX_LIMITED_VERSION_HEX >= 0x030C0000 + { + PyObject *args[] = {func, self}; + result = PyObject_Vectorcall(__pyx_mstate_global->__Pyx_CachedMethodType, args, 2, NULL); + } + #else + result = PyObject_CallFunctionObjArgs(__pyx_mstate_global->__Pyx_CachedMethodType, func, self, NULL); + #endif + return result; +} +#else +static PyObject *__Pyx_PyMethod_New(PyObject *func, PyObject *self, PyObject *typ) { + CYTHON_UNUSED_VAR(typ); + if (!self) + return __Pyx_NewRef(func); + return PyMethod_New(func, self); +} +#endif + +/* PyVectorcallFastCallDict (used by CythonFunctionShared) */ +#if CYTHON_VECTORCALL +static PyObject *__Pyx_PyVectorcall_FastCallDict_kw(PyObject *func, __pyx_vectorcallfunc vc, PyObject *const *args, size_t nargs, PyObject *kw) +{ + PyObject *res = NULL; + PyObject *kwnames; + PyObject **newargs; + PyObject **kwvalues; + Py_ssize_t i; + #if CYTHON_AVOID_BORROWED_REFS + PyObject *pos; + #else + Py_ssize_t pos; + #endif + size_t j; + PyObject *key, *value; + unsigned long keys_are_strings; + #if !CYTHON_ASSUME_SAFE_SIZE + Py_ssize_t nkw = PyDict_Size(kw); + if (unlikely(nkw == -1)) return NULL; + #else + Py_ssize_t nkw = PyDict_GET_SIZE(kw); + #endif + newargs = (PyObject **)PyMem_Malloc((nargs + (size_t)nkw) * sizeof(args[0])); + if (unlikely(newargs == NULL)) { + PyErr_NoMemory(); + return NULL; + } + for (j = 0; j < nargs; j++) newargs[j] = args[j]; + kwnames = PyTuple_New(nkw); + if (unlikely(kwnames == NULL)) { + PyMem_Free(newargs); + return NULL; + } + kwvalues = newargs + nargs; + pos = 0; + i = 0; + keys_are_strings = Py_TPFLAGS_UNICODE_SUBCLASS; + while (__Pyx_PyDict_NextRef(kw, &pos, &key, &value)) { + keys_are_strings &= + #if CYTHON_COMPILING_IN_LIMITED_API + PyType_GetFlags(Py_TYPE(key)); + #else + Py_TYPE(key)->tp_flags; + #endif + #if !CYTHON_ASSUME_SAFE_MACROS + if (unlikely(PyTuple_SetItem(kwnames, i, key) < 0)) goto cleanup; + #else + PyTuple_SET_ITEM(kwnames, i, key); + #endif + kwvalues[i] = value; + i++; + } + if (unlikely(!keys_are_strings)) { + PyErr_SetString(PyExc_TypeError, "keywords must be strings"); + goto cleanup; + } + res = vc(func, newargs, nargs, kwnames); +cleanup: + #if CYTHON_AVOID_BORROWED_REFS + Py_DECREF(pos); + #endif + Py_DECREF(kwnames); + for (i = 0; i < nkw; i++) + Py_DECREF(kwvalues[i]); + PyMem_Free(newargs); + return res; +} +static CYTHON_INLINE PyObject *__Pyx_PyVectorcall_FastCallDict(PyObject *func, __pyx_vectorcallfunc vc, PyObject *const *args, size_t nargs, PyObject *kw) +{ + Py_ssize_t kw_size = + likely(kw == NULL) ? + 0 : +#if !CYTHON_ASSUME_SAFE_SIZE + PyDict_Size(kw); +#else + PyDict_GET_SIZE(kw); +#endif + if (kw_size == 0) { + return vc(func, args, nargs, NULL); + } +#if !CYTHON_ASSUME_SAFE_SIZE + else if (unlikely(kw_size == -1)) { + return NULL; + } +#endif + return __Pyx_PyVectorcall_FastCallDict_kw(func, vc, args, nargs, kw); +} +#endif + +/* CythonFunctionShared (used by CythonFunction) */ +static PyObject * +__Pyx_CyFunction_get_doc_locked(__pyx_CyFunctionObject *op) +{ + if (unlikely(op->func_doc == NULL)) { +#if CYTHON_COMPILING_IN_LIMITED_API + const char *doc = op->func_methoddef->ml_doc; +#else + const char *doc = ((PyCFunctionObject*)op)->m_ml->ml_doc; +#endif + if (doc) { + op->func_doc = PyUnicode_FromString(doc); + if (unlikely(op->func_doc == NULL)) + return NULL; + } else { + Py_INCREF(Py_None); + return Py_None; + } + } + Py_INCREF(op->func_doc); + return op->func_doc; +} +static PyObject * +__Pyx_CyFunction_get_doc(PyObject *op_in, void *closure) { + PyObject *result; + __pyx_CyFunctionObject *op = __Pyx_as_CyFunctionObject(op_in); + CYTHON_UNUSED_VAR(closure); + __Pyx_BEGIN_CRITICAL_SECTION(op_in); + result = __Pyx_CyFunction_get_doc_locked(op); + __Pyx_END_CRITICAL_SECTION(); + return result; +} +static int +__Pyx_CyFunction_set_doc(PyObject *op_in, PyObject *value, void *context) +{ + __pyx_CyFunctionObject *op = __Pyx_as_CyFunctionObject(op_in); + CYTHON_UNUSED_VAR(context); + if (value == NULL) { + value = Py_None; + } + Py_INCREF(value); + __Pyx_BEGIN_CRITICAL_SECTION(op_in); + __Pyx_Py_XDECREF_SET(op->func_doc, value); + __Pyx_END_CRITICAL_SECTION(); + return 0; +} +static PyObject * +__Pyx_CyFunction_get_name_locked(__pyx_CyFunctionObject *op) +{ + if (unlikely(op->func_name == NULL)) { +#if CYTHON_COMPILING_IN_LIMITED_API + const char *name = op->func_methoddef->ml_name; +#else + const char *name = ((PyCFunctionObject*)op)->m_ml->ml_name; +#endif + op->func_name = PyUnicode_InternFromString(name); + if (unlikely(op->func_name == NULL)) + return NULL; + } + Py_INCREF(op->func_name); + return op->func_name; +} +static PyObject * +__Pyx_CyFunction_get_name(PyObject *op, void *context) +{ + PyObject *result = NULL; + CYTHON_UNUSED_VAR(context); + __Pyx_BEGIN_CRITICAL_SECTION(op); + result = __Pyx_CyFunction_get_name_locked(__Pyx_as_CyFunctionObject(op)); + __Pyx_END_CRITICAL_SECTION(); + return result; +} +static int +__Pyx_CyFunction_set_name(PyObject *op_in, PyObject *value, void *context) +{ + CYTHON_UNUSED_VAR(context); + if (unlikely(value == NULL || !PyUnicode_Check(value))) { + PyErr_SetString(PyExc_TypeError, + "__name__ must be set to a string object"); + return -1; + } + __pyx_CyFunctionObject *op = __Pyx_as_CyFunctionObject(op_in); + Py_INCREF(value); + __Pyx_BEGIN_CRITICAL_SECTION(op_in); + __Pyx_Py_XDECREF_SET(op->func_name, value); + __Pyx_END_CRITICAL_SECTION(); + return 0; +} +static PyObject * +__Pyx_CyFunction_get_qualname(PyObject *op_in, void *context) +{ + CYTHON_UNUSED_VAR(context); + __pyx_CyFunctionObject *op = __Pyx_as_CyFunctionObject(op_in); + PyObject *result; + __Pyx_BEGIN_CRITICAL_SECTION(op_in); + Py_INCREF(op->func_qualname); + result = op->func_qualname; + __Pyx_END_CRITICAL_SECTION(); + return result; +} +static int +__Pyx_CyFunction_set_qualname(PyObject *op_in, PyObject *value, void *context) +{ + CYTHON_UNUSED_VAR(context); + if (unlikely(value == NULL || !PyUnicode_Check(value))) { + PyErr_SetString(PyExc_TypeError, + "__qualname__ must be set to a string object"); + return -1; + } + __pyx_CyFunctionObject *op = __Pyx_as_CyFunctionObject(op_in); + Py_INCREF(value); + __Pyx_BEGIN_CRITICAL_SECTION(op_in); + __Pyx_Py_XDECREF_SET(op->func_qualname, value); + __Pyx_END_CRITICAL_SECTION(); + return 0; +} +#if CYTHON_COMPILING_IN_LIMITED_API && __PYX_LIMITED_VERSION_HEX < 0x030A0000 +static PyObject * +__Pyx_CyFunction_get_dict(__pyx_CyFunctionObject *op, void *context) +{ + CYTHON_UNUSED_VAR(context); + if (unlikely(op->func_dict == NULL)) { + op->func_dict = PyDict_New(); + if (unlikely(op->func_dict == NULL)) + return NULL; + } + Py_INCREF(op->func_dict); + return op->func_dict; +} +#endif +static PyObject * +__Pyx_CyFunction_get_globals(PyObject *op_in, void *context) +{ + CYTHON_UNUSED_VAR(context); + __pyx_CyFunctionObject *op = __Pyx_as_CyFunctionObject(op_in); + Py_INCREF(op->func_globals); + return op->func_globals; +} +static PyObject * +__Pyx_CyFunction_get_closure(PyObject *op, void *context) +{ + CYTHON_UNUSED_VAR(op); + CYTHON_UNUSED_VAR(context); + Py_INCREF(Py_None); + return Py_None; +} +static PyObject * +__Pyx_CyFunction_get_code(PyObject *op_in, void *context) +{ + __pyx_CyFunctionObject *op = __Pyx_as_CyFunctionObject(op_in); + PyObject* result = (op->func_code) ? op->func_code : Py_None; + CYTHON_UNUSED_VAR(context); + Py_INCREF(result); + return result; +} +static int +__Pyx_CyFunction_init_defaults(__pyx_CyFunctionObject *op, PyObject *op_obj) { + int result = 0; + PyObject *res = op->defaults_getter(op_obj); + if (unlikely(!res)) + return -1; + #if CYTHON_ASSUME_SAFE_MACROS && !CYTHON_AVOID_BORROWED_REFS + op->defaults_tuple = PyTuple_GET_ITEM(res, 0); + Py_INCREF(op->defaults_tuple); + op->defaults_kwdict = PyTuple_GET_ITEM(res, 1); + Py_INCREF(op->defaults_kwdict); + #else + op->defaults_tuple = __Pyx_PySequence_ITEM(res, 0); + if (unlikely(!op->defaults_tuple)) result = -1; + else { + op->defaults_kwdict = __Pyx_PySequence_ITEM(res, 1); + if (unlikely(!op->defaults_kwdict)) result = -1; + } + #endif + Py_DECREF(res); + return result; +} +static int +__Pyx_CyFunction_set_defaults(PyObject *op_in, PyObject* value, void *context) { + __pyx_CyFunctionObject *op = __Pyx_as_CyFunctionObject(op_in); + CYTHON_UNUSED_VAR(context); + if (!value) { + value = Py_None; + } else if (unlikely(value != Py_None && !PyTuple_Check(value))) { + PyErr_SetString(PyExc_TypeError, + "__defaults__ must be set to a tuple object"); + return -1; + } + PyErr_WarnEx(PyExc_RuntimeWarning, "changes to cyfunction.__defaults__ will not " + "currently affect the values used in function calls", 1); + Py_INCREF(value); + __Pyx_BEGIN_CRITICAL_SECTION(op_in); + __Pyx_Py_XDECREF_SET(op->defaults_tuple, value); + __Pyx_END_CRITICAL_SECTION(); + return 0; +} +static PyObject * +__Pyx_CyFunction_get_defaults_locked(PyObject *op_in) { + __pyx_CyFunctionObject *op = __Pyx_as_CyFunctionObject(op_in); + PyObject* result = op->defaults_tuple; + if (unlikely(!result)) { + if (op->defaults_getter) { + if (unlikely(__Pyx_CyFunction_init_defaults(op, op_in) < 0)) return NULL; + result = op->defaults_tuple; + } else { + result = Py_None; + } + } + Py_INCREF(result); + return result; +} +static PyObject * +__Pyx_CyFunction_get_defaults(PyObject *op, void *context) { + PyObject* result = NULL; + CYTHON_UNUSED_VAR(context); + __Pyx_BEGIN_CRITICAL_SECTION(op); + result = __Pyx_CyFunction_get_defaults_locked(op); + __Pyx_END_CRITICAL_SECTION(); + return result; +} +static int +__Pyx_CyFunction_set_kwdefaults(PyObject *op_in, PyObject* value, void *context) { + __pyx_CyFunctionObject *op = __Pyx_as_CyFunctionObject(op_in); + CYTHON_UNUSED_VAR(context); + if (!value) { + value = Py_None; + } else if (unlikely(value != Py_None && !PyDict_Check(value))) { + PyErr_SetString(PyExc_TypeError, + "__kwdefaults__ must be set to a dict object"); + return -1; + } + PyErr_WarnEx(PyExc_RuntimeWarning, "changes to cyfunction.__kwdefaults__ will not " + "currently affect the values used in function calls", 1); + Py_INCREF(value); + __Pyx_BEGIN_CRITICAL_SECTION(op_in); + __Pyx_Py_XDECREF_SET(op->defaults_kwdict, value); + __Pyx_END_CRITICAL_SECTION(); + return 0; +} +static PyObject * +__Pyx_CyFunction_get_kwdefaults_locked(PyObject *op_in) { + __pyx_CyFunctionObject *op = __Pyx_as_CyFunctionObject(op_in); + PyObject* result = op->defaults_kwdict; + if (unlikely(!result)) { + if (op->defaults_getter) { + if (unlikely(__Pyx_CyFunction_init_defaults(op, op_in) < 0)) return NULL; + result = op->defaults_kwdict; + } else { + result = Py_None; + } + } + Py_INCREF(result); + return result; +} +static PyObject * +__Pyx_CyFunction_get_kwdefaults(PyObject *op, void *context) { + PyObject* result; + CYTHON_UNUSED_VAR(context); + __Pyx_BEGIN_CRITICAL_SECTION(op); + result = __Pyx_CyFunction_get_kwdefaults_locked(op); + __Pyx_END_CRITICAL_SECTION(); + return result; +} +static int __Pyx_CyFunction_set_annotate_in_dict_if_exists(PyObject *op_in, PyObject *value); +static int +__Pyx_CyFunction_set_annotations(PyObject *op_in, PyObject* value, void *context) { + CYTHON_UNUSED_VAR(context); + if (!value || value == Py_None) { + value = NULL; + } else if (unlikely(!PyDict_Check(value))) { + PyErr_SetString(PyExc_TypeError, + "__annotations__ must be set to a dict object"); + return -1; + } + __pyx_CyFunctionObject *op = __Pyx_as_CyFunctionObject(op_in); + Py_XINCREF(value); + __Pyx_BEGIN_CRITICAL_SECTION(op_in); + __Pyx_Py_XDECREF_SET(op->func_annotations, value); + __Pyx_END_CRITICAL_SECTION(); + if (unlikely(__Pyx_CyFunction_set_annotate_in_dict_if_exists(op_in, Py_None) < 0)) return -1; + return 0; +} +static int +__Pyx_CyFunction_get_dict_if_exists(PyObject *op_in, PyObject **dict) { + /* Return 1 if the function dict exists, 0 otherwise. This cannot fail: + * _PyObject_GetDictPtr() may clear errors internally, but never reports them. */ +#if CYTHON_COMPILING_IN_PYPY + *dict = PyObject_GenericGetDict(op_in, NULL); +#elif CYTHON_COMPILING_IN_LIMITED_API || PY_VERSION_HEX < 0x030C0000 + *dict = __Pyx_as_CyFunctionObject(op_in)->func_dict; +#else + PyObject **dictptr = _PyObject_GetDictPtr(op_in); + *dict = likely(dictptr) ? *dictptr : NULL; +#endif + return *dict ? 1 : 0; +} +static int +__Pyx_CyFunction_get_annotate_from_dict_if_exists(PyObject *op_in, PyObject **annotate) { + PyObject *dict; + int dict_found; + *annotate = NULL; + dict_found = __Pyx_CyFunction_get_dict_if_exists(op_in, &dict); + if (!dict_found) return 0; + return __Pyx_PyDict_GetItemRef(dict, __pyx_mstate_global->__pyx_n_u_annotate, annotate); +} +static int +__Pyx_CyFunction_set_annotate_in_dict_if_exists(PyObject *op_in, PyObject *value) { + PyObject *dict; + int dict_found; + dict_found = __Pyx_CyFunction_get_dict_if_exists(op_in, &dict); + if (!dict_found) return 0; + if (value) { + return PyDict_SetItem(dict, __pyx_mstate_global->__pyx_n_u_annotate, value); + } else { +#if !CYTHON_COMPILING_IN_LIMITED_API && PY_VERSION_HEX >= 0x030d0000 + return PyDict_Pop(dict, __pyx_mstate_global->__pyx_n_u_annotate, NULL); +#else + int result = PyDict_Contains(dict, __pyx_mstate_global->__pyx_n_u_annotate); + if (result == 1) { + result = PyDict_DelItem(dict, __pyx_mstate_global->__pyx_n_u_annotate); + } + return result; +#endif + } +} +static int +__Pyx_CyFunction_set_annotate_in_dict(PyObject *op_in, PyObject *value) { + PyObject *dict; + int result; +#if CYTHON_COMPILING_IN_LIMITED_API && __PYX_LIMITED_VERSION_HEX < 0x030A0000 + dict = __Pyx_CyFunction_get_dict(__Pyx_as_CyFunctionObject(op_in), NULL); +#else + dict = PyObject_GenericGetDict(op_in, NULL); +#endif + if (unlikely(!dict)) return -1; + result = PyDict_SetItem(dict, __pyx_mstate_global->__pyx_n_u_annotate, value); + Py_DECREF(dict); + return result; +} +static PyObject * +__Pyx_CyFunction_get_annotations_locked(__pyx_CyFunctionObject *op) { + PyObject* result = op->func_annotations; + if (unlikely(!result)) { + result = PyDict_New(); + if (unlikely(!result)) return NULL; + op->func_annotations = result; + } + Py_INCREF(result); + return result; +} +static PyObject * +__Pyx_CyFunction_get_annotations(PyObject *op_in, void *context) { + PyObject *annotate = NULL; + PyObject *result = NULL; + __pyx_CyFunctionObject *op = __Pyx_as_CyFunctionObject(op_in); + CYTHON_UNUSED_VAR(context); + __Pyx_BEGIN_CRITICAL_SECTION(op_in); + result = __Pyx_XNewRef(op->func_annotations); + __Pyx_END_CRITICAL_SECTION(); + if (result) return result; + if (unlikely(__Pyx_CyFunction_get_annotate_from_dict_if_exists(op_in, &annotate) < 0)) return NULL; + if (!annotate || annotate == Py_None) { + Py_XDECREF(annotate); + __Pyx_BEGIN_CRITICAL_SECTION(op_in); + result = __Pyx_CyFunction_get_annotations_locked(op); + __Pyx_END_CRITICAL_SECTION(); + return result; + } + PyObject *format = PyLong_FromLong(1L); // annotationlib.Format.VALUE + if (likely(format)) { + if (!Py_EnterRecursiveCall(" cyfunction __annotations__.__get__")) { + result = __Pyx_PyObject_CallOneArg(annotate, format); + Py_LeaveRecursiveCall(); + Py_DECREF(format); + } + } + Py_DECREF(annotate); + if (unlikely(!result)) return NULL; + if (unlikely(!PyDict_Check(result))) { + PyErr_SetString(PyExc_TypeError, "__annotate__ must return a dict"); + Py_DECREF(result); + return NULL; + } + __Pyx_BEGIN_CRITICAL_SECTION(op_in); + __Pyx_Py_XDECREF_SET(op->func_annotations, __Pyx_NewRef(result)); + __Pyx_END_CRITICAL_SECTION(); + return result; +} +static PyObject *__Pyx_CyFunction_annotate_impl(PyObject *self, PyObject *arg) { + CYTHON_UNUSED_VAR(arg); + if (unlikely(!self)) { + PyErr_SetString(PyExc_SystemError, "cython __annotate__ called without 'self' argument"); + return NULL; + } + PyObject *result; + if (Py_EnterRecursiveCall(" in cyfunction annotate")) { + return NULL; + } + result = __Pyx_CyFunction_get_annotations(self, NULL); + Py_LeaveRecursiveCall(); + return result; +} +static PyObject * +__Pyx_CyFunction_get_annotate(PyObject *op_in, void *context); +static int +__Pyx_CyFunction_set_annotate(PyObject *op_in, PyObject* value, void *context) { + CYTHON_UNUSED_VAR(context); + if (value == NULL) { + PyErr_SetString(PyExc_TypeError, "__annotate__ cannot be deleted"); + return -1; + } + if (unlikely(value != Py_None && !PyCallable_Check(value))) { + PyErr_SetString(PyExc_TypeError, "__annotate__ must be callable or None"); + return -1; + } + if (unlikely(__Pyx_TypeCheck(value, &PyCFunction_Type))) { +#if !CYTHON_ASSUME_SAFE_MACROS && !CYTHON_COMPILING_IN_PYPY + PyCFunction cfunction = PyCFunction_GetFunction(value); + if (unlikely(!cfunction)) return -1; + PyObject *value_self = PyCFunction_GetSelf(value); + if (unlikely(!value_self && PyErr_Occurred())) return -1; +#else + PyCFunction cfunction = PyCFunction_GET_FUNCTION(value); + PyObject *value_self = PyCFunction_GET_SELF(value); +#endif + if (cfunction == __Pyx_CyFunction_annotate_impl && value_self == op_in) { + return __Pyx_CyFunction_set_annotate_in_dict_if_exists(op_in, NULL); + } + } + if (value != Py_None) { + __pyx_CyFunctionObject *op = __Pyx_as_CyFunctionObject(op_in); + __Pyx_BEGIN_CRITICAL_SECTION(op_in); + Py_CLEAR(op->func_annotations); + __Pyx_END_CRITICAL_SECTION(); + } + return __Pyx_CyFunction_set_annotate_in_dict(op_in, value); +} +#if __PYX_LIMITED_VERSION_HEX < 0x030B0000 +static PyObject * +__Pyx_CyFunction_get_is_coroutine_value(__pyx_CyFunctionObject *op) { + int is_coroutine = op->flags & __Pyx_CYFUNCTION_COROUTINE; + if (is_coroutine) { + PyObject *is_coroutine_value, *module, *fromlist, *marker = __pyx_mstate_global->__pyx_n_u_is_coroutine; + fromlist = PyList_New(1); + if (unlikely(!fromlist)) return NULL; + Py_INCREF(marker); +#if CYTHON_ASSUME_SAFE_MACROS + PyList_SET_ITEM(fromlist, 0, marker); +#else + if (unlikely(PyList_SetItem(fromlist, 0, marker) < 0)) { + Py_DECREF(fromlist); + return NULL; + } +#endif + module = PyImport_ImportModuleLevelObject(__pyx_mstate_global->__pyx_n_u_asyncio_coroutines, NULL, NULL, fromlist, 0); + Py_DECREF(fromlist); + if (unlikely(!module)) { + goto ignore_or_error; + } + is_coroutine_value = __Pyx_PyObject_GetAttrStr(module, marker); + Py_DECREF(module); + if (likely(is_coroutine_value)) { + return is_coroutine_value; + } +ignore_or_error: + if (!__Pyx_IgnoreException(PyExc_Exception)) { + return NULL; + } + } + return __Pyx_PyBool_FromLong(is_coroutine); +} +static PyObject * +__Pyx_CyFunction_get_is_coroutine(PyObject *op_in, void *context) { + __pyx_CyFunctionObject *op = __Pyx_as_CyFunctionObject(op_in); + PyObject *result; + CYTHON_UNUSED_VAR(context); + if (op->func_is_coroutine) { + return __Pyx_NewRef(op->func_is_coroutine); + } + result = __Pyx_CyFunction_get_is_coroutine_value(op); + if (unlikely(!result)) + return NULL; + __Pyx_BEGIN_CRITICAL_SECTION(op_in); + if (op->func_is_coroutine) { + Py_DECREF(result); + result = __Pyx_NewRef(op->func_is_coroutine); + } else { + op->func_is_coroutine = __Pyx_NewRef(result); + } + __Pyx_END_CRITICAL_SECTION(); + return result; +} +#endif +static void __Pyx_CyFunction_raise_argument_count_error(PyObject *func, const char* message, Py_ssize_t size) { +#if CYTHON_COMPILING_IN_LIMITED_API + PyObject *py_name = __Pyx_CyFunction_get_name(func, NULL); + if (!py_name) return; + PyErr_Format(PyExc_TypeError, + "%.200S() %s (%" CYTHON_FORMAT_SSIZE_T "d given)", + py_name, message, size); + Py_DECREF(py_name); +#else + const char* name = ((PyCFunctionObject*)func)->m_ml->ml_name; + PyErr_Format(PyExc_TypeError, + "%.200s() %s (%" CYTHON_FORMAT_SSIZE_T "d given)", + name, message, size); +#endif +} +static void __Pyx_CyFunction_raise_type_error(PyObject *func, const char* message) { +#if CYTHON_COMPILING_IN_LIMITED_API + PyObject *py_name = __Pyx_CyFunction_get_name(func, NULL); + if (!py_name) return; + PyErr_Format(PyExc_TypeError, + "%.200S() %s", + py_name, message); + Py_DECREF(py_name); +#else + const char* name = ((PyCFunctionObject*)func)->m_ml->ml_name; + PyErr_Format(PyExc_TypeError, + "%.200s() %s", + name, message); +#endif +} +#if CYTHON_COMPILING_IN_LIMITED_API +static PyObject * +__Pyx_CyFunction_get_module(PyObject *op_in, void *context) { + CYTHON_UNUSED_VAR(context); + __pyx_CyFunctionObject *op = __Pyx_as_CyFunctionObject(op_in); + Py_INCREF(op->func_module); + return op->func_module; +} +static int +__Pyx_CyFunction_set_module(PyObject *op_in, PyObject* value, void *context) { + CYTHON_UNUSED_VAR(context); + __pyx_CyFunctionObject *op = __Pyx_as_CyFunctionObject(op_in); + if (value == NULL) { + value = Py_None; + } + Py_INCREF(value); + PyObject *old = op->func_module; + op->func_module = value; + Py_DECREF(old); + return 0; +} +#endif +static PyGetSetDef __pyx_CyFunction_getsets[] = { + {"func_doc", (getter)__Pyx_CyFunction_get_doc, (setter)__Pyx_CyFunction_set_doc, 0, 0}, + {"__doc__", (getter)__Pyx_CyFunction_get_doc, (setter)__Pyx_CyFunction_set_doc, 0, 0}, + {"func_name", (getter)__Pyx_CyFunction_get_name, (setter)__Pyx_CyFunction_set_name, 0, 0}, + {"__name__", (getter)__Pyx_CyFunction_get_name, (setter)__Pyx_CyFunction_set_name, 0, 0}, + {"__qualname__", (getter)__Pyx_CyFunction_get_qualname, (setter)__Pyx_CyFunction_set_qualname, 0, 0}, +#if CYTHON_COMPILING_IN_LIMITED_API && __PYX_LIMITED_VERSION_HEX < 0x030A0000 + {"func_dict", (getter)__Pyx_CyFunction_get_dict, (setter)PyObject_GenericSetDict, 0, 0}, + {"__dict__", (getter)__Pyx_CyFunction_get_dict, (setter)PyObject_GenericSetDict, 0, 0}, +#else + {"func_dict", (getter)PyObject_GenericGetDict, (setter)PyObject_GenericSetDict, 0, 0}, + {"__dict__", (getter)PyObject_GenericGetDict, (setter)PyObject_GenericSetDict, 0, 0}, +#endif + {"func_globals", (getter)__Pyx_CyFunction_get_globals, 0, 0, 0}, + {"__globals__", (getter)__Pyx_CyFunction_get_globals, 0, 0, 0}, + {"func_closure", (getter)__Pyx_CyFunction_get_closure, 0, 0, 0}, + {"__closure__", (getter)__Pyx_CyFunction_get_closure, 0, 0, 0}, + {"func_code", (getter)__Pyx_CyFunction_get_code, 0, 0, 0}, + {"__code__", (getter)__Pyx_CyFunction_get_code, 0, 0, 0}, + {"func_defaults", (getter)__Pyx_CyFunction_get_defaults, (setter)__Pyx_CyFunction_set_defaults, 0, 0}, + {"__defaults__", (getter)__Pyx_CyFunction_get_defaults, (setter)__Pyx_CyFunction_set_defaults, 0, 0}, + {"__kwdefaults__", (getter)__Pyx_CyFunction_get_kwdefaults, (setter)__Pyx_CyFunction_set_kwdefaults, 0, 0}, + {"__annotations__", (getter)__Pyx_CyFunction_get_annotations, (setter)__Pyx_CyFunction_set_annotations, 0, 0}, + {"__annotate__", (getter)__Pyx_CyFunction_get_annotate, (setter)__Pyx_CyFunction_set_annotate, 0, 0}, +#if __PYX_LIMITED_VERSION_HEX < 0x030B0000 + {"_is_coroutine", (getter)__Pyx_CyFunction_get_is_coroutine, 0, 0, 0}, +#endif +#if CYTHON_COMPILING_IN_LIMITED_API + {"__module__", (getter)__Pyx_CyFunction_get_module, (setter)__Pyx_CyFunction_set_module, 0, 0}, +#endif + {0, 0, 0, 0, 0} +}; +static PyMemberDef __pyx_CyFunction_members[] = { +#if !CYTHON_COMPILING_IN_LIMITED_API + {"__module__", T_OBJECT, offsetof(PyCFunctionObject, m_module), 0, 0}, +#endif +#if PY_VERSION_HEX < 0x030C0000 || CYTHON_COMPILING_IN_LIMITED_API + {"__dictoffset__", T_PYSSIZET, offsetof(__pyx_CyFunctionObject, func_dict), + __PYX_SHARED_RELATIVE_OFFSET | READONLY, 0}, +#endif +#if CYTHON_VECTORCALL +#if CYTHON_COMPILING_IN_LIMITED_API || CYTHON_COMPILING_IN_PYPY + {"__vectorcalloffset__", T_PYSSIZET, offsetof(__pyx_CyFunctionObject, func_vectorcall), + __PYX_SHARED_RELATIVE_OFFSET | READONLY, 0}, +#else + {"__vectorcalloffset__", T_PYSSIZET, offsetof(PyCFunctionObject, vectorcall), READONLY, 0}, +#endif +#if CYTHON_COMPILING_IN_LIMITED_API + {"__weaklistoffset__", T_PYSSIZET, offsetof(__pyx_CyFunctionObject, func_weakreflist), + __PYX_SHARED_RELATIVE_OFFSET | READONLY, 0}, +#else + {"__weaklistoffset__", T_PYSSIZET, offsetof(PyCFunctionObject, m_weakreflist), READONLY, 0}, +#endif +#endif + {0, 0, 0, 0, 0} +}; +static PyObject * +__Pyx_CyFunction_reduce(PyObject *m_in, PyObject *args) +{ + PyObject *result = NULL; + __pyx_CyFunctionObject *m = __Pyx_as_CyFunctionObject(m_in); + CYTHON_UNUSED_VAR(args); + __Pyx_BEGIN_CRITICAL_SECTION(m_in); + Py_INCREF(m->func_qualname); + result = m->func_qualname; + __Pyx_END_CRITICAL_SECTION(); + return result; +} +static PyMethodDef __pyx_CyFunction_methods[] = { + {"__annotate_cython__", __Pyx_CyFunction_annotate_impl, METH_O, + "Placeholder __annotate__ function to allow 'functools.wraps' to work " + "on Cython functions."}, + {"__reduce__", (PyCFunction)__Pyx_CyFunction_reduce, METH_NOARGS, 0}, + {0, 0, 0, 0} +}; +static PyObject * +__Pyx_CyFunction_get_annotate(PyObject *op_in, void *context) { + PyObject *annotate = NULL; + CYTHON_UNUSED_VAR(context); + if (unlikely(__Pyx_CyFunction_get_annotate_from_dict_if_exists(op_in, &annotate) < 0)) return NULL; + if (annotate) return annotate; + PyObject *method = PyCFunction_New( + &__pyx_CyFunction_methods[0], + op_in); + return method; +} +#if CYTHON_COMPILING_IN_LIMITED_API +#define __Pyx_CyFunction_weakreflist(cyfunc) ((cyfunc)->func_weakreflist) +#else +#define __Pyx_CyFunction_weakreflist(cyfunc) (((PyCFunctionObject*)cyfunc)->m_weakreflist) +#endif +static PyObject *__Pyx_CyFunction_Init(PyObject *op_in, + PyMethodDef *ml, int flags, PyObject* qualname, + PyObject *closure, PyObject *module, PyObject* globals, PyObject* code) { + __pyx_CyFunctionObject* op = __Pyx_as_CyFunctionObject(op_in); +#if !CYTHON_COMPILING_IN_LIMITED_API + PyCFunctionObject *cf = (PyCFunctionObject*) op; +#endif + if (unlikely(op == NULL)) + return NULL; +#if CYTHON_COMPILING_IN_LIMITED_API + op->func_methoddef = ml; + Py_INCREF(module); + op->func_module = module; +#endif + op->flags = flags; + __Pyx_CyFunction_weakreflist(op) = NULL; +#if !CYTHON_COMPILING_IN_LIMITED_API + cf->m_ml = ml; + cf->m_self = (PyObject *) op; +#endif + Py_XINCREF(closure); + op->func_closure = closure; +#if !CYTHON_COMPILING_IN_LIMITED_API + Py_XINCREF(module); + cf->m_module = module; +#endif +#if PY_VERSION_HEX < 0x030C0000 || CYTHON_COMPILING_IN_LIMITED_API + op->func_dict = NULL; +#endif + op->func_name = NULL; + Py_INCREF(qualname); + op->func_qualname = qualname; + op->func_doc = NULL; +#if CYTHON_COMPILING_IN_LIMITED_API + op->func_classobj = NULL; +#else + ((PyCMethodObject*)op)->mm_class = NULL; +#endif + op->func_globals = globals; + Py_INCREF(op->func_globals); + Py_XINCREF(code); + op->func_code = code; + op->defaults = NULL; + op->defaults_tuple = NULL; + op->defaults_kwdict = NULL; + op->defaults_getter = NULL; + op->func_annotations = NULL; +#if __PYX_LIMITED_VERSION_HEX < 0x030B0000 + op->func_is_coroutine = NULL; +#endif +#if CYTHON_VECTORCALL + switch (ml->ml_flags & (METH_VARARGS | METH_FASTCALL | METH_NOARGS | METH_O | METH_KEYWORDS | METH_METHOD)) { + case METH_NOARGS: + __Pyx_CyFunction_func_vectorcall(op) = __Pyx_CyFunction_Vectorcall_NOARGS; + break; + case METH_O: + __Pyx_CyFunction_func_vectorcall(op) = __Pyx_CyFunction_Vectorcall_O; + break; + case METH_METHOD | METH_FASTCALL | METH_KEYWORDS: + __Pyx_CyFunction_func_vectorcall(op) = __Pyx_CyFunction_Vectorcall_FASTCALL_KEYWORDS_METHOD; + break; + case METH_FASTCALL | METH_KEYWORDS: + __Pyx_CyFunction_func_vectorcall(op) = __Pyx_CyFunction_Vectorcall_FASTCALL_KEYWORDS; + break; + case METH_VARARGS | METH_KEYWORDS: + __Pyx_CyFunction_func_vectorcall(op) = NULL; + break; + default: + PyErr_SetString(PyExc_SystemError, "Bad call flags for CyFunction"); + Py_DECREF(op_in); + return NULL; + } +#endif + return op_in; +} +static int __Pyx__CyFunction_clear(__pyx_CyFunctionObject *m) +{ + Py_CLEAR(m->func_closure); +#if CYTHON_COMPILING_IN_LIMITED_API + Py_CLEAR(m->func_module); +#else + Py_CLEAR(((PyCFunctionObject*)m)->m_module); +#endif +#if PY_VERSION_HEX < 0x030C0000 || CYTHON_COMPILING_IN_LIMITED_API + Py_CLEAR(m->func_dict); +#elif PY_VERSION_HEX < 0x030d0000 + _PyObject_ClearManagedDict((PyObject*)m); +#else + PyObject_ClearManagedDict((PyObject*)m); +#endif + Py_CLEAR(m->func_name); + Py_CLEAR(m->func_qualname); + Py_CLEAR(m->func_doc); + Py_CLEAR(m->func_globals); + Py_CLEAR(m->func_code); +#if CYTHON_COMPILING_IN_LIMITED_API + Py_CLEAR(m->func_classobj); +#else + { + PyObject *cls = (PyObject*) ((PyCMethodObject *) (m))->mm_class; + ((PyCMethodObject *) (m))->mm_class = NULL; + Py_XDECREF(cls); + } +#endif + Py_CLEAR(m->defaults_tuple); + Py_CLEAR(m->defaults_kwdict); + Py_CLEAR(m->func_annotations); +#if __PYX_LIMITED_VERSION_HEX < 0x030B0000 + Py_CLEAR(m->func_is_coroutine); +#endif + Py_CLEAR(m->defaults); + return 0; +} +static int +__Pyx_CyFunction_clear(PyObject *m) +{ + return __Pyx__CyFunction_clear(__Pyx_as_CyFunctionObject(m)); +} +static void __Pyx__CyFunction_dealloc(PyObject *m) +{ + __pyx_CyFunctionObject *cyfunc = __Pyx_as_CyFunctionObject(m); + if (__Pyx_CyFunction_weakreflist(cyfunc) != NULL) + PyObject_ClearWeakRefs(m); + __Pyx__CyFunction_clear(cyfunc); + __Pyx_PyHeapTypeObject_GC_Del(m); +} +static void __Pyx_CyFunction_dealloc(PyObject *m) +{ + PyObject_GC_UnTrack(m); + __Pyx__CyFunction_dealloc(m); +} +static int __Pyx_CyFunction_traverse(PyObject *m_in, visitproc visit, void *arg) +{ + __pyx_CyFunctionObject *m = __Pyx_as_CyFunctionObject(m_in); + { + int e = __Pyx_call_type_traverse(m_in, 1, visit, arg); + if (e) return e; + } + Py_VISIT(m->func_closure); +#if CYTHON_COMPILING_IN_LIMITED_API + Py_VISIT(m->func_module); +#else + Py_VISIT(((PyCFunctionObject*)m)->m_module); +#endif +#if PY_VERSION_HEX < 0x030C0000 || CYTHON_COMPILING_IN_LIMITED_API + Py_VISIT(m->func_dict); +#else + { + int e = +#if PY_VERSION_HEX < 0x030d0000 + _PyObject_VisitManagedDict +#else + PyObject_VisitManagedDict +#endif + ((PyObject*)m, visit, arg); + if (e != 0) return e; + } +#endif + __Pyx_VISIT_CONST(m->func_name); + __Pyx_VISIT_CONST(m->func_qualname); + Py_VISIT(m->func_doc); + Py_VISIT(m->func_globals); + __Pyx_VISIT_CONST(m->func_code); + Py_VISIT(__Pyx__CyFunction_GetClassObj(m)); + Py_VISIT(m->defaults_tuple); + Py_VISIT(m->defaults_kwdict); + Py_VISIT(m->func_annotations); +#if __PYX_LIMITED_VERSION_HEX < 0x030B0000 + Py_VISIT(m->func_is_coroutine); +#endif + Py_VISIT(m->defaults); + return 0; +} +static PyObject* +__Pyx_CyFunction_repr(PyObject *op_in) +{ + __pyx_CyFunctionObject *op = __Pyx_as_CyFunctionObject(op_in); + PyObject *repr; + __Pyx_BEGIN_CRITICAL_SECTION(op_in); + repr = PyUnicode_FromFormat("", + op->func_qualname, (void *)op); + __Pyx_END_CRITICAL_SECTION(); + return repr; +} +static PyObject * __Pyx_CyFunction_CallMethod(PyObject *func, PyObject *self, PyObject *arg, PyObject *kw) { +#if CYTHON_COMPILING_IN_LIMITED_API + __pyx_CyFunctionObject *cyfunc = __Pyx_as_CyFunctionObject(func); + PyCFunction meth = cyfunc->func_methoddef->ml_meth; + int flags = cyfunc->func_methoddef->ml_flags; +#else + PyCFunctionObject* f = (PyCFunctionObject*)func; + PyCFunction meth = f->m_ml->ml_meth; + int flags = f->m_ml->ml_flags; +#endif + Py_ssize_t size; + switch (flags & (METH_VARARGS | METH_KEYWORDS | METH_NOARGS | METH_O)) { + case METH_VARARGS: + if (likely(kw == NULL || PyDict_Size(kw) == 0)) + return (*meth)(self, arg); + break; + case METH_VARARGS | METH_KEYWORDS: + return (*(PyCFunctionWithKeywords)(void(*)(void))meth)(self, arg, kw); + case METH_NOARGS: + if (likely(kw == NULL || PyDict_Size(kw) == 0)) { +#if CYTHON_ASSUME_SAFE_SIZE + size = PyTuple_GET_SIZE(arg); +#else + size = PyTuple_Size(arg); + if (unlikely(size < 0)) return NULL; +#endif + if (likely(size == 0)) + return (*meth)(self, NULL); + __Pyx_CyFunction_raise_argument_count_error( + func, + "takes no arguments", size); + return NULL; + } + break; + case METH_O: + if (likely(kw == NULL || PyDict_Size(kw) == 0)) { +#if CYTHON_ASSUME_SAFE_SIZE + size = PyTuple_GET_SIZE(arg); +#else + size = PyTuple_Size(arg); + if (unlikely(size < 0)) return NULL; +#endif + if (likely(size == 1)) { + PyObject *result, *arg0; + #if CYTHON_ASSUME_SAFE_MACROS && !CYTHON_AVOID_BORROWED_REFS + arg0 = PyTuple_GET_ITEM(arg, 0); + #else + arg0 = __Pyx_PySequence_ITEM(arg, 0); if (unlikely(!arg0)) return NULL; + #endif + result = (*meth)(self, arg0); + #if !(CYTHON_ASSUME_SAFE_MACROS && !CYTHON_AVOID_BORROWED_REFS) + Py_DECREF(arg0); + #endif + return result; + } + __Pyx_CyFunction_raise_argument_count_error( + func, + "takes exactly one argument", size); + return NULL; + } + break; + default: + PyErr_SetString(PyExc_SystemError, "Bad call flags for CyFunction"); + return NULL; + } + __Pyx_CyFunction_raise_type_error( + func, "takes no keyword arguments"); + return NULL; +} +static CYTHON_INLINE PyObject *__Pyx_CyFunction_Call(PyObject *func, PyObject *arg, PyObject *kw) { + PyObject *result; + result = __Pyx_CyFunction_CallMethod(func, func, arg, kw); + return result; +} +static PyObject *__Pyx_CyFunction_CallAsMethod(PyObject *func, PyObject *args, PyObject *kw) { + PyObject *result; + __pyx_CyFunctionObject *cyfunc = __Pyx_as_CyFunctionObject(func); +#if CYTHON_VECTORCALL + __pyx_vectorcallfunc vc = __Pyx_CyFunction_func_vectorcall(cyfunc); + if (vc) { +#if CYTHON_ASSUME_SAFE_MACROS && CYTHON_ASSUME_SAFE_SIZE + return __Pyx_PyVectorcall_FastCallDict(func, vc, &PyTuple_GET_ITEM(args, 0), (size_t)PyTuple_GET_SIZE(args), kw); +#else + (void) &__Pyx_PyVectorcall_FastCallDict; + return PyVectorcall_Call(func, args, kw); +#endif + } +#endif + if ((cyfunc->flags & __Pyx_CYFUNCTION_CCLASS) && !(cyfunc->flags & __Pyx_CYFUNCTION_STATICMETHOD)) { + Py_ssize_t argc; + PyObject *new_args; + PyObject *self; +#if CYTHON_ASSUME_SAFE_SIZE + argc = PyTuple_GET_SIZE(args); +#else + argc = PyTuple_Size(args); + if (unlikely(argc < 0)) return NULL; +#endif + new_args = PyTuple_GetSlice(args, 1, argc); + if (unlikely(!new_args)) + return NULL; + self = PyTuple_GetItem(args, 0); + if (unlikely(!self)) { + Py_DECREF(new_args); + PyErr_Format(PyExc_TypeError, + "unbound method %.200S() needs an argument", + cyfunc->func_qualname); + return NULL; + } + result = __Pyx_CyFunction_CallMethod(func, self, new_args, kw); + Py_DECREF(new_args); + } else { + result = __Pyx_CyFunction_Call(func, args, kw); + } + return result; +} +#if CYTHON_VECTORCALL +static CYTHON_INLINE int __Pyx_CyFunction_Vectorcall_CheckArgs(PyObject *func, __pyx_CyFunctionObject *cyfunc, Py_ssize_t nargs, PyObject *kwnames) +{ + int ret = 0; + if ((cyfunc->flags & __Pyx_CYFUNCTION_CCLASS) && !(cyfunc->flags & __Pyx_CYFUNCTION_STATICMETHOD)) { + if (unlikely(nargs < 1)) { + __Pyx_CyFunction_raise_type_error( + func, "needs an argument"); + return -1; + } + ret = 1; + } + if (unlikely(kwnames) && unlikely(__Pyx_PyTuple_GET_SIZE(kwnames))) { + __Pyx_CyFunction_raise_type_error( + func, "takes no keyword arguments"); + return -1; + } + return ret; +} +static PyObject * __Pyx_CyFunction_Vectorcall_NOARGS(PyObject *func, PyObject *const *args, size_t nargsf, PyObject *kwnames) +{ + __pyx_CyFunctionObject *cyfunc = __Pyx_as_CyFunctionObject(func); + Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); + PyObject *self; +#if CYTHON_COMPILING_IN_LIMITED_API + PyCFunction meth = cyfunc->func_methoddef->ml_meth; + if (unlikely(!meth)) return NULL; +#else + PyCFunction meth = ((PyCFunctionObject*)cyfunc)->m_ml->ml_meth; +#endif + switch (__Pyx_CyFunction_Vectorcall_CheckArgs(func, cyfunc, nargs, kwnames)) { + case 1: + self = args[0]; + args += 1; + nargs -= 1; + break; + case 0: + self = func; + break; + default: + return NULL; + } + if (unlikely(nargs != 0)) { + __Pyx_CyFunction_raise_argument_count_error( + func, "takes no arguments", nargs); + return NULL; + } + return meth(self, NULL); +} +static PyObject * __Pyx_CyFunction_Vectorcall_O(PyObject *func, PyObject *const *args, size_t nargsf, PyObject *kwnames) +{ + __pyx_CyFunctionObject *cyfunc = __Pyx_as_CyFunctionObject(func); + Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); + PyObject *self; +#if CYTHON_COMPILING_IN_LIMITED_API + PyCFunction meth = cyfunc->func_methoddef->ml_meth; + if (unlikely(!meth)) return NULL; +#else + PyCFunction meth = ((PyCFunctionObject*)cyfunc)->m_ml->ml_meth; +#endif + switch (__Pyx_CyFunction_Vectorcall_CheckArgs(func, cyfunc, nargs, kwnames)) { + case 1: + self = args[0]; + args += 1; + nargs -= 1; + break; + case 0: + self = func; + break; + default: + return NULL; + } + if (unlikely(nargs != 1)) { + __Pyx_CyFunction_raise_argument_count_error( + func, "takes exactly one argument", nargs); + return NULL; + } + return meth(self, args[0]); +} +static PyObject * __Pyx_CyFunction_Vectorcall_FASTCALL_KEYWORDS(PyObject *func, PyObject *const *args, size_t nargsf, PyObject *kwnames) +{ + __pyx_CyFunctionObject *cyfunc = __Pyx_as_CyFunctionObject(func); + Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); + PyObject *self; +#if CYTHON_COMPILING_IN_LIMITED_API + PyCFunction meth = cyfunc->func_methoddef->ml_meth; + if (unlikely(!meth)) return NULL; +#else + PyCFunction meth = ((PyCFunctionObject*)cyfunc)->m_ml->ml_meth; +#endif + switch (__Pyx_CyFunction_Vectorcall_CheckArgs(func, cyfunc, nargs, NULL)) { + case 1: + self = args[0]; + args += 1; + nargs -= 1; + break; + case 0: + self = func; + break; + default: + return NULL; + } + return ((__Pyx_PyCFunctionFastWithKeywords)(void(*)(void))meth)(self, args, nargs, kwnames); +} +static PyObject * __Pyx_CyFunction_Vectorcall_FASTCALL_KEYWORDS_METHOD(PyObject *func, PyObject *const *args, size_t nargsf, PyObject *kwnames) +{ + __pyx_CyFunctionObject *cyfunc = __Pyx_as_CyFunctionObject(func); + PyTypeObject *cls = (PyTypeObject *) __Pyx__CyFunction_GetClassObj(cyfunc); + Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); + PyObject *self; +#if CYTHON_COMPILING_IN_LIMITED_API + PyCFunction meth = cyfunc->func_methoddef->ml_meth; + if (unlikely(!meth)) return NULL; +#else + PyCFunction meth = ((PyCFunctionObject*)cyfunc)->m_ml->ml_meth; +#endif + switch (__Pyx_CyFunction_Vectorcall_CheckArgs(func, cyfunc, nargs, NULL)) { + case 1: + self = args[0]; + args += 1; + nargs -= 1; + break; + case 0: + self = func; + break; + default: + return NULL; + } + #if PY_VERSION_HEX < 0x030e00A6 + size_t nargs_value = (size_t) nargs; + #else + Py_ssize_t nargs_value = nargs; + #endif + return ((__Pyx_PyCMethod)(void(*)(void))meth)(self, cls, args, nargs_value, kwnames); +} +#endif +static PyType_Slot __pyx_CyFunctionType_slots[] = { + {Py_tp_dealloc, (void *)__Pyx_CyFunction_dealloc}, + {Py_tp_repr, (void *)__Pyx_CyFunction_repr}, + {Py_tp_call, (void *)__Pyx_CyFunction_CallAsMethod}, + {Py_tp_traverse, (void *)__Pyx_CyFunction_traverse}, + {Py_tp_clear, (void *)__Pyx_CyFunction_clear}, + {Py_tp_methods, (void *)__pyx_CyFunction_methods}, + {Py_tp_members, (void *)__pyx_CyFunction_members}, + {Py_tp_getset, (void *)__pyx_CyFunction_getsets}, + {Py_tp_descr_get, (void *)__Pyx_PyMethod_New}, + {0, 0}, +}; +static PyType_Spec __pyx_CyFunctionType_spec = { + __PYX_TYPE_MODULE_PREFIX "cython_function_or_method", + __PYX_SHARED_SIZEOF(__pyx_CyFunctionObject), + 0, +#ifdef Py_TPFLAGS_METHOD_DESCRIPTOR + Py_TPFLAGS_METHOD_DESCRIPTOR | +#endif +#if CYTHON_VECTORCALL +#if defined(Py_TPFLAGS_HAVE_VECTORCALL) + Py_TPFLAGS_HAVE_VECTORCALL | +#elif defined(_Py_TPFLAGS_HAVE_VECTORCALL) + _Py_TPFLAGS_HAVE_VECTORCALL | +#endif +#endif // CYTHON_VECTORCALL +#if PY_VERSION_HEX >= 0x030C0000 && !CYTHON_COMPILING_IN_LIMITED_API + Py_TPFLAGS_MANAGED_DICT | +#endif + Py_TPFLAGS_IMMUTABLETYPE | Py_TPFLAGS_DISALLOW_INSTANTIATION | + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | Py_TPFLAGS_BASETYPE, + __pyx_CyFunctionType_slots +}; +static int __pyx_CyFunction_init(PyObject *module) { + __pyx_mstatetype *mstate = __Pyx_PyModule_GetState(module); + mstate->__pyx_CyFunctionType = __Pyx_FetchCommonTypeFromSpec( + mstate->__pyx_CommonTypesMetaclassType, module, &__pyx_CyFunctionType_spec, NULL); + if (unlikely(mstate->__pyx_CyFunctionType == NULL)) { + return -1; + } + return 0; +} + +/* CythonFunction */ +static PyObject *__Pyx_CyFunction_New(PyMethodDef *ml, int flags, PyObject* qualname, + PyObject *closure, PyObject *module, PyObject* globals, PyObject* code) { + (void)&__Pyx_Get_CyFunction_Type; + PyObject *op = __Pyx_CyFunction_Init( + PyObject_GC_New(PyObject, __pyx_mstate_global->__pyx_CyFunctionType), + ml, flags, qualname, closure, module, globals, code + ); + if (likely(op)) { + PyObject_GC_Track(op); + } + return op; +} +static PyTypeObject *__Pyx_Get_CyFunction_Type(void) { + PyTypeObject *tp = __pyx_mstate_global->__pyx_CyFunctionType; + Py_INCREF((PyObject*)tp); + return tp; +} + +/* CLineInTraceback (used by AddTraceback) */ +#if CYTHON_CLINE_IN_TRACEBACK && CYTHON_CLINE_IN_TRACEBACK_RUNTIME +#if CYTHON_COMPILING_IN_LIMITED_API && __PYX_LIMITED_VERSION_HEX < 0x030A0000 +#define __Pyx_PyProbablyModule_GetDict(o) __Pyx_XNewRef(PyModule_GetDict(o)) +#elif !CYTHON_COMPILING_IN_CPYTHON || CYTHON_COMPILING_IN_CPYTHON_FREETHREADING +#define __Pyx_PyProbablyModule_GetDict(o) PyObject_GenericGetDict(o, NULL); +#else +PyObject* __Pyx_PyProbablyModule_GetDict(PyObject *o) { + PyObject **dict_ptr = _PyObject_GetDictPtr(o); + return dict_ptr ? __Pyx_XNewRef(*dict_ptr) : NULL; +} +#endif +static int __Pyx_CLineForTraceback(PyThreadState *tstate, int c_line) { + PyObject *use_cline = NULL; + PyObject *ptype, *pvalue, *ptraceback; + PyObject *cython_runtime_dict; + CYTHON_MAYBE_UNUSED_VAR(tstate); + if (unlikely(!__pyx_mstate_global->__pyx_cython_runtime)) { + return c_line; + } + __Pyx_ErrFetchInState(tstate, &ptype, &pvalue, &ptraceback); + cython_runtime_dict = __Pyx_PyProbablyModule_GetDict(__pyx_mstate_global->__pyx_cython_runtime); + if (likely(cython_runtime_dict)) { + __PYX_PY_DICT_LOOKUP_IF_MODIFIED( + use_cline, cython_runtime_dict, + __Pyx_PyDict_SetDefault(cython_runtime_dict, __pyx_mstate_global->__pyx_n_u_cline_in_traceback, Py_False)) + } + if (use_cline == NULL || use_cline == Py_False || (use_cline != Py_True && PyObject_Not(use_cline) != 0)) { + c_line = 0; + } + Py_XDECREF(use_cline); + Py_XDECREF(cython_runtime_dict); + __Pyx_ErrRestoreInState(tstate, ptype, pvalue, ptraceback); + return c_line; +} +#endif + +/* CodeObjectCache (used by AddTraceback) */ +static int __pyx_bisect_code_objects(__Pyx_CodeObjectCacheEntry* entries, int count, int code_line) { + int start = 0, mid = 0, end = count - 1; + if (end >= 0 && code_line > entries[end].code_line) { + return count; + } + while (start < end) { + mid = start + (end - start) / 2; + if (code_line < entries[mid].code_line) { + end = mid; + } else if (code_line > entries[mid].code_line) { + start = mid + 1; + } else { + return mid; + } + } + if (code_line <= entries[mid].code_line) { + return mid; + } else { + return mid + 1; + } +} +static __Pyx_CachedCodeObjectType *__pyx__find_code_object(struct __Pyx_CodeObjectCache *code_cache, int code_line) { + __Pyx_CachedCodeObjectType* code_object; + int pos; + if (unlikely(!code_line) || unlikely(!code_cache->entries)) { + return NULL; + } + pos = __pyx_bisect_code_objects(code_cache->entries, code_cache->count, code_line); + if (unlikely(pos >= code_cache->count) || unlikely(code_cache->entries[pos].code_line != code_line)) { + return NULL; + } + code_object = code_cache->entries[pos].code_object; + Py_INCREF(code_object); + return code_object; +} +static __Pyx_CachedCodeObjectType *__pyx_find_code_object(int code_line) { +#if CYTHON_COMPILING_IN_CPYTHON_FREETHREADING && !CYTHON_ATOMICS + (void)__pyx__find_code_object; + return NULL; // Most implementation should have atomics. But otherwise, don't make it thread-safe, just miss. +#else + struct __Pyx_CodeObjectCache *code_cache = &__pyx_mstate_global->__pyx_code_cache; +#if CYTHON_COMPILING_IN_CPYTHON_FREETHREADING + __pyx_nonatomic_int_type old_count = __pyx_atomic_incr_acq_rel(&code_cache->accessor_count); + if (old_count < 0) { + __pyx_atomic_decr_acq_rel(&code_cache->accessor_count); + return NULL; + } +#endif + __Pyx_CachedCodeObjectType *result = __pyx__find_code_object(code_cache, code_line); +#if CYTHON_COMPILING_IN_CPYTHON_FREETHREADING + __pyx_atomic_decr_acq_rel(&code_cache->accessor_count); +#endif + return result; +#endif +} +static void __pyx__insert_code_object(struct __Pyx_CodeObjectCache *code_cache, int code_line, __Pyx_CachedCodeObjectType* code_object) +{ + int pos, i; + __Pyx_CodeObjectCacheEntry* entries = code_cache->entries; + if (unlikely(!code_line)) { + return; + } + if (unlikely(!entries)) { + entries = (__Pyx_CodeObjectCacheEntry*)PyMem_Malloc(64*sizeof(__Pyx_CodeObjectCacheEntry)); + if (likely(entries)) { + code_cache->entries = entries; + code_cache->max_count = 64; + code_cache->count = 1; + entries[0].code_line = code_line; + entries[0].code_object = code_object; + Py_INCREF(code_object); + } + return; + } + pos = __pyx_bisect_code_objects(code_cache->entries, code_cache->count, code_line); + if ((pos < code_cache->count) && unlikely(code_cache->entries[pos].code_line == code_line)) { + __Pyx_CachedCodeObjectType* tmp = entries[pos].code_object; + entries[pos].code_object = code_object; + Py_INCREF(code_object); + Py_DECREF(tmp); + return; + } + if (code_cache->count == code_cache->max_count) { + int new_max = code_cache->max_count + 64; + entries = (__Pyx_CodeObjectCacheEntry*)PyMem_Realloc( + code_cache->entries, ((size_t)new_max) * sizeof(__Pyx_CodeObjectCacheEntry)); + if (unlikely(!entries)) { + return; + } + code_cache->entries = entries; + code_cache->max_count = new_max; + } + for (i=code_cache->count; i>pos; i--) { + entries[i] = entries[i-1]; + } + entries[pos].code_line = code_line; + entries[pos].code_object = code_object; + code_cache->count++; + Py_INCREF(code_object); +} +static void __pyx_insert_code_object(int code_line, __Pyx_CachedCodeObjectType* code_object) { +#if CYTHON_COMPILING_IN_CPYTHON_FREETHREADING && !CYTHON_ATOMICS + (void)__pyx__insert_code_object; + return; // Most implementation should have atomics. But otherwise, don't make it thread-safe, just fail. +#else + struct __Pyx_CodeObjectCache *code_cache = &__pyx_mstate_global->__pyx_code_cache; +#if CYTHON_COMPILING_IN_CPYTHON_FREETHREADING + __pyx_nonatomic_int_type expected = 0; + if (!__pyx_atomic_int_cmp_exchange(&code_cache->accessor_count, &expected, INT_MIN)) { + return; + } +#endif + __pyx__insert_code_object(code_cache, code_line, code_object); +#if CYTHON_COMPILING_IN_CPYTHON_FREETHREADING + __pyx_atomic_sub(&code_cache->accessor_count, INT_MIN); +#endif +#endif +} + +/* AddTraceback */ +#include "compile.h" +#include "frameobject.h" +#include "traceback.h" +#if PY_VERSION_HEX >= 0x030b00a6 && !CYTHON_COMPILING_IN_LIMITED_API && !defined(PYPY_VERSION) + #ifndef Py_BUILD_CORE + #define Py_BUILD_CORE 1 + #endif + #include "internal/pycore_frame.h" +#endif +#if CYTHON_COMPILING_IN_LIMITED_API +static PyObject *__Pyx_PyCode_Replace_For_AddTraceback(PyObject *code, PyObject *scratch_dict, + PyObject *firstlineno, PyObject *name) { + PyObject *replace = NULL; + if (unlikely(PyDict_SetItemString(scratch_dict, "co_firstlineno", firstlineno))) return NULL; + if (unlikely(PyDict_SetItemString(scratch_dict, "co_name", name))) return NULL; + replace = PyObject_GetAttrString(code, "replace"); + if (likely(replace)) { + PyObject *result = PyObject_Call(replace, __pyx_mstate_global->__pyx_empty_tuple, scratch_dict); + Py_DECREF(replace); + return result; + } + return NULL; +} +static void __Pyx_AddTraceback(const char *funcname, int c_line, + int py_line, const char *filename) { + PyObject *code_object = NULL, *py_py_line = NULL, *py_funcname = NULL, *dict = NULL; + PyObject *replace = NULL, *getframe = NULL, *frame = NULL; + PyObject *exc_type, *exc_value, *exc_traceback; + int success = 0; + if (c_line) { + c_line = __Pyx_CLineForTraceback(__Pyx_PyThreadState_Current, c_line); + } + __Pyx_PyErr_FetchException(&exc_type, &exc_value, &exc_traceback); + code_object = __pyx_find_code_object(c_line ? -c_line : py_line); + if (!code_object) { + code_object = Py_CompileString("_getframe()", filename, Py_eval_input); + if (unlikely(!code_object)) goto bad; + py_py_line = PyLong_FromLong(py_line); + if (unlikely(!py_py_line)) goto bad; + if (c_line) { + py_funcname = PyUnicode_FromFormat( "%s (%s:%d)", funcname, __pyx_cfilenm, c_line); + } else { + py_funcname = PyUnicode_FromString(funcname); + } + if (unlikely(!py_funcname)) goto bad; + dict = PyDict_New(); + if (unlikely(!dict)) goto bad; + { + PyObject *old_code_object = code_object; + code_object = __Pyx_PyCode_Replace_For_AddTraceback(code_object, dict, py_py_line, py_funcname); + Py_DECREF(old_code_object); + } + if (unlikely(!code_object)) goto bad; + __pyx_insert_code_object(c_line ? -c_line : py_line, code_object); + } else { + dict = PyDict_New(); + } + getframe = PySys_GetObject("_getframe"); + if (unlikely(!getframe)) goto bad; + if (unlikely(PyDict_SetItemString(dict, "_getframe", getframe))) goto bad; + frame = PyEval_EvalCode(code_object, dict, dict); + if (unlikely(!frame) || frame == Py_None) goto bad; + success = 1; + bad: + __Pyx_PyErr_RestoreException(exc_type, exc_value, exc_traceback); + Py_XDECREF(code_object); + Py_XDECREF(py_py_line); + Py_XDECREF(py_funcname); + Py_XDECREF(dict); + Py_XDECREF(replace); + if (success) { + PyTraceBack_Here( + (struct _frame*)frame); + } + Py_XDECREF(frame); +} +#else +static PyCodeObject* __Pyx_CreateCodeObjectForTraceback( + const char *funcname, int c_line, + int py_line, const char *filename) { + PyCodeObject *py_code = NULL; + PyObject *py_funcname = NULL; + if (c_line) { + py_funcname = PyUnicode_FromFormat( "%s (%s:%d)", funcname, __pyx_cfilenm, c_line); + if (!py_funcname) goto bad; + funcname = PyUnicode_AsUTF8(py_funcname); + if (!funcname) goto bad; + } + py_code = PyCode_NewEmpty(filename, funcname, py_line); + Py_XDECREF(py_funcname); + return py_code; +bad: + Py_XDECREF(py_funcname); + return NULL; +} +static void __Pyx_AddTraceback(const char *funcname, int c_line, + int py_line, const char *filename) { + PyCodeObject *py_code = 0; + PyFrameObject *py_frame = 0; + PyThreadState *tstate = __Pyx_PyThreadState_Current; + PyObject *ptype, *pvalue, *ptraceback; + if (c_line) { + c_line = __Pyx_CLineForTraceback(tstate, c_line); + } + py_code = __pyx_find_code_object(c_line ? -c_line : py_line); + if (!py_code) { + __Pyx_ErrFetchInState(tstate, &ptype, &pvalue, &ptraceback); + py_code = __Pyx_CreateCodeObjectForTraceback( + funcname, c_line, py_line, filename); + if (!py_code) { + /* If the code object creation fails, then we should clear the + fetched exception references and propagate the new exception */ + Py_XDECREF(ptype); + Py_XDECREF(pvalue); + Py_XDECREF(ptraceback); + goto bad; + } + __Pyx_ErrRestoreInState(tstate, ptype, pvalue, ptraceback); + __pyx_insert_code_object(c_line ? -c_line : py_line, py_code); + } + py_frame = PyFrame_New( + tstate, /*PyThreadState *tstate,*/ + py_code, /*PyCodeObject *code,*/ + __pyx_mstate_global->__pyx_d, /*PyObject *globals,*/ + 0 /*PyObject *locals*/ + ); + if (!py_frame) goto bad; + __Pyx_PyFrame_SetLineNumber(py_frame, py_line); + PyTraceBack_Here(py_frame); +bad: + Py_XDECREF(py_code); + Py_XDECREF(py_frame); +} +#endif + +/* Declarations */ +#if CYTHON_CCOMPLEX && (1) && (!0 || __cplusplus) + #ifdef __cplusplus + static CYTHON_INLINE __pyx_t_double_complex __pyx_t_double_complex_from_parts(double x, double y) { + return ::std::complex< double >(x, y); + } + #else + static CYTHON_INLINE __pyx_t_double_complex __pyx_t_double_complex_from_parts(double x, double y) { + return x + y*(__pyx_t_double_complex)_Complex_I; + } + #endif +#else + static CYTHON_INLINE __pyx_t_double_complex __pyx_t_double_complex_from_parts(double x, double y) { + __pyx_t_double_complex z; + z.real = x; + z.imag = y; + return z; + } +#endif + +/* Arithmetic */ +#if CYTHON_CCOMPLEX && (1) && (!0 || __cplusplus) +#else + static CYTHON_INLINE int __Pyx_c_eq_double(__pyx_t_double_complex a, __pyx_t_double_complex b) { + return (a.real == b.real) && (a.imag == b.imag); + } + static CYTHON_INLINE __pyx_t_double_complex __Pyx_c_sum_double(__pyx_t_double_complex a, __pyx_t_double_complex b) { + __pyx_t_double_complex z; + z.real = a.real + b.real; + z.imag = a.imag + b.imag; + return z; + } + static CYTHON_INLINE __pyx_t_double_complex __Pyx_c_diff_double(__pyx_t_double_complex a, __pyx_t_double_complex b) { + __pyx_t_double_complex z; + z.real = a.real - b.real; + z.imag = a.imag - b.imag; + return z; + } + static CYTHON_INLINE __pyx_t_double_complex __Pyx_c_prod_double(__pyx_t_double_complex a, __pyx_t_double_complex b) { + __pyx_t_double_complex z; + z.real = a.real * b.real - a.imag * b.imag; + z.imag = a.real * b.imag + a.imag * b.real; + return z; + } + #if 1 + static CYTHON_INLINE __pyx_t_double_complex __Pyx_c_quot_double(__pyx_t_double_complex a, __pyx_t_double_complex b) { + if (b.imag == 0) { + return __pyx_t_double_complex_from_parts(a.real / b.real, a.imag / b.real); + } else if (fabs(b.real) >= fabs(b.imag)) { + if (b.real == 0 && b.imag == 0) { + return __pyx_t_double_complex_from_parts(a.real / b.real, a.imag / b.imag); + } else { + double r = b.imag / b.real; + double s = (double)(1.0) / (b.real + b.imag * r); + return __pyx_t_double_complex_from_parts( + (a.real + a.imag * r) * s, (a.imag - a.real * r) * s); + } + } else { + double r = b.real / b.imag; + double s = (double)(1.0) / (b.imag + b.real * r); + return __pyx_t_double_complex_from_parts( + (a.real * r + a.imag) * s, (a.imag * r - a.real) * s); + } + } + #else + static CYTHON_INLINE __pyx_t_double_complex __Pyx_c_quot_double(__pyx_t_double_complex a, __pyx_t_double_complex b) { + if (b.imag == 0) { + return __pyx_t_double_complex_from_parts(a.real / b.real, a.imag / b.real); + } else { + double denom = b.real * b.real + b.imag * b.imag; + return __pyx_t_double_complex_from_parts( + (a.real * b.real + a.imag * b.imag) / denom, + (a.imag * b.real - a.real * b.imag) / denom); + } + } + #endif + static CYTHON_INLINE __pyx_t_double_complex __Pyx_c_neg_double(__pyx_t_double_complex a) { + __pyx_t_double_complex z; + z.real = -a.real; + z.imag = -a.imag; + return z; + } + static CYTHON_INLINE int __Pyx_c_is_zero_double(__pyx_t_double_complex a) { + return (a.real == 0) && (a.imag == 0); + } + static CYTHON_INLINE __pyx_t_double_complex __Pyx_c_conj_double(__pyx_t_double_complex a) { + __pyx_t_double_complex z; + z.real = a.real; + z.imag = -a.imag; + return z; + } + #if 1 + static CYTHON_INLINE double __Pyx_c_abs_double(__pyx_t_double_complex z) { + #if !defined(HAVE_HYPOT) || defined(_MSC_VER) + return sqrt(z.real*z.real + z.imag*z.imag); + #else + return hypot(z.real, z.imag); + #endif + } + static CYTHON_INLINE __pyx_t_double_complex __Pyx_c_pow_double(__pyx_t_double_complex a, __pyx_t_double_complex b) { + __pyx_t_double_complex z; + double r, lnr, theta, z_r, z_theta; + if (b.imag == 0 && b.real == (int)b.real) { + if (b.real < 0) { + double denom = a.real * a.real + a.imag * a.imag; + a.real = a.real / denom; + a.imag = -a.imag / denom; + b.real = -b.real; + } + switch ((int)b.real) { + case 0: + z.real = 1; + z.imag = 0; + return z; + case 1: + return a; + case 2: + return __Pyx_c_prod_double(a, a); + case 3: + z = __Pyx_c_prod_double(a, a); + return __Pyx_c_prod_double(z, a); + case 4: + z = __Pyx_c_prod_double(a, a); + return __Pyx_c_prod_double(z, z); + } + } + if (a.imag == 0) { + if (a.real == 0) { + return a; + } else if ((b.imag == 0) && (a.real >= 0)) { + z.real = pow(a.real, b.real); + z.imag = 0; + return z; + } else if (a.real > 0) { + r = a.real; + theta = 0; + } else { + r = -a.real; + theta = atan2(0.0, -1.0); + } + } else { + r = __Pyx_c_abs_double(a); + theta = atan2(a.imag, a.real); + } + lnr = log(r); + z_r = exp(lnr * b.real - theta * b.imag); + z_theta = theta * b.real + lnr * b.imag; + z.real = z_r * cos(z_theta); + z.imag = z_r * sin(z_theta); + return z; + } + #endif +#endif + +/* FromPy */ +static __pyx_t_double_complex __Pyx_PyComplex_As___pyx_t_double_complex(PyObject* o) { +#if CYTHON_COMPILING_IN_LIMITED_API + double real=-1.0, imag=-1.0; + real = PyComplex_RealAsDouble(o); + if (unlikely(real == -1.0 && PyErr_Occurred())) goto end; + imag = PyComplex_ImagAsDouble(o); + end: + return __pyx_t_double_complex_from_parts( + (double)real, (double)imag + ); +#else + Py_complex cval; +#if !CYTHON_COMPILING_IN_PYPY && !CYTHON_COMPILING_IN_GRAAL + if (PyComplex_CheckExact(o)) + cval = ((PyComplexObject *)o)->cval; + else +#endif + cval = PyComplex_AsCComplex(o); + return __pyx_t_double_complex_from_parts( + (double)cval.real, + (double)cval.imag); +#endif +} + +/* CIntFromPyVerify */ +#define __PYX_VERIFY_RETURN_INT(target_type, func_type, func_value)\ + __PYX__VERIFY_RETURN_INT(target_type, func_type, func_value, 0) +#define __PYX_VERIFY_RETURN_INT_EXC(target_type, func_type, func_value)\ + __PYX__VERIFY_RETURN_INT(target_type, func_type, func_value, 1) +#define __PYX__VERIFY_RETURN_INT(target_type, func_type, func_value, exc)\ + {\ + func_type value = func_value;\ + if (sizeof(target_type) < sizeof(func_type)) {\ + if (unlikely(value != (func_type) (target_type) value)) {\ + func_type zero = 0;\ + if (exc && unlikely(value == (func_type)-1 && PyErr_Occurred()))\ + return (target_type) -1;\ + if (is_unsigned && unlikely(value < zero))\ + goto raise_neg_overflow;\ + else\ + goto raise_overflow;\ + }\ + }\ + return (target_type) value;\ + } + +/* CIntFromPy */ +static int __Pyx_LargePyLong___Pyx_PyLong_As_int(PyObject *x); +static int __Pyx_raise_neg_overflow___Pyx_PyLong_As_int(void) { + const char* type_name = "int"; + PyErr_Format(PyExc_OverflowError, + "can't convert negative value to %.200s", type_name); + return (int) -1; +} +static int __Pyx_raise_overflow___Pyx_PyLong_As_int(void) { + const char* type_name = "int"; + PyErr_Format(PyExc_OverflowError, + "value too large to convert to %.200s", type_name); + return (int) -1; +} +static CYTHON_INLINE int __Pyx_PyULong___Pyx_PyLong_As_int(PyObject *x) { + const int is_unsigned = 1; +#if CYTHON_USE_PYLONG_INTERNALS + { + const digit* digits = __Pyx_PyLong_Digits(x); + const Py_ssize_t size = __Pyx_PyLong_DigitCount(x); + if (size == 2 && (8 * sizeof(int) > 1 * PyLong_SHIFT)) { + if ((8 * sizeof(unsigned long) > 2 * PyLong_SHIFT)) { + __PYX_VERIFY_RETURN_INT(int, unsigned long, (((((unsigned long)digits[1]) << PyLong_SHIFT) | (unsigned long)digits[0]))) + } else if ((8 * sizeof(int) >= 2 * PyLong_SHIFT)) { + return (int) (((((int)digits[1]) << PyLong_SHIFT) | (int)digits[0])); + } + } else + if (size == 3 && (8 * sizeof(int) > 2 * PyLong_SHIFT)) { + if ((8 * sizeof(unsigned long) > 3 * PyLong_SHIFT)) { + __PYX_VERIFY_RETURN_INT(int, unsigned long, (((((((unsigned long)digits[2]) << PyLong_SHIFT) | (unsigned long)digits[1]) << PyLong_SHIFT) | (unsigned long)digits[0]))) + } else if ((8 * sizeof(int) >= 3 * PyLong_SHIFT)) { + return (int) (((((((int)digits[2]) << PyLong_SHIFT) | (int)digits[1]) << PyLong_SHIFT) | (int)digits[0])); + } + } else + if (size == 4 && (8 * sizeof(int) > 3 * PyLong_SHIFT)) { + if ((8 * sizeof(unsigned long) > 4 * PyLong_SHIFT)) { + __PYX_VERIFY_RETURN_INT(int, unsigned long, (((((((((unsigned long)digits[3]) << PyLong_SHIFT) | (unsigned long)digits[2]) << PyLong_SHIFT) | (unsigned long)digits[1]) << PyLong_SHIFT) | (unsigned long)digits[0]))) + } else if ((8 * sizeof(int) >= 4 * PyLong_SHIFT)) { + return (int) (((((((((int)digits[3]) << PyLong_SHIFT) | (int)digits[2]) << PyLong_SHIFT) | (int)digits[1]) << PyLong_SHIFT) | (int)digits[0])); + } + } else + {} + } +#elif CYTHON_COMPILING_IN_CPYTHON && PY_VERSION_HEX < 0x030C00A7 + if (unlikely(Py_SIZE(x) < 0)) { + goto raise_neg_overflow; + } +#else + { + int result = PyObject_RichCompareBool(x, Py_False, Py_LT); + if (unlikely(result < 0)) + return (int) -1; + if (unlikely(result == 1)) + goto raise_neg_overflow; + } +#endif + if ((sizeof(int) <= sizeof(unsigned long))) { + __PYX_VERIFY_RETURN_INT_EXC(int, unsigned long, PyLong_AsUnsignedLong(x)) + } else if ((sizeof(int) <= sizeof(unsigned PY_LONG_LONG))) { + __PYX_VERIFY_RETURN_INT_EXC(int, unsigned PY_LONG_LONG, PyLong_AsUnsignedLongLong(x)) + } + return __Pyx_LargePyLong___Pyx_PyLong_As_int(x); +raise_neg_overflow: + return __Pyx_raise_neg_overflow___Pyx_PyLong_As_int(); +raise_overflow: + return __Pyx_raise_overflow___Pyx_PyLong_As_int(); +} +static CYTHON_INLINE int __Pyx_PySLong___Pyx_PyLong_As_int(PyObject *x) { + const int is_unsigned = 0; +#if CYTHON_USE_PYLONG_INTERNALS + if (__Pyx_PyLong_IsNeg(x)) { + const Py_ssize_t size = __Pyx_PyLong_DigitCount(x); + const digit* digits = __Pyx_PyLong_Digits(x); + if (size == 2 && (8 * sizeof(int) > 1 * PyLong_SHIFT)) { + if ((8 * sizeof(long) > 2 * PyLong_SHIFT)) { + long ival = - (long) (((((unsigned long)digits[1]) << PyLong_SHIFT) | (unsigned long)digits[0])); + __PYX_VERIFY_RETURN_INT(int, long, ival) + } else if ((8 * sizeof(int) - 1 > 2 * PyLong_SHIFT)) { + return (int) (((int) -1) * (((((int)digits[1]) << PyLong_SHIFT) | (int)digits[0]))); + } + } else + if (size == 3 && (8 * sizeof(int) > 2 * PyLong_SHIFT)) { + if ((8 * sizeof(long) > 3 * PyLong_SHIFT)) { + long ival = - (long) (((((((unsigned long)digits[2]) << PyLong_SHIFT) | (unsigned long)digits[1]) << PyLong_SHIFT) | (unsigned long)digits[0])); + __PYX_VERIFY_RETURN_INT(int, long, ival) + } else if ((8 * sizeof(int) - 1 > 3 * PyLong_SHIFT)) { + return (int) (((int) -1) * (((((((int)digits[2]) << PyLong_SHIFT) | (int)digits[1]) << PyLong_SHIFT) | (int)digits[0]))); + } + } else + if (size == 4 && (8 * sizeof(int) > 3 * PyLong_SHIFT)) { + if ((8 * sizeof(long) > 4 * PyLong_SHIFT)) { + long ival = - (long) (((((((((unsigned long)digits[3]) << PyLong_SHIFT) | (unsigned long)digits[2]) << PyLong_SHIFT) | (unsigned long)digits[1]) << PyLong_SHIFT) | (unsigned long)digits[0])); + __PYX_VERIFY_RETURN_INT(int, long, ival) + } else if ((8 * sizeof(int) - 1 > 4 * PyLong_SHIFT)) { + return (int) (((int) -1) * (((((((((int)digits[3]) << PyLong_SHIFT) | (int)digits[2]) << PyLong_SHIFT) | (int)digits[1]) << PyLong_SHIFT) | (int)digits[0]))); + } + } else + {} + } else { + const Py_ssize_t size = __Pyx_PyLong_DigitCount(x); + const digit* digits = __Pyx_PyLong_Digits(x); + if (size == 2 && (8 * sizeof(int) > 1 * PyLong_SHIFT)) { + if ((8 * sizeof(long) > 2 * PyLong_SHIFT)) { + __PYX_VERIFY_RETURN_INT(int, unsigned long, (((((unsigned long)digits[1]) << PyLong_SHIFT) | (unsigned long)digits[0]))) + } else if ((8 * sizeof(int) - 1 > 2 * PyLong_SHIFT)) { + return (int) (((((int)digits[1]) << PyLong_SHIFT) | (int)digits[0])); + } + } else + if (size == 3 && (8 * sizeof(int) > 2 * PyLong_SHIFT)) { + if ((8 * sizeof(long) > 3 * PyLong_SHIFT)) { + __PYX_VERIFY_RETURN_INT(int, unsigned long, (((((((unsigned long)digits[2]) << PyLong_SHIFT) | (unsigned long)digits[1]) << PyLong_SHIFT) | (unsigned long)digits[0]))) + } else if ((8 * sizeof(int) - 1 > 3 * PyLong_SHIFT)) { + return (int) (((((((int)digits[2]) << PyLong_SHIFT) | (int)digits[1]) << PyLong_SHIFT) | (int)digits[0])); + } + } else + if (size == 4 && (8 * sizeof(int) > 3 * PyLong_SHIFT)) { + if ((8 * sizeof(long) > 4 * PyLong_SHIFT)) { + __PYX_VERIFY_RETURN_INT(int, unsigned long, (((((((((unsigned long)digits[3]) << PyLong_SHIFT) | (unsigned long)digits[2]) << PyLong_SHIFT) | (unsigned long)digits[1]) << PyLong_SHIFT) | (unsigned long)digits[0]))) + } else if ((8 * sizeof(int) - 1 > 4 * PyLong_SHIFT)) { + return (int) (((((((((int)digits[3]) << PyLong_SHIFT) | (int)digits[2]) << PyLong_SHIFT) | (int)digits[1]) << PyLong_SHIFT) | (int)digits[0])); + } + } else + {} + } +#endif + #if __PYX_LIMITED_VERSION_HEX >= 0x030d0000 + if ((sizeof(int) <= sizeof(int)) && (sizeof(int) < sizeof(long))) { + __PYX_VERIFY_RETURN_INT_EXC(int, int, PyLong_AsInt(x)) + } else + #endif + if ((sizeof(int) <= sizeof(long))) { + __PYX_VERIFY_RETURN_INT_EXC(int, long, PyLong_AsLong(x)) + } else if ((sizeof(int) <= sizeof(PY_LONG_LONG))) { + __PYX_VERIFY_RETURN_INT_EXC(int, PY_LONG_LONG, PyLong_AsLongLong(x)) + } + return __Pyx_LargePyLong___Pyx_PyLong_As_int(x); +raise_neg_overflow: + return __Pyx_raise_neg_overflow___Pyx_PyLong_As_int(); +raise_overflow: + return __Pyx_raise_overflow___Pyx_PyLong_As_int(); +} +static int __Pyx_LargePyLong___Pyx_PyLong_As_int(PyObject *x) { +#ifdef __Pyx_HAS_GCC_DIAGNOSTIC +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wconversion" +#endif + const int neg_one = (int) -1, const_zero = (int) 0; +#ifdef __Pyx_HAS_GCC_DIAGNOSTIC +#pragma GCC diagnostic pop +#endif + const int is_unsigned = neg_one > const_zero; + int val; + int ret = -1; +#if PY_VERSION_HEX >= 0x030d00A6 && !CYTHON_COMPILING_IN_LIMITED_API + Py_ssize_t bytes_copied = PyLong_AsNativeBytes( + x, &val, sizeof(val), Py_ASNATIVEBYTES_NATIVE_ENDIAN | (is_unsigned ? Py_ASNATIVEBYTES_UNSIGNED_BUFFER | Py_ASNATIVEBYTES_REJECT_NEGATIVE : 0)); + if (unlikely(bytes_copied == -1)) { + } else if (unlikely(bytes_copied > (Py_ssize_t) sizeof(val))) { + goto raise_overflow; + } else { + ret = 0; + } +#elif PY_VERSION_HEX < 0x030d0000 && !(CYTHON_COMPILING_IN_PYPY || CYTHON_COMPILING_IN_LIMITED_API) || defined(_PyLong_AsByteArray) + int one = 1; int is_little = (int)*(unsigned char *)&one; + unsigned char *bytes = (unsigned char *)&val; + ret = _PyLong_AsByteArray((PyLongObject *)x, + bytes, sizeof(val), + is_little, !is_unsigned); + if ((0)) goto raise_overflow; +#else + PyObject *v; + PyObject *stepval = NULL, *mask = NULL, *shift = NULL; + int bits, remaining_bits, is_negative = 0; + int chunk_size = (sizeof(long) < 8) ? 30 : 62; + if (likely(PyLong_CheckExact(x))) { + v = __Pyx_NewRef(x); + } else { + v = PyNumber_Long(x); + if (unlikely(!v)) return (int) -1; + assert(PyLong_CheckExact(v)); + } + { + int result = PyObject_RichCompareBool(v, Py_False, Py_LT); + if (unlikely(result < 0)) { + Py_DECREF(v); + return (int) -1; + } + is_negative = result == 1; + } + if (is_unsigned && unlikely(is_negative)) { + Py_DECREF(v); + PyErr_SetString(PyExc_OverflowError, + "can't convert negative value to int"); + return (int) -1; + } else if (is_negative) { + stepval = PyNumber_Invert(v); + Py_DECREF(v); + if (unlikely(!stepval)) + return (int) -1; + } else { + stepval = v; + } + v = NULL; + val = (int) 0; + mask = PyLong_FromLong((1L << chunk_size) - 1); if (unlikely(!mask)) goto done; + shift = PyLong_FromLong(chunk_size); if (unlikely(!shift)) goto done; + for (bits = 0; bits < (int) sizeof(int) * 8 - chunk_size; bits += chunk_size) { + PyObject *tmp, *digit; + long idigit; + digit = PyNumber_And(stepval, mask); + if (unlikely(!digit)) goto done; + idigit = PyLong_AsLong(digit); + Py_DECREF(digit); + if (unlikely(idigit < 0)) goto done; + val |= ((int) idigit) << bits; + tmp = PyNumber_Rshift(stepval, shift); + if (unlikely(!tmp)) goto done; + Py_DECREF(stepval); stepval = tmp; + } + Py_DECREF(shift); shift = NULL; + Py_DECREF(mask); mask = NULL; + { + long idigit = PyLong_AsLong(stepval); + if (unlikely(idigit < 0)) goto done; + remaining_bits = ((int) sizeof(int) * 8) - bits - (is_unsigned ? 0 : 1); + if (unlikely(idigit >= (1L << remaining_bits))) + goto raise_overflow; + val |= ((int) idigit) << bits; + } + if (!is_unsigned) { + if (unlikely(val & (((int) 1) << (sizeof(int) * 8 - 1)))) + goto raise_overflow; + if (is_negative) + val = ~val; + } + ret = 0; +done: + Py_XDECREF(shift); + Py_XDECREF(mask); + Py_XDECREF(stepval); +#endif + if (unlikely(ret)) + return (int) -1; + return val; +raise_overflow: + return __Pyx_raise_overflow___Pyx_PyLong_As_int(); +} +static CYTHON_INLINE int __Pyx_PyLong___Pyx_PyLong_As_int(PyObject *x) { +#ifdef __Pyx_HAS_GCC_DIAGNOSTIC +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wconversion" +#endif + const int neg_one = (int) -1, const_zero = (int) 0; +#ifdef __Pyx_HAS_GCC_DIAGNOSTIC +#pragma GCC diagnostic pop +#endif + const int is_unsigned = neg_one > const_zero; + if (is_unsigned) { + #if CYTHON_USE_PYLONG_INTERNALS + if (unlikely(__Pyx_PyLong_IsNeg(x))) { + goto raise_neg_overflow; + } else if (__Pyx_PyLong_IsCompact(x)) { + __PYX_VERIFY_RETURN_INT(int, __Pyx_compact_upylong, __Pyx_PyLong_CompactValueUnsigned(x)) + } else + #endif + { + return __Pyx_PyULong___Pyx_PyLong_As_int(x); + } + } else { + #if CYTHON_USE_PYLONG_INTERNALS + if (__Pyx_PyLong_IsCompact(x)) { + __PYX_VERIFY_RETURN_INT(int, __Pyx_compact_pylong, __Pyx_PyLong_CompactValue(x)) + } else + #endif + { + return __Pyx_PySLong___Pyx_PyLong_As_int(x); + } + } +#if CYTHON_USE_PYLONG_INTERNALS +raise_neg_overflow: + return __Pyx_raise_neg_overflow___Pyx_PyLong_As_int(); +raise_overflow: + return __Pyx_raise_overflow___Pyx_PyLong_As_int(); +#endif +} +static int __Pyx_NonPyLong___Pyx_PyLong_As_int(PyObject *x) { + int val; + PyObject *tmp = __Pyx_PyNumber_Long(x); + if (!tmp) return (int) -1; + val = __Pyx_PyLong_As_int(tmp); + Py_DECREF(tmp); + return val; +} +static CYTHON_INLINE int __Pyx_PyLong_As_int(PyObject *x) { + if (likely(PyLong_Check(x))) { + return __Pyx_PyLong___Pyx_PyLong_As_int(x); + } else { + return __Pyx_NonPyLong___Pyx_PyLong_As_int(x); + } +} + +/* PyObjectVectorcallKwds (used by PyObjectVectorcallMethodKwds) */ +#if CYTHON_VECTORCALL +CYTHON_UNUSED static int __Pyx_CheckVectorcallKwarg(PyObject *kwnames, Py_ssize_t i) { + PyObject *key = __Pyx_PyTuple_GET_ITEM(kwnames, i); +#if !CYTHON_ASSUME_SAFE_MACROS + if (unlikely(!key)) return -1; +#endif + if (unlikely(!PyUnicode_Check(key))) { + PyErr_SetString(PyExc_TypeError, "keywords must be strings"); + return -1; + } + return 0; +} +#else +CYTHON_UNUSED static PyObject *__Pyx_MakeKwargDict(PyObject **keys, PyObject **values, Py_ssize_t n) { + PyObject *out = PyDict_New(); + if (unlikely(!out)) return NULL; + for (Py_ssize_t i=0; i const_zero; + if (is_unsigned) { + if (sizeof(long) < sizeof(long)) { + return PyLong_FromLong((long) value); + } else if (sizeof(long) <= sizeof(unsigned long)) { + return PyLong_FromUnsignedLong((unsigned long) value); +#if !CYTHON_COMPILING_IN_PYPY + } else if (sizeof(long) <= sizeof(unsigned PY_LONG_LONG)) { + return PyLong_FromUnsignedLongLong((unsigned PY_LONG_LONG) value); +#endif + } + } else { + if (sizeof(long) <= sizeof(long)) { + return PyLong_FromLong((long) value); + } else if (sizeof(long) <= sizeof(PY_LONG_LONG)) { + return PyLong_FromLongLong((PY_LONG_LONG) value); + } + } + { + unsigned char *bytes = (unsigned char *)&value; +#if !CYTHON_COMPILING_IN_LIMITED_API && PY_VERSION_HEX >= 0x030d00A4 + if (is_unsigned) { + return PyLong_FromUnsignedNativeBytes(bytes, sizeof(value), -1); + } else { + return PyLong_FromNativeBytes(bytes, sizeof(value), -1); + } +#elif !CYTHON_COMPILING_IN_LIMITED_API && PY_VERSION_HEX < 0x030d0000 + int one = 1; int little = (int)*(unsigned char *)&one; + return _PyLong_FromByteArray(bytes, sizeof(long), + little, !is_unsigned); +#else + int one = 1; int little = (int)*(unsigned char *)&one; + PyObject *result = NULL, *kwds = NULL; + PyObject *py_bytes = NULL, *order_str = NULL, *from_bytes_str = NULL;; + py_bytes = PyBytes_FromStringAndSize((char*)bytes, sizeof(long)); + if (!py_bytes) goto limited_bad; + from_bytes_str = PyUnicode_FromStringAndSize("from_bytes", 10); + if (!from_bytes_str) goto limited_bad; + order_str = PyUnicode_FromString(little ? "little" : "big"); + if (!order_str) goto limited_bad; + { + PyObject *args[] = { (PyObject*)&PyLong_Type, py_bytes, order_str, Py_True }; + if (!is_unsigned) { + PyObject *signed_str = PyUnicode_FromStringAndSize("signed", 6); + if (!signed_str) goto limited_bad; +#if CYTHON_VECTORCALL + kwds = PyTuple_Pack(1, signed_str); +#else + { + PyObject *keys[] = {signed_str}; + PyObject *values[] = {Py_True}; + kwds = __Pyx_MakeKwargDict(keys, values, 1); + } +#endif + Py_DECREF(signed_str); + if (unlikely(!kwds)) goto limited_bad; + } + result = __Pyx_Object_VectorcallMethodKwds(from_bytes_str, args, 3 | __Pyx_PY_VECTORCALL_ARGUMENTS_OFFSET, kwds); + } + limited_bad: + Py_XDECREF(kwds); + Py_XDECREF(order_str); + Py_XDECREF(py_bytes); + Py_XDECREF(from_bytes_str); + return result; +#endif + } +} + +/* CIntToPy */ +static CYTHON_INLINE PyObject* __Pyx_PyLong_From_int(int value) { +#ifdef __Pyx_HAS_GCC_DIAGNOSTIC +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wconversion" +#endif + const int neg_one = (int) -1, const_zero = (int) 0; +#ifdef __Pyx_HAS_GCC_DIAGNOSTIC +#pragma GCC diagnostic pop +#endif + const int is_unsigned = neg_one > const_zero; + if (is_unsigned) { + if (sizeof(int) < sizeof(long)) { + return PyLong_FromLong((long) value); + } else if (sizeof(int) <= sizeof(unsigned long)) { + return PyLong_FromUnsignedLong((unsigned long) value); +#if !CYTHON_COMPILING_IN_PYPY + } else if (sizeof(int) <= sizeof(unsigned PY_LONG_LONG)) { + return PyLong_FromUnsignedLongLong((unsigned PY_LONG_LONG) value); +#endif + } + } else { + if (sizeof(int) <= sizeof(long)) { + return PyLong_FromLong((long) value); + } else if (sizeof(int) <= sizeof(PY_LONG_LONG)) { + return PyLong_FromLongLong((PY_LONG_LONG) value); + } + } + { + unsigned char *bytes = (unsigned char *)&value; +#if !CYTHON_COMPILING_IN_LIMITED_API && PY_VERSION_HEX >= 0x030d00A4 + if (is_unsigned) { + return PyLong_FromUnsignedNativeBytes(bytes, sizeof(value), -1); + } else { + return PyLong_FromNativeBytes(bytes, sizeof(value), -1); + } +#elif !CYTHON_COMPILING_IN_LIMITED_API && PY_VERSION_HEX < 0x030d0000 + int one = 1; int little = (int)*(unsigned char *)&one; + return _PyLong_FromByteArray(bytes, sizeof(int), + little, !is_unsigned); +#else + int one = 1; int little = (int)*(unsigned char *)&one; + PyObject *result = NULL, *kwds = NULL; + PyObject *py_bytes = NULL, *order_str = NULL, *from_bytes_str = NULL;; + py_bytes = PyBytes_FromStringAndSize((char*)bytes, sizeof(int)); + if (!py_bytes) goto limited_bad; + from_bytes_str = PyUnicode_FromStringAndSize("from_bytes", 10); + if (!from_bytes_str) goto limited_bad; + order_str = PyUnicode_FromString(little ? "little" : "big"); + if (!order_str) goto limited_bad; + { + PyObject *args[] = { (PyObject*)&PyLong_Type, py_bytes, order_str, Py_True }; + if (!is_unsigned) { + PyObject *signed_str = PyUnicode_FromStringAndSize("signed", 6); + if (!signed_str) goto limited_bad; +#if CYTHON_VECTORCALL + kwds = PyTuple_Pack(1, signed_str); +#else + { + PyObject *keys[] = {signed_str}; + PyObject *values[] = {Py_True}; + kwds = __Pyx_MakeKwargDict(keys, values, 1); + } +#endif + Py_DECREF(signed_str); + if (unlikely(!kwds)) goto limited_bad; + } + result = __Pyx_Object_VectorcallMethodKwds(from_bytes_str, args, 3 | __Pyx_PY_VECTORCALL_ARGUMENTS_OFFSET, kwds); + } + limited_bad: + Py_XDECREF(kwds); + Py_XDECREF(order_str); + Py_XDECREF(py_bytes); + Py_XDECREF(from_bytes_str); + return result; +#endif + } +} + +/* CIntFromPy */ +static long __Pyx_LargePyLong___Pyx_PyLong_As_long(PyObject *x); +static long __Pyx_raise_neg_overflow___Pyx_PyLong_As_long(void) { + const char* type_name = "long"; + PyErr_Format(PyExc_OverflowError, + "can't convert negative value to %.200s", type_name); + return (long) -1; +} +static long __Pyx_raise_overflow___Pyx_PyLong_As_long(void) { + const char* type_name = "long"; + PyErr_Format(PyExc_OverflowError, + "value too large to convert to %.200s", type_name); + return (long) -1; +} +static CYTHON_INLINE long __Pyx_PyULong___Pyx_PyLong_As_long(PyObject *x) { + const int is_unsigned = 1; +#if CYTHON_USE_PYLONG_INTERNALS + { + const digit* digits = __Pyx_PyLong_Digits(x); + const Py_ssize_t size = __Pyx_PyLong_DigitCount(x); + if (size == 2 && (8 * sizeof(long) > 1 * PyLong_SHIFT)) { + if ((8 * sizeof(unsigned long) > 2 * PyLong_SHIFT)) { + __PYX_VERIFY_RETURN_INT(long, unsigned long, (((((unsigned long)digits[1]) << PyLong_SHIFT) | (unsigned long)digits[0]))) + } else if ((8 * sizeof(long) >= 2 * PyLong_SHIFT)) { + return (long) (((((long)digits[1]) << PyLong_SHIFT) | (long)digits[0])); + } + } else + if (size == 3 && (8 * sizeof(long) > 2 * PyLong_SHIFT)) { + if ((8 * sizeof(unsigned long) > 3 * PyLong_SHIFT)) { + __PYX_VERIFY_RETURN_INT(long, unsigned long, (((((((unsigned long)digits[2]) << PyLong_SHIFT) | (unsigned long)digits[1]) << PyLong_SHIFT) | (unsigned long)digits[0]))) + } else if ((8 * sizeof(long) >= 3 * PyLong_SHIFT)) { + return (long) (((((((long)digits[2]) << PyLong_SHIFT) | (long)digits[1]) << PyLong_SHIFT) | (long)digits[0])); + } + } else + if (size == 4 && (8 * sizeof(long) > 3 * PyLong_SHIFT)) { + if ((8 * sizeof(unsigned long) > 4 * PyLong_SHIFT)) { + __PYX_VERIFY_RETURN_INT(long, unsigned long, (((((((((unsigned long)digits[3]) << PyLong_SHIFT) | (unsigned long)digits[2]) << PyLong_SHIFT) | (unsigned long)digits[1]) << PyLong_SHIFT) | (unsigned long)digits[0]))) + } else if ((8 * sizeof(long) >= 4 * PyLong_SHIFT)) { + return (long) (((((((((long)digits[3]) << PyLong_SHIFT) | (long)digits[2]) << PyLong_SHIFT) | (long)digits[1]) << PyLong_SHIFT) | (long)digits[0])); + } + } else + {} + } +#elif CYTHON_COMPILING_IN_CPYTHON && PY_VERSION_HEX < 0x030C00A7 + if (unlikely(Py_SIZE(x) < 0)) { + goto raise_neg_overflow; + } +#else + { + int result = PyObject_RichCompareBool(x, Py_False, Py_LT); + if (unlikely(result < 0)) + return (long) -1; + if (unlikely(result == 1)) + goto raise_neg_overflow; + } +#endif + if ((sizeof(long) <= sizeof(unsigned long))) { + __PYX_VERIFY_RETURN_INT_EXC(long, unsigned long, PyLong_AsUnsignedLong(x)) + } else if ((sizeof(long) <= sizeof(unsigned PY_LONG_LONG))) { + __PYX_VERIFY_RETURN_INT_EXC(long, unsigned PY_LONG_LONG, PyLong_AsUnsignedLongLong(x)) + } + return __Pyx_LargePyLong___Pyx_PyLong_As_long(x); +raise_neg_overflow: + return __Pyx_raise_neg_overflow___Pyx_PyLong_As_long(); +raise_overflow: + return __Pyx_raise_overflow___Pyx_PyLong_As_long(); +} +static CYTHON_INLINE long __Pyx_PySLong___Pyx_PyLong_As_long(PyObject *x) { + const int is_unsigned = 0; +#if CYTHON_USE_PYLONG_INTERNALS + if (__Pyx_PyLong_IsNeg(x)) { + const Py_ssize_t size = __Pyx_PyLong_DigitCount(x); + const digit* digits = __Pyx_PyLong_Digits(x); + if (size == 2 && (8 * sizeof(long) > 1 * PyLong_SHIFT)) { + if ((8 * sizeof(long) > 2 * PyLong_SHIFT)) { + long ival = - (long) (((((unsigned long)digits[1]) << PyLong_SHIFT) | (unsigned long)digits[0])); + __PYX_VERIFY_RETURN_INT(long, long, ival) + } else if ((8 * sizeof(long) - 1 > 2 * PyLong_SHIFT)) { + return (long) (((long) -1) * (((((long)digits[1]) << PyLong_SHIFT) | (long)digits[0]))); + } + } else + if (size == 3 && (8 * sizeof(long) > 2 * PyLong_SHIFT)) { + if ((8 * sizeof(long) > 3 * PyLong_SHIFT)) { + long ival = - (long) (((((((unsigned long)digits[2]) << PyLong_SHIFT) | (unsigned long)digits[1]) << PyLong_SHIFT) | (unsigned long)digits[0])); + __PYX_VERIFY_RETURN_INT(long, long, ival) + } else if ((8 * sizeof(long) - 1 > 3 * PyLong_SHIFT)) { + return (long) (((long) -1) * (((((((long)digits[2]) << PyLong_SHIFT) | (long)digits[1]) << PyLong_SHIFT) | (long)digits[0]))); + } + } else + if (size == 4 && (8 * sizeof(long) > 3 * PyLong_SHIFT)) { + if ((8 * sizeof(long) > 4 * PyLong_SHIFT)) { + long ival = - (long) (((((((((unsigned long)digits[3]) << PyLong_SHIFT) | (unsigned long)digits[2]) << PyLong_SHIFT) | (unsigned long)digits[1]) << PyLong_SHIFT) | (unsigned long)digits[0])); + __PYX_VERIFY_RETURN_INT(long, long, ival) + } else if ((8 * sizeof(long) - 1 > 4 * PyLong_SHIFT)) { + return (long) (((long) -1) * (((((((((long)digits[3]) << PyLong_SHIFT) | (long)digits[2]) << PyLong_SHIFT) | (long)digits[1]) << PyLong_SHIFT) | (long)digits[0]))); + } + } else + {} + } else { + const Py_ssize_t size = __Pyx_PyLong_DigitCount(x); + const digit* digits = __Pyx_PyLong_Digits(x); + if (size == 2 && (8 * sizeof(long) > 1 * PyLong_SHIFT)) { + if ((8 * sizeof(long) > 2 * PyLong_SHIFT)) { + __PYX_VERIFY_RETURN_INT(long, unsigned long, (((((unsigned long)digits[1]) << PyLong_SHIFT) | (unsigned long)digits[0]))) + } else if ((8 * sizeof(long) - 1 > 2 * PyLong_SHIFT)) { + return (long) (((((long)digits[1]) << PyLong_SHIFT) | (long)digits[0])); + } + } else + if (size == 3 && (8 * sizeof(long) > 2 * PyLong_SHIFT)) { + if ((8 * sizeof(long) > 3 * PyLong_SHIFT)) { + __PYX_VERIFY_RETURN_INT(long, unsigned long, (((((((unsigned long)digits[2]) << PyLong_SHIFT) | (unsigned long)digits[1]) << PyLong_SHIFT) | (unsigned long)digits[0]))) + } else if ((8 * sizeof(long) - 1 > 3 * PyLong_SHIFT)) { + return (long) (((((((long)digits[2]) << PyLong_SHIFT) | (long)digits[1]) << PyLong_SHIFT) | (long)digits[0])); + } + } else + if (size == 4 && (8 * sizeof(long) > 3 * PyLong_SHIFT)) { + if ((8 * sizeof(long) > 4 * PyLong_SHIFT)) { + __PYX_VERIFY_RETURN_INT(long, unsigned long, (((((((((unsigned long)digits[3]) << PyLong_SHIFT) | (unsigned long)digits[2]) << PyLong_SHIFT) | (unsigned long)digits[1]) << PyLong_SHIFT) | (unsigned long)digits[0]))) + } else if ((8 * sizeof(long) - 1 > 4 * PyLong_SHIFT)) { + return (long) (((((((((long)digits[3]) << PyLong_SHIFT) | (long)digits[2]) << PyLong_SHIFT) | (long)digits[1]) << PyLong_SHIFT) | (long)digits[0])); + } + } else + {} + } +#endif + #if __PYX_LIMITED_VERSION_HEX >= 0x030d0000 + if ((sizeof(long) <= sizeof(int)) && (sizeof(int) < sizeof(long))) { + __PYX_VERIFY_RETURN_INT_EXC(long, int, PyLong_AsInt(x)) + } else + #endif + if ((sizeof(long) <= sizeof(long))) { + __PYX_VERIFY_RETURN_INT_EXC(long, long, PyLong_AsLong(x)) + } else if ((sizeof(long) <= sizeof(PY_LONG_LONG))) { + __PYX_VERIFY_RETURN_INT_EXC(long, PY_LONG_LONG, PyLong_AsLongLong(x)) + } + return __Pyx_LargePyLong___Pyx_PyLong_As_long(x); +raise_neg_overflow: + return __Pyx_raise_neg_overflow___Pyx_PyLong_As_long(); +raise_overflow: + return __Pyx_raise_overflow___Pyx_PyLong_As_long(); +} +static long __Pyx_LargePyLong___Pyx_PyLong_As_long(PyObject *x) { +#ifdef __Pyx_HAS_GCC_DIAGNOSTIC +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wconversion" +#endif + const long neg_one = (long) -1, const_zero = (long) 0; +#ifdef __Pyx_HAS_GCC_DIAGNOSTIC +#pragma GCC diagnostic pop +#endif + const int is_unsigned = neg_one > const_zero; + long val; + int ret = -1; +#if PY_VERSION_HEX >= 0x030d00A6 && !CYTHON_COMPILING_IN_LIMITED_API + Py_ssize_t bytes_copied = PyLong_AsNativeBytes( + x, &val, sizeof(val), Py_ASNATIVEBYTES_NATIVE_ENDIAN | (is_unsigned ? Py_ASNATIVEBYTES_UNSIGNED_BUFFER | Py_ASNATIVEBYTES_REJECT_NEGATIVE : 0)); + if (unlikely(bytes_copied == -1)) { + } else if (unlikely(bytes_copied > (Py_ssize_t) sizeof(val))) { + goto raise_overflow; + } else { + ret = 0; + } +#elif PY_VERSION_HEX < 0x030d0000 && !(CYTHON_COMPILING_IN_PYPY || CYTHON_COMPILING_IN_LIMITED_API) || defined(_PyLong_AsByteArray) + int one = 1; int is_little = (int)*(unsigned char *)&one; + unsigned char *bytes = (unsigned char *)&val; + ret = _PyLong_AsByteArray((PyLongObject *)x, + bytes, sizeof(val), + is_little, !is_unsigned); + if ((0)) goto raise_overflow; +#else + PyObject *v; + PyObject *stepval = NULL, *mask = NULL, *shift = NULL; + int bits, remaining_bits, is_negative = 0; + int chunk_size = (sizeof(long) < 8) ? 30 : 62; + if (likely(PyLong_CheckExact(x))) { + v = __Pyx_NewRef(x); + } else { + v = PyNumber_Long(x); + if (unlikely(!v)) return (long) -1; + assert(PyLong_CheckExact(v)); + } + { + int result = PyObject_RichCompareBool(v, Py_False, Py_LT); + if (unlikely(result < 0)) { + Py_DECREF(v); + return (long) -1; + } + is_negative = result == 1; + } + if (is_unsigned && unlikely(is_negative)) { + Py_DECREF(v); + PyErr_SetString(PyExc_OverflowError, + "can't convert negative value to long"); + return (long) -1; + } else if (is_negative) { + stepval = PyNumber_Invert(v); + Py_DECREF(v); + if (unlikely(!stepval)) + return (long) -1; + } else { + stepval = v; + } + v = NULL; + val = (long) 0; + mask = PyLong_FromLong((1L << chunk_size) - 1); if (unlikely(!mask)) goto done; + shift = PyLong_FromLong(chunk_size); if (unlikely(!shift)) goto done; + for (bits = 0; bits < (int) sizeof(long) * 8 - chunk_size; bits += chunk_size) { + PyObject *tmp, *digit; + long idigit; + digit = PyNumber_And(stepval, mask); + if (unlikely(!digit)) goto done; + idigit = PyLong_AsLong(digit); + Py_DECREF(digit); + if (unlikely(idigit < 0)) goto done; + val |= ((long) idigit) << bits; + tmp = PyNumber_Rshift(stepval, shift); + if (unlikely(!tmp)) goto done; + Py_DECREF(stepval); stepval = tmp; + } + Py_DECREF(shift); shift = NULL; + Py_DECREF(mask); mask = NULL; + { + long idigit = PyLong_AsLong(stepval); + if (unlikely(idigit < 0)) goto done; + remaining_bits = ((int) sizeof(long) * 8) - bits - (is_unsigned ? 0 : 1); + if (unlikely(idigit >= (1L << remaining_bits))) + goto raise_overflow; + val |= ((long) idigit) << bits; + } + if (!is_unsigned) { + if (unlikely(val & (((long) 1) << (sizeof(long) * 8 - 1)))) + goto raise_overflow; + if (is_negative) + val = ~val; + } + ret = 0; +done: + Py_XDECREF(shift); + Py_XDECREF(mask); + Py_XDECREF(stepval); +#endif + if (unlikely(ret)) + return (long) -1; + return val; +raise_overflow: + return __Pyx_raise_overflow___Pyx_PyLong_As_long(); +} +static CYTHON_INLINE long __Pyx_PyLong___Pyx_PyLong_As_long(PyObject *x) { +#ifdef __Pyx_HAS_GCC_DIAGNOSTIC +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wconversion" +#endif + const long neg_one = (long) -1, const_zero = (long) 0; +#ifdef __Pyx_HAS_GCC_DIAGNOSTIC +#pragma GCC diagnostic pop +#endif + const int is_unsigned = neg_one > const_zero; + if (is_unsigned) { + #if CYTHON_USE_PYLONG_INTERNALS + if (unlikely(__Pyx_PyLong_IsNeg(x))) { + goto raise_neg_overflow; + } else if (__Pyx_PyLong_IsCompact(x)) { + __PYX_VERIFY_RETURN_INT(long, __Pyx_compact_upylong, __Pyx_PyLong_CompactValueUnsigned(x)) + } else + #endif + { + return __Pyx_PyULong___Pyx_PyLong_As_long(x); + } + } else { + #if CYTHON_USE_PYLONG_INTERNALS + if (__Pyx_PyLong_IsCompact(x)) { + __PYX_VERIFY_RETURN_INT(long, __Pyx_compact_pylong, __Pyx_PyLong_CompactValue(x)) + } else + #endif + { + return __Pyx_PySLong___Pyx_PyLong_As_long(x); + } + } +#if CYTHON_USE_PYLONG_INTERNALS +raise_neg_overflow: + return __Pyx_raise_neg_overflow___Pyx_PyLong_As_long(); +raise_overflow: + return __Pyx_raise_overflow___Pyx_PyLong_As_long(); +#endif +} +static long __Pyx_NonPyLong___Pyx_PyLong_As_long(PyObject *x) { + long val; + PyObject *tmp = __Pyx_PyNumber_Long(x); + if (!tmp) return (long) -1; + val = __Pyx_PyLong_As_long(tmp); + Py_DECREF(tmp); + return val; +} +static CYTHON_INLINE long __Pyx_PyLong_As_long(PyObject *x) { + if (likely(PyLong_Check(x))) { + return __Pyx_PyLong___Pyx_PyLong_As_long(x); + } else { + return __Pyx_NonPyLong___Pyx_PyLong_As_long(x); + } +} + +/* GetRuntimeVersion */ +#if __PYX_LIMITED_VERSION_HEX < 0x030b0000 +void __Pyx_init_runtime_version(void) { + if (__Pyx_cached_runtime_version == 0) { + const char* rt_version = Py_GetVersion(); + unsigned long version = 0; + unsigned long factor = 0x01000000UL; + unsigned int digit = 0; + int i = 0; + while (factor) { + while ('0' <= rt_version[i] && rt_version[i] <= '9') { + digit = digit * 10 + (unsigned int) (rt_version[i] - '0'); + ++i; + } + version += factor * digit; + if (rt_version[i] != '.') + break; + digit = 0; + factor >>= 8; + ++i; + } + __Pyx_cached_runtime_version = version; + } +} +#endif +static unsigned long __Pyx_get_runtime_version(void) { +#if __PYX_LIMITED_VERSION_HEX >= 0x030b0000 + return Py_Version & ~0xFFUL; +#else + return __Pyx_cached_runtime_version; +#endif +} + +/* SwapException (used by CoroutineBase) */ +#if CYTHON_FAST_THREAD_STATE +static CYTHON_INLINE void __Pyx__ExceptionSwap(PyThreadState *tstate, PyObject **type, PyObject **value, PyObject **tb) { + PyObject *tmp_type, *tmp_value, *tmp_tb; + #if CYTHON_USE_EXC_INFO_STACK && PY_VERSION_HEX >= 0x030B00a4 + _PyErr_StackItem *exc_info = tstate->exc_info; + tmp_value = exc_info->exc_value; + exc_info->exc_value = *value; + if (tmp_value == NULL || tmp_value == Py_None) { + Py_XDECREF(tmp_value); + tmp_value = NULL; + tmp_type = NULL; + tmp_tb = NULL; + } else { + tmp_type = (PyObject*) Py_TYPE(tmp_value); + Py_INCREF(tmp_type); + #if CYTHON_COMPILING_IN_CPYTHON + tmp_tb = ((PyBaseExceptionObject*) tmp_value)->traceback; + Py_XINCREF(tmp_tb); + #else + tmp_tb = PyException_GetTraceback(tmp_value); + #endif + } + #elif CYTHON_USE_EXC_INFO_STACK + _PyErr_StackItem *exc_info = tstate->exc_info; + tmp_type = exc_info->exc_type; + tmp_value = exc_info->exc_value; + tmp_tb = exc_info->exc_traceback; + exc_info->exc_type = *type; + exc_info->exc_value = *value; + exc_info->exc_traceback = *tb; + #else + tmp_type = tstate->exc_type; + tmp_value = tstate->exc_value; + tmp_tb = tstate->exc_traceback; + tstate->exc_type = *type; + tstate->exc_value = *value; + tstate->exc_traceback = *tb; + #endif + *type = tmp_type; + *value = tmp_value; + *tb = tmp_tb; +} +#else +static CYTHON_INLINE void __Pyx_ExceptionSwap(PyObject **type, PyObject **value, PyObject **tb) { + PyObject *tmp_type, *tmp_value, *tmp_tb; + PyErr_GetExcInfo(&tmp_type, &tmp_value, &tmp_tb); + PyErr_SetExcInfo(*type, *value, *tb); + *type = tmp_type; + *value = tmp_value; + *tb = tmp_tb; +} +#endif + +/* PyObjectCallMethod1 (used by CoroutineBase) */ +static CYTHON_INLINE PyObject* __Pyx_PyObject_CallMethod1(PyObject* obj, PyObject* method_name, PyObject* arg) { +#if CYTHON_VECTORCALL && (__PYX_LIMITED_VERSION_HEX >= 0x030C0000 || !CYTHON_COMPILING_IN_LIMITED_API) + PyObject *args[2] = {obj, arg}; + return PyObject_VectorcallMethod(method_name, args, 2 | PY_VECTORCALL_ARGUMENTS_OFFSET, NULL); +#elif CYTHON_COMPILING_IN_LIMITED_API + return PyObject_CallMethodObjArgs(obj, method_name, arg, NULL); +#else + return PyObject_CallMethodOneArg(obj, method_name, arg); +#endif +} + +/* PyObjectCallNoArg (used by CoroutineBase) */ +static CYTHON_INLINE PyObject* __Pyx_PyObject_CallNoArg(PyObject *func) { + PyObject *arg[2] = {NULL, NULL}; + return __Pyx_PyObject_FastCall(func, arg + 1, 0 | __Pyx_PY_VECTORCALL_ARGUMENTS_OFFSET); +} + +/* ReturnWithStopIteration (used by CoroutineBase) */ +static void __Pyx__ReturnWithStopIteration(PyObject* value, int async); +static CYTHON_INLINE void __Pyx_ReturnWithStopIteration(PyObject* value, int async, int iternext) { + if (value == Py_None) { + if (async || !iternext) + PyErr_SetNone(async ? PyExc_StopAsyncIteration : PyExc_StopIteration); + return; + } + __Pyx__ReturnWithStopIteration(value, async); +} +static void __Pyx__ReturnWithStopIteration(PyObject* value, int async) { +#if CYTHON_COMPILING_IN_CPYTHON + __Pyx_PyThreadState_declare +#endif + PyObject *exc; + PyObject *exc_type = async ? PyExc_StopAsyncIteration : PyExc_StopIteration; +#if CYTHON_COMPILING_IN_CPYTHON + if ((PY_VERSION_HEX >= (0x030C00A6)) || unlikely(PyTuple_Check(value) || PyExceptionInstance_Check(value))) { + if (PY_VERSION_HEX >= (0x030e00A1)) { + exc = __Pyx_PyObject_CallOneArg(exc_type, value); + } else { + PyObject *args_tuple = PyTuple_New(1); + if (unlikely(!args_tuple)) return; + Py_INCREF(value); + PyTuple_SET_ITEM(args_tuple, 0, value); + exc = PyObject_Call(exc_type, args_tuple, NULL); + Py_DECREF(args_tuple); + } + if (unlikely(!exc)) return; + } else { + Py_INCREF(value); + exc = value; + } + #if CYTHON_FAST_THREAD_STATE + __Pyx_PyThreadState_assign + #if CYTHON_USE_EXC_INFO_STACK + if (!__pyx_tstate->exc_info->exc_value) + #else + if (!__pyx_tstate->exc_type) + #endif + { + Py_INCREF(exc_type); + __Pyx_ErrRestore(exc_type, exc, NULL); + return; + } + #endif +#else + exc = __Pyx_PyObject_CallOneArg(exc_type, value); + if (unlikely(!exc)) return; +#endif + PyErr_SetObject(exc_type, exc); + Py_DECREF(exc); +} + +/* CoroutineBase (used by Generator) */ +#if !CYTHON_COMPILING_IN_LIMITED_API +#include +#if PY_VERSION_HEX >= 0x030b00a6 && !defined(PYPY_VERSION) + #ifndef Py_BUILD_CORE + #define Py_BUILD_CORE 1 + #endif + #include "internal/pycore_frame.h" +#endif +#endif // CYTHON_COMPILING_IN_LIMITED_API +static CYTHON_INLINE void +__Pyx_Coroutine_Undelegate(__pyx_CoroutineObject *gen) { +#if CYTHON_USE_AM_SEND + gen->yieldfrom_am_send = NULL; +#endif + Py_CLEAR(gen->yieldfrom); +} +static int __Pyx_PyGen__FetchStopIterationValue(PyThreadState *__pyx_tstate, PyObject **pvalue) { + PyObject *et, *ev, *tb; + PyObject *value = NULL; + CYTHON_UNUSED_VAR(__pyx_tstate); + __Pyx_ErrFetch(&et, &ev, &tb); + if (!et) { + Py_XDECREF(tb); + Py_XDECREF(ev); + Py_INCREF(Py_None); + *pvalue = Py_None; + return 0; + } + if (likely(et == PyExc_StopIteration)) { + if (!ev) { + Py_INCREF(Py_None); + value = Py_None; + } + else if (likely(Py_IS_TYPE(ev, (PyTypeObject*)PyExc_StopIteration))) { + #if CYTHON_COMPILING_IN_LIMITED_API || CYTHON_COMPILING_IN_GRAAL + value = PyObject_GetAttr(ev, __pyx_mstate_global->__pyx_n_u_value); + if (unlikely(!value)) goto limited_api_failure; + #else + value = ((PyStopIterationObject *)ev)->value; + Py_INCREF(value); + #endif + Py_DECREF(ev); + } + else if (unlikely(PyTuple_Check(ev))) { + Py_ssize_t tuple_size = __Pyx_PyTuple_GET_SIZE(ev); + #if !CYTHON_ASSUME_SAFE_SIZE + if (unlikely(tuple_size < 0)) { + Py_XDECREF(tb); + Py_DECREF(ev); + Py_DECREF(et); + return -1; + } + #endif + if (tuple_size >= 1) { +#if CYTHON_ASSUME_SAFE_MACROS && !CYTHON_AVOID_BORROWED_REFS + value = PyTuple_GET_ITEM(ev, 0); + Py_INCREF(value); +#elif CYTHON_ASSUME_SAFE_MACROS + value = PySequence_ITEM(ev, 0); +#else + value = PySequence_GetItem(ev, 0); + if (!value) goto limited_api_failure; +#endif + } else { + Py_INCREF(Py_None); + value = Py_None; + } + Py_DECREF(ev); + } + else if (!__Pyx_TypeCheck(ev, (PyTypeObject*)PyExc_StopIteration)) { + value = ev; + } + if (likely(value)) { + Py_XDECREF(tb); + Py_DECREF(et); + *pvalue = value; + return 0; + } + } else if (!__Pyx_PyErr_GivenExceptionMatches(et, PyExc_StopIteration)) { + __Pyx_ErrRestore(et, ev, tb); + return -1; + } + PyErr_NormalizeException(&et, &ev, &tb); + if (unlikely(!PyObject_TypeCheck(ev, (PyTypeObject*)PyExc_StopIteration))) { + __Pyx_ErrRestore(et, ev, tb); + return -1; + } + Py_XDECREF(tb); + Py_DECREF(et); +#if CYTHON_COMPILING_IN_LIMITED_API + value = PyObject_GetAttr(ev, __pyx_mstate_global->__pyx_n_u_value); +#else + value = ((PyStopIterationObject *)ev)->value; + Py_INCREF(value); +#endif + Py_DECREF(ev); +#if CYTHON_COMPILING_IN_LIMITED_API + if (unlikely(!value)) return -1; +#endif + *pvalue = value; + return 0; +#if CYTHON_COMPILING_IN_LIMITED_API || CYTHON_COMPILING_IN_GRAAL || !CYTHON_ASSUME_SAFE_MACROS + limited_api_failure: + Py_XDECREF(et); + Py_XDECREF(tb); + Py_XDECREF(ev); + return -1; +#endif +} +static CYTHON_INLINE +__Pyx_PySendResult __Pyx_Coroutine_status_from_result(PyObject **retval) { + if (*retval) { + return PYGEN_NEXT; + } else if (likely(__Pyx_PyGen__FetchStopIterationValue(__Pyx_PyThreadState_Current, retval) == 0)) { + return PYGEN_RETURN; + } else { + return PYGEN_ERROR; + } +} +static CYTHON_INLINE +void __Pyx_Coroutine_ExceptionClear(__Pyx_ExcInfoStruct *exc_state) { +#if PY_VERSION_HEX >= 0x030B00a4 + Py_CLEAR(exc_state->exc_value); +#else + PyObject *t, *v, *tb; + t = exc_state->exc_type; + v = exc_state->exc_value; + tb = exc_state->exc_traceback; + exc_state->exc_type = NULL; + exc_state->exc_value = NULL; + exc_state->exc_traceback = NULL; + Py_XDECREF(t); + Py_XDECREF(v); + Py_XDECREF(tb); +#endif +} +#define __Pyx_Coroutine_AlreadyRunningError(gen) (__Pyx__Coroutine_AlreadyRunningError(gen), (PyObject*)NULL) +static void __Pyx__Coroutine_AlreadyRunningError(__pyx_CoroutineObject *gen) { + const char *msg; + CYTHON_MAYBE_UNUSED_VAR(gen); + if ((0)) { + #ifdef __Pyx_Coroutine_USED + } else if (__Pyx_Coroutine_Check((PyObject*)gen)) { + msg = "coroutine already executing"; + #endif + #ifdef __Pyx_AsyncGen_USED + } else if (__Pyx_AsyncGen_CheckExact((PyObject*)gen)) { + msg = "async generator already executing"; + #endif + } else { + msg = "generator already executing"; + } + PyErr_SetString(PyExc_ValueError, msg); +} +static void __Pyx_Coroutine_AlreadyTerminatedError(PyObject *gen, PyObject *value, int closing) { + CYTHON_MAYBE_UNUSED_VAR(gen); + CYTHON_MAYBE_UNUSED_VAR(closing); + #ifdef __Pyx_Coroutine_USED + if (!closing && __Pyx_Coroutine_Check(gen)) { + PyErr_SetString(PyExc_RuntimeError, "cannot reuse already awaited coroutine"); + } else + #endif + if (value) { + #ifdef __Pyx_AsyncGen_USED + if (__Pyx_AsyncGen_CheckExact(gen)) + PyErr_SetNone(PyExc_StopAsyncIteration); + else + #endif + PyErr_SetNone(PyExc_StopIteration); + } +} +static +__Pyx_PySendResult __Pyx_Coroutine_SendEx(__pyx_CoroutineObject *self, PyObject *value, PyObject **result, int closing) { + __Pyx_PyThreadState_declare + PyThreadState *tstate; + __Pyx_ExcInfoStruct *exc_state; + PyObject *retval; + assert(__Pyx_Coroutine_get_is_running(self)); // Callers should ensure is_running + if (unlikely(self->resume_label == -1)) { + __Pyx_Coroutine_AlreadyTerminatedError((PyObject*)self, value, closing); + return PYGEN_ERROR; + } +#if CYTHON_FAST_THREAD_STATE + __Pyx_PyThreadState_assign + tstate = __pyx_tstate; +#else + tstate = __Pyx_PyThreadState_Current; +#endif + exc_state = &self->gi_exc_state; + if (exc_state->exc_value) { + #if CYTHON_COMPILING_IN_LIMITED_API || CYTHON_COMPILING_IN_PYPY + #else + PyObject *exc_tb; + #if PY_VERSION_HEX >= 0x030B00a4 && !CYTHON_COMPILING_IN_CPYTHON + exc_tb = PyException_GetTraceback(exc_state->exc_value); + #elif PY_VERSION_HEX >= 0x030B00a4 + exc_tb = ((PyBaseExceptionObject*) exc_state->exc_value)->traceback; + #else + exc_tb = exc_state->exc_traceback; + #endif + if (exc_tb) { + PyTracebackObject *tb = (PyTracebackObject *) exc_tb; + PyFrameObject *f = tb->tb_frame; + assert(f->f_back == NULL); + #if PY_VERSION_HEX >= 0x030B00A1 + f->f_back = PyThreadState_GetFrame(tstate); + #else + Py_XINCREF(tstate->frame); + f->f_back = tstate->frame; + #endif + #if PY_VERSION_HEX >= 0x030B00a4 && !CYTHON_COMPILING_IN_CPYTHON + Py_DECREF(exc_tb); + #endif + } + #endif + } +#if CYTHON_USE_EXC_INFO_STACK + exc_state->previous_item = tstate->exc_info; + tstate->exc_info = exc_state; +#else + if (exc_state->exc_type) { + __Pyx_ExceptionSwap(&exc_state->exc_type, &exc_state->exc_value, &exc_state->exc_traceback); + } else { + __Pyx_Coroutine_ExceptionClear(exc_state); + __Pyx_ExceptionSave(&exc_state->exc_type, &exc_state->exc_value, &exc_state->exc_traceback); + } +#endif + retval = self->body(self, tstate, value); +#if CYTHON_USE_EXC_INFO_STACK + exc_state = &self->gi_exc_state; + tstate->exc_info = exc_state->previous_item; + exc_state->previous_item = NULL; + __Pyx_Coroutine_ResetFrameBackpointer(exc_state); +#endif + *result = retval; + if (self->resume_label == -1) { + return likely(retval) ? PYGEN_RETURN : PYGEN_ERROR; + } + return PYGEN_NEXT; +} +static CYTHON_INLINE void __Pyx_Coroutine_ResetFrameBackpointer(__Pyx_ExcInfoStruct *exc_state) { +#if CYTHON_COMPILING_IN_PYPY || CYTHON_COMPILING_IN_LIMITED_API + CYTHON_UNUSED_VAR(exc_state); +#else + PyObject *exc_tb; + #if PY_VERSION_HEX >= 0x030B00a4 + if (!exc_state->exc_value) return; + exc_tb = PyException_GetTraceback(exc_state->exc_value); + #else + exc_tb = exc_state->exc_traceback; + #endif + if (likely(exc_tb)) { + PyTracebackObject *tb = (PyTracebackObject *) exc_tb; + PyFrameObject *f = tb->tb_frame; + Py_CLEAR(f->f_back); + #if PY_VERSION_HEX >= 0x030B00a4 + Py_DECREF(exc_tb); + #endif + } +#endif +} +#define __Pyx_Coroutine_MethodReturnFromResult(gen, result, retval, iternext)\ + ((result) == PYGEN_NEXT ? (retval) : __Pyx__Coroutine_MethodReturnFromResult(gen, result, retval, iternext)) +static PyObject * +__Pyx__Coroutine_MethodReturnFromResult(PyObject* gen, __Pyx_PySendResult result, PyObject *retval, int iternext) { + CYTHON_MAYBE_UNUSED_VAR(gen); + if (likely(result == PYGEN_RETURN)) { + int is_async = 0; + #ifdef __Pyx_AsyncGen_USED + is_async = __Pyx_AsyncGen_CheckExact(gen); + #endif + __Pyx_ReturnWithStopIteration(retval, is_async, iternext); + Py_XDECREF(retval); + } + return NULL; +} +#if CYTHON_COMPILING_IN_CPYTHON +static CYTHON_INLINE +PyObject *__Pyx_PyGen_Send(PyGenObject *gen, PyObject *arg) { +#if PY_VERSION_HEX <= 0x030A00A1 + return _PyGen_Send(gen, arg); +#else + PyObject *result; + if (PyIter_Send((PyObject*)gen, arg ? arg : Py_None, &result) == PYGEN_RETURN) { + if (PyAsyncGen_CheckExact(gen)) { + assert(result == Py_None); + PyErr_SetNone(PyExc_StopAsyncIteration); + } + else if (result == Py_None) { + PyErr_SetNone(PyExc_StopIteration); + } + else { +#if PY_VERSION_HEX < 0x030d00A1 + _PyGen_SetStopIterationValue(result); +#else + if (!PyTuple_Check(result) && !PyExceptionInstance_Check(result)) { + PyErr_SetObject(PyExc_StopIteration, result); + } else { + PyObject *exc = __Pyx_PyObject_CallOneArg(PyExc_StopIteration, result); + if (likely(exc != NULL)) { + PyErr_SetObject(PyExc_StopIteration, exc); + Py_DECREF(exc); + } + } +#endif + } + Py_DECREF(result); + result = NULL; + } + return result; +#endif +} +#endif +static CYTHON_INLINE __Pyx_PySendResult +__Pyx_Coroutine_FinishDelegation(__pyx_CoroutineObject *gen, PyObject** retval) { + __Pyx_PySendResult result; + PyObject *val = NULL; + assert(__Pyx_Coroutine_get_is_running(gen)); + __Pyx_Coroutine_Undelegate(gen); + __Pyx_PyGen__FetchStopIterationValue(__Pyx_PyThreadState_Current, &val); + result = __Pyx_Coroutine_SendEx(gen, val, retval, 0); + Py_XDECREF(val); + return result; +} +#if CYTHON_USE_AM_SEND +static __Pyx_PySendResult +__Pyx_Coroutine_SendToDelegate(__pyx_CoroutineObject *gen, __Pyx_pyiter_sendfunc gen_am_send, PyObject *value, PyObject **retval) { + PyObject *ret = NULL; + __Pyx_PySendResult delegate_result, result; + assert(__Pyx_Coroutine_get_is_running(gen)); + delegate_result = gen_am_send(gen->yieldfrom, value, &ret); + if (delegate_result == PYGEN_NEXT) { + assert (ret != NULL); + *retval = ret; + return PYGEN_NEXT; + } + assert (delegate_result != PYGEN_ERROR || ret == NULL); + __Pyx_Coroutine_Undelegate(gen); + result = __Pyx_Coroutine_SendEx(gen, ret, retval, 0); + Py_XDECREF(ret); + return result; +} +#endif +static PyObject *__Pyx_Coroutine_Send(PyObject *self, PyObject *value) { + PyObject *retval = NULL; + __Pyx_PySendResult result = __Pyx_Coroutine_AmSend(self, value, &retval); + return __Pyx_Coroutine_MethodReturnFromResult(self, result, retval, 0); +} +static __Pyx_PySendResult +__Pyx_Coroutine_AmSend(PyObject *self, PyObject *value, PyObject **retval) { + __Pyx_PySendResult result; + __pyx_CoroutineObject *gen = (__pyx_CoroutineObject*) self; + if (unlikely(__Pyx_Coroutine_test_and_set_is_running(gen))) { + *retval = __Pyx_Coroutine_AlreadyRunningError(gen); + return PYGEN_ERROR; + } + #if CYTHON_USE_AM_SEND + if (gen->yieldfrom_am_send) { + result = __Pyx_Coroutine_SendToDelegate(gen, gen->yieldfrom_am_send, value, retval); + } else + #endif + if (gen->yieldfrom) { + PyObject *yf = gen->yieldfrom; + PyObject *ret; + #if !CYTHON_USE_AM_SEND + #ifdef __Pyx_Generator_USED + if (__Pyx_Generator_CheckExact(yf)) { + ret = __Pyx_Coroutine_Send(yf, value); + } else + #endif + #ifdef __Pyx_Coroutine_USED + if (__Pyx_Coroutine_Check(yf)) { + ret = __Pyx_Coroutine_Send(yf, value); + } else + #endif + #ifdef __Pyx_AsyncGen_USED + if (__pyx_PyAsyncGenASend_CheckExact(yf)) { + ret = __Pyx_async_gen_asend_send(yf, value); + } else + #endif + #if CYTHON_COMPILING_IN_CPYTHON + if (PyGen_CheckExact(yf)) { + ret = __Pyx_PyGen_Send((PyGenObject*)yf, value == Py_None ? NULL : value); + } else + if (PyCoro_CheckExact(yf)) { + ret = __Pyx_PyGen_Send((PyGenObject*)yf, value == Py_None ? NULL : value); + } else + #endif + #endif + { + if (value == Py_None && PyIter_Check(yf)) + ret = __Pyx_PyIter_Next_Plain(yf); + else + ret = __Pyx_PyObject_CallMethod1(yf, __pyx_mstate_global->__pyx_n_u_send, value); + } + if (likely(ret)) { + __Pyx_Coroutine_unset_is_running(gen); + *retval = ret; + return PYGEN_NEXT; + } + result = __Pyx_Coroutine_FinishDelegation(gen, retval); + } else { + result = __Pyx_Coroutine_SendEx(gen, value, retval, 0); + } + __Pyx_Coroutine_unset_is_running(gen); + return result; +} +static int __Pyx_Coroutine_CloseIter(__pyx_CoroutineObject *gen, PyObject *yf) { + __Pyx_PySendResult result; + PyObject *retval = NULL; + CYTHON_UNUSED_VAR(gen); + assert(__Pyx_Coroutine_get_is_running(gen)); + #ifdef __Pyx_Generator_USED + if (__Pyx_Generator_CheckExact(yf)) { + result = __Pyx_Coroutine_Close(yf, &retval); + } else + #endif + #ifdef __Pyx_Coroutine_USED + if (__Pyx_Coroutine_Check(yf)) { + result = __Pyx_Coroutine_Close(yf, &retval); + } else + if (__Pyx_CoroutineAwait_CheckExact(yf)) { + result = __Pyx_CoroutineAwait_Close((__pyx_CoroutineAwaitObject*)yf); + } else + #endif + #ifdef __Pyx_AsyncGen_USED + if (__pyx_PyAsyncGenASend_CheckExact(yf)) { + retval = __Pyx_async_gen_asend_close(yf, NULL); + result = PYGEN_RETURN; + } else + if (__pyx_PyAsyncGenAThrow_CheckExact(yf)) { + retval = __Pyx_async_gen_athrow_close(yf, NULL); + result = PYGEN_RETURN; + } else + #endif + { + PyObject *meth; + result = PYGEN_RETURN; + meth = __Pyx_PyObject_GetAttrStrNoError(yf, __pyx_mstate_global->__pyx_n_u_close); + if (unlikely(!meth)) { + if (unlikely(PyErr_Occurred())) { + PyErr_WriteUnraisable(yf); + } + } else { + retval = __Pyx_PyObject_CallNoArg(meth); + Py_DECREF(meth); + if (unlikely(!retval)) { + result = PYGEN_ERROR; + } + } + } + Py_XDECREF(retval); + return result == PYGEN_ERROR ? -1 : 0; +} +static PyObject *__Pyx_Generator_Next(PyObject *self) { + __Pyx_PySendResult result; + PyObject *retval = NULL; + __pyx_CoroutineObject *gen = (__pyx_CoroutineObject*) self; + if (unlikely(__Pyx_Coroutine_test_and_set_is_running(gen))) { + return __Pyx_Coroutine_AlreadyRunningError(gen); + } + #if CYTHON_USE_AM_SEND + if (gen->yieldfrom_am_send) { + result = __Pyx_Coroutine_SendToDelegate(gen, gen->yieldfrom_am_send, Py_None, &retval); + } else + #endif + if (gen->yieldfrom) { + PyObject *yf = gen->yieldfrom; + PyObject *ret; + #ifdef __Pyx_Generator_USED + if (__Pyx_Generator_CheckExact(yf)) { + ret = __Pyx_Generator_Next(yf); + } else + #endif + #ifdef __Pyx_Coroutine_USED + if (__Pyx_Coroutine_CheckExact(yf)) { + ret = __Pyx_Coroutine_Send(yf, Py_None); + } else + #endif + #if CYTHON_COMPILING_IN_CPYTHON && (PY_VERSION_HEX < 0x030A00A3 || !CYTHON_USE_AM_SEND) + if (PyGen_CheckExact(yf)) { + ret = __Pyx_PyGen_Send((PyGenObject*)yf, NULL); + } else + #endif + ret = __Pyx_PyIter_Next_Plain(yf); + if (likely(ret)) { + __Pyx_Coroutine_unset_is_running(gen); + return ret; + } + result = __Pyx_Coroutine_FinishDelegation(gen, &retval); + } else { + result = __Pyx_Coroutine_SendEx(gen, Py_None, &retval, 0); + } + __Pyx_Coroutine_unset_is_running(gen); + return __Pyx_Coroutine_MethodReturnFromResult(self, result, retval, 1); +} +static PyObject *__Pyx_Coroutine_Close_Method(PyObject *self, PyObject *arg) { + PyObject *retval = NULL; + __Pyx_PySendResult result; + CYTHON_UNUSED_VAR(arg); + result = __Pyx_Coroutine_Close(self, &retval); + if (unlikely(result == PYGEN_ERROR)) + return NULL; + Py_XDECREF(retval); + Py_RETURN_NONE; +} +static __Pyx_PySendResult +__Pyx_Coroutine_Close(PyObject *self, PyObject **retval) { + __pyx_CoroutineObject *gen = (__pyx_CoroutineObject *) self; + __Pyx_PySendResult result; + PyObject *yf; + int err = 0; + if (unlikely(__Pyx_Coroutine_test_and_set_is_running(gen))) { + *retval = __Pyx_Coroutine_AlreadyRunningError(gen); + return PYGEN_ERROR; + } + yf = gen->yieldfrom; + if (yf) { + Py_INCREF(yf); + err = __Pyx_Coroutine_CloseIter(gen, yf); + __Pyx_Coroutine_Undelegate(gen); + Py_DECREF(yf); + } + if (err == 0) + PyErr_SetNone(PyExc_GeneratorExit); + result = __Pyx_Coroutine_SendEx(gen, NULL, retval, 1); + if (result == PYGEN_ERROR) { + __Pyx_PyThreadState_declare + __Pyx_PyThreadState_assign + __Pyx_Coroutine_unset_is_running(gen); + if (!__Pyx_PyErr_Occurred()) { + return PYGEN_RETURN; + } else if (likely(__Pyx_PyErr_ExceptionMatches2(PyExc_GeneratorExit, PyExc_StopIteration))) { + __Pyx_PyErr_Clear(); + return PYGEN_RETURN; + } + return PYGEN_ERROR; + } else if (likely(result == PYGEN_RETURN && *retval == Py_None)) { + __Pyx_Coroutine_unset_is_running(gen); + return PYGEN_RETURN; + } else { + const char *msg; + Py_DECREF(*retval); + *retval = NULL; + if ((0)) { + #ifdef __Pyx_Coroutine_USED + } else if (__Pyx_Coroutine_Check(self)) { + msg = "coroutine ignored GeneratorExit"; + #endif + #ifdef __Pyx_AsyncGen_USED + } else if (__Pyx_AsyncGen_CheckExact(self)) { + msg = "async generator ignored GeneratorExit"; + #endif + } else { + msg = "generator ignored GeneratorExit"; + } + PyErr_SetString(PyExc_RuntimeError, msg); + __Pyx_Coroutine_unset_is_running(gen); + return PYGEN_ERROR; + } +} +static PyObject *__Pyx__Coroutine_Throw(PyObject *self, PyObject *typ, PyObject *val, PyObject *tb, + PyObject *args, int close_on_genexit) { + __pyx_CoroutineObject *gen = (__pyx_CoroutineObject *) self; + PyObject *yf; + if (unlikely(__Pyx_Coroutine_test_and_set_is_running(gen))) + return __Pyx_Coroutine_AlreadyRunningError(gen); + yf = gen->yieldfrom; + if (yf) { + __Pyx_PySendResult result; + PyObject *ret; + Py_INCREF(yf); + if (__Pyx_PyErr_GivenExceptionMatches(typ, PyExc_GeneratorExit) && close_on_genexit) { + int err = __Pyx_Coroutine_CloseIter(gen, yf); + Py_DECREF(yf); + __Pyx_Coroutine_Undelegate(gen); + if (err < 0) + goto propagate_exception; + goto throw_here; + } + if (0 + #ifdef __Pyx_Generator_USED + || __Pyx_Generator_CheckExact(yf) + #endif + #ifdef __Pyx_Coroutine_USED + || __Pyx_Coroutine_Check(yf) + #endif + ) { + ret = __Pyx__Coroutine_Throw(yf, typ, val, tb, args, close_on_genexit); + #ifdef __Pyx_Coroutine_USED + } else if (__Pyx_CoroutineAwait_CheckExact(yf)) { + ret = __Pyx__Coroutine_Throw(((__pyx_CoroutineAwaitObject*)yf)->coroutine, typ, val, tb, args, close_on_genexit); + #endif + } else { + PyObject *meth = __Pyx_PyObject_GetAttrStrNoError(yf, __pyx_mstate_global->__pyx_n_u_throw); + if (unlikely(!meth)) { + Py_DECREF(yf); + if (unlikely(PyErr_Occurred())) { + __Pyx_Coroutine_unset_is_running(gen); + return NULL; + } + __Pyx_Coroutine_Undelegate(gen); + goto throw_here; + } + if (args) { + ret = __Pyx_PyObject_Call(meth, args, NULL); + } else { + PyObject *cargs[4] = {NULL, typ, val, tb}; + size_t nargs = 1U + (val != NULL) + (tb != NULL); + ret = __Pyx_PyObject_FastCall(meth, cargs+1, nargs | __Pyx_PY_VECTORCALL_ARGUMENTS_OFFSET); + } + Py_DECREF(meth); + } + Py_DECREF(yf); + if (ret) { + __Pyx_Coroutine_unset_is_running(gen); + return ret; + } + result = __Pyx_Coroutine_FinishDelegation(gen, &ret); + __Pyx_Coroutine_unset_is_running(gen); + return __Pyx_Coroutine_MethodReturnFromResult(self, result, ret, 0); + } +throw_here: + __Pyx_Raise(typ, val, tb, NULL); +propagate_exception: + { + PyObject *retval = NULL; + __Pyx_PySendResult result = __Pyx_Coroutine_SendEx(gen, NULL, &retval, 0); + __Pyx_Coroutine_unset_is_running(gen); + return __Pyx_Coroutine_MethodReturnFromResult(self, result, retval, 0); + } +} +static PyObject *__Pyx_Coroutine_Throw(PyObject *self, +#if CYTHON_COMPILING_IN_LIMITED_API && __PYX_LIMITED_VERSION_HEX < 0x030A0000 + PyObject *args +#else + PyObject *const *args, Py_ssize_t nargs +#endif +) { + PyObject *typ; + PyObject *val = NULL; + PyObject *tb = NULL; +#if CYTHON_COMPILING_IN_LIMITED_API && __PYX_LIMITED_VERSION_HEX < 0x030A0000 + if (unlikely(!PyArg_UnpackTuple(args, "throw", 1, 3, &typ, &val, &tb))) + return NULL; +#else + switch (nargs) { + case 3: + tb = args[2]; + CYTHON_FALLTHROUGH; + case 2: + val = args[1]; + CYTHON_FALLTHROUGH; + case 1: + typ = args[0]; + break; + default: + PyErr_SetString(PyExc_TypeError, "throw takes between 1 and 3 positional arguments"); + return NULL; + } +#endif + return __Pyx__Coroutine_Throw(self, typ, val, tb, +#if CYTHON_COMPILING_IN_LIMITED_API && __PYX_LIMITED_VERSION_HEX < 0x030A0000 + args, +#else + NULL, +#endif + 1); +} +static CYTHON_INLINE int __Pyx_Coroutine_traverse_excstate(__Pyx_ExcInfoStruct *exc_state, visitproc visit, void *arg) { +#if PY_VERSION_HEX >= 0x030B00a4 + Py_VISIT(exc_state->exc_value); +#else + Py_VISIT(exc_state->exc_type); + Py_VISIT(exc_state->exc_value); + Py_VISIT(exc_state->exc_traceback); +#endif + return 0; +} +static int __Pyx_Coroutine_traverse(__pyx_CoroutineObject *gen, visitproc visit, void *arg) { + { + int e = __Pyx_call_type_traverse((PyObject*)gen, 1, visit, arg); + if (e) return e; + } + Py_VISIT(gen->closure); + Py_VISIT(gen->classobj); + Py_VISIT(gen->yieldfrom); + return __Pyx_Coroutine_traverse_excstate(&gen->gi_exc_state, visit, arg); +} +static int __Pyx_Coroutine_clear(PyObject *self) { + __pyx_CoroutineObject *gen = (__pyx_CoroutineObject *) self; + Py_CLEAR(gen->closure); + Py_CLEAR(gen->classobj); + __Pyx_Coroutine_Undelegate(gen); + __Pyx_Coroutine_ExceptionClear(&gen->gi_exc_state); +#ifdef __Pyx_AsyncGen_USED + if (__Pyx_AsyncGen_CheckExact(self)) { + Py_CLEAR(((__pyx_PyAsyncGenObject*)gen)->ag_finalizer); + } +#endif + Py_CLEAR(gen->gi_code); + Py_CLEAR(gen->gi_frame); + Py_CLEAR(gen->gi_name); + Py_CLEAR(gen->gi_qualname); + Py_CLEAR(gen->gi_modulename); + return 0; +} +static void __Pyx_Coroutine_dealloc(PyObject *self) { + __pyx_CoroutineObject *gen = (__pyx_CoroutineObject *) self; + PyObject_GC_UnTrack(gen); + #if PY_VERSION_HEX < 0x030C0000 || CYTHON_COMPILING_IN_LIMITED_API + if (gen->gi_weakreflist != NULL) + #endif + PyObject_ClearWeakRefs(self); + if (gen->resume_label >= 0) { +#if CYTHON_USE_TP_FINALIZE + PyObject_GC_Track(self); + if (unlikely(PyObject_CallFinalizerFromDealloc(self))) + { + return; + } + PyObject_GC_UnTrack(self); +#endif + } +#ifdef __Pyx_AsyncGen_USED + if (__Pyx_AsyncGen_CheckExact(self)) { + /* We have to handle this case for asynchronous generators + right here, because this code has to be between UNTRACK + and GC_Del. */ + Py_CLEAR(((__pyx_PyAsyncGenObject*)self)->ag_finalizer); + } +#endif + __Pyx_Coroutine_clear(self); + __Pyx_PyHeapTypeObject_GC_Del(gen); +} +#if CYTHON_USE_TP_FINALIZE +static void __Pyx_Coroutine_del(PyObject *self) { + PyObject *error_type, *error_value, *error_traceback; + __pyx_CoroutineObject *gen = (__pyx_CoroutineObject *) self; + __Pyx_PyThreadState_declare + if (gen->resume_label < 0) { + return; + } + __Pyx_PyThreadState_assign + __Pyx_ErrFetch(&error_type, &error_value, &error_traceback); +#ifdef __Pyx_AsyncGen_USED + if (__Pyx_AsyncGen_CheckExact(self)) { + __pyx_PyAsyncGenObject *agen = (__pyx_PyAsyncGenObject*)self; + PyObject *finalizer = agen->ag_finalizer; + if (finalizer && !agen->ag_closed) { + PyObject *res = __Pyx_PyObject_CallOneArg(finalizer, self); + if (unlikely(!res)) { + PyErr_WriteUnraisable(self); + } else { + Py_DECREF(res); + } + __Pyx_ErrRestore(error_type, error_value, error_traceback); + return; + } + } +#endif + if (unlikely(gen->resume_label == 0 && !error_value)) { +#ifdef __Pyx_Coroutine_USED +#ifdef __Pyx_Generator_USED + if (!__Pyx_Generator_CheckExact(self)) +#endif + { + PyObject_GC_UnTrack(self); + if (unlikely(PyErr_WarnFormat(PyExc_RuntimeWarning, 1, "coroutine '%.50S' was never awaited", gen->gi_qualname) < 0)) + PyErr_WriteUnraisable(self); + PyObject_GC_Track(self); + } +#endif + } else { + PyObject *retval = NULL; + __Pyx_PySendResult result = __Pyx_Coroutine_Close(self, &retval); + if (result == PYGEN_ERROR) { + PyErr_WriteUnraisable(self); + } else { + Py_XDECREF(retval); + } + } + __Pyx_ErrRestore(error_type, error_value, error_traceback); +} +#endif +static PyObject * +__Pyx_Coroutine_get_name(__pyx_CoroutineObject *self, void *context) +{ + PyObject *name = self->gi_name; + CYTHON_UNUSED_VAR(context); + if (unlikely(!name)) name = Py_None; + Py_INCREF(name); + return name; +} +static int +__Pyx_Coroutine_set_name(__pyx_CoroutineObject *self, PyObject *value, void *context) +{ + CYTHON_UNUSED_VAR(context); + if (unlikely(value == NULL || !PyUnicode_Check(value))) { + PyErr_SetString(PyExc_TypeError, + "__name__ must be set to a string object"); + return -1; + } + Py_INCREF(value); + __Pyx_Py_XDECREF_SET(self->gi_name, value); + return 0; +} +static PyObject * +__Pyx_Coroutine_get_qualname(__pyx_CoroutineObject *self, void *context) +{ + PyObject *name = self->gi_qualname; + CYTHON_UNUSED_VAR(context); + if (unlikely(!name)) name = Py_None; + Py_INCREF(name); + return name; +} +static int +__Pyx_Coroutine_set_qualname(__pyx_CoroutineObject *self, PyObject *value, void *context) +{ + CYTHON_UNUSED_VAR(context); + if (unlikely(value == NULL || !PyUnicode_Check(value))) { + PyErr_SetString(PyExc_TypeError, + "__qualname__ must be set to a string object"); + return -1; + } + Py_INCREF(value); + __Pyx_Py_XDECREF_SET(self->gi_qualname, value); + return 0; +} +#if !CYTHON_COMPILING_IN_LIMITED_API +static PyObject * +__Pyx__Coroutine_get_frame_locked(__pyx_CoroutineObject *self) { + PyObject *frame; + frame = self->gi_frame; + if (!frame) { + if (unlikely(!self->gi_code)) { + Py_RETURN_NONE; + } + PyObject *globals = PyDict_New(); + if (unlikely(!globals)) return NULL; + frame = (PyObject *) PyFrame_New( + PyThreadState_Get(), /*PyThreadState *tstate,*/ + (PyCodeObject*) self->gi_code, /*PyCodeObject *code,*/ + globals, /*PyObject *globals,*/ + 0 /*PyObject *locals*/ + ); + Py_DECREF(globals); + if (unlikely(!frame)) + return NULL; + if (unlikely(self->gi_frame)) { + Py_DECREF(frame); + frame = self->gi_frame; + } else { + self->gi_frame = frame; + } + } + Py_INCREF(frame); + return frame; +} +#endif +static PyObject * +__Pyx__Coroutine_get_frame(__pyx_CoroutineObject *self) +{ +#if !CYTHON_COMPILING_IN_LIMITED_API + PyObject *frame; + __Pyx_BEGIN_CRITICAL_SECTION((PyObject*)self); + frame = __Pyx__Coroutine_get_frame_locked(self); + __Pyx_END_CRITICAL_SECTION(); + return frame; +#else + CYTHON_UNUSED_VAR(self); + Py_RETURN_NONE; +#endif +} +static PyObject * +__Pyx_Coroutine_get_frame(__pyx_CoroutineObject *self, void *context) { + CYTHON_UNUSED_VAR(context); + PyObject *frame = self->gi_frame; + if (frame) + return __Pyx_NewRef(frame); + return __Pyx__Coroutine_get_frame(self); +} +static __pyx_CoroutineObject *__Pyx__Coroutine_New( + PyTypeObject* type, __pyx_coroutine_body_t body, PyObject *code, PyObject *closure, + PyObject *name, PyObject *qualname, PyObject *module_name) { + __pyx_CoroutineObject *gen = PyObject_GC_New(__pyx_CoroutineObject, type); + if (unlikely(!gen)) + return NULL; + return __Pyx__Coroutine_NewInit(gen, body, code, closure, name, qualname, module_name); +} +static __pyx_CoroutineObject *__Pyx__Coroutine_NewInit( + __pyx_CoroutineObject *gen, __pyx_coroutine_body_t body, PyObject *code, PyObject *closure, + PyObject *name, PyObject *qualname, PyObject *module_name) { + gen->body = body; + gen->closure = closure; + Py_XINCREF(closure); + gen->is_running = 0; + gen->resume_label = 0; + gen->classobj = NULL; + gen->yieldfrom = NULL; + gen->yieldfrom_am_send = NULL; + #if PY_VERSION_HEX >= 0x030B00a4 && !CYTHON_COMPILING_IN_LIMITED_API + gen->gi_exc_state.exc_value = NULL; + #else + gen->gi_exc_state.exc_type = NULL; + gen->gi_exc_state.exc_value = NULL; + gen->gi_exc_state.exc_traceback = NULL; + #endif +#if CYTHON_USE_EXC_INFO_STACK + gen->gi_exc_state.previous_item = NULL; +#endif +#if PY_VERSION_HEX < 0x030C0000 || CYTHON_COMPILING_IN_LIMITED_API + gen->gi_weakreflist = NULL; +#endif + Py_XINCREF(qualname); + gen->gi_qualname = qualname; + Py_XINCREF(name); + gen->gi_name = name; + Py_XINCREF(module_name); + gen->gi_modulename = module_name; + Py_XINCREF(code); + gen->gi_code = code; + gen->gi_frame = NULL; +#if CYTHON_USE_SYS_MONITORING && (CYTHON_PROFILE || CYTHON_TRACE) + memset(gen->__pyx_pymonitoring_state, 0, sizeof(gen->__pyx_pymonitoring_state)); + gen->__pyx_pymonitoring_version = 0; +#endif + PyObject_GC_Track(gen); + return gen; +} +static char __Pyx_Coroutine_test_and_set_is_running(__pyx_CoroutineObject *gen) { + char result; + __Pyx_BEGIN_CRITICAL_SECTION((PyObject*)gen); + result = gen->is_running; + gen->is_running = 1; + __Pyx_END_CRITICAL_SECTION(); + return result; +} +static void __Pyx_Coroutine_unset_is_running(__pyx_CoroutineObject *gen) { + __Pyx_BEGIN_CRITICAL_SECTION((PyObject*)gen); + assert(gen->is_running); + gen->is_running = 0; + __Pyx_END_CRITICAL_SECTION(); +} +static char __Pyx_Coroutine_get_is_running(__pyx_CoroutineObject *gen) { + char result; + __Pyx_BEGIN_CRITICAL_SECTION((PyObject*)gen); + result = gen->is_running; + __Pyx_END_CRITICAL_SECTION(); + return result; +} +static PyObject *__Pyx_Coroutine_get_is_running_getter(PyObject *gen, void *closure) { + CYTHON_UNUSED_VAR(closure); + char result = __Pyx_Coroutine_get_is_running((__pyx_CoroutineObject*)gen); + if (result) Py_RETURN_TRUE; + else Py_RETURN_FALSE; +} +#if __PYX_HAS_PY_AM_SEND == 2 +static void __Pyx_SetBackportTypeAmSend(PyTypeObject *type, __Pyx_PyAsyncMethodsStruct *static_amsend_methods, __Pyx_pyiter_sendfunc am_send) { + Py_ssize_t ptr_offset = (char*)(type->tp_as_async) - (char*)type; + if (ptr_offset < 0 || ptr_offset > type->tp_basicsize) { + return; + } + memcpy((void*)static_amsend_methods, (void*)(type->tp_as_async), sizeof(*type->tp_as_async)); + static_amsend_methods->am_send = am_send; + type->tp_as_async = __Pyx_SlotTpAsAsync(static_amsend_methods); +} +#endif +static PyObject *__Pyx_Coroutine_fail_reduce_ex(PyObject *self, PyObject *arg) { + CYTHON_UNUSED_VAR(arg); + __Pyx_RaiseTypeErrorWithObjectType( + "cannot pickle '" __Pyx_FMT_TYPENAME "' object", self); + return NULL; +} + +/* Generator */ +static PyMethodDef __pyx_Generator_methods[] = { + {"send", (PyCFunction) __Pyx_Coroutine_Send, METH_O, + PyDoc_STR("send(arg) -> send 'arg' into generator,\nreturn next yielded value or raise StopIteration.")}, + {"throw", __PYX_REINTERPRET_FUNCION(PyCFunction, __Pyx_Coroutine_Throw), +#if CYTHON_COMPILING_IN_LIMITED_API && __PYX_LIMITED_VERSION_HEX < 0x030A0000 + METH_VARARGS, +#else + METH_FASTCALL, +#endif + PyDoc_STR("throw(typ[,val[,tb]]) -> raise exception in generator,\nreturn next yielded value or raise StopIteration.")}, + {"close", (PyCFunction) __Pyx_Coroutine_Close_Method, METH_NOARGS, + PyDoc_STR("close() -> raise GeneratorExit inside generator.")}, + {"__reduce_ex__", (PyCFunction) __Pyx_Coroutine_fail_reduce_ex, METH_O, 0}, + {"__reduce__", (PyCFunction) __Pyx_Coroutine_fail_reduce_ex, METH_NOARGS, 0}, + {0, 0, 0, 0} +}; +static PyMemberDef __pyx_Generator_memberlist[] = { + {"gi_yieldfrom", T_OBJECT, offsetof(__pyx_CoroutineObject, yieldfrom), READONLY, + PyDoc_STR("object being iterated by 'yield from', or None")}, + {"gi_code", T_OBJECT, offsetof(__pyx_CoroutineObject, gi_code), READONLY, NULL}, + {"__module__", T_OBJECT, offsetof(__pyx_CoroutineObject, gi_modulename), 0, 0}, +#if PY_VERSION_HEX < 0x030C0000 || CYTHON_COMPILING_IN_LIMITED_API + {"__weaklistoffset__", T_PYSSIZET, offsetof(__pyx_CoroutineObject, gi_weakreflist), READONLY, 0}, +#endif + {0, 0, 0, 0, 0} +}; +static PyGetSetDef __pyx_Generator_getsets[] = { + {"__name__", (getter)__Pyx_Coroutine_get_name, (setter)__Pyx_Coroutine_set_name, + PyDoc_STR("name of the generator"), 0}, + {"__qualname__", (getter)__Pyx_Coroutine_get_qualname, (setter)__Pyx_Coroutine_set_qualname, + PyDoc_STR("qualified name of the generator"), 0}, + {"gi_frame", (getter)__Pyx_Coroutine_get_frame, NULL, + PyDoc_STR("Frame of the generator"), 0}, + {"gi_running", __Pyx_Coroutine_get_is_running_getter, NULL, NULL, NULL}, + {0, 0, 0, 0, 0} +}; +static PyType_Slot __pyx_GeneratorType_slots[] = { + {Py_tp_dealloc, (void *)__Pyx_Coroutine_dealloc}, + {Py_tp_traverse, (void *)__Pyx_Coroutine_traverse}, + {Py_tp_iter, (void *)PyObject_SelfIter}, + {Py_tp_iternext, (void *)__Pyx_Generator_Next}, + {Py_tp_methods, (void *)__pyx_Generator_methods}, + {Py_tp_members, (void *)__pyx_Generator_memberlist}, + {Py_tp_getset, (void *)__pyx_Generator_getsets}, + {Py_tp_getattro, (void *) PyObject_GenericGetAttr}, +#if CYTHON_USE_TP_FINALIZE + {Py_tp_finalize, (void *)__Pyx_Coroutine_del}, +#endif +#if __PYX_HAS_PY_AM_SEND == 1 + {Py_am_send, (void *)__Pyx_Coroutine_AmSend}, +#endif + {0, 0}, +}; +static PyType_Spec __pyx_GeneratorType_spec = { + __PYX_TYPE_MODULE_PREFIX "generator", + sizeof(__pyx_CoroutineObject), + 0, +#if PY_VERSION_HEX >= 0x030C0000 && !CYTHON_COMPILING_IN_LIMITED_API + Py_TPFLAGS_MANAGED_WEAKREF | +#endif + Py_TPFLAGS_IMMUTABLETYPE | Py_TPFLAGS_DISALLOW_INSTANTIATION | + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | __Pyx_TPFLAGS_HAVE_AM_SEND, + __pyx_GeneratorType_slots +}; +#if __PYX_HAS_PY_AM_SEND == 2 +static __Pyx_PyAsyncMethodsStruct __pyx_Generator_as_async; +#endif +static int __pyx_Generator_init(PyObject *module) { + __pyx_mstatetype *mstate = __Pyx_PyModule_GetState(module); + mstate->__pyx_GeneratorType = __Pyx_FetchCommonTypeFromSpec( + mstate->__pyx_CommonTypesMetaclassType, module, &__pyx_GeneratorType_spec, NULL); + if (unlikely(!mstate->__pyx_GeneratorType)) { + return -1; + } +#if __PYX_HAS_PY_AM_SEND == 2 + __Pyx_SetBackportTypeAmSend(mstate->__pyx_GeneratorType, &__pyx_Generator_as_async, &__Pyx_Coroutine_AmSend); +#endif + return 0; +} +static PyObject *__Pyx_Generator_GetInlinedResult(PyObject *self) { + __pyx_CoroutineObject *gen = (__pyx_CoroutineObject*) self; + PyObject *retval = NULL; + if (unlikely(__Pyx_Coroutine_test_and_set_is_running(gen))) { + return __Pyx_Coroutine_AlreadyRunningError(gen); + } + __Pyx_PySendResult result = __Pyx_Coroutine_SendEx(gen, Py_None, &retval, 0); + __Pyx_Coroutine_unset_is_running(gen); + (void) result; + assert (result == PYGEN_RETURN || result == PYGEN_ERROR); + assert ((result == PYGEN_RETURN && retval != NULL) || (result == PYGEN_ERROR && retval == NULL)); + return retval; +} + +/* CheckBinaryVersion */ +static int __Pyx_check_binary_version(unsigned long ct_version, unsigned long rt_version, int allow_newer) { + const unsigned long MAJOR_MINOR = 0xFFFF0000UL; + if ((rt_version & MAJOR_MINOR) == (ct_version & MAJOR_MINOR)) + return 0; + if (likely(allow_newer && (rt_version & MAJOR_MINOR) > (ct_version & MAJOR_MINOR))) + return 1; + { + char message[200]; + PyOS_snprintf(message, sizeof(message), + "compile time Python version %d.%d " + "of module '%.100s' " + "%s " + "runtime version %d.%d", + (int) (ct_version >> 24), (int) ((ct_version >> 16) & 0xFF), + __Pyx_MODULE_NAME, + (allow_newer) ? "was newer than" : "does not match", + (int) (rt_version >> 24), (int) ((rt_version >> 16) & 0xFF) + ); + return PyErr_WarnEx(NULL, message, 1); + } +} + +/* NewCodeObj */ +#if CYTHON_COMPILING_IN_LIMITED_API + static PyObject* __Pyx__PyCode_New(int a, int p, int k, int l, int s, int f, + PyObject *code, PyObject *c, PyObject* n, PyObject *v, + PyObject *fv, PyObject *cell, PyObject* fn, + PyObject *name, int fline, PyObject *lnos) { + PyObject *exception_table = NULL; + PyObject *types_module=NULL, *code_type=NULL, *result=NULL; + #if __PYX_LIMITED_VERSION_HEX < 0x030b0000 + PyObject *version_info; + PyObject *py_minor_version = NULL; + #endif + long minor_version = 0; + #if __PYX_LIMITED_VERSION_HEX >= 0x030b0000 + minor_version = 11; + #else + if (!(version_info = PySys_GetObject("version_info"))) goto end; + if (!(py_minor_version = PySequence_GetItem(version_info, 1))) goto end; + minor_version = PyLong_AsLong(py_minor_version); + Py_DECREF(py_minor_version); + if (minor_version == -1 && PyErr_Occurred()) goto end; + #endif + if (!(types_module = PyImport_ImportModule("types"))) goto end; + if (!(code_type = PyObject_GetAttrString(types_module, "CodeType"))) goto end; + if (minor_version <= 10) { + result = PyObject_CallFunction(code_type, "iiiiiiOOOOOOiOOO", a,p, k, l, s, f, code, + c, n, v, fn, name, fline, lnos, fv, cell); + } else { + if (!(exception_table = PyBytes_FromStringAndSize(NULL, 0))) goto end; + result = PyObject_CallFunction(code_type, "iiiiiiOOOOOOOiOOOO", a,p, k, l, s, f, code, + c, n, v, fn, name, name, fline, lnos, exception_table, fv, cell); + } + end: + Py_XDECREF(code_type); + Py_XDECREF(exception_table); + Py_XDECREF(types_module); + if (!result) { + PyObject *exc_type, *exc_value, *exc_tb; + PyErr_Fetch(&exc_type, &exc_value, &exc_tb); + if (PyErr_WarnFormat( + PyExc_RuntimeWarning, 1, + "Code object creation failed in Cython module: %S\nTry recompiling with a more recent version of Cython.", + exc_value ? exc_value : exc_type) == 0) { + result = Py_None; + Py_INCREF(result); + } + Py_DECREF(exc_type); + Py_XDECREF(exc_value); + Py_XDECREF(exc_tb); + } + return result; + } +#elif PY_VERSION_HEX >= 0x030B0000 + static PyCodeObject* __Pyx__PyCode_New(int a, int p, int k, int l, int s, int f, + PyObject *code, PyObject *c, PyObject* n, PyObject *v, + PyObject *fv, PyObject *cell, PyObject* fn, + PyObject *name, int fline, PyObject *lnos) { + PyCodeObject *result; + result = + #if PY_VERSION_HEX >= 0x030C0000 + PyUnstable_Code_NewWithPosOnlyArgs + #else + PyCode_NewWithPosOnlyArgs + #endif + (a, p, k, l, s, f, code, c, n, v, fv, cell, fn, name, name, fline, lnos, __pyx_mstate_global->__pyx_empty_bytes); + #if CYTHON_COMPILING_IN_CPYTHON && PY_VERSION_HEX >= 0x030c00A1 + if (likely(result)) + result->_co_firsttraceable = 0; + #endif + return result; + } +#elif !CYTHON_COMPILING_IN_PYPY + #define __Pyx__PyCode_New(a, p, k, l, s, f, code, c, n, v, fv, cell, fn, name, fline, lnos)\ + PyCode_NewWithPosOnlyArgs(a, p, k, l, s, f, code, c, n, v, fv, cell, fn, name, fline, lnos) +#else + #define __Pyx__PyCode_New(a, p, k, l, s, f, code, c, n, v, fv, cell, fn, name, fline, lnos)\ + PyCode_New(a, k, l, s, f, code, c, n, v, fv, cell, fn, name, fline, lnos) +#endif +static PyObject* __Pyx_PyCode_New( + const __Pyx_PyCode_New_function_description descr, + PyObject * const *varnames, + PyObject *filename, + PyObject *funcname, + PyObject *line_table, + PyObject *tuple_dedup_map +) { + PyObject *code_obj = NULL, *varnames_tuple_dedup = NULL, *code_bytes = NULL; + Py_ssize_t var_count = (Py_ssize_t) descr.nlocals; + PyObject *varnames_tuple = PyTuple_New(var_count); + if (unlikely(!varnames_tuple)) return NULL; + for (Py_ssize_t i=0; i < var_count; i++) { + Py_INCREF(varnames[i]); + if (__Pyx_PyTuple_SET_ITEM(varnames_tuple, i, varnames[i]) != (0)) goto done; + } + #if CYTHON_COMPILING_IN_LIMITED_API + varnames_tuple_dedup = PyDict_GetItem(tuple_dedup_map, varnames_tuple); + if (!varnames_tuple_dedup) { + if (unlikely(PyDict_SetItem(tuple_dedup_map, varnames_tuple, varnames_tuple) < 0)) goto done; + varnames_tuple_dedup = varnames_tuple; + } + #else + varnames_tuple_dedup = PyDict_SetDefault(tuple_dedup_map, varnames_tuple, varnames_tuple); + if (unlikely(!varnames_tuple_dedup)) goto done; + #endif + #if CYTHON_AVOID_BORROWED_REFS + Py_INCREF(varnames_tuple_dedup); + #endif + if (__PYX_LIMITED_VERSION_HEX >= (0x030b0000) && line_table != NULL && !CYTHON_COMPILING_IN_GRAAL) { + Py_ssize_t line_table_length = __Pyx_PyBytes_GET_SIZE(line_table); + #if !CYTHON_ASSUME_SAFE_SIZE + if (unlikely(line_table_length == -1)) goto done; + #endif + Py_ssize_t code_len = (line_table_length * 2 + 4) & ~3LL; + code_bytes = PyBytes_FromStringAndSize(NULL, code_len); + if (unlikely(!code_bytes)) goto done; + char* c_code_bytes = PyBytes_AsString(code_bytes); + if (unlikely(!c_code_bytes)) goto done; + memset(c_code_bytes, 0, (size_t) code_len); + } + code_obj = (PyObject*) __Pyx__PyCode_New( + (int) descr.argcount, + (int) descr.num_posonly_args, + (int) descr.num_kwonly_args, + (int) descr.nlocals, + 0, + (int) descr.flags, + code_bytes ? code_bytes : __pyx_mstate_global->__pyx_empty_bytes, + __pyx_mstate_global->__pyx_empty_tuple, + __pyx_mstate_global->__pyx_empty_tuple, + varnames_tuple_dedup, + __pyx_mstate_global->__pyx_empty_tuple, + __pyx_mstate_global->__pyx_empty_tuple, + filename, + funcname, + (int) descr.first_line, + (__PYX_LIMITED_VERSION_HEX >= (0x030b0000) && line_table) ? line_table : __pyx_mstate_global->__pyx_empty_bytes + ); +done: + Py_XDECREF(code_bytes); + #if CYTHON_AVOID_BORROWED_REFS + Py_XDECREF(varnames_tuple_dedup); + #endif + Py_DECREF(varnames_tuple); + return code_obj; +} + +/* DecompressString */ +CYTHON_UNUSED +static CYTHON_SMALL_CODE PyObject *__Pyx_DecompressString(const char *s, Py_ssize_t length, int algo) { +#ifdef __Pyx_DecompressString_UNUSED + CYTHON_UNUSED_VAR(s); + CYTHON_UNUSED_VAR(length); + CYTHON_UNUSED_VAR(algo); + return NULL; +#else + PyObject *module = NULL, *decompress, *compressed_bytes, *decompressed; + const char* module_name = algo == 3 ? "compression.zstd" : algo == 2 ? "bz2" : "zlib"; + PyObject *methodname = PyUnicode_FromString("decompress"); + if (unlikely(!methodname)) return NULL; + #if __PYX_LIMITED_VERSION_HEX >= 0x030e0000 + if (algo == 3) { + PyObject *fromlist = Py_BuildValue("[O]", methodname); + if (unlikely(!fromlist)) goto bad; + module = PyImport_ImportModuleLevel("compression.zstd", NULL, NULL, fromlist, 0); + Py_DECREF(fromlist); + } else + #endif + module = PyImport_ImportModule(module_name); + if (unlikely(!module)) goto import_failed; + decompress = PyObject_GetAttr(module, methodname); + if (unlikely(!decompress)) goto import_failed; + { + #ifdef __cplusplus + char *memview_bytes = const_cast(s); + #else + #if defined(__clang__) + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wcast-qual" + #elif !defined(__INTEL_COMPILER) && defined(__GNUC__) + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wcast-qual" + #endif + char *memview_bytes = (char*) s; + #if defined(__clang__) + #pragma clang diagnostic pop + #elif !defined(__INTEL_COMPILER) && defined(__GNUC__) + #pragma GCC diagnostic pop + #endif + #endif + #if CYTHON_COMPILING_IN_LIMITED_API && !defined(PyBUF_READ) + int memview_flags = 0x100; + #else + int memview_flags = PyBUF_READ; + #endif + compressed_bytes = PyMemoryView_FromMemory(memview_bytes, length, memview_flags); + } + if (unlikely(!compressed_bytes)) { + Py_DECREF(decompress); + goto bad; + } + decompressed = PyObject_CallFunctionObjArgs(decompress, compressed_bytes, NULL); + Py_DECREF(compressed_bytes); + Py_DECREF(decompress); + Py_DECREF(module); + Py_DECREF(methodname); + return decompressed; +import_failed: + PyErr_Format(PyExc_ImportError, + "Failed to import '%.20s.decompress' - cannot initialise module strings. " + "String compression was configured with the C macro 'CYTHON_COMPRESS_STRINGS=%d'.", + module_name, algo); +bad: + Py_XDECREF(module); + Py_DECREF(methodname); + return NULL; +#endif +} + +/* DecompressString_LZSS */ +#ifndef __Pyx_DecompressString_LZSS_UNUSED +CYTHON_UNUSED +static CYTHON_SMALL_CODE size_t __pyx_lzss_decompress(const uint8_t* src, uint8_t* dst, size_t dst_len) { + size_t pos = 0, out_pos = 0; + while (1) { + uint32_t flags = src[pos++] | 0xFF00; + while (flags & 0x100) { + if (flags & 1) { + dst[out_pos++] = src[pos++]; + } else { + uint32_t lo = src[pos++], hi = src[pos++]; + uint32_t end_offset_of_last_occurrence, match_length; + if (!(lo & 0x80)) { + end_offset_of_last_occurrence = lo; + match_length = hi; + } else if (!(hi & 0x80)) { + end_offset_of_last_occurrence = 0x80 + (((hi << 2) & 0x180) | (lo & 0x7F)); + match_length = hi & 0x1F; + } else { + end_offset_of_last_occurrence = 0x80 + ((hi & 0x7F) << 7 | (lo & 0x7F)); + match_length = src[pos++]; + } + match_length += 3; + size_t ref_pos = out_pos - end_offset_of_last_occurrence - match_length; + memcpy(dst + out_pos, dst + ref_pos, match_length); + out_pos += match_length; + } + if (out_pos >= dst_len) return pos; + flags >>= 1; + } + } +} +#endif +static CYTHON_SMALL_CODE PyObject *__Pyx_DecompressString_LZSS(const char *s, size_t compressed_length, size_t uncompressed_length) { +#ifdef __Pyx_DecompressString_LZSS_UNUSED + CYTHON_UNUSED_VAR(s); + CYTHON_UNUSED_VAR(compressed_length); + CYTHON_UNUSED_VAR(uncompressed_length); + return NULL; +#else + PyObject *result; + unsigned char *result_data; + size_t src_length; + result = PyBytes_FromStringAndSize(NULL, (Py_ssize_t) uncompressed_length); + if (unlikely(!result)) return NULL; + result_data = __Pyx_PyBytes_AsWritableUString(result); + if (unlikely(!result_data)) goto bad; + src_length = __pyx_lzss_decompress((const uint8_t*) s, result_data, uncompressed_length); + if (unlikely(src_length != compressed_length)) goto decompression_failed; + return result; +decompression_failed: + PyErr_SetString(PyExc_RuntimeError, "LZSS string data decompression failed"); +bad: + Py_DECREF(result); + return NULL; +#endif +} + +#include +static CYTHON_INLINE Py_ssize_t __Pyx_ssize_strlen(const char *s) { + size_t len = strlen(s); + if (unlikely(len > (size_t) PY_SSIZE_T_MAX)) { + PyErr_SetString(PyExc_OverflowError, "byte string is too long"); + return -1; + } + return (Py_ssize_t) len; +} +static CYTHON_INLINE PyObject* __Pyx_PyUnicode_FromString(const char* c_str) { + Py_ssize_t len = __Pyx_ssize_strlen(c_str); + if (unlikely(len < 0)) return NULL; + return __Pyx_PyUnicode_FromStringAndSize(c_str, len); +} +static CYTHON_INLINE PyObject* __Pyx_PyByteArray_FromString(const char* c_str) { + Py_ssize_t len = __Pyx_ssize_strlen(c_str); + if (unlikely(len < 0)) return NULL; + return PyByteArray_FromStringAndSize(c_str, len); +} +static CYTHON_INLINE const char* __Pyx_PyObject_AsString(PyObject* o) { + Py_ssize_t ignore; + return __Pyx_PyObject_AsStringAndSize(o, &ignore); +} +#if __PYX_DEFAULT_STRING_ENCODING_IS_ASCII || __PYX_DEFAULT_STRING_ENCODING_IS_UTF8 +static CYTHON_INLINE const char* __Pyx_PyUnicode_AsStringAndSize(PyObject* o, Py_ssize_t *length) { + if (unlikely(__Pyx_PyUnicode_READY(o) == -1)) return NULL; +#if CYTHON_COMPILING_IN_LIMITED_API + { + const char* result; + Py_ssize_t unicode_length; + CYTHON_MAYBE_UNUSED_VAR(unicode_length); // only for __PYX_DEFAULT_STRING_ENCODING_IS_ASCII + #if __PYX_LIMITED_VERSION_HEX < 0x030A0000 + if (unlikely(PyArg_Parse(o, "s#", &result, length) < 0)) return NULL; + #else + result = PyUnicode_AsUTF8AndSize(o, length); + #endif + #if __PYX_DEFAULT_STRING_ENCODING_IS_ASCII + unicode_length = PyUnicode_GetLength(o); + if (unlikely(unicode_length < 0)) return NULL; + if (unlikely(unicode_length != *length)) { + PyUnicode_AsASCIIString(o); + return NULL; + } + #endif + return result; + } +#else +#if __PYX_DEFAULT_STRING_ENCODING_IS_ASCII + if (likely(PyUnicode_IS_ASCII(o))) { + *length = PyUnicode_GET_LENGTH(o); + return PyUnicode_AsUTF8(o); + } else { + PyUnicode_AsASCIIString(o); + return NULL; + } +#else + return PyUnicode_AsUTF8AndSize(o, length); +#endif +#endif +} +#endif +static CYTHON_INLINE const char* __Pyx_PyObject_AsStringAndSize(PyObject* o, Py_ssize_t *length) { +#if __PYX_DEFAULT_STRING_ENCODING_IS_ASCII || __PYX_DEFAULT_STRING_ENCODING_IS_UTF8 + if (PyUnicode_Check(o)) { + return __Pyx_PyUnicode_AsStringAndSize(o, length); + } else +#endif + if (PyByteArray_Check(o)) { +#if (CYTHON_ASSUME_SAFE_SIZE && CYTHON_ASSUME_SAFE_MACROS) || (CYTHON_COMPILING_IN_PYPY && (defined(PyByteArray_AS_STRING) && defined(PyByteArray_GET_SIZE))) + *length = PyByteArray_GET_SIZE(o); + return PyByteArray_AS_STRING(o); +#else + *length = PyByteArray_Size(o); + if (*length == -1) return NULL; + return PyByteArray_AsString(o); +#endif + } else + { + char* result; + int r = PyBytes_AsStringAndSize(o, &result, length); + if (unlikely(r < 0)) { + return NULL; + } else { + return result; + } + } +} +static CYTHON_INLINE int __Pyx_PyObject_IsTrue(PyObject* x) { + int is_true = x == Py_True; + if (is_true | (x == Py_False) | (x == Py_None)) return is_true; + else return PyObject_IsTrue(x); +} +static CYTHON_INLINE int __Pyx_PyObject_IsTrueAndDecref(PyObject* x) { + int retval; + if (unlikely(!x)) return -1; + retval = __Pyx_PyObject_IsTrue(x); + Py_DECREF(x); + return retval; +} +static PyObject* __Pyx_PyNumber_LongWrongResultType(PyObject* result) { + __Pyx_TypeName result_type_name = __Pyx_PyType_GetFullyQualifiedName(Py_TYPE(result)); + #if CYTHON_COMPILING_IN_LIMITED_API && __PYX_LIMITED_VERSION_HEX < 0x030d0000 + if (unlikely(!result_type_name)) goto bad; + #endif + if (PyLong_Check(result)) { + if (PyErr_WarnFormat(PyExc_DeprecationWarning, 1, + "__int__ returned non-int (type " __Pyx_FMT_TYPENAME "). " + "The ability to return an instance of a strict subclass of int is deprecated, " + "and may be removed in a future version of Python.", + result_type_name)) { + __Pyx_DECREF_TypeName(result_type_name); + goto bad; + } + __Pyx_DECREF_TypeName(result_type_name); + return result; + } + PyErr_Format(PyExc_TypeError, + "__int__ returned non-int (type " __Pyx_FMT_TYPENAME ")", + result_type_name); + __Pyx_DECREF_TypeName(result_type_name); +bad: + Py_DECREF(result); + return NULL; +} +static CYTHON_INLINE PyObject* __Pyx_PyNumber_Long(PyObject* x) { +#if CYTHON_USE_TYPE_SLOTS + PyNumberMethods *m; +#endif + PyObject *res = NULL; + if (likely(PyLong_Check(x))) + return __Pyx_NewRef(x); +#if CYTHON_USE_TYPE_SLOTS + m = Py_TYPE(x)->tp_as_number; + if (likely(m && m->nb_int)) { + res = m->nb_int(x); + } +#else + if (!PyBytes_CheckExact(x) && !PyUnicode_CheckExact(x)) { + res = PyNumber_Long(x); + } +#endif + if (likely(res)) { + if (unlikely(!PyLong_CheckExact(res))) { + return __Pyx_PyNumber_LongWrongResultType(res); + } + } + else if (!PyErr_Occurred()) { + PyErr_SetString(PyExc_TypeError, + "an integer is required"); + } + return res; +} +static CYTHON_INLINE Py_ssize_t __Pyx_PyLong_AsSsize_t(PyObject* b) { + #if CYTHON_USE_PYLONG_INTERNALS + Py_ssize_t ival; + const Py_ssize_t size = __Pyx_PyLong_DigitCount(b); + const int is_neg = __Pyx_PyLong_IsNeg(b); + const digit* digits = __Pyx_PyLong_Digits(b); + if (size == 0) return 0; + #if SIZEOF_SIZE_T * 8 > 1 * PyLong_SHIFT + if (size == 1) { + ival = (Py_ssize_t) (((size_t)digits[0])); + } else + #endif + #if SIZEOF_SIZE_T * 8 > 2 * PyLong_SHIFT + if (size == 2) { + ival = (Py_ssize_t) (((((size_t)digits[1]) << PyLong_SHIFT) | (size_t)digits[0])); + } else + #endif + #if SIZEOF_SIZE_T * 8 > 3 * PyLong_SHIFT + if (size == 3) { + ival = (Py_ssize_t) (((((((size_t)digits[2]) << PyLong_SHIFT) | (size_t)digits[1]) << PyLong_SHIFT) | (size_t)digits[0])); + } else + #endif + #if SIZEOF_SIZE_T * 8 > 4 * PyLong_SHIFT + if (size == 4) { + ival = (Py_ssize_t) (((((((((size_t)digits[3]) << PyLong_SHIFT) | (size_t)digits[2]) << PyLong_SHIFT) | (size_t)digits[1]) << PyLong_SHIFT) | (size_t)digits[0])); + } else + #endif + return PyLong_AsSsize_t(b); + return is_neg ? -ival : ival; + #else + return PyLong_AsSsize_t(b); + #endif +} +static CYTHON_INLINE Py_ssize_t __Pyx_PyIndex_AsSsize_t(PyObject* b) { + if (likely(PyLong_Check(b))) { + return __Pyx_PyLong_AsSsize_t(b); + } else { + Py_ssize_t ival; + PyObject *x = PyNumber_Index(b); + if (unlikely(!x)) return -1; + ival = __Pyx_PyLong_AsSsize_t(x); + Py_DECREF(x); + return ival; + } +} +static CYTHON_INLINE Py_hash_t __Pyx_PyIndex_AsHash_t(PyObject* o) { + if (sizeof(Py_hash_t) == sizeof(Py_ssize_t)) { + return (Py_hash_t) __Pyx_PyIndex_AsSsize_t(o); + } else { + Py_ssize_t ival; + PyObject *x; + x = PyNumber_Index(o); + if (!x) return -1; + ival = PyLong_AsLong(x); + Py_DECREF(x); + return ival; + } +} +static CYTHON_INLINE PyObject *__Pyx_Owned_Py_None(int b) { + CYTHON_UNUSED_VAR(b); + return __Pyx_NewRef(Py_None); +} +static CYTHON_INLINE PyObject * __Pyx_PyBool_FromLong(long b) { + return __Pyx_NewRef(b ? Py_True: Py_False); +} +static CYTHON_INLINE PyObject * __Pyx_PyLong_FromSize_t(size_t ival) { + return PyLong_FromSize_t(ival); +} + + +/* MultiPhaseInitModuleState */ +#if CYTHON_PEP489_MULTI_PHASE_INIT && CYTHON_USE_MODULE_STATE +#ifndef CYTHON_MODULE_STATE_LOOKUP_THREAD_SAFE +#if (CYTHON_COMPILING_IN_LIMITED_API || PY_VERSION_HEX >= 0x030C0000) + #define CYTHON_MODULE_STATE_LOOKUP_THREAD_SAFE 1 +#else + #define CYTHON_MODULE_STATE_LOOKUP_THREAD_SAFE 0 +#endif +#endif +#if CYTHON_MODULE_STATE_LOOKUP_THREAD_SAFE && !CYTHON_ATOMICS +#error "Module state with PEP489 requires atomics. Currently that's one of\ + C11, C++11, gcc atomic intrinsics or MSVC atomic intrinsics" +#endif +#if !CYTHON_MODULE_STATE_LOOKUP_THREAD_SAFE +#define __Pyx_ModuleStateLookup_Lock() +#define __Pyx_ModuleStateLookup_Unlock() +#elif !CYTHON_COMPILING_IN_LIMITED_API && PY_VERSION_HEX >= 0x030d0000 +static PyMutex __Pyx_ModuleStateLookup_mutex = {0}; +#define __Pyx_ModuleStateLookup_Lock() PyMutex_Lock(&__Pyx_ModuleStateLookup_mutex) +#define __Pyx_ModuleStateLookup_Unlock() PyMutex_Unlock(&__Pyx_ModuleStateLookup_mutex) +#elif defined(__cplusplus) && __cplusplus >= 201103L +#include +static std::mutex __Pyx_ModuleStateLookup_mutex; +#define __Pyx_ModuleStateLookup_Lock() __Pyx_ModuleStateLookup_mutex.lock() +#define __Pyx_ModuleStateLookup_Unlock() __Pyx_ModuleStateLookup_mutex.unlock() +#elif defined(__STDC_VERSION__) && (__STDC_VERSION__ > 201112L) && !defined(__STDC_NO_THREADS__) +#include +static mtx_t __Pyx_ModuleStateLookup_mutex; +static once_flag __Pyx_ModuleStateLookup_mutex_once_flag = ONCE_FLAG_INIT; +static void __Pyx_ModuleStateLookup_initialize_mutex(void) { + mtx_init(&__Pyx_ModuleStateLookup_mutex, mtx_plain); +} +#define __Pyx_ModuleStateLookup_Lock()\ + call_once(&__Pyx_ModuleStateLookup_mutex_once_flag, __Pyx_ModuleStateLookup_initialize_mutex);\ + mtx_lock(&__Pyx_ModuleStateLookup_mutex) +#define __Pyx_ModuleStateLookup_Unlock() mtx_unlock(&__Pyx_ModuleStateLookup_mutex) +#elif defined(HAVE_PTHREAD_H) +#include +static pthread_mutex_t __Pyx_ModuleStateLookup_mutex = PTHREAD_MUTEX_INITIALIZER; +#define __Pyx_ModuleStateLookup_Lock() pthread_mutex_lock(&__Pyx_ModuleStateLookup_mutex) +#define __Pyx_ModuleStateLookup_Unlock() pthread_mutex_unlock(&__Pyx_ModuleStateLookup_mutex) +#elif defined(_WIN32) +#include // synchapi.h on its own doesn't work +static SRWLOCK __Pyx_ModuleStateLookup_mutex = SRWLOCK_INIT; +#define __Pyx_ModuleStateLookup_Lock() AcquireSRWLockExclusive(&__Pyx_ModuleStateLookup_mutex) +#define __Pyx_ModuleStateLookup_Unlock() ReleaseSRWLockExclusive(&__Pyx_ModuleStateLookup_mutex) +#else +#error "No suitable lock available for CYTHON_MODULE_STATE_LOOKUP_THREAD_SAFE.\ + Requires C standard >= C11, or C++ standard >= C++11,\ + or pthreads, or the Windows 32 API, or Python >= 3.13." +#endif +typedef struct { + int64_t id; + PyObject *module; +} __Pyx_InterpreterIdAndModule; +typedef struct { + char interpreter_id_as_index; + Py_ssize_t count; + Py_ssize_t allocated; + __Pyx_InterpreterIdAndModule table[1]; +} __Pyx_ModuleStateLookupData; +#define __PYX_MODULE_STATE_LOOKUP_SMALL_SIZE 32 +#if CYTHON_MODULE_STATE_LOOKUP_THREAD_SAFE +static __pyx_atomic_int_type __Pyx_ModuleStateLookup_read_counter = 0; +#endif +#if CYTHON_MODULE_STATE_LOOKUP_THREAD_SAFE +static __pyx_atomic_ptr_type __Pyx_ModuleStateLookup_data = 0; +#else +static __Pyx_ModuleStateLookupData* __Pyx_ModuleStateLookup_data = NULL; +#endif +static __Pyx_InterpreterIdAndModule* __Pyx_State_FindModuleStateLookupTableLowerBound( + __Pyx_InterpreterIdAndModule* table, + Py_ssize_t count, + int64_t interpreterId) { + __Pyx_InterpreterIdAndModule* begin = table; + __Pyx_InterpreterIdAndModule* end = begin + count; + if (begin->id == interpreterId) { + return begin; + } + while ((end - begin) > __PYX_MODULE_STATE_LOOKUP_SMALL_SIZE) { + __Pyx_InterpreterIdAndModule* halfway = begin + (end - begin)/2; + if (halfway->id == interpreterId) { + return halfway; + } + if (halfway->id < interpreterId) { + begin = halfway; + } else { + end = halfway; + } + } + for (; begin < end; ++begin) { + if (begin->id >= interpreterId) return begin; + } + return begin; +} +static PyObject *__Pyx_State_FindModule(CYTHON_UNUSED void* dummy) { + int64_t interpreter_id = PyInterpreterState_GetID(PyInterpreterState_Get()); + if (interpreter_id == -1) return NULL; +#if CYTHON_MODULE_STATE_LOOKUP_THREAD_SAFE + __Pyx_ModuleStateLookupData* data = (__Pyx_ModuleStateLookupData*)__pyx_atomic_pointer_load_relaxed(&__Pyx_ModuleStateLookup_data); + { + __pyx_atomic_incr_acq_rel(&__Pyx_ModuleStateLookup_read_counter); + if (likely(data)) { + __Pyx_ModuleStateLookupData* new_data = (__Pyx_ModuleStateLookupData*)__pyx_atomic_pointer_load_acquire(&__Pyx_ModuleStateLookup_data); + if (likely(data == new_data)) { + goto read_finished; + } + } + __pyx_atomic_decr_acq_rel(&__Pyx_ModuleStateLookup_read_counter); + __Pyx_ModuleStateLookup_Lock(); + __pyx_atomic_incr_relaxed(&__Pyx_ModuleStateLookup_read_counter); + data = (__Pyx_ModuleStateLookupData*)__pyx_atomic_pointer_load_relaxed(&__Pyx_ModuleStateLookup_data); + __Pyx_ModuleStateLookup_Unlock(); + } + read_finished:; +#else + __Pyx_ModuleStateLookupData* data = __Pyx_ModuleStateLookup_data; +#endif + __Pyx_InterpreterIdAndModule* found = NULL; + if (unlikely(!data)) goto end; + if (data->interpreter_id_as_index) { + if (interpreter_id < data->count) { + found = data->table+interpreter_id; + } + } else { + found = __Pyx_State_FindModuleStateLookupTableLowerBound( + data->table, data->count, interpreter_id); + } + end: + { + PyObject *result=NULL; + if (found && found->id == interpreter_id) { + result = found->module; + } +#if CYTHON_MODULE_STATE_LOOKUP_THREAD_SAFE + __pyx_atomic_decr_acq_rel(&__Pyx_ModuleStateLookup_read_counter); +#endif + return result; + } +} +#if CYTHON_MODULE_STATE_LOOKUP_THREAD_SAFE +static void __Pyx_ModuleStateLookup_wait_until_no_readers(void) { + while (__pyx_atomic_load(&__Pyx_ModuleStateLookup_read_counter) != 0); +} +#else +#define __Pyx_ModuleStateLookup_wait_until_no_readers() +#endif +static int __Pyx_State_AddModuleInterpIdAsIndex(__Pyx_ModuleStateLookupData **old_data, PyObject* module, int64_t interpreter_id) { + Py_ssize_t to_allocate = (*old_data)->allocated; + while (to_allocate <= interpreter_id) { + if (to_allocate == 0) to_allocate = 1; + else to_allocate *= 2; + } + __Pyx_ModuleStateLookupData *new_data = *old_data; + if (to_allocate != (*old_data)->allocated) { + new_data = (__Pyx_ModuleStateLookupData *)realloc( + *old_data, + sizeof(__Pyx_ModuleStateLookupData)+(to_allocate-1)*sizeof(__Pyx_InterpreterIdAndModule)); + if (!new_data) { + PyErr_NoMemory(); + return -1; + } + for (Py_ssize_t i = new_data->allocated; i < to_allocate; ++i) { + new_data->table[i].id = i; + new_data->table[i].module = NULL; + } + new_data->allocated = to_allocate; + } + new_data->table[interpreter_id].module = module; + if (new_data->count < interpreter_id+1) { + new_data->count = interpreter_id+1; + } + *old_data = new_data; + return 0; +} +static void __Pyx_State_ConvertFromInterpIdAsIndex(__Pyx_ModuleStateLookupData *data) { + __Pyx_InterpreterIdAndModule *read = data->table; + __Pyx_InterpreterIdAndModule *write = data->table; + __Pyx_InterpreterIdAndModule *end = read + data->count; + for (; readmodule) { + write->id = read->id; + write->module = read->module; + ++write; + } + } + data->count = write - data->table; + for (; writeid = 0; + write->module = NULL; + } + data->interpreter_id_as_index = 0; +} +static int __Pyx_State_AddModule(PyObject* module, CYTHON_UNUSED void* dummy) { + int64_t interpreter_id = PyInterpreterState_GetID(PyInterpreterState_Get()); + if (interpreter_id == -1) return -1; + int result = 0; + __Pyx_ModuleStateLookup_Lock(); +#if CYTHON_MODULE_STATE_LOOKUP_THREAD_SAFE + __Pyx_ModuleStateLookupData *old_data = (__Pyx_ModuleStateLookupData *) + __pyx_atomic_pointer_exchange(&__Pyx_ModuleStateLookup_data, 0); +#else + __Pyx_ModuleStateLookupData *old_data = __Pyx_ModuleStateLookup_data; +#endif + __Pyx_ModuleStateLookupData *new_data = old_data; + if (!new_data) { + new_data = (__Pyx_ModuleStateLookupData *)calloc(1, sizeof(__Pyx_ModuleStateLookupData)); + if (!new_data) { + result = -1; + PyErr_NoMemory(); + goto end; + } + new_data->allocated = 1; + new_data->interpreter_id_as_index = 1; + } + __Pyx_ModuleStateLookup_wait_until_no_readers(); + if (new_data->interpreter_id_as_index) { + if (interpreter_id < __PYX_MODULE_STATE_LOOKUP_SMALL_SIZE) { + result = __Pyx_State_AddModuleInterpIdAsIndex(&new_data, module, interpreter_id); + goto end; + } + __Pyx_State_ConvertFromInterpIdAsIndex(new_data); + } + { + Py_ssize_t insert_at = 0; + { + __Pyx_InterpreterIdAndModule* lower_bound = __Pyx_State_FindModuleStateLookupTableLowerBound( + new_data->table, new_data->count, interpreter_id); + assert(lower_bound); + insert_at = lower_bound - new_data->table; + if (unlikely(insert_at < new_data->count && lower_bound->id == interpreter_id)) { + lower_bound->module = module; + goto end; // already in table, nothing more to do + } + } + if (new_data->count+1 >= new_data->allocated) { + Py_ssize_t to_allocate = (new_data->count+1)*2; + new_data = + (__Pyx_ModuleStateLookupData*)realloc( + new_data, + sizeof(__Pyx_ModuleStateLookupData) + + (to_allocate-1)*sizeof(__Pyx_InterpreterIdAndModule)); + if (!new_data) { + result = -1; + new_data = old_data; + PyErr_NoMemory(); + goto end; + } + new_data->allocated = to_allocate; + } + ++new_data->count; + int64_t last_id = interpreter_id; + PyObject *last_module = module; + for (Py_ssize_t i=insert_at; icount; ++i) { + int64_t current_id = new_data->table[i].id; + new_data->table[i].id = last_id; + last_id = current_id; + PyObject *current_module = new_data->table[i].module; + new_data->table[i].module = last_module; + last_module = current_module; + } + } + end: +#if CYTHON_MODULE_STATE_LOOKUP_THREAD_SAFE + __pyx_atomic_pointer_exchange(&__Pyx_ModuleStateLookup_data, new_data); +#else + __Pyx_ModuleStateLookup_data = new_data; +#endif + __Pyx_ModuleStateLookup_Unlock(); + return result; +} +static int __Pyx_State_RemoveModule(CYTHON_UNUSED void* dummy) { + int64_t interpreter_id = PyInterpreterState_GetID(PyInterpreterState_Get()); + if (interpreter_id == -1) return -1; + __Pyx_ModuleStateLookup_Lock(); +#if CYTHON_MODULE_STATE_LOOKUP_THREAD_SAFE + __Pyx_ModuleStateLookupData *data = (__Pyx_ModuleStateLookupData *) + __pyx_atomic_pointer_exchange(&__Pyx_ModuleStateLookup_data, 0); +#else + __Pyx_ModuleStateLookupData *data = __Pyx_ModuleStateLookup_data; +#endif + if (data->interpreter_id_as_index) { + if (interpreter_id < data->count) { + data->table[interpreter_id].module = NULL; + } + goto done; + } + { + __Pyx_ModuleStateLookup_wait_until_no_readers(); + __Pyx_InterpreterIdAndModule* lower_bound = __Pyx_State_FindModuleStateLookupTableLowerBound( + data->table, data->count, interpreter_id); + if (!lower_bound) goto done; + if (lower_bound->id != interpreter_id) goto done; + __Pyx_InterpreterIdAndModule *end = data->table+data->count; + for (;lower_boundid = (lower_bound+1)->id; + lower_bound->module = (lower_bound+1)->module; + } + } + --data->count; + if (data->count == 0) { + free(data); + data = NULL; + } + done: +#if CYTHON_MODULE_STATE_LOOKUP_THREAD_SAFE + __pyx_atomic_pointer_exchange(&__Pyx_ModuleStateLookup_data, data); +#else + __Pyx_ModuleStateLookup_data = data; +#endif + __Pyx_ModuleStateLookup_Unlock(); + return 0; +} +#endif + +/* #### Code section: utility_code_pragmas_end ### */ +#ifdef _MSC_VER +#pragma warning( pop ) +#endif + + + +/* #### Code section: end ### */ +#endif /* Py_PYTHON_H */ diff --git a/hug/lib/python3.12/site-packages/fontTools/cu2qu/cu2qu.cpython-312-x86_64-linux-gnu.so b/hug/lib/python3.12/site-packages/fontTools/cu2qu/cu2qu.cpython-312-x86_64-linux-gnu.so new file mode 100644 index 0000000000000000000000000000000000000000..ce1732c2ef822beca2637c0a6bc4624eeff92693 --- /dev/null +++ b/hug/lib/python3.12/site-packages/fontTools/cu2qu/cu2qu.cpython-312-x86_64-linux-gnu.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cde05e06f6fd1b3a480239841110e6bd8ba07a051d8030a6e4be049ab7825ee1 +size 1166864 diff --git a/hug/lib/python3.12/site-packages/fontTools/cu2qu/cu2qu.py b/hug/lib/python3.12/site-packages/fontTools/cu2qu/cu2qu.py new file mode 100644 index 0000000000000000000000000000000000000000..0e58ae2773a1d76f6e380d46bf270ee4c8430664 --- /dev/null +++ b/hug/lib/python3.12/site-packages/fontTools/cu2qu/cu2qu.py @@ -0,0 +1,568 @@ +# cython: language_level=3 +# distutils: define_macros=CYTHON_TRACE_NOGIL=1 + +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +try: + import cython +except (AttributeError, ImportError): + # if cython not installed, use mock module with no-op decorators and types + from fontTools.misc import cython +COMPILED = cython.compiled + +import math + +from .errors import Error as Cu2QuError, ApproxNotFoundError + +__all__ = ["curve_to_quadratic", "curves_to_quadratic"] + +MAX_N = 100 + +NAN = float("NaN") + + +@cython.cfunc +@cython.inline +@cython.returns(cython.double) +@cython.locals(v1=cython.complex, v2=cython.complex, result=cython.double) +def dot(v1, v2): + """Return the dot product of two vectors. + + Args: + v1 (complex): First vector. + v2 (complex): Second vector. + + Returns: + double: Dot product. + """ + result = (v1 * v2.conjugate()).real + # When vectors are perpendicular (i.e. dot product is 0), the above expression may + # yield slightly different results when running in pure Python vs C/Cython, + # both of which are correct within IEEE-754 floating-point precision. + # It's probably due to the different order of operations and roundings in each + # implementation. Because we are using the result in a denominator and catching + # ZeroDivisionError (see `calc_intersect`), it's best to normalize the result here. + if abs(result) < 1e-15: + result = 0.0 + return result + + +@cython.cfunc +@cython.locals(z=cython.complex, den=cython.double) +@cython.locals(zr=cython.double, zi=cython.double) +def _complex_div_by_real(z, den): + """Divide complex by real using Python's method (two separate divisions). + + This ensures bit-exact compatibility with Python's complex division, + avoiding C's multiply-by-reciprocal optimization that can cause 1 ULP differences + on some platforms/compilers (e.g. clang on macOS arm64). + + https://github.com/fonttools/fonttools/issues/3928 + """ + zr = z.real + zi = z.imag + return complex(zr / den, zi / den) + + +@cython.cfunc +@cython.inline +@cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex) +@cython.locals( + _1=cython.complex, _2=cython.complex, _3=cython.complex, _4=cython.complex +) +def calc_cubic_points(a, b, c, d): + _1 = d + _2 = _complex_div_by_real(c, 3.0) + d + _3 = _complex_div_by_real(b + c, 3.0) + _2 + _4 = a + d + c + b + return _1, _2, _3, _4 + + +@cython.cfunc +@cython.inline +@cython.locals( + p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex +) +@cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex) +def calc_cubic_parameters(p0, p1, p2, p3): + c = (p1 - p0) * 3.0 + b = (p2 - p1) * 3.0 - c + d = p0 + a = p3 - d - c - b + return a, b, c, d + + +@cython.cfunc +@cython.inline +@cython.locals( + p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex +) +def split_cubic_into_n_iter(p0, p1, p2, p3, n): + """Split a cubic Bezier into n equal parts. + + Splits the curve into `n` equal parts by curve time. + (t=0..1/n, t=1/n..2/n, ...) + + Args: + p0 (complex): Start point of curve. + p1 (complex): First handle of curve. + p2 (complex): Second handle of curve. + p3 (complex): End point of curve. + + Returns: + An iterator yielding the control points (four complex values) of the + subcurves. + """ + # Hand-coded special-cases + if n == 2: + return iter(split_cubic_into_two(p0, p1, p2, p3)) + if n == 3: + return iter(split_cubic_into_three(p0, p1, p2, p3)) + if n == 4: + a, b = split_cubic_into_two(p0, p1, p2, p3) + return iter( + split_cubic_into_two(a[0], a[1], a[2], a[3]) + + split_cubic_into_two(b[0], b[1], b[2], b[3]) + ) + if n == 6: + a, b = split_cubic_into_two(p0, p1, p2, p3) + return iter( + split_cubic_into_three(a[0], a[1], a[2], a[3]) + + split_cubic_into_three(b[0], b[1], b[2], b[3]) + ) + + return _split_cubic_into_n_gen(p0, p1, p2, p3, n) + + +@cython.locals( + p0=cython.complex, + p1=cython.complex, + p2=cython.complex, + p3=cython.complex, + n=cython.int, +) +@cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex) +@cython.locals( + dt=cython.double, delta_2=cython.double, delta_3=cython.double, i=cython.int +) +@cython.locals( + a1=cython.complex, b1=cython.complex, c1=cython.complex, d1=cython.complex +) +def _split_cubic_into_n_gen(p0, p1, p2, p3, n): + a, b, c, d = calc_cubic_parameters(p0, p1, p2, p3) + dt = 1 / n + delta_2 = dt * dt + delta_3 = dt * delta_2 + for i in range(n): + t1 = i * dt + t1_2 = t1 * t1 + # calc new a, b, c and d + a1 = a * delta_3 + b1 = (3 * a * t1 + b) * delta_2 + c1 = (2 * b * t1 + c + 3 * a * t1_2) * dt + d1 = a * t1 * t1_2 + b * t1_2 + c * t1 + d + yield calc_cubic_points(a1, b1, c1, d1) + + +@cython.cfunc +@cython.inline +@cython.locals( + p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex +) +@cython.locals(mid=cython.complex, deriv3=cython.complex) +def split_cubic_into_two(p0, p1, p2, p3): + """Split a cubic Bezier into two equal parts. + + Splits the curve into two equal parts at t = 0.5 + + Args: + p0 (complex): Start point of curve. + p1 (complex): First handle of curve. + p2 (complex): Second handle of curve. + p3 (complex): End point of curve. + + Returns: + tuple: Two cubic Beziers (each expressed as a tuple of four complex + values). + """ + mid = (p0 + 3 * (p1 + p2) + p3) * 0.125 + deriv3 = (p3 + p2 - p1 - p0) * 0.125 + return ( + (p0, (p0 + p1) * 0.5, mid - deriv3, mid), + (mid, mid + deriv3, (p2 + p3) * 0.5, p3), + ) + + +@cython.cfunc +@cython.inline +@cython.locals( + p0=cython.complex, + p1=cython.complex, + p2=cython.complex, + p3=cython.complex, +) +@cython.locals( + mid1=cython.complex, + deriv1=cython.complex, + mid2=cython.complex, + deriv2=cython.complex, +) +def split_cubic_into_three(p0, p1, p2, p3): + """Split a cubic Bezier into three equal parts. + + Splits the curve into three equal parts at t = 1/3 and t = 2/3 + + Args: + p0 (complex): Start point of curve. + p1 (complex): First handle of curve. + p2 (complex): Second handle of curve. + p3 (complex): End point of curve. + + Returns: + tuple: Three cubic Beziers (each expressed as a tuple of four complex + values). + """ + mid1 = (8 * p0 + 12 * p1 + 6 * p2 + p3) * (1 / 27) + deriv1 = (p3 + 3 * p2 - 4 * p0) * (1 / 27) + mid2 = (p0 + 6 * p1 + 12 * p2 + 8 * p3) * (1 / 27) + deriv2 = (4 * p3 - 3 * p1 - p0) * (1 / 27) + return ( + (p0, _complex_div_by_real(2 * p0 + p1, 3.0), mid1 - deriv1, mid1), + (mid1, mid1 + deriv1, mid2 - deriv2, mid2), + (mid2, mid2 + deriv2, _complex_div_by_real(p2 + 2 * p3, 3.0), p3), + ) + + +@cython.cfunc +@cython.inline +@cython.returns(cython.complex) +@cython.locals( + t=cython.double, + p0=cython.complex, + p1=cython.complex, + p2=cython.complex, + p3=cython.complex, +) +@cython.locals(_p1=cython.complex, _p2=cython.complex) +def cubic_approx_control(t, p0, p1, p2, p3): + """Approximate a cubic Bezier using a quadratic one. + + Args: + t (double): Position of control point. + p0 (complex): Start point of curve. + p1 (complex): First handle of curve. + p2 (complex): Second handle of curve. + p3 (complex): End point of curve. + + Returns: + complex: Location of candidate control point on quadratic curve. + """ + _p1 = p0 + (p1 - p0) * 1.5 + _p2 = p3 + (p2 - p3) * 1.5 + return _p1 + (_p2 - _p1) * t + + +@cython.cfunc +@cython.inline +@cython.returns(cython.complex) +@cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex) +@cython.locals(ab=cython.complex, cd=cython.complex, p=cython.complex, h=cython.double) +def calc_intersect(a, b, c, d): + """Calculate the intersection of two lines. + + Args: + a (complex): Start point of first line. + b (complex): End point of first line. + c (complex): Start point of second line. + d (complex): End point of second line. + + Returns: + complex: Location of intersection if one present, ``complex(NaN,NaN)`` + if no intersection was found. + """ + ab = b - a + cd = d - c + p = ab * 1j + try: + h = dot(p, a - c) / dot(p, cd) + except ZeroDivisionError: + # if 3 or 4 points are equal, we do have an intersection despite the zero-div: + # return one of the off-curves so that the algorithm can attempt a one-curve + # solution if it's within tolerance: + # https://github.com/linebender/kurbo/pull/484 + if b == c and (a == b or c == d): + return b + return complex(NAN, NAN) + return c + cd * h + + +@cython.cfunc +@cython.returns(cython.int) +@cython.locals( + tolerance=cython.double, + p0=cython.complex, + p1=cython.complex, + p2=cython.complex, + p3=cython.complex, +) +@cython.locals(mid=cython.complex, deriv3=cython.complex) +def cubic_farthest_fit_inside(p0, p1, p2, p3, tolerance): + """Check if a cubic Bezier lies within a given distance of the origin. + + "Origin" means *the* origin (0,0), not the start of the curve. Note that no + checks are made on the start and end positions of the curve; this function + only checks the inside of the curve. + + Args: + p0 (complex): Start point of curve. + p1 (complex): First handle of curve. + p2 (complex): Second handle of curve. + p3 (complex): End point of curve. + tolerance (double): Distance from origin. + + Returns: + bool: True if the cubic Bezier ``p`` entirely lies within a distance + ``tolerance`` of the origin, False otherwise. + """ + # First check p2 then p1, as p2 has higher error early on. + if abs(p2) <= tolerance and abs(p1) <= tolerance: + return True + + # Split. + mid = (p0 + 3 * (p1 + p2) + p3) * 0.125 + if abs(mid) > tolerance: + return False + deriv3 = (p3 + p2 - p1 - p0) * 0.125 + return cubic_farthest_fit_inside( + p0, (p0 + p1) * 0.5, mid - deriv3, mid, tolerance + ) and cubic_farthest_fit_inside(mid, mid + deriv3, (p2 + p3) * 0.5, p3, tolerance) + + +@cython.cfunc +@cython.inline +@cython.locals(tolerance=cython.double) +@cython.locals( + q1=cython.complex, + c0=cython.complex, + c1=cython.complex, + c2=cython.complex, + c3=cython.complex, +) +def cubic_approx_quadratic(cubic, tolerance): + """Approximate a cubic Bezier with a single quadratic within a given tolerance. + + Args: + cubic (sequence): Four complex numbers representing control points of + the cubic Bezier curve. + tolerance (double): Permitted deviation from the original curve. + + Returns: + Three complex numbers representing control points of the quadratic + curve if it fits within the given tolerance, or ``None`` if no suitable + curve could be calculated. + """ + + q1 = calc_intersect(cubic[0], cubic[1], cubic[2], cubic[3]) + if math.isnan(q1.imag): + return None + c0 = cubic[0] + c3 = cubic[3] + c1 = c0 + (q1 - c0) * (2 / 3) + c2 = c3 + (q1 - c3) * (2 / 3) + if not cubic_farthest_fit_inside(0, c1 - cubic[1], c2 - cubic[2], 0, tolerance): + return None + return c0, q1, c3 + + +@cython.cfunc +@cython.locals(n=cython.int, tolerance=cython.double) +@cython.locals(i=cython.int) +@cython.locals(all_quadratic=cython.int) +@cython.locals( + c0=cython.complex, c1=cython.complex, c2=cython.complex, c3=cython.complex +) +@cython.locals( + q0=cython.complex, + q1=cython.complex, + next_q1=cython.complex, + q2=cython.complex, + d1=cython.complex, +) +def cubic_approx_spline(cubic, n, tolerance, all_quadratic): + """Approximate a cubic Bezier curve with a spline of n quadratics. + + Args: + cubic (sequence): Four complex numbers representing control points of + the cubic Bezier curve. + n (int): Number of quadratic Bezier curves in the spline. + tolerance (double): Permitted deviation from the original curve. + + Returns: + A list of ``n+2`` complex numbers, representing control points of the + quadratic spline if it fits within the given tolerance, or ``None`` if + no suitable spline could be calculated. + """ + + if n == 1: + return cubic_approx_quadratic(cubic, tolerance) + if n == 2 and all_quadratic == False: + return cubic + + cubics = split_cubic_into_n_iter(cubic[0], cubic[1], cubic[2], cubic[3], n) + + # calculate the spline of quadratics and check errors at the same time. + next_cubic = next(cubics) + next_q1 = cubic_approx_control( + 0, next_cubic[0], next_cubic[1], next_cubic[2], next_cubic[3] + ) + q2 = cubic[0] + d1 = 0j + spline = [cubic[0], next_q1] + for i in range(1, n + 1): + # Current cubic to convert + c0, c1, c2, c3 = next_cubic + + # Current quadratic approximation of current cubic + q0 = q2 + q1 = next_q1 + if i < n: + next_cubic = next(cubics) + next_q1 = cubic_approx_control( + i / (n - 1), next_cubic[0], next_cubic[1], next_cubic[2], next_cubic[3] + ) + spline.append(next_q1) + q2 = (q1 + next_q1) * 0.5 + else: + q2 = c3 + + # End-point deltas + d0 = d1 + d1 = q2 - c3 + + if abs(d1) > tolerance or not cubic_farthest_fit_inside( + d0, + q0 + (q1 - q0) * (2 / 3) - c1, + q2 + (q1 - q2) * (2 / 3) - c2, + d1, + tolerance, + ): + return None + spline.append(cubic[3]) + + return spline + + +@cython.locals(max_err=cython.double) +@cython.locals(n=cython.int) +@cython.locals(all_quadratic=cython.int) +def curve_to_quadratic(curve, max_err, all_quadratic=True): + """Approximate a cubic Bezier curve with a spline of n quadratics. + + Args: + cubic (sequence): Four 2D tuples representing control points of + the cubic Bezier curve. + max_err (double): Permitted deviation from the original curve. + all_quadratic (bool): If True (default) returned value is a + quadratic spline. If False, it's either a single quadratic + curve or a single cubic curve. + + Returns: + If all_quadratic is True: A list of 2D tuples representing + control points of the quadratic spline. + + If all_quadratic is False: Either a quadratic curve (if length + of output is 3), or a cubic curve (if length of output is 4). + + Raises: + fontTools.cu2qu.errors.ApproxNotFoundError: if no suitable + approximation can be found with the given parameters. + """ + + curve = [complex(*p) for p in curve] + + for n in range(1, MAX_N + 1): + spline = cubic_approx_spline(curve, n, max_err, all_quadratic) + if spline is not None: + # done. go home + return [(s.real, s.imag) for s in spline] + + raise ApproxNotFoundError(curve) + + +@cython.locals(l=cython.int, last_i=cython.int, i=cython.int) +@cython.locals(all_quadratic=cython.int) +def curves_to_quadratic(curves, max_errors, all_quadratic=True): + """Return quadratic Bezier splines approximating the input cubic Beziers. + + Args: + curves: A sequence of *n* curves, each curve being a sequence of four + 2D tuples. + max_errors: A sequence of *n* floats representing the maximum permissible + deviation from each of the cubic Bezier curves. + all_quadratic (bool): If True (default) returned values are a + quadratic spline. If False, they are either a single quadratic + curve or a single cubic curve. + + Example:: + + >>> curves_to_quadratic( [ + ... [ (50,50), (100,100), (150,100), (200,50) ], + ... [ (75,50), (120,100), (150,75), (200,60) ] + ... ], [1,1] ) + [[(50.0, 50.0), (75.0, 75.0), (125.0, 91.66666666666666), (175.0, 75.0), (200.0, 50.0)], [(75.0, 50.0), (97.5, 75.0), (135.41666666666666, 82.08333333333333), (175.0, 67.5), (200.0, 60.0)]] + + The returned splines have "implied oncurve points" suitable for use in + TrueType ``glif`` outlines - i.e. in the first spline returned above, + the first quadratic segment runs from (50,50) to + ( (75 + 125)/2 , (120 + 91.666..)/2 ) = (100, 83.333...). + + Returns: + If all_quadratic is True, a list of splines, each spline being a list + of 2D tuples. If ``curves`` is empty, returns an empty list. + + If all_quadratic is False, a list of curves, each curve being a quadratic + (length 3), or cubic (length 4). + + Raises: + ValueError: if ``max_errors`` does not match the number of curves. + fontTools.cu2qu.errors.ApproxNotFoundError: if no suitable approximation + can be found for all curves with the given parameters. + """ + + curves = [[complex(*p) for p in curve] for curve in curves] + if len(max_errors) != len(curves): + raise ValueError("max_errors must match the number of curves") + if not curves: + return [] + + l = len(curves) + splines = [None] * l + last_i = i = 0 + n = 1 + while True: + spline = cubic_approx_spline(curves[i], n, max_errors[i], all_quadratic) + if spline is None: + if n == MAX_N: + break + n += 1 + last_i = i + continue + splines[i] = spline + i = (i + 1) % l + if i == last_i: + # done. go home + return [[(s.real, s.imag) for s in spline] for spline in splines] + + raise ApproxNotFoundError(curves) diff --git a/hug/lib/python3.12/site-packages/fontTools/cu2qu/errors.py b/hug/lib/python3.12/site-packages/fontTools/cu2qu/errors.py new file mode 100644 index 0000000000000000000000000000000000000000..fa3dc42937131c5db54890dde8f519b15f5d0ff1 --- /dev/null +++ b/hug/lib/python3.12/site-packages/fontTools/cu2qu/errors.py @@ -0,0 +1,77 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class Error(Exception): + """Base Cu2Qu exception class for all other errors.""" + + +class ApproxNotFoundError(Error): + def __init__(self, curve): + message = "no approximation found: %s" % curve + super().__init__(message) + self.curve = curve + + +class UnequalZipLengthsError(Error): + pass + + +class IncompatibleGlyphsError(Error): + def __init__(self, glyphs): + assert len(glyphs) > 1 + self.glyphs = glyphs + names = set(repr(g.name) for g in glyphs) + if len(names) > 1: + self.combined_name = "{%s}" % ", ".join(sorted(names)) + else: + self.combined_name = names.pop() + + def __repr__(self): + return "<%s %s>" % (type(self).__name__, self.combined_name) + + +class IncompatibleSegmentNumberError(IncompatibleGlyphsError): + def __str__(self): + return "Glyphs named %s have different number of segments" % ( + self.combined_name + ) + + +class IncompatibleSegmentTypesError(IncompatibleGlyphsError): + def __init__(self, glyphs, segments): + IncompatibleGlyphsError.__init__(self, glyphs) + self.segments = segments + + def __str__(self): + lines = [] + ndigits = len(str(max(self.segments))) + for i, tags in sorted(self.segments.items()): + lines.append( + "%s: (%s)" % (str(i).rjust(ndigits), ", ".join(repr(t) for t in tags)) + ) + return "Glyphs named %s have incompatible segment types:\n %s" % ( + self.combined_name, + "\n ".join(lines), + ) + + +class IncompatibleFontsError(Error): + def __init__(self, glyph_errors): + self.glyph_errors = glyph_errors + + def __str__(self): + return "fonts contains incompatible glyphs: %s" % ( + ", ".join(repr(g) for g in sorted(self.glyph_errors.keys())) + ) diff --git a/hug/lib/python3.12/site-packages/fontTools/cu2qu/ufo.py b/hug/lib/python3.12/site-packages/fontTools/cu2qu/ufo.py new file mode 100644 index 0000000000000000000000000000000000000000..f81658918f88e7664e7807c77b4dcb34cb802f0d --- /dev/null +++ b/hug/lib/python3.12/site-packages/fontTools/cu2qu/ufo.py @@ -0,0 +1,382 @@ +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +"""Converts cubic bezier curves to quadratic splines. + +Conversion is performed such that the quadratic splines keep the same end-curve +tangents as the original cubics. The approach is iterative, increasing the +number of segments for a spline until the error gets below a bound. + +Respective curves from multiple fonts will be converted at once to ensure that +the resulting splines are interpolation-compatible. +""" + +import logging +from fontTools.pens.basePen import AbstractPen +from fontTools.pens.pointPen import PointToSegmentPen +from fontTools.pens.reverseContourPen import ReverseContourPen + +from . import curves_to_quadratic +from .errors import ( + UnequalZipLengthsError, + IncompatibleSegmentNumberError, + IncompatibleSegmentTypesError, + IncompatibleGlyphsError, + IncompatibleFontsError, +) + +__all__ = ["fonts_to_quadratic", "font_to_quadratic"] + +# The default approximation error below is a relative value (1/1000 of the EM square). +# Later on, we convert it to absolute font units by multiplying it by a font's UPEM +# (see fonts_to_quadratic). +DEFAULT_MAX_ERR = 0.001 +CURVE_TYPE_LIB_KEY = "com.github.googlei18n.cu2qu.curve_type" + +logger = logging.getLogger(__name__) + + +_zip = zip + + +def zip(*args): + """Ensure each argument to zip has the same length. Also make sure a list is + returned for python 2/3 compatibility. + """ + + if len(set(len(a) for a in args)) != 1: + raise UnequalZipLengthsError(*args) + return list(_zip(*args)) + + +def _validate_positive_tolerance(value, name): + if value <= 0: + raise ValueError(f"{name} must be greater than zero") + + +def _validate_positive_tolerances(values, name): + for value in values: + _validate_positive_tolerance(value, name) + + +def _validate_length(values, expected, name): + if len(values) != expected: + raise ValueError(f"{name} must match the number of inputs") + + +class GetSegmentsPen(AbstractPen): + """Pen to collect segments into lists of points for conversion. + + Curves always include their initial on-curve point, so some points are + duplicated between segments. + """ + + def __init__(self): + self._last_pt = None + self.segments = [] + + def _add_segment(self, tag, *args): + if tag in ["move", "line", "qcurve", "curve"]: + self._last_pt = args[-1] + self.segments.append((tag, args)) + + def moveTo(self, pt): + self._add_segment("move", pt) + + def lineTo(self, pt): + self._add_segment("line", pt) + + def qCurveTo(self, *points): + self._add_segment("qcurve", self._last_pt, *points) + + def curveTo(self, *points): + self._add_segment("curve", self._last_pt, *points) + + def closePath(self): + self._add_segment("close") + + def endPath(self): + self._add_segment("end") + + def addComponent(self, glyphName, transformation): + pass + + +def _get_segments(glyph): + """Get a glyph's segments as extracted by GetSegmentsPen.""" + + pen = GetSegmentsPen() + # glyph.draw(pen) + # We can't simply draw the glyph with the pen, but we must initialize the + # PointToSegmentPen explicitly with outputImpliedClosingLine=True. + # By default PointToSegmentPen does not outputImpliedClosingLine -- unless + # last and first point on closed contour are duplicated. Because we are + # converting multiple glyphs at the same time, we want to make sure + # this function returns the same number of segments, whether or not + # the last and first point overlap. + # https://github.com/googlefonts/fontmake/issues/572 + # https://github.com/fonttools/fonttools/pull/1720 + pointPen = PointToSegmentPen(pen, outputImpliedClosingLine=True) + glyph.drawPoints(pointPen) + return pen.segments + + +def _set_segments(glyph, segments, reverse_direction): + """Draw segments as extracted by GetSegmentsPen back to a glyph.""" + + glyph.clearContours() + pen = glyph.getPen() + if reverse_direction: + pen = ReverseContourPen(pen) + for tag, args in segments: + if tag == "move": + pen.moveTo(*args) + elif tag == "line": + pen.lineTo(*args) + elif tag == "curve": + pen.curveTo(*args[1:]) + elif tag == "qcurve": + pen.qCurveTo(*args[1:]) + elif tag == "close": + pen.closePath() + elif tag == "end": + pen.endPath() + else: + raise AssertionError('Unhandled segment type "%s"' % tag) + + +def _segments_to_quadratic(segments, max_err, stats, all_quadratic=True): + """Return quadratic approximations of cubic segments.""" + + assert all(s[0] == "curve" for s in segments), "Non-cubic given to convert" + + new_points = curves_to_quadratic([s[1] for s in segments], max_err, all_quadratic) + n = len(new_points[0]) + assert all(len(s) == n for s in new_points[1:]), "Converted incompatibly" + + spline_length = str(n - 2) + stats[spline_length] = stats.get(spline_length, 0) + 1 + + if all_quadratic or n == 3: + return [("qcurve", p) for p in new_points] + else: + return [("curve", p) for p in new_points] + + +def _glyphs_to_quadratic(glyphs, max_err, reverse_direction, stats, all_quadratic=True): + """Do the actual conversion of a set of compatible glyphs, after arguments + have been set up. + + Empty glyphs (without contours) are ignored and passed through unchanged. + + Return True if the glyphs were modified, else return False. + """ + + # Skip empty glyphs (with zero contours) + non_empty_indices = [i for i, g in enumerate(glyphs) if len(g) > 0] + if not non_empty_indices: + return False + + glyphs = [glyphs[i] for i in non_empty_indices] + max_err = [max_err[i] for i in non_empty_indices] + + try: + segments_by_location = zip(*[_get_segments(g) for g in glyphs]) + except UnequalZipLengthsError: + raise IncompatibleSegmentNumberError(glyphs) + if not any(segments_by_location): + return False + + # always modify input glyphs if reverse_direction is True + glyphs_modified = reverse_direction + + new_segments_by_location = [] + incompatible = {} + for i, segments in enumerate(segments_by_location): + tag = segments[0][0] + if not all(s[0] == tag for s in segments[1:]): + incompatible[i] = [s[0] for s in segments] + elif tag == "curve": + new_segments = _segments_to_quadratic( + segments, max_err, stats, all_quadratic + ) + if all_quadratic or new_segments != segments: + glyphs_modified = True + segments = new_segments + new_segments_by_location.append(segments) + + if glyphs_modified: + new_segments_by_glyph = zip(*new_segments_by_location) + for glyph, new_segments in zip(glyphs, new_segments_by_glyph): + _set_segments(glyph, new_segments, reverse_direction) + + if incompatible: + raise IncompatibleSegmentTypesError(glyphs, segments=incompatible) + return glyphs_modified + + +def glyphs_to_quadratic( + glyphs, max_err=None, reverse_direction=False, stats=None, all_quadratic=True +): + """Convert the curves of a set of compatible of glyphs to quadratic. + + All curves will be converted to quadratic at once, ensuring interpolation + compatibility. If this is not required, calling glyphs_to_quadratic with one + glyph at a time may yield slightly more optimized results. + + Empty glyphs (without contours) are ignored and passed through unchanged. + + Return True if glyphs were modified, else return False. + + Raises IncompatibleGlyphsError if glyphs have non-interpolatable outlines. + """ + if stats is None: + stats = {} + + if max_err is None: + # assume 1000 is the default UPEM + max_err = DEFAULT_MAX_ERR * 1000 + + if isinstance(max_err, (list, tuple)): + max_errors = max_err + else: + max_errors = [max_err] * len(glyphs) + _validate_length(max_errors, len(glyphs), "max_err") + _validate_positive_tolerances(max_errors, "max_err") + + return _glyphs_to_quadratic( + glyphs, max_errors, reverse_direction, stats, all_quadratic + ) + + +def fonts_to_quadratic( + fonts, + max_err_em=None, + max_err=None, + reverse_direction=False, + stats=None, + dump_stats=False, + remember_curve_type=True, + all_quadratic=True, +): + """Convert the curves of a collection of fonts to quadratic. + + All curves will be converted to quadratic at once, ensuring interpolation + compatibility. If this is not required, calling fonts_to_quadratic with one + font at a time may yield slightly more optimized results. + + Empty glyphs (without contours) are ignored and passed through unchanged. + + Return the set of modified glyph names if any, else return an empty set. + + By default, cu2qu stores the curve type in the fonts' lib, under a private + key "com.github.googlei18n.cu2qu.curve_type", and will not try to convert + them again if the curve type is already set to "quadratic". + Setting 'remember_curve_type' to False disables this optimization. + + Raises IncompatibleFontsError if same-named glyphs from different fonts + have non-interpolatable outlines. + """ + + if remember_curve_type: + curve_types = {f.lib.get(CURVE_TYPE_LIB_KEY, "cubic") for f in fonts} + if len(curve_types) == 1: + curve_type = next(iter(curve_types)) + if curve_type in ("quadratic", "mixed"): + logger.info("Curves already converted to quadratic") + return False + elif curve_type == "cubic": + pass # keep converting + else: + raise NotImplementedError(curve_type) + elif len(curve_types) > 1: + # going to crash later if they do differ + logger.warning("fonts may contain different curve types") + + if stats is None: + stats = {} + + if max_err_em is not None and max_err is not None: + raise TypeError("Only one of max_err and max_err_em can be specified.") + if max_err_em is None and max_err is None: + max_err_em = DEFAULT_MAX_ERR + + if isinstance(max_err, (list, tuple)): + _validate_length(max_err, len(fonts), "max_err") + max_errors = max_err + _validate_positive_tolerances(max_errors, "max_err") + elif max_err is not None: + _validate_positive_tolerance(max_err, "max_err") + max_errors = [max_err] * len(fonts) + + if isinstance(max_err_em, (list, tuple)): + _validate_length(max_err_em, len(fonts), "max_err_em") + _validate_positive_tolerances(max_err_em, "max_err_em") + max_errors = [f.info.unitsPerEm * e for f, e in zip(fonts, max_err_em)] + elif max_err_em is not None: + _validate_positive_tolerance(max_err_em, "max_err_em") + max_errors = [f.info.unitsPerEm * max_err_em for f in fonts] + + modified = set() + glyph_errors = {} + for name in set().union(*(f.keys() for f in fonts)): + glyphs = [] + cur_max_errors = [] + for font, error in zip(fonts, max_errors): + if name in font: + glyphs.append(font[name]) + cur_max_errors.append(error) + try: + if _glyphs_to_quadratic( + glyphs, cur_max_errors, reverse_direction, stats, all_quadratic + ): + modified.add(name) + except IncompatibleGlyphsError as exc: + logger.error(exc) + glyph_errors[name] = exc + + if glyph_errors: + raise IncompatibleFontsError(glyph_errors) + + if modified and dump_stats: + spline_lengths = sorted(stats.keys()) + logger.info( + "New spline lengths: %s" + % (", ".join("%s: %d" % (l, stats[l]) for l in spline_lengths)) + ) + + if remember_curve_type: + for font in fonts: + curve_type = font.lib.get(CURVE_TYPE_LIB_KEY, "cubic") + new_curve_type = "quadratic" if all_quadratic else "mixed" + if curve_type != new_curve_type: + font.lib[CURVE_TYPE_LIB_KEY] = new_curve_type + return modified + + +def glyph_to_quadratic(glyph, **kwargs): + """Convenience wrapper around glyphs_to_quadratic, for just one glyph. + Return True if the glyph was modified, else return False. + """ + + return glyphs_to_quadratic([glyph], **kwargs) + + +def font_to_quadratic(font, **kwargs): + """Convenience wrapper around fonts_to_quadratic, for just one font. + Return the set of modified glyph names if any, else return empty set. + """ + + return fonts_to_quadratic([font], **kwargs) diff --git a/hug/lib/python3.12/site-packages/fontTools/designspaceLib/__init__.py b/hug/lib/python3.12/site-packages/fontTools/designspaceLib/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..65ef068c2a65ee6e45a9a8ad27f5fda1bd94f1b4 --- /dev/null +++ b/hug/lib/python3.12/site-packages/fontTools/designspaceLib/__init__.py @@ -0,0 +1,3392 @@ +""" +designSpaceDocument + +- Read and write designspace files +""" + +from __future__ import annotations + +import collections +import copy +import itertools +import math +import os +import posixpath +from io import BytesIO, StringIO +from textwrap import indent +from typing import Any, Dict, List, MutableMapping, Optional, Tuple, Union, cast + +from fontTools.misc import etree as ET +from fontTools.misc import plistlib +from fontTools.misc.loggingTools import LogMixin +from fontTools.misc.textTools import tobytes, tostr + +from .types import getVFUserRegion + +__all__ = [ + "AxisDescriptor", + "AxisLabelDescriptor", + "AxisMappingDescriptor", + "BaseDocReader", + "BaseDocWriter", + "DesignSpaceDocument", + "DesignSpaceDocumentError", + "DiscreteAxisDescriptor", + "getVFUserRegion", + "InstanceDescriptor", + "LocationLabelDescriptor", + "RangeAxisSubsetDescriptor", + "RuleDescriptor", + "SourceDescriptor", + "ValueAxisSubsetDescriptor", + "VariableFontDescriptor", +] + +# ElementTree allows to find namespace-prefixed elements, but not attributes +# so we have to do it ourselves for 'xml:lang' +XML_NS = "{http://www.w3.org/XML/1998/namespace}" +XML_LANG = XML_NS + "lang" + + +def posix(path): + """Normalize paths using forward slash to work also on Windows.""" + new_path = posixpath.join(*path.split(os.path.sep)) + if path.startswith("/"): + # The above transformation loses absolute paths + new_path = "/" + new_path + elif path.startswith(r"\\"): + # The above transformation loses leading slashes of UNC path mounts + new_path = "//" + new_path + return new_path + + +def posixpath_property(private_name): + """Generate a propery that holds a path always using forward slashes.""" + + def getter(self): + # Normal getter + return getattr(self, private_name) + + def setter(self, value): + # The setter rewrites paths using forward slashes + if value is not None: + value = posix(value) + setattr(self, private_name, value) + + return property(getter, setter) + + +class DesignSpaceDocumentError(Exception): + def __init__(self, msg, obj=None): + self.msg = msg + self.obj = obj + + def __str__(self): + return str(self.msg) + (": %r" % self.obj if self.obj is not None else "") + + +class AsDictMixin(object): + def asdict(self): + d = {} + for attr, value in self.__dict__.items(): + if attr.startswith("_"): + continue + if hasattr(value, "asdict"): + value = value.asdict() + elif isinstance(value, list): + value = [v.asdict() if hasattr(v, "asdict") else v for v in value] + d[attr] = value + return d + + +class SimpleDescriptor(AsDictMixin): + """Containers for a bunch of attributes""" + + # XXX this is ugly. The 'print' is inappropriate here, and instead of + # assert, it should simply return True/False + def compare(self, other): + # test if this object contains the same data as the other + for attr in self._attrs: + try: + assert getattr(self, attr) == getattr(other, attr) + except AssertionError: + print( + "failed attribute", + attr, + getattr(self, attr), + "!=", + getattr(other, attr), + ) + + def __repr__(self): + attrs = [f"{a}={repr(getattr(self, a))}," for a in self._attrs] + attrs = indent("\n".join(attrs), " ") + return f"{self.__class__.__name__}(\n{attrs}\n)" + + +class SourceDescriptor(SimpleDescriptor): + """Simple container for data related to the source + + .. code:: python + + doc = DesignSpaceDocument() + s1 = SourceDescriptor() + s1.path = masterPath1 + s1.name = "master.ufo1" + s1.font = defcon.Font("master.ufo1") + s1.location = dict(weight=0) + s1.familyName = "MasterFamilyName" + s1.styleName = "MasterStyleNameOne" + s1.localisedFamilyName = dict(fr="Caractère") + s1.mutedGlyphNames.append("A") + s1.mutedGlyphNames.append("Z") + doc.addSource(s1) + + """ + + flavor = "source" + _attrs = [ + "filename", + "path", + "name", + "layerName", + "location", + "copyLib", + "copyGroups", + "copyFeatures", + "muteKerning", + "muteInfo", + "mutedGlyphNames", + "familyName", + "styleName", + "localisedFamilyName", + ] + + filename = posixpath_property("_filename") + path = posixpath_property("_path") + + def __init__( + self, + *, + filename=None, + path=None, + font=None, + name=None, + location=None, + designLocation=None, + layerName=None, + familyName=None, + styleName=None, + localisedFamilyName=None, + copyLib=False, + copyInfo=False, + copyGroups=False, + copyFeatures=False, + muteKerning=False, + muteInfo=False, + mutedGlyphNames=None, + ): + self.filename = filename + """string. A relative path to the source file, **as it is in the document**. + + MutatorMath + VarLib. + """ + self.path = path + """The absolute path, calculated from filename.""" + + self.font = font + """Any Python object. Optional. Points to a representation of this + source font that is loaded in memory, as a Python object (e.g. a + ``defcon.Font`` or a ``fontTools.ttFont.TTFont``). + + The default document reader will not fill-in this attribute, and the + default writer will not use this attribute. It is up to the user of + ``designspaceLib`` to either load the resource identified by + ``filename`` and store it in this field, or write the contents of + this field to the disk and make ```filename`` point to that. + """ + + self.name = name + """string. Optional. Unique identifier name for this source. + + MutatorMath + varLib. + """ + + self.designLocation = ( + designLocation if designLocation is not None else location or {} + ) + """dict. Axis values for this source, in design space coordinates. + + MutatorMath + varLib. + + This may be only part of the full design location. + See :meth:`getFullDesignLocation()` + + .. versionadded:: 5.0 + """ + + self.layerName = layerName + """string. The name of the layer in the source to look for + outline data. Default ``None`` which means ``foreground``. + """ + self.familyName = familyName + """string. Family name of this source. Though this data + can be extracted from the font, it can be efficient to have it right + here. + + varLib. + """ + self.styleName = styleName + """string. Style name of this source. Though this data + can be extracted from the font, it can be efficient to have it right + here. + + varLib. + """ + self.localisedFamilyName = localisedFamilyName or {} + """dict. A dictionary of localised family name strings, keyed by + language code. + + If present, will be used to build localized names for all instances. + + .. versionadded:: 5.0 + """ + + self.copyLib = copyLib + """bool. Indicates if the contents of the font.lib need to + be copied to the instances. + + MutatorMath. + + .. deprecated:: 5.0 + """ + self.copyInfo = copyInfo + """bool. Indicates if the non-interpolating font.info needs + to be copied to the instances. + + MutatorMath. + + .. deprecated:: 5.0 + """ + self.copyGroups = copyGroups + """bool. Indicates if the groups need to be copied to the + instances. + + MutatorMath. + + .. deprecated:: 5.0 + """ + self.copyFeatures = copyFeatures + """bool. Indicates if the feature text needs to be + copied to the instances. + + MutatorMath. + + .. deprecated:: 5.0 + """ + self.muteKerning = muteKerning + """bool. Indicates if the kerning data from this source + needs to be muted (i.e. not be part of the calculations). + + MutatorMath only. + """ + self.muteInfo = muteInfo + """bool. Indicated if the interpolating font.info data for + this source needs to be muted. + + MutatorMath only. + """ + self.mutedGlyphNames = mutedGlyphNames or [] + """list. Glyphnames that need to be muted in the + instances. + + MutatorMath only. + """ + + @property + def location(self): + """dict. Axis values for this source, in design space coordinates. + + MutatorMath + varLib. + + .. deprecated:: 5.0 + Use the more explicit alias for this property :attr:`designLocation`. + """ + return self.designLocation + + @location.setter + def location(self, location: Optional[SimpleLocationDict]): + self.designLocation = location or {} + + def setFamilyName(self, familyName, languageCode="en"): + """Setter for :attr:`localisedFamilyName` + + .. versionadded:: 5.0 + """ + self.localisedFamilyName[languageCode] = tostr(familyName) + + def getFamilyName(self, languageCode="en"): + """Getter for :attr:`localisedFamilyName` + + .. versionadded:: 5.0 + """ + return self.localisedFamilyName.get(languageCode) + + def getFullDesignLocation(self, doc: "DesignSpaceDocument") -> SimpleLocationDict: + """Get the complete design location of this source, from its + :attr:`designLocation` and the document's axis defaults. + + .. versionadded:: 5.0 + """ + result: SimpleLocationDict = {} + for axis in doc.axes: + if axis.name in self.designLocation: + result[axis.name] = self.designLocation[axis.name] + else: + result[axis.name] = axis.map_forward(axis.default) + return result + + +class RuleDescriptor(SimpleDescriptor): + """Represents the rule descriptor element: a set of glyph substitutions to + trigger conditionally in some parts of the designspace. + + .. code:: python + + r1 = RuleDescriptor() + r1.name = "unique.rule.name" + r1.conditionSets.append([dict(name="weight", minimum=-10, maximum=10), dict(...)]) + r1.conditionSets.append([dict(...), dict(...)]) + r1.subs.append(("a", "a.alt")) + + .. code:: xml + + + + + + + + + + + + + + """ + + _attrs = ["name", "conditionSets", "subs"] # what do we need here + + def __init__(self, *, name=None, conditionSets=None, subs=None): + self.name = name + """string. Unique name for this rule. Can be used to reference this rule data.""" + # list of lists of dict(name='aaaa', minimum=0, maximum=1000) + self.conditionSets = conditionSets or [] + """a list of conditionsets. + + - Each conditionset is a list of conditions. + - Each condition is a dict with ``name``, ``minimum`` and ``maximum`` keys. + """ + # list of substitutions stored as tuples of glyphnames ("a", "a.alt") + self.subs = subs or [] + """list of substitutions. + + - Each substitution is stored as tuples of glyphnames, e.g. ("a", "a.alt"). + - Note: By default, rules are applied first, before other text + shaping/OpenType layout, as they are part of the + `Required Variation Alternates OpenType feature `_. + See ref:`rules-element` § Attributes. + """ + + +def evaluateRule(rule, location): + """Return True if any of the rule's conditionsets matches the given location.""" + return any(evaluateConditions(c, location) for c in rule.conditionSets) + + +def evaluateConditions(conditions, location): + """Return True if all the conditions matches the given location. + + - If a condition has no minimum, check for < maximum. + - If a condition has no maximum, check for > minimum. + """ + for cd in conditions: + value = location[cd["name"]] + if cd.get("minimum") is None: + if value > cd["maximum"]: + return False + elif cd.get("maximum") is None: + if cd["minimum"] > value: + return False + elif not cd["minimum"] <= value <= cd["maximum"]: + return False + return True + + +def processRules(rules, location, glyphNames): + """Apply these rules at this location to these glyphnames. + + Return a new list of glyphNames with substitutions applied. + + - rule order matters + """ + newNames = [] + for rule in rules: + if evaluateRule(rule, location): + for name in glyphNames: + swap = False + for a, b in rule.subs: + if name == a: + swap = True + break + if swap: + newNames.append(b) + else: + newNames.append(name) + glyphNames = newNames + newNames = [] + return glyphNames + + +AnisotropicLocationDict = Dict[str, Union[float, Tuple[float, float]]] +SimpleLocationDict = Dict[str, float] + + +class AxisMappingDescriptor(SimpleDescriptor): + """Represents the axis mapping element: mapping an input location + to an output location in the designspace. + + .. code:: python + + m1 = AxisMappingDescriptor() + m1.inputLocation = {"weight": 900, "width": 150} + m1.outputLocation = {"weight": 870} + + .. code:: xml + + + + + + + + + + + + + """ + + _attrs = ["inputLocation", "outputLocation"] + + def __init__( + self, + *, + inputLocation=None, + outputLocation=None, + description=None, + groupDescription=None, + ): + self.inputLocation: SimpleLocationDict = inputLocation or {} + """dict. Axis values for the input of the mapping, in design space coordinates. + + varLib. + + .. versionadded:: 5.1 + """ + self.outputLocation: SimpleLocationDict = outputLocation or {} + """dict. Axis values for the output of the mapping, in design space coordinates. + + varLib. + + .. versionadded:: 5.1 + """ + self.description = description + """string. A description of the mapping. + + varLib. + + .. versionadded:: 5.2 + """ + self.groupDescription = groupDescription + """string. A description of the group of mappings. + + varLib. + + .. versionadded:: 5.2 + """ + + +class InstanceDescriptor(SimpleDescriptor): + """Simple container for data related to the instance + + + .. code:: python + + i2 = InstanceDescriptor() + i2.path = instancePath2 + i2.familyName = "InstanceFamilyName" + i2.styleName = "InstanceStyleName" + i2.name = "instance.ufo2" + # anisotropic location + i2.designLocation = dict(weight=500, width=(400,300)) + i2.postScriptFontName = "InstancePostscriptName" + i2.styleMapFamilyName = "InstanceStyleMapFamilyName" + i2.styleMapStyleName = "InstanceStyleMapStyleName" + i2.lib['com.coolDesignspaceApp.specimenText'] = 'Hamburgerwhatever' + doc.addInstance(i2) + """ + + flavor = "instance" + _defaultLanguageCode = "en" + _attrs = [ + "filename", + "path", + "name", + "locationLabel", + "designLocation", + "userLocation", + "familyName", + "styleName", + "postScriptFontName", + "styleMapFamilyName", + "styleMapStyleName", + "localisedFamilyName", + "localisedStyleName", + "localisedStyleMapFamilyName", + "localisedStyleMapStyleName", + "glyphs", + "kerning", + "info", + "lib", + ] + + filename = posixpath_property("_filename") + path = posixpath_property("_path") + + def __init__( + self, + *, + filename=None, + path=None, + font=None, + name=None, + location=None, + locationLabel=None, + designLocation=None, + userLocation=None, + familyName=None, + styleName=None, + postScriptFontName=None, + styleMapFamilyName=None, + styleMapStyleName=None, + localisedFamilyName=None, + localisedStyleName=None, + localisedStyleMapFamilyName=None, + localisedStyleMapStyleName=None, + glyphs=None, + kerning=True, + info=True, + lib=None, + ): + self.filename = filename + """string. Relative path to the instance file, **as it is + in the document**. The file may or may not exist. + + MutatorMath + VarLib. + """ + self.path = path + """string. Absolute path to the instance file, calculated from + the document path and the string in the filename attr. The file may + or may not exist. + + MutatorMath. + """ + self.font = font + """Same as :attr:`SourceDescriptor.font` + + .. seealso:: :attr:`SourceDescriptor.font` + """ + self.name = name + """string. Unique identifier name of the instance, used to + identify it if it needs to be referenced from elsewhere in the + document. + """ + self.locationLabel = locationLabel + """Name of a :class:`LocationLabelDescriptor`. If + provided, the instance should have the same location as the + LocationLabel. + + .. seealso:: + :meth:`getFullDesignLocation` + :meth:`getFullUserLocation` + + .. versionadded:: 5.0 + """ + self.designLocation: AnisotropicLocationDict = ( + designLocation if designLocation is not None else (location or {}) + ) + """dict. Axis values for this instance, in design space coordinates. + + MutatorMath + varLib. + + .. seealso:: This may be only part of the full location. See: + :meth:`getFullDesignLocation` + :meth:`getFullUserLocation` + + .. versionadded:: 5.0 + """ + self.userLocation: SimpleLocationDict = userLocation or {} + """dict. Axis values for this instance, in user space coordinates. + + MutatorMath + varLib. + + .. seealso:: This may be only part of the full location. See: + :meth:`getFullDesignLocation` + :meth:`getFullUserLocation` + + .. versionadded:: 5.0 + """ + self.familyName = familyName + """string. Family name of this instance. + + MutatorMath + varLib. + """ + self.styleName = styleName + """string. Style name of this instance. + + MutatorMath + varLib. + """ + self.postScriptFontName = postScriptFontName + """string. Postscript fontname for this instance. + + MutatorMath + varLib. + """ + self.styleMapFamilyName = styleMapFamilyName + """string. StyleMap familyname for this instance. + + MutatorMath + varLib. + """ + self.styleMapStyleName = styleMapStyleName + """string. StyleMap stylename for this instance. + + MutatorMath + varLib. + """ + self.localisedFamilyName = localisedFamilyName or {} + """dict. A dictionary of localised family name + strings, keyed by language code. + """ + self.localisedStyleName = localisedStyleName or {} + """dict. A dictionary of localised stylename + strings, keyed by language code. + """ + self.localisedStyleMapFamilyName = localisedStyleMapFamilyName or {} + """A dictionary of localised style map + familyname strings, keyed by language code. + """ + self.localisedStyleMapStyleName = localisedStyleMapStyleName or {} + """A dictionary of localised style map + stylename strings, keyed by language code. + """ + self.glyphs = glyphs or {} + """dict for special master definitions for glyphs. If glyphs + need special masters (to record the results of executed rules for + example). + + MutatorMath. + + .. deprecated:: 5.0 + Use rules or sparse sources instead. + """ + self.kerning = kerning + """ bool. Indicates if this instance needs its kerning + calculated. + + MutatorMath. + + .. deprecated:: 5.0 + """ + self.info = info + """bool. Indicated if this instance needs the interpolating + font.info calculated. + + .. deprecated:: 5.0 + """ + + self.lib = lib or {} + """Custom data associated with this instance.""" + + @property + def location(self): + """dict. Axis values for this instance. + + MutatorMath + varLib. + + .. deprecated:: 5.0 + Use the more explicit alias for this property :attr:`designLocation`. + """ + return self.designLocation + + @location.setter + def location(self, location: Optional[AnisotropicLocationDict]): + self.designLocation = location or {} + + def setStyleName(self, styleName, languageCode="en"): + """These methods give easier access to the localised names.""" + self.localisedStyleName[languageCode] = tostr(styleName) + + def getStyleName(self, languageCode="en"): + return self.localisedStyleName.get(languageCode) + + def setFamilyName(self, familyName, languageCode="en"): + self.localisedFamilyName[languageCode] = tostr(familyName) + + def getFamilyName(self, languageCode="en"): + return self.localisedFamilyName.get(languageCode) + + def setStyleMapStyleName(self, styleMapStyleName, languageCode="en"): + self.localisedStyleMapStyleName[languageCode] = tostr(styleMapStyleName) + + def getStyleMapStyleName(self, languageCode="en"): + return self.localisedStyleMapStyleName.get(languageCode) + + def setStyleMapFamilyName(self, styleMapFamilyName, languageCode="en"): + self.localisedStyleMapFamilyName[languageCode] = tostr(styleMapFamilyName) + + def getStyleMapFamilyName(self, languageCode="en"): + return self.localisedStyleMapFamilyName.get(languageCode) + + def clearLocation(self, axisName: Optional[str] = None): + """Clear all location-related fields. Ensures that + :attr:``designLocation`` and :attr:``userLocation`` are dictionaries + (possibly empty if clearing everything). + + In order to update the location of this instance wholesale, a user + should first clear all the fields, then change the field(s) for which + they have data. + + .. code:: python + + instance.clearLocation() + instance.designLocation = {'Weight': (34, 36.5), 'Width': 100} + instance.userLocation = {'Opsz': 16} + + In order to update a single axis location, the user should only clear + that axis, then edit the values: + + .. code:: python + + instance.clearLocation('Weight') + instance.designLocation['Weight'] = (34, 36.5) + + Args: + axisName: if provided, only clear the location for that axis. + + .. versionadded:: 5.0 + """ + self.locationLabel = None + if axisName is None: + self.designLocation = {} + self.userLocation = {} + else: + if self.designLocation is None: + self.designLocation = {} + if axisName in self.designLocation: + del self.designLocation[axisName] + if self.userLocation is None: + self.userLocation = {} + if axisName in self.userLocation: + del self.userLocation[axisName] + + def getLocationLabelDescriptor( + self, doc: "DesignSpaceDocument" + ) -> Optional[LocationLabelDescriptor]: + """Get the :class:`LocationLabelDescriptor` instance that matches + this instances's :attr:`locationLabel`. + + Raises if the named label can't be found. + + .. versionadded:: 5.0 + """ + if self.locationLabel is None: + return None + label = doc.getLocationLabel(self.locationLabel) + if label is None: + raise DesignSpaceDocumentError( + "InstanceDescriptor.getLocationLabelDescriptor(): " + f"unknown location label `{self.locationLabel}` in instance `{self.name}`." + ) + return label + + def getFullDesignLocation( + self, doc: "DesignSpaceDocument" + ) -> AnisotropicLocationDict: + """Get the complete design location of this instance, by combining data + from the various location fields, default axis values and mappings, and + top-level location labels. + + The source of truth for this instance's location is determined for each + axis independently by taking the first not-None field in this list: + + - ``locationLabel``: the location along this axis is the same as the + matching STAT format 4 label. No anisotropy. + - ``designLocation[axisName]``: the explicit design location along this + axis, possibly anisotropic. + - ``userLocation[axisName]``: the explicit user location along this + axis. No anisotropy. + - ``axis.default``: default axis value. No anisotropy. + + .. versionadded:: 5.0 + """ + label = self.getLocationLabelDescriptor(doc) + if label is not None: + return doc.map_forward(label.userLocation) # type: ignore + result: AnisotropicLocationDict = {} + for axis in doc.axes: + if axis.name in self.designLocation: + result[axis.name] = self.designLocation[axis.name] + elif axis.name in self.userLocation: + result[axis.name] = axis.map_forward(self.userLocation[axis.name]) + else: + result[axis.name] = axis.map_forward(axis.default) + return result + + def getFullUserLocation(self, doc: "DesignSpaceDocument") -> SimpleLocationDict: + """Get the complete user location for this instance. + + .. seealso:: :meth:`getFullDesignLocation` + + .. versionadded:: 5.0 + """ + return doc.map_backward(self.getFullDesignLocation(doc)) + + +def tagForAxisName(name): + # try to find or make a tag name for this axis name + names = { + "weight": ("wght", dict(en="Weight")), + "width": ("wdth", dict(en="Width")), + "optical": ("opsz", dict(en="Optical Size")), + "slant": ("slnt", dict(en="Slant")), + "italic": ("ital", dict(en="Italic")), + } + if name.lower() in names: + return names[name.lower()] + if len(name) < 4: + tag = name + "*" * (4 - len(name)) + else: + tag = name[:4] + return tag, dict(en=name) + + +class AbstractAxisDescriptor(SimpleDescriptor): + name: str | None + map: list[tuple[float, float]] + + flavor = "axis" + + def __init__( + self, + *, + tag=None, + name: str | None = None, + labelNames=None, + hidden=False, + map: list[tuple[float, float]] | None = None, + axisOrdering=None, + axisLabels=None, + ): + # opentype tag for this axis + self.tag = tag + """string. Four letter tag for this axis. Some might be + registered at the `OpenType + specification `__. + Privately-defined axis tags must begin with an uppercase letter and + use only uppercase letters or digits. + """ + # name of the axis used in locations + self.name = name + """string. Name of the axis as it is used in the location dicts. + + MutatorMath + varLib. + """ + # names for UI purposes, if this is not a standard axis, + self.labelNames = labelNames or {} + """dict. When defining a non-registered axis, it will be + necessary to define user-facing readable names for the axis. Keyed by + xml:lang code. Values are required to be ``unicode`` strings, even if + they only contain ASCII characters. + """ + self.hidden = hidden + """bool. Whether this axis should be hidden in user interfaces. + """ + self.map = map or [] + """list of input / output values that can describe a warp of user space + to design space coordinates. If no map values are present, it is assumed + user space is the same as design space, as in [(minimum, minimum), + (maximum, maximum)]. + + varLib. + """ + self.axisOrdering = axisOrdering + """STAT table field ``axisOrdering``. + + See: `OTSpec STAT Axis Record `_ + + .. versionadded:: 5.0 + """ + self.axisLabels: List[AxisLabelDescriptor] = axisLabels or [] + """STAT table entries for Axis Value Tables format 1, 2, 3. + + See: `OTSpec STAT Axis Value Tables `_ + + .. versionadded:: 5.0 + """ + + def get_validated_map(self) -> list[tuple[float, float]]: + """Return a new list with exact duplicate pairs collapsed. + + The stored :attr:`map` is not modified. + Multiple distinct input coordinates may map to the same output; + duplicate outputs (many-to-one mappings) are valid. + + This validates only ambiguity in the input coordinates: an input + coordinate mapping to different outputs raises + :class:`DesignSpaceDocumentError`. It does not validate axis endpoint + coverage or output monotonicity. + """ + validated = {} + for input_value, output_value in self.map: + if input_value in validated: + previous_output = validated[input_value] + if previous_output != output_value: + raise DesignSpaceDocumentError( + f"Axis '{self.name}': mapping input value {input_value} " + f"has conflicting outputs {previous_output} and {output_value}." + ) + continue + validated[input_value] = output_value + return list(validated.items()) + + +class AxisDescriptor(AbstractAxisDescriptor): + """Simple container for the axis data. + + Add more localisations? + + .. code:: python + + a1 = AxisDescriptor() + a1.minimum = 1 + a1.maximum = 1000 + a1.default = 400 + a1.name = "weight" + a1.tag = "wght" + a1.labelNames['fa-IR'] = "قطر" + a1.labelNames['en'] = "Wéíght" + a1.map = [(1.0, 10.0), (400.0, 66.0), (1000.0, 990.0)] + a1.axisOrdering = 1 + a1.axisLabels = [ + AxisLabelDescriptor(name="Regular", userValue=400, elidable=True) + ] + doc.addAxis(a1) + """ + + _attrs = [ + "tag", + "name", + "maximum", + "minimum", + "default", + "map", + "axisOrdering", + "axisLabels", + ] + + def __init__( + self, + *, + tag=None, + name: str | None = None, + labelNames=None, + minimum=None, + default=None, + maximum=None, + hidden=False, + map: list[tuple[float, float]] | None = None, + axisOrdering=None, + axisLabels=None, + ): + super().__init__( + tag=tag, + name=name, + labelNames=labelNames, + hidden=hidden, + map=map, + axisOrdering=axisOrdering, + axisLabels=axisLabels, + ) + self.minimum = minimum + """number. The minimum value for this axis in user space. + + MutatorMath + varLib. + """ + self.maximum = maximum + """number. The maximum value for this axis in user space. + + MutatorMath + varLib. + """ + self.default = default + """number. The default value for this axis, i.e. when a new location is + created, this is the value this axis will get in user space. + + MutatorMath + varLib. + """ + + def serialize(self): + # output to a dict, used in testing + return dict( + tag=self.tag, + name=self.name, + labelNames=self.labelNames, + maximum=self.maximum, + minimum=self.minimum, + default=self.default, + hidden=self.hidden, + map=self.map, + axisOrdering=self.axisOrdering, + axisLabels=self.axisLabels, + ) + + def map_forward(self, v): + """Maps value from axis mapping's input (user) to output (design).""" + from fontTools.varLib.models import piecewiseLinearMap + + axis_map = self.get_validated_map() + if not axis_map: + return v + return piecewiseLinearMap(v, dict(axis_map)) + + def map_backward(self, v): + """Maps value from axis mapping's output (design) to input (user).""" + if isinstance(v, tuple): + v = v[0] + axis_map = self.get_validated_map() + if not axis_map: + return v + # Build (design, user) pairs sorted by design then user, keeping + # both endpoints of any many-to-one (flat) segments so we can + # invert them and interpolation is correct on both sides. + # https://github.com/googlefonts/ufo2ft/issues/978 + backward = sorted((design, user) for user, design in axis_map) + design0, user0 = backward[0] + if v <= design0: + return v + user0 - design0 + for (design1, user1), (design2, user2) in zip(backward, backward[1:]): + if design1 <= v <= design2: + if design1 == design2: + return user1 + return user1 + (user2 - user1) * (v - design1) / (design2 - design1) + designN, userN = backward[-1] + return v + userN - designN + + +class DiscreteAxisDescriptor(AbstractAxisDescriptor): + """Container for discrete axis data. + + Use this for axes that do not interpolate. The main difference from a + continuous axis is that a continuous axis has a ``minimum`` and ``maximum``, + while a discrete axis has a list of ``values``. + + Example: an Italic axis with 2 stops, Roman and Italic, that are not + compatible. The axis still allows to bind together the full font family, + which is useful for the STAT table, however it can't become a variation + axis in a VF. + + .. code:: python + + a2 = DiscreteAxisDescriptor() + a2.values = [0, 1] + a2.default = 0 + a2.name = "Italic" + a2.tag = "ITAL" + a2.labelNames['fr'] = "Italique" + a2.map = [(0, 0), (1, -11)] + a2.axisOrdering = 2 + a2.axisLabels = [ + AxisLabelDescriptor(name="Roman", userValue=0, elidable=True) + ] + doc.addAxis(a2) + + .. versionadded:: 5.0 + """ + + flavor = "axis" + _attrs = ("tag", "name", "values", "default", "map", "axisOrdering", "axisLabels") + + def __init__( + self, + *, + tag=None, + name: str | None = None, + labelNames=None, + values=None, + default=None, + hidden=False, + map: list[tuple[float, float]] | None = None, + axisOrdering=None, + axisLabels=None, + ): + super().__init__( + tag=tag, + name=name, + labelNames=labelNames, + hidden=hidden, + map=map, + axisOrdering=axisOrdering, + axisLabels=axisLabels, + ) + self.default: float = default + """The default value for this axis, i.e. when a new location is + created, this is the value this axis will get in user space. + + However, this default value is less important than in continuous axes: + + - it doesn't define the "neutral" version of outlines from which + deltas would apply, as this axis does not interpolate. + - it doesn't provide the reference glyph set for the designspace, as + fonts at each value can have different glyph sets. + """ + self.values: List[float] = values or [] + """List of possible values for this axis. Contrary to continuous axes, + only the values in this list can be taken by the axis, nothing in-between. + """ + + def map_forward(self, value): + """Maps value from axis mapping's input to output. + + Returns value unchanged if no mapping entry is found. + + Note: for discrete axes, each value must have its mapping entry, if + you intend that value to be mapped. + """ + return next((v for k, v in self.get_validated_map() if k == value), value) + + def map_backward(self, value): + """Maps value from axis mapping's output to input. + + Returns value unchanged if no mapping entry is found. + + Note: for discrete axes, each value must have its mapping entry, if + you intend that value to be mapped. + """ + if isinstance(value, tuple): + value = value[0] + return next((k for k, v in self.get_validated_map() if v == value), value) + + +class AxisLabelDescriptor(SimpleDescriptor): + """Container for axis label data. + + Analogue of OpenType's STAT data for a single axis (formats 1, 2 and 3). + All values are user values. + See: `OTSpec STAT Axis value table, format 1, 2, 3 `_ + + The STAT format of the Axis value depends on which field are filled-in, + see :meth:`getFormat` + + .. versionadded:: 5.0 + """ + + flavor = "label" + _attrs = ( + "userMinimum", + "userValue", + "userMaximum", + "name", + "elidable", + "olderSibling", + "linkedUserValue", + "labelNames", + ) + + def __init__( + self, + *, + name, + userValue, + userMinimum=None, + userMaximum=None, + elidable=False, + olderSibling=False, + linkedUserValue=None, + labelNames=None, + ): + self.userMinimum: Optional[float] = userMinimum + """STAT field ``rangeMinValue`` (format 2).""" + self.userValue: float = userValue + """STAT field ``value`` (format 1, 3) or ``nominalValue`` (format 2).""" + self.userMaximum: Optional[float] = userMaximum + """STAT field ``rangeMaxValue`` (format 2).""" + self.name: str = name + """Label for this axis location, STAT field ``valueNameID``.""" + self.elidable: bool = elidable + """STAT flag ``ELIDABLE_AXIS_VALUE_NAME``. + + See: `OTSpec STAT Flags `_ + """ + self.olderSibling: bool = olderSibling + """STAT flag ``OLDER_SIBLING_FONT_ATTRIBUTE``. + + See: `OTSpec STAT Flags `_ + """ + self.linkedUserValue: Optional[float] = linkedUserValue + """STAT field ``linkedValue`` (format 3).""" + self.labelNames: MutableMapping[str, str] = labelNames or {} + """User-facing translations of this location's label. Keyed by + ``xml:lang`` code. + """ + + def getFormat(self) -> int: + """Determine which format of STAT Axis value to use to encode this label. + + =========== ========= =========== =========== =============== + STAT Format userValue userMinimum userMaximum linkedUserValue + =========== ========= =========== =========== =============== + 1 ✅ ❌ ❌ ❌ + 2 ✅ ✅ ✅ ❌ + 3 ✅ ❌ ❌ ✅ + =========== ========= =========== =========== =============== + """ + if self.linkedUserValue is not None: + return 3 + if self.userMinimum is not None or self.userMaximum is not None: + return 2 + return 1 + + @property + def defaultName(self) -> str: + """Return the English name from :attr:`labelNames` or the :attr:`name`.""" + return self.labelNames.get("en") or self.name + + +class LocationLabelDescriptor(SimpleDescriptor): + """Container for location label data. + + Analogue of OpenType's STAT data for a free-floating location (format 4). + All values are user values. + + See: `OTSpec STAT Axis value table, format 4 `_ + + .. versionadded:: 5.0 + """ + + flavor = "label" + _attrs = ("name", "elidable", "olderSibling", "userLocation", "labelNames") + + def __init__( + self, + *, + name, + userLocation, + elidable=False, + olderSibling=False, + labelNames=None, + ): + self.name: str = name + """Label for this named location, STAT field ``valueNameID``.""" + self.userLocation: SimpleLocationDict = userLocation or {} + """Location in user coordinates along each axis. + + If an axis is not mentioned, it is assumed to be at its default location. + + .. seealso:: This may be only part of the full location. See: + :meth:`getFullUserLocation` + """ + self.elidable: bool = elidable + """STAT flag ``ELIDABLE_AXIS_VALUE_NAME``. + + See: `OTSpec STAT Flags `_ + """ + self.olderSibling: bool = olderSibling + """STAT flag ``OLDER_SIBLING_FONT_ATTRIBUTE``. + + See: `OTSpec STAT Flags `_ + """ + self.labelNames: Dict[str, str] = labelNames or {} + """User-facing translations of this location's label. Keyed by + xml:lang code. + """ + + @property + def defaultName(self) -> str: + """Return the English name from :attr:`labelNames` or the :attr:`name`.""" + return self.labelNames.get("en") or self.name + + def getFullUserLocation(self, doc: "DesignSpaceDocument") -> SimpleLocationDict: + """Get the complete user location of this label, by combining data + from the explicit user location and default axis values. + + .. versionadded:: 5.0 + """ + return { + axis.name: self.userLocation.get(axis.name, axis.default) + for axis in doc.axes + } + + +class VariableFontDescriptor(SimpleDescriptor): + """Container for variable fonts, sub-spaces of the Designspace. + + Use-cases: + + - From a single DesignSpace with discrete axes, define 1 variable font + per value on the discrete axes. Before version 5, you would have needed + 1 DesignSpace per such variable font, and a lot of data duplication. + - From a big variable font with many axes, define subsets of that variable + font that only include some axes and freeze other axes at a given location. + + .. versionadded:: 5.0 + """ + + flavor = "variable-font" + _attrs = ("filename", "axisSubsets", "lib") + + filename = posixpath_property("_filename") + + def __init__(self, *, name, filename=None, axisSubsets=None, lib=None): + self.name: str = name + """string, required. Name of this variable to identify it during the + build process and from other parts of the document, and also as a + filename in case the filename property is empty. + + VarLib. + """ + self.filename: str = filename + """string, optional. Relative path to the variable font file, **as it is + in the document**. The file may or may not exist. + + If not specified, the :attr:`name` will be used as a basename for the file. + + .. note:: + This is intended to be a simple filename (basename or stem) only. + Build tools will only use the basename component and ignore any + directory separators for security reasons. + """ + self.axisSubsets: List[ + Union[RangeAxisSubsetDescriptor, ValueAxisSubsetDescriptor] + ] = (axisSubsets or []) + """Axis subsets to include in this variable font. + + If an axis is not mentioned, assume that we only want the default + location of that axis (same as a :class:`ValueAxisSubsetDescriptor`). + """ + self.lib: MutableMapping[str, Any] = lib or {} + """Custom data associated with this variable font.""" + + +class RangeAxisSubsetDescriptor(SimpleDescriptor): + """Subset of a continuous axis to include in a variable font. + + .. versionadded:: 5.0 + """ + + flavor = "axis-subset" + _attrs = ("name", "userMinimum", "userDefault", "userMaximum") + + def __init__( + self, *, name, userMinimum=-math.inf, userDefault=None, userMaximum=math.inf + ): + self.name: str = name + """Name of the :class:`AxisDescriptor` to subset.""" + self.userMinimum: float = userMinimum + """New minimum value of the axis in the target variable font. + If not specified, assume the same minimum value as the full axis. + (default = ``-math.inf``) + """ + self.userDefault: Optional[float] = userDefault + """New default value of the axis in the target variable font. + If not specified, assume the same default value as the full axis. + (default = ``None``) + """ + self.userMaximum: float = userMaximum + """New maximum value of the axis in the target variable font. + If not specified, assume the same maximum value as the full axis. + (default = ``math.inf``) + """ + + +class ValueAxisSubsetDescriptor(SimpleDescriptor): + """Single value of a discrete or continuous axis to use in a variable font. + + .. versionadded:: 5.0 + """ + + flavor = "axis-subset" + _attrs = ("name", "userValue") + + def __init__(self, *, name, userValue): + self.name: str = name + """Name of the :class:`AxisDescriptor` or :class:`DiscreteAxisDescriptor` + to "snapshot" or "freeze". + """ + self.userValue: float = userValue + """Value in user coordinates at which to freeze the given axis.""" + + +class BaseDocWriter(object): + _whiteSpace = " " + axisDescriptorClass = AxisDescriptor + discreteAxisDescriptorClass = DiscreteAxisDescriptor + axisLabelDescriptorClass = AxisLabelDescriptor + axisMappingDescriptorClass = AxisMappingDescriptor + locationLabelDescriptorClass = LocationLabelDescriptor + ruleDescriptorClass = RuleDescriptor + sourceDescriptorClass = SourceDescriptor + variableFontDescriptorClass = VariableFontDescriptor + valueAxisSubsetDescriptorClass = ValueAxisSubsetDescriptor + rangeAxisSubsetDescriptorClass = RangeAxisSubsetDescriptor + instanceDescriptorClass = InstanceDescriptor + + @classmethod + def getAxisDecriptor(cls): + return cls.axisDescriptorClass() + + @classmethod + def getAxisMappingDescriptor(cls): + return cls.axisMappingDescriptorClass() + + @classmethod + def getSourceDescriptor(cls): + return cls.sourceDescriptorClass() + + @classmethod + def getInstanceDescriptor(cls): + return cls.instanceDescriptorClass() + + @classmethod + def getRuleDescriptor(cls): + return cls.ruleDescriptorClass() + + def __init__(self, documentPath, documentObject: DesignSpaceDocument): + self.path = documentPath + self.documentObject = documentObject + self.effectiveFormatTuple = self._getEffectiveFormatTuple() + self.root = ET.Element("designspace") + + def write(self, pretty=True, encoding="UTF-8", xml_declaration=True): + self.root.attrib["format"] = ".".join(str(i) for i in self.effectiveFormatTuple) + + if ( + self.documentObject.axes + or self.documentObject.axisMappings + or self.documentObject.elidedFallbackName is not None + ): + axesElement = ET.Element("axes") + if self.documentObject.elidedFallbackName is not None: + axesElement.attrib["elidedfallbackname"] = ( + self.documentObject.elidedFallbackName + ) + self.root.append(axesElement) + for axisObject in self.documentObject.axes: + self._addAxis(axisObject) + + if self.documentObject.axisMappings: + mappingsElement = None + lastGroup = object() + for mappingObject in self.documentObject.axisMappings: + if getattr(mappingObject, "groupDescription", None) != lastGroup: + if mappingsElement is not None: + self.root.findall(".axes")[0].append(mappingsElement) + lastGroup = getattr(mappingObject, "groupDescription", None) + mappingsElement = ET.Element("mappings") + if lastGroup is not None: + mappingsElement.attrib["description"] = lastGroup + self._addAxisMapping(mappingsElement, mappingObject) + if mappingsElement is not None: + self.root.findall(".axes")[0].append(mappingsElement) + + if self.documentObject.locationLabels: + labelsElement = ET.Element("labels") + for labelObject in self.documentObject.locationLabels: + self._addLocationLabel(labelsElement, labelObject) + self.root.append(labelsElement) + + if self.documentObject.rules: + if getattr(self.documentObject, "rulesProcessingLast", False): + attributes = {"processing": "last"} + else: + attributes = {} + self.root.append(ET.Element("rules", attributes)) + for ruleObject in self.documentObject.rules: + self._addRule(ruleObject) + + if self.documentObject.sources: + self.root.append(ET.Element("sources")) + for sourceObject in self.documentObject.sources: + self._addSource(sourceObject) + + if self.documentObject.variableFonts: + variableFontsElement = ET.Element("variable-fonts") + for variableFont in self.documentObject.variableFonts: + self._addVariableFont(variableFontsElement, variableFont) + self.root.append(variableFontsElement) + + if self.documentObject.instances: + self.root.append(ET.Element("instances")) + for instanceObject in self.documentObject.instances: + self._addInstance(instanceObject) + + if self.documentObject.lib: + self._addLib(self.root, self.documentObject.lib, 2) + + tree = ET.ElementTree(self.root) + tree.write( + self.path, + encoding=encoding, + method="xml", + xml_declaration=xml_declaration, + pretty_print=pretty, + ) + + def _getEffectiveFormatTuple(self): + """Try to use the version specified in the document, or a sufficiently + recent version to be able to encode what the document contains. + """ + minVersion = self.documentObject.formatTuple + if ( + any( + hasattr(axis, "values") + or axis.axisOrdering is not None + or axis.axisLabels + for axis in self.documentObject.axes + ) + or self.documentObject.locationLabels + or any(source.localisedFamilyName for source in self.documentObject.sources) + or self.documentObject.variableFonts + or any( + instance.locationLabel or instance.userLocation + for instance in self.documentObject.instances + ) + ): + if minVersion < (5, 0): + minVersion = (5, 0) + if self.documentObject.axisMappings: + if minVersion < (5, 1): + minVersion = (5, 1) + return minVersion + + def _makeLocationElement(self, locationObject, name=None): + """Convert Location dict to a locationElement.""" + locElement = ET.Element("location") + if name is not None: + locElement.attrib["name"] = name + validatedLocation = self.documentObject.newDefaultLocation() + for axisName, axisValue in locationObject.items(): + if axisName in validatedLocation: + # only accept values we know + validatedLocation[axisName] = axisValue + for dimensionName, dimensionValue in validatedLocation.items(): + dimElement = ET.Element("dimension") + dimElement.attrib["name"] = dimensionName + if type(dimensionValue) == tuple: + dimElement.attrib["xvalue"] = self.intOrFloat(dimensionValue[0]) + dimElement.attrib["yvalue"] = self.intOrFloat(dimensionValue[1]) + else: + dimElement.attrib["xvalue"] = self.intOrFloat(dimensionValue) + locElement.append(dimElement) + return locElement, validatedLocation + + def intOrFloat(self, num): + if int(num) == num: + return "%d" % num + return ("%f" % num).rstrip("0").rstrip(".") + + def _addRule(self, ruleObject): + ruleElement = ET.Element("rule") + if ruleObject.name is not None: + ruleElement.attrib["name"] = ruleObject.name + for conditions in ruleObject.conditionSets: + conditionsetElement = ET.Element("conditionset") + for cond in conditions: + if cond.get("minimum") is None and cond.get("maximum") is None: + # neither is defined, don't add this condition + continue + conditionElement = ET.Element("condition") + conditionElement.attrib["name"] = cond.get("name") + if cond.get("minimum") is not None: + conditionElement.attrib["minimum"] = self.intOrFloat( + cond.get("minimum") + ) + if cond.get("maximum") is not None: + conditionElement.attrib["maximum"] = self.intOrFloat( + cond.get("maximum") + ) + conditionsetElement.append(conditionElement) + # Serialize the conditionset even if it is empty, as this is the + # canonical way of defining a rule that is always true. + ruleElement.append(conditionsetElement) + for sub in ruleObject.subs: + subElement = ET.Element("sub") + subElement.attrib["name"] = sub[0] + subElement.attrib["with"] = sub[1] + ruleElement.append(subElement) + if len(ruleElement): + self.root.findall(".rules")[0].append(ruleElement) + + def _addAxis(self, axisObject): + axisElement = ET.Element("axis") + axisElement.attrib["tag"] = axisObject.tag + axisElement.attrib["name"] = axisObject.name + self._addLabelNames(axisElement, axisObject.labelNames) + if axisObject.map: + for inputValue, outputValue in axisObject.map: + mapElement = ET.Element("map") + mapElement.attrib["input"] = self.intOrFloat(inputValue) + mapElement.attrib["output"] = self.intOrFloat(outputValue) + axisElement.append(mapElement) + if axisObject.axisOrdering is not None or axisObject.axisLabels: + labelsElement = ET.Element("labels") + if axisObject.axisOrdering is not None: + labelsElement.attrib["ordering"] = str(axisObject.axisOrdering) + for label in axisObject.axisLabels: + self._addAxisLabel(labelsElement, label) + axisElement.append(labelsElement) + if hasattr(axisObject, "minimum"): + axisElement.attrib["minimum"] = self.intOrFloat(axisObject.minimum) + axisElement.attrib["maximum"] = self.intOrFloat(axisObject.maximum) + elif hasattr(axisObject, "values"): + axisElement.attrib["values"] = " ".join( + self.intOrFloat(v) for v in axisObject.values + ) + axisElement.attrib["default"] = self.intOrFloat(axisObject.default) + if axisObject.hidden: + axisElement.attrib["hidden"] = "1" + self.root.findall(".axes")[0].append(axisElement) + + def _addAxisMapping(self, mappingsElement, mappingObject): + mappingElement = ET.Element("mapping") + if getattr(mappingObject, "description", None) is not None: + mappingElement.attrib["description"] = mappingObject.description + for what in ("inputLocation", "outputLocation"): + whatObject = getattr(mappingObject, what, None) + if whatObject is None: + continue + whatElement = ET.Element(what[:-8]) + mappingElement.append(whatElement) + + for name, value in whatObject.items(): + dimensionElement = ET.Element("dimension") + dimensionElement.attrib["name"] = name + dimensionElement.attrib["xvalue"] = self.intOrFloat(value) + whatElement.append(dimensionElement) + + mappingsElement.append(mappingElement) + + def _addAxisLabel( + self, axisElement: ET.Element, label: AxisLabelDescriptor + ) -> None: + labelElement = ET.Element("label") + labelElement.attrib["uservalue"] = self.intOrFloat(label.userValue) + if label.userMinimum is not None: + labelElement.attrib["userminimum"] = self.intOrFloat(label.userMinimum) + if label.userMaximum is not None: + labelElement.attrib["usermaximum"] = self.intOrFloat(label.userMaximum) + labelElement.attrib["name"] = label.name + if label.elidable: + labelElement.attrib["elidable"] = "true" + if label.olderSibling: + labelElement.attrib["oldersibling"] = "true" + if label.linkedUserValue is not None: + labelElement.attrib["linkeduservalue"] = self.intOrFloat( + label.linkedUserValue + ) + self._addLabelNames(labelElement, label.labelNames) + axisElement.append(labelElement) + + def _addLabelNames(self, parentElement, labelNames): + for languageCode, labelName in sorted(labelNames.items()): + languageElement = ET.Element("labelname") + languageElement.attrib[XML_LANG] = languageCode + languageElement.text = labelName + parentElement.append(languageElement) + + def _addLocationLabel( + self, parentElement: ET.Element, label: LocationLabelDescriptor + ) -> None: + labelElement = ET.Element("label") + labelElement.attrib["name"] = label.name + if label.elidable: + labelElement.attrib["elidable"] = "true" + if label.olderSibling: + labelElement.attrib["oldersibling"] = "true" + self._addLabelNames(labelElement, label.labelNames) + self._addLocationElement(labelElement, userLocation=label.userLocation) + parentElement.append(labelElement) + + def _addLocationElement( + self, + parentElement, + *, + designLocation: AnisotropicLocationDict = None, + userLocation: SimpleLocationDict = None, + ): + locElement = ET.Element("location") + for axis in self.documentObject.axes: + if designLocation is not None and axis.name in designLocation: + dimElement = ET.Element("dimension") + dimElement.attrib["name"] = axis.name + value = designLocation[axis.name] + if isinstance(value, tuple): + dimElement.attrib["xvalue"] = self.intOrFloat(value[0]) + dimElement.attrib["yvalue"] = self.intOrFloat(value[1]) + else: + dimElement.attrib["xvalue"] = self.intOrFloat(value) + locElement.append(dimElement) + elif userLocation is not None and axis.name in userLocation: + dimElement = ET.Element("dimension") + dimElement.attrib["name"] = axis.name + value = userLocation[axis.name] + dimElement.attrib["uservalue"] = self.intOrFloat(value) + locElement.append(dimElement) + if len(locElement) > 0: + parentElement.append(locElement) + + def _addInstance(self, instanceObject): + instanceElement = ET.Element("instance") + if instanceObject.name is not None: + instanceElement.attrib["name"] = instanceObject.name + if instanceObject.locationLabel is not None: + instanceElement.attrib["location"] = instanceObject.locationLabel + if instanceObject.familyName is not None: + instanceElement.attrib["familyname"] = instanceObject.familyName + if instanceObject.styleName is not None: + instanceElement.attrib["stylename"] = instanceObject.styleName + # add localisations + if instanceObject.localisedStyleName: + languageCodes = list(instanceObject.localisedStyleName.keys()) + languageCodes.sort() + for code in languageCodes: + if code == "en": + continue # already stored in the element attribute + localisedStyleNameElement = ET.Element("stylename") + localisedStyleNameElement.attrib[XML_LANG] = code + localisedStyleNameElement.text = instanceObject.getStyleName(code) + instanceElement.append(localisedStyleNameElement) + if instanceObject.localisedFamilyName: + languageCodes = list(instanceObject.localisedFamilyName.keys()) + languageCodes.sort() + for code in languageCodes: + if code == "en": + continue # already stored in the element attribute + localisedFamilyNameElement = ET.Element("familyname") + localisedFamilyNameElement.attrib[XML_LANG] = code + localisedFamilyNameElement.text = instanceObject.getFamilyName(code) + instanceElement.append(localisedFamilyNameElement) + if instanceObject.localisedStyleMapStyleName: + languageCodes = list(instanceObject.localisedStyleMapStyleName.keys()) + languageCodes.sort() + for code in languageCodes: + if code == "en": + continue + localisedStyleMapStyleNameElement = ET.Element("stylemapstylename") + localisedStyleMapStyleNameElement.attrib[XML_LANG] = code + localisedStyleMapStyleNameElement.text = ( + instanceObject.getStyleMapStyleName(code) + ) + instanceElement.append(localisedStyleMapStyleNameElement) + if instanceObject.localisedStyleMapFamilyName: + languageCodes = list(instanceObject.localisedStyleMapFamilyName.keys()) + languageCodes.sort() + for code in languageCodes: + if code == "en": + continue + localisedStyleMapFamilyNameElement = ET.Element("stylemapfamilyname") + localisedStyleMapFamilyNameElement.attrib[XML_LANG] = code + localisedStyleMapFamilyNameElement.text = ( + instanceObject.getStyleMapFamilyName(code) + ) + instanceElement.append(localisedStyleMapFamilyNameElement) + + if self.effectiveFormatTuple >= (5, 0): + if instanceObject.locationLabel is None: + self._addLocationElement( + instanceElement, + designLocation=instanceObject.designLocation, + userLocation=instanceObject.userLocation, + ) + else: + # Pre-version 5.0 code was validating and filling in the location + # dict while writing it out, as preserved below. + if instanceObject.location is not None: + locationElement, instanceObject.location = self._makeLocationElement( + instanceObject.location + ) + instanceElement.append(locationElement) + if instanceObject.filename is not None: + instanceElement.attrib["filename"] = instanceObject.filename + if instanceObject.postScriptFontName is not None: + instanceElement.attrib["postscriptfontname"] = ( + instanceObject.postScriptFontName + ) + if instanceObject.styleMapFamilyName is not None: + instanceElement.attrib["stylemapfamilyname"] = ( + instanceObject.styleMapFamilyName + ) + if instanceObject.styleMapStyleName is not None: + instanceElement.attrib["stylemapstylename"] = ( + instanceObject.styleMapStyleName + ) + if self.effectiveFormatTuple < (5, 0): + # Deprecated members as of version 5.0 + if instanceObject.glyphs: + if instanceElement.findall(".glyphs") == []: + glyphsElement = ET.Element("glyphs") + instanceElement.append(glyphsElement) + glyphsElement = instanceElement.findall(".glyphs")[0] + for glyphName, data in sorted(instanceObject.glyphs.items()): + glyphElement = self._writeGlyphElement( + instanceElement, instanceObject, glyphName, data + ) + glyphsElement.append(glyphElement) + if instanceObject.kerning: + kerningElement = ET.Element("kerning") + instanceElement.append(kerningElement) + if instanceObject.info: + infoElement = ET.Element("info") + instanceElement.append(infoElement) + self._addLib(instanceElement, instanceObject.lib, 4) + self.root.findall(".instances")[0].append(instanceElement) + + def _addSource(self, sourceObject): + sourceElement = ET.Element("source") + if sourceObject.filename is not None: + sourceElement.attrib["filename"] = sourceObject.filename + if sourceObject.name is not None: + if sourceObject.name.find("temp_master") != 0: + # do not save temporary source names + sourceElement.attrib["name"] = sourceObject.name + if sourceObject.familyName is not None: + sourceElement.attrib["familyname"] = sourceObject.familyName + if sourceObject.styleName is not None: + sourceElement.attrib["stylename"] = sourceObject.styleName + if sourceObject.layerName is not None: + sourceElement.attrib["layer"] = sourceObject.layerName + if sourceObject.localisedFamilyName: + languageCodes = list(sourceObject.localisedFamilyName.keys()) + languageCodes.sort() + for code in languageCodes: + if code == "en": + continue # already stored in the element attribute + localisedFamilyNameElement = ET.Element("familyname") + localisedFamilyNameElement.attrib[XML_LANG] = code + localisedFamilyNameElement.text = sourceObject.getFamilyName(code) + sourceElement.append(localisedFamilyNameElement) + if sourceObject.copyLib: + libElement = ET.Element("lib") + libElement.attrib["copy"] = "1" + sourceElement.append(libElement) + if sourceObject.copyGroups: + groupsElement = ET.Element("groups") + groupsElement.attrib["copy"] = "1" + sourceElement.append(groupsElement) + if sourceObject.copyFeatures: + featuresElement = ET.Element("features") + featuresElement.attrib["copy"] = "1" + sourceElement.append(featuresElement) + if sourceObject.copyInfo or sourceObject.muteInfo: + infoElement = ET.Element("info") + if sourceObject.copyInfo: + infoElement.attrib["copy"] = "1" + if sourceObject.muteInfo: + infoElement.attrib["mute"] = "1" + sourceElement.append(infoElement) + if sourceObject.muteKerning: + kerningElement = ET.Element("kerning") + kerningElement.attrib["mute"] = "1" + sourceElement.append(kerningElement) + if sourceObject.mutedGlyphNames: + for name in sourceObject.mutedGlyphNames: + glyphElement = ET.Element("glyph") + glyphElement.attrib["name"] = name + glyphElement.attrib["mute"] = "1" + sourceElement.append(glyphElement) + if self.effectiveFormatTuple >= (5, 0): + self._addLocationElement( + sourceElement, designLocation=sourceObject.location + ) + else: + # Pre-version 5.0 code was validating and filling in the location + # dict while writing it out, as preserved below. + locationElement, sourceObject.location = self._makeLocationElement( + sourceObject.location + ) + sourceElement.append(locationElement) + self.root.findall(".sources")[0].append(sourceElement) + + def _addVariableFont( + self, parentElement: ET.Element, vf: VariableFontDescriptor + ) -> None: + vfElement = ET.Element("variable-font") + vfElement.attrib["name"] = vf.name + if vf.filename is not None: + vfElement.attrib["filename"] = vf.filename + if vf.axisSubsets: + subsetsElement = ET.Element("axis-subsets") + for subset in vf.axisSubsets: + subsetElement = ET.Element("axis-subset") + subsetElement.attrib["name"] = subset.name + # Mypy doesn't support narrowing union types via hasattr() + # https://mypy.readthedocs.io/en/stable/type_narrowing.html + # TODO(Python 3.10): use TypeGuard + if hasattr(subset, "userMinimum"): + subset = cast(RangeAxisSubsetDescriptor, subset) + if subset.userMinimum != -math.inf: + subsetElement.attrib["userminimum"] = self.intOrFloat( + subset.userMinimum + ) + if subset.userMaximum != math.inf: + subsetElement.attrib["usermaximum"] = self.intOrFloat( + subset.userMaximum + ) + if subset.userDefault is not None: + subsetElement.attrib["userdefault"] = self.intOrFloat( + subset.userDefault + ) + elif hasattr(subset, "userValue"): + subset = cast(ValueAxisSubsetDescriptor, subset) + subsetElement.attrib["uservalue"] = self.intOrFloat( + subset.userValue + ) + subsetsElement.append(subsetElement) + vfElement.append(subsetsElement) + self._addLib(vfElement, vf.lib, 4) + parentElement.append(vfElement) + + def _addLib(self, parentElement: ET.Element, data: Any, indent_level: int) -> None: + if not data: + return + libElement = ET.Element("lib") + libElement.append(plistlib.totree(data, indent_level=indent_level)) + parentElement.append(libElement) + + def _writeGlyphElement(self, instanceElement, instanceObject, glyphName, data): + glyphElement = ET.Element("glyph") + if data.get("mute"): + glyphElement.attrib["mute"] = "1" + if data.get("unicodes") is not None: + glyphElement.attrib["unicode"] = " ".join( + [hex(u) for u in data.get("unicodes")] + ) + if data.get("instanceLocation") is not None: + locationElement, data["instanceLocation"] = self._makeLocationElement( + data.get("instanceLocation") + ) + glyphElement.append(locationElement) + if glyphName is not None: + glyphElement.attrib["name"] = glyphName + if data.get("note") is not None: + noteElement = ET.Element("note") + noteElement.text = data.get("note") + glyphElement.append(noteElement) + if data.get("masters") is not None: + mastersElement = ET.Element("masters") + for m in data.get("masters"): + masterElement = ET.Element("master") + if m.get("glyphName") is not None: + masterElement.attrib["glyphname"] = m.get("glyphName") + if m.get("font") is not None: + masterElement.attrib["source"] = m.get("font") + if m.get("location") is not None: + locationElement, m["location"] = self._makeLocationElement( + m.get("location") + ) + masterElement.append(locationElement) + mastersElement.append(masterElement) + glyphElement.append(mastersElement) + return glyphElement + + +class BaseDocReader(LogMixin): + axisDescriptorClass = AxisDescriptor + discreteAxisDescriptorClass = DiscreteAxisDescriptor + axisLabelDescriptorClass = AxisLabelDescriptor + axisMappingDescriptorClass = AxisMappingDescriptor + locationLabelDescriptorClass = LocationLabelDescriptor + ruleDescriptorClass = RuleDescriptor + sourceDescriptorClass = SourceDescriptor + variableFontsDescriptorClass = VariableFontDescriptor + valueAxisSubsetDescriptorClass = ValueAxisSubsetDescriptor + rangeAxisSubsetDescriptorClass = RangeAxisSubsetDescriptor + instanceDescriptorClass = InstanceDescriptor + + def __init__(self, documentPath, documentObject): + self.path = documentPath + self.documentObject = documentObject + tree = ET.parse(self.path, parser=ET.XMLParser()) + self.root = tree.getroot() + self.documentObject.formatVersion = self.root.attrib.get("format", "3.0") + self._axes = [] + self.rules = [] + self.sources = [] + self.instances = [] + self.axisDefaults = {} + self._strictAxisNames = True + + @classmethod + def fromstring(cls, string, documentObject): + f = BytesIO(tobytes(string, encoding="utf-8")) + self = cls(f, documentObject) + self.path = None + return self + + def read(self): + self.readAxes() + self.readLabels() + self.readRules() + self.readVariableFonts() + self.readSources() + self.readInstances() + self.readLib() + + def readRules(self): + # we also need to read any conditions that are outside of a condition set. + rules = [] + rulesElement = self.root.find(".rules") + if rulesElement is not None: + processingValue = rulesElement.attrib.get("processing", "first") + if processingValue not in {"first", "last"}: + raise DesignSpaceDocumentError( + " processing attribute value is not valid: %r, " + "expected 'first' or 'last'" % processingValue + ) + self.documentObject.rulesProcessingLast = processingValue == "last" + for ruleElement in self.root.findall(".rules/rule"): + ruleObject = self.ruleDescriptorClass() + ruleName = ruleObject.name = ruleElement.attrib.get("name") + # read any stray conditions outside a condition set + externalConditions = self._readConditionElements( + ruleElement, + ruleName, + ) + if externalConditions: + ruleObject.conditionSets.append(externalConditions) + self.log.info( + "Found stray rule conditions outside a conditionset. " + "Wrapped them in a new conditionset." + ) + # read the conditionsets + for conditionSetElement in ruleElement.findall(".conditionset"): + conditionSet = self._readConditionElements( + conditionSetElement, + ruleName, + ) + if conditionSet is not None: + ruleObject.conditionSets.append(conditionSet) + for subElement in ruleElement.findall(".sub"): + a = subElement.attrib["name"] + b = subElement.attrib["with"] + ruleObject.subs.append((a, b)) + rules.append(ruleObject) + self.documentObject.rules = rules + + def _readConditionElements(self, parentElement, ruleName=None): + cds = [] + for conditionElement in parentElement.findall(".condition"): + cd = {} + cdMin = conditionElement.attrib.get("minimum") + if cdMin is not None: + cd["minimum"] = float(cdMin) + else: + # will allow these to be None, assume axis.minimum + cd["minimum"] = None + cdMax = conditionElement.attrib.get("maximum") + if cdMax is not None: + cd["maximum"] = float(cdMax) + else: + # will allow these to be None, assume axis.maximum + cd["maximum"] = None + cd["name"] = conditionElement.attrib.get("name") + # # test for things + if cd.get("minimum") is None and cd.get("maximum") is None: + raise DesignSpaceDocumentError( + "condition missing required minimum or maximum in rule" + + (" '%s'" % ruleName if ruleName is not None else "") + ) + cds.append(cd) + return cds + + def readAxes(self): + # read the axes elements, including the warp map. + axesElement = self.root.find(".axes") + if axesElement is not None and "elidedfallbackname" in axesElement.attrib: + self.documentObject.elidedFallbackName = axesElement.attrib[ + "elidedfallbackname" + ] + axisElements = self.root.findall(".axes/axis") + if not axisElements: + return + for axisElement in axisElements: + if ( + self.documentObject.formatTuple >= (5, 0) + and "values" in axisElement.attrib + ): + axisObject = self.discreteAxisDescriptorClass() + axisObject.values = [ + float(s) for s in axisElement.attrib["values"].split(" ") + ] + else: + axisObject = self.axisDescriptorClass() + axisObject.minimum = float(axisElement.attrib.get("minimum")) + axisObject.maximum = float(axisElement.attrib.get("maximum")) + axisObject.default = float(axisElement.attrib.get("default")) + axisObject.name = axisElement.attrib.get("name") + if axisElement.attrib.get("hidden", False): + axisObject.hidden = True + axisObject.tag = axisElement.attrib.get("tag") + for mapElement in axisElement.findall("map"): + a = float(mapElement.attrib["input"]) + b = float(mapElement.attrib["output"]) + axisObject.map.append((a, b)) + for labelNameElement in axisElement.findall("labelname"): + # Note: elementtree reads the "xml:lang" attribute name as + # '{http://www.w3.org/XML/1998/namespace}lang' + for key, lang in labelNameElement.items(): + if key == XML_LANG: + axisObject.labelNames[lang] = tostr(labelNameElement.text) + labelElement = axisElement.find(".labels") + if labelElement is not None: + if "ordering" in labelElement.attrib: + axisObject.axisOrdering = int(labelElement.attrib["ordering"]) + for label in labelElement.findall(".label"): + axisObject.axisLabels.append(self.readAxisLabel(label)) + self.documentObject.axes.append(axisObject) + self.axisDefaults[axisObject.name] = axisObject.default + + self.documentObject.axisMappings = [] + for mappingsElement in self.root.findall(".axes/mappings"): + groupDescription = mappingsElement.attrib.get("description") + for mappingElement in mappingsElement.findall("mapping"): + description = mappingElement.attrib.get("description") + inputElement = mappingElement.find("input") + outputElement = mappingElement.find("output") + inputLoc = {} + outputLoc = {} + for dimElement in inputElement.findall(".dimension"): + name = dimElement.attrib["name"] + value = float(dimElement.attrib["xvalue"]) + inputLoc[name] = value + for dimElement in outputElement.findall(".dimension"): + name = dimElement.attrib["name"] + value = float(dimElement.attrib["xvalue"]) + outputLoc[name] = value + axisMappingObject = self.axisMappingDescriptorClass( + inputLocation=inputLoc, + outputLocation=outputLoc, + description=description, + groupDescription=groupDescription, + ) + self.documentObject.axisMappings.append(axisMappingObject) + + def readAxisLabel(self, element: ET.Element): + xml_attrs = { + "userminimum", + "uservalue", + "usermaximum", + "name", + "elidable", + "oldersibling", + "linkeduservalue", + } + unknown_attrs = set(element.attrib) - xml_attrs + if unknown_attrs: + raise DesignSpaceDocumentError( + f"label element contains unknown attributes: {', '.join(unknown_attrs)}" + ) + + name = element.get("name") + if name is None: + raise DesignSpaceDocumentError("label element must have a name attribute.") + valueStr = element.get("uservalue") + if valueStr is None: + raise DesignSpaceDocumentError( + "label element must have a uservalue attribute." + ) + value = float(valueStr) + minimumStr = element.get("userminimum") + minimum = float(minimumStr) if minimumStr is not None else None + maximumStr = element.get("usermaximum") + maximum = float(maximumStr) if maximumStr is not None else None + linkedValueStr = element.get("linkeduservalue") + linkedValue = float(linkedValueStr) if linkedValueStr is not None else None + elidable = True if element.get("elidable") == "true" else False + olderSibling = True if element.get("oldersibling") == "true" else False + labelNames = { + lang: label_name.text or "" + for label_name in element.findall("labelname") + for attr, lang in label_name.items() + if attr == XML_LANG + # Note: elementtree reads the "xml:lang" attribute name as + # '{http://www.w3.org/XML/1998/namespace}lang' + } + return self.axisLabelDescriptorClass( + name=name, + userValue=value, + userMinimum=minimum, + userMaximum=maximum, + elidable=elidable, + olderSibling=olderSibling, + linkedUserValue=linkedValue, + labelNames=labelNames, + ) + + def readLabels(self): + if self.documentObject.formatTuple < (5, 0): + return + + xml_attrs = {"name", "elidable", "oldersibling"} + for labelElement in self.root.findall(".labels/label"): + unknown_attrs = set(labelElement.attrib) - xml_attrs + if unknown_attrs: + raise DesignSpaceDocumentError( + f"Label element contains unknown attributes: {', '.join(unknown_attrs)}" + ) + + name = labelElement.get("name") + if name is None: + raise DesignSpaceDocumentError( + "label element must have a name attribute." + ) + designLocation, userLocation = self.locationFromElement(labelElement) + if designLocation: + raise DesignSpaceDocumentError( + f'