Skip to content

Commit bc3874e

Browse files
committed
PYCBC-1563: Add Support for Python 3.12
Motivation ========== Add support for Python 3.12. Modification ============ * Update bindings to fix build errors with Python 3.12. * Add configure_ext Command to setup.py. Results ======= All tests pass. Change-Id: I267ded9b255b9c61e800ce7b7eb676fb04502276 Reviewed-on: https://review.couchbase.org/c/couchbase-python-client/+/206598 Tested-by: Build Bot <build@couchbase.com> Reviewed-by: Dimitris Christodoulou <dimitris.christodoulou@couchbase.com>
1 parent b25dff2 commit bc3874e

27 files changed

+732
-901
lines changed

conftest.py

+4
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
"acouchbase/tests/bucketmgmt_t.py::BucketManagementTests",
7979
"acouchbase/tests/collectionmgmt_t.py::CollectionManagementTests",
8080
"acouchbase/tests/eventingmgmt_t.py::EventingManagementTests",
81+
"acouchbase/tests/eventingmgmt_t.py::ScopeEventingManagementTests",
8182
"acouchbase/tests/querymgmt_t.py::QueryIndexManagementTests",
8283
"acouchbase/tests/querymgmt_t.py::QueryIndexCollectionManagementTests",
8384
"acouchbase/tests/searchmgmt_t.py::SearchIndexManagementTests",
@@ -88,6 +89,7 @@
8889
"couchbase/tests/bucketmgmt_t.py::ClassicBucketManagementTests",
8990
"couchbase/tests/collectionmgmt_t.py::ClassicCollectionManagementTests",
9091
"couchbase/tests/eventingmgmt_t.py::ClassicEventingManagementTests",
92+
"couchbase/tests/eventingmgmt_t.py::ClassicScopeEventingManagementTests",
9193
"couchbase/tests/querymgmt_t.py::ClassicQueryIndexManagementTests",
9294
"couchbase/tests/querymgmt_t.py::ClassicQueryIndexCollectionManagementTests",
9395
"couchbase/tests/searchmgmt_t.py::ClassicSearchIndexManagementTests",
@@ -97,7 +99,9 @@
9799

98100
_SLOW_MGMT_TESTS = [
99101
"acouchbase/tests/eventingmgmt_t.py::EventingManagementTests",
102+
"acouchbase/tests/eventingmgmt_t.py::ScopeEventingManagementTests",
100103
"couchbase/tests/eventingmgmt_t.py::ClassicEventingManagementTests",
104+
"couchbase/tests/eventingmgmt_t.py::ClassicScopeEventingManagementTests",
101105
]
102106

