├── .gitignore ├── .gitlab-ci.yml ├── CMakeLists.txt ├── COPYING ├── MANIFEST.in ├── README.rst ├── ci ├── cibuildwheel_linux_before_all.sh └── conan │ ├── global.conf │ └── profiles │ └── cppstd17 ├── cmake ├── BuildType.cmake └── ParsePythonVersion.cmake ├── conanfile.py ├── doc └── .gitignore ├── examples ├── applicationsession.py ├── authentication_with_application.py ├── qiclient.py ├── qinm.py └── qiservice.py ├── pyproject.toml ├── qi ├── __init__.py ├── _binder.py ├── _type.py ├── _version.py.in ├── logging.py ├── native.py.in ├── path.py ├── test │ ├── __init__.py │ ├── conftest.py │ ├── test_applicationsession.py │ ├── test_async.py │ ├── test_call.py │ ├── test_log.py │ ├── test_module.py │ ├── test_promise.py │ ├── test_property.py │ ├── test_return_empty_object.py │ ├── test_session.py │ ├── test_signal.py │ ├── test_strand.py │ └── test_typespassing.py └── translator.py ├── qipython ├── common.hpp ├── common.hxx ├── pyapplication.hpp ├── pyasync.hpp ├── pyclock.hpp ├── pyexport.hpp ├── pyfuture.hpp ├── pyguard.hpp ├── pylog.hpp ├── pymodule.hpp ├── pyobject.hpp ├── pypath.hpp ├── pyproperty.hpp ├── pysession.hpp ├── pysignal.hpp ├── pystrand.hpp ├── pytranslator.hpp └── pytypes.hpp ├── src ├── module.cpp ├── pyapplication.cpp ├── pyasync.cpp ├── pyclock.cpp ├── pyexport.cpp ├── pyfuture.cpp ├── pylog.cpp ├── pymodule.cpp ├── pyobject.cpp ├── pypath.cpp ├── pyproperty.cpp ├── pysession.cpp ├── pysignal.cpp ├── pystrand.cpp ├── pytranslator.cpp ├── pytypes.cpp └── qimodule_python_plugin.cpp └── tests ├── common.hpp ├── moduletest.cpp ├── service_object_holder.cpp ├── test_guard.cpp ├── test_module.cpp ├── test_object.cpp ├── test_property.cpp ├── test_qipython.cpp ├── test_qipython_local_interpreter.cpp ├── test_signal.cpp └── test_types.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.user 3 | .pytest_cache 4 | .cache 5 | build* 6 | _skbuild 7 | dist 8 | *.egg-info 9 | CMakeUserPresets.json 10 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - build 3 | - publish 4 | - release 5 | 6 | .if-tag: &if-tag 7 | if: "$CI_COMMIT_TAG =~ /^qi-python-v/" 8 | 9 | build-wheel: 10 | stage: build 11 | rules: 12 | # Allow manually building on a Git commit on any branch. 13 | - if: $CI_COMMIT_BRANCH 14 | when: manual 15 | # Always build on a Git tag. 16 | - <<: *if-tag 17 | tags: 18 | - docker 19 | image: python:3.8 20 | script: 21 | - curl -sSL https://get.docker.com/ | sh 22 | - pip install cibuildwheel==2.14.1 23 | - cibuildwheel --output-dir wheelhouse 24 | artifacts: 25 | paths: 26 | - wheelhouse/ 27 | 28 | publish-wheel: 29 | stage: publish 30 | image: python:latest 31 | rules: 32 | - <<: *if-tag 33 | needs: 34 | - build-wheel 35 | script: 36 | - pip install build twine 37 | - python -m build 38 | - TWINE_PASSWORD="${CI_JOB_TOKEN}" 39 | TWINE_USERNAME=gitlab-ci-token 40 | python -m twine upload 41 | --repository-url "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi" 42 | wheelhouse/* 43 | 44 | create-release: 45 | stage: release 46 | rules: 47 | - <<: *if-tag 48 | script: 49 | - echo "Releasing $CI_COMMIT_TAG." 50 | release: 51 | tag_name: $CI_COMMIT_TAG 52 | name: 'lib$CI_COMMIT_TITLE' 53 | description: '$CI_COMMIT_TAG_MESSAGE' 54 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2020, Softbank Robotics Europe 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the Aldebaran Robotics nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL Aldebaran Robotics BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include COPYING 3 | include CMakeLists.txt 4 | graft cmake 5 | graft qi 6 | graft examples 7 | graft qipython 8 | graft src 9 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | LibQiPython - LibQi Python bindings 3 | =================================== 4 | 5 | This repository contains the official Python bindings of the `LibQi`__, the ``qi`` 6 | Python module. 7 | 8 | __ LibQi_repo_ 9 | 10 | Building 11 | ======== 12 | 13 | To build the project, you need: 14 | 15 | - a compiler that supports C++17. 16 | 17 | - on Ubuntu: `apt-get install build-essential`. 18 | 19 | - CMake with at least version 3.23. 20 | 21 | - on PyPI (**recommended**): `pip install "cmake>=3.23"`. 22 | - on Ubuntu: `apt-get install cmake`. 23 | 24 | - Python with at least version 3.7 and its development libraries. 25 | 26 | - On Ubuntu: `apt-get install libpython3-dev`. 27 | 28 | - a Python `virtualenv`. 29 | 30 | - On Ubuntu: 31 | 32 | .. code-block:: console 33 | 34 | apt-get install python3-venv 35 | python3 -m venv ~/my-venv # Use the path of your convenience. 36 | source ~/my-venv/bin/activate 37 | 38 | .. note:: 39 | The CMake project offers several configuration options and exports a set 40 | of targets when installed. You may refer to the ``CMakeLists.txt`` file 41 | for more details about available parameters and exported targets. 42 | 43 | .. note:: 44 | The procedures described below assume that you have downloaded the project 45 | sources, that your current working directory is the project sources root 46 | directory and that you are working inside your virtualenv. 47 | 48 | Conan 49 | ^^^^^ 50 | 51 | Additionally, `libqi-python` is available as a Conan 2 project, which means you 52 | can use Conan to fetch dependencies. 53 | 54 | You can install and/or upgrade Conan 2 and create a default profile in the 55 | following way: 56 | 57 | .. code-block:: console 58 | 59 | # install/upgrade Conan 2 60 | pip install --upgrade conan~=2 61 | # create a default profile 62 | conan profile detect 63 | 64 | Install dependencies from Conan and build with CMake 65 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 66 | 67 | The procedure to build the project using Conan to fetch dependencies is the 68 | following. 69 | 70 | Most dependencies are available on Conan Center repository and should not 71 | require any additional steps to make them available. However, you might need to 72 | first get and export the `libqi` recipe into your local Conan cache. 73 | 74 | .. code-block:: console 75 | 76 | # GitHub is available, but you can also use internal GitLab. 77 | QI_REPOSITORY="https://github.com/aldebaran/libqi.git" 78 | QI_VERSION="4.0.1" # Checkout the version your project need. 79 | QI_PATH="$HOME/libqi" # Or whatever path you want. 80 | git clone \ 81 | --depth=1 `# Only fetch one commit.` \ 82 | --branch "qi-framework-v${QI_VERSION}" \ 83 | "${QI_REPOSITORY}" \ 84 | "${QI_PATH}" 85 | conan export "${QI_PATH}" \ 86 | --version "${QI_VERSION}" # Technically not required but some 87 | # versions of libqi require it 88 | # because of a bug. 89 | 90 | You can then install the `libqi-python` dependencies in Conan. 91 | 92 | .. code-block:: console 93 | 94 | conan install . \ 95 | --build=missing `# Build dependencies binaries that are missing in Conan.` \ 96 | -s build_type=Debug `# Build in debug mode.` \ 97 | -c tools.build:skip_test=true `# Skip tests building for dependencies.` \ 98 | -c '&:tools.build:skip_test=false' # Do not skip tests for the project. 99 | 100 | This will generate a build directory containing a configuration with a 101 | toolchain file that allows CMake to find dependencies inside the Conan cache. 102 | 103 | You can then invoke CMake directly inside the build configuration directory to 104 | configure and build the project. Fortunately, Conan also generates a CMake 105 | preset that simplifies the process. The name of the preset may differ on 106 | your machine. You may need to find the preset generated by Conan first by 107 | calling: 108 | 109 | .. code-block:: console 110 | 111 | cmake --list-presets 112 | 113 | Here, we'll assume that the preset is named `conan-linux-x86_64-gcc-debug`. 114 | To start building, you need to configure with CMake and then build: 115 | 116 | .. code-block:: console 117 | 118 | cmake --preset conan-linux-x86_64-gcc-debug 119 | cmake --build --preset conan-linux-x86_64-gcc-debug 120 | 121 | Tests can now be invoked using CTest_, but they require a runtime environment 122 | from Conan so that all dependencies are found: 123 | 124 | .. code-block:: console 125 | 126 | source build/linux-x86_64-gcc-debug/generators/conanrun.sh 127 | ctest --preset conan-linux-x86_64-gcc-debug --output-on-failure 128 | source build/linux-x86_64-gcc-debug/generators/deactivate_conanrun.sh 129 | 130 | Finally, you can install the project in the directory of your choice. 131 | 132 | The project defines a single install component, the ``Module`` component. 133 | 134 | .. code-block:: console 135 | 136 | # `cmake --install` does not support presets sadly. 137 | cmake \ 138 | --install build/linux-x86_64-gcc-debug \ 139 | --component Module --prefix ~/my-libqi-python-install 140 | 141 | Wheel (PEP 517) 142 | --------------- 143 | 144 | You may build this project as a wheel package using PEP 517. 145 | 146 | It uses a scikit-build_ backend which interfaces with CMake. 147 | 148 | You may need to provide a toolchain file so that CMake finds the required 149 | dependencies, such as a toolchain generated by Conan: 150 | 151 | .. code-block:: console 152 | 153 | conan install . \ 154 | --build=missing `# Build dependencies binaries that are missing in Conan.` \ 155 | -c tools.build:skip_test=true # Skip any test. 156 | 157 | You now can use the ``build`` Python module to build the wheel using PEP 517. 158 | 159 | .. code-block:: console 160 | 161 | pip install -U build 162 | python -m build \ 163 | --config-setting cmake.define.CMAKE_TOOLCHAIN_FILE=$PWD/build/linux-x86_64-gcc-release/generators/conan_toolchain.cmake 164 | 165 | When built that way, the native libraries present in the wheel are most likely incomplete. 166 | You will need to use ``auditwheel`` or ``delocate`` to fix it. 167 | 168 | .. note:: 169 | `auditwheel` requires the `patchelf` utility program on Linux. You may need 170 | to install it (on Ubuntu: `apt-get install patchelf`). 171 | 172 | .. code-block:: console 173 | 174 | pip install -U auditwheel # or `delocate` on MacOS. 175 | auditwheel repair \ 176 | --strip `# Strip debugging symbols to get a lighter archive.` \ 177 | `# The desired platform, which may differ depending on your build host.` \ 178 | `# With Ubuntu 20.04, we can target manylinux_2_31. Newer versions of` \ 179 | `# Ubuntu will have to target newer versions of manylinux.` \ 180 | `# If you don't need a manylinux archive, you can also target the` \ 181 | `# 'linux_x86_64' platform.` \ 182 | --plat manylinux_2_31_x86_64 \ 183 | `# Path to the wheel archive.` \ 184 | dist/qi-*.whl 185 | # The wheel will be by default placed in a `./wheelhouse/` directory. 186 | 187 | Crosscompiling 188 | -------------- 189 | 190 | The project supports cross-compiling as explained in the `CMake manual about 191 | toolchains`__. You may simply set the ``CMAKE_TOOLCHAIN_FILE`` variable to the 192 | path of the CMake file of your toolchain. 193 | 194 | __ CMake_toolchains_ 195 | 196 | .. _LibQi_repo: https://github.com/aldebaran/libqi 197 | .. _scikit-build: https://scikit-build.readthedocs.io/en/latest/ 198 | .. _CMake_toolchains: https://cmake.org/cmake/help/latest/manual/cmake-toolchains.7.html 199 | .. _CTest: https://cmake.org/cmake/help/latest/manual/ctest.1.html 200 | -------------------------------------------------------------------------------- /ci/cibuildwheel_linux_before_all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -x -e 3 | 4 | PACKAGE=$1 5 | 6 | pip install 'conan>=2' 'cmake>=3.23' ninja 7 | 8 | # Perl dependencies required to build OpenSSL. 9 | yum install -y perl-IPC-Cmd perl-Digest-SHA 10 | 11 | # Install Conan configuration. 12 | conan profile detect 13 | conan config install "$PACKAGE/ci/conan" 14 | 15 | # Clone and export libqi to Conan cache. 16 | GIT_SSL_NO_VERIFY=true \ 17 | git clone \ 18 | --branch master \ 19 | https://github.com/aldebaran/libqi.git \ 20 | /work/libqi 21 | conan export /work/libqi 22 | 23 | # Install dependencies of libqi-python from Conan, including libqi. 24 | # 25 | # Build everything from sources, so that we do not reuse precompiled binaries. 26 | # This is because the GLIBC from the manylinux images are often older than the 27 | # ones that were used to build the precompiled binaries, which means the binaries 28 | # cannot by executed. 29 | conan install "$PACKAGE" --build="*" --profile:all default --profile:all cppstd17 30 | -------------------------------------------------------------------------------- /ci/conan/global.conf: -------------------------------------------------------------------------------- 1 | tools.build:skip_test=true 2 | tools.cmake.cmaketoolchain:generator=Ninja 3 | # Only use the build_type as a variable for the build folder name, so 4 | # that the generated CMake preset is named "conan-release". 5 | tools.cmake.cmake_layout:build_folder_vars=["settings.build_type"] 6 | -------------------------------------------------------------------------------- /ci/conan/profiles/cppstd17: -------------------------------------------------------------------------------- 1 | [settings] 2 | compiler.cppstd=gnu17 3 | -------------------------------------------------------------------------------- /cmake/BuildType.cmake: -------------------------------------------------------------------------------- 1 | # Inspired by https://www.kitware.com/cmake-and-the-default-build-type/. 2 | # 3 | # Set a default build type if none was specified. 4 | set(default_build_type "Release") 5 | if(EXISTS "${CMAKE_SOURCE_DIR}/.git") 6 | set(default_build_type "Debug") 7 | endif() 8 | 9 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 10 | message(STATUS "Setting build type to '${default_build_type}' as none was specified.") 11 | set(CMAKE_BUILD_TYPE "${default_build_type}" CACHE 12 | STRING "Choose the type of build." FORCE) 13 | # Set the possible values of build type for cmake-gui 14 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" 15 | "MinSizeRel" "RelWithDebInfo") 16 | endif() 17 | -------------------------------------------------------------------------------- /cmake/ParsePythonVersion.cmake: -------------------------------------------------------------------------------- 1 | # Parses a Python version as defined in PEP440 2 | # (see https://www.python.org/dev/peps/pep-0440/) in some input and sets the 3 | # following variables accordingly: 4 | # 5 | # - `_VERSION_STRING`, the whole version string 6 | # - `_VERSION_EPOCH`, the epoch part of the version 7 | # - `_VERSION_RELEASE`, the release part of the version, itself 8 | # splitted into: 9 | # - `_VERSION_RELEASE_MAJOR` 10 | # - `_VERSION_RELEASE_MINOR` 11 | # - `_VERSION_RELEASE_MAJOR_MINOR` equivalent to ".". 12 | # - `_VERSION_RELEASE_PATCH` 13 | # - `_VERSION_PRERELEASE` 14 | # - `_VERSION_POSTRELEASE` 15 | # - `_VERSION_DEVRELEASE` 16 | function(parse_python_version prefix input) 17 | if(NOT input) 18 | return() 19 | endif() 20 | 21 | # Simplified regex 22 | set(_regex "^([0-9]+!)?([0-9]+)((\\.[0-9]+)*)([abrc]+[0-9]+)?(\\.(post[0-9]+))?(\\.(dev[0-9]+))?$") 23 | string(REGEX MATCH "${_regex}" _match "${input}") 24 | if(NOT _match) 25 | message(WARNING "Could not parse a Python version from '${input}'.") 26 | return() 27 | endif() 28 | 29 | set(${prefix}_VERSION_STRING "${_match}" PARENT_SCOPE) 30 | set(${prefix}_VERSION_EPOCH "${CMAKE_MATCH_1}" PARENT_SCOPE) 31 | set(_release "${CMAKE_MATCH_2}${CMAKE_MATCH_3}") 32 | set(${prefix}_VERSION_RELEASE ${_release} PARENT_SCOPE) 33 | set(${prefix}_VERSION_PRERELEASE "${CMAKE_MATCH_5}" PARENT_SCOPE) 34 | set(${prefix}_VERSION_POSTRELEASE "${CMAKE_MATCH_7}" PARENT_SCOPE) 35 | set(${prefix}_VERSION_DEVRELEASE "${CMAKE_MATCH_9}" PARENT_SCOPE) 36 | 37 | set(_release_regex "^([0-9]+)(\\.([0-9]+))?(\\.([0-9]+))?$") 38 | string(REGEX MATCH "${_release_regex}" _match "${_release}") 39 | if(NOT _match) 40 | message(WARNING "Could not parse a Python release version from '${_release}'.") 41 | return() 42 | endif() 43 | 44 | set(${prefix}_VERSION_RELEASE_MAJOR "${CMAKE_MATCH_1}" PARENT_SCOPE) 45 | set(${prefix}_VERSION_RELEASE_MINOR "${CMAKE_MATCH_3}" PARENT_SCOPE) 46 | set(${prefix}_VERSION_RELEASE_MAJOR_MINOR "${CMAKE_MATCH_1}${CMAKE_MATCH_2}" PARENT_SCOPE) 47 | set(${prefix}_VERSION_RELEASE_PATCH "${CMAKE_MATCH_5}" PARENT_SCOPE) 48 | endfunction() 49 | -------------------------------------------------------------------------------- /conanfile.py: -------------------------------------------------------------------------------- 1 | from conan import ConanFile, tools 2 | from conan.tools.cmake import cmake_layout 3 | 4 | BOOST_COMPONENTS = [ 5 | "atomic", 6 | "chrono", 7 | "container", 8 | "context", 9 | "contract", 10 | "coroutine", 11 | "date_time", 12 | "exception", 13 | "fiber", 14 | "filesystem", 15 | "graph", 16 | "graph_parallel", 17 | "iostreams", 18 | "json", 19 | "locale", 20 | "log", 21 | "math", 22 | "mpi", 23 | "nowide", 24 | "program_options", 25 | "python", 26 | "random", 27 | "regex", 28 | "serialization", 29 | "stacktrace", 30 | "system", 31 | "test", 32 | "thread", 33 | "timer", 34 | "type_erasure", 35 | "wave", 36 | ] 37 | 38 | USED_BOOST_COMPONENTS = [ 39 | "thread", 40 | # required by libqi 41 | "filesystem", 42 | "locale", 43 | "program_options", 44 | "random", 45 | "regex", 46 | # required by boost.thread 47 | "atomic", 48 | "chrono", 49 | "container", 50 | "date_time", 51 | "exception", 52 | "system", 53 | ] 54 | 55 | 56 | class QiPythonConan(ConanFile): 57 | requires = [ 58 | "boost/[~1.83]", 59 | "pybind11/[^2.11]", 60 | "qi/[~4]", 61 | ] 62 | 63 | test_requires = [ 64 | "gtest/[~1.14]", 65 | ] 66 | 67 | generators = "CMakeToolchain", "CMakeDeps" 68 | 69 | # Binary configuration 70 | settings = "os", "compiler", "build_type", "arch" 71 | 72 | default_options = { 73 | "boost/*:shared": True, 74 | "openssl/*:shared": True, 75 | "qi/*:with_boost_locale": True, # for `pytranslator.cpp` 76 | } 77 | 78 | # Disable every components of Boost unless we actively use them. 79 | default_options.update( 80 | { 81 | f"boost/*:without_{_name}": ( 82 | False if _name in USED_BOOST_COMPONENTS else True 83 | ) 84 | for _name in BOOST_COMPONENTS 85 | } 86 | ) 87 | 88 | def layout(self): 89 | # Configure the format of the build folder name, based on the value of some variables. 90 | self.folders.build_folder_vars = [ 91 | "settings.os", 92 | "settings.arch", 93 | "settings.compiler", 94 | "settings.build_type", 95 | ] 96 | 97 | # The cmake_layout() sets the folders and cpp attributes to follow the 98 | # structure of a typical CMake project. 99 | cmake_layout(self) 100 | -------------------------------------------------------------------------------- /doc/.gitignore: -------------------------------------------------------------------------------- 1 | # ignore doxygen generated files 2 | html/ 3 | doxygen* 4 | -------------------------------------------------------------------------------- /examples/applicationsession.py: -------------------------------------------------------------------------------- 1 | ## 2 | ## Copyright (C) 2012, 2013 Aldebaran Robotics 3 | ## 4 | 5 | """ Typical usage of ApplicationSession 6 | """ 7 | 8 | import sys 9 | import qi 10 | 11 | def main(): 12 | """ 13 | ApplicationSession is an Application with an embedded session that you 14 | can get anytime using the session method. It automatically connects the 15 | session once start or run are called. It also does its own 16 | arguments-parsing. If you don't want either of those behaviors, you may 17 | prefer to use a regular Application and your own Session instead. 18 | The initialization takes a list as argument which correspond to the 19 | arguments given to the program. If the --qi-url is given to your program, 20 | the session will be connected to the given value later on. The same goes 21 | for --qi-listen-url option. 22 | If no --qi-url is given, the default connect URL is tcp://127.0.0.1:9559. 23 | If no --qi-listen-url is given, the default listen URL is 24 | tcp://127.0.0.1:0. 25 | Note : The parsed options will removed from the given list by Application. 26 | """ 27 | 28 | # Initialize the ApplicationSession first. 29 | app = qi.ApplicationSession(sys.argv) 30 | 31 | # You can check what url the session will connect to with the url method. 32 | print "The url used by ApplicationSession will be " + app.url() 33 | 34 | # You can manipulate the session used by the application with the session 35 | # method. You must not use the connect method on it though since it will be 36 | # done automatically with either start or run method. 37 | session = app.session() 38 | 39 | # The start method connects the session and starts listening if the --qi-url 40 | # options was given. This call is optional though since the run method calls 41 | # it if it hasn't been done yet. 42 | app.start() 43 | 44 | # Calls the start method if needed and then run the application. 45 | app.run() 46 | 47 | # The session is now connected and available at that point. 48 | 49 | if __name__ == "__main__": 50 | main() 51 | -------------------------------------------------------------------------------- /examples/authentication_with_application.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import qi 3 | import sys 4 | 5 | 6 | class Authenticator: 7 | 8 | def __init__(self, username, password): 9 | self.username = username 10 | self.password = password 11 | 12 | # This method is expected by libqi and must return a dictionary containing 13 | # login information with the keys 'user' and 'token'. 14 | def initialAuthData(self): 15 | return {'user': self.username, 'token': self.password} 16 | 17 | 18 | class AuthenticatorFactory: 19 | 20 | def __init__(self, username, password): 21 | self.username = username 22 | self.password = password 23 | 24 | # This method is expected by libqi and must return an object with at least 25 | # the `initialAuthData` method. 26 | def newAuthenticator(self): 27 | return Authenticator(self.username, self.password) 28 | 29 | 30 | # Reads a file containing the username on the first line and the password on 31 | # the second line. This is the format used by qilaunch. 32 | def read_auth_file(path): 33 | with open(path) as f: 34 | username = f.readline().strip() 35 | password = f.readline().strip() 36 | return (username, password) 37 | 38 | 39 | def make_application(argv=sys.argv): 40 | """ 41 | Create and return the qi.Application, with authentication set up 42 | according to the command line options. 43 | """ 44 | # create the app and edit `argv` in place to remove the consumed 45 | # arguments. 46 | # As a side effect, if "-h" is in the list, it is replaced with "--help". 47 | app = qi.Application(argv) 48 | 49 | # Setup a non-intrusive parser, behaving like `qi.Application`'s own 50 | # parser: 51 | # * don't complain about unknown arguments 52 | # * consume known arguments 53 | # * if the "--help" option is present: 54 | # * print its own options help 55 | # * do not print the main app usage 56 | # * do not call `sys.exit()` 57 | parser = argparse.ArgumentParser(add_help=False, usage=argparse.SUPPRESS) 58 | parser.add_argument( 59 | "-a", "--authfile", 60 | help="Path to the authentication config file. This file must " 61 | "contain the username on the first line and the password on the " 62 | "second line.") 63 | if "--help" in argv: 64 | parser.print_help() 65 | return app 66 | args, unparsed_args = parser.parse_known_args(argv[1:]) 67 | logins = read_auth_file(args.authfile) if args.authfile else ("nao", "nao") 68 | factory = AuthenticatorFactory(*logins) 69 | app.session.setClientAuthenticatorFactory(factory) 70 | # edit argv in place. 71 | # Note: this might modify sys.argv, like qi.Application does. 72 | argv[1:] = unparsed_args 73 | return app 74 | 75 | 76 | if __name__ == "__main__": 77 | parser = argparse.ArgumentParser() 78 | parser.add_argument("--msg", default="Hello python") 79 | app = make_application() 80 | args = parser.parse_args() 81 | logger = qi.Logger("authentication_with_application") 82 | logger.info("connecting session") 83 | app.start() 84 | logger.info("fetching ALTextToSpeech service") 85 | tts = app.session.service("ALTextToSpeech") 86 | logger.info("Saying something") 87 | tts.call("say", args.msg) 88 | -------------------------------------------------------------------------------- /examples/qiclient.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2 2 | ## 3 | ## Copyright (C) 2012, 2013 Aldebaran Robotics 4 | ## 5 | 6 | """ Python client implementation of famous QiMessaging hello world : serviceTest 7 | """ 8 | 9 | import time 10 | import sys 11 | import qi 12 | 13 | def get_servicedirectory_address(argv): 14 | """ Parse command line arguments 15 | 16 | Print usage is service directory address is not set. 17 | """ 18 | if len(argv) != 2: 19 | print('Usage : python2 qi-client.py directory-address') 20 | print('Assuming service directory address is tcp://127.0.0.1:9559') 21 | return "tcp://127.0.0.1:9559" 22 | 23 | return argv[1] 24 | 25 | def onReply(fut): 26 | print("async repl:", fut.value()) 27 | 28 | def onServiceAvailable(fut): 29 | print("onServiceAvailable") 30 | 31 | def onTestEvent(v): 32 | print("Event:", v) 33 | 34 | def onTestEventGeneric(*args): 35 | print("EventGeneric:", args) 36 | 37 | def main(): 38 | """ Entry point of qiservice 39 | """ 40 | session = qi.Session() 41 | f = session.connect("tcp://127.0.0.1:9559", _async=True) 42 | print("connected?", not f.hasError()) 43 | 44 | #3 Get service serviceTest 45 | fut = session.service("serviceTest", _async=True) 46 | fut.addCallback(onServiceAvailable) 47 | 48 | obj = fut.value() 49 | 50 | #obj.testEvent.connect(onTestEvent) 51 | obj.testEventGeneric.connect(onTestEventGeneric) 52 | 53 | print("repl:", obj.call("reply", "plouf")) 54 | f = obj.reply("plaf", _async=True) 55 | f.addCallback(onReply) 56 | 57 | i = 0 58 | while i < 2: 59 | print("waiting...") 60 | time.sleep(1) 61 | i = i + 1 62 | session.close() 63 | 64 | if __name__ == "__main__": 65 | main() 66 | -------------------------------------------------------------------------------- /examples/qinm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | ## 3 | ## Author(s): 4 | ## - Cedric GESTES 5 | ## 6 | ## Copyright (C) 2014 Aldebaran 7 | ## 8 | 9 | """ Inspect qi modules 10 | """ 11 | 12 | import time 13 | import sys 14 | import qi 15 | import argparse 16 | 17 | def main(): 18 | """ Entry point of qiservice 19 | """ 20 | app = qi.Application(sys.argv, raw=True) 21 | 22 | 23 | parser = argparse.ArgumentParser(description='Inspect qi module') 24 | parser.add_argument('--module', '-m', help='module name') 25 | parser.add_argument("--list", "-l", action="store_true", default=False, help="list all available modules") 26 | args = parser.parse_args() 27 | 28 | if args.list: 29 | print "modules:" 30 | for m in qi.listModules(): 31 | print m 32 | return 0 33 | 34 | if args.list: 35 | print "modules:" 36 | for m in qi.listModules(): 37 | print m 38 | return 0 39 | 40 | 41 | print "module:", args.module 42 | print "prefixes:", qi.path.sdkPrefixes() 43 | 44 | mod = qi.module(args.module) 45 | for o in mod.objects(): 46 | print "object:", o 47 | for f in mod.functions(): 48 | print "func :", f 49 | for c in mod.constants(): 50 | print "const :", c 51 | 52 | if __name__ == "__main__": 53 | main() 54 | -------------------------------------------------------------------------------- /examples/qiservice.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2 2 | ## 3 | ## Copyright (C) 2012, 2013 Aldebaran Robotics 4 | ## 5 | 6 | """ Python service providing the famous 'reply::s(s)' function 7 | """ 8 | 9 | import sys 10 | import time 11 | import qi 12 | import threading 13 | 14 | def makeIt(p): 15 | time.sleep(1) 16 | print("PAFFF") 17 | p.setValue(42) 18 | 19 | class ServiceTest: 20 | def __init__(self): 21 | self.onFoo = qi.Signal("(i)") 22 | self.testEvent = qi.Signal("(s)") 23 | self.testEventGeneric = qi.Signal() 24 | 25 | def reply(self, plaf): 26 | print("v:", plaf) 27 | return plaf + "bim" 28 | 29 | def error(self): 30 | d= dict() 31 | print("I Will throw") 32 | r = d['pleaseraise'] 33 | 34 | def fut(self): 35 | p = qi.Promise() 36 | #p.setValue(42) 37 | threading.Thread(target=makeIt, args=[p]).start() 38 | return p.future() 39 | 40 | @qi.nobind 41 | def nothing(self): 42 | print("nothing") 43 | pass 44 | 45 | @qi.bind(qi.String, (qi.String, qi.Int32), "plik") 46 | def plok(self, name, index): 47 | print("ploK") 48 | return name[index] 49 | 50 | @qi.bind(qi.Dynamic, qi.AnyArguments) 51 | def special(self, *args): 52 | print("args:", args) 53 | 54 | def special2(self, *args): 55 | print("args2:", args) 56 | 57 | def get_servicedirectory_address(): 58 | """ Parse command line arguments 59 | 60 | Print usage is service directory address is not set. 61 | """ 62 | if len(sys.argv) != 2: 63 | print('Usage : %s qi-service.py directory-address' % sys.argv[0]) 64 | print('Assuming service directory address is tcp://127.0.0.1:9559') 65 | return "tcp://127.0.0.1:9559" 66 | 67 | return sys.argv[1] 68 | 69 | def main(): 70 | """ Entry point of qiservice 71 | """ 72 | #1 Check if user give us service directory address. 73 | sd_addr = get_servicedirectory_address() 74 | 75 | s = qi.Session() 76 | s.connect(sd_addr) 77 | m = ServiceTest() 78 | s.registerService("serviceTest", m) 79 | 80 | #5 Call Application.run() to join event loop. 81 | i = 0 82 | while True: 83 | mystr = "bim" + str(i) 84 | print("posting:", mystr) 85 | myplouf = [ "bim", 42 ] 86 | m.testEvent(mystr) 87 | m.testEventGeneric(myplouf) 88 | time.sleep(1); 89 | i += 1 90 | 91 | #6 Clean 92 | s.close() 93 | #main : Done. 94 | 95 | if __name__ == "__main__": 96 | main() 97 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # PEP 518 2 | [build-system] 3 | requires = ["scikit-build-core"] 4 | build-backend = "scikit_build_core.build" 5 | 6 | # PEP 621 7 | [project] 8 | name = "qi" 9 | description = "LibQi Python bindings" 10 | version = "3.1.5" 11 | readme = "README.rst" 12 | requires-python = ">=3.7" 13 | license = { "file" = "COPYING" } 14 | keywords=[ 15 | "libqi", 16 | "qi", 17 | "naoqi", 18 | "aldebaran", 19 | "robotics", 20 | "robot", 21 | "nao", 22 | "pepper", 23 | "romeo", 24 | "plato", 25 | ] 26 | classifiers=[ 27 | "Development Status :: 5 - Production/Stable", 28 | "License :: OSI Approved :: BSD License", 29 | "Intended Audience :: Developers", 30 | "Topic :: Software Development :: Libraries", 31 | "Topic :: Software Development :: Libraries :: Application Frameworks", 32 | "Topic :: Software Development :: Embedded Systems", 33 | "Framework :: Robot Framework :: Library", 34 | "Programming Language :: Python :: 3", 35 | "Programming Language :: Python :: 3.7", 36 | "Programming Language :: Python :: 3.8", 37 | "Programming Language :: Python :: 3.9", 38 | "Programming Language :: Python :: 3.10", 39 | "Programming Language :: Python :: 3.11", 40 | "Programming Language :: Python :: 3 :: Only", 41 | ] 42 | maintainers = [ 43 | { email = "framework@aldebaran.com" }, 44 | { name = "Vincent Palancher", email = "vincent.palancher@aldebaran.com" }, 45 | { name = "Jérémy Monnon", email = "jmonnon@aldebaran.com" }, 46 | ] 47 | 48 | [project.urls] 49 | repository = "https://github.com/aldebaran/libqi-python" 50 | 51 | [tool.scikit-build] 52 | # Rely only on CMake install, not on Python packages detection. 53 | wheel.packages = [] 54 | 55 | [tool.scikit-build.cmake.define] 56 | # Disable building tests by default when building a wheel. 57 | BUILD_TESTING = "OFF" 58 | 59 | [tool.cibuildwheel] 60 | build = "cp*manylinux*x86_64" 61 | build-frontend = "build" 62 | 63 | [tool.cibuildwheel.linux] 64 | before-all = ["ci/cibuildwheel_linux_before_all.sh {package}"] 65 | 66 | [tool.cibuildwheel.linux.config-settings] 67 | "cmake.args" = ["--preset=conan-release"] 68 | -------------------------------------------------------------------------------- /qi/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2010 - 2020 Softbank Robotics Europe 3 | # 4 | # -*- coding: utf-8 -*- 5 | 6 | """LibQi Python bindings.""" 7 | 8 | import sys # noqa: E402 9 | import atexit # noqa: E402 10 | 11 | from .qi_python \ 12 | import (FutureState, FutureTimeout, Future, futureBarrier, # noqa: E402 13 | Promise, Property, Session, Signal, runAsync, PeriodicTask, 14 | clockNow, steadyClockNow, systemClockNow, module, listModules, 15 | Application as _Application, 16 | ApplicationSession as _ApplicationSession) 17 | from . import path # noqa: E402 18 | from ._type import (Void, Bool, Int8, UInt8, Int16, UInt16, # noqa: E402 19 | Int32, UInt32, Int64, UInt64, 20 | Float, Double, String, List, 21 | Optional, Map, Struct, Object, Dynamic, 22 | Buffer, AnyArguments, typeof, _isinstance) # noqa: E402 23 | from ._binder import bind, nobind, singleThreaded, multiThreaded # noqa: E402 24 | from .logging import fatal, error, warning, info, verbose, Logger # noqa: E402 25 | from .translator import defaultTranslator, tr, Translator # noqa: E402 26 | from ._version import __version__ # noqa: E402, F401 27 | 28 | 29 | __all__ = [ 30 | 'FutureState', 'FutureTimeout', 'Future', 'futureBarrier', 'Promise', 31 | 'Property', 'Session', 'Signal', 'runAsync', 'PeriodicTask', 'clockNow', 32 | 'steadyClockNow', 'systemClockNow', 'module', 'listModules', 33 | 'path', 'Void', 'Bool', 'Int8', 'UInt8', 'Int16', 'UInt16', 'Int32', 34 | 'UInt32', 'Int64', 'UInt64', 'Float', 'Double', 'String', 'List', 'Optional', 35 | 'Map', 'Struct', 'Object', 'Dynamic', 'Buffer', 'AnyArguments', 'typeof', 36 | 'isinstance', 'bind', 'nobind', 'singleThreaded', 'multiThreaded', 'fatal', 37 | 'error', 'warning', 'info', 'verbose', 'Logger', 'defaultTranslator', 'tr', 38 | 'Translator', 'Application', 'ApplicationSession' 39 | ] 40 | 41 | def PromiseNoop(*args, **kwargs): 42 | """No operation function 43 | .. deprecated:: 1.5.0""" 44 | pass 45 | 46 | 47 | isinstance = _isinstance 48 | 49 | _app = None 50 | 51 | 52 | # We want to stop all threads before Python start destroying module and the 53 | # like (this avoids callbacks calling Python while it's destroying). 54 | def _stop_application(): 55 | global _app 56 | if _app is not None: 57 | _app.stop() 58 | del _app 59 | _app = None 60 | 61 | 62 | # Register _stop_application as a function to be executed at termination 63 | atexit.register(_stop_application) 64 | 65 | 66 | # Application is a singleton, it should live till the end 67 | # of the program because it owns eventloops 68 | def Application(args=None, raw=False, autoExit=True, url=None): 69 | """Instantiates and returns the Application instance.""" 70 | global _app 71 | if _app is None: 72 | if args is None: 73 | args = sys.argv 74 | if url is None: 75 | url = '' 76 | if not args: 77 | args = [sys.executable] 78 | elif args[0] == '': 79 | args[0] = sys.executable 80 | if raw: 81 | _app = _Application(args) 82 | else: 83 | _app = _ApplicationSession(args, autoExit, url) 84 | else: 85 | raise Exception("Application was already initialized") 86 | return _app 87 | 88 | 89 | ApplicationSession = Application 90 | 91 | # Retrocompatibility with older versions of the module where `runAsync` was 92 | # named `async`. 93 | if sys.version_info < (3,7): 94 | globals()['async'] = runAsync 95 | __all__.append('async') 96 | -------------------------------------------------------------------------------- /qi/_binder.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2010 - 2020 Softbank Robotics Europe 3 | # 4 | # -*- coding: utf-8 -*- 5 | 6 | import inspect 7 | from ._type import AnyArguments 8 | 9 | 10 | class bind(object): 11 | """Allows specifying types and methodName for bound methods.""" 12 | 13 | def __init__(self, returnType=None, paramsType=None, methodName=None): 14 | """Bind Constructor""" 15 | if returnType is None: 16 | self._retsig = None 17 | else: 18 | self._retsig = str(returnType) 19 | if paramsType is None: 20 | self._sig = None 21 | elif isinstance(paramsType, (list, tuple)): 22 | self._sig = "(%s)" % "".join([str(x) for x in paramsType]) 23 | elif isinstance(paramsType, AnyArguments) \ 24 | or (inspect.isclass(paramsType) and 25 | issubclass(paramsType, AnyArguments)): 26 | self._sig = "m" 27 | else: 28 | raise Exception("Invalid types for parameters") 29 | self._name = methodName 30 | 31 | def __call__(self, f): 32 | """Function generator.""" 33 | f.__qi_name__ = self._name 34 | f.__qi_signature__ = self._sig 35 | f.__qi_return_signature__ = self._retsig 36 | return f 37 | 38 | 39 | def nobind(func): 40 | """This function decorator will prevent the function from being bound.""" 41 | func.__qi_signature__ = "DONOTBIND" 42 | return func 43 | 44 | 45 | class singleThreaded(object): 46 | """This class decorator specifies that some methods of this class will 47 | never be called at the same time on the same instance by the qi module, 48 | by doing the calls sequentially and ensuring thread safety without the 49 | need of some extra synchronisation mechanism. 50 | This guarantee only applies to method calls that originate from the qi 51 | module, which mostly concerns bound methods and methods connected as 52 | callbacks of signals. 53 | It does not apply to private methods (methods that start with 54 | '__'), including but not restricted to __init__, __del__, __enter__ and 55 | __exit__. 56 | One consequence of this is that a sequenced method call on an object 57 | must finish before another sequenced method call on the same object can 58 | be made. 59 | This is the default behavior. 60 | """ 61 | 62 | def __init(self, _): 63 | pass 64 | 65 | def __call__(self, f): 66 | """ Function Generator """ 67 | f.__qi_threading__ = "single" 68 | return f 69 | 70 | 71 | class multiThreaded(object): 72 | """This class decorator specifies that methods in the class are allowed to 73 | be called concurrently. This implies that the developer of the class 74 | must garantee no concurrent access to it's internal data, usually by 75 | using some synchronization mechanism. 76 | """ 77 | 78 | def __init(self, _): 79 | pass 80 | 81 | def __call__(self, f): 82 | """ Function Generator. """ 83 | f.__qi_threading__ = "multi" 84 | return f 85 | -------------------------------------------------------------------------------- /qi/_type.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2010 - 2020 Softbank Robotics Europe 3 | # 4 | # -*- coding: utf-8 -*- 5 | 6 | from .qi_python import Object as _Object 7 | 8 | 9 | class _MetaSignature(type): 10 | """ MetaSignature """ 11 | 12 | def __str__(self): 13 | return self.signature 14 | 15 | def __unicode__(self): 16 | return self.signature 17 | 18 | # Support comparing class and instance (Int8 == Int8()) 19 | def __eq__(self, other): 20 | if isinstance(other, str): 21 | return other == self.signature 22 | return other.signature == self.signature 23 | 24 | def __ne__(self, other): 25 | if isinstance(other, str): 26 | return other != self.signature 27 | return other.signature != self.signature 28 | 29 | 30 | # This syntax works for defining metaclass in python2 and python3 31 | _ToInheritMetaSignature = _MetaSignature( 32 | '_ToInheritMetaSignature', (object, ), {}) 33 | 34 | 35 | class _Signature(_ToInheritMetaSignature): 36 | def __str__(self): 37 | return self.signature 38 | 39 | def __unicode__(self): 40 | return self.signature 41 | 42 | def __eq__(self, other): 43 | if isinstance(other, str): 44 | return other == self.signature() 45 | return other.signature == self.signature 46 | 47 | def __ne__(self, other): 48 | return not self.__eq__(other) 49 | 50 | 51 | class Void(_Signature): 52 | """ Void Type """ 53 | signature = 'v' 54 | 55 | 56 | class Bool(_Signature): 57 | """ Bool Type """ 58 | signature = 'b' 59 | 60 | 61 | class Int8(_Signature): 62 | """ Signed 8 bits Integer Type """ 63 | signature = 'c' 64 | 65 | 66 | class UInt8(_Signature): 67 | """ Unsigned 8 bits Integer Type """ 68 | signature = 'C' 69 | 70 | 71 | class Int16(_Signature): 72 | """ Signed 16 bits Integer Type """ 73 | signature = 'w' 74 | 75 | 76 | class UInt16(_Signature): 77 | """ Unsigned 16 bits Integer Type """ 78 | signature = 'W' 79 | 80 | 81 | class Int32(_Signature): 82 | """ Signed 32 bits Integer Type """ 83 | signature = 'i' 84 | 85 | 86 | class UInt32(_Signature): 87 | """ Unsigned 32 bits Integer Type """ 88 | signature = 'I' 89 | 90 | 91 | class Int64(_Signature): 92 | """ Signed 64 bits Integer Type """ 93 | signature = 'l' 94 | 95 | 96 | class UInt64(_Signature): 97 | """ Unsigned 64 bits Integer Type """ 98 | signature = 'L' 99 | 100 | 101 | class Float(_Signature): 102 | """ 32 bits Floating Point Type """ 103 | signature = 'f' 104 | 105 | 106 | class Double(_Signature): 107 | """ 64 bits Floating Point Type """ 108 | signature = 'd' 109 | 110 | 111 | class String(_Signature): 112 | """ String Type """ 113 | __metaclass__ = _MetaSignature 114 | signature = "s" 115 | 116 | 117 | class List(_Signature): 118 | """ List Type, an element type need to be specified """ 119 | 120 | def __init__(self, elementType): 121 | self.signature = "[%s]" % elementType.signature 122 | 123 | class Optional(_Signature): 124 | """ Optional Type, a value type need to be specified """ 125 | 126 | def __init__(self, valueType): 127 | self.signature = "+%s" % valueType.signature 128 | 129 | class Map(_Signature): 130 | """ List Type, a key and an element type need to be specified """ 131 | 132 | def __init__(self, keyType, elementType): 133 | self.signature = "{%s%s}" % (keyType.signature, elementType.signature) 134 | 135 | 136 | class Struct(_Signature): 137 | """ Structure Type """ 138 | 139 | def __init__(self, fields): 140 | self.signature = "(%s)" % fields.join("") 141 | 142 | 143 | class Object(_Signature): 144 | """ Object Type """ 145 | __metaclass__ = _MetaSignature 146 | signature = 'o' 147 | 148 | 149 | class Dynamic(_Signature): 150 | """ Any Type """ 151 | __metaclass__ = _MetaSignature 152 | signature = 'm' 153 | 154 | 155 | class Buffer(_Signature): 156 | """ Buffer Type """ 157 | __metaclass__ = _MetaSignature 158 | signature = 'r' 159 | 160 | 161 | # Yes this look similar to Dynamic but it's not. 162 | # eg: qi.bind(Void, (Dynamic, Dynamic)) this mean a tuple of two dynamic. 163 | # eg: qi.bind(Void, AnyArguments) this is not a tuple. (m not in tuple, 164 | # mean anythings) 165 | # eg: qi.bind(Void, Dynamic) this is a function with one argument 166 | class AnyArguments(_Signature): 167 | """ Any Arguments Types. A function or a signal taking AnyArguments 168 | will accept all kind of arguments. AnyArguments is a list of AnyValue 169 | """ 170 | __metaclass__ = _MetaSignature 171 | signature = 'm' 172 | 173 | 174 | # Return the qi.type of the parameter 175 | def typeof(a): 176 | """ return the qi type of a variable 177 | .. warning:: 178 | this function is only implemented for Object 179 | """ 180 | if isinstance(a, _Object): 181 | return Object 182 | raise NotImplementedError( 183 | "typeOf is only implemented for Object right now") 184 | 185 | 186 | # Cant be called isinstance or typeof will run into infinite loop 187 | # See qi.__init__ for the renaming 188 | def _isinstance(a, type): 189 | """ return true if `a` is of type `type` 190 | .. warning:: 191 | this function is only implemented for Object 192 | """ 193 | if type != Object: 194 | raise NotImplementedError("isinstance is only implemented for Object" 195 | "right now") 196 | try: 197 | return typeof(a) == type 198 | except NotImplementedError: 199 | return False 200 | -------------------------------------------------------------------------------- /qi/_version.py.in: -------------------------------------------------------------------------------- 1 | __version__ = "@QI_PYTHON_VERSION_STRING@" 2 | -------------------------------------------------------------------------------- /qi/logging.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2010 - 2020 Softbank Robotics Europe 3 | # 4 | # -*- coding: utf-8 -*- 5 | 6 | from .qi_python import LogLevel, pylog, setLevel, setContext, setFilters 7 | from collections import namedtuple 8 | import inspect 9 | 10 | __all__ = [ 11 | "SILENT", "FATAL", "ERROR", "WARNING", "INFO", "VERBOSE", "DEBUG", 12 | "fatal", "error", "warning", "info", "verbose", 13 | "Logger", "setLevel", "setContext", "setFilters" 14 | ] 15 | 16 | # Log Level 17 | SILENT = LogLevel.Silent 18 | FATAL = LogLevel.Fatal 19 | ERROR = LogLevel.Error 20 | WARNING = LogLevel.Warning 21 | INFO = LogLevel.Info 22 | VERBOSE = LogLevel.Verbose 23 | DEBUG = LogLevel.Debug 24 | 25 | 26 | def log_get_trace_info(): 27 | info = None 28 | try: 29 | stack = inspect.stack() 30 | # current stack's frame 0 is this frame 31 | # frame 1 is the call on the Logger object 32 | # frame 2 must be the place where the call was made from 33 | callerframerecord = stack[2] 34 | frame = callerframerecord[0] 35 | info = inspect.getframeinfo(frame) 36 | except Exception: 37 | FakeTrackback = namedtuple("FakeTrackback", 38 | ["filename", "function", "lineno"]) 39 | info = FakeTrackback('', '', -1) 40 | return info 41 | 42 | 43 | def print_to_string(mess, *args): 44 | return ' '.join(str(x) for x in (mess,) + args) 45 | 46 | 47 | class Logger: 48 | def __init__(self, category): 49 | self.category = category 50 | 51 | def fatal(self, mess, *args): 52 | """ fatal(mess, *args) -> None 53 | :param mess: Messages string 54 | :param *args: Messages format string working the same way as python 55 | function print. 56 | Logs a message with level FATAL on this logger.""" 57 | info = log_get_trace_info() 58 | pylog(FATAL, self.category, print_to_string(mess, *args), 59 | info.filename, info.function, info.lineno) 60 | 61 | def error(self, mess, *args): 62 | """ error(mess, *args) -> None 63 | :param mess: Messages string 64 | :param *args: Arguments are interpreted as for 65 | :py:func:`qi.Logger.fatal`. 66 | Logs a message with level ERROR on this logger.""" 67 | info = log_get_trace_info() 68 | pylog(ERROR, self.category, print_to_string(mess, *args), 69 | info.filename, info.function, info.lineno) 70 | 71 | def warning(self, mess, *args): 72 | """ warning(mess, *args) -> None 73 | :param mess: Messages string 74 | :param *args: Arguments are interpreted as for 75 | :py:func:`qi.Logger.fatal`. 76 | Logs a message with level WARNING on this logger.""" 77 | info = log_get_trace_info() 78 | pylog(WARNING, self.category, print_to_string(mess, *args), 79 | info.filename, info.function, info.lineno) 80 | 81 | def info(self, mess, *args): 82 | """ info(mess, *args) -> None 83 | :param mess: Messages string 84 | :param *args: Arguments are interpreted as for 85 | :py:func:`qi.Logger.fatal`. 86 | Logs a message with level INFO on this logger.""" 87 | info = log_get_trace_info() 88 | pylog(INFO, self.category, print_to_string(mess, *args), 89 | info.filename, info.function, info.lineno) 90 | 91 | def verbose(self, mess, *args): 92 | """ verbose(mess, *args) -> None 93 | :param mess: Messages string 94 | :param *args: Arguments are interpreted as for 95 | :py:func:`qi.Logger.fatal`. 96 | Logs a message with level VERBOSE on this logger.""" 97 | info = log_get_trace_info() 98 | pylog(VERBOSE, self.category, print_to_string(mess, *args), 99 | info.filename, info.function, info.lineno) 100 | 101 | 102 | def fatal(cat, mess, *args): 103 | """ fatal(cat, mess, *args) -> None 104 | :param cat: The category is potentially a period-separated hierarchical 105 | value. 106 | :param mess: Messages string 107 | :param *args: Messages format string working the same way as print python 108 | function. 109 | Logs a message with level FATAL.""" 110 | info = log_get_trace_info() 111 | pylog(FATAL, cat, print_to_string(mess, *args), 112 | info.filename, info.function, info.lineno) 113 | 114 | 115 | def error(cat, mess, *args): 116 | """ error(cat, mess, *args) -> None 117 | :param cat: The category is potentially a period-separated hierarchical 118 | value. 119 | :param mess: Messages string 120 | :param *args: Messages format string working the same way as print python 121 | function. 122 | Logs a message with level ERROR.""" 123 | info = log_get_trace_info() 124 | pylog(ERROR, cat, print_to_string(mess, *args), 125 | info.filename, info.function, info.lineno) 126 | 127 | 128 | def warning(cat, mess, *args): 129 | """ warning(cat, mess, *args) -> None 130 | :param cat: The category is potentially a period-separated hierarchical 131 | value. 132 | :param mess: Messages string 133 | :param *args: Messages format string working the same way as print python 134 | function. 135 | Logs a message with level WARNING.""" 136 | info = log_get_trace_info() 137 | pylog(WARNING, cat, print_to_string(mess, *args), 138 | info.filename, info.function, info.lineno) 139 | 140 | 141 | def info(cat, mess, *args): 142 | """ info(cat, mess, *args) -> None 143 | :param cat: The category is potentially a period-separated hierarchical 144 | value. 145 | :param mess: Messages string 146 | :param *args: Messages format string working the same way as print python 147 | function. 148 | Logs a message with level INFO.""" 149 | info = log_get_trace_info() 150 | pylog(INFO, cat, print_to_string(mess, *args), 151 | info.filename, info.function, info.lineno) 152 | 153 | 154 | def verbose(cat, mess, *args): 155 | """ verbose(cat, mess, *args) -> None 156 | :param cat: The category is potentially a period-separated hierarchical 157 | value. 158 | :param mess: Messages string 159 | :param *args: Messages format string working the same way as print python 160 | function. 161 | Logs a message with level VERBOSE.""" 162 | info = log_get_trace_info() 163 | pylog(VERBOSE, cat, print_to_string(mess, *args), 164 | info.filename, info.function, info.lineno) 165 | -------------------------------------------------------------------------------- /qi/native.py.in: -------------------------------------------------------------------------------- 1 | __version__ = "@NATIVE_VERSION@" 2 | -------------------------------------------------------------------------------- /qi/path.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2010 - 2020 Softbank Robotics Europe 3 | # 4 | # -*- coding: utf-8 -*- 5 | 6 | from .qi_python \ 7 | import (findBin, findLib, findConf, findData, listData, confPaths, 8 | dataPaths, binPaths, libPaths, setWritablePath, 9 | userWritableDataPath, userWritableConfPath, sdkPrefix, sdkPrefixes, 10 | addOptionalSdkPrefix, clearOptionalSdkPrefix) 11 | 12 | __all__ = [ 13 | "findBin", "findLib", "findConf", "findData", "listData", "confPaths", 14 | "dataPaths", "binPaths", "libPaths", "setWritablePath", 15 | "userWritableDataPath", "userWritableConfPath", "sdkPrefix", "sdkPrefixes", 16 | "addOptionalSdkPrefix", "clearOptionalSdkPrefix", 17 | ] 18 | -------------------------------------------------------------------------------- /qi/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aldebaran/libqi-python/a0ed2086db1fedc599317fb6eb5a42243d9a622e/qi/test/__init__.py -------------------------------------------------------------------------------- /qi/test/conftest.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2010 - 2020 Softbank Robotics Europe 3 | # 4 | # -*- coding: utf-8 -*- 5 | 6 | import pytest 7 | import qi 8 | import subprocess 9 | 10 | 11 | def make_session(request, url): 12 | """Connect a session to a URL.""" 13 | sess = qi.Session() 14 | sess.connect(url) 15 | request.addfinalizer(sess.close) 16 | return sess 17 | 18 | 19 | @pytest.fixture 20 | def session(request, url): 21 | return make_session(request, url) 22 | 23 | 24 | @pytest.fixture 25 | def process(request, exec_path): 26 | """Process launched from an executable path.""" 27 | proc = subprocess.Popen(exec_path.split( 28 | " "), stdout=subprocess.PIPE, universal_newlines=True) 29 | 30 | def fin(): 31 | proc.stdout.close() 32 | proc.terminate() 33 | proc.wait() 34 | request.addfinalizer(fin) 35 | return proc 36 | 37 | 38 | @pytest.fixture 39 | def process_service_endpoint(process): 40 | """Endpoint of a service with its name that a process returns on 41 | its standard output. 42 | """ 43 | service_name_prefix = "service_name=" 44 | endpoint_prefix = "endpoint=" 45 | service_name, service_endpoint = str(), str() 46 | # iterating over pipes doesn't work in python 2.7 47 | for line in map(str.strip, iter(process.stdout.readline, "")): 48 | if line.startswith(endpoint_prefix): 49 | service_endpoint = line[len(endpoint_prefix):] 50 | elif line.startswith(service_name_prefix): 51 | service_name = line[len(service_name_prefix):] 52 | if len(service_endpoint) > 0 and len(service_name) > 0: 53 | break 54 | return service_name, service_endpoint 55 | 56 | 57 | @pytest.fixture 58 | def session_to_process_service_endpoint(request, process_service_endpoint): 59 | service_name, service_endpoint = process_service_endpoint 60 | return service_name, make_session(request, service_endpoint) 61 | 62 | 63 | def pytest_addoption(parser): 64 | parser.addoption('--url', action='append', type=str, default=[], 65 | help='Url to connect the session to.') 66 | parser.addoption('--exec_path', action='append', type=str, default=[], 67 | help='Executable with arguments to launch ' 68 | 'before the test.') 69 | 70 | 71 | def pytest_generate_tests(metafunc): 72 | if 'url' in metafunc.fixturenames: 73 | metafunc.parametrize("url", metafunc.config.getoption('url')) 74 | if 'exec_path' in metafunc.fixturenames: 75 | metafunc.parametrize( 76 | "exec_path", metafunc.config.getoption('exec_path')) 77 | -------------------------------------------------------------------------------- /qi/test/test_applicationsession.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2010 - 2020 Softbank Robotics Europe 3 | # 4 | # -*- coding: utf-8 -*- 5 | 6 | import time 7 | import sys 8 | import qi 9 | 10 | global usual_timeout 11 | usual_timeout = 10 12 | 13 | 14 | def has_succeeded(promise): 15 | return promise.future().wait(usual_timeout) \ 16 | == qi.FutureState.FinishedWithValue 17 | 18 | 19 | def test_applicationsession(): 20 | 21 | connecting = qi.Promise() 22 | 23 | def callback_conn(): 24 | connecting.setValue(None) 25 | print("Connected!") 26 | 27 | def has_connected(): 28 | return has_succeeded(connecting) 29 | 30 | disconnecting = qi.Promise() 31 | 32 | def callback_disconn(s): 33 | disconnecting.setValue(None) 34 | print("Disconnected!") 35 | 36 | def has_disconnected(): 37 | return has_succeeded(disconnecting) 38 | 39 | sd = qi.Session() 40 | sd.listenStandalone("tcp://127.0.0.1:0") 41 | 42 | sys.argv = sys.argv + ["--qi-url", sd.endpoints()[0]] 43 | app = qi.ApplicationSession(sys.argv) 44 | assert app.url == sd.endpoints()[0] 45 | assert not has_connected() 46 | assert not has_disconnected() 47 | app.session.connected.connect(callback_conn) 48 | app.session.disconnected.connect(callback_disconn) 49 | app.start() 50 | 51 | assert has_connected() 52 | assert not has_disconnected() 53 | 54 | running = qi.Promise() 55 | 56 | def validate(): 57 | running.setValue(None) 58 | 59 | def has_run(): 60 | return has_succeeded(running) 61 | 62 | app.atRun(validate) 63 | 64 | def runApp(): 65 | app.run() 66 | 67 | qi.runAsync(runApp) 68 | assert has_run() 69 | 70 | app.session.close() 71 | time.sleep(0.01) 72 | 73 | assert has_disconnected() 74 | -------------------------------------------------------------------------------- /qi/test/test_async.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2010 - 2020 Softbank Robotics Europe 3 | # 4 | # -*- coding: utf-8 -*- 5 | 6 | import qi 7 | import time 8 | 9 | 10 | def add(a, b, c = 1): 11 | return (a + b) * c 12 | 13 | 14 | def fail(): 15 | assert(False) 16 | 17 | 18 | def err(): 19 | raise RuntimeError("sdsd") 20 | 21 | 22 | def test_async_fun(): 23 | f = qi.runAsync(add, 21, 21) 24 | assert(f.value() == 42) 25 | 26 | def test_async_fun_kw(): 27 | # delay is a kw arg for runAsync, the others are kw args for add. 28 | f = qi.runAsync(add, c=3, b=9, a=4, delay=100) 29 | assert(f.value() == 39) 30 | 31 | 32 | def test_async_error(): 33 | f = qi.runAsync(err) 34 | assert(f.hasError()) 35 | assert("RuntimeError: sdsd" in f.error()) 36 | 37 | 38 | class Adder: 39 | def __init__(self): 40 | self.v = 0 41 | 42 | def add(self, a): 43 | self.v += a 44 | return self.v 45 | 46 | def val(self): 47 | return self.v 48 | 49 | 50 | def test_async_meth(): 51 | ad = Adder() 52 | f = qi.runAsync(ad.add, 21) 53 | assert(f.value() == 21) 54 | f = qi.runAsync(ad.add, 21) 55 | assert(f.value() == 42) 56 | f = qi.runAsync(ad.val) 57 | assert(f.value() == 42) 58 | 59 | 60 | def test_async_delay(): 61 | f = qi.runAsync(add, 21, 21, delay=1000) 62 | assert(f.value() == 42) 63 | 64 | def test_periodic_task(): 65 | t = qi.PeriodicTask() 66 | global result 67 | result = 0 68 | def add(): 69 | global result 70 | result += 1 71 | t.setCallback(add) 72 | t.setUsPeriod(1000) 73 | t.start(True) 74 | time.sleep(1) 75 | t.stop() 76 | assert result > 5 # how to find 5: plouf plouf plouf 77 | cur = result 78 | time.sleep(1) 79 | assert cur == result 80 | del result 81 | 82 | 83 | def test_async_cancel(): 84 | f = qi.runAsync(fail, delay=1000000) 85 | assert(f.isCancelable()) 86 | f.cancel() 87 | f.wait() 88 | assert(f.isFinished()) 89 | assert(not f.hasError()) 90 | assert(f.isCanceled()) 91 | 92 | 93 | def test_async_nested_future(): 94 | f = qi.runAsync(lambda: qi.Future(42)) 95 | assert isinstance(f, qi.Future) 96 | assert isinstance(f.value(), qi.Future) 97 | -------------------------------------------------------------------------------- /qi/test/test_call.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2010 - 2020 Softbank Robotics Europe 3 | # 4 | # -*- coding: utf-8 -*- 5 | 6 | import qi 7 | import time 8 | import threading 9 | import pytest 10 | 11 | 12 | def setValue(p, v): 13 | time.sleep(0.2) 14 | p.setValue(v) 15 | 16 | 17 | @qi.multiThreaded() 18 | class FooService: 19 | def __init__(self): 20 | pass 21 | 22 | def simple(self): 23 | return 42 24 | 25 | def vargs(self, *args): 26 | return args 27 | 28 | def vargsdrop(self, titi, *args): 29 | return args 30 | 31 | @qi.nobind 32 | def hidden(self): 33 | pass 34 | 35 | @qi.bind(qi.Dynamic, qi.AnyArguments) 36 | def bind_vargs(self, *args): 37 | return args 38 | 39 | @qi.bind(qi.Dynamic, (), "renamed") 40 | def oldname(self): 41 | return 42 42 | 43 | @qi.bind(qi.Int64, (qi.Int32, qi.Int32)) 44 | def add(self, a, b): 45 | return a + b 46 | 47 | @qi.bind(None, None) 48 | def reta(self, a): 49 | return a 50 | 51 | def retfutint(self): 52 | p = qi.Promise() 53 | t = threading.Thread(target=setValue, args=(p, 42, )) 54 | t.start() 55 | return p.future() 56 | 57 | @qi.bind(qi.Int32) 58 | def bind_retfutint(self): 59 | p = qi.Promise() 60 | t = threading.Thread(target=setValue, args=(p, 42, )) 61 | t.start() 62 | return p.future() 63 | 64 | def retfutmap(self): 65 | p = qi.Promise() 66 | t = threading.Thread(target=setValue, args=( 67 | p, {'titi': 'toto', "foo": "bar"},)) 68 | t.start() 69 | return p.future() 70 | 71 | @qi.bind(qi.Int32()) 72 | def retc(self, name, index): 73 | return name[index] 74 | 75 | @staticmethod 76 | def fooStat(i): 77 | return i * 3 78 | 79 | def slow(self): 80 | time.sleep(.2) 81 | return 18 82 | 83 | 84 | FooService.fooLambda = lambda self, x: x * 2 85 | 86 | 87 | @qi.multiThreaded() 88 | class Multi: 89 | def slow(self): 90 | time.sleep(0.1) 91 | return 42 92 | 93 | 94 | def docalls(sserver, sclient): 95 | sserver.registerService("FooService", FooService()) 96 | s = sclient.service("FooService") 97 | sserver.registerService("Multi", Multi()) 98 | m = sclient.service("Multi") 99 | 100 | print("simple test") 101 | assert s.simple() == 42 102 | 103 | print("vargs") 104 | assert s.vargs(42) == (42,) 105 | assert s.vargs("titi", "toto") == ("titi", "toto",) 106 | 107 | print("vargs drop") 108 | assert s.vargsdrop(4, 42) == (42,) 109 | 110 | print("hidden") 111 | try: 112 | s.hidden() 113 | assert False 114 | except Exception: 115 | pass 116 | 117 | print("bound methods") 118 | assert s.bind_vargs(42) == (42,) 119 | assert s.bind_vargs("titi", "toto") == ("titi", "toto",) 120 | 121 | print("renamed") 122 | assert s.renamed() == 42 123 | 124 | print("test types restrictions") 125 | assert s.add(40, 2) == 42 126 | try: 127 | s.add("40", "2") 128 | assert False 129 | except Exception: 130 | pass 131 | 132 | print("test future") 133 | assert s.retfutint() == 42 134 | print("test bound future") 135 | assert s.bind_retfutint() == 42 136 | 137 | print("test future async") 138 | assert s.retfutint(_async=True).value() == 42 139 | print("test bound future async") 140 | assert s.bind_retfutint(_async=True).value() == 42 141 | 142 | print("test future async") 143 | assert qi.runAsync(s.retfutint).value() == 42 144 | print("test bound future async") 145 | assert qi.runAsync(s.bind_retfutint).value() == 42 146 | 147 | print("test future map") 148 | assert s.retfutmap() == {'titi': 'toto', "foo": "bar"} 149 | 150 | print("test future map async") 151 | fut = s.retfutmap(_async=True) 152 | assert fut.hasValue() 153 | assert fut.value() == {'titi': 'toto', "foo": "bar"} 154 | 155 | print("test lambda") 156 | assert s.fooLambda(42) == 42 * 2 157 | 158 | print("test staticmethod") 159 | assert s.fooStat(4) == 4 * 3 160 | 161 | print("test async") 162 | start = time.time() 163 | print("call !") 164 | f1 = m.slow(_async=True) 165 | print("call !") 166 | f2 = m.slow(_async=True) 167 | print("call !") 168 | f3 = m.slow(_async=True) 169 | print("done !") 170 | assert f1.value() == 42 171 | assert f2.value() == 42 172 | assert f3.value() == 42 173 | end = time.time() 174 | print(end - start) 175 | assert end - start < 0.15 176 | 177 | 178 | def test_calldirect(): 179 | ses = qi.Session() 180 | ses.listenStandalone("tcp://127.0.0.1:0") 181 | # MODE DIRECT 182 | print("## DIRECT MODE") 183 | try: 184 | docalls(ses, ses) 185 | finally: 186 | ses.close() 187 | 188 | 189 | def test_callsd(): 190 | sd = qi.Session() 191 | try: 192 | sd.listenStandalone("tcp://127.0.0.1:0") 193 | local = sd.endpoints()[0] 194 | 195 | # MODE NETWORK 196 | print("## NETWORK MODE") 197 | ses = qi.Session() 198 | ses2 = qi.Session() 199 | try: 200 | ses.connect(local) 201 | ses2.connect(local) 202 | docalls(ses, ses2) 203 | finally: 204 | ses.close() 205 | ses2.close() 206 | finally: 207 | sd.close() 208 | 209 | 210 | class Invalid1: 211 | def titi(): 212 | pass 213 | 214 | 215 | def test_missingself(): 216 | sd = qi.Session() 217 | try: 218 | sd.listenStandalone("tcp://127.0.0.1:0") 219 | local = sd.endpoints()[0] 220 | 221 | print("## TestInvalid (missing self)") 222 | ses = qi.Session() 223 | ses.connect(local) 224 | i = Invalid1() 225 | with pytest.raises(Exception): 226 | ses.registerService("Invalid1", i) 227 | finally: 228 | ses.close() 229 | sd.close() 230 | 231 | 232 | class Invalid2: 233 | @qi.bind(42) 234 | def titi(self, a): 235 | pass 236 | 237 | 238 | def test_badbind(): 239 | sd = qi.Session() 240 | try: 241 | sd.listenStandalone("tcp://127.0.0.1:0") 242 | local = sd.endpoints()[0] 243 | 244 | print("## TestInvalid (bind: bad return value)") 245 | ses = qi.Session() 246 | ses.connect(local) 247 | i = Invalid2() 248 | with pytest.raises(Exception): 249 | ses.registerService("Invalid2", i) 250 | finally: 251 | ses.close() 252 | sd.close() 253 | 254 | 255 | class Invalid3: 256 | @qi.bind(qi.Float, [42]) 257 | def titi(self, a): 258 | pass 259 | 260 | 261 | def test_badbind2(): 262 | sd = qi.Session() 263 | try: 264 | sd.listenStandalone("tcp://127.0.0.1:0") 265 | local = sd.endpoints()[0] 266 | 267 | print("## TestInvalid (bind: bad params value)") 268 | ses = qi.Session() 269 | ses.connect(local) 270 | i = Invalid3() 271 | with pytest.raises(Exception): 272 | ses.registerService("Invalid3", i) 273 | finally: 274 | ses.close() 275 | sd.close() 276 | 277 | 278 | def test_cancelcall(): 279 | try: 280 | s = qi.Session() 281 | s.listenStandalone('tcp://127.0.0.1:0') 282 | f = s.waitForService("my", _async=True) 283 | f.cancel() 284 | f.wait() 285 | finally: 286 | s.close() 287 | -------------------------------------------------------------------------------- /qi/test/test_log.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2010 - 2020 Softbank Robotics Europe 3 | # 4 | # -*- coding: utf-8 -*- 5 | 6 | import qi 7 | import qi.logging 8 | 9 | 10 | def test_directlog(): 11 | qi.fatal("test.logger", "log fatal") 12 | qi.error("test.logger", "log error") 13 | qi.warning("test.logger", "log warning") 14 | qi.info("test.logger", "log info") 15 | qi.verbose("test.logger", "log verbose") 16 | 17 | qi.fatal("test.logger", "log fatal", 1) 18 | qi.error("test.logger", "log error", 1) 19 | qi.warning("test.logger", "log warning", 1) 20 | qi.info("test.logger", "log info", 1) 21 | qi.verbose("test.logger", "log verbose", 1) 22 | 23 | 24 | def test_loggingLevel(): 25 | logger = qi.logging.Logger("test.logging") 26 | qi.logging.setContext(254) 27 | qi.logging.setLevel(qi.logging.FATAL) 28 | logger.fatal("log fatal") 29 | logger.error("log error") 30 | logger.warning("log warning") 31 | logger.info("log info") 32 | 33 | logger.fatal("log fatal", 1) 34 | logger.error("log error", 1) 35 | logger.warning("log warning", 1) 36 | logger.info("log info", 1) 37 | # reset log level for other tests 38 | qi.logging.setLevel(qi.logging.INFO) 39 | 40 | 41 | def test_loggingFilters(): 42 | logger = qi.logging.Logger("test.logging") 43 | qi.logging.setContext(254) 44 | qi.logging.setFilters("+test*=2") 45 | logger.fatal("log fatal") 46 | logger.error("log error") 47 | logger.warning("log warning") 48 | logger.info("log info") 49 | 50 | logger.fatal("log fatal", 1) 51 | logger.error("log error", 1) 52 | logger.warning("log warning", 1) 53 | logger.info("log info", 1) 54 | -------------------------------------------------------------------------------- /qi/test/test_module.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2010 - 2020 Softbank Robotics Europe 3 | # 4 | # -*- coding: utf-8 -*- 5 | 6 | import qi 7 | import pytest 8 | 9 | 10 | def test_module(): 11 | mod = qi.module("moduletest") 12 | 13 | cat = mod.createObject("Cat", "truc") 14 | assert cat.meow(3) == 'meow' 15 | 16 | mouse = mod.createObject("Mouse") 17 | assert mouse.squeak() == 18 18 | 19 | session = qi.Session() 20 | session.listenStandalone('tcp://localhost:0') 21 | cat = mod.createObject("Cat", session) 22 | assert cat.meow(3) == 'meow' 23 | 24 | assert cat.cloneMe().meow(3) == 'meow' 25 | 26 | assert mod.call("lol") == 3 27 | 28 | 29 | def test_module_undef(): 30 | mod = qi.module("moduletest") 31 | 32 | with pytest.raises(AttributeError): 33 | mod.createObject("LOL") 34 | 35 | 36 | def test_module_service(): 37 | session = qi.Session() 38 | session.listenStandalone("tcp://localhost:0") 39 | 40 | session.loadServiceRename("moduletest.Cat", "", "truc") 41 | 42 | cat = session.service("Cat") 43 | assert cat.meow(3) == 'meow' 44 | 45 | def test_module_service_object_lifetime(): 46 | session = qi.Session() 47 | session.listenStandalone("tcp://localhost:0") 48 | session.loadServiceRename("moduletest.Cat", "", "truc") 49 | cat = session.service("Cat") 50 | 51 | # We use `del` to release the reference to an object, but there is no 52 | # guarantee that the interpreter will finalize the object right away. 53 | # In CPython, this is "mostly" guaranteed as long as it is the last 54 | # reference to the object and that there is no cyclic dependency of 55 | # reference anywhere that might hold the reference. 56 | # As there is no practical alternative, we consider that assuming the 57 | # object is finalized immediately is an acceptable hypothesis. 58 | 59 | # Purr has a property, Play has a signal, Sleep has none. 60 | sleep = cat.makeSleep() 61 | assert cat.nbSleep() == 1 62 | del sleep 63 | assert cat.nbSleep() == 0 64 | 65 | purr = cat.makePurr() 66 | assert cat.nbPurr() == 1 67 | del purr 68 | assert cat.nbPurr() == 0 69 | 70 | play = cat.makePlay() 71 | assert cat.nbPlay() == 1 72 | del play 73 | assert cat.nbPlay() == 0 74 | 75 | def test_object_bound_functions_arguments_conversion_does_not_leak(): 76 | session = qi.Session() 77 | session.listenStandalone("tcp://localhost:0") 78 | session.loadServiceRename("moduletest.Cat", "", "truc") 79 | cat = session.service("Cat") 80 | 81 | play = cat.makePlay() 82 | assert cat.nbPlay() == 1 83 | cat.order(play) 84 | assert cat.nbPlay() == 1 85 | del play 86 | assert cat.nbPlay() == 0 87 | 88 | def test_temporary_object_bound_properties_are_usable(): 89 | session = qi.Session() 90 | session.listenStandalone("tcp://localhost:0") 91 | session.loadServiceRename("moduletest.Cat", "", "truc") 92 | # The `volume` member is a bound property of the `Purr` object. 93 | # It should keep the object alive so that setting the property, which 94 | # requires accessing the object, does not fail. 95 | session.service("Cat").makePurr().volume.setValue(42) 96 | -------------------------------------------------------------------------------- /qi/test/test_property.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2010 - 2020 Softbank Robotics Europe 3 | # 4 | # -*- coding: utf-8 -*- 5 | 6 | import qi 7 | import time 8 | import pytest 9 | 10 | 11 | def test_set(): 12 | prop = qi.Property('i') 13 | prop.setValue(15) 14 | assert prop.value() == 15 15 | with pytest.raises(RuntimeError): 16 | prop.setValue("lol") 17 | 18 | 19 | def test_signal(): 20 | prop = qi.Property('i') 21 | test_signal.gotval = 0 22 | 23 | def cb(val): 24 | test_signal.gotval += val 25 | link = prop.connect(cb) 26 | prop.setValue(42) 27 | time.sleep(0.05) 28 | assert test_signal.gotval == 42 29 | prop.disconnect(link) 30 | prop.setValue(38) 31 | time.sleep(0.05) 32 | assert test_signal.gotval == 42 33 | 34 | 35 | @pytest.fixture() 36 | def property_service_fixture(): 37 | def run_property_service_fixture(signature): 38 | class PropObj: 39 | def __init__(self): 40 | self.prop = qi.Property(signature) 41 | 42 | class PropServiceFixture: 43 | def __init__(self, servSess, servObj, clientSess, clientObj): 44 | self.servSess = servSess 45 | self.servObj = servObj 46 | self.clientSess = clientSess 47 | self.clientObj = clientObj 48 | 49 | def assert_both_sides_eq(self, val): 50 | if val is None: 51 | assert self.servObj.prop.value() is None 52 | assert self.clientObj.prop.value() is None 53 | else: 54 | assert self.servObj.prop.value() == val 55 | assert self.clientObj.prop.value() == val 56 | 57 | servSess = qi.Session() 58 | servSess.listenStandalone('tcp://localhost:0') 59 | servObj = PropObj() 60 | servSess.registerService('Serv', servObj) 61 | 62 | clientSess = qi.Session() 63 | clientSess.connect(servSess.url()) 64 | clientObj = clientSess.service('Serv') 65 | return PropServiceFixture(servSess, servObj, clientSess, clientObj) 66 | return run_property_service_fixture 67 | 68 | 69 | def test_remote_property(property_service_fixture): 70 | fixture = property_service_fixture('s') 71 | servObj, clientObj = fixture.servObj, fixture.clientObj 72 | 73 | servObj.prop.setValue("rofl") 74 | fixture.assert_both_sides_eq("rofl") 75 | 76 | clientObj.prop.setValue("lol") 77 | fixture.assert_both_sides_eq("lol") 78 | 79 | test_remote_property.gotval = "" 80 | 81 | def cb(val): 82 | test_remote_property.gotval += val 83 | rlink = clientObj.prop.addCallback(cb) 84 | llink = servObj.prop.addCallback(cb) 85 | servObj.prop.setValue("ha") 86 | clientObj.prop.setValue("ha") 87 | time.sleep(0.05) 88 | assert test_remote_property.gotval == "hahahaha" 89 | clientObj.prop.disconnect(rlink) 90 | servObj.prop.disconnect(llink) 91 | servObj.prop.setValue("ha") 92 | clientObj.prop.setValue("ha") 93 | time.sleep(0.05) 94 | assert test_remote_property.gotval == "hahahaha" 95 | 96 | 97 | @pytest.mark.parametrize("signature, value", 98 | [('i', -42), 99 | ('+i', -42), 100 | ('I', 42), 101 | ('+I', 42), 102 | ('l', -42), 103 | ('+l', -42), 104 | ('L', 42), 105 | ('+L', 42), 106 | ('f', 3.14), 107 | ('+f', 3.14), 108 | ('d', 3.14), 109 | ('+d', 3.14), 110 | ('[i]', [4, 2]), 111 | ('+[i]', [4, 2]), 112 | ('{si}', {"a": 4, "b": 2}), 113 | ('+{si}', {"a": 4, "b": 2}), 114 | ('(ii)', (4, 2)), 115 | ('+(ii)', (4, 2))]) 116 | def test_optional_set_value(signature, value): 117 | prop = qi.Property(signature) 118 | prop.setValue(value) 119 | actual = prop.value() 120 | 121 | def isclose(a, b, rel_tol=1e-09, abs_tol=0.0): 122 | return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol) 123 | assert actual == value or isclose(actual, value, rel_tol=1e-6) 124 | 125 | 126 | def test_non_optional_cannot_be_none(): 127 | prop = qi.Property('i') 128 | assert prop.value() is not None 129 | with pytest.raises(RuntimeError): 130 | prop.setValue(None) 131 | 132 | 133 | def test_remote_optional_property(property_service_fixture): 134 | fixture = property_service_fixture('+s') 135 | servObj, clientObj = fixture.servObj, fixture.clientObj 136 | 137 | fixture.assert_both_sides_eq(None) 138 | 139 | servObj.prop.setValue("cookies") 140 | fixture.assert_both_sides_eq("cookies") 141 | 142 | servObj.prop.setValue(None) 143 | fixture.assert_both_sides_eq(None) 144 | 145 | clientObj.prop.setValue("muffins") 146 | fixture.assert_both_sides_eq("muffins") 147 | 148 | clientObj.prop.setValue(None) 149 | fixture.assert_both_sides_eq(None) 150 | -------------------------------------------------------------------------------- /qi/test/test_return_empty_object.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2010 - 2020 Softbank Robotics Europe 3 | # 4 | # -*- coding: utf-8 -*- 5 | 6 | import qi 7 | import logging 8 | 9 | logging.basicConfig(level=logging.INFO) 10 | 11 | 12 | def test_return_empty_object(session_to_process_service_endpoint): 13 | service_name, service_session = session_to_process_service_endpoint 14 | log = logging.getLogger("test_return_empty_object") 15 | obj = service_session.service(service_name) 16 | prom = qi.Promise() 17 | 18 | def callback(obj): 19 | try: 20 | log.info("object is valid: {}.".format(obj.isValid())) 21 | prom.setValue(not obj.isValid()) 22 | except Exception as err: 23 | prom.setError(str(err)) 24 | except: # noqa: E722 25 | prom.setError("Unknown exception.") 26 | raise 27 | obj.signal.connect(callback) 28 | obj.resetObject() 29 | obj.emit() 30 | assert prom.future().value() 31 | -------------------------------------------------------------------------------- /qi/test/test_session.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2010 - 2020 Softbank Robotics Europe 3 | # 4 | # -*- coding: utf-8 -*- 5 | 6 | import time 7 | import qi 8 | 9 | isconnected = False 10 | isdisconnected = False 11 | 12 | 13 | def test_session_make_does_not_crash(): 14 | qi.Session() 15 | 16 | 17 | def test_session_listen_then_close_does_not_crash_or_deadlock(): 18 | session = qi.Session() 19 | session.listenStandalone('tcp://127.0.0.1:0') 20 | session.close() 21 | 22 | 23 | def test_session_callbacks(): 24 | def callback_conn(): 25 | global isconnected 26 | isconnected = True 27 | print("Connected!") 28 | 29 | def callback_disconn(s): 30 | global isdisconnected 31 | isdisconnected = True 32 | print("Disconnected!") 33 | 34 | local = "tcp://127.0.0.1:0" 35 | sd = qi.Session() 36 | sd.listenStandalone(local) 37 | 38 | s = qi.Session() 39 | assert not s.isConnected() 40 | assert not isconnected 41 | s.connected.connect(callback_conn) 42 | s.disconnected.connect(callback_disconn) 43 | s.connect(sd.endpoints()[0]) 44 | time.sleep(0.01) 45 | 46 | assert s.isConnected() 47 | assert isconnected 48 | assert not isdisconnected 49 | 50 | s.close() 51 | time.sleep(0.01) 52 | 53 | assert isdisconnected 54 | -------------------------------------------------------------------------------- /qi/test/test_signal.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2010 - 2020 Softbank Robotics Europe 3 | # 4 | # -*- coding: utf-8 -*- 5 | 6 | import time 7 | import qi 8 | 9 | 10 | class subscriber: 11 | def __init__(self): 12 | self.done = False 13 | 14 | def callback(self): 15 | print("callback") 16 | self.done = True 17 | 18 | def callback_42(self, nb): 19 | print("callback_42:", nb) 20 | if (nb == 42): 21 | self.done = True 22 | 23 | def callback_5args(self, a, b, c, d, e): 24 | print("callback_5args:", a, b, c, d, e) 25 | self.done = True 26 | 27 | def wait(self): 28 | while not self.done: 29 | time.sleep(0.1) 30 | 31 | 32 | def test_signal(): 33 | print("\nInit...") 34 | sub1 = subscriber() 35 | sub2 = subscriber() 36 | mysignal = qi.Signal() 37 | 38 | print("\nTest #1 : Multiple subscribers to signal") 39 | callback = sub1.callback 40 | callback2 = sub2.callback 41 | mysignal.connect(callback) 42 | signalid = mysignal.connect(callback2) 43 | 44 | mysignal() 45 | sub2.wait() 46 | sub1.wait() 47 | 48 | print("\nTest #2 : Disconnect only one") 49 | mysignal.disconnect(signalid) 50 | 51 | sub1.done = False 52 | sub2.done = False 53 | mysignal() 54 | sub1.wait() 55 | 56 | print("\nTest #3 : Disconnect All") 57 | mysignal.connect(callback2) 58 | assert mysignal.disconnectAll() 59 | 60 | sub1.done = False 61 | sub2.done = False 62 | mysignal() 63 | time.sleep(0.5) 64 | 65 | assert not sub1.done 66 | assert not sub2.done 67 | 68 | print("\nTest #4 : Trigger with one parameter") 69 | mysignal.connect(sub1.callback_42) 70 | 71 | sub1.done = False 72 | sub2.done = False 73 | mysignal(42) 74 | sub1.wait() 75 | 76 | assert sub1.done 77 | assert not sub2.done 78 | 79 | assert mysignal.disconnectAll() 80 | print("\nTest #5 : Trigger with five parameters") 81 | mysignal.connect(sub1.callback_5args) 82 | 83 | sub1.done = False 84 | sub2.done = False 85 | mysignal(42, "hey", "a", 0.42, (0, 1)) 86 | sub1.wait() 87 | assert sub1.done 88 | assert not sub2.done 89 | 90 | 91 | lastSubs = False 92 | 93 | 94 | def test_subscribe(): 95 | global lastSubs 96 | lastSubs = False 97 | 98 | def onSubscriber(subs): 99 | global lastSubs 100 | lastSubs = subs 101 | sig = qi.Signal('m', onSubscriber) 102 | assert not lastSubs 103 | connlink = sig.connect(lambda x: None) 104 | assert lastSubs 105 | assert sig.disconnect(connlink) 106 | assert not lastSubs 107 | -------------------------------------------------------------------------------- /qi/test/test_strand.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2010 - 2020 Softbank Robotics Europe 3 | # 4 | # -*- coding: utf-8 -*- 5 | 6 | import qi 7 | import time 8 | from functools import partial 9 | 10 | FUTURE_WAIT_MS = 5000 # 5sec 11 | 12 | @qi.singleThreaded() 13 | class Stranded: 14 | def __init__(self, max): 15 | self.calling = False 16 | self.counter = 0 17 | self.max = max 18 | self.end = qi.Promise() 19 | 20 | def cb(self, val): 21 | assert not self.calling 22 | self.calling = True 23 | 24 | time.sleep(0.01) 25 | 26 | assert self.calling 27 | self.calling = False 28 | 29 | self.counter += 1 30 | 31 | if self.counter == self.max: 32 | self.end.setValue(None) 33 | 34 | def cbn(self, *args): 35 | self.cb(args[-1]) 36 | 37 | 38 | def test_future_strand(): 39 | obj = Stranded(30) 40 | 41 | prom = qi.Promise() 42 | for i in range(30): 43 | prom.future().addCallback(obj.cb) 44 | prom.setValue(None) 45 | 46 | state = obj.end.future().wait(FUTURE_WAIT_MS) 47 | assert state == qi.FutureState.FinishedWithValue 48 | 49 | 50 | def test_future_partials_strand(): 51 | obj = Stranded(50) 52 | 53 | prom = qi.Promise() 54 | for i in range(10): 55 | prom.future().addCallback(obj.cb) 56 | for i in range(10): 57 | prom.future().addCallback(partial(obj.cbn, 1)) 58 | for i in range(10): 59 | prom.future().addCallback(partial(partial(obj.cbn, 1), 2)) 60 | for i in range(10): 61 | prom.future().addCallback(partial(partial(Stranded.cbn, obj, 1), 2)) 62 | for i in range(10): 63 | prom.future().addCallback(partial(Stranded.cbn, obj, 1)) 64 | prom.setValue(None) 65 | 66 | state = obj.end.future().wait(FUTURE_WAIT_MS) 67 | assert state == qi.FutureState.FinishedWithValue 68 | 69 | 70 | def test_signal_strand(): 71 | obj = Stranded(30) 72 | 73 | sig = qi.Signal("m") 74 | for i in range(30): 75 | sig.connect(obj.cb) 76 | sig(None) 77 | 78 | state = obj.end.future().wait(FUTURE_WAIT_MS) 79 | assert state == qi.FutureState.FinishedWithValue 80 | 81 | 82 | def test_remote_signal_strand(): 83 | server = qi.Session() 84 | server.listenStandalone("tcp://localhost:0") 85 | client = qi.Session() 86 | client.connect(server.url()) 87 | 88 | obj = Stranded(30) 89 | 90 | class Serv: 91 | def __init__(self): 92 | self.sig = qi.Signal('m') 93 | s = Serv() 94 | server.registerService('Serv', s) 95 | 96 | sig = client.service('Serv').sig 97 | for i in range(30): 98 | sig.connect(obj.cb) 99 | s.sig(None) 100 | 101 | state = obj.end.future().wait(FUTURE_WAIT_MS) 102 | assert state == qi.FutureState.FinishedWithValue 103 | 104 | 105 | def test_property_strand(): 106 | obj = Stranded(30) 107 | 108 | prop = qi.Property("m") 109 | for i in range(30): 110 | prop.connect(obj.cb) 111 | prop.setValue(42) 112 | 113 | state = obj.end.future().wait(FUTURE_WAIT_MS) 114 | assert state == qi.FutureState.FinishedWithValue 115 | 116 | 117 | def test_remote_call_strand(): 118 | server = qi.Session() 119 | server.listenStandalone("tcp://localhost:0") 120 | client = qi.Session() 121 | client.connect(server.url()) 122 | 123 | obj = Stranded(50) 124 | 125 | server.registerService('Serv', obj) 126 | 127 | robj = client.service('Serv') 128 | for i in range(25): 129 | robj.cb(None, _async=True) 130 | for i in range(25): 131 | qi.runAsync(partial(obj.cb, None)) 132 | 133 | state = obj.end.future().wait(FUTURE_WAIT_MS) 134 | assert state == qi.FutureState.FinishedWithValue 135 | 136 | 137 | def test_remote_property_strand(): 138 | server = qi.Session() 139 | server.listenStandalone("tcp://localhost:0") 140 | client = qi.Session() 141 | client.connect(server.url()) 142 | 143 | obj = Stranded(30) 144 | 145 | class Serv: 146 | def __init__(self): 147 | self.prop = qi.Property('m') 148 | s = Serv() 149 | server.registerService('Serv', s) 150 | 151 | prop = client.service('Serv').prop 152 | for i in range(30): 153 | prop.connect(obj.cb) 154 | s.prop.setValue(42) 155 | 156 | state = obj.end.future().wait(FUTURE_WAIT_MS) 157 | assert state == qi.FutureState.FinishedWithValue 158 | 159 | 160 | def test_async_strand(): 161 | obj = Stranded(30) 162 | for i in range(30): 163 | qi.runAsync(partial(obj.cb, None)) 164 | 165 | state = obj.end.future().wait(FUTURE_WAIT_MS) 166 | assert state == qi.FutureState.FinishedWithValue 167 | 168 | # periodic task is tested only here as there is no point in testing it alone 169 | 170 | 171 | def test_all_strand(): 172 | obj = Stranded(81) 173 | 174 | sig = qi.Signal("m") 175 | prop = qi.Property("m") 176 | prom = qi.Promise() 177 | per = qi.PeriodicTask() 178 | per.setCallback(partial(obj.cb, None)) 179 | per.setUsPeriod(10000) 180 | for i in range(20): 181 | prom.future().addCallback(obj.cb) 182 | for i in range(20): 183 | sig.connect(obj.cb) 184 | for i in range(20): 185 | prop.connect(obj.cb) 186 | per.start(True) 187 | sig(None) 188 | prop.setValue(42) 189 | prom.setValue(None) 190 | for i in range(20): 191 | qi.runAsync(partial(obj.cb, None)) 192 | 193 | state = obj.end.future().wait(FUTURE_WAIT_MS) 194 | assert state == qi.FutureState.FinishedWithValue 195 | per.stop() 196 | -------------------------------------------------------------------------------- /qi/test/test_typespassing.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2010 - 2020 Softbank Robotics Europe 3 | # 4 | # -*- coding: utf-8 -*- 5 | 6 | import time 7 | import qi 8 | import pytest 9 | 10 | 11 | class TestService: 12 | def display(self, t): 13 | return t 14 | 15 | 16 | def test_throwing_callback(): 17 | def raising(f): 18 | raise Exception("woops") 19 | 20 | local = "tcp://127.0.0.1:0" 21 | sd = qi.Session() 22 | sd.listenStandalone(local) 23 | 24 | s = qi.Session() 25 | s.connect(sd.endpoints()[0]) 26 | f = s.service("ServiceDirectory", _async=True) 27 | 28 | f.addCallback(raising) 29 | time.sleep(0.01) 30 | s.close() 31 | 32 | 33 | def test_unicode_strings(): 34 | local = "tcp://127.0.0.1:0" 35 | sd = qi.Session() 36 | sd.listenStandalone(local) 37 | 38 | s = qi.Session() 39 | s.connect(sd.endpoints()[0]) 40 | 41 | m = TestService() 42 | s.registerService("TestService", m) 43 | service = s.service("TestService") 44 | # ASCII range 45 | unicode_string = ''.join([chr(i) for i in range(1, 128)]) 46 | mystring = service.display(unicode_string) 47 | print("mystr:", mystring) 48 | print("uystr:", unicode_string) 49 | assert type(mystring) == str 50 | assert mystring.encode("ascii") == unicode_string.encode("ascii") 51 | 52 | # Wide unicode 53 | wide_string = "\\U00010000" * 39 + "\\uffff" * 4096 54 | mystring = service.display(wide_string) 55 | assert mystring == wide_string 56 | 57 | # String with many unicode chars 58 | unicode_string = ''.join([chr(i) for i in range(1, 50000)]) 59 | service.display(unicode_string) 60 | time.sleep(0.01) 61 | s.close() 62 | 63 | 64 | def test_builtin_types(): 65 | local = "tcp://127.0.0.1:0" 66 | sd = qi.Session() 67 | sd.listenStandalone(local) 68 | 69 | s = qi.Session() 70 | s.connect(sd.endpoints()[0]) 71 | 72 | m = TestService() 73 | s.registerService("TestService", m) 74 | service = s.service("TestService") 75 | 76 | # None 77 | assert service.display(None) is None 78 | # bool 79 | t, f = service.display(True), service.display(False) 80 | assert t == 1 # is True ? 81 | assert f == 0 # is False ? 82 | 83 | # int 84 | integer_types = (int,) 85 | assert isinstance(service.display(42), integer_types) 86 | assert service.display(42) == 42 87 | 88 | # float 89 | assert service.display(0.1337) == 0.1337 90 | 91 | # long (32b) 92 | assert service.display(2 ** 31 - 1) == 2147483647 93 | 94 | # list 95 | assert service.display([]) == [] 96 | assert service.display([1]) == [1] 97 | assert service.display(["bla", "bli"]) == ["bla", "bli"] 98 | 99 | # sets 100 | assert service.display(set([1, 2])) == (1, 2) 101 | assert service.display(frozenset([1, 2])) == (1, 2) 102 | ret = service.display(frozenset([frozenset("a"), frozenset("b")])) 103 | assert ret == (("b",), ("a",)) or ret == (("a",), ("b",)) 104 | 105 | # tuple 106 | assert service.display(()) == () 107 | assert service.display((1)) == (1) 108 | assert service.display((1, 2)) == (1, 2) 109 | 110 | # dict 111 | assert service.display({}) == {} 112 | assert service.display({1: "bla", 3: []}) == {1: "bla", 3: []} 113 | 114 | # bytearray 115 | assert service.display(bytearray("lol", encoding="ascii")) == "lol" 116 | 117 | # buffer (not implemented) 118 | with pytest.raises(RuntimeError): 119 | service.display(memoryview("lol".encode())) 120 | 121 | time.sleep(0.01) 122 | s.close() 123 | 124 | 125 | def test_object_types(): 126 | local = "tcp://127.0.0.1:0" 127 | sd = qi.Session() 128 | sd.listenStandalone(local) 129 | 130 | s = qi.Session() 131 | s.connect(sd.endpoints()[0]) 132 | 133 | m = TestService() 134 | s.registerService("TestService", m) 135 | service = s.service("TestService") 136 | 137 | # new style 138 | class A(object): 139 | pass 140 | obj = A() 141 | 142 | service.display(A) 143 | service.display(obj) 144 | 145 | # old style 146 | class Aold: 147 | pass 148 | objold = Aold() 149 | 150 | try: 151 | service.display(Aold) 152 | except RuntimeError: 153 | pass 154 | 155 | service.display(objold) 156 | 157 | 158 | def test_qi_object_instance(): 159 | local = "tcp://127.0.0.1:0" 160 | sd = qi.Session() 161 | sd.listenStandalone(local) 162 | 163 | s = qi.Session() 164 | s.connect(sd.endpoints()[0]) 165 | 166 | m = s.service("ServiceDirectory") 167 | assert qi.typeof(m) == qi.Object 168 | assert qi.typeof(m) == qi.Object() 169 | assert qi.isinstance(m, qi.Object) 170 | assert qi.isinstance(m, qi.Object()) 171 | 172 | 173 | def test_type(): 174 | assert qi.Int8 == qi.Int8 175 | assert qi.Int8() == qi.Int8() 176 | assert qi.Int8 == qi.Int8() 177 | assert qi.Int8() == qi.Int8 178 | with pytest.raises(Exception): 179 | assert qi.Map != qi.Int8 180 | with pytest.raises(Exception): 181 | assert qi.List != qi.List(qi.Int8) 182 | assert qi.List(qi.Int8) == qi.List(qi.Int8) 183 | assert qi.List(qi.Int8()) == qi.List(qi.Int8()) 184 | assert qi.List(qi.Int8()) == qi.List(qi.Int8) 185 | assert qi.List(qi.Int8) == qi.List(qi.Int8()) 186 | with pytest.raises(Exception): 187 | assert qi.Optional != qi.Optional(qi.Int8) 188 | assert qi.Optional(qi.Int8) == qi.Optional(qi.Int8) 189 | assert qi.Optional(qi.Int8()) == qi.Optional(qi.Int8()) 190 | assert qi.Optional(qi.Int8()) == qi.Optional(qi.Int8) 191 | assert qi.Optional(qi.Int8) == qi.Optional(qi.Int8()) 192 | assert qi.Object != qi.Int8 193 | assert qi.Object != qi.Int8() 194 | assert qi.Object != qi.Int32() 195 | assert qi.Int8() != qi.UInt8() 196 | assert not (qi.Int8() != qi.Int8()) 197 | assert not (qi.Int8 != qi.Int8) 198 | assert not (qi.Int8 != qi.Int8()) 199 | assert not (qi.Int8() != qi.Int8) 200 | -------------------------------------------------------------------------------- /qi/translator.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2010 - 2020 Softbank Robotics Europe 3 | # 4 | # -*- coding: utf-8 -*- 5 | 6 | from .qi_python import Translator 7 | from .logging import warning 8 | 9 | __all__ = ("defaultTranslator", "tr") 10 | 11 | glob_translator = None 12 | 13 | 14 | def defaultTranslator(name): 15 | global glob_translator 16 | if glob_translator: 17 | return glob_translator 18 | glob_translator = Translator(name) 19 | return glob_translator 20 | 21 | 22 | def tr(msg, domain=None, locale=None): 23 | global glob_translator 24 | if not glob_translator: 25 | warning("qi.translator", "You must init your translator first.") 26 | return msg 27 | if domain is None: 28 | return glob_translator.translate(msg) 29 | if locale is None: 30 | return glob_translator.translate(msg, domain) 31 | return glob_translator.translate(msg, domain, locale) 32 | -------------------------------------------------------------------------------- /qipython/common.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #pragma once 7 | 8 | #ifndef QIPYTHON_COMMON_HPP 9 | #define QIPYTHON_COMMON_HPP 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | 23 | // MAJOR, MINOR and PATCH must be in [0, 255]. 24 | #define QI_PYBIND11_VERSION(MAJOR, MINOR, PATCH) \ 25 | (((MAJOR) << 16) | \ 26 | ((MINOR) << 8) | \ 27 | ((PATCH) << 0)) 28 | 29 | #ifdef PYBIND11_VERSION_HEX 30 | // Remove the lowest 8 bits which represent the serial and level version components. 31 | # define QI_CURRENT_PYBIND11_VERSION (PYBIND11_VERSION_HEX >> 8) 32 | #else 33 | # define QI_CURRENT_PYBIND11_VERSION \ 34 | QI_PYBIND11_VERSION(PYBIND11_VERSION_MAJOR, \ 35 | PYBIND11_VERSION_MINOR, \ 36 | PYBIND11_VERSION_PATCH) 37 | #endif 38 | 39 | namespace qi 40 | { 41 | namespace py 42 | { 43 | 44 | /// Returns the instance of the bound method if it exists, otherwise returns 45 | /// a `None` object. 46 | /// 47 | /// @pre `obj` is not a null object. 48 | /// @pre The GIL is locked. 49 | /// @post the returned value is not a null object (it evaluates to true). 50 | inline pybind11::object getMethodSelf(const pybind11::function& func) 51 | { 52 | QI_ASSERT_TRUE(func); 53 | if (PyMethod_Check(func.ptr())) 54 | return pybind11::reinterpret_borrow( 55 | PyMethod_GET_SELF(func.ptr())); 56 | return pybind11::none(); 57 | } 58 | 59 | /// Casts a value into a Python object, throwing an exception if the conversion 60 | /// fails. 61 | /// 62 | /// @pre The GIL is locked. 63 | /// @post the returned value is not a null object (it evaluates to true). 64 | // See https://github.com/pybind/pybind11/issues/2336 65 | // Casting does not throw if the type is unregistered, so we throw explicitly in 66 | // case of an error. 67 | // TODO: Remove this function and its uses when the issue is fixed. 68 | template>::value, 70 | int> = 0> 71 | pybind11::object castToPyObject(T&& t, Extra&&... extra) 72 | { 73 | auto obj = pybind11::cast(std::forward(t), std::forward(extra)...); 74 | if (PyErr_Occurred()) 75 | throw pybind11::error_already_set(); 76 | QI_ASSERT_TRUE(obj); 77 | return obj; 78 | } 79 | 80 | /// Extracts a keyword argument out of a dictionary of keywords arguments. 81 | /// 82 | /// If the argument is not in the dictionary, does nothing and returns an empty 83 | /// optional. 84 | /// 85 | /// If the argument is is the dictionary, it is removed from it and its value is 86 | /// then cast to a value of type `T` and an optional of this value is returned. 87 | /// 88 | /// If the argument has value `None`, an empty optional is returned unless 89 | /// `allowNone` is set to true, in which case the `None` value will be cast to 90 | /// a value of type `T`. 91 | /// 92 | /// @pre The GIL is locked. 93 | /// @warning If the value of the argument cannot be cast to type `T`, an 94 | /// exception is thrown but the argument is still removed from the 95 | /// dictionary. 96 | template 97 | boost::optional extractKeywordArg(pybind11::dict kwargs, 98 | const char* argName, 99 | bool allowNone = false) 100 | { 101 | if (!kwargs.contains(argName)) 102 | return {}; 103 | 104 | const pybind11::object pyArg = kwargs[argName]; 105 | PyDict_DelItemString(kwargs.ptr(), argName); 106 | 107 | if (pyArg.is_none() && !allowNone) 108 | return {}; 109 | 110 | return pyArg.cast(); 111 | } 112 | 113 | /// Returns whether or not the interpreter is finalizing. If the information 114 | /// could not be fetched, the function returns an empty optional. Otherwise, it 115 | /// returns an optional set with a boolean stating if the interpreter is indeed 116 | /// finalizing or not. 117 | inline boost::optional interpreterIsFinalizing() 118 | { 119 | // `_Py_IsFinalizing` is only available on CPython 3.7+ 120 | #if PY_VERSION_HEX >= 0x03070000 121 | return boost::make_optional(_Py_IsFinalizing() != 0); 122 | #else 123 | // There is no way of knowing on older versions. 124 | return {}; 125 | #endif 126 | } 127 | 128 | /// Acquires the GIL, then invokes a function with the given arguments, but 129 | /// catches any `pybind11::error_already_set` exception thrown during this 130 | /// invocation, and throws a `std::runtime_error` instead. This is useful to 131 | /// avoid the undefined behavior caused by the `what` member function of 132 | /// `pybind11::error_already_set` when the GIL is not acquired. 133 | template 134 | auto invokeCatchPythonError(F&& f, Args&&... args); 135 | 136 | /// Exception type thrown when an operation fails because the interpreter is 137 | /// finalizing. 138 | /// 139 | /// Some operations are forbidden during interpreter finalization as they may 140 | /// terminate the thread they are called from. This exception allows stopping 141 | /// the flow of execution in this case before such functions are called. 142 | struct InterpreterFinalizingException : std::exception 143 | { 144 | inline const char* what() const noexcept override 145 | { 146 | return "the interpreter is finalizing"; 147 | } 148 | }; 149 | 150 | } // namespace py 151 | } // namespace qi 152 | 153 | namespace pybind11 154 | { 155 | namespace detail 156 | { 157 | template <> 158 | struct type_caster 159 | { 160 | public: 161 | PYBIND11_TYPE_CASTER(qi::AnyReference, _("qi_python.AnyReference")); 162 | 163 | // Python -> C++ 164 | bool load(handle src, bool /* enable implicit conversions */) 165 | { 166 | QI_ASSERT_TRUE(src); 167 | value = ka::invoke_catch(ka::compose( 168 | [&](const std::string&) { 169 | currentObject = {}; 170 | return qi::AnyReference{}; 171 | }, 172 | ka::exception_message_t()), 173 | [&] { 174 | currentObject = 175 | reinterpret_borrow(src); 176 | return qi::py::unwrapAsRef(currentObject); 177 | }); 178 | return value.isValid(); 179 | } 180 | 181 | // C++ -> Python 182 | static handle cast(qi::AnyReference src, 183 | return_value_policy /* policy */, 184 | handle /* parent */) 185 | { 186 | return qi::py::unwrapValue(src).release(); 187 | } 188 | 189 | object currentObject; 190 | }; 191 | 192 | template <> 193 | struct type_caster 194 | { 195 | public: 196 | PYBIND11_TYPE_CASTER(qi::AnyValue, _("qi_python.AnyValue")); 197 | 198 | using RefCaster = type_caster; 199 | 200 | // Python -> C++ 201 | bool load(handle src, bool enableImplicitConv) 202 | { 203 | QI_ASSERT_TRUE(src); 204 | RefCaster refCaster; 205 | refCaster.load(src, enableImplicitConv); 206 | value.reset(std::move(refCaster)); 207 | return value.isValid(); 208 | } 209 | 210 | // C++ -> Python 211 | static handle cast(qi::AnyValue src, 212 | return_value_policy policy, 213 | handle parent) 214 | { 215 | return RefCaster::cast(src.asReference(), policy, parent); 216 | } 217 | }; 218 | 219 | template 220 | struct type_caster> : optional_caster> 221 | {}; 222 | 223 | } // namespace detail 224 | } // namespace pybind11 225 | 226 | PYBIND11_DECLARE_HOLDER_TYPE(T, boost::shared_ptr); 227 | 228 | #include 229 | 230 | #endif // QIPYTHON_COMMON_HPP 231 | -------------------------------------------------------------------------------- /qipython/common.hxx: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2023 Aldebaran Robotics 3 | ** See COPYING for the license 4 | */ 5 | 6 | #pragma once 7 | 8 | #ifndef QIPYTHON_COMMON_HXX 9 | #define QIPYTHON_COMMON_HXX 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | namespace qi 16 | { 17 | namespace py 18 | { 19 | 20 | template 21 | auto invokeCatchPythonError(F&& f, Args&&... args) 22 | { 23 | GILAcquire acquire; 24 | try 25 | { 26 | return std::invoke(std::forward(f), std::forward(args)...); 27 | } 28 | catch (const pybind11::error_already_set& err) 29 | { 30 | throw std::runtime_error(err.what()); 31 | } 32 | } 33 | 34 | } // namespace py 35 | } // namespace qi 36 | 37 | #endif // QIPYTHON_COMMON_HXX 38 | -------------------------------------------------------------------------------- /qipython/pyapplication.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #pragma once 7 | 8 | #ifndef QIPYTHON_PYAPPLICATION_HPP 9 | #define QIPYTHON_PYAPPLICATION_HPP 10 | 11 | #include 12 | 13 | namespace qi 14 | { 15 | namespace py 16 | { 17 | 18 | void exportApplication(pybind11::module& m); 19 | 20 | } 21 | } 22 | 23 | #endif // QIPYTHON_PYAPPLICATION_HPP 24 | -------------------------------------------------------------------------------- /qipython/pyasync.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #pragma once 7 | 8 | #ifndef QIPYTHON_PYASYNC_HPP 9 | #define QIPYTHON_PYASYNC_HPP 10 | 11 | #include 12 | 13 | namespace qi 14 | { 15 | namespace py 16 | { 17 | 18 | void exportAsync(pybind11::module& module); 19 | 20 | } 21 | } 22 | 23 | #endif // QIPYTHON_PYASYNC_HPP 24 | -------------------------------------------------------------------------------- /qipython/pyclock.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #pragma once 7 | 8 | #ifndef QIPYTHON_PYCLOCK_HPP 9 | #define QIPYTHON_PYCLOCK_HPP 10 | 11 | #include 12 | 13 | namespace qi 14 | { 15 | namespace py 16 | { 17 | 18 | void exportClock(pybind11::module& module); 19 | 20 | } // namespace py 21 | } // namespace qi 22 | 23 | #endif // QIPYTHON_PYCLOCK_HPP 24 | -------------------------------------------------------------------------------- /qipython/pyexport.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #pragma once 7 | 8 | #ifndef QIPYTHON_PYEXPORT_HPP 9 | #define QIPYTHON_PYEXPORT_HPP 10 | 11 | #include 12 | 13 | namespace qi 14 | { 15 | namespace py 16 | { 17 | void exportAll(pybind11::module& module); 18 | } 19 | } 20 | 21 | #endif // QIPYTHON_PYEXPORT_HPP 22 | -------------------------------------------------------------------------------- /qipython/pyfuture.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #pragma once 7 | 8 | #ifndef QIPYTHON_PYFUTURE_HPP 9 | #define QIPYTHON_PYFUTURE_HPP 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace qi 17 | { 18 | namespace py 19 | { 20 | 21 | using Future = qi::Future; 22 | using Promise = qi::Promise; 23 | 24 | // Depending on whether we want an asynchronous result or not, returns the 25 | // future or its value. 26 | inline pybind11::object resultObject(const Future& fut, bool async) 27 | { 28 | GILAcquire lock; 29 | if (async) 30 | return castToPyObject(fut); 31 | 32 | // Wait for the future outside of the GIL. 33 | auto res = invokeGuarded(qi::SrcFuture{}, fut); 34 | return castToPyObject(res); 35 | } 36 | 37 | inline Future toFuture(qi::Future f) 38 | { 39 | return f.andThen(FutureCallbackType_Sync, 40 | [](void*) { return AnyValue::makeVoid(); }); 41 | } 42 | 43 | inline Future toFuture(qi::Future f) { return f; } 44 | inline Future toFuture(qi::Future f) 45 | { 46 | return f.andThen(FutureCallbackType_Sync, 47 | [](const AnyReference& ref) { return AnyValue(ref); }); 48 | } 49 | 50 | template 51 | inline Future toFuture(qi::Future f) 52 | { 53 | return f.andThen(FutureCallbackType_Sync, 54 | [](const T& val) { return AnyValue::from(val); }); 55 | } 56 | 57 | void exportFuture(pybind11::module& module); 58 | 59 | } // namespace py 60 | } // namespace qi 61 | 62 | #endif // QIPYTHON_PYFUTURE_HPP 63 | -------------------------------------------------------------------------------- /qipython/pyguard.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #pragma once 7 | 8 | #ifndef QIPYTHON_GUARD_HPP 9 | #define QIPYTHON_GUARD_HPP 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace qi 17 | { 18 | namespace py 19 | { 20 | 21 | /// Returns whether or not the GIL is currently held by the current thread. If 22 | /// the interpreter is not yet initialized or has been finalized, returns an 23 | /// empty optional, as there is no GIL available. 24 | inline boost::optional currentThreadHoldsGil() 25 | { 26 | // PyGILState_Check() returns 1 (success) before the creation of the GIL and 27 | // after the destruction of the GIL. 28 | if (Py_IsInitialized() == 1) { 29 | const auto gilAcquired = PyGILState_Check() == 1; 30 | return boost::make_optional(gilAcquired); 31 | } 32 | return boost::none; 33 | } 34 | 35 | /// Returns whether or not the GIL exists (i.e the interpreter is initialized 36 | /// and not finalizing) and is currently held by the current thread. 37 | inline bool gilExistsAndCurrentThreadHoldsIt() 38 | { 39 | return currentThreadHoldsGil().value_or(false); 40 | } 41 | 42 | /// DefaultConstructible Guard 43 | /// Procedure<_ (Args)> F 44 | template 45 | ka::ResultOf invokeGuarded(F&& f, Args&&... args) 46 | { 47 | Guard g; 48 | return std::forward(f)(std::forward(args)...); 49 | } 50 | 51 | namespace detail 52 | { 53 | 54 | /// G == pybind11::gil_scoped_acquire || G == pybind11::gil_scoped_release 55 | template 56 | void pybind11GuardDisarm(G& guard) 57 | { 58 | QI_IGNORE_UNUSED(guard); 59 | // The disarm API was introduced in v2.6.2. 60 | #if QI_CURRENT_PYBIND11_VERSION >= QI_PYBIND11_VERSION(2,6,2) 61 | guard.disarm(); 62 | #endif 63 | } 64 | 65 | } // namespace detail 66 | 67 | /// RAII utility type that guarantees that the GIL is locked for the scope of 68 | /// the lifetime of the object. If the GIL cannot be acquired (for example, 69 | /// because the interpreter is finalizing), throws an `InterpreterFinalizingException` 70 | /// exception. 71 | /// 72 | /// Objects of this type (or objects composed of them) must not be kept alive 73 | /// after the hand is given back to the interpreter. 74 | /// 75 | /// This type is re-entrant. 76 | /// 77 | /// postcondition: `GILAcquire acq;` establishes `gilExistsAndCurrentThreadHoldsIt()` 78 | struct GILAcquire 79 | { 80 | inline GILAcquire() 81 | { 82 | if (gilExistsAndCurrentThreadHoldsIt()) 83 | return; 84 | 85 | const auto isFinalizing = interpreterIsFinalizing().value_or(false); 86 | if (isFinalizing) 87 | throw InterpreterFinalizingException(); 88 | 89 | _state = ::PyGILState_Ensure(); 90 | QI_ASSERT(gilExistsAndCurrentThreadHoldsIt()); 91 | } 92 | 93 | inline ~GILAcquire() 94 | { 95 | // Even if releasing the GIL while the interpreter is finalizing is allowed, it does 96 | // require the GIL to be currently held. But we have no guarantee that this is the case, 97 | // because the GIL may have been released since we acquired it, and we could not 98 | // reacquire it after that (maybe the interpreter is in fact finalizing). 99 | // Therefore, only release the GIL if it is currently held by this thread. 100 | if (_state && gilExistsAndCurrentThreadHoldsIt()) 101 | ::PyGILState_Release(*_state); 102 | } 103 | 104 | 105 | GILAcquire(const GILAcquire&) = delete; 106 | GILAcquire& operator=(const GILAcquire&) = delete; 107 | 108 | private: 109 | boost::optional _state; 110 | }; 111 | 112 | /// RAII utility type that (as a best effort) tries to ensure that the GIL is 113 | /// unlocked for the scope of the lifetime of the object. 114 | /// 115 | /// Objects of this type (or objects composed of them) must not be kept alive 116 | /// after the hand is given back to the interpreter. 117 | /// 118 | /// This type is re-entrant. 119 | /// 120 | /// postcondition: `GILRelease rel;` establishes 121 | /// `interpreterIsFinalizing().value_or(false) || 122 | /// !gilExistsAndCurrentThreadHoldsIt()` 123 | struct GILRelease 124 | { 125 | inline GILRelease() 126 | { 127 | // Even if releasing the GIL while the interpreter is finalizing is allowed, 128 | // it does require the GIL to be currently held. However, reacquiring the 129 | // GIL is forbidden if finalization started. 130 | // 131 | // It may happen that we try to release the GIL from a function ('F') called 132 | // by the Python interpreter, while it is holding the GIL. In this case, if 133 | // we try to release it, then fail to reacquire it and return the hand to 134 | // the interpreter, the interpreter will most likely terminate the process, 135 | // as it expects to still be holding the GIL. Failure to reacquire the GIL 136 | // can only happen if the interpreter started finalization, either before we 137 | // released it, or while it was released. 138 | // 139 | // Fortunately, in this case, because the interpreter is busy executing the 140 | // function 'F', it is not possible for it to start finalization until 'F' 141 | // returns. This means that the only possible reason of failure of 142 | // reacquisition of the GIL is because the interpreter was finalizing 143 | // *before* calling 'F', therefore before we released to GIL. Therefore, to 144 | // prevent this failure, we check if the interpreter is finalizing before 145 | // releasing the GIL, and if it is, we do nothing, and the GIL stays held. 146 | const auto isFinalizing = interpreterIsFinalizing().value_or(false); 147 | if (!isFinalizing && gilExistsAndCurrentThreadHoldsIt()) 148 | _release.emplace(); 149 | QI_ASSERT(isFinalizing || !gilExistsAndCurrentThreadHoldsIt()); 150 | } 151 | 152 | inline ~GILRelease() 153 | { 154 | // Reacquiring the GIL is forbidden when the interpreter is finalizing, as 155 | // it may terminate the current thread. 156 | const auto isFinalizing = interpreterIsFinalizing().value_or(false); 157 | if (_release && isFinalizing) 158 | detail::pybind11GuardDisarm(*_release); 159 | } 160 | 161 | GILRelease(const GILRelease&) = delete; 162 | GILRelease& operator=(const GILRelease&) = delete; 163 | 164 | private: 165 | boost::optional _release; 166 | }; 167 | 168 | /// Wraps a Python object as a shared reference-counted value that does not 169 | /// require the GIL to copy, move or assign to. 170 | /// 171 | /// On destruction, it releases the object with the GIL acquired. However, if 172 | /// the GIL cannot be acquired, the object is leaked. This is most likely 173 | /// mitigated by the fact that if the GIL is not available, it means the object 174 | /// already has been or soon will be garbage collected by interpreter 175 | /// finalization. 176 | template 177 | class SharedObject 178 | { 179 | static_assert(std::is_base_of_v, 180 | "template parameter T must be a subclass of pybind11::object"); 181 | 182 | struct State 183 | { 184 | std::mutex mutex; 185 | T object; 186 | }; 187 | std::shared_ptr _state; 188 | 189 | struct StateDeleter 190 | { 191 | inline void operator()(State* state) const 192 | { 193 | auto handle = state->object.release(); 194 | delete state; 195 | 196 | // Do not lock the GIL if there is nothing to release. 197 | if (!handle) 198 | return; 199 | 200 | try 201 | { 202 | GILAcquire acquire; 203 | handle.dec_ref(); 204 | } 205 | catch (const qi::py::InterpreterFinalizingException&) 206 | { 207 | // Nothing, the interpreter is finalizing. 208 | } 209 | } 210 | }; 211 | 212 | public: 213 | SharedObject() = default; 214 | 215 | inline explicit SharedObject(T object) 216 | : _state(new State { std::mutex(), std::move(object) }, StateDeleter()) 217 | { 218 | } 219 | 220 | /// Copies the inner Python object value by incrementing its reference count. 221 | /// 222 | /// @pre: If the inner value is not null, the GIL must be acquired. 223 | T inner() const 224 | { 225 | QI_ASSERT_NOT_NULL(_state); 226 | std::scoped_lock lock(_state->mutex); 227 | return _state->object; 228 | } 229 | 230 | /// Takes the inner Python object value and leaves a null value in its place. 231 | /// 232 | /// Any copy of the shared object will now have a null inner value. 233 | /// 234 | /// This operation does not require the GIL as the reference count of the 235 | /// object is preserved. 236 | T takeInner() 237 | { 238 | QI_ASSERT_NOT_NULL(_state); 239 | std::scoped_lock lock(_state->mutex); 240 | /// Hypothesis: Moving a `pybind11::object` steals the object and preserves 241 | /// its reference count and therefore does not require the GIL. 242 | return ka::exchange(_state->object, {}); 243 | } 244 | }; 245 | 246 | /// Deleter that deletes the pointer outside the GIL. 247 | /// 248 | /// Useful for types that might deadlock on destruction if they keep the GIL 249 | /// locked. 250 | struct DeleteOutsideGIL 251 | { 252 | template 253 | void operator()(T* ptr) const 254 | { 255 | GILRelease unlock; 256 | delete ptr; 257 | } 258 | }; 259 | 260 | } // namespace py 261 | } // namespace qi 262 | 263 | #endif // QIPYTHON_GUARD_HPP 264 | -------------------------------------------------------------------------------- /qipython/pylog.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #pragma once 7 | 8 | #ifndef QIPYTHON_PYLOG_HPP 9 | #define QIPYTHON_PYLOG_HPP 10 | 11 | #include 12 | 13 | namespace qi 14 | { 15 | namespace py 16 | { 17 | 18 | void exportLog(pybind11::module& module); 19 | 20 | } 21 | } // namespace qi 22 | 23 | #endif // QIPYTHON_PYLOG_HPP 24 | -------------------------------------------------------------------------------- /qipython/pymodule.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #pragma once 7 | 8 | #ifndef QIPYTHON_PYOBJECTFACTORY_HPP 9 | #define QIPYTHON_PYOBJECTFACTORY_HPP 10 | 11 | #include 12 | 13 | namespace qi 14 | { 15 | namespace py 16 | { 17 | 18 | void exportObjectFactory(pybind11::module& m); 19 | 20 | } 21 | } 22 | 23 | #endif // QIPYTHON_PYOBJECTFACTORY_HPP 24 | -------------------------------------------------------------------------------- /qipython/pyobject.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #pragma once 7 | 8 | #ifndef QIPYTHON_PYOBJECT_HPP 9 | #define QIPYTHON_PYOBJECT_HPP 10 | 11 | #include 12 | #include 13 | 14 | namespace qi 15 | { 16 | namespace py 17 | { 18 | 19 | namespace detail 20 | { 21 | 22 | boost::optional readObjectUid(const pybind11::object& obj); 23 | 24 | /// @invariant `(writeObjectUid(obj, uid), *readObjectUid(obj)) == uid` 25 | void writeObjectUid(const pybind11::object& obj, const ObjectUid& uid); 26 | 27 | } // namespace detail 28 | 29 | using Object = qi::AnyObject; 30 | 31 | /// @post the returned value is not a null object (it evaluates to true). 32 | pybind11::object toPyObject(Object obj); 33 | 34 | /// Converts a Python object into a qi Object. 35 | /// 36 | /// If the Python object is already a wrapper around a C++ type that is known 37 | /// to be registered as an Object type in the qi type system, it is converted 38 | /// into that type and then into a qi `AnyObject`. 39 | /// 40 | /// Otherwise, a dynamic qi Object is created by introspection of the member 41 | /// attributes of the Python object. 42 | /// 43 | /// Member attributes may have an explicitly associated signature and/or return 44 | /// signature set as their own attributes. The name can also be set as their 45 | /// attribute in which case it overrides the member attribute name. 46 | /// 47 | /// If the signature has some special value that reflects the intention of 48 | /// the user to not bind this member, the member is ignored. 49 | /// 50 | /// The member might be a callable (method or function). It is then exposed 51 | /// as a method of the `qi::Object` using the signature and return signatures 52 | /// either deduced from the function object or from its attributes. 53 | /// Functions that have a name starting with two underscores are considered 54 | /// private and therefore ignored. 55 | /// 56 | /// The member might also be a Property or a Signal, in which case it is 57 | /// exposed as such in the `qi::Object`, but its signature is always 58 | /// dynamic (its attributes are ignored). 59 | /// 60 | /// Furthermore, the function will ensure that any object UID existing in the 61 | /// Python object is set in the resulting dynamic `qi::Object`. 62 | Object toObject(const pybind11::object& obj); 63 | 64 | void exportObject(pybind11::module& module); 65 | 66 | } 67 | } 68 | 69 | #endif // QIPYTHON_PYOBJECT_HPP 70 | -------------------------------------------------------------------------------- /qipython/pypath.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #pragma once 7 | 8 | #ifndef QIPYTHON_QIPATH_HPP 9 | #define QIPYTHON_QIPATH_HPP 10 | 11 | #include 12 | 13 | namespace qi 14 | { 15 | namespace py 16 | { 17 | 18 | void exportPath(pybind11::module& module); 19 | 20 | } 21 | } 22 | 23 | #endif // QIPYTHON_QIPATH_HPP 24 | -------------------------------------------------------------------------------- /qipython/pyproperty.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #pragma once 7 | 8 | #ifndef QIPYTHON_PYPROPERTY_HPP 9 | #define QIPYTHON_PYPROPERTY_HPP 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace qi 17 | { 18 | namespace py 19 | { 20 | 21 | using Property = qi::GenericProperty; 22 | 23 | namespace detail 24 | { 25 | struct ProxyProperty 26 | { 27 | AnyObject object; 28 | unsigned int propertyId; 29 | 30 | ~ProxyProperty(); 31 | }; 32 | } 33 | 34 | /// Checks if an object is an instance of a property. 35 | /// 36 | /// Unfortunately, as property types are a bit messy in libqi, we don't have 37 | /// an easy way to test that an object is an instance of a Property. This 38 | /// function tries to hide the details of the underlying types. 39 | bool isProperty(const pybind11::object& obj); 40 | 41 | void exportProperty(pybind11::module& module); 42 | 43 | } 44 | } 45 | 46 | #endif // QIPYTHON_PYPROPERTY_HPP 47 | -------------------------------------------------------------------------------- /qipython/pysession.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #pragma once 7 | 8 | #ifndef QIPYTHON_PYSESSION_HPP 9 | #define QIPYTHON_PYSESSION_HPP 10 | 11 | #include 12 | #include 13 | 14 | namespace qi 15 | { 16 | namespace py 17 | { 18 | 19 | pybind11::object makeSession(SessionPtr sess); 20 | 21 | void exportSession(pybind11::module& module); 22 | 23 | } // namespace py 24 | } // namespace qi 25 | 26 | #endif // QIPYTHON_PYSESSION_HPP 27 | -------------------------------------------------------------------------------- /qipython/pysignal.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #pragma once 7 | 8 | #ifndef QIPYTHON_PYSIGNAL_HPP 9 | #define QIPYTHON_PYSIGNAL_HPP 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | namespace qi 18 | { 19 | namespace py 20 | { 21 | 22 | using Signal = qi::SignalBase; 23 | using SignalPtr = std::shared_ptr; 24 | 25 | namespace detail 26 | { 27 | 28 | struct ProxySignal 29 | { 30 | AnyObject object; 31 | unsigned int signalId; 32 | 33 | ~ProxySignal(); 34 | }; 35 | 36 | pybind11::object signalConnect(SignalBase& sig, 37 | const pybind11::function& pyCallback, 38 | bool async); 39 | 40 | pybind11::object signalDisconnect(SignalBase& sig, SignalLink id, bool async); 41 | 42 | pybind11::object signalDisconnectAll(SignalBase& sig, bool async); 43 | 44 | pybind11::object proxySignalConnect(const AnyObject& obj, 45 | unsigned int signalId, 46 | const pybind11::function& callback, 47 | bool async); 48 | 49 | pybind11::object proxySignalDisconnect(const AnyObject& obj, 50 | SignalLink id, 51 | bool async); 52 | } // namespace detail 53 | 54 | // Checks if an object is an instance of a signal. 55 | bool isSignal(const pybind11::object& obj); 56 | 57 | void exportSignal(pybind11::module& m); 58 | 59 | } // namespace py 60 | } // namespace qi 61 | 62 | #endif // QIPYTHON_PYSIGNAL_HPP 63 | -------------------------------------------------------------------------------- /qipython/pystrand.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #pragma once 7 | 8 | #ifndef QIPYTHON_PYSTRAND_HPP 9 | #define QIPYTHON_PYSTRAND_HPP 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | namespace qi 16 | { 17 | namespace py 18 | { 19 | 20 | using StrandPtr = boost::shared_ptr; 21 | 22 | /// Returns the strand for the given function. 23 | /// 24 | /// If the function is bound to an instance, returns the strand of that object. 25 | /// 26 | /// @pre `func` is not a null object. 27 | StrandPtr strandOfFunction(const pybind11::function& func); 28 | 29 | /// Returns the strand associated to an object. 30 | /// 31 | /// If the object is `None`, returns nullptr. 32 | /// If the object is tagged as multithreaded (see the `isMultithreaded` 33 | /// function), returns nullptr. 34 | /// 35 | /// If the object already has a strand associated to it, returns it. Otherwise 36 | /// the function tries to create a new one and associate it to the object by 37 | /// setting it as an attribute of the object. If new attributes cannot be set on 38 | /// the object, returns nullptr. 39 | /// 40 | /// @pre `object` is not a null object. 41 | StrandPtr strandOf(const pybind11::object& obj); 42 | 43 | /// Returns true if the object was declared as multithreaded, false otherwise. 44 | /// 45 | /// An object is multithreaded only if it is declared as such (by having an attribute 46 | /// `__qi_threading__` set to `multi`. In any other case it is, either explicitly or implicitly, 47 | /// singlethreaded. 48 | /// 49 | /// @pre `obj` is not a null object. 50 | bool isMultithreaded(const pybind11::object& obj); 51 | 52 | void exportStrand(pybind11::module& module); 53 | 54 | } // namespace py 55 | } // namespace qi 56 | 57 | #endif // QIPYTHON_PYSTRAND_HPP 58 | -------------------------------------------------------------------------------- /qipython/pytranslator.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #pragma once 7 | 8 | #ifndef QIPYTHON_PYTRANSLATOR_HPP 9 | #define QIPYTHON_PYTRANSLATOR_HPP 10 | 11 | #include 12 | #include 13 | 14 | namespace qi 15 | { 16 | namespace py 17 | { 18 | 19 | void exportTranslator(pybind11::module& module); 20 | 21 | } // namespace py 22 | } // namespace qi 23 | 24 | #endif // QIPYTHON_PYTRANSLATOR_HPP 25 | -------------------------------------------------------------------------------- /qipython/pytypes.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #pragma once 7 | 8 | #ifndef QIPYTHON_PYTYPES_HPP 9 | #define QIPYTHON_PYTYPES_HPP 10 | 11 | #include 12 | #include 13 | 14 | namespace qi 15 | { 16 | namespace py 17 | { 18 | 19 | pybind11::object unwrapValue(AnyReference val); 20 | 21 | /// Introspects a Python object to create a `qi::AnyReference` around its value 22 | /// with the corresponding type. 23 | /// 24 | /// With 'exactly a T' meaning that the object's type is the type T and not one 25 | /// of its subclasses, (note that bool cannot be used as a base type), the type 26 | /// kind of the qi type of the result depends whether the object is: 27 | /// - `None`: TypeKind_Void. 28 | /// - exactly an integer: TypeKind_Int (the size returned by the type 29 | /// interface is `sizeof(long long)`). 30 | /// - exactly a float: TypeKind_Float (the size returned by the type 31 | /// interface is `sizeof(double)`). 32 | /// - exactly a bool: TypeKind_Int (the size returned by the type interface 33 | /// is 0). 34 | /// - exactly a unicode string: TypeKind_Str. 35 | /// - exactly a byte array or bytes: TypeKind_Str. 36 | /// - exactly a tuple: TypeKind_Tuple. 37 | /// - exactly a set (or frozenset): TypeKind_Tuple. 38 | /// - exactly a list: TypeKind_List. 39 | /// - exactly a dict: TypeKind_Map. 40 | /// 41 | /// The function will throw an exception if the object is: 42 | /// - an ellipsis. 43 | /// - exactly a complex. 44 | /// - a memoryview. 45 | /// - a slice. 46 | /// - a module. 47 | /// 48 | /// If the object type is none of the supported or unsupported types, it is 49 | /// introspected and a `qi::py::Object` is constructed out of it by executing 50 | /// `qi::py::toObject`. 51 | /// 52 | /// @pre `obj` 53 | AnyReference unwrapAsRef(pybind11::object& obj); 54 | 55 | void registerTypes(); 56 | 57 | } 58 | } 59 | 60 | #endif // QIPYTHON_PYTYPES_HPP 61 | -------------------------------------------------------------------------------- /src/module.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 Softbank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace py = pybind11; 12 | 13 | PYBIND11_MODULE(qi_python, module) 14 | { 15 | py::options options; 16 | options.enable_user_defined_docstrings(); 17 | 18 | module.doc() = "LibQi bindings for Python."; 19 | qi::py::exportAll(module); 20 | } 21 | -------------------------------------------------------------------------------- /src/pyapplication.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | qiLogCategory("qi.python.application"); 23 | 24 | namespace py = pybind11; 25 | 26 | namespace qi 27 | { 28 | namespace py 29 | { 30 | 31 | namespace 32 | { 33 | 34 | template 35 | struct WithArgcArgv 36 | { 37 | F f; 38 | 39 | auto operator()(::py::list args, ArgsExtras... extras) const 40 | -> decltype(f(std::declval(), 41 | std::declval(), 42 | std::forward(extras)...)) 43 | { 44 | std::vector argsStr; 45 | std::transform(args.begin(), args.end(), std::back_inserter(argsStr), 46 | [](const ::py::handle& arg){ return arg.cast(); }); 47 | 48 | std::vector argsCStr; 49 | std::transform(argsStr.begin(), argsStr.end(), std::back_inserter(argsCStr), 50 | [](const std::string& s) { return const_cast(s.c_str()); }); 51 | 52 | int argc = argsCStr.size(); 53 | char** argv = argsCStr.data(); 54 | 55 | auto res = f(argc, argv, std::forward(extras)...); 56 | 57 | // The constructor of `qi::Application` modifies its parameters to 58 | // consume the arguments it processed. After the constructor, the first 59 | // `argc` elements of `argv` are the arguments that were not recognized by 60 | // the application. We then have to propagate the consumption of the 61 | // elements to Python, by clearing the list then reinserting the elements 62 | // that were left. 63 | args.attr("clear")(); 64 | for (std::size_t i = 0; i < static_cast(argc); ++i) 65 | args.insert(i, argv[i]); 66 | 67 | return res; 68 | } 69 | }; 70 | 71 | template 72 | WithArgcArgv, ExtraArgs...> withArgcArgv(F&& f) 73 | { 74 | return { std::forward(f) }; 75 | } 76 | 77 | } // namespace 78 | 79 | void exportApplication(::py::module& m) 80 | { 81 | using namespace ::py; 82 | using namespace ::py::literals; 83 | 84 | GILAcquire lock; 85 | 86 | class_( 87 | m, "Application") 88 | .def(init(withArgcArgv<>([](int& argc, char**& argv) { 89 | GILRelease unlock; 90 | return new Application(argc, argv); 91 | })), 92 | "args"_a) 93 | .def_static("run", &Application::run, call_guard()) 94 | .def_static("stop", &Application::stop, call_guard()); 95 | 96 | class_( 97 | m, "ApplicationSession") 98 | 99 | .def(init(withArgcArgv( 100 | [](int& argc, char**& argv, bool autoExit, const std::string& url) { 101 | GILRelease unlock; 102 | ApplicationSession::Config config; 103 | if (!autoExit) 104 | config.setOption(qi::ApplicationSession::Option_NoAutoExit); 105 | if (!url.empty()) 106 | config.setConnectUrl(url); 107 | return new ApplicationSession(argc, argv, config); 108 | })), 109 | "args"_a, "autoExit"_a, "url"_a) 110 | 111 | .def("run", &ApplicationSession::run, call_guard(), 112 | doc("Block until the end of the program (call " 113 | ":py:func:`qi.ApplicationSession.stop` to end the program).")) 114 | 115 | .def_static("stop", &ApplicationSession::stop, 116 | call_guard(), 117 | doc( 118 | "Ask the application to stop, the run function will return.")) 119 | 120 | .def("start", &ApplicationSession::startSession, 121 | call_guard(), 122 | doc("Start the connection of the session, once this function is " 123 | "called everything is fully initialized and working.")) 124 | 125 | .def_static("atRun", &ApplicationSession::atRun, 126 | call_guard(), "func"_a, 127 | doc( 128 | "Add a callback that will be executed when run() is called.")) 129 | 130 | .def_property_readonly("url", 131 | [](const ApplicationSession& app) { 132 | return app.url().str(); 133 | }, 134 | call_guard(), 135 | doc("The url given to the Application. It's the url " 136 | "used to connect the session.")) 137 | 138 | .def_property_readonly("session", 139 | [](const ApplicationSession& app) { 140 | return makeSession(app.session()); 141 | }, 142 | doc("The session associated to the application.")); 143 | } 144 | 145 | } // namespace py 146 | } // namespace qi 147 | -------------------------------------------------------------------------------- /src/pyasync.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | namespace py = pybind11; 18 | 19 | namespace qi 20 | { 21 | namespace py 22 | { 23 | 24 | namespace 25 | { 26 | 27 | constexpr const auto delayArgName = "delay"; 28 | 29 | ::py::object async(::py::function pyCallback, 30 | ::py::args args, 31 | ::py::kwargs kwargs) 32 | { 33 | GILAcquire lock; 34 | 35 | qi::uint64_t usDelay = 0; 36 | if (const auto optUsDelay = extractKeywordArg(kwargs, delayArgName)) 37 | usDelay = *optUsDelay; 38 | 39 | const MicroSeconds delay(usDelay); 40 | 41 | SharedObject sharedCb(pyCallback); 42 | SharedObject sharedArgs(std::move(args)); 43 | SharedObject sharedKwArgs(std::move(kwargs)); 44 | auto invokeCallback = [=]() mutable { 45 | GILAcquire acquire; 46 | return invokeCatchPythonError( 47 | sharedCb.takeInner(), 48 | *sharedArgs.takeInner(), 49 | **sharedKwArgs.takeInner()).cast(); 50 | }; 51 | 52 | // If there is a strand attached to that callable, we use it but we cannot use 53 | // asyncDelay (we use defer instead). This is because we might be executing 54 | // this function from inside the strand execution context, and thus asyncDelay 55 | // might be blocking. 56 | Promise prom; 57 | const auto strand = strandOfFunction(pyCallback); 58 | if (strand) 59 | { 60 | strand->defer([=]() mutable { prom.setValue(invokeCallback()); }, delay) 61 | .connect([=](qi::Future fut) mutable { 62 | if (fut.hasValue()) return; 63 | adaptFuture(fut, prom); 64 | }); 65 | } 66 | else 67 | adaptFuture(asyncDelay(invokeCallback, delay), prom); 68 | 69 | return castToPyObject(prom.future()); 70 | } 71 | 72 | } // namespace 73 | 74 | void exportAsync(::py::module& m) 75 | { 76 | using namespace ::py; 77 | using namespace ::py::literals; 78 | 79 | GILAcquire lock; 80 | 81 | m.def("runAsync", &async, 82 | "callback"_a, 83 | // TODO: use `::py:kwonly, ::py::arg(delayArgName) = 0` when available. 84 | doc(":param callback: the callback that will be called\n" 85 | ":param delay: an optional delay in microseconds\n" 86 | ":returns: a future with the return value of the function")); 87 | 88 | class_(m, "PeriodicTask") 89 | .def(init<>()) 90 | .def( 91 | "setCallback", 92 | [](PeriodicTask& task, ::py::function pyCallback) { 93 | auto callback = pyCallback.cast>(); 94 | task.setCallback(std::move(callback)); 95 | task.setStrand(strandOfFunction(pyCallback).get()); 96 | }, 97 | "callable"_a, 98 | doc( 99 | "Set the callback used by the periodic task, this function can only be " 100 | "called once.\n" 101 | ":param callable: a python callable, could be a method or a function.")) 102 | .def("setUsPeriod", 103 | [](PeriodicTask& task, qi::int64_t usPeriod) { 104 | task.setPeriod(qi::MicroSeconds(usPeriod)); 105 | }, 106 | call_guard(), "usPeriod"_a, 107 | doc("Set the call interval in microseconds.\n" 108 | "This call will wait until next callback invocation to apply the " 109 | "change.\n" 110 | "To apply the change immediately, use: \n" 111 | "\n" 112 | ".. code-block:: python\n" 113 | "\n" 114 | " task.stop()\n" 115 | " task.setUsPeriod(100)\n" 116 | " task.start()\n" 117 | ":param usPeriod: the period in microseconds")) 118 | .def("start", &PeriodicTask::start, call_guard(), 119 | "immediate"_a, 120 | doc("Start the periodic task at specified period. No effect if " 121 | "already running.\n" 122 | ":param immediate: immediate if true, first schedule of the task " 123 | "will happen with no delay.\n" 124 | ".. warning::\n" 125 | " concurrent calls to start() and stop() will result in " 126 | "undefined behavior.")) 127 | .def("stop", &PeriodicTask::stop, call_guard(), 128 | doc("Stop the periodic task. When this function returns, the callback " 129 | "will not be called " 130 | "anymore. Can be called from within the callback function\n" 131 | ".. warning::\n" 132 | " concurrent calls to start() and stop() will result in " 133 | "undefined behavior.")) 134 | .def("asyncStop", &PeriodicTask::asyncStop, 135 | call_guard(), 136 | doc("Request for periodic task to stop asynchronously.\n" 137 | "Can be called from within the callback function.")) 138 | .def( 139 | "compensateCallbackTime", &PeriodicTask::compensateCallbackTime, 140 | call_guard(), "compensate"_a, 141 | doc( 142 | ":param compensate: boolean. True to activate the compensation.\n" 143 | "When compensation is activated, call interval will take into account " 144 | "call duration to maintain the period.\n" 145 | ".. warning::\n" 146 | " when the callback is longer than the specified period, " 147 | "compensation will result in the callback being called successively " 148 | "without pause.")) 149 | .def("setName", &PeriodicTask::setName, call_guard(), 150 | "name"_a, doc("Set name for debugging and tracking purpose")) 151 | .def("isRunning", &PeriodicTask::isRunning, 152 | doc(":returns: true if task is running")) 153 | .def("isStopping", &PeriodicTask::isStopping, 154 | doc("Can be called from within the callback to know if stop() or " 155 | "asyncStop() was called.\n" 156 | ":returns: whether state is stopping or stopped.")); 157 | } 158 | 159 | } // namespace py 160 | } // namespace qi 161 | -------------------------------------------------------------------------------- /src/pyclock.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace py = pybind11; 13 | 14 | namespace qi 15 | { 16 | namespace py 17 | { 18 | 19 | namespace 20 | { 21 | 22 | template 23 | typename Clock::rep now() 24 | { 25 | return Clock::now().time_since_epoch().count(); 26 | } 27 | 28 | } // namespace 29 | 30 | void exportClock(::py::module& m) 31 | { 32 | using namespace ::py; 33 | using namespace ::py::literals; 34 | 35 | GILAcquire lock; 36 | 37 | m.def("clockNow", &now, 38 | doc(":returns: current timestamp on qi::Clock, as a number of nanoseconds")); 39 | m.def("steadyClockNow", &now, 40 | doc(":returns: current timestamp on qi::SteadyClock, as a number of nanoseconds")); 41 | m.def("systemClockNow", &now, 42 | doc(":returns: current timestamp on qi::SystemClock, as a number of nanoseconds")); 43 | } 44 | 45 | } // namespace py 46 | } // namespace qi 47 | -------------------------------------------------------------------------------- /src/pyexport.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | namespace py = pybind11; 25 | 26 | namespace qi 27 | { 28 | namespace py 29 | { 30 | 31 | void exportAll(pybind11::module& module) 32 | { 33 | registerTypes(); 34 | 35 | GILAcquire lock; 36 | 37 | exportFuture(module); 38 | exportSignal(module); 39 | exportProperty(module); 40 | exportObject(module); 41 | exportSession(module); 42 | exportApplication(module); 43 | exportObjectFactory(module); 44 | exportAsync(module); 45 | exportLog(module); 46 | exportPath(module); 47 | exportTranslator(module); 48 | exportStrand(module); 49 | exportClock(module); 50 | } 51 | 52 | } // namespace py 53 | } // namespace qi 54 | -------------------------------------------------------------------------------- /src/pylog.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | namespace py = pybind11; 15 | 16 | namespace qi 17 | { 18 | namespace py 19 | { 20 | 21 | void exportLog(::py::module& m) 22 | { 23 | using namespace ::py; 24 | using namespace ::py::literals; 25 | 26 | GILAcquire lock; 27 | 28 | enum_(m, "LogLevel") 29 | .value("Silent", LogLevel_Silent) 30 | .value("Fatal", LogLevel_Fatal) 31 | .value("Error", LogLevel_Error) 32 | .value("Warning", LogLevel_Warning) 33 | .value("Info", LogLevel_Info) 34 | .value("Verbose", LogLevel_Verbose) 35 | .value("Debug", LogLevel_Debug); 36 | 37 | m.def("pylog", 38 | [](LogLevel level, const std::string& name, const std::string& message, 39 | const std::string& file, const std::string& func, int line) { 40 | log::log(level, name.c_str(), message.c_str(), file.c_str(), 41 | func.c_str(), line); 42 | }, 43 | call_guard(), "level"_a, "name"_a, "message"_a, 44 | "file"_a, "func"_a, "line"_a); 45 | 46 | m.def("setFilters", 47 | [](const std::string& filters) { log::addFilters(filters); }, 48 | call_guard(), "filters"_a, 49 | doc("Set log filtering options.\n" 50 | "Each rule can be:\n\n" 51 | " +CAT: enable category CAT\n\n" 52 | " -CAT: disable category CAT\n\n" 53 | " CAT=level : set category CAT to level\n\n" 54 | "Each category can include a '*' for globbing.\n" 55 | "\n" 56 | ".. code-block:: python\n" 57 | "\n" 58 | " qi.logging.setFilter(\"qi.*=debug:-qi.foo:+qi.foo.bar\")\n" 59 | "\n" 60 | "(all qi.* logs in info, remove all qi.foo logs except qi.foo.bar)\n" 61 | ":param filters: List of rules separated by colon.")); 62 | 63 | m.def("setContext", [](int context) { qi::log::setContext(context); }, 64 | call_guard(), "context"_a, 65 | doc(" 1 : Verbosity \n" 66 | " 2 : ShortVerbosity \n" 67 | " 4 : Date \n" 68 | " 8 : ThreadId \n" 69 | " 16 : Category \n" 70 | " 32 : File \n" 71 | " 64 : Function \n" 72 | " 128: EndOfLine \n\n" 73 | " Some useful values for context are: \n" 74 | " 26 : (verb+threadId+cat) \n" 75 | " 30 : (verb+threadId+date+cat) \n" 76 | " 126: (verb+threadId+date+cat+file+fun) \n" 77 | " 254: (verb+threadId+date+cat+file+fun+eol)\n\n" 78 | ":param context: A bitfield (sum of described values).")); 79 | 80 | m.def("setLevel", [](LogLevel level) { log::setLogLevel(level); }, 81 | call_guard(), "level"_a, 82 | doc( 83 | "Sets the threshold for the logger to level. " 84 | "Logging messages which are less severe than level will be ignored. " 85 | "Note that the logger is created with level INFO.\n" 86 | ":param level: The minimum log level.")); 87 | } 88 | } // namespace py 89 | } // namespace qi 90 | -------------------------------------------------------------------------------- /src/pymodule.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | namespace py = pybind11; 15 | 16 | namespace qi 17 | { 18 | namespace py 19 | { 20 | 21 | namespace 22 | { 23 | 24 | ::py::object call(::py::object obj, ::py::str name, 25 | ::py::args args, ::py::kwargs kwargs) 26 | { 27 | GILAcquire lock; 28 | return obj.attr(name)(*args, **kwargs); 29 | } 30 | 31 | ::py::object getModule(const std::string& name) 32 | { 33 | const auto mod = import(name); 34 | const auto pyMod = toPyObject(mod); 35 | const ::py::cpp_function callFn(&call, ::py::is_method(pyMod.get_type()), 36 | ::py::arg("name")); 37 | 38 | const auto types = ::py::module::import("types"); 39 | ::py::setattr(pyMod, "createObject", types.attr("MethodType")(callFn, pyMod)); 40 | return pyMod; 41 | } 42 | 43 | ::py::list listModules() 44 | { 45 | const auto modules = invokeGuarded(&qi::listModules); 46 | return castToPyObject(AnyReference::from(modules)); 47 | } 48 | 49 | } // namespace 50 | 51 | void exportObjectFactory(::py::module& m) 52 | { 53 | using namespace ::py; 54 | 55 | GILAcquire lock; 56 | 57 | m.def("module", &getModule, 58 | doc(":returns: an object that represents the requested module.\n")); 59 | m.def("listModules", &listModules); 60 | } 61 | 62 | } // namespace py 63 | } // namespace qi 64 | -------------------------------------------------------------------------------- /src/pypath.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | namespace py = pybind11; 16 | 17 | namespace qi 18 | { 19 | namespace py 20 | { 21 | 22 | void exportPath(::py::module& m) 23 | { 24 | using namespace ::py; 25 | using namespace ::py::literals; 26 | 27 | GILAcquire lock; 28 | 29 | m.def("sdkPrefix", &path::sdkPrefix, call_guard(), 30 | doc(":returns: The SDK prefix path. It is always a complete, native " 31 | "path.\n")); 32 | 33 | m.def( 34 | "findBin", &path::findBin, call_guard(), "name"_a, 35 | "searchInPath"_a = false, 36 | doc("Look for a binary in the system.\n" 37 | ":param name: string. The full name of the binary, or just the name.\n" 38 | ":param searchInPath: boolean. Search in $PATH if it hasn't been " 39 | "found in sdk dirs. Optional.\n" 40 | ":returns: the complete, native path to the file found. An empty string " 41 | "otherwise.")); 42 | 43 | m.def( 44 | "findLib", &path::findLib, call_guard(), "name"_a, 45 | doc("Look for a library in the system.\n" 46 | ":param name: string. The full name of the library, or just the name.\n" 47 | ":returns: the complete, native path to the file found. An empty string " 48 | "otherwise.")); 49 | 50 | m.def( 51 | "findConf", &path::findConf, call_guard(), 52 | "application"_a, "file"_a, "excludeUserWritablePath"_a = false, 53 | doc("Look for a configuration file in the system.\n" 54 | ":param application: string. The name of the application.\n" 55 | ":param file: string. The name of the file to look for." 56 | " You can specify subdirectories using '/' as a separator.\n" 57 | ":param excludeUserWritablePath: If true, findConf() won't search into " 58 | "userWritableConfPath.\n" 59 | ":returns: the complete, native path to the file found. An empty string " 60 | "otherwise.")); 61 | 62 | m.def( 63 | "findData", &path::findData, call_guard(), 64 | "application"_a, "file"_a, "excludeUserWritablePath"_a = false, 65 | doc("Look for a file in all dataPaths(application) directories. Return the " 66 | "first match.\n" 67 | ":param application: string. The name of the application.\n" 68 | ":param file: string. The name of the file to look for." 69 | " You can specify subdirectories using a '/' as a separator.\n" 70 | ":param excludeUserWritablePath: If true, findData() won't search into " 71 | "userWritableDataPath.\n" 72 | ":returns: the complete, native path to the file found. An empty string " 73 | "otherwise.")); 74 | 75 | m.def( 76 | "listData", 77 | [](const std::string& applicationName, const std::string& pattern) { 78 | return path::listData(applicationName, pattern); 79 | }, 80 | call_guard(), "applicationName"_a, "pattern"_a, 81 | doc("List data files matching the given pattern in all " 82 | "dataPaths(application) directories.\n" 83 | " For each match, return the occurrence from the first dataPaths prefix." 84 | " Directories are discarded.\n\n" 85 | ":param application: string. The name of the application.\n" 86 | ":param patten: string. Wildcard pattern of the files to look for." 87 | " You can specify subdirectories using a '/' as a separator." 88 | " \"*\" by default.\n" 89 | ":returns: a list of the complete, native paths of the files that " 90 | "matched.")); 91 | 92 | m.def("listData", [](const std::string& applicationName) { 93 | return path::listData(applicationName); 94 | }, 95 | call_guard(), "applicationName"_a); 96 | 97 | m.def("confPaths", [](const std::string& applicationName) { 98 | return path::confPaths(applicationName); 99 | }, 100 | call_guard(), 101 | "applicationName"_a, 102 | doc("Get the list of directories used when searching for " 103 | "configuration files for the given application.\n" 104 | ":param applicationName: string. Name of the application. " 105 | "\"\" by default.\n" 106 | ":returns: The list of configuration directories.\n" 107 | ".. warning::\n" 108 | " You should not assume those directories exist," 109 | " nor that they are writable.")); 110 | 111 | m.def("confPaths", [] { return path::confPaths(); }, 112 | call_guard()); 113 | 114 | m.def("dataPaths", [](const std::string& applicationName) { 115 | return path::dataPaths(applicationName); 116 | }, 117 | call_guard(), 118 | "applicationName"_a, 119 | doc("Get the list of directories used when searching for " 120 | "configuration files for the given application.\n" 121 | ":param application: string. Name of the application. " 122 | "\"\" by default.\n" 123 | ":returns: The list of data directories.\n" 124 | ".. warning::\n" 125 | " You should not assume those directories exist," 126 | " nor that they are writable.")); 127 | 128 | m.def("dataPaths", [] { return path::dataPaths(); }, 129 | call_guard()); 130 | 131 | m.def("binPaths", [] { return path::binPaths(); }, 132 | call_guard(), 133 | doc( 134 | ":returns: The list of directories used when searching for binaries.\n" 135 | ".. warning::\n" 136 | " You should not assume those directories exist," 137 | " nor that they are writable.")); 138 | 139 | m.def( 140 | "libPaths", [] { return path::libPaths(); }, 141 | call_guard(), 142 | doc(":returns: The list of directories used when searching for libraries.\n\n" 143 | ".. warning::\n" 144 | " You should not assume those directories exist," 145 | " nor that they are writable.")); 146 | 147 | m.def("setWritablePath", &path::detail::setWritablePath, 148 | call_guard(), "path"_a, 149 | doc("Set the writable files path for users.\n" 150 | ":param path: string. A path on the system.\n" 151 | "Use an empty path to reset it to its default value.")); 152 | 153 | m.def("userWritableDataPath", &path::userWritableDataPath, 154 | call_guard(), "applicationName"_a, "fileName"_a, 155 | doc("Get the writable data files path for users.\n" 156 | ":param applicationName: string. Name of the application.\n" 157 | ":param fileName: string. Name of the file.\n" 158 | ":returns: The file path.")); 159 | 160 | m.def("userWritableConfPath", &path::userWritableConfPath, 161 | call_guard(), "applicationName"_a, "fileName"_a, 162 | doc("Get the writable configuration files path for users.\n" 163 | ":param applicationName: string. Name of the application.\n" 164 | ":param fileName: string. Name of the file.\n" 165 | ":returns: The file path.")); 166 | 167 | m.def("sdkPrefixes", [] { return path::detail::getSdkPrefixes(); }, 168 | call_guard(), 169 | doc("List of SDK prefixes.\n" 170 | ":returns: The list of sdk prefixes.")); 171 | 172 | m.def("addOptionalSdkPrefix", &path::detail::addOptionalSdkPrefix, 173 | call_guard(), "prefix"_a, 174 | doc("Add a new SDK path.\n" 175 | ":param: an sdk prefix.")); 176 | 177 | m.def("clearOptionalSdkPrefix", &path::detail::clearOptionalSdkPrefix, 178 | call_guard(), 179 | doc("Clear all optional sdk prefixes.")); 180 | } 181 | 182 | } // namespace py 183 | } // namespace qi 184 | -------------------------------------------------------------------------------- /src/pyproperty.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | namespace py = pybind11; 16 | 17 | namespace qi 18 | { 19 | namespace py 20 | { 21 | 22 | namespace 23 | { 24 | 25 | constexpr static const auto asyncArgName = "_async"; 26 | 27 | pybind11::object propertyConnect(Property& prop, 28 | const pybind11::function& pyCallback, 29 | bool async) 30 | { 31 | GILAcquire lock; 32 | return detail::signalConnect(prop, pyCallback, async); 33 | } 34 | 35 | 36 | ::py::object proxyPropertyConnect(detail::ProxyProperty& prop, 37 | const ::py::function& callback, 38 | bool async) 39 | { 40 | GILAcquire lock; 41 | return detail::proxySignalConnect(prop.object, prop.propertyId, callback, async); 42 | } 43 | 44 | } // namespace 45 | 46 | detail::ProxyProperty::~ProxyProperty() 47 | { 48 | // The destructor can lock when waiting for callbacks to end. 49 | GILRelease unlock; 50 | object.reset(); 51 | } 52 | 53 | bool isProperty(const pybind11::object& obj) 54 | { 55 | GILAcquire lock; 56 | return ::py::isinstance(obj) || 57 | ::py::isinstance(obj); 58 | } 59 | 60 | void exportProperty(::py::module& m) 61 | { 62 | using namespace ::py; 63 | using namespace ::py::literals; 64 | 65 | GILAcquire lock; 66 | 67 | using PropertyPtr = std::unique_ptr; 68 | class_(m, "Property") 69 | 70 | .def(init([] { return new Property(TypeInterface::fromSignature("m")); }), 71 | call_guard()) 72 | 73 | .def(init([](const std::string& sig) { 74 | return PropertyPtr(new Property(TypeInterface::fromSignature(sig)), 75 | DeleteOutsideGIL()); 76 | }), 77 | "signature"_a, call_guard()) 78 | 79 | .def("value", 80 | [](const Property& prop, bool async) { 81 | const auto fut = prop.value().async(); 82 | GILAcquire lock; 83 | return resultObject(fut, async); 84 | }, 85 | arg(asyncArgName) = false, call_guard(), 86 | doc("Return the value stored inside the property.")) 87 | 88 | .def("setValue", 89 | [](Property& prop, AnyValue value, bool async) { 90 | const auto fut = toFuture(prop.setValue(std::move(value)).async()); 91 | GILAcquire lock; 92 | return resultObject(fut, async); 93 | }, 94 | call_guard(), "value"_a, arg(asyncArgName) = false, 95 | doc("Set the value of the property.")) 96 | 97 | .def("addCallback", &propertyConnect, "cb"_a, arg(asyncArgName) = false, 98 | doc("Add an event subscriber to the property.\n" 99 | ":param cb: the callback to call when the property changes\n" 100 | ":returns: the id of the property subscriber")) 101 | 102 | .def("connect", &propertyConnect, "cb"_a, arg(asyncArgName) = false, 103 | doc("Add an event subscriber to the property.\n" 104 | ":param cb: the callback to call when the property changes\n" 105 | ":returns: the id of the property subscriber")) 106 | 107 | .def("disconnect", 108 | [](Property& prop, SignalLink id, bool async) { 109 | return detail::signalDisconnect(prop, id, async); 110 | }, 111 | call_guard(), "id"_a, arg(asyncArgName) = false, 112 | doc("Disconnect the callback associated to id.\n\n" 113 | ":param id: the connection id returned by :method:connect or " 114 | ":method:addCallback\n" 115 | ":returns: true on success")) 116 | 117 | .def("disconnectAll", 118 | [](Property& prop, bool async) { 119 | return detail::signalDisconnectAll(prop, async); 120 | }, 121 | call_guard(), arg(asyncArgName) = false, 122 | doc("Disconnect all subscribers associated to the property.\n\n" 123 | "This function should be used with caution, as it may also remove " 124 | "subscribers that were added by other callers.\n\n" 125 | ":returns: true on success\n")); 126 | 127 | class_(m, "_ProxyProperty") 128 | .def("value", 129 | [](const detail::ProxyProperty& prop, bool async) { 130 | const auto fut = prop.object.property(prop.propertyId).async(); 131 | GILAcquire lock; 132 | return resultObject(fut, async); 133 | }, 134 | call_guard(), arg(asyncArgName) = false) 135 | .def("setValue", 136 | [](detail::ProxyProperty& prop, object pyValue, bool async) { 137 | AnyValue value(unwrapAsRef(pyValue)); 138 | GILRelease unlock; 139 | const auto fut = 140 | toFuture(prop.object.setProperty(prop.propertyId, std::move(value)) 141 | .async()); 142 | GILAcquire lock; 143 | return resultObject(fut, async); 144 | }, 145 | "value"_a, arg(asyncArgName) = false) 146 | .def("addCallback", &proxyPropertyConnect, "cb"_a, 147 | arg(asyncArgName) = false) 148 | .def("connect", &proxyPropertyConnect, "cb"_a, 149 | arg(asyncArgName) = false) 150 | .def("disconnect", 151 | [](detail::ProxyProperty& prop, SignalLink id, bool async) { 152 | return detail::proxySignalDisconnect(prop.object, id, async); 153 | }, 154 | "id"_a, arg(asyncArgName) = false); 155 | } 156 | 157 | } // namespace py 158 | } // namespace qi 159 | -------------------------------------------------------------------------------- /src/pysession.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace py = pybind11; 17 | 18 | namespace qi 19 | { 20 | namespace py 21 | { 22 | 23 | namespace 24 | { 25 | 26 | ::py::object makeSession() 27 | { 28 | // Ensures that the Session is deleted after the shared_pointer on it is deleted. 29 | // This is to mimic the behavior of qiApplication. 30 | // This is a fix for #38167. 31 | struct Deleter 32 | { 33 | boost::weak_ptr wptr; 34 | 35 | void operator()(Session* p) const 36 | { 37 | constexpr const auto category = "qi.python.session.deleter"; 38 | constexpr const auto msg = "Waiting for the shared pointer destruction..."; 39 | GILRelease x; 40 | auto fut = std::async(std::launch::async, [=](std::unique_ptr) { 41 | while (!wptr.expired()) 42 | { 43 | qiLogDebug(category) << msg; 44 | constexpr const std::chrono::milliseconds delay(100); 45 | std::this_thread::sleep_for(delay); 46 | } 47 | qiLogDebug(category) << msg << " done."; 48 | }, std::unique_ptr(p)); 49 | QI_IGNORE_UNUSED(fut); 50 | } 51 | }; 52 | 53 | auto ptr = SessionPtr(new qi::Session{}, Deleter{}); 54 | boost::get_deleter(ptr)->wptr = ka::weak_ptr(ptr); 55 | 56 | GILAcquire lock; 57 | return py::makeSession(ptr); 58 | } 59 | 60 | } // namespace 61 | 62 | ::py::object makeSession(SessionPtr sess) 63 | { 64 | GILAcquire lock; 65 | return toPyObject(sess); 66 | } 67 | 68 | void exportSession(::py::module& m) 69 | { 70 | using namespace ::py; 71 | 72 | GILAcquire lock; 73 | 74 | m.def("Session", []{ return makeSession(); }); 75 | } 76 | 77 | } // namespace py 78 | } // namespace qi 79 | -------------------------------------------------------------------------------- /src/pysignal.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | qiLogCategory("qi.python.signal"); 16 | 17 | namespace py = pybind11; 18 | 19 | namespace qi 20 | { 21 | namespace py 22 | { 23 | 24 | namespace 25 | { 26 | 27 | constexpr static const auto asyncArgName = "_async"; 28 | 29 | AnyReference dynamicCallFunction(const SharedObject<::py::function>& func, 30 | const AnyReferenceVector& args) 31 | { 32 | GILAcquire lock; 33 | ::py::list pyArgs(args.size()); 34 | ::py::size_t i = 0; 35 | for (const auto& arg : args) 36 | pyArgs[i++] = castToPyObject(arg); 37 | invokeCatchPythonError(func.inner(), *pyArgs); 38 | return AnyValue::makeVoid().release(); 39 | } 40 | 41 | // Procedure(SignalSubscriber)> F 42 | template 43 | ::py::object connect(F&& connect, 44 | const ::py::function& pyCallback, 45 | bool async) 46 | { 47 | GILAcquire lock; 48 | const auto strand = strandOfFunction(pyCallback); 49 | 50 | SignalSubscriber subscriber(AnyFunction::fromDynamicFunction( 51 | boost::bind(dynamicCallFunction, 52 | SharedObject(pyCallback), 53 | _1)), 54 | strand.get()); 55 | subscriber.setCallType(MetaCallType_Auto); 56 | const auto fut = std::forward(connect)(std::move(subscriber)) 57 | .andThen(FutureCallbackType_Sync, [](SignalLink link) { 58 | return AnyValue::from(link); 59 | }); 60 | return resultObject(fut, async); 61 | } 62 | 63 | ::py::object proxySignalConnect(detail::ProxySignal& sig, 64 | const ::py::function& callback, 65 | bool async) 66 | { 67 | GILAcquire lock; 68 | return detail::proxySignalConnect(sig.object, sig.signalId, callback, async); 69 | } 70 | 71 | } // namespace 72 | 73 | namespace detail 74 | { 75 | 76 | detail::ProxySignal::~ProxySignal() 77 | { 78 | // The destructor can lock waiting for callbacks to end. 79 | GILRelease unlock; 80 | object.reset(); 81 | } 82 | 83 | 84 | ::py::object signalConnect(SignalBase& sig, 85 | const ::py::function& callback, 86 | bool async) 87 | { 88 | GILAcquire lock; 89 | return connect( 90 | [&](const SignalSubscriber& sub) { 91 | GILRelease unlock; 92 | return sig.connectAsync(sub).andThen(FutureCallbackType_Sync, 93 | [](const SignalSubscriber& sub) { 94 | return sub.link(); 95 | }); 96 | }, 97 | callback, async); 98 | } 99 | 100 | ::py::object signalDisconnect(SignalBase& sig, SignalLink id, bool async) 101 | { 102 | const auto fut = 103 | sig.disconnectAsync(id).andThen(FutureCallbackType_Sync, [](bool success) { 104 | return AnyValue::from(success); 105 | }); 106 | 107 | GILAcquire lock; 108 | return resultObject(fut, async); 109 | } 110 | 111 | ::py::object signalDisconnectAll(SignalBase& sig, bool async) 112 | { 113 | const auto fut = 114 | sig.disconnectAllAsync().andThen(FutureCallbackType_Sync, [](bool success) { 115 | return AnyValue::from(success); 116 | }); 117 | 118 | GILAcquire lock; 119 | return resultObject(fut, async); 120 | } 121 | 122 | ::py::object proxySignalConnect(const AnyObject& obj, 123 | unsigned int signalId, 124 | const ::py::function& callback, 125 | bool async) 126 | { 127 | GILAcquire lock; 128 | return connect( 129 | [&](const SignalSubscriber& sub) { 130 | GILRelease unlock; 131 | return obj.connect(signalId, sub).async(); 132 | }, 133 | callback, async); 134 | } 135 | 136 | ::py::object proxySignalDisconnect(const AnyObject& obj, 137 | SignalLink id, 138 | bool async) 139 | { 140 | const auto fut = [&] { 141 | GILRelease unlock; 142 | return toFuture(obj.disconnect(id)); 143 | }(); 144 | GILAcquire lock; 145 | return resultObject(fut, async); 146 | } 147 | 148 | } // namespace detail 149 | 150 | bool isSignal(const ::py::object& obj) 151 | { 152 | GILAcquire lock; 153 | return ::py::isinstance(obj) || 154 | ::py::isinstance(obj); 155 | } 156 | 157 | void exportSignal(::py::module& m) 158 | { 159 | using namespace ::py; 160 | using namespace ::py::literals; 161 | 162 | GILAcquire lock; 163 | 164 | using SignalPtr = std::unique_ptr; 165 | class_(m, "Signal") 166 | 167 | .def(init([](std::string signature, std::function onConnect) { 168 | qi::SignalBase::OnSubscribers onSub; 169 | if (onConnect) 170 | onSub = qi::futurizeOutput(onConnect); 171 | return new Signal(signature, onSub); 172 | }), 173 | call_guard(), 174 | "signature"_a = "m", "onConnect"_a = std::function()) 175 | 176 | .def( 177 | "connect", &detail::signalConnect, 178 | call_guard(), 179 | "callback"_a, arg(asyncArgName) = false, 180 | doc( 181 | "Connect the signal to a callback, the callback will be called each " 182 | "time the signal is triggered. Use the id returned to unregister the " 183 | "callback." 184 | ":param callback: the callback that will be called when the signal is " 185 | "triggered.\n" 186 | ":returns: the connection id of the registered callback.")) 187 | 188 | .def("disconnect", &detail::signalDisconnect, 189 | call_guard(), "id"_a, arg(asyncArgName) = false, 190 | doc("Disconnect the callback associated to id.\n" 191 | ":param id: the connection id returned by connect.\n" 192 | ":returns: true on success.")) 193 | 194 | .def("disconnectAll", &detail::signalDisconnectAll, 195 | call_guard(), arg(asyncArgName) = false, 196 | doc("Disconnect all subscribers associated to the property.\n\n" 197 | "This function should be used with caution, as it may also remove " 198 | "subscribers that were added by other callers.\n\n" 199 | ":returns: true on success\n")) 200 | 201 | .def("__call__", 202 | [](Signal& sig, args pyArgs) { 203 | const auto args = 204 | AnyReference::from(pyArgs).content().asTupleValuePtr(); 205 | GILRelease unlock; 206 | sig.trigger(args); 207 | }, 208 | doc("Trigger the signal")); 209 | 210 | class_(m, "_ProxySignal") 211 | .def("connect", &proxySignalConnect, "callback"_a, 212 | arg(asyncArgName) = false) 213 | .def("disconnect", 214 | [](detail::ProxySignal& sig, SignalLink id, bool async) { 215 | return detail::proxySignalDisconnect(sig.object, id, async); 216 | }, 217 | "id"_a, arg(asyncArgName) = false) 218 | .def("__call__", 219 | [](detail::ProxySignal& sig, args pyArgs) { 220 | const auto args = 221 | AnyReference::from(pyArgs).content().asTupleValuePtr(); 222 | GILRelease unlock; 223 | sig.object.metaPost(sig.signalId, args); 224 | }); 225 | } 226 | 227 | } // namespace py 228 | } // namespace qi 229 | -------------------------------------------------------------------------------- /src/pystrand.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | qiLogCategory("qi.python.strand"); 12 | 13 | namespace py = pybind11; 14 | 15 | namespace qi 16 | { 17 | namespace py 18 | { 19 | 20 | namespace 21 | { 22 | 23 | const char* const objectAttributeStrandName = "__qi_strand__"; 24 | const char* const objectAttributeThreadingName = "__qi_threading__"; 25 | const char* const objectAttributeThreadingValueMulti = "multi"; 26 | 27 | // Returns true if `func` is an unbound method of `object`, by checking if one 28 | // of the attribute of `object` is `func`. 29 | // 30 | // @pre `func` and `object` are not null objects. 31 | bool isUnboundMethod(const ::py::function& func, const ::py::object& object) 32 | { 33 | QI_ASSERT_TRUE(func); 34 | QI_ASSERT_TRUE(object); 35 | 36 | GILAcquire lock; 37 | const auto dir = ::py::reinterpret_steal<::py::list>(PyObject_Dir(object.ptr())); 38 | for (const auto& attrName : dir) 39 | { 40 | const auto attr = object.attr(attrName); 41 | QI_ASSERT_TRUE(attr); 42 | 43 | if (!PyMethod_Check(attr.ptr())) 44 | continue; 45 | 46 | const auto unboundMethod = ::py::reinterpret_borrow<::py::object>(PyMethod_GET_FUNCTION(attr.ptr())); 47 | if (func.is(unboundMethod)) 48 | return true; 49 | } 50 | return false; 51 | } 52 | 53 | // Returns the instance associated with a partial application of a method, if 54 | // passed. 55 | // 56 | // If no instance is associated, or if the object is not a partial application, 57 | // returns `None`. 58 | // 59 | // See https://docs.python.org/3/library/functools.html for more details about 60 | // partial objects. 61 | // 62 | // They have three read-only attributes: 63 | // - partial.func 64 | // A callable object or function. Calls to the partial object will be 65 | // forwarded to func with new arguments and keywords. 66 | // 67 | // - partial.args 68 | // The leftmost positional arguments that will be prepended to the 69 | // positional arguments provided to a partial object call. 70 | // 71 | // - partial.keywords 72 | // The keyword arguments that will be supplied when the partial object is 73 | // called. 74 | // 75 | // @pre `obj` is not a null object. 76 | // @post the returned value is not a null object (it evaluates to true). 77 | ::py::object getPartialSelf(const ::py::function& partialFunc) 78 | { 79 | QI_ASSERT_TRUE(partialFunc); 80 | 81 | GILAcquire lock; 82 | 83 | constexpr const char* argsAttr = "args"; 84 | constexpr const char* funcAttr = "func"; 85 | if (!::py::hasattr(partialFunc, argsAttr) || !::py::hasattr(partialFunc, funcAttr)) 86 | // It's not a partial func, return immediately. 87 | return ::py::none(); 88 | 89 | try 90 | { 91 | auto func = partialFunc.attr(funcAttr).cast<::py::function>(); 92 | QI_ASSERT_TRUE(func); 93 | 94 | // The function might be a (bound) method. 95 | const auto self = getMethodSelf(func); 96 | if (!self.is_none()) 97 | return self; 98 | 99 | // The function might be a `partial`. 100 | const auto subpartialSelf = getPartialSelf(func); 101 | if (!subpartialSelf.is_none()) 102 | return subpartialSelf; 103 | 104 | // The function might be an unbound method. 105 | // 106 | // Unlike in Python 2, there is no known way to differentiate between an 107 | // unbound method and a free function in Python 3. So to maintain 108 | // retrocompatibility with older versions of libqi-python, we still decide 109 | // to assume that the first argument is an instance (`self`), even though 110 | // the function might be a free function, and not an unbound function. Then 111 | // we check the function is a reference to one of the function attribute of 112 | // that first argument. 113 | // 114 | // We can also check if the function is a not static method (which won't 115 | // have a self parameter). 116 | const bool isStaticMethod = ::py::isinstance<::py::staticmethod>(func); 117 | if (!isStaticMethod) 118 | { 119 | ::py::tuple args = partialFunc.attr(argsAttr); 120 | if (args.empty()) 121 | return ::py::none(); 122 | const ::py::object selfCandidate = args[0]; 123 | if (isUnboundMethod(func, selfCandidate)) 124 | return selfCandidate; 125 | } 126 | 127 | // Fallback to just returning the function. 128 | return std::move(func); 129 | } 130 | catch (const std::exception& ex) 131 | { 132 | qiLogVerbose() 133 | << "An exception occurred when extracting a partial function: " 134 | << ex.what(); 135 | return ::py::none(); 136 | } 137 | } 138 | 139 | // Returns the instance of the object on which the function is called (the 140 | // `self` parameter). 141 | // 142 | // This function tries to detect partial application of functions and deduce 143 | // the instance from the arguments, if passed. 144 | // 145 | // @pre `func` is not a null object. 146 | // @post the returned value is not a null object (it evaluates to true). 147 | ::py::object getSelf(const ::py::function& func) 148 | { 149 | QI_ASSERT_TRUE(func); 150 | 151 | GILAcquire lock; 152 | const auto self = getMethodSelf(func); 153 | if (!self.is_none()) 154 | return self; 155 | return getPartialSelf(func); 156 | } 157 | 158 | } // namespace 159 | 160 | StrandPtr strandOfFunction(const ::py::function& func) 161 | { 162 | GILAcquire lock; 163 | return strandOf(getSelf(func)); 164 | } 165 | 166 | StrandPtr strandOf(const ::py::object& obj) 167 | { 168 | QI_ASSERT_TRUE(obj); 169 | 170 | GILAcquire lock; 171 | 172 | if (obj.is_none()) 173 | return {}; 174 | 175 | if (isMultithreaded(obj)) 176 | return {}; 177 | 178 | auto strandObj = ::py::getattr(obj, objectAttributeStrandName, ::py::none()); 179 | if (strandObj.is_none()) 180 | { 181 | try 182 | { 183 | strandObj = ::py::cast(StrandPtr(new Strand, DeleteOutsideGIL())); 184 | ::py::setattr(obj, objectAttributeStrandName, strandObj); 185 | } 186 | catch (const ::py::error_already_set& ex) 187 | { 188 | // If setting the attribute fails with an AttributeError, it may mean that 189 | // we cannot set attributes on this object, therefore it cannot have an 190 | // associated strand. 191 | if (ex.matches(PyExc_AttributeError)) return {}; 192 | throw; 193 | } 194 | } 195 | 196 | if (strandObj.is_none() || !::py::isinstance(strandObj)) 197 | return {}; 198 | 199 | return strandObj.cast(); 200 | } 201 | 202 | bool isMultithreaded(const ::py::object& obj) 203 | { 204 | QI_ASSERT_TRUE(obj); 205 | 206 | GILAcquire lock; 207 | const auto pyqisig = ::py::getattr(obj, objectAttributeThreadingName, ::py::none()); 208 | if (pyqisig.is_none()) 209 | return false; 210 | return pyqisig.cast() == objectAttributeThreadingValueMulti; 211 | } 212 | 213 | void exportStrand(::py::module& m) 214 | { 215 | using namespace ::py; 216 | 217 | GILAcquire lock; 218 | 219 | class_(m, "Strand") 220 | .def(init([] { 221 | return StrandPtr(new Strand, DeleteOutsideGIL()); 222 | })); 223 | } 224 | 225 | } // namespace py 226 | } // namespace qi 227 | -------------------------------------------------------------------------------- /src/pytranslator.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace py = pybind11; 12 | 13 | namespace qi 14 | { 15 | namespace py 16 | { 17 | 18 | void exportTranslator(::py::module& m) 19 | { 20 | using namespace ::py; 21 | using namespace ::py::literals; 22 | 23 | GILAcquire lock; 24 | 25 | class_(m, "Translator") 26 | .def(init([](const std::string& name) { return new Translator(name); }), 27 | call_guard(), "name"_a) 28 | .def("translate", &Translator::translate, call_guard(), 29 | "msg"_a, "domain"_a = "", "locale"_a = "", "context"_a = "", 30 | doc("Translate a message from a domain to a locale.")) 31 | .def("translate", &Translator::translateContext, 32 | call_guard(), "msg"_a, "context"_a, 33 | doc("Translate a message with a context.")) 34 | .def("setCurrentLocale", &Translator::setCurrentLocale, 35 | call_guard(), "locale"_a, doc("Set the locale.")) 36 | .def("setDefaultDomain", &Translator::setDefaultDomain, "domain"_a, 37 | call_guard(), doc("Set the domain.")) 38 | .def("addDomain", &Translator::addDomain, "domain"_a, 39 | doc("Add a new domain.")); 40 | } 41 | 42 | } // namespace py 43 | } // namespace qi 44 | -------------------------------------------------------------------------------- /src/qimodule_python_plugin.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #include 7 | #include 8 | 9 | qiLogCategory("qi.python.module"); 10 | 11 | namespace 12 | { 13 | 14 | // Python AnyModule Factory 15 | qi::AnyModule importPyModule(const qi::ModuleInfo& /*name*/) 16 | { 17 | qiLogInfo() << "import in python not implemented yet"; 18 | return qi::AnyModule(); 19 | } 20 | 21 | } // namespace 22 | 23 | QI_REGISTER_MODULE_FACTORY_PLUGIN("python", &::importPyModule); 24 | -------------------------------------------------------------------------------- /tests/common.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #pragma once 7 | 8 | #ifndef QIPYTHON_TESTS_COMMON_HPP 9 | #define QIPYTHON_TESTS_COMMON_HPP 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | namespace qi 19 | { 20 | namespace py 21 | { 22 | namespace test 23 | { 24 | 25 | struct CheckPoint 26 | { 27 | CheckPoint(int count = 1) : count(count) {} 28 | CheckPoint(qi::Promise p) : count(1), prom(p) {} 29 | 30 | CheckPoint(const CheckPoint&) = delete; 31 | CheckPoint& operator=(const CheckPoint&) = delete; 32 | 33 | std::atomic_int count; 34 | qi::Promise prom = qi::Promise(); 35 | template 36 | void operator()(Args&&...) 37 | { 38 | if (--count == 0) 39 | prom.setValue(nullptr); 40 | } 41 | qi::Future fut() const { return prom.future(); } 42 | }; 43 | 44 | struct Execute 45 | { 46 | Execute() 47 | { 48 | GILAcquire lock; 49 | _locals.emplace(); 50 | } 51 | 52 | ~Execute() 53 | { 54 | GILAcquire lock; 55 | _locals.reset(); 56 | } 57 | 58 | void exec(const std::string& str) 59 | { 60 | GILAcquire lock; 61 | pybind11::exec(str, pybind11::globals(), *_locals); 62 | } 63 | 64 | pybind11::object eval(const std::string& str) 65 | { 66 | GILAcquire lock; 67 | return pybind11::eval(str, pybind11::globals(), *_locals); 68 | } 69 | 70 | const pybind11::dict& locals() const 71 | { 72 | return *_locals; 73 | } 74 | 75 | private: 76 | boost::optional _locals; 77 | }; 78 | 79 | template 80 | qi::Future toFutOf(qi::py::Future fut) 81 | { 82 | return fut.andThen([](const qi::AnyValue& val){ return val.to(); }); 83 | } 84 | 85 | template 86 | testing::AssertionResult finishesWithValue(const Fut& fut, 87 | qi::MilliSeconds timeout = qi::MilliSeconds{ 500 }) 88 | { 89 | const auto state = fut.wait(timeout); 90 | auto result = (state == qi::FutureState_FinishedWithValue) ? testing::AssertionSuccess() : 91 | testing::AssertionFailure(); 92 | result << ", the future state after " << timeout.count() << "ms is " << state; 93 | 94 | if (state == qi::FutureState_FinishedWithError) 95 | result << ", the error is " << fut.error(); 96 | 97 | return result; 98 | } 99 | 100 | } // namespace test 101 | } // namespace py 102 | } // namespace qi 103 | 104 | 105 | #endif // QIPYTHON_TESTS_COMMON_HPP 106 | -------------------------------------------------------------------------------- /tests/moduletest.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | qiLogCategory("qi.python.moduletest"); 6 | 7 | class Mouse 8 | { 9 | public: 10 | int squeak(); 11 | 12 | qi::Property size; 13 | }; 14 | 15 | QI_REGISTER_OBJECT(Mouse, squeak, size); 16 | 17 | int Mouse::squeak() 18 | { 19 | qiLogInfo() << "squeak"; 20 | return 18; 21 | } 22 | 23 | class Purr 24 | { 25 | public: 26 | Purr(std::shared_ptr> counter) : counter(counter) 27 | { 28 | qiLogInfo() << this << " purr constructor "; 29 | ++(*counter); 30 | } 31 | ~Purr() 32 | { 33 | qiLogInfo() << this << " purr destructor "; 34 | --(*counter); 35 | } 36 | void run() 37 | { 38 | qiLogInfo() << this << " purring"; 39 | } 40 | qi::Property volume; 41 | private: 42 | std::shared_ptr> counter; 43 | }; 44 | 45 | QI_REGISTER_OBJECT(Purr, run, volume); 46 | 47 | class Sleep 48 | { 49 | public: 50 | Sleep(std::shared_ptr> counter) : counter(counter) 51 | { 52 | qiLogInfo() << this << " sleep constructor "; 53 | ++(*counter); 54 | } 55 | ~Sleep() 56 | { 57 | qiLogInfo() << this << " sleep destructor "; 58 | --(*counter); 59 | } 60 | void run() 61 | { 62 | qiLogInfo() << this << " sleeping"; 63 | } 64 | private: 65 | std::shared_ptr> counter; 66 | }; 67 | 68 | QI_REGISTER_OBJECT(Sleep, run); 69 | 70 | class Play 71 | { 72 | public: 73 | Play(std::shared_ptr> counter) : counter(counter) 74 | { 75 | qiLogInfo() << this << " play constructor "; 76 | ++(*counter); 77 | } 78 | ~Play() 79 | { 80 | qiLogInfo() << this << " play destructor "; 81 | --(*counter); 82 | } 83 | void run() 84 | { 85 | qiLogInfo() << this << " playing"; 86 | } 87 | qi::Signal caught; 88 | private: 89 | std::shared_ptr> counter; 90 | }; 91 | 92 | QI_REGISTER_OBJECT(Play, run, caught); 93 | 94 | class Cat 95 | { 96 | public: 97 | Cat(); 98 | Cat(const std::string& s); 99 | Cat(const qi::SessionPtr& s); 100 | 101 | std::string meow(int volume); 102 | bool eat(const Mouse& m); 103 | 104 | boost::shared_ptr cloneMe() 105 | { 106 | return boost::make_shared(); 107 | } 108 | 109 | boost::shared_ptr makePurr() const 110 | { 111 | return boost::make_shared(purrCounter); 112 | } 113 | 114 | boost::shared_ptr makeSleep() const 115 | { 116 | return boost::make_shared(sleepCounter); 117 | } 118 | 119 | boost::shared_ptr makePlay() const 120 | { 121 | return boost::make_shared(playCounter); 122 | } 123 | 124 | void order(qi::AnyObject /*action*/) const 125 | { 126 | // Cats do not follow orders, they do nothing. 127 | } 128 | 129 | int nbPurr() 130 | { 131 | return purrCounter->load(); 132 | } 133 | 134 | int nbSleep() 135 | { 136 | return sleepCounter->load(); 137 | } 138 | 139 | int nbPlay() 140 | { 141 | return playCounter->load(); 142 | } 143 | 144 | qi::Property hunger; 145 | qi::Property boredom; 146 | qi::Property cuteness; 147 | 148 | std::shared_ptr> purrCounter = std::make_shared>(0); 149 | std::shared_ptr> sleepCounter = std::make_shared>(0); 150 | std::shared_ptr> playCounter = std::make_shared>(0); 151 | }; 152 | 153 | QI_REGISTER_OBJECT(Cat, meow, cloneMe, hunger, boredom, cuteness, 154 | makePurr, makeSleep, makePlay, order, 155 | nbPurr, nbSleep, nbPlay); 156 | 157 | Cat::Cat() 158 | { 159 | qiLogInfo() << "Cat constructor"; 160 | } 161 | 162 | Cat::Cat(const std::string& s) : Cat() 163 | { 164 | qiLogInfo() << "Cat string constructor: " << s; 165 | } 166 | 167 | Cat::Cat(const qi::SessionPtr& s) : Cat() 168 | { 169 | qiLogInfo() << "Cat string constructor with session"; 170 | s->services(); // SEGV? 171 | } 172 | 173 | std::string Cat::meow(int volume) 174 | { 175 | qiLogInfo() << "meow: " << volume; 176 | return "meow"; 177 | } 178 | 179 | bool Cat::eat(const Mouse&) 180 | { 181 | qiLogInfo() << "eating mouse"; 182 | return true; 183 | } 184 | 185 | int lol() 186 | { 187 | return 3; 188 | } 189 | 190 | void registerObjs(qi::ModuleBuilder* mb) 191 | { 192 | mb->advertiseFactory("Mouse"); 193 | mb->advertiseFactory("Cat"); 194 | mb->advertiseFactory("Cat"); 195 | mb->advertiseFactory("Cat"); 196 | mb->advertiseMethod("lol", &lol); 197 | mb->advertiseMethod("_hidden", []{}); 198 | } 199 | 200 | QI_REGISTER_MODULE("moduletest", ®isterObjs); 201 | -------------------------------------------------------------------------------- /tests/service_object_holder.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | using namespace qi; 7 | 8 | class ObjectHolder 9 | { 10 | public: 11 | void emit() 12 | { 13 | signal(_obj); 14 | } 15 | 16 | void setObject(AnyObject newObj) 17 | { 18 | _obj = newObj; 19 | } 20 | 21 | void resetObject() 22 | { 23 | _obj = {}; 24 | } 25 | 26 | AnyObject getObject() const 27 | { 28 | return _obj; 29 | } 30 | 31 | Signal signal; 32 | 33 | private: 34 | AnyObject _obj; 35 | }; 36 | 37 | QI_REGISTER_OBJECT(ObjectHolder, emit, setObject, resetObject, getObject, signal) 38 | 39 | int main(int argc, char** argv) 40 | { 41 | return ka::invoke_catch( 42 | ka::compose([] { return EXIT_FAILURE; }, 43 | qi::ExceptionLogError{ "service_object_holder.main", 44 | "Terminating because of unhandled error" }), 45 | [&] { 46 | ApplicationSession app{ argc, argv }; 47 | app.startSession(); 48 | 49 | auto sess = app.session(); 50 | if (!sess || !sess->isConnected()) 51 | throw std::runtime_error{ "Session is not connected." }; 52 | const auto serviceName = "ObjectHolder"; 53 | std::cout << "service_name=" << serviceName << std::endl; 54 | std::cout << "endpoint=" << sess->endpoints()[0].str() << std::endl; 55 | sess->registerService(serviceName, boost::make_shared()); 56 | app.run(); 57 | return EXIT_SUCCESS; 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /tests/test_guard.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | namespace 11 | { 12 | struct GuardTest : testing::Test 13 | { 14 | static thread_local bool guarded; 15 | void SetUp() override { guarded = false; } 16 | }; 17 | 18 | thread_local bool GuardTest::guarded = false; 19 | 20 | struct SetFlagGuard 21 | { 22 | SetFlagGuard() { GuardTest::guarded = true; } 23 | ~SetFlagGuard() { GuardTest::guarded = false; } 24 | }; 25 | } // namespace 26 | 27 | using InvokeGuardedTest = GuardTest; 28 | 29 | TEST_F(InvokeGuardedTest, ExecutesFunctionWithGuard) 30 | { 31 | using T = InvokeGuardedTest; 32 | 33 | EXPECT_FALSE(T::guarded); 34 | auto r = qi::py::invokeGuarded([](int i){ 35 | EXPECT_TRUE(T::guarded); 36 | return i + 10; 37 | }, 42); 38 | EXPECT_FALSE(T::guarded); 39 | EXPECT_EQ(52, r); 40 | } 41 | 42 | TEST(GILAcquire, IsReentrant) 43 | { 44 | qi::py::GILAcquire acq0; QI_IGNORE_UNUSED(acq0); 45 | qi::py::GILRelease rel; QI_IGNORE_UNUSED(rel); 46 | qi::py::GILAcquire acq1; QI_IGNORE_UNUSED(acq1); 47 | qi::py::GILAcquire acq2; QI_IGNORE_UNUSED(acq2); 48 | SUCCEED(); 49 | } 50 | 51 | TEST(GILRelease, IsReentrant) 52 | { 53 | qi::py::GILRelease rel0; QI_IGNORE_UNUSED(rel0); 54 | qi::py::GILAcquire acq; QI_IGNORE_UNUSED(acq); 55 | qi::py::GILRelease rel1; QI_IGNORE_UNUSED(rel1); 56 | qi::py::GILRelease rel2; QI_IGNORE_UNUSED(rel2); 57 | SUCCEED(); 58 | } 59 | 60 | struct SharedObject : testing::Test 61 | { 62 | SharedObject() 63 | { 64 | // GIL is only required for creation of the inner object. 65 | object = [&]{ 66 | qi::py::GILAcquire lock; 67 | return pybind11::capsule( 68 | &this->destroyed, 69 | [](void* destroyed){ 70 | *reinterpret_cast(destroyed) = true; 71 | } 72 | ); 73 | }(); 74 | } 75 | 76 | ~SharedObject() 77 | { 78 | if (object) { 79 | qi::py::GILAcquire lock; 80 | object = {}; 81 | } 82 | } 83 | 84 | bool destroyed = false; 85 | pybind11::capsule object; 86 | }; 87 | 88 | TEST_F(SharedObject, KeepsRefCount) 89 | { 90 | std::optional sharedObject = qi::py::SharedObject(std::move(object)); 91 | ASSERT_FALSE(object); // object has been released. 92 | EXPECT_FALSE(destroyed); // inner object is still alive. 93 | { 94 | auto sharedObjectCpy = *sharedObject; // copy the shared object locally. 95 | EXPECT_FALSE(destroyed); // inner object is maintained by both copies. 96 | sharedObject.reset(); 97 | EXPECT_FALSE(destroyed); // inner object is maintained by the copy. 98 | } 99 | EXPECT_TRUE(destroyed); // inner object has been destroyed. 100 | } 101 | 102 | TEST_F(SharedObject, TakeInnerStealsInnerRefCount) 103 | { 104 | std::optional sharedObject = qi::py::SharedObject(std::move(object)); 105 | auto inner = sharedObject->takeInner(); 106 | EXPECT_FALSE(sharedObject->inner()); // inner object is null. 107 | sharedObject.reset(); 108 | EXPECT_FALSE(destroyed); // inner object is still alive. 109 | qi::py::GILAcquire lock; // release local inner object, which requires the GIL. 110 | inner = {}; 111 | EXPECT_TRUE(destroyed); // inner object has been destroyed. 112 | } 113 | -------------------------------------------------------------------------------- /tests/test_module.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | qiLogCategory("TestQiPython.Module"); 10 | 11 | namespace py = pybind11; 12 | 13 | namespace 14 | { 15 | 16 | void globalExec(const std::string& str) 17 | { 18 | qi::py::GILAcquire _lock; 19 | py::exec(str.c_str()); 20 | } 21 | 22 | testing::AssertionResult assertGlobalExec(const std::string& str) 23 | { 24 | try 25 | { 26 | globalExec(str); 27 | } 28 | catch (const std::exception& ex) 29 | { 30 | return testing::AssertionFailure() << ex.what(); 31 | } 32 | return testing::AssertionSuccess() << "the code was executed successfully"; 33 | } 34 | 35 | bool sameModule(const qi::ModuleInfo& lhs, const qi::ModuleInfo& rhs) 36 | { 37 | return lhs.name == rhs.name && lhs.path == rhs.path && lhs.type == rhs.type; 38 | } 39 | 40 | } // namespace 41 | 42 | MATCHER(ModuleEq, "") { return sameModule(std::get<0>(arg), std::get<1>(arg)); } 43 | 44 | TEST(Module, listModules) 45 | { 46 | qi::py::GILAcquire _lock; 47 | auto qi = py::globals()["qi"]; 48 | auto modulesFromPython = 49 | qi::AnyReference::from(qi.attr("listModules")()).to>(); 50 | 51 | using namespace testing; 52 | ASSERT_THAT(modulesFromPython, Pointwise(ModuleEq(), qi::listModules())); 53 | } 54 | 55 | TEST(ModuleFromCpp, importModule) 56 | { 57 | EXPECT_TRUE(assertGlobalExec("qi.module('moduletest')")); 58 | } 59 | 60 | TEST(ModuleFromCpp, importAbsentModuleThrows) 61 | { 62 | EXPECT_FALSE(assertGlobalExec("qi.module('nobody_here')")); 63 | } 64 | 65 | TEST(ModuleFromCpp, callModuleHiddenMethod) 66 | { 67 | EXPECT_TRUE(assertGlobalExec("module=qi.module('moduletest')\nmodule._hidden()")); 68 | } 69 | -------------------------------------------------------------------------------- /tests/test_object.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include "common.hpp" 19 | 20 | namespace py = pybind11; 21 | using namespace qi::py; 22 | 23 | #define TEST_PYTHON_OBJECT_UID \ 24 | R"(b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14")" 25 | 26 | namespace 27 | { 28 | 29 | constexpr const auto declareTypeAWithUid = R"py( 30 | class A(object): 31 | __qi_objectuid__ = )py" TEST_PYTHON_OBJECT_UID; 32 | 33 | constexpr const std::array testUidData = 34 | { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 }; 35 | const auto testUid = *qi::deserializeObjectUid(testUidData); 36 | 37 | } 38 | 39 | struct ReadObjectUidTest : qi::py::test::Execute, testing::Test {}; 40 | 41 | TEST_F(ReadObjectUidTest, ReturnsEmptyIfAbsent) 42 | { 43 | GILAcquire lock; 44 | const auto a = eval("object()"); 45 | auto objectUid = qi::py::detail::readObjectUid(a); 46 | EXPECT_FALSE(objectUid); 47 | } 48 | 49 | TEST_F(ReadObjectUidTest, ReturnsValueIfPresent) 50 | { 51 | GILAcquire lock; 52 | exec(declareTypeAWithUid); 53 | auto a = eval("A()"); 54 | auto objectUid = qi::py::detail::readObjectUid(a); 55 | 56 | ASSERT_TRUE(objectUid); 57 | EXPECT_THAT(qi::serializeObjectUid>(*objectUid), 58 | testing::ElementsAreArray(testUidData)); 59 | } 60 | 61 | struct WriteObjectUidTest : qi::py::test::Execute, testing::Test 62 | { 63 | static constexpr const std::array data = 64 | { 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 65 | 110, 120, 130, 140, 150, 160, 170, 180, 190, 200 }; 66 | 67 | const qi::ObjectUid uid = *qi::deserializeObjectUid(data); 68 | }; 69 | 70 | constexpr const std::array WriteObjectUidTest::data; 71 | 72 | TEST_F(WriteObjectUidTest, CanBeReadAfterWritten) 73 | { 74 | GILAcquire lock; 75 | exec(R"( 76 | class A(object): 77 | pass 78 | )"); 79 | auto a = eval("A()"); 80 | 81 | qi::py::detail::writeObjectUid(a, uid); 82 | auto uidRead = qi::py::detail::readObjectUid(a); 83 | ASSERT_TRUE(uidRead); 84 | EXPECT_EQ(uid, *uidRead); 85 | } 86 | 87 | TEST_F(WriteObjectUidTest, CanBeReadAfterOverwritten) 88 | { 89 | GILAcquire lock; 90 | exec(declareTypeAWithUid); 91 | auto a = eval("A()"); 92 | 93 | auto uidRead = qi::py::detail::readObjectUid(a); 94 | EXPECT_TRUE(uidRead); 95 | EXPECT_NE(uid, uidRead); 96 | 97 | qi::py::detail::writeObjectUid(a, uid); 98 | uidRead = qi::py::detail::readObjectUid(a); 99 | ASSERT_TRUE(uidRead); 100 | EXPECT_EQ(uid, *uidRead); 101 | } 102 | 103 | struct ToObjectTest : qi::py::test::Execute, testing::Test 104 | { 105 | ToObjectTest() 106 | { 107 | constexpr const auto declareType = R"py( 108 | class Cookies(object): 109 | def __init__(self, count=1): 110 | self.count = count 111 | self.__qi_objectuid__ = )py" TEST_PYTHON_OBJECT_UID R"py( 112 | 113 | t = type(self) 114 | t.prepare.__qi_signature__ = "([s])" 115 | t.prepare.__qi_return_signature__ = "s" 116 | t.must_not_be_bound.__qi_signature__ = "DONOTBIND" 117 | t.unnamed.__qi_name__ = "do_nothing" 118 | 119 | @staticmethod 120 | def bake_instructions(temp, dura): 121 | return "Your cookies must be baked at {} degrees for {} minutes." \ 122 | .format(temp, dura) 123 | 124 | def bake(self, temp, dura): 125 | return [(temp, dura) for i in range(self.count)] 126 | 127 | def prepare_with(self, recipe, *ingredients): 128 | return "To prepare {} cookies according to {} recipe, " \ 129 | "you need {} as ingredients.".format(self.count, 130 | recipe, 131 | ", ".join(ingredients)) 132 | 133 | def prepare(self, ingredients): 134 | return "Put {} in a bowl".format(", ".join(ingredients)) 135 | 136 | def must_not_be_bound(self): 137 | pass 138 | 139 | def unnamed(self): 140 | return "This function does nothing." 141 | )py"; 142 | exec(declareType); 143 | } 144 | 145 | template 146 | qi::AnyObject makeObject(Args&&... args) 147 | { 148 | GILAcquire lock; 149 | const auto type = locals()["Cookies"]; 150 | return qi::py::toObject(type(std::forward(args)...)); 151 | } 152 | }; 153 | 154 | TEST_F(ToObjectTest, StaticMethodCanBeCalled) 155 | { 156 | auto obj = makeObject(); 157 | const auto res = obj.call("bake_instructions", 180, 20); 158 | EXPECT_EQ("Your cookies must be baked at 180 degrees for 20 minutes.", res); 159 | } 160 | 161 | TEST_F(ToObjectTest, MemberMethodCanBeCalled) 162 | { 163 | auto obj = makeObject(12); 164 | const auto res = obj.call>>("bake", 180, 20); 165 | const std::vector> expected(12, std::make_pair(180, 20)); 166 | EXPECT_EQ(expected, res); 167 | } 168 | 169 | TEST_F(ToObjectTest, MemberVariadicMethodCanBeCalled) 170 | { 171 | auto obj = makeObject(16); 172 | const auto res = 173 | obj.call("prepare_with", "grandma's", "150g of wheat", 174 | "100g of sugar", "2 eggs"); 175 | EXPECT_EQ( 176 | "To prepare 16 cookies according to grandma's recipe, you need 150g of " 177 | "wheat, 100g of sugar, 2 eggs as ingredients.", 178 | res); 179 | } 180 | 181 | TEST_F(ToObjectTest, MethodWithExplicitSignature) 182 | { 183 | auto obj = makeObject(); 184 | const auto metaObj = obj.metaObject(); 185 | const auto methods = metaObj.findMethod("prepare"); 186 | ASSERT_EQ(1, methods.size()); 187 | 188 | const auto method = methods[0]; 189 | EXPECT_EQ(qi::Signature("([s])"), method.parametersSignature()); 190 | EXPECT_EQ(qi::Signature("s"), method.returnSignature()); 191 | } 192 | 193 | TEST_F(ToObjectTest, MethodWithExplicitBindForbiddance) 194 | { 195 | auto obj = makeObject(); 196 | const auto metaObj = obj.metaObject(); 197 | const auto methods = metaObj.findMethod("must_not_be_bound"); 198 | ASSERT_TRUE(methods.empty()); 199 | } 200 | 201 | TEST_F(ToObjectTest, MethodWithOverridenName) 202 | { 203 | auto obj = makeObject(); 204 | auto res = obj.call("do_nothing"); 205 | EXPECT_EQ("This function does nothing.", res); 206 | } 207 | 208 | TEST_F(ToObjectTest, ObjectUidIsReused) 209 | { 210 | auto obj = makeObject(); 211 | EXPECT_EQ(testUid, obj.uid()); 212 | } 213 | 214 | struct ObjectTest : testing::Test 215 | { 216 | void SetUp() override 217 | { 218 | object = boost::make_shared(); 219 | 220 | GILAcquire lock; 221 | pyObject = qi::py::toPyObject(object); 222 | ASSERT_TRUE(pyObject); 223 | } 224 | 225 | void TearDown() override 226 | { 227 | GILAcquire lock; 228 | pyObject.release().dec_ref(); 229 | } 230 | 231 | struct Muffins 232 | { 233 | std::string count(int i) 234 | { 235 | std::ostringstream oss; 236 | oss << "You have " << i << " muffins."; 237 | return oss.str(); 238 | } 239 | 240 | qi::Property bakedCount; 241 | qi::Signal baked; 242 | }; 243 | 244 | qi::Object object; 245 | py::object pyObject; 246 | }; 247 | 248 | QI_REGISTER_OBJECT(ObjectTest::Muffins, count, bakedCount, baked) 249 | 250 | TEST_F(ObjectTest, IsValid) 251 | { 252 | GILAcquire lock; 253 | { 254 | EXPECT_TRUE(py::bool_(pyObject)); 255 | EXPECT_TRUE(py::bool_(pyObject.attr("isValid")())); 256 | } 257 | 258 | { 259 | const auto pyObject = qi::py::toPyObject(qi::py::Object{}); 260 | // It still returns a non null Python object. 261 | ASSERT_TRUE(pyObject); 262 | EXPECT_FALSE(py::bool_(pyObject)); 263 | EXPECT_FALSE(py::bool_(pyObject.attr("isValid")())); 264 | } 265 | } 266 | 267 | TEST_F(ObjectTest, Call) 268 | { 269 | GILAcquire lock; 270 | const auto res = pyObject.attr("call")("count", 832).cast(); 271 | EXPECT_EQ("You have 832 muffins.", res); 272 | } 273 | 274 | TEST_F(ObjectTest, Async) 275 | { 276 | GILAcquire lock; 277 | { 278 | const auto res = qi::py::test::toFutOf( 279 | py::cast(pyObject.attr("async")("count", 2356))); 280 | GILRelease unlock; 281 | ASSERT_TRUE(test::finishesWithValue(res)); 282 | EXPECT_EQ("You have 2356 muffins.", res.value()); 283 | } 284 | 285 | { 286 | const auto res = qi::py::test::toFutOf(py::cast( 287 | pyObject.attr("call")("count", 32897, py::arg("_async") = true))); 288 | GILRelease unlock; 289 | ASSERT_TRUE(test::finishesWithValue(res)); 290 | EXPECT_EQ("You have 32897 muffins.", res.value()); 291 | } 292 | } 293 | 294 | using ToPyObjectTest = ObjectTest; 295 | 296 | TEST_F(ToPyObjectTest, MethodCanBeCalled) 297 | { 298 | GILAcquire lock; 299 | { 300 | const auto res = pyObject.attr("count")(8).cast(); 301 | EXPECT_EQ("You have 8 muffins.", res); 302 | } 303 | 304 | // Can also be called asynchronously. 305 | { 306 | const auto res = qi::py::test::toFutOf(py::cast( 307 | pyObject.attr("count")(5, py::arg("_async") = true))); 308 | 309 | GILRelease unlock; 310 | ASSERT_TRUE(test::finishesWithValue(res)); 311 | EXPECT_EQ("You have 5 muffins.", res.value()); 312 | } 313 | } 314 | 315 | TEST_F(ToPyObjectTest, PropertyIsExposed) 316 | { 317 | GILAcquire lock; 318 | const py::object prop = pyObject.attr("bakedCount"); 319 | EXPECT_TRUE(qi::py::isProperty(prop)); 320 | } 321 | 322 | TEST_F(ToPyObjectTest, SignalIsExposed) 323 | { 324 | GILAcquire lock; 325 | const py::object sig = pyObject.attr("baked"); 326 | EXPECT_TRUE(qi::py::isSignal(sig)); 327 | } 328 | 329 | TEST_F(ToPyObjectTest, FutureAsObjectIsReturnedAsPyFuture) 330 | { 331 | GILAcquire lock; 332 | const auto res = pyObject.attr("count")(8).cast(); 333 | EXPECT_EQ("You have 8 muffins.", res); 334 | } 335 | -------------------------------------------------------------------------------- /tests/test_property.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include "common.hpp" 13 | 14 | namespace py = pybind11; 15 | using namespace qi::py; 16 | 17 | struct PropertyTest : qi::py::test::Execute, testing::Test 18 | { 19 | PropertyTest() 20 | { 21 | GILAcquire lock; 22 | type = py::globals()["qi"].attr("Property"); 23 | } 24 | 25 | ~PropertyTest() 26 | { 27 | GILAcquire lock; 28 | type.release().dec_ref(); 29 | } 30 | 31 | py::object type; 32 | }; 33 | 34 | TEST_F(PropertyTest, DefaultConstructedHasDynamicSignature) 35 | { 36 | GILAcquire lock; 37 | const auto pyProp = type(); 38 | const auto prop = pyProp.cast(); 39 | const auto sigChildren = prop->signature().children(); 40 | ASSERT_FALSE(sigChildren.empty()); 41 | EXPECT_EQ(qi::Signature::Type_Dynamic, sigChildren[0].type()); 42 | } 43 | 44 | TEST_F(PropertyTest, ConstructedWithSignature) 45 | { 46 | GILAcquire lock; 47 | const auto pyProp = 48 | type(qi::Signature::fromType(qi::Signature::Type_String).toString()); 49 | const auto prop = pyProp.cast(); 50 | const auto sigChildren = prop->signature().children(); 51 | ASSERT_FALSE(sigChildren.empty()); 52 | EXPECT_EQ(qi::Signature::Type_String, sigChildren[0].type()); 53 | } 54 | 55 | struct ConstructedPropertyTest : PropertyTest 56 | { 57 | ConstructedPropertyTest() 58 | { 59 | GILAcquire lock; 60 | pyProp = type("i"); 61 | prop = pyProp.cast(); 62 | } 63 | 64 | ~ConstructedPropertyTest() 65 | { 66 | GILAcquire lock; 67 | prop = nullptr; 68 | pyProp = {}; 69 | } 70 | 71 | py::object pyProp; 72 | qi::py::Property* prop = nullptr; 73 | }; 74 | 75 | using PropertyValueTest = ConstructedPropertyTest; 76 | 77 | TEST_F(PropertyValueTest, CanBeReadAfterSetFromCxx) 78 | { 79 | auto setFut = prop->setValue(839).async(); 80 | ASSERT_TRUE(test::finishesWithValue(setFut)); 81 | 82 | GILAcquire lock; 83 | const auto value = pyProp.attr("value")().cast(); 84 | EXPECT_EQ(839, value); 85 | } 86 | 87 | TEST_F(PropertyValueTest, ValueCanBeReadAfterSetFromCxxAsync) 88 | { 89 | auto setFut = prop->setValue(59940).async(); 90 | ASSERT_TRUE(test::finishesWithValue(setFut)); 91 | 92 | const auto valueFut = [&] { 93 | GILAcquire lock; 94 | return qi::py::test::toFutOf( 95 | pyProp.attr("value")(py::arg("_async") = true).cast()); 96 | }(); 97 | 98 | ASSERT_TRUE(test::finishesWithValue(valueFut)); 99 | EXPECT_EQ(59940, valueFut.value()); 100 | } 101 | 102 | using PropertySetValueTest = ConstructedPropertyTest; 103 | 104 | TEST_F(PropertySetValueTest, ValueCanBeReadFromCxx) 105 | { 106 | { 107 | GILAcquire lock; 108 | pyProp.attr("setValue")(4893); 109 | } 110 | 111 | const auto valueFut = qi::py::test::toFutOf(prop->value()); 112 | ASSERT_TRUE(test::finishesWithValue(valueFut)); 113 | EXPECT_EQ(4893, valueFut.value()); 114 | } 115 | 116 | namespace 117 | { 118 | } 119 | 120 | TEST_F(PropertySetValueTest, ValueCanBeReadFromCxxAsync) 121 | { 122 | const auto setValueFut = [&] { 123 | GILAcquire lock; 124 | return qi::py::test::toFutOf( 125 | pyProp.attr("setValue")(3423409, py::arg("_async") = true) 126 | .cast()); 127 | }(); 128 | 129 | ASSERT_TRUE(test::finishesWithValue(setValueFut)); 130 | 131 | const auto valueFut = qi::py::test::toFutOf(prop->value()); 132 | ASSERT_TRUE(test::finishesWithValue(valueFut)); 133 | EXPECT_EQ(3423409, valueFut.value()); 134 | } 135 | 136 | using PropertyAddCallbackTest = ConstructedPropertyTest; 137 | 138 | TEST_F(PropertyAddCallbackTest, CallbackIsCalledWhenValueIsSet) 139 | { 140 | using namespace testing; 141 | StrictMock> mockFn; 142 | 143 | { 144 | GILAcquire lock; 145 | const auto id = 146 | pyProp.attr("addCallback")(mockFn.AsStdFunction()).cast(); 147 | EXPECT_NE(qi::SignalBase::invalidSignalLink, id); 148 | } 149 | 150 | qi::py::test::CheckPoint cp; 151 | EXPECT_CALL(mockFn, Call(4893)).WillOnce(Invoke(std::ref(cp))); 152 | auto setValueFut = prop->setValue(4893); 153 | ASSERT_TRUE(test::finishesWithValue(setValueFut)); 154 | ASSERT_TRUE(test::finishesWithValue(cp.fut())); 155 | } 156 | 157 | TEST_F(PropertyAddCallbackTest, PythonCallbackIsCalledWhenValueIsSet) 158 | { 159 | using namespace testing; 160 | StrictMock> mockFn; 161 | 162 | { 163 | GILAcquire lock; 164 | auto pyFn = eval("lambda f : lambda i : f(i * 2)"); 165 | const auto id = pyProp.attr("addCallback")(pyFn(mockFn.AsStdFunction())) 166 | .cast(); 167 | EXPECT_NE(qi::SignalBase::invalidSignalLink, id); 168 | } 169 | 170 | qi::py::test::CheckPoint cp; 171 | EXPECT_CALL(mockFn, Call(15686)).WillOnce(Invoke(std::ref(cp))); 172 | auto setValueFut = prop->setValue(7843); 173 | ASSERT_TRUE(test::finishesWithValue(setValueFut)); 174 | ASSERT_TRUE(test::finishesWithValue(cp.fut())); 175 | } 176 | 177 | TEST_F(PropertyAddCallbackTest, CallbackIsCalledWhenValueIsSetAsync) 178 | { 179 | using namespace testing; 180 | StrictMock> mockFn; 181 | 182 | const auto idFut = [&] { 183 | GILAcquire lock; 184 | return qi::py::test::toFutOf( 185 | pyProp 186 | .attr("addCallback")(mockFn.AsStdFunction(), py::arg("_async") = true) 187 | .cast()); 188 | }(); 189 | ASSERT_TRUE(test::finishesWithValue(idFut)); 190 | EXPECT_NE(qi::SignalBase::invalidSignalLink, idFut.value()); 191 | 192 | qi::py::test::CheckPoint cp; 193 | EXPECT_CALL(mockFn, Call(7854)).WillOnce(Invoke(std::ref(cp))); 194 | auto setValueFut = prop->setValue(7854); 195 | ASSERT_TRUE(test::finishesWithValue(setValueFut)); 196 | ASSERT_TRUE(test::finishesWithValue(cp.fut())); 197 | } 198 | 199 | TEST_F(PropertyAddCallbackTest, CallbackThrowsWhenCalledDoesNotReportError) 200 | { 201 | using namespace testing; 202 | StrictMock> mockFn; 203 | 204 | { 205 | GILAcquire lock; 206 | const auto id = 207 | pyProp.attr("addCallback")(mockFn.AsStdFunction()).cast(); 208 | EXPECT_NE(qi::SignalBase::invalidSignalLink, id); 209 | } 210 | 211 | qi::py::test::CheckPoint cp; 212 | EXPECT_CALL(mockFn, Call(3287)) 213 | .WillOnce(DoAll(Invoke(std::ref(cp)), Throw(std::runtime_error("test error")))); 214 | auto setValueFut = prop->setValue(3287); 215 | ASSERT_TRUE(test::finishesWithValue(setValueFut)); 216 | ASSERT_TRUE(test::finishesWithValue(cp.fut())); 217 | 218 | GILAcquire lock; 219 | EXPECT_EQ(nullptr, PyErr_Occurred()); 220 | } 221 | 222 | using PropertyDisconnectTest = ConstructedPropertyTest; 223 | 224 | TEST_F(PropertyDisconnectTest, AddedCallbackCanBeDisconnected) 225 | { 226 | GILAcquire lock; 227 | const auto id = pyProp.attr("addCallback")(py::cpp_function([](int) {})) 228 | .cast(); 229 | EXPECT_NE(qi::SignalBase::invalidSignalLink, id); 230 | 231 | auto success = pyProp.attr("disconnect")(id).cast(); 232 | EXPECT_TRUE(success); 233 | } 234 | 235 | TEST_F(PropertyDisconnectTest, AddedCallbackCanBeDisconnectedAsync) 236 | { 237 | GILAcquire lock; 238 | const auto id = pyProp.attr("addCallback")(py::cpp_function([](int) {})) 239 | .cast(); 240 | EXPECT_NE(qi::SignalBase::invalidSignalLink, id); 241 | 242 | auto success = qi::py::test::toFutOf( 243 | pyProp.attr("disconnect")(id, py::arg("_async") = true) 244 | .cast()).value(); 245 | EXPECT_TRUE(success); 246 | } 247 | 248 | TEST_F(PropertyDisconnectTest, DisconnectRandomSignalLinkFails) 249 | { 250 | GILAcquire lock; 251 | const auto id = qi::SignalLink(42); 252 | 253 | auto success = pyProp.attr("disconnect")(id).cast(); 254 | EXPECT_FALSE(success); 255 | } 256 | -------------------------------------------------------------------------------- /tests/test_qipython.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | qiLogCategory("TestQiPython"); 15 | 16 | namespace py = pybind11; 17 | 18 | PYBIND11_EMBEDDED_MODULE(qi, m) { 19 | qi::py::exportAll(m); 20 | } 21 | 22 | int main(int argc, char **argv) 23 | { 24 | ::testing::InitGoogleTest(&argc, argv); 25 | 26 | pybind11::scoped_interpreter interp; 27 | 28 | boost::optional app; 29 | app.emplace(argc, argv); 30 | 31 | py::globals()["qi"] = py::module::import("qi"); 32 | 33 | 34 | int ret = EXIT_FAILURE; 35 | { 36 | qi::py::GILRelease unlock; 37 | ret = RUN_ALL_TESTS(); 38 | 39 | // Destroy the application outside of the GIL to avoid deadlocks, but while 40 | // the interpreter still runs to avoid crashes while trying to release the 41 | // references we still hold in callbacks. 42 | app.reset(); 43 | } 44 | 45 | return ret; 46 | } 47 | -------------------------------------------------------------------------------- /tests/test_qipython_local_interpreter.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | PYBIND11_EMBEDDED_MODULE(test_local_interpreter, m) { 14 | struct ObjectDtorOutsideGIL 15 | { 16 | ~ObjectDtorOutsideGIL() 17 | { 18 | qi::py::GILRelease _rel; 19 | // nothing. 20 | } 21 | 22 | }; 23 | pybind11::class_(m, "ObjectDtorOutsideGIL") 24 | .def(pybind11::init([]{ 25 | return std::make_unique(); 26 | })); 27 | } 28 | 29 | TEST(InterpreterFinalize, GarbageObjectDtorOutsideGIL) 30 | { 31 | pybind11::scoped_interpreter interp; 32 | pybind11::globals()["qitli"] = pybind11::module::import("test_local_interpreter"); 33 | pybind11::exec("obj = qitli.ObjectDtorOutsideGIL()"); 34 | } 35 | 36 | // This test checks that concurrent uses of GIL guards during finalization 37 | // of the interpreter does not cause crashes. 38 | // 39 | // It uses 2 threads: a main thread, and a second native thread. 40 | // 41 | // The main thread starts an interpreter, starts the second thread and 42 | // releases the GIL. The second thread acquires the GIL, then releases 43 | // it, and waits for the interpreter to finalize. Once it is finalized, 44 | // it tries to reacquire the GIL, and then release it, according to 45 | // GIL guards destructors. 46 | // 47 | // Horizontal lines are synchronization points between the threads. 48 | // 49 | // main thread (T1) 50 | // ~~~~~~~~~~~~~~~~ 51 | // ▼ 52 | // ╔═══════╪════════╗ 53 | // ║ interpreter ║ 54 | // ----------------------------------------------> start native thread T2 55 | // ║ ╎ ║ native thread (T2) 56 | // ║ ╎ ║ ~~~~~~~~~~~~~~~~~~ 57 | // ║ ╎ ║ ▼ 58 | // ║ ╔═════╪══════╗ ║ ╎ 59 | // ║ ║ GILRelease ║ ║ ╎ -> T1 releases the GIL 60 | // ----------------------------------------------> GIL shift T1 -> T2 61 | // ║ ║ ╎ ║ ║ ╔═══════╪════════╗ 62 | // ║ ║ ╎ ║ ║ ║ GILAcquire ║ -> T2 acquires the GIL 63 | // ║ ║ ╎ ║ ║ ║ ╔═════╪══════╗ ║ 64 | // ║ ║ ╎ ║ ║ ║ ║ GILRelease ║ ║ -> T2 releases the GIL 65 | // ----------------------------------------------> GIL shift T2 -> T1 66 | // ║ ╚═════╪══════╝ ║ ║ ║ ╎ ║ ║ -> T1 acquires the GIL 67 | // ╚═══════╪════════╝ ║ ║ ╎ ║ ║ -> interpreter starts finalizing 68 | // ----------------------------------------------> interpreter finalized 69 | // ╎ ║ ╚═════╪══════╝ ║ -> T2 tries to reacquire GIL but fails, it's a noop. 70 | // ╎ ╚═══════╪════════╝ -> T2 tries to release GIL, but it's a noop. 71 | // ╎ ╎ 72 | TEST(InterpreterFinalize, GILReleasedInOtherThread) 73 | { 74 | // Synchronization mechanism for the first GIL shift, otherwise T1 will release and reacquire the 75 | // GIL instantly without waiting for T2. 76 | qi::Promise shift1Prom; 77 | auto shift1Fut = shift1Prom.future(); 78 | // Second GIL shift does not require an additional synchronization 79 | // mechanism. Waiting for interpreter finalization ensures that the 80 | // GIL was acquired back by thread T1. 81 | // Synchronization mechanism for interpreter finalization. 82 | qi::Promise finalizedProm; 83 | std::future asyncFut; 84 | { 85 | pybind11::scoped_interpreter interp; 86 | asyncFut = std::async( 87 | std::launch::async, 88 | [finalizedFut = finalizedProm.future(), shift1Prom]() mutable { 89 | qi::py::GILAcquire acquire; 90 | // First GIL shift is done. 91 | shift1Prom.setValue(nullptr); 92 | qi::py::GILRelease release; 93 | finalizedFut.value(); 94 | }); 95 | qi::py::GILRelease release; 96 | shift1Fut.value(); 97 | } 98 | finalizedProm.setValue(nullptr); 99 | // Join the thread, wait for the task to finish and ensure no exception was thrown. 100 | asyncFut.get(); 101 | } 102 | 103 | int main(int argc, char **argv) 104 | { 105 | ::testing::InitGoogleTest(&argc, argv); 106 | qi::Application app(argc, argv); 107 | return RUN_ALL_TESTS(); 108 | } 109 | -------------------------------------------------------------------------------- /tests/test_signal.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright (C) 2020 SoftBank Robotics Europe 3 | ** See COPYING for the license 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include "common.hpp" 14 | 15 | namespace py = pybind11; 16 | using namespace qi::py; 17 | 18 | struct DefaultConstructedSignalTest : qi::py::test::Execute, testing::Test 19 | { 20 | DefaultConstructedSignalTest() 21 | { 22 | GILAcquire lock; 23 | type = py::globals()["qi"].attr("Signal"); 24 | pySig = type(); 25 | sig = pySig.cast(); 26 | } 27 | 28 | ~DefaultConstructedSignalTest() 29 | { 30 | GILAcquire lock; 31 | sig = nullptr; 32 | pySig = {}; 33 | type = {}; 34 | } 35 | 36 | py::object type; 37 | py::object pySig; 38 | qi::py::Signal* sig = nullptr; 39 | }; 40 | 41 | TEST_F(DefaultConstructedSignalTest, AcceptsCallbackWithoutArgument) 42 | { 43 | using namespace testing; 44 | StrictMock> mockFn; 45 | 46 | { 47 | GILAcquire lock; 48 | const auto id = pySig.attr("connect")(mockFn.AsStdFunction()) 49 | .cast(); 50 | EXPECT_NE(qi::SignalBase::invalidSignalLink, id); 51 | } 52 | 53 | qi::py::test::CheckPoint cp; 54 | EXPECT_CALL(mockFn, Call()).WillOnce(Invoke(std::ref(cp))); 55 | (*sig)(); 56 | ASSERT_TRUE(test::finishesWithValue(cp.fut())); 57 | } 58 | 59 | TEST_F(DefaultConstructedSignalTest, AcceptsCallbackWithAnyArgument) 60 | { 61 | using namespace testing; 62 | StrictMock> mockFn; 63 | 64 | { 65 | GILAcquire lock; 66 | const auto id = pySig.attr("connect")(mockFn.AsStdFunction()) 67 | .cast(); 68 | EXPECT_NE(qi::SignalBase::invalidSignalLink, id); 69 | } 70 | 71 | qi::py::test::CheckPoint cp(2); 72 | EXPECT_CALL(mockFn, Call(13, "cookies")).WillOnce(Invoke(std::ref(cp))); 73 | EXPECT_CALL(mockFn, Call(52, "muffins")).WillOnce(Invoke(std::ref(cp))); 74 | using Ref = qi::AutoAnyReference; 75 | using RefVec = qi::AnyReferenceVector; 76 | sig->trigger(RefVec{ Ref(13), Ref("cookies") }); 77 | sig->trigger(RefVec{ Ref(52), Ref("muffins") }); 78 | sig->trigger(RefVec{ Ref("cupcakes") }); // wrong arguments types, the callback is not called. 79 | ASSERT_TRUE(test::finishesWithValue(cp.fut())); 80 | } 81 | 82 | struct ConstructedThroughServiceSignalTest : qi::py::test::Execute, testing::Test 83 | { 84 | void SetUp() override 85 | { 86 | servSession = qi::makeSession(); 87 | ASSERT_TRUE( 88 | test::finishesWithValue(servSession->listenStandalone("tcp://localhost:0"))); 89 | clientSession = qi::makeSession(); 90 | ASSERT_TRUE( 91 | test::finishesWithValue(clientSession->connect(servSession->url()))); 92 | 93 | { 94 | GILAcquire lock; 95 | exec(R"py( 96 | class Cookies(object): 97 | def __init__(self): 98 | self.baked = qi.Signal() 99 | )py"); 100 | 101 | const auto pyObj = eval("Cookies()"); 102 | pySig = pyObj.attr(sigName.c_str()); 103 | servObj = qi::py::toObject(pyObj); 104 | } 105 | 106 | ASSERT_TRUE(test::finishesWithValue( 107 | servSession->registerService(serviceName, servObj))); 108 | ASSERT_TRUE( 109 | test::finishesWithValue(clientSession->waitForService(serviceName))); 110 | const auto clientObjFut = clientSession->service(serviceName).async(); 111 | ASSERT_TRUE(test::finishesWithValue(clientObjFut)); 112 | clientObj = clientObjFut.value(); 113 | } 114 | 115 | void TearDown() override 116 | { 117 | GILAcquire lock; 118 | pySig = {}; 119 | } 120 | 121 | const std::string serviceName = "Cookies"; 122 | const std::string sigName = "baked"; 123 | py::object pySig; 124 | qi::SessionPtr servSession; 125 | qi::SessionPtr clientSession; 126 | qi::AnyObject servObj; 127 | qi::AnyObject clientObj; 128 | }; 129 | 130 | TEST_F(ConstructedThroughServiceSignalTest, 131 | AcceptDynamicCallbackThroughService) 132 | { 133 | using namespace testing; 134 | StrictMock> mockFn; 135 | 136 | ASSERT_TRUE(test::finishesWithValue( 137 | clientObj.connect(sigName, qi::AnyFunction::fromDynamicFunction( 138 | [&](const qi::AnyReferenceVector&) { 139 | mockFn.Call(); 140 | return qi::AnyValue::makeVoid().release(); 141 | })))); 142 | 143 | { 144 | GILAcquire lock; 145 | pySig(); 146 | } 147 | 148 | qi::py::test::CheckPoint cp; 149 | EXPECT_CALL(mockFn, Call()).WillOnce(Invoke(std::ref(cp))); 150 | EXPECT_TRUE(test::finishesWithValue(cp.fut())); 151 | } 152 | --------------------------------------------------------------------------------