Skip to content

Commit de360d8

Browse files
authored
Fix compatibility with C++03 (#42)
* _Py_CAST() uses old-style cast on old Python versions to prevent C++ warnings. * Fix _Py_NULL on C++ older than C++11: don't use nullptr. * Test compatibility with C++03. * Enable C tests on Python 3.11b1. * runtests.py --verbose logs __cplusplus value. * GitHub Actions: update Python 3.11 to 3.11b3.
1 parent 73e6386 commit de360d8

9 files changed

+169
-107
lines changed

.github/workflows/build.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
- "3.8"
2323
- "3.9"
2424
- "3.10"
25-
- "3.11.0-alpha.5"
25+
- "3.11.0-beta.3"
2626
- "pypy2"
2727
- "pypy3"
2828
include:

README.rst

+4-3
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ Python C API compatibility
66
:alt: Build status of pythoncapi-compat on GitHub Actions
77
:target: https://github.com/python/pythoncapi-compat/actions
88

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

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

docs/api.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Supported Python versions:
1010
* Python 2.7, Python 3.4 - 3.11
1111
* PyPy 2.7, 3.6 and 3.7
1212

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

1515
A C99 subset is required, like ``static inline`` functions: see `PEP 7
1616
<https://www.python.org/dev/peps/pep-0007/>`_. ISO C90 is partially supported

docs/changelog.rst

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
Changelog
22
=========
33

4+
* 2022-06-14: Fix compatibility with C++ older than C++11.
45
* 2022-05-03: Add ``PyCode_GetCode()`` function.
56
* 2022-04-26: Rename the project from ``pythoncapi_compat`` to
67
``pythoncapi-compat``: replace the underscore separator with a dash.

docs/index.rst

+4-3
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
Python C API compatibility
33
++++++++++++++++++++++++++
44

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

910
* Homepage: `GitHub pythoncapi-compat project
1011
<https://github.com/python/pythoncapi-compat>`_.

pythoncapi_compat.h

+5-30
Original file line numberDiff line numberDiff line change
@@ -32,39 +32,14 @@ extern "C" {
3232
#endif
3333

3434

35-
// C++ compatibility: _Py_CAST() and _Py_NULL
3635
#ifndef _Py_CAST
37-
# ifdef __cplusplus
38-
extern "C++" {
39-
namespace {
40-
template <typename type, typename expr_type>
41-
inline type _Py_CAST_impl(expr_type *expr) {
42-
return reinterpret_cast<type>(expr);
43-
}
44-
45-
template <typename type, typename expr_type>
46-
inline type _Py_CAST_impl(expr_type const *expr) {
47-
return reinterpret_cast<type>(const_cast<expr_type *>(expr));
48-
}
49-
50-
template <typename type, typename expr_type>
51-
inline type _Py_CAST_impl(expr_type &expr) {
52-
return static_cast<type>(expr);
53-
}
54-
55-
template <typename type, typename expr_type>
56-
inline type _Py_CAST_impl(expr_type const &expr) {
57-
return static_cast<type>(const_cast<expr_type &>(expr));
58-
}
59-
}
60-
}
61-
# define _Py_CAST(type, expr) _Py_CAST_impl<type>(expr)
62-
# else
63-
# define _Py_CAST(type, expr) ((type)(expr))
64-
# endif
36+
# define _Py_CAST(type, expr) ((type)(expr))
6537
#endif
38+
39+
// On C++11 and newer, _Py_NULL is defined as nullptr on C++11,
40+
// otherwise it is defined as NULL.
6641
#ifndef _Py_NULL
67-
# ifdef __cplusplus
42+
# if defined(__cplusplus) && __cplusplus >= 201103
6843
# define _Py_NULL nullptr
6944
# else
7045
# define _Py_NULL NULL

tests/setup.py

+30-14
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,18 @@
33
import sys
44

55

6-
TEST_CPP = False
6+
# C++ is only supported on Python 3.6 and newer
7+
TEST_CPP = (sys.version_info >= (3, 6))
8+
if 0x30b0000 <= sys.hexversion <= 0x30b00b3:
9+
# Don't test C++ on Python 3.11b1 - 3.11b3: these versions have C++
10+
# compatibility issues.
11+
TEST_CPP = False
12+
713
SRC_DIR = os.path.normpath(os.path.join(os.path.dirname(__file__), '..'))
814

15+
# Windows uses MSVC compiler
16+
MSVC = (os.name == "nt")
17+
918
# C compiler flags for GCC and clang
1019
COMMON_FLAGS = [
1120
# Treat warnings as error
@@ -37,15 +46,9 @@ def main():
3746
except ImportError:
3847
from distutils.core import setup, Extension
3948

40-
if len(sys.argv) >= 3 and sys.argv[2] == '--build-cppext':
41-
global TEST_CPP
42-
TEST_CPP = True
43-
del sys.argv[2]
44-
4549
cflags = ['-I' + SRC_DIR]
4650
cppflags = list(cflags)
47-
# Windows uses MSVC compiler
48-
if os.name != "nt":
51+
if not MSVC:
4952
cflags.extend(CFLAGS)
5053
cppflags.extend(CPPFLAGS)
5154

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

5962
if TEST_CPP:
6063
# C++ extension
61-
cpp_ext = Extension(
62-
'test_pythoncapi_compat_cppext',
63-
sources=['test_pythoncapi_compat_cppext.cpp'],
64-
extra_compile_args=cppflags,
65-
language='c++')
66-
extensions.append(cpp_ext)
64+
65+
# MSVC has /std flag but doesn't support /std:c++11
66+
if not MSVC:
67+
versions = [
68+
('test_pythoncapi_compat_cpp03ext', '-std=c++03'),
69+
('test_pythoncapi_compat_cpp11ext', '-std=c++11'),
70+
]
71+
else:
72+
versions = [('test_pythoncapi_compat_cppext', None)]
73+
for name, flag in versions:
74+
flags = list(cppflags)
75+
if flag is not None:
76+
flags.append(flag)
77+
cpp_ext = Extension(
78+
name,
79+
sources=['test_pythoncapi_compat_cppext.cpp'],
80+
extra_compile_args=flags,
81+
language='c++')
82+
extensions.append(cpp_ext)
6783

6884
setup(name="test_pythoncapi_compat",
6985
ext_modules=extensions)

tests/test_pythoncapi_compat.py

+45-33
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,13 @@
2323
from utils import run_command, command_stdout
2424

2525

26-
# C++ is only supported on Python 3.6 and newer
27-
TEST_CPP = (sys.version_info >= (3, 6))
26+
TESTS = [
27+
("test_pythoncapi_compat_cext", "C"),
28+
("test_pythoncapi_compat_cppext", "C++"),
29+
("test_pythoncapi_compat_cpp03ext", "C++03"),
30+
("test_pythoncapi_compat_cpp11ext", "C++11"),
31+
]
32+
2833
VERBOSE = False
2934

3035

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

4348

4449
def build_ext():
45-
if TEST_CPP:
46-
display_title("Build the C and C++ extensions")
47-
else:
48-
display_title("Build the C extension")
50+
display_title("Build test extensions")
4951
if os.path.exists("build"):
5052
shutil.rmtree("build")
5153
cmd = [sys.executable, "setup.py", "build"]
52-
if TEST_CPP:
53-
cmd.append('--build-cppext')
5454
if VERBOSE:
5555
run_command(cmd)
5656
print()
@@ -98,15 +98,42 @@ def _check_refleak(test_func, verbose):
9898
raise AssertionError("refcnt leak, diff: %s" % diff)
9999

100100

101-
def run_tests(module_name):
102-
if "cppext" in module_name:
103-
lang = "C++"
101+
def python_version():
102+
ver = sys.version_info
103+
build = 'debug' if hasattr(sys, 'gettotalrefcount') else 'release'
104+
if hasattr(sys, 'implementation'):
105+
python_impl = sys.implementation.name
106+
if python_impl == 'cpython':
107+
python_impl = 'CPython'
108+
elif python_impl == 'pypy':
109+
python_impl = 'PyPy'
104110
else:
105-
lang = "C"
111+
if "PyPy" in sys.version:
112+
python_impl = "PyPy"
113+
else:
114+
python_impl = 'Python'
115+
return "%s %s.%s (%s build)" % (python_impl, ver.major, ver.minor, build)
116+
117+
118+
def run_tests(module_name, lang):
106119
title = "Test %s (%s)" % (module_name, lang)
107120
display_title(title)
108121

109-
testmod = import_tests(module_name)
122+
try:
123+
testmod = import_tests(module_name)
124+
except ImportError:
125+
# The C extension must always be available
126+
if lang == "C":
127+
raise
128+
129+
if VERBOSE:
130+
print("%s: skip %s, missing %s extension"
131+
% (python_version(), lang, module_name))
132+
print()
133+
return
134+
135+
if VERBOSE and hasattr(testmod, "__cplusplus"):
136+
print("__cplusplus: %s" % testmod.__cplusplus)
110137

111138
check_refleak = hasattr(sys, 'gettotalrefcount')
112139

@@ -125,21 +152,8 @@ def test_func():
125152
if VERBOSE:
126153
print()
127154

128-
ver = sys.version_info
129-
build = 'debug' if hasattr(sys, 'gettotalrefcount') else 'release'
130155
msg = "%s %s tests succeeded!" % (len(tests), lang)
131-
if hasattr(sys, 'implementation'):
132-
python_impl = sys.implementation.name
133-
if python_impl == 'cpython':
134-
python_impl = 'CPython'
135-
elif python_impl == 'pypy':
136-
python_impl = 'PyPy'
137-
else:
138-
if "PyPy" in sys.version:
139-
python_impl = "PyPy"
140-
else:
141-
python_impl = 'Python'
142-
msg = "%s %s.%s (%s build): %s" % (python_impl, ver.major, ver.minor, build, msg)
156+
msg = "%s: %s" % (python_version(), msg)
143157
if check_refleak:
144158
msg = "%s (no reference leak detected)" % msg
145159
print(msg)
@@ -150,9 +164,8 @@ def main():
150164
VERBOSE = ("-v" in sys.argv[1:] or "--verbose" in sys.argv[1:])
151165

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

167180
build_ext()
168181

169-
run_tests("test_pythoncapi_compat_cext")
170-
if TEST_CPP:
171-
run_tests("test_pythoncapi_compat_cppext")
182+
for module_name, lang in TESTS:
183+
run_tests(module_name, lang)
172184

173185

174186
if __name__ == "__main__":

0 commit comments

Comments
 (0)