diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 61b2b8c..be06278 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -17,8 +17,6 @@ build-wheel: tags: - docker image: python:3.8 - variables: - LIBQI_REPOSITORY_URL: "https://gitlab-ci-token:$CI_JOB_TOKEN@$CI_SERVER_HOST/qi/libqi" script: - curl -sSL https://get.docker.com/ | sh - pip install cibuildwheel==2.14.1 diff --git a/CMakeLists.txt b/CMakeLists.txt index db1acd8..c4dd4a0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -297,7 +297,7 @@ if(BUILD_TESTING) include(GoogleTest) find_package(qimodule REQUIRED) - qi_create_module(moduletest NO_INSTALL) + qi_add_module(moduletest) target_sources( moduletest PRIVATE diff --git a/ci/cibuildwheel_linux_before_all.sh b/ci/cibuildwheel_linux_before_all.sh index f033f37..faf4ebd 100755 --- a/ci/cibuildwheel_linux_before_all.sh +++ b/ci/cibuildwheel_linux_before_all.sh @@ -6,20 +6,19 @@ PACKAGE=$1 pip install 'conan>=2' 'cmake>=3.23' ninja # Perl dependencies required to build OpenSSL. -yum install -y perl-IPC-Cmd perl-Digest-SHA +yum install -y perl-IPC-Cmd perl-Digest-SHA perl-Time-Piece # Install Conan configuration. +conan profile detect conan config install "$PACKAGE/ci/conan" # Clone and export libqi to Conan cache. -QI_VERSION=$(sed -nE '/^\s*requires\s*=/,/^\s*]/{ s/\s*"qi\/([^"]+)".*/\1/p }' "$PACKAGE/conanfile.py") - GIT_SSL_NO_VERIFY=true \ - git clone --depth=1 \ - --branch "qi-framework-v${QI_VERSION}" \ - "$LIBQI_REPOSITORY_URL" \ + git clone \ + --branch master \ + https://github.com/aldebaran/libqi.git \ /work/libqi -conan export /work/libqi --version="${QI_VERSION}" +conan export /work/libqi # Install dependencies of libqi-python from Conan, including libqi. # @@ -27,4 +26,8 @@ conan export /work/libqi --version="${QI_VERSION}" # This is because the GLIBC from the manylinux images are often older than the # ones that were used to build the precompiled binaries, which means the binaries # cannot by executed. -conan install "$PACKAGE" --build="*" +# +# Use a fixed output folder so that the generated toolchain file path is +# predictable and can be referenced by scikit-build-core. +conan install "$PACKAGE" --build="*" --profile:all default --profile:all cppstd17 \ + --output-folder=/conan-generators diff --git a/ci/conan/global.conf b/ci/conan/global.conf index 7ad81c5..f90cae0 100644 --- a/ci/conan/global.conf +++ b/ci/conan/global.conf @@ -1,5 +1,3 @@ -core:default_profile=default -core:default_build_profile=default tools.build:skip_test=true tools.cmake.cmaketoolchain:generator=Ninja # Only use the build_type as a variable for the build folder name, so diff --git a/ci/conan/profiles/cppstd17 b/ci/conan/profiles/cppstd17 new file mode 100644 index 0000000..5b1d062 --- /dev/null +++ b/ci/conan/profiles/cppstd17 @@ -0,0 +1,2 @@ +[settings] +compiler.cppstd=gnu17 diff --git a/ci/conan/profiles/default b/ci/conan/profiles/default deleted file mode 100644 index 4ca0f95..0000000 --- a/ci/conan/profiles/default +++ /dev/null @@ -1,8 +0,0 @@ -[settings] -arch=x86_64 -build_type=Release -compiler=gcc -compiler.cppstd=gnu17 -compiler.libcxx=libstdc++ -compiler.version=10 -os=Linux diff --git a/conanfile.py b/conanfile.py index 9e16c4c..d2a74aa 100644 --- a/conanfile.py +++ b/conanfile.py @@ -52,11 +52,12 @@ "system", ] + class QiPythonConan(ConanFile): requires = [ - "boost/[~1.78]", - "pybind11/[^2.9]", - "qi/4.0.2", + "boost/[~1.83]", + "pybind11/[^2.11]", + "qi/[~4]", ] test_requires = [ @@ -77,9 +78,9 @@ class QiPythonConan(ConanFile): # Disable every components of Boost unless we actively use them. default_options.update( { - f"boost/*:without_{_name}": False - if _name in USED_BOOST_COMPONENTS - else True + f"boost/*:without_{_name}": ( + False if _name in USED_BOOST_COMPONENTS else True + ) for _name in BOOST_COMPONENTS } ) diff --git a/pyproject.toml b/pyproject.toml index 794715e..52500f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ build-backend = "scikit_build_core.build" [project] name = "qi" description = "LibQi Python bindings" -version = "3.1.4" +version = "3.1.5" readme = "README.rst" requires-python = ">=3.7" license = { "file" = "COPYING" } @@ -37,6 +37,8 @@ classifiers=[ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3 :: Only", ] maintainers = [ @@ -60,11 +62,8 @@ BUILD_TESTING = "OFF" build = "cp*manylinux*x86_64" build-frontend = "build" -environment-pass = ["LIBQI_REPOSITORY_URL"] - [tool.cibuildwheel.linux] -manylinux-x86_64-image = "manylinux2014" before-all = ["ci/cibuildwheel_linux_before_all.sh {package}"] [tool.cibuildwheel.linux.config-settings] -"cmake.args" = ["--preset=conan-release"] +"cmake.args" = ["-DCMAKE_TOOLCHAIN_FILE=/conan-generators/build/release/generators/conan_toolchain.cmake", "-DCMAKE_BUILD_TYPE=Release", "-DCMAKE_PREFIX_PATH=/conan-generators/build/release/generators"] diff --git a/qipython/common.hpp b/qipython/common.hpp index 3335f66..ee39c41 100644 --- a/qipython/common.hpp +++ b/qipython/common.hpp @@ -116,8 +116,13 @@ boost::optional extractKeywordArg(pybind11::dict kwargs, /// finalizing or not. inline boost::optional interpreterIsFinalizing() { -// `_Py_IsFinalizing` is only available on CPython 3.7+ -#if PY_VERSION_HEX >= 0x03070000 +// `_Py_IsFinalizing` was a private CPython API available since 3.7. +// It was removed in Python 3.13 (replaced by the public `Py_IsFinalizing`). +// `Py_IsFinalizing` is the public API, available since CPython 3.9, +// but only guaranteed visible outside of Py_LIMITED_API mode from 3.13. +#if PY_VERSION_HEX >= 0x030d0000 + return boost::make_optional(Py_IsFinalizing() != 0); +#elif PY_VERSION_HEX >= 0x03070000 return boost::make_optional(_Py_IsFinalizing() != 0); #else // There is no way of knowing on older versions. diff --git a/src/pytypes.cpp b/src/pytypes.cpp index fa03102..41795d3 100644 --- a/src/pytypes.cpp +++ b/src/pytypes.cpp @@ -595,20 +595,13 @@ class ListInterface : public ObjectInterfaceBase using DefaultImpl = DefaultTypeImplMethods>; - void* initializeStorage(void* ptr = nullptr) override - { - return DefaultImpl::initializeStorage(ptr); - } - + void* initializeStorage(void* ptr = nullptr) override { return DefaultImpl::initializeStorage(ptr); } void* clone(void* storage) override { return DefaultImpl::clone(storage); } - void destroy(void* storage) override - { - destroyDisownedReferences(storage); - return DefaultImpl::destroy(storage); - } + void destroy(void* storage) override { return DefaultImpl::destroy(storage); } const TypeInfo& info() override { return DefaultImpl::info(); } void* ptrFromStorage(void** s) override { return DefaultImpl::ptrFromStorage(s); } bool less(void* a, void* b) override { return DefaultImpl::less(a, b); } + Iterator* asIterPtr(void** storage) { return static_cast(ptrFromStorage(storage)); } Iterator& asIter(void** storage) { return *asIterPtr(storage); } }; @@ -680,12 +673,13 @@ class DictInterface: public ObjectInterfaceBase std::advance(it, index); const auto key = ::py::reinterpret_borrow<::py::object>(it->first); const auto element = ::py::reinterpret_borrow<::py::object>(it->second); - const auto keyElementPair = std::make_pair(key, element); - auto ref = AnyReference::from(keyElementPair).clone(); + auto keyRef = AnyReference::from(key); + auto elementRef = AnyReference::from(element); + auto pairRef = makeGenericTuple({keyRef, elementRef}); // Store the disowned reference with the list as a context instead of the // iterator because the reference might outlive the iterator. - storeDisownedReference(dictStorage, ref); - return ref; + storeDisownedReference(dictStorage, pairRef); + return pairRef; } void next(void** storage) override { ++asIter(storage).second; } @@ -693,21 +687,13 @@ class DictInterface: public ObjectInterfaceBase using DefaultImpl = DefaultTypeImplMethods>; - void* initializeStorage(void* ptr = nullptr) override - { - return DefaultImpl::initializeStorage(ptr); - } - + void* initializeStorage(void* ptr = nullptr) override { return DefaultImpl::initializeStorage(ptr); } void* clone(void* storage) override { return DefaultImpl::clone(storage); } - void destroy(void* storage) override - { - destroyDisownedReferences(storage); - return DefaultImpl::destroy(storage); - } - + void destroy(void* storage) override { return DefaultImpl::destroy(storage); } const TypeInfo& info() override { return DefaultImpl::info(); } void* ptrFromStorage(void** s) override { return DefaultImpl::ptrFromStorage(s); } bool less(void* a, void* b) override { return DefaultImpl::less(a, b); } + Iterator* asIterPtr(void** storage) { return static_cast(ptrFromStorage(storage)); } Iterator& asIter(void** storage) { return *asIterPtr(storage); } }; diff --git a/tests/test_types.cpp b/tests/test_types.cpp index 5eae486..a3712e1 100644 --- a/tests/test_types.cpp +++ b/tests/test_types.cpp @@ -387,20 +387,40 @@ TEST_F(TypePassing, Recursive) } } +TEST_F(TypePassing, ReverseList) +{ + exec( + "class TestService:\n" + " def func(self, list):\n" + // Test the iterator interface. + " for value in list:\n" + " assert(isinstance(value, str))\n" + " assert(list == ['hello', 'world'])\n" + ); + registerService(); + const std::vector list {"hello", "world"}; + getService().call("func", list); +} + + TEST_F(TypePassing, ReverseDict) { exec( "class TestService:\n" " def func(self, dict):\n" - " return dict == {'one' : 1, 'two' : 2, 'three' : 3}\n" + // Test the iterator interface. + " for key, value in dict.items():\n" + " assert(isinstance(key, str))\n" + " assert(isinstance(value, int))\n" + " assert(dict == {'one' : 1, 'two' : 2, 'three' : 3})\n" ); registerService(); - const std::map expected { + const std::map dict { {"one", 1}, {"two", 2}, {"three", 3}, }; - EXPECT_TRUE(getService().call("func", expected)); + getService().call("func", dict); } TEST_F(TypePassing, LogLevel)