Skip to content
Merged
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
- "3.8"
- "3.9"
- "3.10"
- "3.11.0-alpha.5"
- "3.11.0-beta.3"
- "pypy2"
- "pypy3"
include:
Expand Down
7 changes: 4 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ Python C API compatibility
:alt: Build status of pythoncapi-compat on GitHub Actions
:target: https://github.com/python/pythoncapi-compat/actions

The ``pythoncapi-compat`` project can be used to write a C extension supporting
a wide range of Python versions with a single code base. It is made of the
``pythoncapi_compat.h`` header file and the ``upgrade_pythoncapi.py`` script.
The ``pythoncapi-compat`` project can be used to write a C or C++ extension
supporting a wide range of Python versions with a single code base. It is made
of the ``pythoncapi_compat.h`` header file and the ``upgrade_pythoncapi.py``
script.

``upgrade_pythoncapi.py`` requires Python 3.6 or newer.

Expand Down
2 changes: 1 addition & 1 deletion docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Supported Python versions:
* Python 2.7, Python 3.4 - 3.11
* PyPy 2.7, 3.6 and 3.7

C++ is supported on Python 3.6 and newer.
C++03 and C++11 are supported on Python 3.6 and newer.

A C99 subset is required, like ``static inline`` functions: see `PEP 7
<https://www.python.org/dev/peps/pep-0007/>`_. ISO C90 is partially supported
Expand Down
1 change: 1 addition & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
Changelog
=========

* 2022-06-14: Fix compatibility with C++ older than C++11.
* 2022-05-03: Add ``PyCode_GetCode()`` function.
* 2022-04-26: Rename the project from ``pythoncapi_compat`` to
``pythoncapi-compat``: replace the underscore separator with a dash.
Expand Down
7 changes: 4 additions & 3 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
Python C API compatibility
++++++++++++++++++++++++++

The ``pythoncapi-compat`` project can be used to write a C extension supporting
a wide range of Python versions with a single code base. It is made of the
``pythoncapi_compat.h`` header file and the ``upgrade_pythoncapi.py`` script.
The ``pythoncapi-compat`` project can be used to write a C or C++ extension
supporting a wide range of Python versions with a single code base. It is made
of the ``pythoncapi_compat.h`` header file and the ``upgrade_pythoncapi.py``
script.

