Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Crude hack for testing stable ABI
import unittest
import subprocess
import tempfile
import collections
import sys
import re
#
# Collect symbols tested for below
#
_STABLE_SYMBOLS = collections.defaultdict(set)
def stable_symbol(symbolname, py_ver=None):
def decorator(function):
_STABLE_SYMBOLS[py_ver].add(symbolname)
return function
return decorator
#
# Actual test cases
#
class TestPubliCAPI (unittest.TestCase):
def validate_symbols(self, api_version, *symbolnames):
with tempfile.NamedTemporaryFile(mode='w+', suffix='.c') as fp:
fp.write('#include "Python.h"\n')
fp.write('#include <stdio.h>\n')
fp.write('\n')
fp.write('int main(void) {\n')
for symbol in symbolnames:
fp.write(f' printf("%p\\n", &{symbol});\n')
fp.write(' return 0;\n')
fp.write('}\n')
fp.flush()
if api_version is None:
api_flags = "-DPy_LIMITED_API"
else:
version_num = tuple(int(x) for x in api_version.split('.'))
api_flags = "-DPy_LIMITED_API=0x%02x%02x0000"%version_num
subprocess.check_call(["cc", api_flags,
"-Werror=implicit-function-declaration",
"-I", "/Library/Frameworks/Python.framework/Versions/3.7/include/python3.7m/",
"-L", "/Library/Frameworks/Python.framework/Versions/3.7/lib/",
'-lpython3.7',
fp.name])
@stable_symbol('PyTuple_GetItem')
@stable_symbol('PyIndex_Check')
def test_abi(self):
self.validate_symbols(None, 'PyTuple_GetItem')
self.validate_symbols(None, 'PyIndex_Check')
@stable_symbol('PySlice_Unpack', '3.7')
@stable_symbol('PySlice_AdjustIndices', '3.7')
@stable_symbol('PyInterpreterState_GetID', '3.7')
def test_abi_37(self):
self.validate_symbols('3.7', 'PySlice_Unpack', 'PySlice_AdjustIndices', 'PyInterpreterState_GetID')
#
# The hard part, find a way to reliably extract symbols from headers (and on windows the stable abi library stub)
#
class TestExportedSymbols (unittest.TestCase):
def get_exported_names(self, api_version=None):
with tempfile.NamedTemporaryFile(mode='w+', suffix='.c') as fp:
fp.write('#include "Python.h"\n')
fp.flush()
if api_version is None:
output = subprocess.check_output(["cc", "-DPy_LIMITED_API", "-I", "/Library/Frameworks/Python.framework/Versions/3.7/include/python3.7m/", "-E", fp.name])
else:
version_num = tuple(int(x) for x in api_version.split('.'))
output = subprocess.check_output(["cc", "-DPy_LIMITED_API=0x%02x%02x0000"%version_num, "-I", "/Library/Frameworks/Python.framework/Versions/3.7/include/python3.7m/", "-E", fp.name])
python_file = False
symbols = set()
definitions = []
for ln in output.decode('utf-8').splitlines():
if not ln:
continue
elif ln.startswith('#') and ln.split(None, 2)[1].isdigit():
python_file = sys.prefix in ln.split(None,2)[-1]
else:
if python_file:
definitions.append(ln)
exported_names = set()
for definition in re.findall('[^;]*;', ' '.join(definitions), re.MULTILINE):
#print("BEFORE", definition)
definition = re.sub('__attribute__.*', '', definition)
definition = re.sub('\([^)]*\)', '', definition)
definition = re.sub('{[^)]*}', '', definition)
definition = definition.replace(';', '')
definition = definition.strip()
exported_names.add(definition.split()[-1].lstrip('*'))
return exported_names
def test_exported_names(self):
seen = set()
default_exported = self.get_exported_names()
with self.subTest("default"):
seen.update(default_exported)
self.assertEqual(len(_STABLE_SYMBOLS[None]), len(default_exported))
for version in ('3.%d'%(i,) for i in range(sys.version_info.minor+1)):
exported = self.get_exported_names(version) - seen
seen.update(exported)
with self.subTest(version):
self.assertEqual(_STABLE_SYMBOLS[version], exported)
if __name__ == "__main__":
unittest.main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment