Skip to content

Commit 18c6b5e

Browse files
committed
Support building against the Limited API by providing fallback implementations.
1 parent 6e2db49 commit 18c6b5e

6 files changed

Lines changed: 1571 additions & 12 deletions

File tree

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ clean:
4141
realclean: clean
4242
rm -fr src/*.c src/*.html
4343

44+
src/todecimal.h: gen_todecimal.py
45+
$(PYTHON) $< > $@
46+
4447
qemu-user-static:
4548
docker run --rm --privileged hypriot/qemu-register
4649

gen_todecimal.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
2+
import sys
3+
import unicodedata
4+
from collections import defaultdict
5+
6+
7+
def list_digits():
8+
category_of = unicodedata.category
9+
return [
10+
ch for ch in range(128, 1114111+1)
11+
if category_of(chr(ch)) == 'Nd'
12+
]
13+
14+
15+
def map_to_ascii_digit(digits):
16+
adigit_to_udigit = defaultdict(list)
17+
for ch in digits:
18+
adigit = int(chr(ch))
19+
if ch & 15 == adigit:
20+
adigit_to_udigit['{digit} & 15'].append(ch)
21+
elif (ch - 6) & 15 == adigit:
22+
adigit_to_udigit['({digit} - 6) & 15'].append(ch)
23+
elif (ch - 0x116da) & 15 == adigit:
24+
adigit_to_udigit['({digit} - 0x116da) & 15'].append(ch)
25+
elif (ch - 0x1e5f1) & 15 == adigit:
26+
adigit_to_udigit['({digit} - 0x1e5f1) & 15'].append(ch)
27+
elif ch >= 0x1d7ce and (ch - 0x1d7ce) % 10 == adigit:
28+
adigit_to_udigit['({digit} - 0x1d7ce) % 10'].append(ch)
29+
else:
30+
adigit_to_udigit[str(adigit)].append(ch)
31+
return adigit_to_udigit
32+
33+
34+
def gen_switch_cases(digits_by_adigit):
35+
pyver = sys.version_info
36+
print(f"/* Switch cases generated from Python {pyver[0]}.{pyver[1]}.{pyver[2]} {pyver[3]} {pyver[4]} */")
37+
print("/* Deliberately excluding ASCII digit characters. */")
38+
print()
39+
print(f"/* Lowest digit: {min(ch for characters in digits_by_adigit.values() for ch in characters)} */")
40+
print("switch (digit) {")
41+
42+
for adigit_format, udigits in digits_by_adigit.items():
43+
for ch in udigits:
44+
print(f' case 0x{ch:x}:')
45+
46+
print(f" return {adigit_format.format(digit='digit')};")
47+
48+
print(" default:")
49+
print(" return -1;")
50+
print("}")
51+
52+
if __name__ == '__main__':
53+
gen_switch_cases(map_to_ascii_digit(list_digits()))

setup.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@
66
from setuptools import setup, Extension
77

88

9-
ext_modules = [
10-
Extension("quicktions", ["src/quicktions.pyx"]),
11-
]
12-
139
try:
1410
sys.argv.remove("--with-profile")
1511
except ValueError:
@@ -20,6 +16,44 @@
2016
enable_coverage = os.environ.get("WITH_COVERAGE") == "1"
2117
force_rebuild = os.environ.get("FORCE_REBUILD") == "1"
2218

19+
def check_limited_api_option(value):
20+
if not value:
21+
return None
22+
value = value.lower()
23+
if value == "true":
24+
# The default Limited API version is 3.9, unless we're on a lower Python version
25+
# (which is mainly for the sake of testing 3.8 on the CI)
26+
if sys.version_info >= (3, 9):
27+
return (3, 9)
28+
else:
29+
return sys.version_info[:2]
30+
if value == 'false':
31+
return None
32+
major, minor = value.split('.', 1)
33+
return (int(major), int(minor))
34+
35+
extra_setup_args = {}
36+
c_defines = []
37+
38+
option_limited_api = check_limited_api_option(os.environ.get("QUICKTIONS_LIMITED_API"))
39+
if option_limited_api:
40+
c_defines.append(('Py_LIMITED_API', f'0x{option_limited_api[0]:02x}{option_limited_api[1]:02x}0000'))
41+
42+
setup_options = extra_setup_args.setdefault('options', {})
43+
bdist_wheel_options = setup_options.setdefault('bdist_wheel', {})
44+
bdist_wheel_options['py_limited_api'] = f'cp{option_limited_api[0]}{option_limited_api[1]}'
45+
46+
47+
ext_modules = [
48+
Extension(
49+
"quicktions",
50+
["src/quicktions.pyx"],
51+
py_limited_api=True if option_limited_api else False,
52+
define_macros=c_defines,
53+
),
54+
]
55+
56+
2357
try:
2458
sys.argv.remove("--with-cython")
2559
except ValueError:
@@ -101,4 +135,5 @@
101135
"Topic :: Scientific/Engineering :: Mathematics",
102136
"Topic :: Office/Business :: Financial",
103137
],
138+
**extra_setup_args,
104139
)

src/quicktions.pyx

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ __all__ = ['Fraction']
2626
__version__ = '1.23'
2727

2828
cimport cython
29-
from cpython.unicode cimport Py_UNICODE_TODECIMAL
3029
from cpython.object cimport Py_LT, Py_LE, Py_EQ, Py_NE, Py_GT, Py_GE
3130
from cpython.long cimport PyLong_FromString
3231

@@ -1830,31 +1829,50 @@ cdef _raise_parse_overflow(s):
18301829

18311830
cdef extern from *:
18321831
"""
1832+
static CYTHON_INLINE int __QUICKTIONS_to_decimal(Py_UCS4 digit) {
1833+
#if CYTHON_COMPILING_IN_LIMITED_API
1834+
#include "todigit.h"
1835+
#else
1836+
return Py_UNICODE_TODECIMAL(digit);
1837+
#endif
1838+
}
1839+
18331840
static CYTHON_INLINE int __QUICKTIONS_unpack_ustring(
18341841
PyObject* string, Py_ssize_t *length, void** data, int *kind) {
1835-
if (PyUnicode_READY(string) < 0) return -1;
1836-
*kind = PyUnicode_KIND(string);
1837-
*length = PyUnicode_GET_LENGTH(string);
1838-
*data = PyUnicode_DATA(string);
1842+
if (__Pyx_PyUnicode_READY(string) < 0) return -1;
1843+
*kind = __Pyx_PyUnicode_KIND(string);
1844+
*length = __Pyx_PyUnicode_GET_LENGTH(string);
1845+
1846+
#if CYTHON_COMPILING_IN_LIMITED_API
1847+
*data = (void*) string;
1848+
#else
1849+
*data = __Pyx_PyUnicode_DATA(string);
1850+
#endif
1851+
18391852
return 0;
18401853
}
1854+
1855+
#if CYTHON_COMPILING_IN_LIMITED_API
1856+
#define __QUICKTIONS_char_at(data, kind, index) (Py_UCS4) (((void)kind), PyUnicode_ReadChar(data, index))
1857+
#else
18411858
#define __QUICKTIONS_char_at(data, kind, index) \
18421859
(((kind == 1) ? (Py_UCS4) ((char*) data)[index] : (Py_UCS4) PyUnicode_READ(kind, data, index)))
1860+
#endif
18431861
"""
18441862
int _unpack_ustring "__QUICKTIONS_unpack_ustring" (
18451863
object string, Py_ssize_t *length, void **data, int *kind) except -1
18461864
Py_UCS4 _char_at "__QUICKTIONS_char_at" (void *data, int kind, Py_ssize_t index)
1847-
Py_UCS4 PyUnicode_READ(int kind, void *data, Py_ssize_t index)
1865+
int _to_decimal "__QUICKTIONS_to_decimal" (Py_UCS4 c)
18481866

18491867

1850-
cdef inline int _parse_digit(char** c_digits, Py_UCS4 c, int allow_unicode):
1868+
cdef inline int _parse_digit(char** c_digits, Py_UCS4 c, int allow_unicode) noexcept:
18511869
cdef unsigned int unum
18521870
cdef int num
18531871
unum = (<unsigned int> c) - <unsigned int> '0' # Relies on integer underflow for dots etc.
18541872
if unum > 9:
18551873
if not allow_unicode:
18561874
return -1
1857-
num = Py_UNICODE_TODECIMAL(c)
1875+
num = _to_decimal(c)
18581876
if num == -1:
18591877
return -1
18601878
unum = <unsigned int> num

0 commit comments

Comments
 (0)