* Homepage: `GitHub pythoncapi-compat project
<https://github.com/python/pythoncapi-compat>`_.
Expand Down
35 changes: 5 additions & 30 deletions pythoncapi_compat.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,39 +32,14 @@ extern "C" {
#endif


// C++ compatibility: _Py_CAST() and _Py_NULL
#ifndef _Py_CAST
# ifdef __cplusplus
extern "C++" {
namespace {
template <typename type, typename expr_type>
inline type _Py_CAST_impl(expr_type *expr) {
return reinterpret_cast<type>(expr);
}

template <typename type, typename expr_type>
inline type _Py_CAST_impl(expr_type const *expr) {
return reinterpret_cast<type>(const_cast<expr_type *>(expr));
}

template <typename type, typename expr_type>
inline type _Py_CAST_impl(expr_type &expr) {
return static_cast<type>(expr);
}

template <typename type, typename expr_type>
inline type _Py_CAST_impl(expr_type const &expr) {
return static_cast<type>(const_cast<expr_type &>(expr));
}
}
}
# define _Py_CAST(type, expr) _Py_CAST_impl<type>(expr)
# else
# define _Py_CAST(type, expr) ((type)(expr))
# endif
# define _Py_CAST(type, expr) ((type)(expr))
#endif

// On C++11 and newer, _Py_NULL is defined as nullptr on C++11,
// otherwise it is defined as NULL.
#ifndef _Py_NULL
# ifdef __cplusplus
# if defined(__cplusplus) && __cplusplus >= 201103
# define _Py_NULL nullptr
# else
# define _Py_NULL NULL
Expand Down
44 changes: 30 additions & 14 deletions tests/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,18 @@
import sys


TEST_CPP = False
# C++ is only supported on Python 3.6 and newer
TEST_CPP = (sys.version_info >= (3, 6))
if 0x30b0000 <= sys.hexversion <= 0x30b00b3:
# Don't test C++ on Python 3.11b1 - 3.11b3: these versions have C++
# compatibility issues.
TEST_CPP = False

SRC_DIR = os.path.normpath(os.path.join(os.path.dirname(__file__), '..'))

# Windows uses MSVC compiler
MSVC = (os.name == "nt")

# C compiler flags for GCC and clang
COMMON_FLAGS = [
# Treat warnings as error
Expand Down Expand Up @@ -37,15 +46,9 @@ def main():
except ImportError:
from distutils.core import setup, Extension

if len(sys.argv) >= 3 and sys.argv[2] == '--build-cppext':
global TEST_CPP
TEST_CPP = True
del sys.argv[2]

cflags = ['-I' + SRC_DIR]
cppflags = list(cflags)
# Windows uses MSVC compiler
if os.name != "nt":
if not MSVC:
cflags.extend(CFLAGS)
cppflags.extend(CPPFLAGS)

Expand All @@ -58,12 +61,25 @@ def main():

if TEST_CPP:
# C++ extension
cpp_ext = Extension(
'test_pythoncapi_compat_cppext',
sources=['test_pythoncapi_compat_cppext.cpp'],
extra_compile_args=cppflags,
language='c++')
extensions.append(cpp_ext)

# MSVC has /std flag but doesn't support /std:c++11
if not MSVC:
versions = [
('test_pythoncapi_compat_cpp03ext', '-std=c++03'),
('test_pythoncapi_compat_cpp11ext', '-std=c++11'),
]
else:
versions = [('test_pythoncapi_compat_cppext', None)]
for name, flag in versions:
flags = list(cppflags)
if flag is not None:
flags.append(flag)
cpp_ext = Extension(
name,
sources=['test_pythoncapi_compat_cppext.cpp'],
extra_compile_args=flags,
language='c++')
extensions.append(cpp_ext)

setup(name="test_pythoncapi_compat",
ext_modules=extensions)
Expand Down
78 changes: 45 additions & 33 deletions tests/test_pythoncapi_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,13 @@
from utils import run_command, command_stdout


# C++ is only supported on Python 3.6 and newer
TEST_CPP = (sys.version_info >= (3, 6))
TESTS = [
("test_pythoncapi_compat_cext", "C"),
("test_pythoncapi_compat_cppext", "C++"),
("test_pythoncapi_compat_cpp03ext", "C++03"),
("test_pythoncapi_compat_cpp11ext", "C++11"),
]

VERBOSE = False


Expand All @@ -42,15 +47,10 @@ def display_title(title):


def build_ext():
if TEST_CPP:
display_title("Build the C and C++ extensions")
else:
display_title("Build the C extension")
display_title("Build test extensions")
if os.path.exists("build"):
shutil.rmtree("build")
cmd = [sys.executable, "setup.py", "build"]
if TEST_CPP:
cmd.append('--build-cppext')
if VERBOSE:
run_command(cmd)
print()
Expand Down Expand Up @@ -98,15 +98,42 @@ def _check_refleak(test_func, verbose):
raise AssertionError("refcnt leak, diff: %s" % diff)


def run_tests(module_name):
if "cppext" in module_name:
lang = "C++"
def python_version():
ver = sys.version_info
build = 'debug' if hasattr(sys, 'gettotalrefcount') else 'release'
if hasattr(sys, 'implementation'):
python_impl = sys.implementation.name
if python_impl == 'cpython':
python_impl = 'CPython'
elif python_impl == 'pypy':
python_impl = 'PyPy'
else:
lang = "C"
if "PyPy" in sys.version:
python_impl = "PyPy"
else:
python_impl = 'Python'
return "%s %s.%s (%s build)" % (python_impl, ver.major, ver.minor, build)


def run_tests(module_name, lang):
title = "Test %s (%s)" % (module_name, lang)
display_title(title)

testmod = import_tests(module_name)
try:
testmod = import_tests(module_name)
except ImportError:
# The C extension must always be available
if lang == "C":
raise

if VERBOSE:
print("%s: skip %s, missing %s extension"
% (python_version(), lang, module_name))
print()
return

if VERBOSE and hasattr(testmod, "__cplusplus"):
print("__cplusplus: %s" % testmod.__cplusplus)

check_refleak = hasattr(sys, 'gettotalrefcount')

Expand All @@ -125,21 +152,8 @@ def test_func():
if VERBOSE:
print()

ver = sys.version_info
build = 'debug' if hasattr(sys, 'gettotalrefcount') else 'release'
msg = "%s %s tests succeeded!" % (len(tests), lang)
if hasattr(sys, 'implementation'):
python_impl = sys.implementation.name
if python_impl == 'cpython':
python_impl = 'CPython'
elif python_impl == 'pypy':
python_impl = 'PyPy'
else:
if "PyPy" in sys.version:
python_impl = "PyPy"
else:
python_impl = 'Python'
msg = "%s %s.%s (%s build): %s" % (python_impl, ver.major, ver.minor, build, msg)
msg = "%s: %s" % (python_version(), msg)
if check_refleak:
msg = "%s (no reference leak detected)" % msg
print(msg)
Expand All @@ -150,9 +164,8 @@ def main():
VERBOSE = ("-v" in sys.argv[1:] or "--verbose" in sys.argv[1:])

# Implementing PyFrame_GetLocals() and PyCode_GetCode() require the
# internal C API in Python 3.11 alpha versions. Skip also Python 3.11b1
# which has issues with C++ casts: _Py_CAST() macro.
if 0x30b0000 <= sys.hexversion <= 0x30b00b1:
# internal C API in Python 3.11 alpha versions.
if 0x30b0000 <= sys.hexversion < 0x30b00b1:
version = sys.version.split()[0]
print("SKIP TESTS: Python %s is not supported" % version)
return
Expand All @@ -166,9 +179,8 @@ def main():

build_ext()

run_tests("test_pythoncapi_compat_cext")
if TEST_CPP:
run_tests("test_pythoncapi_compat_cppext")
for module_name, lang in TESTS:
run_tests(module_name, lang)


if __name__ == "__main__":
Expand Down
Loading