You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[C API] Add all macros to Misc/stable_abi.toml #158886
macros.py: extract macros from Include/*.h header files
get_version.py: check if a macro is really defined in the limited C API and get the Python version which introduced it (using git show VERSION:FILENAME)
update.py: generate an ouput in stable_abi.toml format. It checks if a macro is already documented as a function in Doc/c-api/*.rst.
The get_version.py requires many checkout of the Python Git repository, one per version (3.2 to 3.15 + "main"). Just comment th call to "double_check()" if you don't want that (it's not really needed, it's to check if the script has bugs).
Check stderr for anything suspicious. There are many warnings about datetime.h macros which are not in the limited C API.
macros.py:
Details
importglobimportos.pathimportreimportsysEXCLUDE_HEADERS= {
# Header files not included by Python.h and ignored by this script'dynamic_annotations.h',
'errcode.h',
'opcode.h',
'opcode_ids.h',
'osdefs.h',
'pyexpat.h',
'structmember.h',
# Header files not included by Python.h but parsed by this script:# - datetime.h# - frameobject.h# - marshal.h# - py_curses.h# - pydtrace.h
}
SRC_DIR=os.path.dirname(__file__)
DEFINE_REGEX=re.compile(r'\s*#\s*(?P<directive>define|undef)\s+(?P<macro>.*)')
PYTHON_PREFIX=re.compile(r'(Py|PY)')
ID_REGEX=r'(?:[a-zA-Z_][a-zA-Z0-9_]*)'ARGS_REGEX=fr'(?:(?:{ID_REGEX})(?:,\s*{ID_REGEX})*\s*|)'NAME_REGEX=re.compile(r'([A-Za-z_][A-Za-z0-9_]*)\b\s*\(\s*'+ARGS_REGEX+r'\)')
CAUSES_BY_DIRECTIVE= {
"define": "defined",
"undef": "undefined",
}
defparse_file(filename, names):
withopen(filename, encoding='utf8') asfp:
forlineno, lineinenumerate(fp, start=1):
# Check for '#define MACRO'match=DEFINE_REGEX.match(line)
ifnotmatch:
continuemacro=match['macro']
directive=match['directive']
cause=CAUSES_BY_DIRECTIVE.get(directive, directive)
ifnotPYTHON_PREFIX.match(macro):
continuematch=NAME_REGEX.match(macro)
ifnotmatch:
continuename=match[1]
names.append((name, filename, lineno, cause))
defmain():
# Parse header filesinclude_dir=os.path.join(SRC_DIR, 'Include')
files=glob.glob(os.path.join(include_dir, '*.h'))
files= [filenameforfilenameinfilesifos.path.basename(filename) notinEXCLUDE_HEADERS]
names= [] # list of (name: str, filename: str, lineno: int, cause: str)forfilenameinfiles:
parse_file(filename, names)
names= [nameforname, *ignoredinnames]
fornameinsorted(set(names)):
print(name)
if__name__=="__main__":
main()
get_version.py:
Details
importsysimportglobimportsubprocessimportredefgit_tag(ver):
ifver==15:
return ('3.15', 'v3.15.0rc3')
ifverin (0, 1, 2):
return (f'3.{ver}', f'v3.{ver}')
return (f'3.{ver}', f'v3.{ver}.0')
defold_filename(filename):
iffilename=='Include/refcount.h':
return'Include/object.h'iffilename=='Include/exports.h':
return'Include/pyport.h'returnNonedefcommand(args):
proc=subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)
return (proc.returncode, proc.stdout)
IF_LIMITED_API_LINE=re.compile(r'#if.*(Py_LIMITED_API|Py_BUILD_CORE).*$', re.MULTILINE)
IF_LIMITED_API=re.compile(
r' defined\(Py_LIMITED_API\) &&'r'| !defined\(Py_LIMITED_API\) \|\| \(?Py_LIMITED_API\+0 (<|>=)'r'|#ifdef Py_LIMITED_API'r'|#if Py_LIMITED_API == 3'r'|#if defined\(Py_DEBUG\) \|\| defined\(Py_LIMITED_API\)'r'|#if defined\(Py_REF_DEBUG\) && !\(defined\(Py_LIMITED_API\) && Py_LIMITED_API\+0 'r'|#if \(defined\(Py_LIMITED_API\) && \(?Py_LIMITED_API\+0 '
)
IFNDEF_LIMITED_API=re.compile(
r'#ifndef Py_LIMITED_API'r'|#if !defined\(Py_LIMITED_API\)'r'|#if .* && !defined\(Py_LIMITED_API\)'# r'|#if .* && \(!defined\(Py_LIMITED_API\) && 'r'|#if !defined\(Py_LIMITED_API\) && 'r'|#if !defined\(Py_LIMITED_API\) && 'r'|#ifdef Py_BUILD_CORE'r'|#if defined\(Py_BUILD_CORE\)'r'|#if defined\(Py_BUILD_CORE_BUILTIN\) && 'r'|#if defined\(Py_BUILD_CORE_MODULE\) && '
)
FIND_ENDIF=re.compile(r'#\s*(endif)|#\s*(else)|#\s*(if)')
# Very ugly parser to exclude non-limited API.# For example, remove '#ifndef Py_LIMITED_API (...) #endif'.# and remove '#ifdef Py_BUILD_CORE (...) #else'.defstrip_non_limited_api(filename, code):
start=0whileTrue:
line_match=IF_LIMITED_API_LINE.search(code, start)
ifline_matchisNone:
breakline=line_match.group(0)
match=IF_LIMITED_API.search(line)
ifmatchisnotNone:
start+=match.end()
continueifIFNDEF_LIMITED_API.match(line):
ifndef_start=line_match.start()
match=FIND_ENDIF.search(code, line_match.end())
ifmatchisNone:
print(f"Error: {filename}: unable to find #endif")
sys.exit(1)
depth=1whileTrue:
what=match.group(1) ormatch.group(2) ormatch.group(3)
ifdepth==1andwhatin ('endif', 'else'):
ifwhat=='else':
end=match.start()
else:
end=match.end()
code=code[:ifndef_start] +code[end:]
start=ifndef_startbreakifwhat=='endif':
depth-=1elifwhat=='if':
# we found a nested #if, oh no!depth+=1# elif kind == 'else': keep the same depthstart=match.end()
match=FIND_ENDIF.search(code, start)
ifmatchisNone:
print(f"Error: {filename}: unable to find #endif")
sys.exit(1)
continueprint(f"Error: {filename}: unable to parse: {line!r}")
sys.exit(1)
returncodedefdouble_check(version, name):
forfilenameinsorted(glob.glob(f'../{version}/Include/*.h')):
withopen(filename) asfp:
code=fp.read()
#code = strip_non_limited_api(filename, code)ifnameincode:
print(f"WARNING: {name} found in {version}:{filename}", file=sys.stderr)
returndefget_version(name):
tags= [('3.16', 'main')]
tags+= [git_tag(version) forversioninlist(range(15, -1, -1))]
regex=re.compile(r'#\s*define\s+'+name)
forfilenameinsorted(glob.glob('Include/*.h')):
withopen(filename) asfp:
code=fp.read()
code=strip_non_limited_api(filename, code)
ifregex.search(code):
breakelse:
double_check('main', name)
#raise ValueError(f"unable to find {name!r}")return (None, None)
found=Noneforversion, tagintags:
cmd= ['git', 'show', f'{tag}:{filename}']
returncode, code=command(cmd)
ifreturncode==128:
filename2=old_filename(filename)
iffilename2:
cmd= ['git', 'show', f'{tag}:{filename2}']
returncode, code=command(cmd)
ifreturncode==0:
filename=filename2else:
# file doesn't exist in old Python versiondouble_check(version, name)
breakifreturncode:
print(f"{cmd} failed with exit code {returncode}")
sys.exit(1)
code=strip_non_limited_api(filename, code)
ifregex.search(code):
found=versionelse:
double_check(version, name)
#print(f"{name} doesn't exist in {version}:{filename}")breakiffoundisNone:
print("Unable to get version")
sys.exit(1)
return (found, filename)
defmain():
name=sys.argv[1]
version, filename=get_version(name)
ifversion:
print(f"{name} was added to Python {version} in {filename}")
else:
print(f"ERROR: {name} is NOT part of the limited C API")
sys.exit(1)
if__name__=="__main__":
main()
Activity
vstinner commentedon Oct 5, 2026
I wrote some very ugly scripts to:
Include/*.hheader filesgit show VERSION:FILENAME)stable_abi.tomlformat. It checks if a macro is already documented as a function inDoc/c-api/*.rst.The
get_version.pyrequires many checkout of the Python Git repository, one per version (3.2 to 3.15 + "main"). Just comment th call to "double_check()" if you don't want that (it's not really needed, it's to check if the script has bugs).Check
stderrfor anything suspicious. There are many warnings aboutdatetime.hmacros which are not in the limited C API.macros.py:
Details
get_version.py:
Details
update.py:
Details
vstinner commentedon Oct 5, 2026
Oh, there are 3 bugs;
#define Py_INVALID_SIZE (Py_ssize_t)-1The
macros.pyscripts only checks for macros which have arguments:#define Py_MIN(x, y)is listed, whereas#define Py_T_SHORT 0is not listed.vstinner commentedon Oct 5, 2026
More bugs:
PyBUF_CONTIG_ROandPyBUF_STRIDED_ROare constants, not functions. By the way, they were already listed bystable_abi.toml!vstinner commentedon Oct 5, 2026
To avoid error on building the documentation, I declared
PyBool_Check()macro as a function since it's documented as a function byDoc/c-api/bool.rst:Same for other macros documented as functions.
Problem: if it's declared as a function by stable_abi.toml,
Lib/test/test_stable_abi_ctypes.pylooks for symbols in Python... and it fails.vstinner commentedon Oct 5, 2026
More bugs: PyDoc_STR, PyDoc_STRVAR, PyDoc_VAR and Py_CHARMASK were already present in Python 3.2 in fact. I already updated the PR.
encukou commentedon Oct 6, 2026
For some macros like
Py_ULLorPyAPI_DATA, I think it would be better to treat them as exposed by mistake, rather than part of limited API.gh-158886: Add INCREF/DECREF macros to stable_abi.toml (GH-158941)
[3.15] gh-158886: Add INCREF/DECREF macros to stable_abi.toml (GH-158941
[3.14] gh-158886: Add INCREF/DECREF macros to stable_abi.toml (GH-158941