103107
_MISC_TESTS = [

couchbase/logic/analytics.py

+3
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ def __init__(self, raw # type: Dict[str, Any]
123123
) -> None:
124124
if raw is not None:
125125
self._raw = raw.get('metadata', None)
126+
sig = self._raw.get('signature', None)
127+
if sig is not None:
128+
self._raw['signature'] = json.loads(sig)
126129
else:
127130
self._raw = None
128131

couchbase/logic/n1ql.py

+7
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from __future__ import annotations
1717

18+
import json
1819
from datetime import timedelta
1920
from enum import Enum
2021
from typing import (TYPE_CHECKING,
@@ -225,6 +226,12 @@ def __init__(self, raw # type: Dict[str, Any]
225226
) -> None:
226227
if raw is not None:
227228
self._raw = raw.get('metadata', None)
229+
sig = self._raw.get('signature', None)
230+
if sig is not None:
231+
self._raw['signature'] = json.loads(sig)
232+
prof = self._raw.get('profile', None)
233+
if prof is not None:
234+
self._raw['profile'] = json.loads(prof)
228235
else:
229236
self._raw = None
230237

couchbase/result.py

+2-10
Original file line numberDiff line numberDiff line change
@@ -32,25 +32,17 @@
3232
EndpointPingReport,
3333
EndpointState,
3434
ServiceType)
35-
from couchbase.exceptions import (CLIENT_ERROR_MAP,
36-
CouchbaseException,
37-
ErrorMapper,
38-
InvalidArgumentException)
35+
from couchbase.exceptions import ErrorMapper, InvalidArgumentException
3936
from couchbase.exceptions import exception as CouchbaseBaseException
40-
from couchbase.pycbc_core import exception, result
37+
from couchbase.pycbc_core import result
4138
from couchbase.subdocument import parse_subdocument_content_as, parse_subdocument_exists
4239

4340

4441
class Result:
4542
def __init__(
4643
self,
4744
orig, # type: result
48-
should_raise=True, # type: bool
4945
):
50-
if should_raise and orig.err():
51-
base = exception(orig)
52-
klass = CLIENT_ERROR_MAP.get(orig.err(), CouchbaseException)
53-
raise klass(base)
5446

5547
self._orig = orig
5648

pycbc_build_setup.py

+155-84
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,25 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515

16+
from __future__ import annotations
17+
1618
import os
1719
import platform
1820
import shutil
1921
import subprocess # nosec
2022
import sys
23+
from dataclasses import dataclass, field
2124
from sysconfig import get_config_var
25+
from typing import (Dict,
26+
List,
27+
Optional)
28+
29+
from setuptools import Command, Extension
2230

23-
from setuptools import Extension
31+
# need at least setuptools v62.3.0
2432
from setuptools.command.build import build
2533
from setuptools.command.build_ext import build_ext
34+
from setuptools.errors import OptionError, SetupError
2635

2736
CMAKE_EXE = os.environ.get('CMAKE_EXE', shutil.which('cmake'))
2837
PYCBC_ROOT = os.path.dirname(__file__)
@@ -98,13 +107,146 @@ def process_build_env_vars(): # noqa: C901
98107
os.environ['CMAKE_COMMON_VARIABLES'] = ' '.join(cmake_extra_args)
99108

100109

110+
@dataclass
111+
class CMakeConfig:
112+
build_type: str
113+
num_threads: int
114+
set_cpm_cache: bool
115+
env: Dict[str, str] = field(default_factory=dict)
116+
config_args: List[str] = field(default_factory=list)
117+
118+
@classmethod
119+
def create_cmake_config(cls, # noqa: C901
120+
output_dir: str,
121+
source_dir: str,
122+
set_cpm_cache: Optional[bool] = None
123+
) -> CMakeConfig:
124+
env = os.environ.copy()
125+
num_threads = env.pop('PYCBC_CMAKE_PARALLEL_THREADS', '4')
126+
build_type = env.pop('PYCBC_BUILD_TYPE')
127+
cmake_generator = env.pop('PYCBC_CMAKE_SET_GENERATOR', None)
128+
cmake_arch = env.pop('PYCBC_CMAKE_SET_ARCH', None)
129+
cmake_config_args = [CMAKE_EXE,
130+
source_dir,
131+
f'-DCMAKE_BUILD_TYPE={build_type}',
132+
f'-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={output_dir}',
133+
f'-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{build_type.upper()}={output_dir}']
134+
135+
cmake_config_args.extend(
136+
[x for x in
137+
os.environ.get('CMAKE_COMMON_VARIABLES', '').split(' ')
138+
if x])
139+
140+
python3_executable = env.pop('PYCBC_PYTHON3_EXECUTABLE', None)
141+
if python3_executable:
142+
cmake_config_args += [f'-DPython3_EXECUTABLE={python3_executable}']
143+
144+
python3_include = env.pop('PYCBC_PYTHON3_INCLUDE_DIR', None)
145+
if python3_include:
146+
cmake_config_args += [f'-DPython3_INCLUDE_DIR={python3_include}']
147+
148+
if set_cpm_cache is None:
149+
set_cpm_cache = env.pop('PYCBC_SET_CPM_CACHE', 'false').lower() in ENV_TRUE
150+
use_cpm_cache = env.pop('PYCBC_USE_CPM_CACHE', 'true').lower() in ENV_TRUE
151+
152+
if set_cpm_cache is True:
153+
# if we are setting the cache, we don't want to attempt a build (it will fail).
154+
use_cpm_cache = False
155+
if os.path.exists(CXXCBC_CACHE_DIR):
156+
shutil.rmtree(CXXCBC_CACHE_DIR)
157+
cmake_config_args += [f'-DCOUCHBASE_CXX_CPM_CACHE_DIR={CXXCBC_CACHE_DIR}',
158+
'-DCPM_DOWNLOAD_ALL=ON',
159+
'-DCPM_USE_NAMED_CACHE_DIRECTORIES=ON',
160+
'-DCPM_USE_LOCAL_PACKAGES=OFF']
161+
162+
if use_cpm_cache is True:
163+
if not os.path.exists(CXXCBC_CACHE_DIR):
164+
raise OptionError(f'Cannot use cached dependencies, path={CXXCBC_CACHE_DIR} does not exist.')
165+
cmake_config_args += ['-DCPM_DOWNLOAD_ALL=OFF',
166+
'-DCPM_USE_NAMED_CACHE_DIRECTORIES=ON',
167+
'-DCPM_USE_LOCAL_PACKAGES=OFF',
168+
f'-DCPM_SOURCE_CACHE={CXXCBC_CACHE_DIR}',
169+
f'-DCOUCHBASE_CXX_CLIENT_EMBED_MOZILLA_CA_BUNDLE_ROOT={CXXCBC_CACHE_DIR}"']
170+
171+
if platform.system() == "Windows":
172+
cmake_config_args += [f'-DCMAKE_RUNTIME_OUTPUT_DIRECTORY_{build_type.upper()}={output_dir}']
173+
174+
if cmake_generator:
175+
if cmake_generator.upper() == 'TRUE':
176+
cmake_config_args += ['-G', 'Visual Studio 16 2019']
177+
else:
178+
cmake_config_args += ['-G', f'{cmake_generator}']
179+
180+
if cmake_arch:
181+
if cmake_arch.upper() == 'TRUE':
182+
if sys.maxsize > 2 ** 32:
183+
cmake_config_args += ['-A', 'x64']
184+
else:
185+
cmake_config_args += ['-A', f'{cmake_arch}']
186+
# maybe??
187+
# '-DCMAKE_WINDOWS_EXPORT_ALL_SYMBOLS=TRUE',
188+
189+
return CMakeConfig(build_type,
190+
num_threads,
191+
set_cpm_cache,
192+
env,
193+
cmake_config_args)
194+
195+
101196
class CMakeExtension(Extension):
102197
def __init__(self, name, sourcedir=''):
103198
check_for_cmake()
104199
Extension.__init__(self, name, sources=[])
105200
self.sourcedir = os.path.abspath(sourcedir)
106201

107202

203+
class CMakeConfigureExt(Command):
204+
description = 'Configure Python Operational SDK C Extension'
205+
user_options = []
206+
207+
def initialize_options(self) -> None:
208+
return
209+
210+
def finalize_options(self) -> None:
211+
return
212+
213+
def run(self) -> None:
214+
check_for_cmake()
215+
process_build_env_vars()
216+
build_ext = self.get_finalized_command('build_ext')
217+
if len(self.distribution.ext_modules) != 1:
218+
raise SetupError('Should have only the Python SDK extension module.')
219+
ext = self.distribution.ext_modules[0]
220+
output_dir = os.path.abspath(os.path.dirname(build_ext.get_ext_fullpath(ext.name)))
221+
set_cpm_cache = os.environ.get('PYCBC_SET_CPM_CACHE', 'true').lower() in ENV_TRUE
222+
cmake_config = CMakeConfig.create_cmake_config(output_dir, ext.sourcedir, set_cpm_cache=set_cpm_cache)
223+
if not os.path.exists(build_ext.build_temp):
224+
os.makedirs(build_ext.build_temp)
225+
print(f'cmake config args: {cmake_config.config_args}')
226+
# configure (i.e. cmake ..)
227+
subprocess.check_call(cmake_config.config_args, # nosec
228+
cwd=build_ext.build_temp,
229+
env=cmake_config.env)
230+
231+
self._clean_cache_cpm_dependencies()
232+
233+
def _clean_cache_cpm_dependencies(self):
234+
import re
235+
from fileinput import FileInput
236+
from pathlib import Path
237+
238+
cxx_cache_path = Path(CXXCBC_CACHE_DIR)
239+
cmake_cpm = next((p for p in cxx_cache_path.glob('cpm/*') if f'{p}'.endswith('.cmake')), None)
240+
if cmake_cpm is not None:
241+
with FileInput(files=[cmake_cpm], inplace=True) as cpm_cmake:
242+
for line in cpm_cmake:
243+
# used so that we don't have a dependency on git w/in environment
244+
if 'find_package(Git REQUIRED)' in line:
245+
line = re.sub(r'Git REQUIRED', 'Git', line)
246+
# remove ending whitespace to avoid double spaced output
247+
print(line.rstrip())
248+
249+
108250
class CMakeBuildExt(build_ext):
109251

110252
def get_ext_filename(self, ext_name):
@@ -117,100 +259,29 @@ def build_extension(self, ext): # noqa: C901
117259
check_for_cmake()
118260
process_build_env_vars()
119261
if isinstance(ext, CMakeExtension):
120-
env = os.environ.copy()
121-
output_dir = os.path.abspath(
122-
os.path.dirname(self.get_ext_fullpath(ext.name)))
123-
124-
num_threads = env.pop('PYCBC_CMAKE_PARALLEL_THREADS', '4')
125-
build_type = env.pop('PYCBC_BUILD_TYPE')
126-
cmake_generator = env.pop('PYCBC_CMAKE_SET_GENERATOR', None)
127-
cmake_arch = env.pop('PYCBC_CMAKE_SET_ARCH', None)
128-
129-
cmake_config_args = [CMAKE_EXE,
130-
ext.sourcedir,
131-
f'-DCMAKE_BUILD_TYPE={build_type}',
132-
f'-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={output_dir}',
133-
f'-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{build_type.upper()}={output_dir}']
134-
135-
cmake_config_args.extend(
136-
[x for x in
137-
os.environ.get('CMAKE_COMMON_VARIABLES', '').split(' ')
138-
if x])
139-
140-
python3_executable = env.pop('PYCBC_PYTHON3_EXECUTABLE', None)
141-
if python3_executable:
142-
cmake_config_args += [f'-DPython3_EXECUTABLE={python3_executable}']
143-
144-
python3_include = env.pop('PYCBC_PYTHON3_INCLUDE_DIR', None)
145-
if python3_include:
146-
cmake_config_args += [f'-DPython3_INCLUDE_DIR={python3_include}']
147-
148-
set_cpm_cache = env.pop('PYCBC_SET_CPM_CACHE', 'false').lower() in ENV_TRUE
149-
use_cpm_cache = env.pop('PYCBC_USE_CPM_CACHE', 'true').lower() in ENV_TRUE
150-
151-
if set_cpm_cache is True:
152-
# if we are setting the cache, we don't want to attempt a build (it will fail).
153-
use_cpm_cache = False
154-
if os.path.exists(CXXCBC_CACHE_DIR):
155-
shutil.rmtree(CXXCBC_CACHE_DIR)
156-
cmake_config_args += [f'-DCOUCHBASE_CXX_CPM_CACHE_DIR={CXXCBC_CACHE_DIR}',
157-
'-DCPM_DOWNLOAD_ALL=ON',
158-
'-DCPM_USE_NAMED_CACHE_DIRECTORIES=ON',
159-
'-DCPM_USE_LOCAL_PACKAGES=OFF']
160-
161-
if use_cpm_cache is True:
162-
if not os.path.exists(CXXCBC_CACHE_DIR):
163-
raise Exception(f'Cannot use cached dependencies, path={CXXCBC_CACHE_DIR} does not exist.')
164-
cmake_config_args += ['-DCPM_DOWNLOAD_ALL=OFF',
165-
'-DCPM_USE_NAMED_CACHE_DIRECTORIES=ON',
166-
'-DCPM_USE_LOCAL_PACKAGES=OFF',
167-
f'-DCPM_SOURCE_CACHE={CXXCBC_CACHE_DIR}',
168-
f'-DCOUCHBASE_CXX_CLIENT_EMBED_MOZILLA_CA_BUNDLE_ROOT={CXXCBC_CACHE_DIR}"']
169-
170-
if platform.system() == "Windows":
171-
cmake_config_args += [f'-DCMAKE_RUNTIME_OUTPUT_DIRECTORY_{build_type.upper()}={output_dir}']
172-
173-
if cmake_generator:
174-
if cmake_generator.upper() == 'TRUE':
175-
cmake_config_args += ['-G', 'Visual Studio 16 2019']
176-
else:
177-
cmake_config_args += ['-G', f'{cmake_generator}']
178-
179-
if cmake_arch:
180-
if cmake_arch.upper() == 'TRUE':
181-
if sys.maxsize > 2 ** 32:
182-
cmake_config_args += ['-A', 'x64']
183-
else:
184-
cmake_config_args += ['-A', f'{cmake_arch}']
185-
# maybe??
186-
# '-DCMAKE_WINDOWS_EXPORT_ALL_SYMBOLS=TRUE',
262+
output_dir = os.path.abspath(os.path.dirname(self.get_ext_fullpath(ext.name)))
263+
cmake_config = CMakeConfig.create_cmake_config(output_dir, ext.sourcedir)
187264

188265
cmake_build_args = [CMAKE_EXE,
189266
'--build',
190267
'.',
191268
'--config',
192-
f'{build_type}',
269+
f'{cmake_config.build_type}',
193270
'--parallel',
194-
f'{num_threads}']
271+
f'{cmake_config.num_threads}']
195272

196273
if not os.path.exists(self.build_temp):
197274
os.makedirs(self.build_temp)
198-
print(f'cmake config args: {cmake_config_args}')
275+
print(f'cmake config args: {cmake_config.config_args}')
199276
# configure (i.e. cmake ..)
200-
subprocess.check_call(cmake_config_args, # nosec
277+
subprocess.check_call(cmake_config.config_args, # nosec
278+
cwd=self.build_temp,
279+
env=cmake_config.env)
280+
print(f'cmake build args: {cmake_build_args}')
281+
# build (i.e. cmake --build .)
282+
subprocess.check_call(cmake_build_args, # nosec
201283
cwd=self.build_temp,
202-
env=env)
203-
204-
if set_cpm_cache is True:
205-
self._clean_cache_cpm_dependencies()
206-
# NOTE: since we are not building, this will create an error. Okay, as attempting the
207-
# build will fail anyway and we really just want to update the CXX cache.
208-
else:
209-
print(f'cmake build args: {cmake_build_args}')
210-
# build (i.e. cmake --build .)
211-
subprocess.check_call(cmake_build_args, # nosec
212-
cwd=self.build_temp,
213-
env=env)
284+
env=cmake_config.env)
214285

215286
else:
216287
super().build_extension(ext)

setup.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import couchbase_version # nopep8 # isort:skip # noqa: E402
2424
from pycbc_build_setup import (BuildCommand, # nopep8 # isort:skip # noqa: E402
2525
CMakeBuildExt,
26+
CMakeConfigureExt,
2627
CMakeExtension)
2728

2829
try:
@@ -44,7 +45,9 @@
4445
setup(name='couchbase',
4546
version=PYCBC_VERSION,
4647
ext_modules=[CMakeExtension('couchbase.pycbc_core')],
47-
cmdclass={'build': BuildCommand, 'build_ext': CMakeBuildExt},
48+
cmdclass={'build': BuildCommand,
49+
'build_ext': CMakeBuildExt,
50+
'configure_ext': CMakeConfigureExt},
4851
python_requires='>=3.7',
4952
packages=find_packages(
5053
include=['acouchbase', 'couchbase', 'txcouchbase', 'couchbase.*', 'acouchbase.*', 'txcouchbase.*'],

0 commit comments

Comments
 (0)