├── .clang-format ├── .github ├── dependabot.yml ├── release-drafter.yml └── workflows │ ├── labeler.yml │ ├── main.yml │ ├── master-merge.yml │ └── release.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── docs ├── Doxyfile ├── Makefile └── source │ ├── appendix.rst │ ├── arrays.rst │ ├── conf.py │ ├── index.rst │ ├── install.rst │ ├── shock-and-awe.rst │ ├── tutorial.rst │ └── tutorial │ ├── functions.rst │ ├── libpy_tutorial │ ├── __init__.py │ ├── arrays.cc │ ├── classes.cc │ ├── data │ │ └── original.png │ ├── exceptions.cc │ ├── function.cc │ ├── ndarrays.cc │ └── scalar_functions.cc │ └── setup.py ├── etc ├── asan-path ├── build-and-run ├── detect-compiler.cc ├── ext_suffix.py ├── ld_flags.py └── python_version.py ├── gdb └── libpy-gdb.py ├── include └── libpy │ ├── abi.h │ ├── any.h │ ├── any_vector.h │ ├── autoclass.h │ ├── autofunction.h │ ├── automodule.h │ ├── borrowed_ref.h │ ├── buffer.h │ ├── build_tuple.h │ ├── call_function.h │ ├── char_sequence.h │ ├── datetime64.h │ ├── demangle.h │ ├── detail │ ├── api.h │ ├── autoclass_cache.h │ ├── autoclass_object.h │ ├── no_destruct_wrapper.h │ ├── numpy.h │ └── python.h │ ├── devirtualize.h │ ├── dict_range.h │ ├── exception.h │ ├── from_object.h │ ├── getattr.h │ ├── gil.h │ ├── hash.h │ ├── itertools.h │ ├── library_wrappers │ └── sparsehash.h │ ├── meta.h │ ├── ndarray_view.h │ ├── numpy_utils.h │ ├── object_map_key.h │ ├── owned_ref.h │ ├── range.h │ ├── scope_guard.h │ ├── singletons.h │ ├── str_convert.h │ ├── stream.h │ ├── table.h │ ├── table_details.h │ ├── to_object.h │ └── util.h ├── libpy ├── __init__.py ├── _build-and-run ├── _detect-compiler.cc ├── build.py └── include ├── setup.py ├── src ├── abi.cc ├── autoclass.cc ├── buffer.cc ├── demangle.cc ├── dict_range.cc ├── exception.cc ├── gil.cc ├── object_map_key.cc └── range.cc ├── testleaks.supp ├── tests ├── .dir-locals.el ├── __init__.py ├── _runner.cc ├── _test_automodule.cc ├── conftest.py ├── cxx.py ├── library_wrappers │ └── test_sparsehash.cc ├── test_any.cc ├── test_any_vector.cc ├── test_autoclass.cc ├── test_autofunction.cc ├── test_automodule.py ├── test_call_function.cc ├── test_cxx.py ├── test_datetime64.cc ├── test_demangle.cc ├── test_devirtualize.cc ├── test_dict_range.cc ├── test_exception.cc ├── test_from_object.cc ├── test_getattr.cc ├── test_hash.cc ├── test_itertools.cc ├── test_meta.cc ├── test_ndarray_view.cc ├── test_numpy_utils.h ├── test_object_map_key.cc ├── test_range.cc ├── test_scope_guard.cc ├── test_scoped_ref.cc ├── test_singletons.cc ├── test_str_convert.cc ├── test_table.cc ├── test_to_object.cc ├── test_util.cc └── test_utils.h ├── tox.ini └── version /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: llvm 2 | 3 | BreakBeforeBraces: Custom 4 | BraceWrapping: 5 | BeforeElse: true 6 | BeforeCatch: true 7 | 8 | AccessModifierOffset: -4 9 | AlignEscapedNewlines: Right 10 | AllowAllParametersOfDeclarationOnNextLine: false 11 | AllowShortBlocksOnASingleLine: false 12 | AllowShortFunctionsOnASingleLine: Empty 13 | AlwaysBreakTemplateDeclarations: true 14 | BinPackArguments: false 15 | BinPackParameters: false 16 | BreakConstructorInitializers: BeforeColon 17 | ColumnLimit: 90 18 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 19 | IndentWidth: 4 20 | NamespaceIndentation: None 21 | PointerAlignment: Left 22 | SortIncludes: true 23 | SortUsingDeclarations: true 24 | SpacesBeforeTrailingComments: 2 25 | SpacesInSquareBrackets: false 26 | SpaceAfterCStyleCast: true 27 | SpaceAfterTemplateKeyword: false 28 | 29 | PenaltyBreakAssignment: 60 30 | PenaltyBreakBeforeFirstCallParameter: 175 31 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | 2 | name-template: '$NEXT_PATCH_VERSION 🌈' 3 | tag-template: '$NEXT_PATCH_VERSION' 4 | categories: 5 | - title: 'API Changes' 6 | labels: 7 | - 'api' 8 | - title: 'Benchmark Changes' 9 | labels: 10 | - 'bench' 11 | - title: 'Build Changes' 12 | labels: 13 | - 'build' 14 | - title: 'Bug Fixes' 15 | labels: 16 | - 'bug' 17 | - title: 'Deprecation' 18 | labels: 19 | - 'deprecation' 20 | - title: 'Development Enhancements' 21 | labels: 22 | - 'development' 23 | - title: 'Documentation Updates' 24 | labels: 25 | - 'documentation' 26 | - title: 'Enhancements' 27 | labels: 28 | - 'enhancement' 29 | - title: 'Maintenance' 30 | labels: 31 | - 'maintenance' 32 | - title: 'Reverts' 33 | labels: 34 | - 'revert' 35 | - title: 'Style Changes' 36 | labels: 37 | - 'style' 38 | - title: 'Test Changes' 39 | labels: 40 | - 'test' 41 | - title: 'Release Changes' 42 | labels: 43 | - 'release' 44 | template: | 45 | # What’s Changed 46 | $CHANGES 47 | -------------------------------------------------------------------------------- /.github/workflows/labeler.yml: -------------------------------------------------------------------------------- 1 | name: "Pull Request Labeler" 2 | on: 3 | pull_request_target: 4 | types: [opened, synchronize, reopened, edited] 5 | 6 | jobs: 7 | pr-labeler: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Label the PR 11 | uses: gerrymanoim/pr-prefix-labeler@v3 12 | env: 13 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 14 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build-and-test: 13 | 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | os: [ubuntu-20.04, ubuntu-18.04, macos-10.15] 19 | python-version: [3.5, 3.6, 3.8] 20 | compiler: [gcc, clang] 21 | exclude: 22 | - os: macos-10.15 23 | compiler: gcc 24 | include: 25 | - python-version: 3.5 26 | numpy: 1.11.3 27 | - python-version: 3.6 28 | numpy: 1.19 29 | - python-version: 3.8 30 | numpy: 1.19 31 | steps: 32 | - uses: actions/checkout@v2 33 | with: 34 | submodules: 'recursive' 35 | - name: Set release name env variable (ubuntu) 36 | if: startsWith(matrix.os, 'ubuntu') 37 | run: | 38 | echo ::set-env name=UBUNTU_RELEASE::$(lsb_release -sc) 39 | - name: Install newer clang (ubuntu) 40 | if: startsWith(matrix.os, 'ubuntu') && matrix.compiler == 'clang' 41 | run: | 42 | wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key 2>/dev/null | sudo apt-key add - 43 | sudo add-apt-repository "deb http://apt.llvm.org/$UBUNTU_RELEASE/ llvm-toolchain-$UBUNTU_RELEASE-10 main" -y 44 | sudo apt-get update -q 45 | sudo apt-get install -y clang-10 lld-10 libc++-10-dev libc++abi-10-dev clang-tools-10 46 | echo ::set-env name=AR::llvm-ar-10 47 | - name: Install newer clang (macos) 48 | if: startsWith(matrix.os, 'macos') && matrix.compiler == 'clang' 49 | run: | 50 | brew install llvm 51 | - name: Set up Python ${{ matrix.python-version }} 52 | uses: actions/setup-python@v2.1.4 53 | with: 54 | python-version: ${{ matrix.python-version }} 55 | - name: Install python dependencies 56 | env: 57 | PYTHONWARNINGS: ignore:DEPRECATION::pip._internal.cli.base_command 58 | run: | 59 | python -m pip install --upgrade pip 60 | pip install pytest==4.4.1 numpy==${{ matrix.numpy }} 61 | - name: Install c++ dependencies (ubuntu) 62 | if: startsWith(matrix.os, 'ubuntu') 63 | run: | 64 | sudo apt-get -y install libsparsehash-dev doxygen 65 | - name: Install c++ dependencies (macos) 66 | if: startsWith(matrix.os, 'macos') 67 | run: | 68 | brew install google-sparsehash doxygen 69 | - name: Set llvm related envvars (macos) 70 | if: startsWith(matrix.os, 'macos') 71 | run: | 72 | echo ::set-env name=EXTRA_INCLUDE_DIRS::/usr/local/include/ 73 | echo ::add-path::/usr/local/opt/llvm/bin 74 | echo ::set-env name=LDFLAGS::-L/usr/local/opt/llvm/lib 75 | echo ::set-env name=CPPFLAGS::-I/usr/local/opt/llvm/include 76 | echo ::set-env name=AR::llvm-ar 77 | - name: Set clang envvars 78 | if: matrix.compiler == 'clang' 79 | run: | 80 | echo ::set-env name=CC::clang-10 81 | echo ::set-env name=CXX::clang++ 82 | - name: Set gcc envvars 83 | if: matrix.compiler == 'gcc' 84 | run: | 85 | echo ::set-env name=CC::gcc-9 86 | echo ::set-env name=CXX::g++-9 87 | - name: Run the tests 88 | run: | 89 | make -j2 test 90 | - name: Build and install from an sdist 91 | run: | 92 | python setup.py sdist 93 | pip install dist/libpy-*.tar.gz 94 | - name: Check that docs can be built 95 | run: | 96 | pip install sphinx sphinx_rtd_theme breathe ipython 97 | make docs 98 | -------------------------------------------------------------------------------- /.github/workflows/master-merge.yml: -------------------------------------------------------------------------------- 1 | name: On Master Merge 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | draft-release-publish: 10 | name: Draft a new release 11 | runs-on: ubuntu-latest 12 | steps: 13 | # Drafts your next Release notes as Pull Requests are merged into "master" 14 | - uses: release-drafter/release-drafter@v5 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python 🐍 distributions 📦 to PyPI 2 | on: 3 | release: 4 | types: [published] 5 | 6 | jobs: 7 | build-n-publish: 8 | name: Build and publish Python 🐍 distributions 📦 to TestPyPI 9 | runs-on: ubuntu-18.04 10 | steps: 11 | - uses: actions/checkout@v2 12 | with: 13 | submodules: 'recursive' 14 | - name: Set up Python 3.8 15 | uses: actions/setup-python@v2.1.4 16 | with: 17 | python-version: 3.8 18 | - name: Install python dependencies 19 | env: 20 | PYTHONWARNINGS: ignore:DEPRECATION::pip._internal.cli.base_command 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install numpy==1.19 24 | - name: Install c++ dependencies (ubuntu) 25 | run: | 26 | sudo apt-get -y install libsparsehash-dev doxygen 27 | - name: Set gcc envvars 28 | run: | 29 | echo ::set-env name=CC::gcc-8 30 | echo ::set-env name=CXX::g++-8 31 | - name: Build sdist 32 | run: python setup.py sdist 33 | 34 | - name: Publish distribution 📦 to Test PyPI 35 | uses: pypa/gh-action-pypi-publish@master 36 | with: 37 | skip_existing: true 38 | password: ${{ secrets.test_pypi_password }} 39 | repository_url: https://test.pypi.org/legacy/ 40 | 41 | - name: Install from test and test running 42 | run: | 43 | pip install --extra-index-url https://test.pypi.org/simple libpy 44 | mkdir tmp && cd tmp && python -c 'import libpy;print(libpy.__version__)' 45 | pip uninstall -y libpy 46 | cd .. 47 | 48 | - name: Publish distribution 📦 to PyPI 49 | uses: pypa/gh-action-pypi-publish@master 50 | with: 51 | skip_existing: true 52 | password: ${{ secrets.pypi_password }} 53 | 54 | - name: Install and test running 55 | run: | 56 | pip install libpy 57 | cd tmp && python -c 'import libpy;print(libpy.__version__)' 58 | cd .. 59 | 60 | - name: Build the docs 61 | run: | 62 | pip install sphinx sphinx_rtd_theme breathe ipython 63 | make docs 64 | 65 | - name: Deploy the docs 66 | uses: peaceiris/actions-gh-pages@v3 67 | with: 68 | github_token: ${{ secrets.GITHUB_TOKEN }} 69 | publish_dir: ./docs/build/html 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | 3 | # Mac OSX folder properties 4 | .DS_Store 5 | 6 | 7 | # test runner built with `make test` 8 | tests/run 9 | bench/run 10 | 11 | # doc artifacts 12 | html/* 13 | docs/doxygen-build/* 14 | docs/build/* 15 | docs/.built-doxygen 16 | docs/.installed-tutorial 17 | 18 | # scratch file for testing linking 19 | scratch.cc 20 | a.out 21 | 22 | # compiled artifacts 23 | *.o 24 | *.a 25 | *.so* 26 | 27 | # make artifacts 28 | *.d 29 | .make/* 30 | 31 | # coverage artifacts 32 | *.gcov 33 | *.gcno 34 | *.gcda 35 | 36 | # etags 37 | TAGS 38 | 39 | # Local makefile overrides. 40 | Makefile.local 41 | 42 | .gdb_history 43 | 44 | *.pyc 45 | 46 | # make helper 47 | .compiler_flags 48 | 49 | testbin 50 | 51 | # jenkins test output 52 | libpy_report.xml 53 | 54 | # tox envs 55 | .tox/* 56 | .cache/* 57 | 58 | # pytest artifact 59 | .coverage 60 | 61 | # python support artifacts 62 | *.egg-info/* 63 | docs/source/tutorial/libpy_tutorial.egg-info/* 64 | docs/source/savefig/* 65 | dist/* 66 | 67 | venv*/* 68 | 69 | .dir-locals.el 70 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "submodules/googletest"] 2 | path = submodules/googletest 3 | url = git@github.com:google/googletest 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # buildtime data 2 | include Makefile 3 | include etc/detect-compiler.cc 4 | include etc/build-and-run 5 | include etc/ext_suffix.py 6 | include etc/asan-path 7 | include etc/ld_flags.py 8 | include etc/python_version.py 9 | include version 10 | recursive-include src/ *.cc 11 | recursive-include include/ *.h 12 | 13 | # runtime data 14 | include LICENSE 15 | include libpy/_build-and-run 16 | include libpy/_detect-compiler.cc 17 | recursive-include libpy *.h 18 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ``libpy`` 2 | ========= 3 | 4 | .. image:: https://github.com/quantopian/libpy/workflows/CI/badge.svg 5 | :alt: GitHub Actions status 6 | :target: https://github.com/quantopian/libpy/actions?query=workflow%3ACI+branch%3Amaster 7 | 8 | .. image:: https://badge.fury.io/py/libpy.svg 9 | :target: https://badge.fury.io/py/libpy 10 | 11 | ``libpy`` is a library to help you write amazing Python extensions in C++. 12 | ``libpy`` makes it easy to expose C++ code to Python. 13 | ``libpy`` lets you automatically wrap functions and classes. 14 | ``libpy`` is designed for high performance and safety: libpy extension modules should be both faster and safer than using the C API directly. 15 | 16 | `Full documentation `_ 17 | 18 | Requirements 19 | ------------ 20 | 21 | libpy supports: 22 | 23 | - macOS/Linux 24 | - Python >=3.5 25 | 26 | libpy requires: 27 | 28 | - gcc>=9 or clang>=10 29 | - numpy>=1.11.3 30 | 31 | Optional Requirements 32 | --------------------- 33 | 34 | libpy optionally provides wrappers for the following libraries: 35 | 36 | - google sparsehash 37 | 38 | 39 | Install 40 | ------- 41 | 42 | To install for development: 43 | 44 | .. code-block:: bash 45 | 46 | $ make 47 | 48 | Otherwise, ``pip install libpy``, making sure ``CC`` and ``CXX`` environment variables are set to the the right compiler. 49 | 50 | **Note**: The installation of ``libpy`` will use the ``python`` executable to 51 | figure out information about your environment. If you are not using a virtual 52 | environment or ``python`` does not point to the Python installation you want 53 | to use (checked with ``which python`` and ``python --version``) you must 54 | point to your Python executable using the ``PYTHON`` environment variable, 55 | i.e. ``PYTHON=python3 make`` or ``PYTHON=python3 pip3 install libpy``. 56 | 57 | Tests 58 | ----- 59 | 60 | To run the unit tests, invoke: 61 | 62 | .. code-block:: bash 63 | 64 | $ make test 65 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | SPHINXOPTS ?= 2 | SPHINXBUILD ?= sphinx-build 3 | SOURCEDIR = source 4 | BUILDDIR = build 5 | 6 | # Put it first so that "make" without argument is like "make help". 7 | .PHONY: help 8 | help: 9 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 10 | 11 | .PHONY: Makefile 12 | 13 | # Catch-all target: route all unknown targets to Sphinx using the new 14 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 15 | %: .built-doxygen .installed-tutorial Makefile 16 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 17 | 18 | Doxyfile: 19 | touch $@ 20 | 21 | .built-doxygen: Doxyfile $(shell find ../include/libpy/ -type f -name '*.h') 22 | doxygen 23 | @touch $@ 24 | 25 | .installed-tutorial: 26 | pip install -e source/tutorial 27 | @touch $@ 28 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | project = 'libpy' 2 | copyright = '2020, Quantopian Inc.' 3 | author = 'Quantopian Inc.' 4 | 5 | # The full version, including alpha/beta/rc tags 6 | release = '0.1.0' 7 | 8 | extensions = [ 9 | 'breathe', 10 | 'IPython.sphinxext.ipython_console_highlighting', 11 | 'IPython.sphinxext.ipython_directive', 12 | 'sphinx.ext.autodoc', 13 | ] 14 | 15 | breathe_projects = {'libpy': '../doxygen-build/xml'} 16 | breathe_default_project = 'libpy' 17 | 18 | 19 | # Add any paths that contain templates here, relative to this directory. 20 | templates_path = ['_templates'] 21 | 22 | # List of patterns, relative to source directory, that match files and 23 | # directories to ignore when looking for source files. 24 | # This pattern also affects html_static_path and html_extra_path. 25 | exclude_patterns = [] 26 | 27 | # The theme to use for HTML and HTML Help pages. See the documentation for 28 | # a list of builtin themes. 29 | # 30 | html_theme = "sphinx_rtd_theme" 31 | 32 | # Add any paths that contain custom static files (such as style sheets) here, 33 | # relative to this directory. They are copied after the builtin static files, 34 | # so a file named "default.css" will overwrite the builtin "default.css". 35 | html_static_path = ['_static'] 36 | 37 | highlight_language = 'cpp' 38 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to libpy's documentation! 2 | ================================= 3 | 4 | ``libpy`` is a library to help you write amazing Python extensions in C++. 5 | ``libpy`` makes it easy to expose C++ code to Python. 6 | ``libpy`` lets you automatically wrap functions and classes. 7 | ``libpy`` is designed for high performance and safety: libpy extension modules should be both faster and safer than using the C API directly. 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | shock-and-awe 14 | install 15 | tutorial 16 | arrays 17 | appendix 18 | 19 | 20 | 21 | Indices and tables 22 | ================== 23 | 24 | * :ref:`genindex` 25 | * :ref:`modindex` 26 | * :ref:`search` 27 | -------------------------------------------------------------------------------- /docs/source/install.rst: -------------------------------------------------------------------------------- 1 | Setup 2 | ===== 3 | 4 | Requirements 5 | ------------ 6 | 7 | lipy supports: 8 | 9 | - macOS/Linux 10 | - Python >=3.5 11 | 12 | lipy requires: 13 | 14 | - gcc>=9 or clang>=10 15 | - numpy>=1.11.3 16 | 17 | Optional Requirements 18 | --------------------- 19 | 20 | libpy optionally provides wrappers for the following libraries: 21 | 22 | - google sparsehash 23 | 24 | Install 25 | ------- 26 | 27 | To install for development: 28 | 29 | .. code-block:: bash 30 | 31 | $ make 32 | 33 | Otherwise, ``pip install libpy``, making sure ``CC`` and ``CXX`` environment variables are set to the the right compiler. 34 | 35 | .. note:: 36 | The installation of ``libpy`` will use the ``python`` executable to 37 | figure out information about your environment. If you are not using a virtual 38 | environment or ``python`` does not point to the Python installation you want 39 | to use (checked with ``which python`` and ``python --version``) you must 40 | point to your Python executable using the ``PYTHON`` environment variable, 41 | i.e. ``PYTHON=python3 make`` or ``PYTHON=python3 pip3 install libpy``. 42 | 43 | Tests 44 | ----- 45 | 46 | To run the unit tests, invoke: 47 | 48 | .. code-block:: bash 49 | 50 | $ make test 51 | -------------------------------------------------------------------------------- /docs/source/shock-and-awe.rst: -------------------------------------------------------------------------------- 1 | ==================== 2 | Shock and Awe [#f1]_ 3 | ==================== 4 | 5 | A *concise* overview of ``libpy``. For an introduction to extending Python with C or C++ please see `the Python documentation `_ or `Joe Jevnik's C Extension Tutorial `_. 6 | 7 | Simple Scalar Functions 8 | ======================= 9 | 10 | We start by building simple scalar functions in C++ which we can call from Python. 11 | 12 | .. ipython:: python 13 | 14 | from libpy_tutorial import scalar_functions 15 | 16 | A simple scalar function: 17 | 18 | .. literalinclude:: tutorial/libpy_tutorial/scalar_functions.cc 19 | :lines: 11-13 20 | 21 | .. ipython:: python 22 | 23 | scalar_functions.bool_scalar(False) 24 | 25 | A great way to use ``libpy`` is to write the code that needs to be fast in C++ and expose that code via Python. Let's estimate ``pi`` using a monte carlo simulation: 26 | 27 | .. literalinclude:: tutorial/libpy_tutorial/scalar_functions.cc 28 | :lines: 15-30 29 | 30 | .. ipython:: python 31 | 32 | scalar_functions.monte_carlo_pi(10000000) 33 | 34 | Of course, we can build C++ functions that support all the features of regular Python functions. 35 | 36 | ``libpy`` supports optional args: 37 | 38 | .. literalinclude:: tutorial/libpy_tutorial/scalar_functions.cc 39 | :lines: 34-36 40 | 41 | .. ipython:: python 42 | 43 | scalar_functions.optional_arg(b"An argument was passed") 44 | scalar_functions.optional_arg() 45 | 46 | and keyword/optional keyword arguments: 47 | 48 | .. literalinclude:: tutorial/libpy_tutorial/scalar_functions.cc 49 | :lines: 38-44 50 | 51 | .. ipython:: python 52 | 53 | scalar_functions.keyword_args(kw_arg_kwd=1) 54 | scalar_functions.keyword_args(kw_arg_kwd=1, opt_kw_arg_kwd=55) 55 | 56 | 57 | Working With Arrays 58 | =================== 59 | 60 | In order to write performant code it is often useful to write vectorized functions that act on arrays. Thus, libpy has extenstive support for ``numpy`` arrays. 61 | 62 | .. ipython:: python 63 | 64 | from libpy_tutorial import arrays 65 | import numpy as np 66 | 67 | We can take ``numpy`` arrays as input: 68 | 69 | .. literalinclude:: tutorial/libpy_tutorial/arrays.cc 70 | :lines: 11-17 71 | 72 | .. ipython:: python 73 | 74 | some_numbers = np.arange(20000) 75 | arrays.simple_sum(some_numbers) 76 | 77 | and return them as output: 78 | 79 | .. literalinclude:: tutorial/libpy_tutorial/arrays.cc 80 | :lines: 23-43 81 | 82 | .. ipython:: python 83 | 84 | prime_mask = arrays.is_prime(some_numbers) 85 | some_numbers[prime_mask][:100] 86 | 87 | .. note:: ``numpy`` arrays passed to C++ are `ranges `_. 88 | 89 | .. literalinclude:: tutorial/libpy_tutorial/arrays.cc 90 | :lines: 19-21 91 | 92 | .. ipython:: python 93 | 94 | arrays.simple_sum_iterator(some_numbers) 95 | 96 | N Dimensional Arrays 97 | ==================== 98 | 99 | We can also work with n-dimensional arrays. As a motivating example, let's sharpen an image. Specifically - we will sharpen: 100 | 101 | .. ipython:: python 102 | 103 | from PIL import Image 104 | import matplotlib.pyplot as plt # to show the image in documenation 105 | import numpy as np 106 | import pkg_resources 107 | img_file = pkg_resources.resource_stream("libpy_tutorial", "data/original.png") 108 | img = Image.open(img_file) 109 | @savefig original.png width=200px 110 | plt.imshow(img) 111 | 112 | .. literalinclude:: tutorial/libpy_tutorial/ndarrays.cc 113 | :lines: 10-55 114 | 115 | .. ipython:: python 116 | 117 | pixels = np.array(img) 118 | kernel = np.array([ 119 | [0, -1, 0], 120 | [-1, 5, -1], 121 | [0, -1, 0] 122 | ]) # already normalized 123 | from libpy_tutorial import ndarrays 124 | res = ndarrays.apply_kernel(pixels, kernel) 125 | @savefig sharpened.png width=200px 126 | plt.imshow(res) 127 | 128 | 129 | .. note:: We are able to pass a shaped n-dimensional array as input and return one as output. 130 | 131 | 132 | Creating Classes 133 | ================ 134 | 135 | ``libpy`` also allows you to construct C++ classes and then easily expose them as if they are regular Python classes. 136 | 137 | .. ipython:: python 138 | 139 | from libpy_tutorial.classes import Vec3d 140 | 141 | C++ classes are able to emulate all the features of Python classes: 142 | 143 | .. literalinclude:: tutorial/libpy_tutorial/classes.cc 144 | :lines: 9-67 145 | 146 | .. literalinclude:: tutorial/libpy_tutorial/classes.cc 147 | :lines: 93-106 148 | 149 | .. ipython:: python 150 | 151 | Vec3d.__doc__ 152 | v = Vec3d(1, 2, 3) 153 | v 154 | str(v) 155 | v.x(), v.y(), v.z() 156 | w = Vec3d(4, 5, 6); w 157 | v + w 158 | v * w 159 | v.magnitude() 160 | 161 | Exceptions 162 | ========== 163 | 164 | Working with exceptions is also important. 165 | 166 | .. ipython:: python 167 | 168 | from libpy_tutorial import exceptions 169 | 170 | We can throw exceptions in C++ that will then be dealt with in Python. Two patterns: 171 | 172 | 1. Throw your own exception: ``throw py::exception(type, msg...)``, maybe in response to an exception from a C-API function. 173 | 2. Throw a C++ exception directly. 174 | 175 | .. literalinclude:: tutorial/libpy_tutorial/exceptions.cc 176 | :lines: 11-17 177 | 178 | :: 179 | 180 | In [40]: exceptions.throw_value_error(4) 181 | --------------------------------------------------------------------------- 182 | ValueError Traceback (most recent call last) 183 | in 184 | ----> 1 exceptions.throw_value_error(4) 185 | 186 | ValueError: You passed 4 and this is the exception 187 | 188 | In [41]: exceptions.raise_from_cxx() 189 | --------------------------------------------------------------------------- 190 | RuntimeError Traceback (most recent call last) 191 | in 192 | ----> 1 exceptions.raise_from_cxx() 193 | 194 | RuntimeError: a C++ exception was raised: Supposedly a bad argument was used 195 | 196 | .. rubric:: Footnotes 197 | 198 | .. [#f1] With naming credit to the intorduction of `Q for Mortals `_. 199 | -------------------------------------------------------------------------------- /docs/source/tutorial.rst: -------------------------------------------------------------------------------- 1 | Tutorial 2 | ======== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Contents: 7 | 8 | tutorial/functions 9 | -------------------------------------------------------------------------------- /docs/source/tutorial/functions.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Functions 3 | ========= 4 | 5 | The simplest unit of code that can be exposed to Python from C++ is a function. 6 | ``libpy`` supports automatically converting C++ functions into Python functions by adapting the parameter types and return type. 7 | 8 | ``libpy`` uses :cpp:func:`py::autofunction` to convert a C++ function into a Python function definition [#f1]_. 9 | The result of :cpp:func:`py::autofunction` can be attached to a Python module object and made available to Python. 10 | 11 | A Simple C++ Function 12 | ===================== 13 | 14 | Let's start by writing a simple C++ function to expose to Python: 15 | 16 | .. code-block:: c++ 17 | 18 | double fma(double a, double b, double c) { 19 | return a * b + c; 20 | } 21 | 22 | 23 | ``fma`` is a standard C++ function with no knowledge of Python. 24 | 25 | 26 | Adapting a Function 27 | =================== 28 | 29 | To adapt ``fma`` into a Python function, we need to use :cpp:func:`py::autofunction`. 30 | 31 | .. code-block:: c++ 32 | 33 | PyMethodDef fma_methoddef = py::autofunction("fma"); 34 | 35 | :cpp:func:`py::autofunction` is a template function which takes as a template argument the C++ function to adapt. 36 | :cpp:func:`py::autofunction` also takes a string which is the name of the function as it will be exposed to Python. 37 | The Python function name does not need to match the C++ name. 38 | :cpp:func:`py::autofunction` takes an optional second argument: a string to use as the Python docstring. 39 | For example, a docstring could be added to ``fma`` with: 40 | 41 | .. code-block:: c++ 42 | 43 | PyMethodDef fma_methoddef = py::autofunction("fma", "Fused Multiply Add"); 44 | 45 | .. warning:: 46 | 47 | Currently the ``name`` and ``doc`` string parameters **must outlive** the resulting :c:struct:`PyMethodDef`. 48 | In practice, this means it should be a static string, or string literal. 49 | 50 | Adding the Function to a Module 51 | =============================== 52 | 53 | To use an adapted function from Python, it must be attached to a module so that it may be imported by Python code. 54 | To create a Python method, we can use :c:macro:`LIBPY_AUTOMETHOD`. 55 | :c:macro:`LIBPY_AUTOMETHOD` is a macro which takes in the package name, the module name, and the set of functions to add. 56 | Following the call to :c:macro:`LIBPY_AUTOMETHOD`, we must provide a function which is called when the module is first imported. 57 | To just add functions, our body can be a simple ``return false`` to indicate that no errors occurred. 58 | 59 | .. code-block:: c++ 60 | 61 | LIBPY_AUTOMODULE(libpy_tutorial, function, ({fma_methoddef})) 62 | (py::borrowed_ref<>) { 63 | return false; 64 | } 65 | 66 | Building and Importing the Module 67 | ================================= 68 | 69 | To build a libpy extension, we can use ``setup.py`` and libpy's :class:`~libpy.build.LibpyExtension` class. 70 | 71 | In the ``setup.py``\'s ``setup`` call, we can add a list of ``ext_modules`` to be built: 72 | 73 | .. code-block:: python 74 | 75 | from libpy.build import LibpyExtension 76 | 77 | setup( 78 | # ... 79 | ext_modules=[ 80 | LibpyExtension( 81 | 'libpy_tutorial.function', 82 | ['libpy_tutorial/function.cc'], 83 | ), 84 | ], 85 | # ... 86 | ) 87 | 88 | Now, the extension can be built with: 89 | 90 | .. code-block:: bash 91 | 92 | $ python setup.py build_ext --inplace 93 | 94 | Finally, the function can be imported and used from python: 95 | 96 | 97 | .. ipython:: python 98 | 99 | import libpy # we need to ensure we import libpy before importing our extension 100 | from libpy_tutorial.function import fma 101 | fma(2.0, 3.0, 4.0) 102 | 103 | .. rubric:: Footnotes 104 | 105 | .. [#f1] :cpp:func:`py::autofunction` creates a :c:struct:`PyMethodDef` instance, which is not yet a Python object. 106 | -------------------------------------------------------------------------------- /docs/source/tutorial/libpy_tutorial/__init__.py: -------------------------------------------------------------------------------- 1 | import libpy # noqa 2 | -------------------------------------------------------------------------------- /docs/source/tutorial/libpy_tutorial/arrays.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace libpy_tutorial { 10 | std::int64_t simple_sum(py::array_view values) { 11 | std::int64_t out = 0; 12 | for (auto value : values) { 13 | out += value; 14 | } 15 | return out; 16 | } 17 | 18 | std::int64_t simple_sum_iterator(py::array_view values) { 19 | return std::accumulate(values.begin(), values.end(), 0); 20 | } 21 | 22 | void negate_inplace(py::array_view values) { 23 | std::transform(values.cbegin(), 24 | values.cend(), 25 | values.begin(), 26 | [](std::int64_t v) { return -v; }); 27 | } 28 | 29 | bool check_prime(std::int64_t n) { 30 | if (n <= 3) { 31 | return n > 1; 32 | } 33 | else if (n % 2 == 0 || n % 3 == 0) { 34 | return false; 35 | } 36 | for (auto i = 5; std::pow(i, 2) < n; i += 6) { 37 | if (n % i == 0 || n % (i + 2) == 0) { 38 | return false; 39 | } 40 | } 41 | return true; 42 | } 43 | 44 | py::owned_ref<> is_prime(py::array_view values) { 45 | std::vector out(values.size()); 46 | std::transform(values.begin(), values.end(), out.begin(), check_prime); 47 | 48 | return py::move_to_numpy_array(std::move(out)); 49 | } 50 | 51 | LIBPY_AUTOMODULE(libpy_tutorial, 52 | arrays, 53 | ({py::autofunction("simple_sum"), 54 | py::autofunction("simple_sum_iterator"), 55 | py::autofunction("negate_inplace"), 56 | py::autofunction("is_prime")})) 57 | (py::borrowed_ref<>) { 58 | return false; 59 | } 60 | } // namespace libpy_tutorial 61 | -------------------------------------------------------------------------------- /docs/source/tutorial/libpy_tutorial/classes.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | namespace libpy_tutorial { 10 | class vec3d { 11 | private: 12 | std::array m_values; 13 | 14 | public: 15 | vec3d(double x, double y, double z) : m_values({x, y, z}) {} 16 | 17 | double x() const { 18 | return m_values[0]; 19 | } 20 | 21 | double y() const { 22 | return m_values[1]; 23 | } 24 | 25 | double z() const { 26 | return m_values[2]; 27 | } 28 | 29 | vec3d operator+(const vec3d& other) const { 30 | return {x() + other.x(), y() + other.y(), z() + other.z()}; 31 | } 32 | 33 | vec3d operator-(const vec3d& other) const { 34 | return {x() - other.x(), y() - other.y(), z() - other.z()}; 35 | } 36 | 37 | double operator*(const vec3d& other) const { 38 | return std::inner_product(m_values.begin(), 39 | m_values.end(), 40 | other.m_values.begin(), 41 | 0.0); 42 | } 43 | 44 | double magnitude() const { 45 | return std::sqrt(*this * *this); 46 | } 47 | }; 48 | 49 | std::ostream& operator<<(std::ostream& s, const vec3d& v) { 50 | return s << '{' << v.x() << ", " << v.y() << ", " << v.z() << '}'; 51 | } 52 | 53 | // `repr` could also be a member function, but free functions are useful for adding 54 | // a Python repr without modifying the methods of the type. 55 | std::string repr(const vec3d& v) { 56 | std::stringstream ss; 57 | ss << "Vec3d(" << v.x() << ", " << v.y() << ", " << v.z() << ')'; 58 | return ss.str(); 59 | } 60 | } // namespace libpy_tutorial 61 | 62 | namespace py::dispatch { 63 | // Make it possible to convert a `vec3d` into a Python object. 64 | template<> 65 | struct LIBPY_NO_EXPORT to_object 66 | : public py::autoclass::to_object {}; 67 | } // namespace py::dispatch 68 | 69 | namespace libpy_tutorial { 70 | 71 | using namespace std::string_literals; 72 | 73 | LIBPY_AUTOMODULE(libpy_tutorial, classes, ({})) 74 | (py::borrowed_ref<> m) { 75 | py::owned_ref t = 76 | py::autoclass(PyModule_GetName(m.get()) + ".Vec3d"s) 77 | .doc("An efficient 3-vector.") // add a class docstring 78 | .new_() //__new__ takes parameters 79 | // bind the named methods to Python 80 | .def<&vec3d::x>("x") 81 | .def<&vec3d::y>("y") 82 | .def<&vec3d::z>("z") 83 | .def<&vec3d::magnitude>("magnitude") 84 | .str() // set `operator<<(std::ostream&, vec3d) to `str(x)` in Python 85 | .repr() // set `repr` to be the result of `repr(x)` in Python 86 | .arithmetic() // bind the arithmetic operators to their Python 87 | // equivalents 88 | .type(); 89 | return PyObject_SetAttrString(m.get(), "Vec3d", static_cast(t)); 90 | } 91 | } // namespace libpy_tutorial 92 | -------------------------------------------------------------------------------- /docs/source/tutorial/libpy_tutorial/data/original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantopian/libpy/e174ee103db76a9d0fcd29165d54c676ed1f2629/docs/source/tutorial/libpy_tutorial/data/original.png -------------------------------------------------------------------------------- /docs/source/tutorial/libpy_tutorial/exceptions.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | namespace libpy_tutorial { 10 | 11 | void throw_value_error(int a) { 12 | throw py::exception(PyExc_ValueError, "You passed ", a, " and this is the exception"); 13 | } 14 | 15 | void raise_from_cxx() { 16 | throw std::invalid_argument("Supposedly a bad argument was used"); 17 | } 18 | 19 | LIBPY_AUTOMODULE(libpy_tutorial, 20 | exceptions, 21 | ({py::autofunction("throw_value_error"), 22 | py::autofunction("raise_from_cxx")})) 23 | (py::borrowed_ref<>) { 24 | return false; 25 | } 26 | 27 | } // namespace libpy_tutorial 28 | -------------------------------------------------------------------------------- /docs/source/tutorial/libpy_tutorial/function.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | namespace libpy_tutorial { 5 | double fma(double a, double b, double c) { 6 | return a * b + c; 7 | } 8 | 9 | PyMethodDef fma_methoddef = py::autofunction("fma", "Fused Multiply Add"); 10 | 11 | LIBPY_AUTOMODULE(libpy_tutorial, function, ({fma_methoddef})) 12 | (py::borrowed_ref<>) { 13 | return false; 14 | } 15 | } // namespace libpy_tutorial 16 | -------------------------------------------------------------------------------- /docs/source/tutorial/libpy_tutorial/ndarrays.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace libpy_tutorial { 9 | 10 | py::owned_ref<> apply_kernel(py::ndarray_view pixels, 11 | py::ndarray_view kernel) { 12 | 13 | auto n_dimensions = pixels.shape()[2]; 14 | auto n_rows = pixels.shape()[0]; 15 | auto n_columns = pixels.shape()[1]; 16 | 17 | auto k_rows = kernel.shape()[0]; 18 | auto k_columns = kernel.shape()[1]; 19 | std::vector out(n_dimensions * n_rows * n_columns, 0); 20 | py::ndarray_view out_view(out.data(), 21 | pixels.shape(), 22 | {static_cast(n_dimensions * n_rows), 23 | static_cast(n_dimensions), 24 | 1}); 25 | 26 | for (std::size_t dim = 0; dim < n_dimensions; ++dim) { 27 | for (std::size_t row = 0; row < n_rows; ++row) { 28 | for (std::size_t column = 0; column < n_columns; ++column) { 29 | 30 | auto accumulated_sum = 0.0; 31 | 32 | for (std::size_t k_row = 0; k_row < k_rows; ++k_row) { 33 | for (std::size_t k_column = 0; k_column < k_columns; ++k_column) { 34 | 35 | auto input_row_idx = row + 1 - k_row; 36 | auto input_column_idx = column + 1 - k_column; 37 | 38 | if (input_row_idx < n_rows && input_column_idx < n_columns) { 39 | accumulated_sum += 40 | pixels(input_row_idx, input_column_idx, dim) * 41 | kernel(k_row, k_column); 42 | } 43 | } 44 | } 45 | if (accumulated_sum < 0) { 46 | accumulated_sum = 0; 47 | } 48 | else if (accumulated_sum > 255) { 49 | accumulated_sum = 255; 50 | } 51 | out_view(row, column, dim) = accumulated_sum; 52 | } 53 | } 54 | } 55 | return py::move_to_numpy_array(std::move(out), 56 | py::new_dtype(), 57 | pixels.shape(), 58 | pixels.strides()); 59 | } 60 | 61 | LIBPY_AUTOMODULE(libpy_tutorial, 62 | ndarrays, 63 | ({py::autofunction("apply_kernel")})) 64 | (py::borrowed_ref<>) { 65 | return false; 66 | } 67 | } // namespace libpy_tutorial 68 | -------------------------------------------------------------------------------- /docs/source/tutorial/libpy_tutorial/scalar_functions.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace libpy_tutorial { 10 | 11 | bool bool_scalar(bool a) { 12 | return !a; 13 | } 14 | 15 | double monte_carlo_pi(int n_samples) { 16 | int accumulator = 0; 17 | 18 | std::random_device rd; // Will be used to obtain a seed for the random number engine 19 | std::mt19937 gen(rd()); // Standard mersenne_twister_engine seeded with rd() 20 | std::uniform_real_distribution<> dis(0, 1); 21 | 22 | for (int i = 0; i < n_samples; ++i) { 23 | auto x = dis(gen); 24 | auto y = dis(gen); 25 | if ((std::pow(x, 2) + std::pow(y, 2)) < 1.0) { 26 | accumulator += 1; 27 | } 28 | } 29 | return 4.0 * accumulator / n_samples; 30 | } 31 | 32 | using namespace py::cs::literals; 33 | 34 | std::string optional_arg(py::arg::optional opt_arg) { 35 | return opt_arg.get().value_or("default value"); 36 | } 37 | 38 | py::owned_ref<> 39 | keyword_args(py::arg::kwd kw_arg_kwd, 40 | py::arg::opt_kwd opt_kw_arg_kwd) { 41 | 42 | return py::build_tuple(kw_arg_kwd.get(), opt_kw_arg_kwd.get()); 43 | } 44 | 45 | LIBPY_AUTOMODULE(libpy_tutorial, 46 | scalar_functions, 47 | ({py::autofunction("bool_scalar"), 48 | py::autofunction("monte_carlo_pi"), 49 | py::autofunction("optional_arg"), 50 | py::autofunction("keyword_args")})) 51 | (py::borrowed_ref<>) { 52 | return false; 53 | } 54 | 55 | } // namespace libpy_tutorial 56 | -------------------------------------------------------------------------------- /docs/source/tutorial/setup.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import glob 3 | import os 4 | import sys 5 | 6 | from libpy.build import LibpyExtension 7 | from setuptools import find_packages, setup 8 | 9 | if ast.literal_eval(os.environ.get("LIBPY_TUTORIAL_DEBUG_BUILD", "0")): 10 | optlevel = 0 11 | debug_symbols = True 12 | max_errors = 5 13 | else: 14 | optlevel = 3 15 | debug_symbols = False 16 | max_errors = None 17 | 18 | 19 | def extension(*args, **kwargs): 20 | extra_compile_args = [] 21 | if sys.platform == 'darwin': 22 | extra_compile_args.append('-mmacosx-version-min=10.15') 23 | 24 | return LibpyExtension( 25 | *args, 26 | optlevel=optlevel, 27 | debug_symbols=debug_symbols, 28 | werror=True, 29 | max_errors=max_errors, 30 | include_dirs=["."] + kwargs.pop("include_dirs", []), 31 | extra_compile_args=extra_compile_args, 32 | depends=glob.glob("**/*.h", recursive=True), 33 | **kwargs 34 | ) 35 | 36 | 37 | install_requires = [ 38 | 'setuptools', 39 | 'libpy', 40 | 'matplotlib', 41 | 'pillow', 42 | ] 43 | 44 | setup( 45 | name="libpy_tutorial", 46 | version="0.1.0", 47 | description="Tutorial for libpy", 48 | author="Quantopian Inc.", 49 | author_email="opensource@quantopian.com", 50 | packages=find_packages(), 51 | package_data={ 52 | "": ["*.png"], 53 | }, 54 | include_package_data=True, 55 | install_requires=install_requires, 56 | license="Apache 2.0", 57 | url="https://github.com/quantopian/libpy", 58 | ext_modules=[ 59 | extension( 60 | "libpy_tutorial.scalar_functions", 61 | ["libpy_tutorial/scalar_functions.cc"], 62 | ), 63 | extension( 64 | "libpy_tutorial.arrays", 65 | ["libpy_tutorial/arrays.cc"], 66 | ), 67 | extension( 68 | "libpy_tutorial.ndarrays", 69 | ["libpy_tutorial/ndarrays.cc"], 70 | ), 71 | extension( 72 | "libpy_tutorial.exceptions", 73 | ["libpy_tutorial/exceptions.cc"], 74 | ), 75 | extension( 76 | "libpy_tutorial.classes", 77 | ["libpy_tutorial/classes.cc"], 78 | ), 79 | extension( 80 | "libpy_tutorial.function", 81 | ["libpy_tutorial/function.cc"], 82 | ), 83 | ], 84 | ) 85 | -------------------------------------------------------------------------------- /etc/asan-path: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ubuntu="/usr/lib/gcc/x86_64-linux-gnu/$($CXX -dumpversion)/libasan.so" 4 | arch="/usr/lib/libasan.so" 5 | macos="/usr/local/lib/gcc/$($CXX -dumpversion)/libasan.dylib" 6 | if [ -f "$ubuntu" ];then 7 | echo $ubuntu 8 | elif [ -f "$arch" ];then 9 | echo $arch 10 | elif [ -f "$macos" ];then 11 | echo $macos 12 | else 13 | echo "could not find libasan.so" 14 | exit 1 15 | fi 16 | -------------------------------------------------------------------------------- /etc/build-and-run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | dir=$(mktemp -d) 4 | file=$dir/a.out 5 | ${CXX:-g++} $@ -o $file 6 | $file 7 | rm -rf dir 8 | -------------------------------------------------------------------------------- /etc/detect-compiler.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() { 4 | const char* name; 5 | #if defined(__clang__) 6 | name = "CLANG"; 7 | #elif defined (__GNUC__) 8 | name = "GCC"; 9 | #else 10 | name = "UNKNOWN"; 11 | #endif 12 | std::cout << name << '\n'; 13 | return 0; 14 | } 15 | -------------------------------------------------------------------------------- /etc/ext_suffix.py: -------------------------------------------------------------------------------- 1 | import sysconfig 2 | print(sysconfig.get_config_var('EXT_SUFFIX') or '.so') 3 | -------------------------------------------------------------------------------- /etc/ld_flags.py: -------------------------------------------------------------------------------- 1 | # via https://github.com/python/cpython/blob/deb016224cc506503fb05e821a60158c83918ed4/Misc/python-config.in#L50 # noqa 2 | 3 | import sysconfig 4 | 5 | libs = [] 6 | libpl = sysconfig.get_config_vars('LIBPL') 7 | if libpl: 8 | libs.append("-L"+libpl[0]) 9 | 10 | libpython = sysconfig.get_config_var('LIBPYTHON') 11 | if libpython: 12 | libs.append(libpython) 13 | libs.extend(sysconfig.get_config_vars("LIBS", "SYSLIBS")) 14 | print(' '.join(libs)) 15 | -------------------------------------------------------------------------------- /etc/python_version.py: -------------------------------------------------------------------------------- 1 | import sys 2 | print(' '.join(map(str, sys.version_info[:2]))) 3 | -------------------------------------------------------------------------------- /gdb/libpy-gdb.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import gdb 4 | import numpy as np 5 | 6 | 7 | def pretty_printer(cls): 8 | gdb.pretty_printers.append(cls.maybe_construct) 9 | return cls 10 | 11 | 12 | @pretty_printer 13 | class Datetime64: 14 | _pattern = re.compile( 15 | r'^py::datetime64 > >$' 17 | ) 18 | 19 | _units = { 20 | (1, 1000000000): 'ns', 21 | (1, 1000000): 'ms', 22 | (1, 1000): 'us', 23 | (1, 1): 's', 24 | (60, 1): 'm', 25 | (60 * 60, 1): 'h', 26 | (60 * 60 * 24, 1): 'D', 27 | } 28 | 29 | def __init__(self, val, count, unit): 30 | self.val = val 31 | self.count = count 32 | self.unit = unit 33 | 34 | @classmethod 35 | def maybe_construct(cls, val): 36 | underlying = gdb.types.get_basic_type(val.type) 37 | 38 | match = cls._pattern.match(str(underlying)) 39 | if match is None: 40 | return None 41 | 42 | num = int(match[1]) 43 | den = int(match[2]) 44 | return cls(val, int(val['m_value']['__r']), cls._units[num, den]) 45 | 46 | def children(self): 47 | yield 'm_value', str(np.datetime64(self.count, self.unit)) 48 | 49 | def to_string(self): 50 | return f'py::datetime64' 51 | -------------------------------------------------------------------------------- /include/libpy/abi.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "libpy/detail/api.h" 4 | #include "libpy/exception.h" 5 | 6 | namespace py::abi { 7 | /** Structure for holding the ABI version of the libpy library. 8 | 9 | @note This must match `libpy.load_library._abi_verson` in the Python support library. 10 | */ 11 | struct abi_version { 12 | int major; 13 | int minor; 14 | int patch; 15 | }; 16 | 17 | LIBPY_EXPORT std::ostream& operator<<(std::ostream&, abi_version); 18 | 19 | namespace detail { 20 | constexpr abi_version header_libpy_abi_version{LIBPY_MAJOR_VERSION, 21 | LIBPY_MINOR_VERSION, 22 | LIBPY_MICRO_VERSION}; 23 | } // namespace detail 24 | 25 | /** The version of the libpy shared object. 26 | */ 27 | extern "C" LIBPY_EXPORT abi_version libpy_abi_version; 28 | 29 | /** Check if two abi versions are compatible. 30 | 31 | @param provider The version of the implementation provider. 32 | @param consumer The version of the implementation consumer. 33 | */ 34 | inline bool compatible_versions(abi_version provider, abi_version consumer) { 35 | return provider.major == consumer.major && provider.minor >= consumer.minor; 36 | } 37 | 38 | /** Check that the ABI of the libpy object is compatible with an extension module 39 | compiled against libpy. 40 | 41 | @return true with a Python exception raised if the ABI versions are incompatible. 42 | */ 43 | inline bool ensure_compatible_libpy_abi() { 44 | if (!compatible_versions(libpy_abi_version, detail::header_libpy_abi_version)) { 45 | py::raise(PyExc_ImportError) 46 | << "libpy compiled version is incompatible with the compiled version of this " 47 | "extension module\nlibpy version: " 48 | << libpy_abi_version 49 | << "\nthis library: " << detail::header_libpy_abi_version; 50 | return true; 51 | } 52 | return false; 53 | } 54 | } // namespace py::abi 55 | -------------------------------------------------------------------------------- /include/libpy/automodule.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "libpy/abi.h" 8 | #include "libpy/borrowed_ref.h" 9 | #include "libpy/detail/api.h" 10 | #include "libpy/detail/numpy.h" 11 | #include "libpy/detail/python.h" 12 | #include "libpy/owned_ref.h" 13 | 14 | #define _libpy_XSTR(s) #s 15 | #define _libpy_STR(s) _libpy_XSTR(s) 16 | 17 | #define _libpy_MODULE_PATH(parent, name) _libpy_STR(parent) "." _libpy_STR(name) 18 | 19 | #define _libpy_XCAT(a, b) a##b 20 | #define _libpy_CAT(a, b) _libpy_XCAT(a, b) 21 | 22 | #define _libpy_MODINIT_NAME(name) _libpy_CAT(PyInit_, name) 23 | #define _libpy_MODULE_CREATE(path) PyModule_Create(&_libpy_module) 24 | 25 | /** Define a Python module. 26 | 27 | For example, to create a module `my_package.submodule.my_module` with two functions 28 | `f` and `g` and one type `T`: 29 | 30 | \code 31 | LIBPY_AUTOMODULE(my_package.submodule, 32 | my_module, 33 | ({py::autofunction("f"), 34 | py::autofunction("g")})) 35 | (py::borrowed_ref<> m) { 36 | py::borrowed_ref t = py::autoclass("T").new_().type(); 37 | return PyObject_SetAttrString(m.get(), "T", static_cast(t)); 38 | } 39 | \endcode 40 | 41 | @param parent A symbol indicating the parent module. 42 | @param name The leaf name of the module. 43 | @param methods `({...})` list of objects representing the functions to add to the 44 | module. Note this list must be surrounded by parentheses. 45 | */ 46 | #define LIBPY_AUTOMODULE(parent, name, methods) \ 47 | bool _libpy_user_mod_init(py::borrowed_ref<>); \ 48 | PyMODINIT_FUNC _libpy_MODINIT_NAME(name)() LIBPY_EXPORT; \ 49 | PyMODINIT_FUNC _libpy_MODINIT_NAME(name)() { \ 50 | import_array(); \ 51 | if (py::abi::ensure_compatible_libpy_abi()) { \ 52 | return nullptr; \ 53 | } \ 54 | static std::vector ms methods; \ 55 | ms.emplace_back(PyMethodDef({nullptr})); \ 56 | static PyModuleDef _libpy_module{ \ 57 | PyModuleDef_HEAD_INIT, \ 58 | _libpy_MODULE_PATH(parent, name), \ 59 | nullptr, \ 60 | -1, \ 61 | ms.data(), \ 62 | }; \ 63 | py::owned_ref m(_libpy_MODULE_CREATE(_libpy_MODULE_PATH(parent, name))); \ 64 | if (!m) { \ 65 | return nullptr; \ 66 | } \ 67 | try { \ 68 | if (_libpy_user_mod_init(m)) { \ 69 | return nullptr; \ 70 | } \ 71 | } \ 72 | catch (const std::exception& e) { \ 73 | py::raise_from_cxx_exception(e); \ 74 | return nullptr; \ 75 | } \ 76 | catch (...) { \ 77 | if (!PyErr_Occurred()) { \ 78 | py::raise(PyExc_RuntimeError) << "an unknown C++ exception was raised"; \ 79 | return nullptr; \ 80 | } \ 81 | } \ 82 | return std::move(m).escape(); \ 83 | } \ 84 | bool _libpy_user_mod_init 85 | -------------------------------------------------------------------------------- /include/libpy/borrowed_ref.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "libpy/detail/python.h" 4 | 5 | namespace py { 6 | template 7 | class owned_ref; 8 | 9 | /** A type that explicitly indicates that a Python object is a borrowed 10 | reference. This is implicitly convertible from a regular `PyObject*` or a 11 | `py::owned_ref`. This type should be used to accept Python object parameters like: 12 | 13 | \code 14 | int f(py::borrowed_ref<> a, py::borrowed_ref<> b); 15 | \endcode 16 | 17 | This allows calling this function with a `py::owned_ref`, 18 | `PyObject*`, or a `py::borrowed_ref`. 19 | 20 | `py::borrowed_ref<>` should be used instead of `PyObject*` wherever possible to avoid 21 | ambiguity. 22 | 23 | @note A `borrowed_ref` may hold a value of `nullptr`. 24 | */ 25 | template 26 | class borrowed_ref { 27 | private: 28 | T* m_ref; 29 | 30 | public: 31 | /** The type of the underlying pointer. 32 | */ 33 | using element_type = T; 34 | 35 | /** Default construct a borrowed ref to a `nullptr`. 36 | */ 37 | constexpr borrowed_ref() : m_ref(nullptr) {} 38 | constexpr borrowed_ref(std::nullptr_t) : m_ref(nullptr) {} 39 | constexpr borrowed_ref(T* ref) : m_ref(ref) {} 40 | constexpr borrowed_ref(const py::owned_ref& ref) : m_ref(ref.get()) {} 41 | 42 | constexpr borrowed_ref(const borrowed_ref&) = default; 43 | constexpr borrowed_ref& operator=(const borrowed_ref& ob) = default; 44 | 45 | /** Get the underlying pointer. 46 | 47 | @return The pointer managed by this `borrowed_ref`. 48 | */ 49 | constexpr T* get() const { 50 | return m_ref; 51 | } 52 | 53 | explicit constexpr operator T*() const { 54 | return m_ref; 55 | } 56 | 57 | // use an enable_if to resolve the ambiguous dispatch when T is PyObject 58 | template::value>> 60 | explicit operator PyObject*() const { 61 | return reinterpret_cast(m_ref); 62 | } 63 | 64 | T& operator*() const { 65 | return *m_ref; 66 | } 67 | 68 | T* operator->() const { 69 | return m_ref; 70 | } 71 | 72 | explicit operator bool() const { 73 | return m_ref; 74 | } 75 | 76 | /** Object identity comparison. 77 | 78 | @return `get() == other.get()`. 79 | */ 80 | bool operator==(borrowed_ref other) const { 81 | return get() == other.get(); 82 | } 83 | 84 | /** Object identity comparison. 85 | 86 | @return `get() != other.get()`. 87 | */ 88 | bool operator!=(borrowed_ref other) const { 89 | return get() != other.get(); 90 | } 91 | }; 92 | } // namespace py 93 | -------------------------------------------------------------------------------- /include/libpy/buffer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "libpy/borrowed_ref.h" 8 | #include "libpy/demangle.h" 9 | #include "libpy/detail/api.h" 10 | #include "libpy/detail/python.h" 11 | #include "libpy/exception.h" 12 | 13 | namespace py { 14 | namespace detail { 15 | struct buffer_free { 16 | inline void operator()(Py_buffer* view) { 17 | if (view) { 18 | PyBuffer_Release(view); 19 | delete view; 20 | } 21 | } 22 | }; 23 | } // namespace detail 24 | 25 | /** A smart pointer adapter for `Py_buffer` that ensures the underlying buffer is 26 | released. 27 | */ 28 | using buffer = std::unique_ptr; 29 | 30 | enum buffer_format_code : char {}; 31 | 32 | /** The Python buffor format character for the given type. 33 | 34 | If there is no corresponding buffer format character, this template evaluates to 35 | `'\0'`. 36 | */ 37 | template 38 | inline constexpr buffer_format_code buffer_format{'\0'}; 39 | 40 | template<> 41 | inline constexpr buffer_format_code buffer_format{'c'}; 42 | 43 | template<> 44 | inline constexpr buffer_format_code buffer_format{'b'}; 45 | 46 | template<> 47 | inline constexpr buffer_format_code buffer_format{'B'}; 48 | 49 | template<> 50 | inline constexpr buffer_format_code buffer_format{'?'}; 51 | 52 | template<> 53 | inline constexpr buffer_format_code buffer_format{'h'}; 54 | 55 | template<> 56 | inline constexpr buffer_format_code buffer_format{'H'}; 57 | 58 | template<> 59 | inline constexpr buffer_format_code buffer_format{'i'}; 60 | 61 | template<> 62 | inline constexpr buffer_format_code buffer_format{'I'}; 63 | 64 | template<> 65 | inline constexpr buffer_format_code buffer_format{'l'}; 66 | 67 | template<> 68 | inline constexpr buffer_format_code buffer_format{'L'}; 69 | 70 | template<> 71 | inline constexpr buffer_format_code buffer_format{'q'}; 72 | 73 | template<> 74 | inline constexpr buffer_format_code buffer_format{'Q'}; 75 | 76 | template<> 77 | inline constexpr buffer_format_code buffer_format{'f'}; 78 | 79 | template<> 80 | inline constexpr buffer_format_code buffer_format{'d'}; 81 | 82 | namespace detail { 83 | // clang-format off 84 | template 85 | constexpr bool buffer_format_compatible = 86 | (std::is_integral_v == std::is_integral_v && 87 | std::is_signed_v == std::is_signed_v && 88 | sizeof(A) == sizeof(B)) || 89 | // special case for char and unsigned char; bytes objects report 90 | // that they are unsigned but we want to be able to read them into 91 | // std::string_view 92 | ((std::is_same_v && std::is_same_v) || 93 | (std::is_same_v && std::is_same_v)); 94 | // clang-format on 95 | 96 | using buffer_format_types = std::tuple; 110 | 111 | template 112 | struct buffer_compatible_format_chars; 113 | 114 | template 115 | struct buffer_compatible_format_chars> { 116 | using type = decltype(py::cs::cat( 117 | std::conditional_t, 118 | py::cs::char_sequence>, 119 | py::cs::char_sequence<>>{}, 120 | typename buffer_compatible_format_chars>::type{})); 121 | }; 122 | 123 | template 124 | struct buffer_compatible_format_chars> { 125 | using type = py::cs::char_sequence<>; 126 | }; 127 | } // namespace detail 128 | 129 | /** Get a Python `Py_Buffer` from the given object. 130 | 131 | @param ob The object to read the buffer from. 132 | @flags The Python buffer request flags. 133 | @return buf A smart-pointer adapted `Py_Buffer`. 134 | @throws An exception is thrown if `ob` doesn't expose the buffer interface. 135 | */ 136 | LIBPY_EXPORT py::buffer get_buffer(py::borrowed_ref<> ob, int flags); 137 | 138 | /** Check if a buffer format character is compatible with the given C++ type. 139 | 140 | @tparam T The type to check. 141 | @param fmt The Python buffer code. 142 | @return Is the buffer code compatible with `T`? 143 | */ 144 | template 145 | bool buffer_type_compatible(buffer_format_code fmt) { 146 | auto arr = py::cs::to_array( 147 | typename detail::buffer_compatible_format_chars:: 148 | type{}); 149 | for (char c : arr) { 150 | if (fmt == c) { 151 | return true; 152 | } 153 | } 154 | return false; 155 | } 156 | 157 | /** Check if a buffer's type is compatible with the given C++ type. 158 | 159 | @tparam T The type to check. 160 | @param buf The buffer to check. 161 | @return Is the buffer code compatible with `T`? 162 | */ 163 | template 164 | bool buffer_type_compatible(const py::buffer& buf) { 165 | if (!buf->format || std::strlen(buf->format) != 1) { 166 | return false; 167 | } 168 | return buffer_type_compatible(buffer_format_code{*buf->format}); 169 | } 170 | } // namespace py 171 | -------------------------------------------------------------------------------- /include/libpy/build_tuple.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "libpy/owned_ref.h" 4 | #include "libpy/to_object.h" 5 | 6 | namespace py { 7 | /** Build a Python tuple from a variadic amount of arguments. 8 | 9 | All parameters are adapted using `py::to_object`. 10 | 11 | @param args The arguments to adapt into Python objects and pack into a tuple. 12 | @return A new Python tuple or `nullptr` with a Python exception set. 13 | @see py::to_object 14 | */ 15 | template 16 | py::owned_ref<> build_tuple(const Args&... args) { 17 | py::owned_ref out(PyTuple_New(sizeof...(args))); 18 | if (!out) { 19 | return nullptr; 20 | } 21 | 22 | Py_ssize_t ix = 0; 23 | (PyTuple_SET_ITEM(out.get(), ix++, py::to_object(args).escape()), ...); 24 | for (ix = 0; ix < static_cast(sizeof...(args)); ++ix) { 25 | if (!PyTuple_GET_ITEM(out.get(), ix)) { 26 | return nullptr; 27 | } 28 | } 29 | 30 | return out; 31 | } 32 | } // namespace py 33 | -------------------------------------------------------------------------------- /include/libpy/call_function.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "libpy/borrowed_ref.h" 9 | #include "libpy/build_tuple.h" 10 | #include "libpy/detail/python.h" 11 | #include "libpy/exception.h" 12 | #include "libpy/numpy_utils.h" 13 | #include "libpy/owned_ref.h" 14 | #include "libpy/to_object.h" 15 | 16 | namespace py { 17 | /** Call a python function on C++ data. 18 | 19 | @param function The function to call 20 | @param args The arguments to call it with, these will be adapted to 21 | temporary python objects. 22 | @return The result of the function call or nullptr if an error occurred. 23 | */ 24 | template 25 | owned_ref<> call_function(py::borrowed_ref<> function, Args&&... args) { 26 | auto pyargs = py::build_tuple(std::forward(args)...); 27 | return owned_ref(PyObject_CallObject(function.get(), pyargs.get())); 28 | } 29 | 30 | /** Call a python function on C++ data. 31 | 32 | @param function The function to call 33 | @param args The arguments to call it with, these will be adapted to 34 | temporary python objects. 35 | @return The result of the function call. If the function throws a Python 36 | exception, a `py::exception` will be thrown. 37 | */ 38 | template 39 | owned_ref<> call_function_throws(py::borrowed_ref<> function, Args&&... args) { 40 | auto pyargs = py::build_tuple(std::forward(args)...); 41 | owned_ref res(PyObject_CallObject(function.get(), pyargs.get())); 42 | if (!res) { 43 | throw py::exception{}; 44 | } 45 | return res; 46 | } 47 | 48 | /** Call a python method on C++ data. 49 | 50 | @param ob The object to call the method on. 51 | @param method The method to call, this must be null-terminated. 52 | @param args The arguments to call it with, these will be adapted to 53 | temporary python objects. 54 | @return The result of the method call or nullptr if an error occurred. 55 | */ 56 | template 57 | owned_ref<> 58 | call_method(py::borrowed_ref<> ob, const std::string& method, Args&&... args) { 59 | owned_ref bound_method(PyObject_GetAttrString(ob.get(), method.data())); 60 | if (!bound_method) { 61 | return nullptr; 62 | } 63 | 64 | return call_function(bound_method.get(), std::forward(args)...); 65 | } 66 | 67 | /** Call a python method on C++ data. 68 | 69 | @param ob The object to call the method on. 70 | @param method The method to call, this must be null-terminated. 71 | @param args The arguments to call it with, these will be adapted to 72 | temporary python objects. 73 | @return The result of the method call or nullptr. If the method throws a 74 | Python exception, a `py::exception` will be thrown. 75 | */ 76 | template 77 | owned_ref<> 78 | call_method_throws(py::borrowed_ref<> ob, const std::string& method, Args&&... args) { 79 | owned_ref bound_method(PyObject_GetAttrString(ob.get(), method.data())); 80 | if (!bound_method) { 81 | throw py::exception{}; 82 | } 83 | 84 | return call_function_throws(bound_method.get(), std::forward(args)...); 85 | } 86 | } // namespace py 87 | -------------------------------------------------------------------------------- /include/libpy/char_sequence.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace py::cs { 8 | /** A compile time sequence of characters. 9 | */ 10 | template 11 | using char_sequence = std::integer_sequence; 12 | 13 | inline namespace literals { 14 | /** User defined literal for creating a `py::cs::char_sequence` value. 15 | 16 | \code 17 | using a = py::cs::char_sequence<'a', 'b', 'c', 'd'>; 18 | using b = decltype("abcd"_cs); 19 | 20 | static_assert(std::is_same_v); 21 | \endcode 22 | */ 23 | template 24 | constexpr char_sequence operator""_cs() { 25 | return {}; 26 | } 27 | 28 | /** User defined literal for creating a `std::array` of characters. 29 | 30 | \code 31 | constexpr std::array a = {'a', 'b', 'c', 'd'}; 32 | constexpr std::array b = "abcd"_arr; 33 | 34 | static_assert(a == b); 35 | \endcode 36 | 37 | @note This does not add a nul terminator to the array. 38 | */ 39 | template 40 | constexpr std::array operator""_arr() { 41 | return {cs...}; 42 | } 43 | }; // namespace literals 44 | 45 | namespace detail { 46 | template 47 | constexpr auto binary_cat(char_sequence, char_sequence) { 48 | return char_sequence{}; 49 | } 50 | } // namespace detail 51 | 52 | /** Concatenate character sequences. 53 | */ 54 | constexpr char_sequence<> cat() { 55 | return {}; 56 | } 57 | 58 | /** Concatenate character sequences. 59 | */ 60 | template 61 | constexpr auto cat(Cs cs) { 62 | return cs; 63 | } 64 | 65 | /** Concatenate character sequences. 66 | */ 67 | template 68 | constexpr auto cat(Cs cs, Ds ds) { 69 | return detail::binary_cat(cs, ds); 70 | } 71 | 72 | /** Concatenate character sequences. 73 | */ 74 | template 75 | constexpr auto cat(Cs cs, Ds ds, Ts... es) { 76 | return detail::binary_cat(detail::binary_cat(cs, ds), cat(es...)); 77 | } 78 | 79 | /** Convert a character sequence into a `std::array` with a trailing null byte. 80 | */ 81 | template 82 | constexpr auto to_array(char_sequence) { 83 | return std::array{cs..., '\0'}; 84 | } 85 | 86 | namespace detail { 87 | template 88 | struct intersperse; 89 | 90 | // recursive base case 91 | template 92 | struct intersperse> { 93 | constexpr static char_sequence<> value{}; 94 | }; 95 | 96 | template 97 | struct intersperse> { 98 | constexpr static auto value = cat(char_sequence{}, 99 | intersperse>::value); 100 | }; 101 | }; // namespace detail 102 | 103 | /** Intersperse a character between all the characters of a `char_sequence`. 104 | 105 | @tparam c The character to intersperse into the sequence 106 | @param cs The sequence to intersperse the character into. 107 | */ 108 | template 109 | constexpr auto intersperse(Cs) { 110 | return detail::intersperse::value; 111 | } 112 | 113 | namespace detail { 114 | template 115 | struct join; 116 | 117 | // recursive base case 118 | template 119 | struct join { 120 | constexpr static char_sequence<> value{}; 121 | }; 122 | 123 | template 124 | struct join, char_sequence, Tail...> { 125 | private: 126 | using joiner = char_sequence; 127 | 128 | public: 129 | constexpr static auto value = 130 | cs::cat(char_sequence{}, 131 | std::conditional_t>{}, 132 | join::value); 133 | }; 134 | } // namespace detail 135 | 136 | /** Join a sequence of compile-time strings together with another compile-time 137 | string. 138 | 139 | This is like `joiner.join(cs)` in Python. 140 | 141 | @param joiner The string to join with. 142 | @param cs... The strings to join together. 143 | */ 144 | template 145 | constexpr auto join(J, Cs...) { 146 | return detail::join::value; 147 | } 148 | } // namespace py::cs 149 | -------------------------------------------------------------------------------- /include/libpy/demangle.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "libpy/detail/api.h" 10 | 11 | namespace py::util { 12 | LIBPY_BEGIN_EXPORT 13 | /** Exception raised when an invalid `py::demangle_string` call is performed. 14 | */ 15 | class demangle_error : public std::exception { 16 | private: 17 | std::string m_msg; 18 | 19 | public: 20 | inline demangle_error(const std::string& msg) : m_msg(msg) {} 21 | 22 | inline const char* what() const noexcept override { 23 | return m_msg.data(); 24 | } 25 | }; 26 | 27 | class invalid_mangled_name : public demangle_error { 28 | public: 29 | inline invalid_mangled_name() : demangle_error("invalid mangled name") {} 30 | }; 31 | 32 | /** Demangle the given string. 33 | 34 | @param cs The mangled symbol or type name. 35 | @return The demangled string. 36 | */ 37 | std::string demangle_string(const char* cs); 38 | 39 | /** Demangle the given string. 40 | 41 | @param cs The mangled symbol or type name. 42 | @return The demangled string. 43 | */ 44 | std::string demangle_string(const std::string& cs); 45 | LIBPY_END_EXPORT 46 | 47 | /** Get the name for a given type. If the demangled name cannot be given, returns the 48 | mangled name. 49 | 50 | @tparam T The type to get the name of. 51 | @return The type's name. 52 | */ 53 | template 54 | std::string type_name() { 55 | const char* name = typeid(T).name(); 56 | std::string out; 57 | try { 58 | out = demangle_string(name); 59 | } 60 | catch (const invalid_mangled_name&) { 61 | out = name; 62 | } 63 | 64 | if (std::is_lvalue_reference_v) { 65 | out.push_back('&'); 66 | } 67 | else if (std::is_rvalue_reference_v) { 68 | out.insert(out.end(), 2, '&'); 69 | } 70 | 71 | return out; 72 | } 73 | } // namespace py::util 74 | -------------------------------------------------------------------------------- /include/libpy/detail/api.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /** Marker for a single function or type to indicate that it should be exported in the 4 | shared object. 5 | 6 | # Examples 7 | ``` 8 | LIBPY_EXPORT int public_function(float); 9 | 10 | struct LIBPY_EXPORT public_struct {}; 11 | ``` 12 | 13 | @note The visibility modifiers for types must appear between the initial keyword and 14 | the name of the type. Type visibility is applied only to "vague linkage entities" 15 | associated with the type. For example a vtable or typeinfo node. Public types do not 16 | automatically make all of their members public. 17 | */ 18 | #define LIBPY_EXPORT __attribute__((visibility("default"))) 19 | 20 | /** Marker for a single function or type to indicate that it should not be exported in the 21 | libpy shared object. The default visibility is *hidden*, so this can be used inside a 22 | `LIBPY_BEGIN_EXPORT/LIBPY_END_EXPORT` block to turn off only some types and functions. 23 | 24 | # Examples 25 | ``` 26 | LIBPY_NO_EXPORT int hidden_function(float); 27 | 28 | struct LIBPY_NO_EXPORT hidden_struct {}; 29 | ``` 30 | 31 | @note The visibility modifiers for types must appear between the initial keyword and 32 | the name of the type. Type visibility is applied only to "vague linkage entities" 33 | associated with the type. For example a vtable or typeinfo node. Public types do not 34 | automatically make all of their members public. 35 | */ 36 | #define LIBPY_NO_EXPORT __attribute__((visibility("hidden"))) 37 | 38 | /** Temporarily change the default visibility to public. 39 | 40 | # Examples 41 | ``` 42 | int hidden_function(float); 43 | 44 | LIBPY_BEGIN_EXPORT 45 | int public_function(float); 46 | LIBPY_END_EXPORT 47 | 48 | int another_hidden_function(float); 49 | ``` 50 | 51 | @see LIBPY_END_EXPORT 52 | */ 53 | #define LIBPY_BEGIN_EXPORT _Pragma("GCC visibility push(default)") 54 | 55 | /** Close the scope entered by `LIBPY_BEGIN_EXPORT`. 56 | 57 | @see LIBPY_BEGIN_EXPORT 58 | */ 59 | #define LIBPY_END_EXPORT _Pragma("GCC visibility pop") 60 | -------------------------------------------------------------------------------- /include/libpy/detail/autoclass_cache.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | /* This file is lifted out of `autoclass.h` because both `from_object.h` and 3 | `autoclass.h` need to read this cache, but `autoclass.h` depends on `from_object.h`. 4 | */ 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "libpy/borrowed_ref.h" 12 | #include "libpy/detail/api.h" 13 | #include "libpy/detail/no_destruct_wrapper.h" 14 | #include "libpy/detail/python.h" 15 | #include "libpy/owned_ref.h" 16 | 17 | namespace py::detail { 18 | using unbox_fn = void* (*)(py::borrowed_ref<>); 19 | struct autoclass_storage { 20 | // Pointer to the function which handles unboxing objects of this type. The resulting 21 | // pointer can be safely cast to the static type of the contained object. 22 | unbox_fn unbox; 23 | 24 | // Borrowed reference to the type that this struct contains storage for. 25 | py::borrowed_ref type; 26 | 27 | // The method storage for `type`. We may use a vector because this is just a 28 | // collection of pointers and ints. `PyMethodDef` objects may move around until 29 | // we call `PyType_FromSpec`, at that point pointer will be taken to these objects 30 | // and we cannot relocate them. 31 | std::vector methods; 32 | 33 | // The storage for `type.tp_name`, and the method `name` and `doc` fields. This uses a 34 | // forward list because the `PyMethodDef` objects and `PyTypeObject` point to strings 35 | // in this list. Because of small buffer optimization (SBO), `std::string` does not 36 | // have reference stability on it's contents across a move. `std::forward_list` gives 37 | // us the reference stability that we need. We don't use `std::list` because we want 38 | // to limit the overhead of this structure. 39 | std::forward_list strings; 40 | 41 | // Storage for the `PyMethodDef` of the callback owned by `cleanup_wr`. This is not 42 | // in the `methods` array, because that array is passed as the `Py_tp_methods` slot 43 | // and will become the methods of the type. This is a free function. 44 | PyMethodDef callback_method; 45 | 46 | // A Python weakref that will delete this object from `autoclass_type_cache` when the 47 | // type dies. 48 | py::owned_ref<> cleanup_wr; 49 | 50 | // The Python base class for this type. 51 | py::owned_ref m_pybase; 52 | 53 | autoclass_storage() = default; 54 | 55 | autoclass_storage(unbox_fn unbox, std::string&& name) 56 | : unbox(unbox), 57 | type(nullptr), 58 | strings({std::move(name)}), 59 | callback_method({nullptr, nullptr, 0, nullptr}) {} 60 | }; 61 | 62 | // A map from a C++ RTTI object to the Python class that was created to wrap it using 63 | // `py::autoclass`. 64 | LIBPY_EXPORT extern no_destruct_wrapper< 65 | std::unordered_map>> 66 | autoclass_type_cache; 67 | } // namespace py::detail 68 | -------------------------------------------------------------------------------- /include/libpy/detail/autoclass_object.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "libpy/borrowed_ref.h" 4 | #include "libpy/detail/python.h" 5 | 6 | namespace py::detail { 7 | template 8 | struct autoclass_object : public b { 9 | using base = b; 10 | 11 | T value; 12 | 13 | static T& unbox(py::borrowed_ref ob) { 14 | return static_cast(ob.get())->value; 15 | } 16 | 17 | template>> 18 | static T& unbox(py::borrowed_ref<> self) { 19 | return unbox(reinterpret_cast(self.get())); 20 | } 21 | }; 22 | 23 | template 24 | struct autoclass_interface_object : public PyObject { 25 | using base = PyObject; 26 | 27 | I* virt_storage_ptr; 28 | 29 | static I& unbox(py::borrowed_ref<> ob) { 30 | return *std::launder( 31 | static_cast(ob.get())->virt_storage_ptr); 32 | } 33 | }; 34 | 35 | template 36 | using autoclass_interface_instance_object = 37 | autoclass_object>; 38 | } // namespace py::detail 39 | -------------------------------------------------------------------------------- /include/libpy/detail/no_destruct_wrapper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace py::detail { 6 | /** A wrapper which prevents a destructor from being called. This is useful for 7 | static objects that hold `py::owned_ref` objects which may not be able 8 | to be cleaned up do to the interpreter state. 9 | */ 10 | template 11 | class no_destruct_wrapper { 12 | public: 13 | /** Forward all arguments to the underlying object. 14 | */ 15 | template 16 | no_destruct_wrapper(Args... args) { 17 | new (&m_storage) T(std::forward(args)...); 18 | } 19 | 20 | T& get() { 21 | return *std::launder(reinterpret_cast(&m_storage)); 22 | } 23 | 24 | const T& get() const { 25 | return *std::launder(reinterpret_cast(&m_storage)); 26 | } 27 | 28 | private: 29 | std::aligned_storage_t m_storage; 30 | }; 31 | } // namespace libpy::detail 32 | -------------------------------------------------------------------------------- /include/libpy/detail/numpy.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Numpy expects code is being used in a way that has a 1:1 correspondence from source 4 | // file to Python extension (as a shared object), for example: 5 | // 6 | // // my_extension.c 7 | // #include arrayobject.h 8 | // ... 9 | // PyMODINIT_FUNC PyInit_myextension() { 10 | // import_array(); 11 | // // Rest of module setup. 12 | // } 13 | // 14 | // Normally when writing a C++ library, header files provide declarations for functions 15 | // and types but not the definitions (except for templates and inline functions) and the 16 | // actual definition lives in one or more C++ files that get linked together into a single 17 | // shared object. Code that just wants to consume the library can include the files so 18 | // that the compiler knows the types and signatures provided by the library, but it 19 | // doesn't need to include the definitions in the consumer's compiled output. Instead, 20 | // when the object is loaded, it will also load the shared object and resolve all of the 21 | // symbols to the definitions in the shared object. This allows the implementation to 22 | // changed in backwards compatible ways and ensures that in a single process, all users of 23 | // the library have the same version. One downside is that the linker uses it's own path 24 | // resolution machinery to find the actual shared object file to use. Also, the resolution 25 | // happens when the shared object is loaded, no code can run before the symbols are 26 | // resolved. Numpy doesn't want users to need to deal with C/C++ linker stuff, and just 27 | // wants to be able to use the Python import system to find the numpy shared object(s). To 28 | // do this, numpy uses it's own linking system. The `numpy/arrayobject.h` will put a 29 | // `static void** PyArray_API = nullptr` name into *each* object that includes it. Many if 30 | // not all of the API functions in numpy are actually macros that resolve to something 31 | // like: `((PyArrayAPIObject*) PyArray_API)->function`. The `import_array()` macro will 32 | // import (through Python) the needed numpy extension modules to get the `PyArray_API` out 33 | // of a capsule-like object. `import_array()` is doing something very similar to what a 34 | // linker does, but without special compiler/linker assistance. 35 | // 36 | // This whole system works fine for when a single TU turns into a single object; however, 37 | // the test suite for libpy links all the test files together along with `main.cc` into a 38 | // single program. This has made it very hard to figure out when and how to initialize the 39 | // `PyArray_API` value. Instead, we now set a macro when compiling for the tests 40 | // (`LIBPY_COMPILING_FOR_TESTS`) which will control the `NO_IMPORT_ARRAY` flag. This flag 41 | // tells numpy to declare the `PyArray_API` flag as an `extern "C" void** PyArray_API`, 42 | // meaning we expect to have this symbol defined by another object we are to be linked 43 | // with. In `main.cc` we also set `LIBPY_TEST_MAIN` to disable `NO_IMPORT_ARRAY` which 44 | // causes changes the declaration of `PyArray_API` to change to: `#define PyArray_API 45 | // PY_ARRAY_UNIQUE_SYMBOL` and then `void** PyArray_API`. Importantly, this removes the 46 | // `static` and `extern` causing the symbol to have external linkage. Then, because the 47 | // tests are declaring the same symbol as extern, they will all resolve to the same 48 | // `PyArray_API` instance and we only need to call `import_array` once in `main.cc`. 49 | #if LIBPY_COMPILING_FOR_TESTS 50 | #define PY_ARRAY_UNIQUE_SYMBOL PyArray_API_libpy 51 | #ifndef LIBPY_TEST_MAIN 52 | #define NO_IMPORT_ARRAY 53 | #endif 54 | #endif 55 | #define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION 56 | #include "libpy/detail/python.h" 57 | #include 58 | -------------------------------------------------------------------------------- /include/libpy/detail/python.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // NOTE: This is a relic of supporting both py2 and py3, but this provides 4 | // A standard place to find this header and make python version dependent 5 | // modifications if necessary. 6 | #include 7 | -------------------------------------------------------------------------------- /include/libpy/dict_range.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "libpy/borrowed_ref.h" 6 | #include "libpy/detail/api.h" 7 | #include "libpy/detail/python.h" 8 | #include "libpy/exception.h" 9 | 10 | namespace py { 11 | LIBPY_BEGIN_EXPORT 12 | /** A range which iterates over the (key, value) pairs of a Python dictionary. 13 | */ 14 | class dict_range { 15 | private: 16 | py::owned_ref<> m_map; 17 | 18 | class iterator { 19 | public: 20 | using value_type = std::pair, py::borrowed_ref<>>; 21 | using reference = value_type&; 22 | 23 | private: 24 | py::borrowed_ref<> m_map; 25 | Py_ssize_t m_pos; 26 | value_type m_item; 27 | 28 | public: 29 | inline iterator() : m_map(nullptr), m_pos(-1), m_item(nullptr, nullptr) {} 30 | 31 | explicit iterator(py::borrowed_ref<> map); 32 | 33 | iterator(const iterator&) = default; 34 | iterator& operator=(const iterator&) = default; 35 | 36 | reference operator*(); 37 | value_type* operator->(); 38 | 39 | iterator& operator++(); 40 | iterator operator++(int); 41 | 42 | bool operator!=(const iterator& other) const; 43 | bool operator==(const iterator& other) const; 44 | }; 45 | 46 | public: 47 | /** Create an object which iterates a Python dictionary as key, value pairs. 48 | 49 | @note This does not do a type check, `map` must be a Python dictionary. 50 | @param map The map to create a range over. 51 | */ 52 | explicit dict_range(py::borrowed_ref<> map); 53 | 54 | /** Assert that `map` is a Python dictionary and then construct a 55 | `dict_range`. 56 | 57 | @param map The object to check and then make a view over. 58 | @return A new dict range. 59 | */ 60 | static dict_range checked(py::borrowed_ref<> map); 61 | 62 | iterator begin() const; 63 | iterator end() const; 64 | }; 65 | LIBPY_END_EXPORT 66 | } // namespace py 67 | -------------------------------------------------------------------------------- /include/libpy/getattr.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "libpy/borrowed_ref.h" 6 | #include "libpy/exception.h" 7 | #include "libpy/owned_ref.h" 8 | 9 | namespace py { 10 | /** Look up an attribute on a Python object and return a new owning reference. 11 | 12 | @param ob The object to look up the attribute on. 13 | @param attr The name of the attribute to lookup. 14 | @return A new reference to `gettattr(ob, attr)` or `nullptr` with a Python 15 | exception set on failure. 16 | */ 17 | inline py::owned_ref<> getattr(py::borrowed_ref<> ob, const std::string& attr) { 18 | return py::owned_ref{PyObject_GetAttrString(ob.get(), attr.data())}; 19 | } 20 | 21 | /** Look up an attribute on a Python object and return a new owning reference. 22 | 23 | @param ob The object to look up the attribute on. 24 | @param attr The name of the attribute to lookup. 25 | @return A new reference to `gettattr(ob, attr)`. If the attribute doesn't 26 | exist, a `py::exception` will be thrown. 27 | */ 28 | inline py::owned_ref<> getattr_throws(py::borrowed_ref<> ob, const std::string& attr) { 29 | PyObject* res = PyObject_GetAttrString(ob.get(), attr.data()); 30 | if (!res) { 31 | throw py::exception{}; 32 | } 33 | return py::owned_ref{res}; 34 | } 35 | 36 | inline py::owned_ref<> nested_getattr(py::borrowed_ref<> ob) { 37 | return py::owned_ref<>::new_reference(ob); 38 | } 39 | 40 | /** Perform nested getattr calls with intermediate error checking. 41 | 42 | @param ob The root object to look up the attribute on. 43 | @param attrs The name of the attributes to lookup. 44 | @return A new reference to `getattr(gettattr(ob, attrs[0]), attrs[1]), ...` 45 | or `nullptr` with a Python exception set on failure. 46 | */ 47 | template 48 | py::owned_ref<> nested_getattr(py::borrowed_ref<> ob, const T& head, const Ts&... tail) { 49 | py::owned_ref<> result = getattr(ob, head); 50 | if (!result) { 51 | return nullptr; 52 | } 53 | return nested_getattr(result, tail...); 54 | } 55 | 56 | /** Perform nested getattr calls with intermediate error checking. 57 | 58 | @param ob The root object to look up the attribute on. 59 | @param attrs The name of the attributes to lookup. 60 | @return A new reference to `getattr(gettattr(ob, attrs[0]), attrs[1]), ...`. 61 | If an attribute in the chain doesn't exist, a `py::exception` will 62 | be thrown. 63 | */ 64 | template 65 | py::owned_ref<> nested_getattr_throws(const Ts&... args) { 66 | auto res = nested_getattr(args...); 67 | if (!res) { 68 | throw py::exception{}; 69 | } 70 | return res; 71 | } 72 | } // namespace py 73 | -------------------------------------------------------------------------------- /include/libpy/gil.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "libpy/detail/api.h" 4 | #include "libpy/detail/python.h" 5 | 6 | namespace py { 7 | LIBPY_BEGIN_EXPORT 8 | /** A wrapper around the threadstate. 9 | */ 10 | struct gil final { 11 | private: 12 | LIBPY_EXPORT static thread_local PyThreadState* m_save; 13 | 14 | public: 15 | gil() = delete; 16 | 17 | /** Release the GIL. The GIL must be held. 18 | 19 | @note `release` is a low-level utility. Please see `release_block` for 20 | a safer alternative. 21 | */ 22 | static inline void release() { 23 | m_save = PyEval_SaveThread(); 24 | } 25 | 26 | /** Acquire the GIL. The GIL must not be held. 27 | 28 | @note `acquire` is a low-level utility. Please see `hold_block` for 29 | a safer alternative. 30 | */ 31 | static inline void acquire() { 32 | PyEval_RestoreThread(m_save); 33 | m_save = nullptr; 34 | } 35 | 36 | /** Release the GIL if it is not already released. 37 | 38 | @note `ensure_released` is a low-level utility. Please see 39 | `release_block` for a safer alternative. 40 | */ 41 | static inline void ensure_released() { 42 | if (held()) { 43 | release(); 44 | } 45 | } 46 | 47 | /** Acquire the GIL if we do not already hold it. 48 | 49 | @note `ensure_acquired` is a low-level utility. Please see `hold_block` 50 | for a safer alternative. 51 | */ 52 | static inline void ensure_acquired() { 53 | if (!held()) { 54 | acquire(); 55 | } 56 | } 57 | 58 | /** Check if the gil is currently held. 59 | */ 60 | static inline bool held() { 61 | return PyGILState_Check(); 62 | } 63 | 64 | /** RAII resource for ensuring that the gil is released in a given block. 65 | 66 | For example: 67 | 68 | \code 69 | // the gil may or may not be released here 70 | { 71 | py::gil::release_block released; 72 | // the gil is now definitely released 73 | } 74 | // the gil may or may not be released here 75 | \endcode 76 | */ 77 | struct release_block final { 78 | private: 79 | bool m_acquire; 80 | 81 | public: 82 | inline release_block() : m_acquire(gil::held()) { 83 | gil::ensure_released(); 84 | } 85 | 86 | /** Reset this gil back to the state it was in when this object was created. 87 | */ 88 | inline void dismiss() { 89 | if (m_acquire) { 90 | gil::acquire(); 91 | m_acquire = false; 92 | } 93 | } 94 | 95 | inline ~release_block() { 96 | dismiss(); 97 | } 98 | }; 99 | 100 | /** RAII resource for ensuring that the gil is held in a given block. 101 | 102 | For example: 103 | 104 | \code 105 | // the gil may or may not be held here 106 | { 107 | py::gil::hold_block held; 108 | // the gil is now definitely held 109 | } 110 | // the gil may or may not be held here 111 | \endcode 112 | */ 113 | struct hold_block final { 114 | private: 115 | bool m_release; 116 | 117 | public: 118 | inline hold_block() : m_release(!gil::held()) { 119 | gil::ensure_acquired(); 120 | } 121 | 122 | /** Reset this gil back to the state it was in when this object was created. 123 | */ 124 | inline void dismiss() { 125 | if (m_release) { 126 | gil::release(); 127 | m_release = false; 128 | } 129 | } 130 | 131 | inline ~hold_block() { 132 | dismiss(); 133 | } 134 | }; 135 | }; 136 | LIBPY_END_EXPORT 137 | } // namespace py 138 | -------------------------------------------------------------------------------- /include/libpy/hash.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace py { 8 | namespace detail { 9 | /** Combine two hashes into one. This algorithm is taken from `boost`. 10 | */ 11 | template 12 | T bin_hash_combine(T hash_1, T hash_2) { 13 | return hash_1 ^ (hash_2 + 0x9e3779b9 + (hash_1 << 6) + (hash_1 >> 2)); 14 | } 15 | } // namespace detail 16 | 17 | /** Combine two or more hashes into one. 18 | */ 19 | template 20 | auto hash_combine(T head, Ts... tail) { 21 | ((head = detail::bin_hash_combine(head, tail)), ...); 22 | return head; 23 | } 24 | 25 | /** Hash multiple values by `hash_combine`ing them together. 26 | */ 27 | template 28 | auto hash_many(const Ts&... vs) { 29 | return hash_combine(std::hash{}(vs)...); 30 | } 31 | 32 | /** Hash a tuple by `hash_many`ing all the values together. 33 | */ 34 | template 35 | auto hash_tuple(const std::tuple& t) { 36 | return std::apply)>(hash_many, t); 37 | } 38 | 39 | /** Hash a buffer of characters using the same algorithm as 40 | `std::hash` 41 | 42 | @param buf The buffer to hash. 43 | @param len The length of the buffer. 44 | @return The hash of the string. 45 | */ 46 | inline std::size_t hash_buffer(const char* buf, std::size_t len) { 47 | return std::hash{}(std::string_view{buf, len}); 48 | } 49 | } // namespace py 50 | -------------------------------------------------------------------------------- /include/libpy/library_wrappers/sparsehash.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "libpy/to_object.h" 11 | 12 | namespace py { 13 | /** A wrapper around `google::dense_hash_map` which uses `std::hash` instead of 14 | `tr1::hash` and requires an empty key at construction time. 15 | 16 | @tparam Key The key type. 17 | @tparam T The value type. 18 | @tparam HashFcn The key hash functor type. 19 | @tparam Alloc The allocator object to use. In general, don't change this. 20 | */ 21 | template, // change the default to std::hash 24 | typename EqualKey = std::equal_to, 25 | typename Alloc = google::libc_allocator_with_realloc>> 26 | struct dense_hash_map : public google::dense_hash_map { 27 | private: 28 | using base = google::dense_hash_map; 29 | 30 | public: 31 | dense_hash_map() = delete; // User must give a missing value. 32 | 33 | /** 34 | @param empty_key An element of type `Key` which denotes an empty slot. 35 | This value can not itself be used as a valid key. 36 | @param expected_size A size hint for the map. 37 | */ 38 | dense_hash_map(const Key& empty_key, std::size_t expected_size = 0) 39 | : base(expected_size) { 40 | if (empty_key != empty_key) { 41 | // the first insert will hang forever if `empty_key != empty_key` 42 | throw std::invalid_argument{"dense_hash_map: empty_key != empty_key"}; 43 | } 44 | this->set_empty_key(empty_key); 45 | } 46 | 47 | dense_hash_map(const dense_hash_map& cpfrom) : base(cpfrom) {} 48 | 49 | dense_hash_map(dense_hash_map&& mvfrom) noexcept { 50 | this->swap(mvfrom); 51 | } 52 | 53 | dense_hash_map& operator=(const dense_hash_map& cpfrom) { 54 | base::operator=(cpfrom); 55 | return *this; 56 | } 57 | 58 | dense_hash_map& operator=(dense_hash_map&& mvfrom) noexcept { 59 | this->swap(mvfrom); 60 | return *this; 61 | } 62 | }; 63 | 64 | /** A wrapper around `google::sparse_hash_map` which uses `std::hash` instead of 65 | `tr1::hash` and requires an empty key at construction time. 66 | 67 | @tparam Key The key type. 68 | @tparam T The value type. 69 | @tparam HashFcn The key hash functor type. 70 | @tparam Alloc The allocator object to use. In general, don't change this. 71 | */ 72 | template, // change the default to std::hash 75 | typename EqualKey = std::equal_to, 76 | typename Alloc = google::libc_allocator_with_realloc>> 77 | struct sparse_hash_map 78 | : public google::sparse_hash_map { 79 | private: 80 | using base = google::sparse_hash_map; 81 | 82 | public: 83 | using base::sparse_hash_map; 84 | 85 | sparse_hash_map(const sparse_hash_map& cpfrom) : base(cpfrom) {} 86 | 87 | sparse_hash_map(sparse_hash_map&& mvfrom) noexcept { 88 | this->swap(mvfrom); 89 | } 90 | 91 | sparse_hash_map& operator=(const sparse_hash_map& cpfrom) { 92 | base::operator=(cpfrom); 93 | return *this; 94 | } 95 | 96 | sparse_hash_map& operator=(sparse_hash_map&& mvfrom) noexcept { 97 | this->swap(mvfrom); 98 | return *this; 99 | } 100 | }; 101 | 102 | template, 104 | typename EqualKey = std::equal_to, 105 | typename Alloc = google::libc_allocator_with_realloc> 106 | struct dense_hash_set : public google::dense_hash_set { 107 | private: 108 | using base = google::dense_hash_set; 109 | 110 | public: 111 | dense_hash_set() = delete; // User must give a missing value. 112 | 113 | /** 114 | @param empty_key An element of type `Key` which denotes an empty slot. 115 | This value can not itself be used as a valid key. 116 | @param expected_size A size hint for the set. 117 | */ 118 | dense_hash_set(const Key& empty_key, std::size_t expected_size = 0) 119 | : base(expected_size) { 120 | if (empty_key != empty_key) { 121 | // the first insert will hang forever if `empty_key != empty_key` 122 | throw std::invalid_argument{"dense_hash_set: empty_key != empty_key"}; 123 | } 124 | this->set_empty_key(empty_key); 125 | } 126 | 127 | dense_hash_set(const dense_hash_set& cpfrom) : base(cpfrom) {} 128 | 129 | dense_hash_set(dense_hash_set&& mvfrom) noexcept { 130 | this->swap(mvfrom); 131 | } 132 | 133 | dense_hash_set& operator=(const dense_hash_set& cpfrom) { 134 | base::operator=(cpfrom); 135 | return *this; 136 | } 137 | 138 | dense_hash_set& operator=(dense_hash_set&& mvfrom) noexcept { 139 | this->swap(mvfrom); 140 | return *this; 141 | } 142 | }; 143 | 144 | namespace dispatch { 145 | template 146 | struct to_object> 147 | : public map_to_object> {}; 148 | 149 | template 150 | struct to_object> 151 | : public map_to_object> {}; 152 | 153 | template 154 | struct to_object> 155 | : public set_to_object> {}; 156 | 157 | template 158 | struct to_object> 159 | : public map_to_object> {}; 160 | 161 | template 162 | struct to_object> 163 | : public map_to_object> {}; 164 | 165 | template 166 | struct to_object> 167 | : public set_to_object> {}; 168 | 169 | template 170 | struct to_object> 171 | : public set_to_object> {}; 172 | 173 | } // namespace dispatch 174 | } // namespace py 175 | -------------------------------------------------------------------------------- /include/libpy/object_map_key.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "libpy/borrowed_ref.h" 4 | #include "libpy/detail/python.h" 5 | #include "libpy/exception.h" 6 | #include "libpy/from_object.h" 7 | #include "libpy/owned_ref.h" 8 | #include "libpy/to_object.h" 9 | 10 | namespace py { 11 | 12 | LIBPY_BEGIN_EXPORT 13 | /** A wrapper that allows a `py::owned_ref` to be used as a key in a mapping structure. 14 | 15 | `object_map_key` overloads `operator==` to dispatch to the underlying Python object's 16 | `__eq__`. 17 | `object_map_key` is specialized for `std::hash` to dispatch to the underlying Python 18 | object's `__hash__`. 19 | 20 | If either operation would throw a Python exception, a C++ `py::exception` is raised. 21 | */ 22 | class object_map_key { 23 | private: 24 | py::owned_ref<> m_ob; 25 | 26 | public: 27 | inline object_map_key(std::nullptr_t) : m_ob(nullptr) {} 28 | inline object_map_key(py::borrowed_ref<> ob) 29 | : m_ob(py::owned_ref<>::xnew_reference(ob)) {} 30 | inline object_map_key(py::owned_ref<> ob) : m_ob(std::move(ob)) {} 31 | 32 | object_map_key() = default; 33 | object_map_key(const object_map_key&) = default; 34 | object_map_key(object_map_key&&) = default; 35 | 36 | object_map_key& operator=(const object_map_key&) = default; 37 | object_map_key& operator=(object_map_key&&) = default; 38 | 39 | inline PyObject* get() const { 40 | return m_ob.get(); 41 | } 42 | 43 | inline explicit operator bool() const noexcept { 44 | return static_cast(m_ob); 45 | } 46 | 47 | inline operator const py::owned_ref<>&() const noexcept { 48 | return m_ob; 49 | } 50 | 51 | inline operator py::borrowed_ref<>() const { 52 | return m_ob; 53 | } 54 | 55 | bool operator==(py::borrowed_ref<> other) const; 56 | bool operator!=(py::borrowed_ref<> other) const; 57 | bool operator<(py::borrowed_ref<> other) const; 58 | bool operator<=(py::borrowed_ref<> other) const; 59 | bool operator>=(py::borrowed_ref<> other) const; 60 | bool operator>(py::borrowed_ref<> other) const; 61 | }; 62 | LIBPY_END_EXPORT 63 | 64 | namespace dispatch { 65 | template<> 66 | struct from_object { 67 | static object_map_key f(py::borrowed_ref<> ob) { 68 | return object_map_key{ob}; 69 | } 70 | }; 71 | 72 | template<> 73 | struct to_object { 74 | static py::owned_ref<> f(const object_map_key& ob) { 75 | return py::owned_ref<>::new_reference(ob.get()); 76 | } 77 | }; 78 | } // namespace dispatch 79 | } // namespace py 80 | 81 | namespace std { 82 | template<> 83 | struct hash { 84 | auto operator()(const py::object_map_key& ob) const { 85 | using out_type = decltype(PyObject_Hash(ob.get())); 86 | 87 | if (!ob.get()) { 88 | return out_type{0}; 89 | } 90 | 91 | out_type r = PyObject_Hash(ob.get()); 92 | if (r == -1) { 93 | throw py::exception{}; 94 | } 95 | 96 | return r; 97 | } 98 | }; 99 | } // namespace std 100 | -------------------------------------------------------------------------------- /include/libpy/owned_ref.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "libpy/borrowed_ref.h" 6 | #include "libpy/detail/python.h" 7 | 8 | namespace py { 9 | /** A type that explicitly indicates that a Python object is an owned 10 | reference. This type should be used to hold Python objects in containers or local 11 | variables. 12 | 13 | `py::borrowed_ref<>` should be used instead of `PyObject*` wherever possible to avoid 14 | reference counting issues. 15 | 16 | @note An `owned_ref` may hold a value of `nullptr`. 17 | */ 18 | 19 | template 20 | class owned_ref final { 21 | private: 22 | T* m_ref; 23 | 24 | public: 25 | /** The type of the underlying pointer. 26 | */ 27 | using element_type = T; 28 | 29 | /** Default construct an owned ref to a `nullptr`. 30 | */ 31 | constexpr owned_ref() : m_ref(nullptr) {} 32 | 33 | constexpr owned_ref(std::nullptr_t) : m_ref(nullptr) {} 34 | 35 | /** Claim ownership of a new reference. `ref` should not be used outside of the 36 | `owned_ref`. This constructor should be used when calling CPython C API functions 37 | which return new references. For example: 38 | 39 | \code 40 | // PyDict_New() returns a new owned reference or nullptr on failure. 41 | py::scoped_ref ob{PyDict_New()}; 42 | if (!ob) { 43 | throw py::exception{}; 44 | } 45 | \endcode 46 | 47 | @param ref The reference to manage 48 | */ 49 | constexpr explicit owned_ref(T* ref) : m_ref(ref) {} 50 | 51 | constexpr owned_ref(const owned_ref& cpfrom) : m_ref(cpfrom.m_ref) { 52 | Py_XINCREF(m_ref); 53 | } 54 | 55 | constexpr owned_ref(owned_ref&& mvfrom) noexcept : m_ref(mvfrom.m_ref) { 56 | mvfrom.m_ref = nullptr; 57 | } 58 | 59 | constexpr owned_ref& operator=(const owned_ref& cpfrom) { 60 | // we need to incref before we decref to support self assignment 61 | Py_XINCREF(cpfrom.m_ref); 62 | Py_XDECREF(m_ref); 63 | m_ref = cpfrom.m_ref; 64 | return *this; 65 | } 66 | 67 | constexpr owned_ref& operator=(owned_ref&& mvfrom) noexcept { 68 | std::swap(m_ref, mvfrom.m_ref); 69 | return *this; 70 | } 71 | 72 | /** Create a scoped ref that is a new reference to `ref`. 73 | 74 | @param ref The Python object to create a new managed reference to. 75 | */ 76 | constexpr static owned_ref new_reference(py::borrowed_ref ref) { 77 | Py_INCREF(ref.get()); 78 | return owned_ref{ref.get()}; 79 | } 80 | 81 | /** Create a scoped ref that is a new reference to `ref` if `ref` is non-null. 82 | 83 | @param ref The Python object to create a new managed reference to. If `ref` 84 | is `nullptr`, then the resulting object just holds `nullptr` also. 85 | */ 86 | constexpr static owned_ref xnew_reference(py::borrowed_ref ref) { 87 | Py_XINCREF(ref.get()); 88 | return owned_ref{ref.get()}; 89 | } 90 | 91 | /** Decref the managed pointer if it is not `nullptr`. 92 | */ 93 | ~owned_ref() { 94 | Py_XDECREF(m_ref); 95 | } 96 | 97 | /** Return the underlying pointer and invalidate the `owned_ref`. 98 | 99 | This allows the reference to "escape" the current scope. 100 | 101 | @return The underlying pointer. 102 | @see get 103 | */ 104 | T* escape() && { 105 | T* ret = m_ref; 106 | m_ref = nullptr; 107 | return ret; 108 | } 109 | 110 | /** Get the underlying managed pointer. 111 | 112 | @return The pointer managed by this `owned_ref`. 113 | @see escape 114 | */ 115 | constexpr T* get() const { 116 | return m_ref; 117 | } 118 | 119 | explicit operator T*() const { 120 | return m_ref; 121 | } 122 | 123 | // use an enable_if to resolve the ambiguous dispatch when T is PyObject 124 | template::value>> 126 | explicit operator PyObject*() const { 127 | return reinterpret_cast(m_ref); 128 | } 129 | 130 | T& operator*() const { 131 | return *m_ref; 132 | } 133 | 134 | T* operator->() const { 135 | return m_ref; 136 | } 137 | 138 | /** Returns True if the underlying pointer is non-null. 139 | */ 140 | explicit operator bool() const { 141 | return m_ref; 142 | } 143 | 144 | /** Object identity comparison. 145 | 146 | @return `get() == other.get()`. 147 | */ 148 | bool operator==(py::borrowed_ref other) const { 149 | return get() == other.get(); 150 | } 151 | 152 | /** Object identity comparison. 153 | 154 | @return `get() != other.get()`. 155 | */ 156 | bool operator!=(py::borrowed_ref other) const { 157 | return get() != other.get(); 158 | } 159 | }; 160 | static_assert(std::is_standard_layout>::value, 161 | "owned_ref<> should be standard layout"); 162 | static_assert(sizeof(owned_ref<>) == sizeof(PyObject*), 163 | "alias type should be the same size as aliased type"); 164 | } // namespace py 165 | -------------------------------------------------------------------------------- /include/libpy/range.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "libpy/borrowed_ref.h" 6 | #include "libpy/detail/api.h" 7 | #include "libpy/detail/python.h" 8 | #include "libpy/exception.h" 9 | #include "libpy/owned_ref.h" 10 | 11 | namespace py { 12 | LIBPY_BEGIN_EXPORT 13 | /** A range which lazily iterates over the elements of a Python iterable. 14 | */ 15 | class range { 16 | private: 17 | py::owned_ref<> m_iterator; 18 | 19 | class iterator { 20 | public: 21 | using value_type = py::owned_ref<>; 22 | using reference = value_type&; 23 | 24 | private: 25 | py::borrowed_ref<> m_iterator; 26 | value_type m_value; 27 | 28 | public: 29 | inline iterator() : m_iterator(nullptr), m_value(nullptr) {} 30 | 31 | explicit iterator(py::borrowed_ref<> it); 32 | 33 | iterator(const iterator&) = default; 34 | iterator& operator=(const iterator&) = default; 35 | 36 | reference operator*(); 37 | value_type* operator->(); 38 | 39 | iterator& operator++(); 40 | iterator operator++(int); 41 | 42 | bool operator!=(const iterator& other) const; 43 | bool operator==(const iterator& other) const; 44 | }; 45 | 46 | public: 47 | explicit range(py::borrowed_ref<> iterable); 48 | 49 | iterator begin() const; 50 | iterator end() const; 51 | }; 52 | LIBPY_END_EXPORT 53 | } // namespace py 54 | -------------------------------------------------------------------------------- /include/libpy/scope_guard.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | namespace py::util { 6 | /** Assign an arbitrary callback to run when the scope closes, either through an exception 7 | or return. This callback may be dismissed later. This is useful for implementing 8 | transactional behavior, where all operations either succeed or fail together. 9 | 10 | ### Example 11 | 12 | \code 13 | // add objects to all vectors, if an exception is thrown, no objects will be added 14 | // to any vectors. 15 | void add_objects(std::vector& as, 16 | const A& a, 17 | std::vector& bs, 18 | const B& b, 19 | std::vector& cs, 20 | const C& c) { 21 | as.push_back(a); 22 | py::util::scope_guard a_guard([&] { as.pop_back(); }); 23 | 24 | bs.push_back(b); 25 | py::util::scope_guard b_guard([&] { bs.pop_back(); }); 26 | 27 | cs.push_back(c); 28 | 29 | // everything that could fail has already run, if we make it here we succeeded so 30 | // we can dismiss the guards 31 | a_guard.dismiss(); 32 | b_guard.dismiss(); 33 | } 34 | \endcode 35 | */ 36 | template 37 | struct scope_guard { 38 | private: 39 | std::optional m_callback; 40 | 41 | public: 42 | scope_guard(F&& callback) : m_callback(std::move(callback)) {} 43 | 44 | /** Dismiss the scope guard causing the registered callback to not be called. 45 | */ 46 | void dismiss() { 47 | m_callback = std::nullopt; 48 | } 49 | 50 | ~scope_guard() { 51 | if (m_callback) { 52 | (*m_callback)(); 53 | } 54 | } 55 | }; 56 | } // namespace py::util 57 | -------------------------------------------------------------------------------- /include/libpy/singletons.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "libpy/owned_ref.h" 4 | 5 | namespace py { 6 | inline py::owned_ref<> none{py::owned_ref<>::new_reference(Py_None)}; 7 | inline py::owned_ref<> ellipsis{py::owned_ref<>::new_reference(Py_Ellipsis)}; 8 | inline py::owned_ref<> not_implemented{py::owned_ref<>::new_reference(Py_NotImplemented)}; 9 | } // namespace py 10 | -------------------------------------------------------------------------------- /include/libpy/str_convert.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "libpy/char_sequence.h" 4 | #include "libpy/owned_ref.h" 5 | 6 | namespace py { 7 | 8 | enum class str_type { 9 | bytes, 10 | str, 11 | }; 12 | 13 | /** Convert a compile-time string into a Python string-like value. 14 | 15 | @param s Char sequence whose type encodes a compile-time string. 16 | @param type Enum representing the type into which to convert `s`. 17 | 18 | If the requested output py::str_type::str the input string must 19 | be valid utf-8. 20 | */ 21 | template 22 | owned_ref<> to_stringlike(py::cs::char_sequence s, py::str_type type) { 23 | const auto as_null_terminated_array = py::cs::to_array(s); 24 | const char* data = as_null_terminated_array.data(); 25 | Py_ssize_t size = sizeof...(cs); 26 | 27 | switch (type) { 28 | case py::str_type::bytes: { 29 | return owned_ref<>{PyBytes_FromStringAndSize(data, size)}; 30 | } 31 | case py::str_type::str: { 32 | return owned_ref<>{PyUnicode_FromStringAndSize(data, size)}; 33 | } 34 | } 35 | __builtin_unreachable(); 36 | } 37 | 38 | } // namespace py 39 | -------------------------------------------------------------------------------- /include/libpy/stream.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "libpy/call_function.h" 9 | #include "libpy/detail/python.h" 10 | #include "libpy/owned_ref.h" 11 | 12 | namespace py { 13 | /** A stream buffer that writes to a Python file-like object. 14 | */ 15 | template> 16 | class basic_streambuf : public std::basic_streambuf { 17 | private: 18 | using base = std::basic_streambuf; 19 | using int_type = typename base::int_type; 20 | 21 | /** The reference to the file-like object. 22 | */ 23 | py::owned_ref<> m_file_ob; 24 | 25 | std::array m_write_buffer; 26 | std::size_t m_write_ix = 0; 27 | 28 | protected: 29 | /** Write a character to the file-like object. 30 | */ 31 | virtual int_type overflow(int_type ch) override { 32 | if (ch == Traits::eof()) { 33 | return ch; 34 | } 35 | 36 | m_write_buffer[m_write_ix++] = ch; 37 | if (m_write_ix == m_write_buffer.size() && sync()) { 38 | throw exception(); 39 | } 40 | 41 | return ch; 42 | } 43 | 44 | /** Flush the data to the Python file. 45 | 46 | Note: This does not actually flush the underlying Python file-like object, it just 47 | commits the C++ buffered writes to the Python object. 48 | */ 49 | virtual int sync() override { 50 | if (m_write_ix) { 51 | auto view = std::string_view(m_write_buffer.data(), m_write_ix); 52 | auto result = call_method(m_file_ob.get(), "write", view); 53 | if (!result) { 54 | return -1; 55 | } 56 | } 57 | 58 | m_write_ix = 0; 59 | return 0; 60 | } 61 | 62 | public: 63 | explicit basic_streambuf(const py::borrowed_ref<>& file) 64 | : m_file_ob(py::owned_ref<>::new_reference(file)) {} 65 | }; 66 | 67 | /** A C++ output stream which writes to a Python file-like object. 68 | */ 69 | template> 70 | class basic_ostream : public std::basic_ostream { 71 | private: 72 | using ostream = std::basic_ostream; 73 | 74 | basic_streambuf m_buf; 75 | 76 | public: 77 | basic_ostream(py::borrowed_ref<> file) : std::ios(0), m_buf(file) { 78 | this->rdbuf(&m_buf); 79 | } 80 | 81 | virtual ~basic_ostream() { 82 | // behave like a file and flush ourselves on destruction 83 | m_buf.pubsync(); 84 | } 85 | }; 86 | 87 | /** A C++ output stream which writes to a Python file-like object. 88 | */ 89 | using ostream = basic_ostream; 90 | } // namespace py 91 | -------------------------------------------------------------------------------- /include/libpy/table_details.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "libpy/meta.h" 6 | 7 | namespace py::detail { 8 | /** Sentinel struct used to store a name and a type for py::table and related types. 9 | 10 | The classes defined in table.h (table, table_view, row, and row_view) are all 11 | templated on a variable number of "column singletons", which are pointers to 12 | specializations of ``column``. 13 | 14 | We use pointers because we want to be able to template tables on "columns", 15 | which carry two pieces of information: 16 | 17 | 1. A name (e.g. "asof_date"). 18 | 2. A type (e.g. "datetime64"). 19 | 20 | Templating on types is straightforward, but templating on names is hard to make 21 | ergonomic. We can use py::char_sequence literals ("foo"_cs) to define values that 22 | encode compile-time strings, but we can't make them template parameters directly 23 | without requiring heavy use of ``decltype`` for clients of ``table``, because 24 | char_sequence values that can't be used as template parameters. 25 | 26 | What we can do, however, is make ``py::C`` be a function that returns a **pointer** to 27 | an instance of the type we want to encode, and to template ``table`` and friends on 28 | those pointers. Inside the table and row types, we can use `py::unwrap_column` on the 29 | value to get the column type that it is a pointer to. 30 | 31 | All of this enables the following, relatively pleasant syntax for consumers: 32 | 33 | \code 34 | using my_table = py::table("some_name"_cs), 35 | py::C("some_other_name"_cs)>; 36 | \endcode 37 | */ 38 | template 39 | struct column { 40 | using key = Key; 41 | using value = Value; 42 | 43 | using const_column = column>; 44 | using remove_const_column = column>; 45 | }; 46 | 47 | /** Variable template for sentinel instances of ``column``. 48 | 49 | We use addresses of these values as template parameters for ``table`` and its 50 | associated types. 51 | */ 52 | template 53 | T column_singleton; 54 | 55 | /** Helper for unwrapping a column singleton to get the underlying column type. 56 | */ 57 | template 58 | using unwrap_column = typename std::remove_pointer_t; 59 | 60 | template 61 | struct relabeled_column_name_impl { 62 | using type = C; 63 | }; 64 | 65 | template 66 | struct relabeled_column_name_impl< 67 | C, 68 | std::tuple...>, 69 | std::enable_if_t>>> { 70 | 71 | using type = std::tuple_element_t>, 72 | std::tuple>; 73 | }; 74 | 75 | /** Given a column name, and a set of relabel mappings to apply, get the new column 76 | name. This is used to help implement `relabel()` on `row_view` and `table_view`. 77 | 78 | @tparam C The name of the column to lookup. 79 | @tparam Mappings A `std::tuple` of `std::pair`s mapping old column names to new column 80 | names. If `C` is the value of `first_type` on any of the pairs, the result 81 | will be that same pair's `second_type`. Otherwise, `C` will be returned 82 | unchanged. 83 | */ 84 | template 85 | using relabeled_column_name = typename relabeled_column_name_impl::type; 86 | } // namespace py::detail 87 | -------------------------------------------------------------------------------- /include/libpy/util.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "libpy/borrowed_ref.h" 9 | #include "libpy/detail/python.h" 10 | #include "libpy/owned_ref.h" 11 | 12 | /** Miscellaneous utilities. 13 | */ 14 | namespace py::util { 15 | /** Format a string by building up an intermediate `std::stringstream`. 16 | 17 | @param msg The components of the message. 18 | @return A new string which formatted all parts of msg with 19 | `operator(std::ostream&, decltype(msg))` 20 | */ 21 | template 22 | std::string format_string(Ts&&... msg) { 23 | std::stringstream s; 24 | (s << ... << msg); 25 | return s.str(); 26 | } 27 | 28 | /** Create an exception object by forwarding the result of `msg` to 29 | `py::util::format_string`. 30 | 31 | @tparam Exc The type of the exception to raise. 32 | @param msg The components of the message. 33 | @return A new exception object. 34 | */ 35 | template 36 | Exc formatted_error(Args&&... msg) { 37 | return Exc(format_string(std::forward(msg)...)); 38 | } 39 | 40 | /** Check if all parameters are equal. 41 | */ 42 | template 43 | constexpr bool all_equal(T&& head, Ts&&... tail) { 44 | return (... && (head == tail)); 45 | } 46 | 47 | constexpr inline bool all_equal() { 48 | return true; 49 | } 50 | 51 | /** Extract a C-style string from a `str` object. 52 | 53 | The result will be a view into a cached utf-8 representation of `ob`. 54 | 55 | The lifetime of the returned value is the same as the lifetime of `ob`. 56 | */ 57 | inline const char* pystring_to_cstring(py::borrowed_ref<> ob) { 58 | return PyUnicode_AsUTF8(ob.get()); 59 | } 60 | 61 | /** Get a view over the contents of a `str`. 62 | 63 | The view will be over a cached utf-8 representation of `ob`. 64 | 65 | The lifetime of the returned value is the same as the lifetime of `ob`. 66 | */ 67 | inline std::string_view pystring_to_string_view(py::borrowed_ref<> ob) { 68 | Py_ssize_t size; 69 | const char* cs; 70 | 71 | cs = PyUnicode_AsUTF8AndSize(ob.get(), &size); 72 | if (!cs) { 73 | throw formatted_error( 74 | "failed to get string and size from object of type: ", 75 | Py_TYPE(ob.get())->tp_name); 76 | } 77 | return {cs, static_cast(size)}; 78 | } 79 | 80 | /* Taken from google benchmark, this is useful for debugging. 81 | 82 | The DoNotOptimize(...) function can be used to prevent a value or 83 | expression from being optimized away by the compiler. This function is 84 | intended to add little to no overhead. 85 | See: https://youtu.be/nXaxk27zwlk?t=2441 86 | */ 87 | template 88 | inline __attribute__((always_inline)) void do_not_optimize(const T& value) { 89 | asm volatile("" : : "r,m"(value) : "memory"); 90 | } 91 | 92 | template 93 | inline __attribute__((always_inline)) void do_not_optimize(T& value) { 94 | #if defined(__clang__) 95 | asm volatile("" : "+r,m"(value) : : "memory"); 96 | #else 97 | asm volatile("" : "+m,r"(value) : : "memory"); 98 | #endif 99 | } 100 | 101 | /** Find lower bound index for needle within contianer. 102 | */ 103 | template 104 | std::int64_t searchsorted_l(const C& container, const T& needle) { 105 | auto begin = container.begin(); 106 | return std::lower_bound(begin, container.end(), needle) - begin; 107 | } 108 | 109 | /** Find upper bound index for needle within container. 110 | */ 111 | template 112 | std::int64_t searchsorted_r(const C& container, const T& needle) { 113 | auto begin = container.begin(); 114 | return std::upper_bound(begin, container.end(), needle) - begin; 115 | } 116 | 117 | /** Call `f` with value, start and stop (exclusive) indices for each contiguous region in 118 | `it` of equal value. 119 | */ 120 | template 121 | void apply_to_groups(I begin, I end, F&& f) { 122 | if (begin == end) { 123 | return; 124 | } 125 | 126 | std::size_t start_ix = 0; 127 | auto previous = *begin; 128 | ++begin; 129 | 130 | std::size_t ix = 1; 131 | for (; begin != end; ++begin, ++ix) { 132 | auto value = *begin; 133 | if (value == previous) { 134 | continue; 135 | } 136 | 137 | f(previous, start_ix, ix); 138 | start_ix = ix; 139 | previous = value; 140 | } 141 | 142 | f(previous, start_ix, ix); 143 | } 144 | 145 | template 146 | void apply_to_groups(R&& range, F&& f) { 147 | apply_to_groups(range.begin(), range.end(), f); 148 | } 149 | } // namespace py::util 150 | -------------------------------------------------------------------------------- /libpy/__init__.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import os 3 | 4 | 5 | _so = ctypes.CDLL( 6 | os.path.join(os.path.dirname(__file__), 'libpy.so'), 7 | ctypes.RTLD_GLOBAL, 8 | ) 9 | 10 | 11 | class VersionInfo(ctypes.Structure): 12 | _fields_ = [ 13 | ('major', ctypes.c_int), 14 | ('minor', ctypes.c_int), 15 | ('micro', ctypes.c_int), 16 | ] 17 | 18 | def __repr__(self): 19 | return ( 20 | '{type_name}(major={0.major},' 21 | ' minor={0.minor}, patch={0.micro})' 22 | ).format(self, type_name=type(self).__name__) 23 | 24 | def __str__(self): 25 | return '{0.major}.{0.minor}.{0.micro}'.format(self) 26 | 27 | 28 | version_info = VersionInfo.in_dll(_so, 'libpy_abi_version') 29 | __version__ = str(version_info) 30 | -------------------------------------------------------------------------------- /libpy/_build-and-run: -------------------------------------------------------------------------------- 1 | ../etc/build-and-run -------------------------------------------------------------------------------- /libpy/_detect-compiler.cc: -------------------------------------------------------------------------------- 1 | ../etc/detect-compiler.cc -------------------------------------------------------------------------------- /libpy/build.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | import subprocess 4 | import sys 5 | import warnings 6 | 7 | import setuptools 8 | import numpy as np 9 | 10 | import libpy 11 | 12 | 13 | def detect_compiler(): 14 | p = subprocess.Popen( 15 | [ 16 | os.path.join(os.path.dirname(__file__), '_build-and-run'), 17 | os.path.join(os.path.dirname(__file__), '_detect-compiler.cc'), 18 | ], 19 | stdin=subprocess.PIPE, 20 | stdout=subprocess.PIPE, 21 | ) 22 | stdout, _ = p.communicate() 23 | if stdout == b'GCC\n': 24 | return 'GCC' 25 | elif stdout == b'CLANG\n': 26 | return 'CLANG' 27 | 28 | warnings.warn( 29 | 'Could not detect which compiler is being used, assuming gcc.', 30 | ) 31 | return 'GCC' 32 | 33 | 34 | def get_include(): 35 | """Get the path to the libpy headers relative to the installed package. 36 | 37 | Returns 38 | ------- 39 | include_path : str 40 | The path to the libpy include directory. 41 | """ 42 | return os.path.join(os.path.dirname(__file__), 'include') 43 | 44 | 45 | class LibpyExtension(setuptools.Extension, object): 46 | """A :class:`setuptools.Extension` replacement for libpy based extensions. 47 | 48 | Parameters 49 | ---------- 50 | *args 51 | All positional arguments forwarded to :class:`setuptools.Extension`. 52 | language_std : str, optional 53 | The language standard to use. Defaults to ``c++17``. 54 | optlevel : int, optional 55 | The optimization level to forward to the C++ compiler. 56 | Defaults to 0. 57 | debug_symbols : bool, optional 58 | Should debug symbols be generated? 59 | Defaults to True. 60 | use_libpy_suggested_warnings : bool, optional 61 | Should libpy add it's default set of warnings to the compiler flags. 62 | This set is picked to aid code clarity and attempt to common mistakes. 63 | Defaults to True. 64 | werror : bool, optional 65 | Treat warnings as errors. The libpy developers believe that most 66 | compiler warnings indicate serious problems and should fail the build. 67 | Defaults to True. 68 | max_errors : int or None, optional 69 | Limit the number of error messages that are shown. 70 | Defaults to None, showing all error messages. 71 | ubsan : bool, optional 72 | Compile with ubsan? Implies optlevel=0. 73 | use_autoclass_unsafe_api : bool, optional 74 | Whether to allow the usage of the unsafe libpy api in autoclass 75 | **kwargs 76 | All other keyword arguments forwarded to :class:`setuptools.Extension`. 77 | 78 | Notes 79 | ----- 80 | This class sets the `language` field to `c++` because libpy only works with 81 | C++. 82 | 83 | This class also passes `-std=gnu++17` which is the minimum language 84 | standard required by libpy. 85 | 86 | Any compiler flags added by libpy will appear *before* 87 | `extra_compile_args`. This gives the user the ability to override any of 88 | libpy's options. 89 | """ 90 | _compiler = detect_compiler() 91 | 92 | _recommended_warnings = [ 93 | '-Werror', 94 | '-Wall', 95 | '-Wextra', 96 | '-Wno-register', 97 | '-Wno-missing-field-initializers', 98 | '-Wsign-compare', 99 | '-Wparentheses', 100 | ] 101 | if _compiler == 'GCC': 102 | _recommended_warnings.extend([ 103 | '-Wsuggest-override', 104 | '-Wno-maybe-uninitialized', 105 | '-Waggressive-loop-optimizations', 106 | ]) 107 | elif _compiler == 'CLANG': 108 | _recommended_warnings.extend([ 109 | '-Wno-gnu-string-literal-operator-template', 110 | '-Wno-missing-braces', 111 | '-Wno-self-assign-overloaded', 112 | ]) 113 | else: 114 | raise AssertionError('unknown compiler: %s' % _compiler) 115 | 116 | _base_flags = [ 117 | '-pipe', 118 | '-fvisibility-inlines-hidden', 119 | '-DPY_MAJOR_VERSION=%d' % sys.version_info.major, 120 | '-DPY_MINOR_VERSION=%d' % sys.version_info.minor, 121 | '-DLIBPY_MAJOR_VERSION=%d' % libpy.version_info.major, 122 | '-DLIBPY_MINOR_VERSION=%d' % libpy.version_info.minor, 123 | '-DLIBPY_MICRO_VERSION=%d' % libpy.version_info.micro, 124 | ] 125 | 126 | def __init__(self, *args, **kwargs): 127 | kwargs['language'] = 'c++' 128 | 129 | std = kwargs.pop('language_std', 'c++17') 130 | 131 | libpy_extra_compile_args = self._base_flags.copy() 132 | libpy_extra_compile_args.append('-std=%s' % std) 133 | 134 | libpy_extra_link_args = [] 135 | 136 | optlevel = kwargs.pop('optlevel', 0) 137 | ubsan = kwargs.pop('ubsan', False) 138 | if ubsan: 139 | optlevel = 0 140 | libpy_extra_compile_args.append('-fsanitize=undefined') 141 | libpy_extra_link_args.append('-lubsan') 142 | 143 | libpy_extra_compile_args.append('-O%d' % optlevel) 144 | if kwargs.pop('debug_symbols', True): 145 | libpy_extra_compile_args.append('-g') 146 | 147 | if kwargs.pop('use_libpy_suggested_warnings', True): 148 | libpy_extra_compile_args.extend(self._recommended_warnings) 149 | 150 | if kwargs.pop('werror', True): 151 | libpy_extra_compile_args.append('-Werror') 152 | 153 | max_errors = kwargs.pop('max_errors', None) 154 | if max_errors is not None: 155 | if self._compiler == 'GCC': 156 | libpy_extra_compile_args.append( 157 | '-fmax-errors=%d' % max_errors, 158 | ) 159 | elif self._compiler == 'CLANG': 160 | libpy_extra_compile_args.append( 161 | '-ferror-limit=%d' % max_errors, 162 | ) 163 | else: 164 | raise AssertionError('unknown compiler: %s' % self._compiler) 165 | 166 | if kwargs.pop('use_autoclass_unsafe_api', False): 167 | libpy_extra_compile_args.append('-DLIBPY_AUTOCLASS_UNSAFE_API') 168 | 169 | kwargs['extra_compile_args'] = ( 170 | libpy_extra_compile_args + 171 | kwargs.get('extra_compile_args', []) 172 | ) 173 | kwargs['extra_link_args'] = ( 174 | libpy_extra_link_args + 175 | kwargs.get('extra_link_args', []) 176 | ) 177 | depends = kwargs.setdefault('depends', []).copy() 178 | depends.extend(glob.glob(get_include() + '/**/*.h', recursive=True)) 179 | depends.extend(glob.glob(np.get_include() + '/**/*.h', recursive=True)) 180 | 181 | include_dirs = kwargs.setdefault('include_dirs', []) 182 | include_dirs.append(get_include()) 183 | include_dirs.append(np.get_include()) 184 | 185 | super(LibpyExtension, self).__init__(*args, **kwargs) 186 | -------------------------------------------------------------------------------- /libpy/include: -------------------------------------------------------------------------------- 1 | ../include/ -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import ast 2 | from distutils.command.build_py import build_py as _build_py 3 | import os 4 | import pathlib 5 | import shutil 6 | import stat 7 | 8 | from setuptools import setup 9 | 10 | 11 | class BuildFailed(Exception): 12 | pass 13 | 14 | 15 | # Setting ``LIBPY_DONT_BUILD`` to a truthy value will disable building the 16 | # libpy extension, but allow the setup.py to run so that the Python support 17 | # code may be installed. This exists to allow libpy to be used with alternative 18 | # which may produce the libpy shared object in an alternative way. This flag 19 | # prevents the setup.py from attempting to rebuild the shared object which may 20 | # clobber or duplicate work done by the larger build system. This is an 21 | # advanced feature and shouldn't be used without care as it may produce invalid 22 | # installs of ``libpy``. 23 | dont_build = ast.literal_eval(os.environ.get('LIBPY_DONT_BUILD', '0')) 24 | 25 | 26 | class build_py(_build_py): 27 | def run(self): 28 | if self.dry_run or dont_build: 29 | return super().run() 30 | 31 | super().run() 32 | path = os.path.dirname(os.path.abspath(__file__)) 33 | command = 'make -C "%s" libpy/libpy.so' % path 34 | out = os.system(command) 35 | if out: 36 | raise BuildFailed( 37 | "Command {!r} failed with code {}".format(command, out) 38 | ) 39 | 40 | shutil.copyfile( 41 | 'libpy/libpy.so', 42 | os.path.join(self.build_lib, 'libpy', 'libpy.so'), 43 | ) 44 | 45 | p = pathlib.Path(self.build_lib) / 'libpy/_build-and-run' 46 | p.chmod(p.stat().st_mode | stat.S_IEXEC) 47 | 48 | 49 | setup( 50 | name='libpy', 51 | description='Utilities for writing C++ extension modules.', 52 | long_description=open('README.rst').read(), 53 | url='https://github.com/quantopian/libpy', 54 | version=open('version').read().strip(), 55 | author='Quantopian Inc.', 56 | author_email='opensource@quantopian.com', 57 | packages=['libpy'], 58 | license='Apache 2.0', 59 | classifiers=[ 60 | 'Development Status :: 4 - Beta', 61 | 'License :: OSI Approved :: Apache Software License', 62 | 'Natural Language :: English', 63 | 'Topic :: Software Development', 64 | 'Programming Language :: Python', 65 | 'Programming Language :: Python :: 3.5', 66 | 'Programming Language :: Python :: 3.6', 67 | 'Programming Language :: Python :: 3.7', 68 | 'Programming Language :: Python :: 3.8', 69 | 'Programming Language :: Python :: Implementation :: CPython', 70 | 'Programming Language :: C++', 71 | 'Operating System :: POSIX', 72 | 'Intended Audience :: Developers', 73 | ], 74 | # we need the headers to be available to the C compiler as regular files; 75 | # we cannot be imported from a ziparchive. 76 | zip_safe=False, 77 | install_requires=['numpy'], 78 | cmdclass={'build_py': build_py}, 79 | package_data={ 80 | 'libpy': [ 81 | 'include/libpy/*.h', 82 | 'include/libpy/detail/*.h', 83 | '_build-and-run', 84 | '_detect-compiler.cc', 85 | ], 86 | }, 87 | ) 88 | -------------------------------------------------------------------------------- /src/abi.cc: -------------------------------------------------------------------------------- 1 | #include "libpy/abi.h" 2 | 3 | namespace py::abi { 4 | abi_version libpy_abi_version = detail::header_libpy_abi_version; 5 | 6 | std::ostream& operator<<(std::ostream& stream, abi_version v) { 7 | return stream << v.major << '.' << v.minor << '.' << v.patch; 8 | } 9 | } // namespace py::abi 10 | -------------------------------------------------------------------------------- /src/autoclass.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "libpy/detail/python.h" 7 | #include 8 | #include 9 | 10 | #include "libpy/detail/autoclass_cache.h" 11 | 12 | namespace py::detail { 13 | no_destruct_wrapper< 14 | std::unordered_map>> 15 | autoclass_type_cache{}; 16 | } // namespace py::detail 17 | -------------------------------------------------------------------------------- /src/buffer.cc: -------------------------------------------------------------------------------- 1 | #include "libpy/buffer.h" 2 | 3 | namespace py { 4 | py::buffer get_buffer(py::borrowed_ref<> ob, int flags) { 5 | py::buffer buf(new Py_buffer); 6 | if (PyObject_GetBuffer(ob.get(), buf.get(), flags)) { 7 | throw py::exception{}; 8 | } 9 | 10 | return buf; 11 | } 12 | } // namespace py 13 | -------------------------------------------------------------------------------- /src/demangle.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "libpy/demangle.h" 4 | 5 | namespace py::util { 6 | std::string demangle_string(const char* cs) { 7 | int status; 8 | char* demangled = ::abi::__cxa_demangle(cs, nullptr, nullptr, &status); 9 | 10 | switch (status) { 11 | case 0: { 12 | std::string out(demangled); 13 | std::free(demangled); 14 | return out; 15 | } 16 | case -1: 17 | throw demangle_error("memory error"); 18 | case -2: 19 | throw invalid_mangled_name(); 20 | case -3: 21 | throw demangle_error("invalid argument to cxa_demangle"); 22 | default: 23 | throw demangle_error("unknown failure"); 24 | } 25 | } 26 | 27 | std::string demangle_string(const std::string& cs) { 28 | return demangle_string(cs.data()); 29 | } 30 | } // namespace py::util 31 | -------------------------------------------------------------------------------- /src/dict_range.cc: -------------------------------------------------------------------------------- 1 | #include "libpy/dict_range.h" 2 | 3 | namespace py { 4 | dict_range::dict_range(py::borrowed_ref<> map) 5 | : m_map(py::owned_ref<>::new_reference(map)) {} 6 | 7 | dict_range::iterator::iterator(py::borrowed_ref<> map) : m_map(map), m_pos(0) { 8 | ++(*this); 9 | } 10 | 11 | dict_range::iterator::reference dict_range::iterator::operator*() { 12 | return m_item; 13 | } 14 | 15 | dict_range::iterator::value_type* dict_range::iterator::operator->() { 16 | return &m_item; 17 | } 18 | 19 | dict_range::iterator& dict_range::iterator::operator++() { 20 | PyObject* k; 21 | PyObject* v; 22 | if (!PyDict_Next(m_map.get(), &m_pos, &k, &v)) { 23 | m_map = nullptr; 24 | m_pos = -1; 25 | m_item.first = nullptr; 26 | m_item.second = nullptr; 27 | } 28 | else { 29 | m_item.first = k; 30 | m_item.second = v; 31 | } 32 | return *this; 33 | } 34 | 35 | dict_range::iterator dict_range::iterator::operator++(int) { 36 | dict_range::iterator out = *this; 37 | return ++out; 38 | } 39 | 40 | bool dict_range::iterator::operator!=(const iterator& other) const { 41 | return m_pos != other.m_pos; 42 | } 43 | 44 | bool dict_range::iterator::operator==(const iterator& other) const { 45 | return m_pos == other.m_pos; 46 | } 47 | 48 | dict_range dict_range::checked(py::borrowed_ref<> map) { 49 | if (!PyDict_Check(map)) { 50 | throw py::exception(PyExc_TypeError, 51 | "argument to py::dict_range::checked isn't a dict, got: ", 52 | Py_TYPE(map)->tp_name); 53 | } 54 | return dict_range(map); 55 | } 56 | 57 | dict_range::iterator dict_range::begin() const { 58 | return dict_range::iterator{m_map}; 59 | } 60 | 61 | dict_range::iterator dict_range::end() const { 62 | return dict_range::iterator{}; 63 | } 64 | } // namespace py 65 | -------------------------------------------------------------------------------- /src/exception.cc: -------------------------------------------------------------------------------- 1 | #include "libpy/exception.h" 2 | 3 | namespace py { 4 | namespace { 5 | void deep_what_recursive_helper(std::string& out, const std::exception& e, int level) { 6 | if (level) { 7 | out.push_back('\n'); 8 | } 9 | out.insert(out.end(), level * 2, ' '); 10 | const char* what = e.what(); 11 | if (*what) { 12 | if (level) { 13 | out += "raised from: "; 14 | } 15 | out += what; 16 | } 17 | else { 18 | out += "raised from exception with empty what()"; 19 | } 20 | try { 21 | std::rethrow_if_nested(e); 22 | } 23 | catch (const std::exception& next) { 24 | deep_what_recursive_helper(out, next, level + 1); 25 | } 26 | } 27 | 28 | std::string deep_what(const std::exception& e) { 29 | std::string out; 30 | deep_what_recursive_helper(out, e, 0); 31 | return out; 32 | } 33 | } // namespace 34 | 35 | std::nullptr_t raise_from_cxx_exception(const std::exception& e) { 36 | if (!PyErr_Occurred()) { 37 | py::raise(PyExc_RuntimeError) << "a C++ exception was raised: " << deep_what(e); 38 | return nullptr; 39 | } 40 | if (dynamic_cast(&e)) { 41 | // this already raised an exception with the message we want to show to 42 | // Python 43 | return nullptr; 44 | } 45 | PyObject* type; 46 | PyObject* value; 47 | PyObject* tb; 48 | PyErr_Fetch(&type, &value, &tb); 49 | PyErr_NormalizeException(&type, &value, &tb); 50 | Py_XDECREF(tb); 51 | const char* what = e.what(); 52 | if (!what[0]) { 53 | raise(type) << value << "; raised from C++ exception"; 54 | } 55 | else { 56 | raise(type) << value << "; raised from C++ exception: " << deep_what(e); 57 | } 58 | Py_DECREF(type); 59 | Py_DECREF(value); 60 | return nullptr; 61 | } 62 | 63 | std::string exception::msg_from_current_pyexc() { 64 | PyObject* type; 65 | PyObject* value; 66 | PyObject* tb; 67 | PyErr_Fetch(&type, &value, &tb); 68 | PyErr_NormalizeException(&type, &value, &tb); 69 | 70 | py::owned_ref as_str(PyObject_Str(value)); 71 | std::string out; 72 | if (!as_str) { 73 | out = ""; 74 | } 75 | else { 76 | out = reinterpret_cast(type)->tp_name; 77 | out += ": "; 78 | out += py::util::pystring_to_string_view(as_str); 79 | } 80 | PyErr_Restore(type, value, tb); 81 | return out; 82 | } 83 | } // namespace py 84 | -------------------------------------------------------------------------------- /src/gil.cc: -------------------------------------------------------------------------------- 1 | #include "libpy/gil.h" 2 | 3 | namespace py { 4 | thread_local PyThreadState* gil::m_save; 5 | } // namespace py 6 | -------------------------------------------------------------------------------- /src/object_map_key.cc: -------------------------------------------------------------------------------- 1 | #include "libpy/object_map_key.h" 2 | 3 | namespace py { 4 | bool object_map_key::operator==(py::borrowed_ref<> other) const { 5 | if (!m_ob) { 6 | return !static_cast(other); 7 | } 8 | if (!other) { 9 | return false; 10 | } 11 | 12 | int r = PyObject_RichCompareBool(m_ob.get(), other.get(), Py_EQ); 13 | if (r < 0) { 14 | throw py::exception{}; 15 | } 16 | 17 | return r; 18 | } 19 | 20 | bool object_map_key::operator!=(py::borrowed_ref<> other) const { 21 | if (!m_ob) { 22 | return static_cast(other); 23 | } 24 | if (!other) { 25 | return true; 26 | } 27 | 28 | int r = PyObject_RichCompareBool(m_ob.get(), other.get(), Py_NE); 29 | if (r < 0) { 30 | throw py::exception{}; 31 | } 32 | 33 | return r; 34 | } 35 | 36 | bool object_map_key::operator<(py::borrowed_ref<> other) const { 37 | if (!m_ob) { 38 | return false; 39 | } 40 | if (!other) { 41 | return true; 42 | } 43 | 44 | int r = PyObject_RichCompareBool(m_ob.get(), other.get(), Py_LT); 45 | if (r < 0) { 46 | throw py::exception{}; 47 | } 48 | 49 | return r; 50 | } 51 | 52 | bool object_map_key::operator<=(py::borrowed_ref<> other) const { 53 | if (!m_ob) { 54 | return !static_cast(other); 55 | } 56 | if (!other) { 57 | return true; 58 | } 59 | 60 | int r = PyObject_RichCompareBool(m_ob.get(), other.get(), Py_LE); 61 | if (r < 0) { 62 | throw py::exception{}; 63 | } 64 | 65 | return r; 66 | } 67 | 68 | bool object_map_key::operator>(py::borrowed_ref<> other) const { 69 | if (!m_ob) { 70 | return static_cast(other); 71 | } 72 | if (!other) { 73 | return false; 74 | } 75 | 76 | int r = PyObject_RichCompareBool(m_ob.get(), other.get(), Py_GT); 77 | if (r < 0) { 78 | throw py::exception{}; 79 | } 80 | 81 | return r; 82 | } 83 | 84 | bool object_map_key::operator>=(py::borrowed_ref<> other) const { 85 | if (!m_ob) { 86 | return true; 87 | } 88 | if (!other) { 89 | return false; 90 | } 91 | 92 | int r = PyObject_RichCompareBool(m_ob.get(), other.get(), Py_GE); 93 | if (r < 0) { 94 | throw py::exception{}; 95 | } 96 | 97 | return r; 98 | } 99 | 100 | } // namespace py 101 | -------------------------------------------------------------------------------- /src/range.cc: -------------------------------------------------------------------------------- 1 | #include "libpy/range.h" 2 | 3 | namespace py { 4 | range::iterator::iterator(py::borrowed_ref<> it) : m_iterator(it), m_value(nullptr) { 5 | ++(*this); 6 | } 7 | 8 | range::iterator::reference range::iterator::operator*() { 9 | return m_value; 10 | } 11 | 12 | range::iterator::value_type* range::iterator::operator->() { 13 | return &m_value; 14 | } 15 | 16 | range::iterator& range::iterator::operator++() { 17 | m_value = py::owned_ref(PyIter_Next(m_iterator.get())); 18 | if (!m_value) { 19 | if (PyErr_Occurred()) { 20 | throw py::exception{}; 21 | } 22 | m_iterator = nullptr; 23 | } 24 | return *this; 25 | } 26 | 27 | range::iterator range::iterator::operator++(int) { 28 | range::iterator out = *this; 29 | return ++out; 30 | } 31 | 32 | bool range::iterator::operator!=(const iterator& other) const { 33 | return !(*this == other); 34 | } 35 | 36 | bool range::iterator::operator==(const iterator& other) const { 37 | return m_iterator == other.m_iterator && m_value.get() == other.m_value.get(); 38 | } 39 | 40 | range::range(py::borrowed_ref<> iterable) : m_iterator(PyObject_GetIter(iterable.get())) { 41 | if (!m_iterator) { 42 | throw py::exception{}; 43 | } 44 | } 45 | 46 | range::iterator range::begin() const { 47 | return range::iterator{m_iterator.get()}; 48 | } 49 | 50 | range::iterator range::end() const { 51 | return range::iterator{}; 52 | } 53 | } // namespace py 54 | -------------------------------------------------------------------------------- /testleaks.supp: -------------------------------------------------------------------------------- 1 | leak:libpython 2 | leak:_ctypes 3 | leak:_struct 4 | leak:_ctypes.cpython 5 | leak:_ctypes.x86_64-linux-gnu.so 6 | leak:numpy/core/src/multiarray/arraytypes.c.src 7 | leak:numpy/core/src/multiarray/alloc.c 8 | leak:numpy/core/src/multiarray/ctors.c 9 | -------------------------------------------------------------------------------- /tests/.dir-locals.el: -------------------------------------------------------------------------------- 1 | ((c-mode . ((mode . c++) 2 | (eval . (setq flycheck-gcc-include-path 3 | (list 4 | "../" 5 | "../include" 6 | "../submodules/googletest/googletest/include" 7 | (expand-file-name "~/.virtualenvs/fundamentals/include/python3.6m") 8 | (expand-file-name "~/.virtualenvs/fundamentals/lib/python3.6/site-packages/numpy/core/include") 9 | "/usr/include/python3.6m"))) 10 | (eval . (set-fill-column 90)) 11 | (eval . (c-set-offset 'innamespace 0)))) 12 | (c++-mode . ((eval . (setq flycheck-gcc-include-path 13 | (list 14 | "../" 15 | "../include" 16 | "../submodules/googletest/googletest/include" 17 | (expand-file-name "~/.virtualenvs/fundamentals/include/python3.6m") 18 | (expand-file-name "~/.virtualenvs/fundamentals/lib/python3.6/site-packages/numpy/core/include") 19 | "/usr/include/python3.6m"))) 20 | (eval . (c-set-offset 'innamespace 0)) 21 | (eval . (set-fill-column 90)) 22 | (flycheck-gcc-language-standard . "gnu++17")))) 23 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import libpy # noqa 2 | -------------------------------------------------------------------------------- /tests/_runner.cc: -------------------------------------------------------------------------------- 1 | #include "gtest/gtest.h" 2 | 3 | #define LIBPY_TEST_MAIN 4 | #include "libpy/detail/api.h" 5 | #include "libpy/detail/python.h" 6 | #include "libpy/numpy_utils.h" 7 | 8 | namespace test { 9 | PyObject* run_tests(PyObject*, PyObject* py_argv) { 10 | if (!PyTuple_Check(py_argv)) { 11 | PyErr_SetString(PyExc_TypeError, "py_argv must be a tuple of strings"); 12 | return nullptr; 13 | } 14 | std::vector argv; 15 | for (Py_ssize_t ix = 0; ix < PyTuple_GET_SIZE(py_argv); ++ix) { 16 | PyObject* cs = PyTuple_GET_ITEM(py_argv, ix); 17 | if (!PyBytes_Check(cs)) { 18 | PyErr_SetString(PyExc_TypeError, "py_argv must be a tuple of strings"); 19 | return nullptr; 20 | } 21 | argv.push_back(PyBytes_AS_STRING(cs)); 22 | } 23 | int argc = argv.size(); 24 | argv.push_back(nullptr); 25 | testing::InitGoogleTest(&argc, argv.data()); 26 | // print a newline to start output fresh from the partial line that pytest starts 27 | // us with 28 | std::cout << '\n'; 29 | int out = RUN_ALL_TESTS(); 30 | PyErr_Clear(); 31 | return PyLong_FromLong(out); 32 | } 33 | 34 | PyMethodDef methods[] = { 35 | {"run_tests", run_tests, METH_O, nullptr}, 36 | {nullptr, nullptr, 0, nullptr}, 37 | }; 38 | 39 | PyModuleDef module = { 40 | PyModuleDef_HEAD_INIT, 41 | "_runner", 42 | nullptr, 43 | -1, 44 | methods, 45 | nullptr, 46 | nullptr, 47 | nullptr, 48 | nullptr, 49 | }; 50 | 51 | PyMODINIT_FUNC PyInit__runner() LIBPY_EXPORT; 52 | PyMODINIT_FUNC PyInit__runner() { 53 | import_array(); 54 | return PyModule_Create(&module); 55 | } 56 | } // namespace test 57 | -------------------------------------------------------------------------------- /tests/_test_automodule.cc: -------------------------------------------------------------------------------- 1 | #include "libpy/autoclass.h" 2 | #include "libpy/autofunction.h" 3 | #include "libpy/automodule.h" 4 | 5 | bool is_42(int arg) { 6 | return arg == 42; 7 | } 8 | 9 | bool is_true(bool arg) { 10 | return arg; 11 | } 12 | 13 | using int_float_pair = std::pair; 14 | 15 | int first(const int_float_pair& ob) { 16 | return ob.first; 17 | } 18 | 19 | float second(const int_float_pair& ob) { 20 | return ob.second; 21 | } 22 | 23 | LIBPY_AUTOMODULE(tests, 24 | _test_automodule, 25 | ({py::autofunction("is_42"), 26 | py::autofunction("is_true")})) 27 | (py::borrowed_ref<> m) { 28 | py::autoclass(m, "int_float_pair") 29 | .new_() 30 | .comparisons() 31 | .def("first") 32 | .def("second") 33 | .type(); 34 | return false; 35 | } 36 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantopian/libpy/e174ee103db76a9d0fcd29165d54c676ed1f2629/tests/conftest.py -------------------------------------------------------------------------------- /tests/cxx.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | with warnings.catch_warnings(): 4 | warnings.simplefilter('ignore') 5 | from ._runner import * # noqa 6 | -------------------------------------------------------------------------------- /tests/library_wrappers/test_sparsehash.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "gtest/gtest.h" 5 | 6 | #include "libpy/datetime64.h" 7 | #include "libpy/itertools.h" 8 | #include "libpy/library_wrappers/sparsehash.h" 9 | 10 | #include "test_utils.h" 11 | 12 | namespace test_sparsehash { 13 | 14 | using namespace std::literals; 15 | using namespace py::cs::literals; 16 | 17 | class sparsehash_to_object : public with_python_interpreter {}; 18 | 19 | TEST_F(sparsehash_to_object, sparse_hash_map) { 20 | auto map = google::sparse_hash_map(); 21 | py_test::test_map_to_object_impl(map); 22 | } 23 | 24 | TEST_F(sparsehash_to_object, dense_hash_map) { 25 | auto map = google::dense_hash_map(); 26 | map.set_empty_key("the_empty_key"s); 27 | 28 | py_test::test_map_to_object_impl(map); 29 | } 30 | 31 | TEST_F(sparsehash_to_object, sparse_hash_set) { 32 | auto filler = py_test::examples(); 33 | auto a = google::sparse_hash_set(filler.begin(), filler.end()); 34 | py_test::test_set_to_object_impl(a); 35 | } 36 | 37 | TEST_F(sparsehash_to_object, dense_hash_set) { 38 | auto filler = py_test::examples(); 39 | auto a = google::dense_hash_set(filler.begin(), 40 | filler.end(), 41 | "the_empty_key"s); 42 | py_test::test_set_to_object_impl(a); 43 | } 44 | 45 | TEST(dense_hash_map, invalid_empty_key) { 46 | using double_key = py::dense_hash_map; 47 | EXPECT_THROW((double_key{std::numeric_limits::quiet_NaN()}), 48 | std::invalid_argument); 49 | EXPECT_THROW((double_key{std::numeric_limits::quiet_NaN(), 10}), 50 | std::invalid_argument); 51 | 52 | using float_key = py::dense_hash_map; 53 | EXPECT_THROW((float_key{std::numeric_limits::quiet_NaN()}), 54 | std::invalid_argument); 55 | EXPECT_THROW((float_key{std::numeric_limits::quiet_NaN(), 10}), 56 | std::invalid_argument); 57 | 58 | using M8_key = py::dense_hash_map; 59 | EXPECT_THROW((M8_key{py::datetime64ns::nat()}), std::invalid_argument); 60 | EXPECT_THROW((M8_key{py::datetime64ns::nat(), 10}), std::invalid_argument); 61 | } 62 | 63 | TEST(dense_hash_set, invalid_empty_key) { 64 | using double_key = py::dense_hash_set; 65 | EXPECT_THROW((double_key{std::numeric_limits::quiet_NaN()}), 66 | std::invalid_argument); 67 | EXPECT_THROW((double_key{std::numeric_limits::quiet_NaN(), 10}), 68 | std::invalid_argument); 69 | 70 | using float_key = py::dense_hash_set; 71 | EXPECT_THROW((float_key{std::numeric_limits::quiet_NaN()}), 72 | std::invalid_argument); 73 | EXPECT_THROW((float_key{std::numeric_limits::quiet_NaN(), 10}), 74 | std::invalid_argument); 75 | 76 | using M8_key = py::dense_hash_set; 77 | EXPECT_THROW((M8_key{py::datetime64ns::nat()}), std::invalid_argument); 78 | EXPECT_THROW((M8_key{py::datetime64ns::nat(), 10}), std::invalid_argument); 79 | } 80 | 81 | } // namespace test_sparsehash 82 | -------------------------------------------------------------------------------- /tests/test_automodule.py: -------------------------------------------------------------------------------- 1 | from . import _test_automodule as mod 2 | 3 | 4 | def test_modname(): 5 | assert mod.__name__ == 'tests._test_automodule' 6 | 7 | 8 | def test_function(): 9 | assert mod.is_42(42) 10 | assert not mod.is_42(~42) 11 | 12 | 13 | def test_type(): 14 | assert isinstance(mod.int_float_pair, type) 15 | a = mod.int_float_pair(1, 2.5) 16 | assert a.first() == 1 17 | assert a.second() == 2.5 18 | b = mod.int_float_pair(1, 2.5) 19 | assert a == b 20 | c = mod.int_float_pair(1, 3.5) 21 | assert a != c 22 | -------------------------------------------------------------------------------- /tests/test_call_function.cc: -------------------------------------------------------------------------------- 1 | #include "test_utils.h" 2 | 3 | #include "libpy/call_function.h" 4 | 5 | namespace test_call_function { 6 | using namespace std::literals; 7 | class call_function : public with_python_interpreter {}; 8 | 9 | TEST_F(call_function, basic) { 10 | py::owned_ref ns = RUN_PYTHON(R"( 11 | def f(a, b): 12 | return a + b 13 | )"); 14 | ASSERT_TRUE(ns); 15 | 16 | py::borrowed_ref f = PyDict_GetItemString(ns.get(), "f"); 17 | ASSERT_TRUE(f); 18 | 19 | // Python functions are duck-typed, `f` should be callable with both ints and strings 20 | // (and more) 21 | { 22 | auto result_ob = py::call_function(f, 1, 2); 23 | ASSERT_TRUE(result_ob); 24 | EXPECT_EQ(py::from_object(result_ob), 3); 25 | } 26 | 27 | { 28 | auto result_ob = py::call_function(f, "abc", "def"); 29 | ASSERT_TRUE(result_ob); 30 | EXPECT_EQ(py::from_object(result_ob), "abcdef"s); 31 | } 32 | } 33 | 34 | TEST_F(call_function, exception) { 35 | py::owned_ref ns = RUN_PYTHON(R"( 36 | def f(): 37 | raise ValueError('ayy lmao'); 38 | )"); 39 | ASSERT_TRUE(ns); 40 | 41 | py::borrowed_ref f = PyDict_GetItemString(ns.get(), "f"); 42 | ASSERT_TRUE(f); 43 | 44 | py::owned_ref<> result = py::call_function(f); 45 | EXPECT_FALSE(result); 46 | expect_pyerr_type_and_message(PyExc_ValueError, "ayy lmao"); 47 | PyErr_Clear(); 48 | } 49 | 50 | TEST_F(call_function, method) { 51 | py::owned_ref ns = RUN_PYTHON(R"( 52 | class C(object): 53 | def __init__(self): 54 | self.a = 1 55 | 56 | def f(self, b): 57 | return self.a + b 58 | 59 | ob = C() 60 | )"); 61 | ASSERT_TRUE(ns); 62 | 63 | py::borrowed_ref ob = PyDict_GetItemString(ns.get(), "ob"); 64 | ASSERT_TRUE(ob); 65 | 66 | // Python functions are duck-typed, `f` should be callable with both ints and doubles 67 | // (and more) 68 | { 69 | auto result_ob = py::call_method(ob, "f", 2); 70 | ASSERT_TRUE(result_ob); 71 | EXPECT_EQ(py::from_object(result_ob), 3); 72 | } 73 | 74 | { 75 | auto result_ob = py::call_method(ob, "f", 2.5); 76 | ASSERT_TRUE(result_ob); 77 | EXPECT_EQ(py::from_object(result_ob), 3.5); 78 | } 79 | } 80 | 81 | TEST_F(call_function, method_exception) { 82 | py::owned_ref ns = RUN_PYTHON(R"( 83 | class C(object): 84 | def f(self): 85 | raise ValueError('ayy lmao') 86 | 87 | ob = C() 88 | )"); 89 | ASSERT_TRUE(ns); 90 | 91 | py::borrowed_ref ob = PyDict_GetItemString(ns.get(), "ob"); 92 | ASSERT_TRUE(ob); 93 | 94 | py::owned_ref<> result = py::call_method(ob, "f"); 95 | EXPECT_FALSE(result); 96 | expect_pyerr_type_and_message(PyExc_ValueError, "ayy lmao"); 97 | PyErr_Clear(); 98 | 99 | // should still throw if the method doesn't exist 100 | result = py::call_method(ob, "g"); 101 | EXPECT_FALSE(result); 102 | expect_pyerr_type_and_message(PyExc_AttributeError, 103 | "'C' object has no attribute 'g'"); 104 | PyErr_Clear(); 105 | } 106 | 107 | class call_function_throws : public with_python_interpreter {}; 108 | 109 | TEST_F(call_function_throws, basic) { 110 | py::owned_ref ns = RUN_PYTHON(R"( 111 | def f(a, b): 112 | return a + b 113 | )"); 114 | ASSERT_TRUE(ns); 115 | 116 | py::borrowed_ref f = PyDict_GetItemString(ns.get(), "f"); 117 | ASSERT_TRUE(f); 118 | 119 | // Python functions are duck-typed, `f` should be callable with both ints and strings 120 | // (and more) 121 | { 122 | auto result_ob = py::call_function_throws(f, 1, 2); 123 | ASSERT_TRUE(result_ob); 124 | EXPECT_EQ(py::from_object(result_ob), 3); 125 | } 126 | 127 | { 128 | auto result_ob = py::call_function_throws(f, "abc", "def"); 129 | ASSERT_TRUE(result_ob); 130 | EXPECT_EQ(py::from_object(result_ob), "abcdef"s); 131 | } 132 | } 133 | 134 | TEST_F(call_function_throws, exception) { 135 | py::owned_ref ns = RUN_PYTHON(R"( 136 | def f(): 137 | raise ValueError('ayy lmao'); 138 | )"); 139 | ASSERT_TRUE(ns); 140 | 141 | py::borrowed_ref f = PyDict_GetItemString(ns.get(), "f"); 142 | ASSERT_TRUE(f); 143 | 144 | EXPECT_THROW(py::call_function_throws(f), py::exception); 145 | expect_pyerr_type_and_message(PyExc_ValueError, "ayy lmao"); 146 | PyErr_Clear(); 147 | } 148 | 149 | TEST_F(call_function_throws, method) { 150 | py::owned_ref ns = RUN_PYTHON(R"( 151 | class C(object): 152 | def __init__(self): 153 | self.a = 1 154 | 155 | def f(self, b): 156 | return self.a + b 157 | 158 | ob = C() 159 | )"); 160 | ASSERT_TRUE(ns); 161 | 162 | py::borrowed_ref ob = PyDict_GetItemString(ns.get(), "ob"); 163 | ASSERT_TRUE(ob); 164 | 165 | // Python functions are duck-typed, `f` should be callable with both ints and doubles 166 | // (and more) 167 | { 168 | auto result_ob = py::call_method_throws(ob, "f", 2); 169 | ASSERT_TRUE(result_ob); 170 | EXPECT_EQ(py::from_object(result_ob), 3); 171 | } 172 | 173 | { 174 | auto result_ob = py::call_method_throws(ob, "f", 2.5); 175 | ASSERT_TRUE(result_ob); 176 | EXPECT_EQ(py::from_object(result_ob), 3.5); 177 | } 178 | } 179 | 180 | TEST_F(call_function_throws, method_exception) { 181 | py::owned_ref ns = RUN_PYTHON(R"( 182 | class C(object): 183 | def f(self): 184 | raise ValueError('ayy lmao') 185 | 186 | ob = C() 187 | )"); 188 | ASSERT_TRUE(ns); 189 | 190 | py::borrowed_ref ob = PyDict_GetItemString(ns.get(), "ob"); 191 | ASSERT_TRUE(ob); 192 | 193 | EXPECT_THROW(py::call_method_throws(ob, "f"), py::exception); 194 | expect_pyerr_type_and_message(PyExc_ValueError, "ayy lmao"); 195 | PyErr_Clear(); 196 | } 197 | 198 | } // namespace test_call_function 199 | -------------------------------------------------------------------------------- /tests/test_cxx.py: -------------------------------------------------------------------------------- 1 | import os 2 | import warnings 3 | 4 | from . import cxx 5 | 6 | 7 | def test_cxx(capfd): 8 | # capfd.disabled() will let gtest print its own output 9 | with warnings.catch_warnings(), capfd.disabled(): 10 | warnings.simplefilter('ignore') 11 | assert not cxx.run_tests((b'python',) + tuple( 12 | arg.encode() for arg in os.environ.get('GTEST_ARGS', '').split() 13 | )) 14 | -------------------------------------------------------------------------------- /tests/test_datetime64.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "gtest/gtest.h" 6 | 7 | #include "libpy/call_function.h" 8 | #include "libpy/char_sequence.h" 9 | #include "libpy/datetime64.h" 10 | #include "libpy/detail/python.h" 11 | #include "libpy/exception.h" 12 | #include "libpy/owned_ref.h" 13 | #include "test_utils.h" 14 | 15 | namespace test_datetime64 { 16 | using namespace std::string_literals; 17 | using namespace py::cs::literals; 18 | 19 | template 20 | class datetime64_all_units : public with_python_interpreter {}; 21 | TYPED_TEST_SUITE_P(datetime64_all_units); 22 | 23 | using int64_limits = std::numeric_limits; 24 | 25 | TYPED_TEST_P(datetime64_all_units, from_int) { 26 | constexpr std::int64_t step_size = int64_limits::max() / (1 << 16); 27 | for (int step = 0; step < (1 << 16); ++step) { 28 | auto ticks = int64_limits::min() + step * step_size; 29 | py::datetime64 value(ticks); 30 | ASSERT_EQ(static_cast(value), ticks); 31 | } 32 | } 33 | 34 | TYPED_TEST_P(datetime64_all_units, epoch) { 35 | EXPECT_EQ(static_cast(py::datetime64::epoch()), 0LL); 36 | } 37 | 38 | TYPED_TEST_P(datetime64_all_units, max) { 39 | EXPECT_EQ(static_cast(py::datetime64::max()), 40 | int64_limits::max()); 41 | } 42 | 43 | TYPED_TEST_P(datetime64_all_units, nat) { 44 | EXPECT_EQ(static_cast(py::datetime64::nat()), 45 | int64_limits::min()); 46 | 47 | // default construct is also nat 48 | EXPECT_EQ(static_cast(py::datetime64{}), 49 | int64_limits::min()); 50 | 51 | // unambiguously test `operator==()` and `operator!=()`: 52 | EXPECT_FALSE(py::datetime64{} == py::datetime64{}); 53 | EXPECT_TRUE(py::datetime64{} != py::datetime64{}); 54 | } 55 | 56 | TYPED_TEST_P(datetime64_all_units, min) { 57 | if (std::is_same_v) { 58 | // we use the same value as pandas.Timestamp.min for datetime64 59 | EXPECT_EQ(static_cast(py::datetime64::min()), 60 | -9223285636854775000LL); 61 | } 62 | else { 63 | EXPECT_EQ(static_cast(py::datetime64::min()), 64 | // min is reserved for nat 65 | int64_limits::min() + 1); 66 | } 67 | } 68 | 69 | TYPED_TEST_P(datetime64_all_units, stream_format_nat) { 70 | std::stringstream stream; 71 | stream << py::datetime64::nat(); 72 | EXPECT_EQ(stream.str(), "NaT"s); 73 | } 74 | 75 | template 76 | constexpr void* numpy_unit_str = std::enable_if_t>{}; 77 | 78 | template<> 79 | constexpr auto numpy_unit_str = "ns"_arr; 80 | 81 | template<> 82 | constexpr auto numpy_unit_str = "us"_arr; 83 | 84 | template<> 85 | constexpr auto numpy_unit_str = "ms"_arr; 86 | 87 | template<> 88 | constexpr auto numpy_unit_str = "s"_arr; 89 | 90 | template<> 91 | constexpr auto numpy_unit_str = "m"_arr; 92 | 93 | template<> 94 | constexpr auto numpy_unit_str = "h"_arr; 95 | 96 | template<> 97 | constexpr auto numpy_unit_str = "D"_arr; 98 | 99 | TYPED_TEST_P(datetime64_all_units, stream_format) { 100 | auto numpy_mod = py::owned_ref(PyImport_ImportModule("numpy")); 101 | if (!numpy_mod) { 102 | throw py::exception{}; 103 | } 104 | auto numpy_datetime64 = py::owned_ref( 105 | PyObject_GetAttrString(numpy_mod.get(), "datetime64")); 106 | if (!numpy_datetime64) { 107 | throw py::exception{}; 108 | } 109 | 110 | using dt = py::datetime64; 111 | constexpr std::size_t step_size = 18446657673709550807ULL / (1 << 16); 112 | constexpr auto min_ticks = static_cast(dt::min()); 113 | // numpy overflows the repr code somewhere if we use really massively negative values 114 | for (int step = 1; step < (1 << 16); ++step) { 115 | std::int64_t ticks = min_ticks + step * step_size; 116 | dt value(ticks); 117 | 118 | std::stringstream stream; 119 | stream << value; 120 | auto res = py::call_function(numpy_datetime64, 121 | static_cast(value), 122 | numpy_unit_str); 123 | if (!res) { 124 | throw py::exception{}; 125 | } 126 | auto repr = py::owned_ref(PyObject_Str(res.get())); 127 | if (!repr) { 128 | throw py::exception{}; 129 | } 130 | auto repr_text = py::util::pystring_to_cstring(repr.get()); 131 | if (!repr_text) { 132 | throw py::exception{}; 133 | } 134 | 135 | ASSERT_STREQ(stream.str().c_str(), repr_text) << "ticks=" << ticks; 136 | } 137 | } 138 | 139 | REGISTER_TYPED_TEST_SUITE_P(datetime64_all_units, 140 | from_int, 141 | epoch, 142 | max, 143 | nat, 144 | min, 145 | stream_format_nat, 146 | stream_format); 147 | 148 | using units = testing::Types; 155 | INSTANTIATE_TYPED_TEST_SUITE_P(typed_, datetime64_all_units, units); 156 | } // namespace test_datetime64 157 | -------------------------------------------------------------------------------- /tests/test_demangle.cc: -------------------------------------------------------------------------------- 1 | #include "gtest/gtest.h" 2 | 3 | #include "libpy/demangle.h" 4 | 5 | struct test_demangle_global_type {}; 6 | 7 | namespace test_demangle { 8 | class test_type_name_type { 9 | public: 10 | class inner_type {}; 11 | }; 12 | 13 | namespace inner { 14 | class test_type_name_type {}; 15 | } // namespace inner 16 | 17 | TEST(demangle, type_name) { 18 | { 19 | auto name = py::util::type_name(); 20 | EXPECT_EQ(name, "test_demangle_global_type"); 21 | } 22 | { 23 | auto name = py::util::type_name(); 24 | EXPECT_EQ(name, "test_demangle::test_type_name_type"); 25 | } 26 | 27 | { 28 | auto name = py::util::type_name(); 29 | EXPECT_EQ(name, "test_demangle::inner::test_type_name_type"); 30 | } 31 | 32 | { 33 | class test_type_name_type {}; 34 | auto name = py::util::type_name(); 35 | EXPECT_EQ( 36 | name, 37 | "test_demangle::demangle_type_name_Test::TestBody()::test_type_name_type"); 38 | } 39 | 40 | { 41 | auto name = py::util::type_name(); 42 | EXPECT_EQ(name, "test_demangle::test_type_name_type::inner_type"); 43 | } 44 | } 45 | 46 | TEST(demangle, reference) { 47 | { 48 | auto name = py::util::type_name(); 49 | EXPECT_EQ(name, "int&"); 50 | } 51 | 52 | { 53 | auto name = py::util::type_name(); 54 | EXPECT_EQ(name, "int&&"); 55 | } 56 | } 57 | } // namespace test_demangle 58 | -------------------------------------------------------------------------------- /tests/test_dict_range.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "gtest/gtest.h" 5 | 6 | #include "libpy/detail/python.h" 7 | #include "libpy/dict_range.h" 8 | #include "libpy/exception.h" 9 | #include "test_utils.h" 10 | 11 | namespace test_dict_range { 12 | using namespace std::literals; 13 | 14 | class dict_range : public with_python_interpreter {}; 15 | 16 | TEST_F(dict_range, iteration) { 17 | py::owned_ref ns = RUN_PYTHON(R"( 18 | dict_0 = {} 19 | dict_1 = {b'a': 1} 20 | dict_2 = {b'a': 1, b'b': 2, b'c': 3} 21 | )"); 22 | ASSERT_TRUE(ns); 23 | 24 | { 25 | py::borrowed_ref dict_0 = PyDict_GetItemString(ns.get(), "dict_0"); 26 | ASSERT_TRUE(dict_0); 27 | 28 | for ([[maybe_unused]] auto item : py::dict_range(dict_0)) { 29 | FAIL() << "empty dict should not enter the loop"; 30 | } 31 | } 32 | 33 | { 34 | py::borrowed_ref dict_1 = PyDict_GetItemString(ns.get(), "dict_1"); 35 | ASSERT_TRUE(dict_1); 36 | 37 | std::unordered_map expected = {{"a"s, 1}}; 38 | std::unordered_map actual; 39 | 40 | for (auto [key, value] : py::dict_range(dict_1)) { 41 | actual.emplace(py::from_object(key), 42 | py::from_object(value)); 43 | } 44 | 45 | EXPECT_EQ(actual, expected); 46 | } 47 | 48 | { 49 | py::borrowed_ref dict_2 = PyDict_GetItemString(ns.get(), "dict_2"); 50 | ASSERT_TRUE(dict_2); 51 | 52 | std::unordered_map expected = {{"a"s, 1}, 53 | {"b"s, 2}, 54 | {"c"s, 3}}; 55 | std::unordered_map actual; 56 | 57 | for (auto [key, value] : py::dict_range(dict_2)) { 58 | actual.emplace(py::from_object(key), 59 | py::from_object(value)); 60 | } 61 | 62 | EXPECT_EQ(actual, expected); 63 | } 64 | } 65 | 66 | TEST_F(dict_range, not_a_dict) { 67 | py::owned_ref not_a_dict(PyList_New(0)); 68 | ASSERT_TRUE(not_a_dict); 69 | 70 | EXPECT_THROW(py::dict_range::checked(not_a_dict), py::exception); 71 | PyErr_Clear(); 72 | } 73 | } // namespace test_dict_range 74 | -------------------------------------------------------------------------------- /tests/test_exception.cc: -------------------------------------------------------------------------------- 1 | #include "libpy/exception.h" 2 | #include "test_utils.h" 3 | 4 | namespace test_exception { 5 | class exception : public with_python_interpreter {}; 6 | 7 | TEST_F(exception, raise_from_cxx) { 8 | ASSERT_FALSE(PyErr_Occurred()); 9 | 10 | py::raise_from_cxx_exception(std::runtime_error("msg")); 11 | expect_pyerr_type_and_message(PyExc_RuntimeError, "a C++ exception was raised: msg"); 12 | 13 | // Raising again should preserve existing error indicator type and append to the 14 | // message 15 | py::raise_from_cxx_exception(std::runtime_error("msg2")); 16 | expect_pyerr_type_and_message( 17 | PyExc_RuntimeError, 18 | "a C++ exception was raised: msg; raised from C++ exception: msg2"); 19 | 20 | PyErr_Clear(); 21 | 22 | PyErr_SetString(PyExc_IndentationError, "pymsg"); 23 | py::raise_from_cxx_exception(std::runtime_error("msg")); 24 | expect_pyerr_type_and_message(PyExc_IndentationError, 25 | "pymsg; raised from C++ exception: msg"); 26 | 27 | // Raising again should preserve existing error indicator type and append to the 28 | // message 29 | py::raise_from_cxx_exception(std::runtime_error("msg2")); 30 | expect_pyerr_type_and_message( 31 | PyExc_IndentationError, 32 | "pymsg; raised from C++ exception: msg; raised from C++ exception: msg2"); 33 | 34 | PyErr_Clear(); 35 | } 36 | } // namespace test_exception 37 | -------------------------------------------------------------------------------- /tests/test_getattr.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "gtest/gtest.h" 5 | 6 | #include "libpy/detail/python.h" 7 | #include "libpy/getattr.h" 8 | #include "test_utils.h" 9 | 10 | namespace test_getattr { 11 | class getattr : public with_python_interpreter {}; 12 | 13 | TEST_F(getattr, simple) { 14 | py::owned_ref ns = RUN_PYTHON(R"( 15 | class A(object): 16 | b = 1 17 | 18 | expected = A.b 19 | )"); 20 | ASSERT_TRUE(ns); 21 | 22 | py::borrowed_ref A = PyDict_GetItemString(ns.get(), "A"); 23 | ASSERT_TRUE(A); 24 | 25 | py::owned_ref<> actual = py::getattr(A, "b"); 26 | ASSERT_TRUE(actual); 27 | 28 | py::borrowed_ref expected = PyDict_GetItemString(ns.get(), "expected"); 29 | ASSERT_TRUE(expected); 30 | 31 | // compare them using object identity 32 | EXPECT_EQ(actual, expected); 33 | } 34 | 35 | TEST_F(getattr, attribute_error) { 36 | py::owned_ref ns = RUN_PYTHON(R"( 37 | class A(object): 38 | pass 39 | )"); 40 | ASSERT_TRUE(ns); 41 | 42 | py::borrowed_ref A = PyDict_GetItemString(ns.get(), "A"); 43 | ASSERT_TRUE(A); 44 | 45 | py::owned_ref<> actual = py::getattr(A, "b"); 46 | ASSERT_FALSE(actual); 47 | expect_pyerr_type_and_message(PyExc_AttributeError, 48 | "type object 'A' has no attribute 'b'"); 49 | PyErr_Clear(); 50 | } 51 | 52 | TEST_F(getattr, nested) { 53 | py::owned_ref ns = RUN_PYTHON(R"( 54 | class A(object): 55 | class B(object): 56 | class C(object): 57 | d = 1 58 | 59 | expected = A.B.C.d 60 | )"); 61 | ASSERT_TRUE(ns); 62 | 63 | py::borrowed_ref A = PyDict_GetItemString(ns.get(), "A"); 64 | ASSERT_TRUE(A); 65 | 66 | py::owned_ref<> actual = py::nested_getattr(A, "B", "C", "d"); 67 | ASSERT_TRUE(actual); 68 | 69 | py::borrowed_ref expected = PyDict_GetItemString(ns.get(), "expected"); 70 | ASSERT_TRUE(expected); 71 | 72 | // compare them using object identity 73 | EXPECT_EQ(actual, expected); 74 | } 75 | 76 | TEST_F(getattr, nested_failure) { 77 | py::owned_ref ns = RUN_PYTHON(R"( 78 | class A(object): 79 | class B(object): 80 | class C(object): 81 | pass 82 | )"); 83 | ASSERT_TRUE(ns); 84 | 85 | py::borrowed_ref A = PyDict_GetItemString(ns.get(), "A"); 86 | ASSERT_TRUE(A); 87 | 88 | // attempt to access a few fields past the end of the real attribute chain. 89 | py::owned_ref<> actual = py::nested_getattr(A, "B", "C", "D", "E"); 90 | ASSERT_FALSE(actual); 91 | expect_pyerr_type_and_message(PyExc_AttributeError, 92 | "type object 'C' has no attribute 'D'"); 93 | PyErr_Clear(); 94 | } 95 | 96 | class getattr_throws : public with_python_interpreter {}; 97 | 98 | TEST_F(getattr_throws, simple) { 99 | py::owned_ref ns = RUN_PYTHON(R"( 100 | class A(object): 101 | b = 1 102 | 103 | expected = A.b 104 | )"); 105 | ASSERT_TRUE(ns); 106 | 107 | py::borrowed_ref A = PyDict_GetItemString(ns.get(), "A"); 108 | ASSERT_TRUE(A); 109 | 110 | py::owned_ref<> actual = py::getattr_throws(A, "b"); 111 | ASSERT_TRUE(actual); 112 | 113 | py::borrowed_ref expected = PyDict_GetItemString(ns.get(), "expected"); 114 | ASSERT_TRUE(expected); 115 | 116 | // compare them using object identity 117 | EXPECT_EQ(actual, expected); 118 | } 119 | 120 | TEST_F(getattr_throws, attribute_error) { 121 | py::owned_ref ns = RUN_PYTHON(R"( 122 | class A(object): 123 | pass 124 | )"); 125 | ASSERT_TRUE(ns); 126 | 127 | py::borrowed_ref A = PyDict_GetItemString(ns.get(), "A"); 128 | ASSERT_TRUE(A); 129 | 130 | EXPECT_THROW(py::getattr_throws(A, "b"), py::exception); 131 | expect_pyerr_type_and_message(PyExc_AttributeError, 132 | "type object 'A' has no attribute 'b'"); 133 | PyErr_Clear(); 134 | } 135 | 136 | TEST_F(getattr_throws, nested) { 137 | py::owned_ref ns = RUN_PYTHON(R"( 138 | class A(object): 139 | class B(object): 140 | class C(object): 141 | d = 1 142 | 143 | expected = A.B.C.d 144 | )"); 145 | ASSERT_TRUE(ns); 146 | 147 | py::borrowed_ref A = PyDict_GetItemString(ns.get(), "A"); 148 | ASSERT_TRUE(A); 149 | 150 | py::owned_ref<> actual = py::nested_getattr_throws(A, "B", "C", "d"); 151 | ASSERT_TRUE(actual); 152 | 153 | py::borrowed_ref expected = PyDict_GetItemString(ns.get(), "expected"); 154 | ASSERT_TRUE(expected); 155 | 156 | // compare them using object identity 157 | EXPECT_EQ(actual, expected); 158 | } 159 | 160 | TEST_F(getattr_throws, nested_failure) { 161 | py::owned_ref ns = RUN_PYTHON(R"( 162 | class A(object): 163 | class B(object): 164 | class C(object): 165 | pass 166 | )"); 167 | ASSERT_TRUE(ns); 168 | 169 | py::borrowed_ref A = PyDict_GetItemString(ns.get(), "A"); 170 | ASSERT_TRUE(A); 171 | 172 | // attempt to access a few fields past the end of the real attribute chain. 173 | EXPECT_THROW(py::nested_getattr_throws(A, "B", "C", "D", "E"), py::exception); 174 | expect_pyerr_type_and_message(PyExc_AttributeError, 175 | "type object 'C' has no attribute 'D'"); 176 | PyErr_Clear(); 177 | } 178 | } // namespace test_getattr 179 | -------------------------------------------------------------------------------- /tests/test_hash.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "gtest/gtest.h" 5 | 6 | #include "libpy/hash.h" 7 | 8 | namespace test_hash { 9 | template 10 | std::string random_string(RandomEngine& g) { 11 | std::uniform_int_distribution d(0); 12 | unsigned char length = d(g); 13 | std::string out; 14 | for (unsigned char ix = 0; ix < length; ++ix) { 15 | out.push_back(d(g)); 16 | } 17 | return out; 18 | } 19 | 20 | TEST(hash, buffer) { 21 | std::mt19937 g(1868655980); 22 | 23 | for (std::size_t n = 0; n < 1000; ++n) { 24 | std::string s = random_string(g); 25 | EXPECT_EQ(std::hash{}(s), py::hash_buffer(s.data(), s.size())); 26 | } 27 | } 28 | } // namespace test_hash 29 | -------------------------------------------------------------------------------- /tests/test_itertools.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "gtest/gtest.h" 5 | 6 | #include "libpy/itertools.h" 7 | 8 | namespace test_itertools { 9 | TEST(zip, mismatched_sizes) { 10 | std::vector as(10); 11 | std::vector bs(5); 12 | 13 | EXPECT_THROW(py::zip(as, bs), std::invalid_argument); 14 | } 15 | 16 | TEST(zip, const_iterator) { 17 | std::size_t size = 10; 18 | 19 | int counter = 0; 20 | std::vector as(size); 21 | std::vector bs(size); 22 | std::vector cs(size); 23 | 24 | auto gen = [&counter]() { return counter++; }; 25 | std::generate(as.begin(), as.end(), gen); 26 | std::generate(bs.begin(), bs.end(), gen); 27 | std::generate(cs.begin(), cs.end(), gen); 28 | 29 | std::size_t ix = 0; 30 | for (const auto [a, b, c] : py::zip(as, bs, cs)) { 31 | EXPECT_EQ(a, as[ix]); 32 | EXPECT_EQ(b, bs[ix]); 33 | EXPECT_EQ(c, cs[ix]); 34 | 35 | ++ix; 36 | } 37 | 38 | EXPECT_EQ(ix, size); 39 | } 40 | 41 | TEST(zip, mutable_iterator) { 42 | std::size_t size = 10; 43 | 44 | int counter = 0; 45 | std::vector as(size); 46 | std::vector bs(size); 47 | std::vector cs(size); 48 | 49 | auto gen = [&counter]() { return counter++; }; 50 | std::generate(as.begin(), as.end(), gen); 51 | std::generate(bs.begin(), bs.end(), gen); 52 | std::generate(cs.begin(), cs.end(), gen); 53 | 54 | std::vector as_original = as; 55 | std::vector bs_original = bs; 56 | std::vector cs_original = cs; 57 | 58 | std::size_t ix = 0; 59 | for (auto [a, b, c] : py::zip(as, bs, cs)) { 60 | EXPECT_EQ(a, as[ix]); 61 | EXPECT_EQ(b, bs[ix]); 62 | EXPECT_EQ(c, cs[ix]); 63 | 64 | a = -a; 65 | b = -b; 66 | c = -c; 67 | 68 | ++ix; 69 | } 70 | 71 | EXPECT_EQ(ix, size); 72 | 73 | for (std::size_t ix = 0; ix < size; ++ix) { 74 | EXPECT_EQ(as[ix], -as_original[ix]); 75 | EXPECT_EQ(bs[ix], -bs_original[ix]); 76 | EXPECT_EQ(cs[ix], -cs_original[ix]); 77 | } 78 | } 79 | 80 | TEST(imap, simple_iterator) { 81 | std::vector it = {0, 1, 2, 3, 4}; 82 | 83 | { 84 | std::vector expected = {0, 2, 4, 6, 8}; 85 | 86 | std::size_t ix = 0; 87 | for (auto cs : py::imap([](auto i) { return i * 2; }, it)) { 88 | EXPECT_EQ(cs, expected[ix++]); 89 | } 90 | } 91 | 92 | { 93 | std::vector expected = {"0", "1", "2", "3", "4"}; 94 | 95 | std::size_t ix = 0; 96 | for (auto cs : py::imap([](auto i) { return std::to_string(i); }, it)) { 97 | EXPECT_EQ(cs, expected[ix++]); 98 | } 99 | } 100 | 101 | { 102 | std::vector expected = {5, 6, 7, 8, 9}; 103 | 104 | int rhs = 5; // test mapping a closure 105 | std::size_t ix = 0; 106 | for (auto cs : py::imap([rhs](auto i) { return i + rhs; }, it)) { 107 | EXPECT_EQ(cs, expected[ix++]); 108 | } 109 | } 110 | } 111 | 112 | TEST(imap, const_iterator) { 113 | const std::vector it = {0, 1, 2, 3, 4}; 114 | 115 | { 116 | std::vector expected = {0, 2, 4, 6, 8}; 117 | 118 | std::size_t ix = 0; 119 | for (auto cs : py::imap([](auto i) { return i * 2; }, it)) { 120 | EXPECT_EQ(cs, expected[ix++]); 121 | } 122 | } 123 | 124 | { 125 | std::vector expected = {"0", "1", "2", "3", "4"}; 126 | 127 | std::size_t ix = 0; 128 | for (auto cs : py::imap([](auto i) { return std::to_string(i); }, it)) { 129 | EXPECT_EQ(cs, expected[ix++]); 130 | } 131 | } 132 | 133 | { 134 | std::vector expected = {5, 6, 7, 8, 9}; 135 | 136 | int rhs = 5; // test mapping a closure 137 | std::size_t ix = 0; 138 | for (auto cs : py::imap([rhs](auto i) { return i + rhs; }, it)) { 139 | EXPECT_EQ(cs, expected[ix++]); 140 | } 141 | } 142 | } 143 | } // namespace test_itertools 144 | -------------------------------------------------------------------------------- /tests/test_meta.cc: -------------------------------------------------------------------------------- 1 | #include "gtest/gtest.h" 2 | 3 | #include "libpy/meta.h" 4 | 5 | namespace test_meta { 6 | struct my_type {}; 7 | 8 | TEST(element_of, true_cases) { 9 | bool case0 = py::meta::element_of>; 10 | EXPECT_EQ(case0, true); 11 | 12 | bool case1 = py::meta::element_of>; 13 | EXPECT_EQ(case1, true); 14 | 15 | bool case2 = py::meta::element_of>; 16 | EXPECT_EQ(case2, true); 17 | 18 | bool case3 = py::meta::element_of, std::tuple>>; 19 | EXPECT_EQ(case3, true); 20 | 21 | bool case4 = py::meta::element_of>; 22 | EXPECT_EQ(case4, true); 23 | 24 | bool case5 = py::meta::element_of>; 25 | EXPECT_EQ(case5, true); 26 | } 27 | 28 | TEST(element_of, false_cases) { 29 | bool case0 = py::meta::element_of>; 30 | EXPECT_EQ(case0, false); 31 | 32 | bool case1 = py::meta::element_of>; 33 | EXPECT_EQ(case1, false); 34 | 35 | bool case2 = py::meta::element_of, std::tuple>; 36 | EXPECT_EQ(case2, false); 37 | } 38 | 39 | TEST(set_diff, tests) { 40 | using A = std::tuple; 41 | 42 | { 43 | using B = std::tuple; 44 | using Expected = std::tuple; 45 | testing::StaticAssertTypeEq, Expected>(); 46 | } 47 | 48 | { 49 | using B = std::tuple; 50 | using Expected = std::tuple; 51 | testing::StaticAssertTypeEq, Expected>(); 52 | } 53 | 54 | { 55 | using B = std::tuple; 56 | using Expected = std::tuple; 57 | testing::StaticAssertTypeEq, Expected>(); 58 | } 59 | 60 | { 61 | using A = std::tuple; 62 | using B = std::tuple; 63 | using Expected = std::tuple; 64 | testing::StaticAssertTypeEq, Expected>(); 65 | } 66 | } 67 | } // namespace test_meta 68 | -------------------------------------------------------------------------------- /tests/test_numpy_utils.h: -------------------------------------------------------------------------------- 1 | #include "gtest/gtest.h" 2 | 3 | #include "libpy/numpy_utils.h" 4 | 5 | namespace test_numpy_utils { 6 | TEST(py_bool, hash) { 7 | auto check_hash = [](bool v) { 8 | py::py_bool u{v}; 9 | EXPECT_EQ(std::hash{}(u), std::hash{}(v)); 10 | }; 11 | 12 | check_hash(false); 13 | check_hash(true); 14 | } 15 | } // namespace test_numpy_utils 16 | -------------------------------------------------------------------------------- /tests/test_object_map_key.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "gtest/gtest.h" 5 | 6 | #include "libpy/exception.h" 7 | #include "libpy/meta.h" 8 | #include "libpy/object_map_key.h" 9 | #include "libpy/owned_ref.h" 10 | #include "libpy/to_object.h" 11 | #include "test_utils.h" 12 | 13 | namespace test_object_hash_key { 14 | class object_map_key : public with_python_interpreter {}; 15 | 16 | TEST_F(object_map_key, eq) { 17 | py::object_map_key a{py::to_object(9001)}; 18 | ASSERT_TRUE(a); 19 | 20 | py::object_map_key b{py::to_object(9001)}; 21 | ASSERT_TRUE(b); 22 | 23 | // check that we aren't just checking pointer equality 24 | EXPECT_NE(a.get(), b.get()); 25 | EXPECT_EQ(a, b); 26 | 27 | EXPECT_EQ(py::object_map_key{nullptr}, py::object_map_key{nullptr}); 28 | } 29 | 30 | TEST_F(object_map_key, ne) { 31 | py::object_map_key a{py::to_object(9001)}; 32 | ASSERT_TRUE(a); 33 | 34 | py::object_map_key b{py::to_object(9002)}; 35 | ASSERT_TRUE(b); 36 | 37 | // check that we aren't just checking pointer equality 38 | EXPECT_NE(a.get(), b.get()); 39 | EXPECT_NE(a, b); 40 | EXPECT_NE(a, nullptr); 41 | } 42 | 43 | TEST_F(object_map_key, lt) { 44 | py::object_map_key a{py::to_object(9000)}; 45 | ASSERT_TRUE(a); 46 | 47 | py::object_map_key b{py::to_object(9001)}; 48 | ASSERT_TRUE(b); 49 | 50 | // check that we aren't just checking pointer equality 51 | EXPECT_NE(a.get(), b.get()); 52 | EXPECT_LT(a, b); 53 | 54 | EXPECT_LT(a, nullptr); 55 | 56 | // NOTE: don't test EXPECT_GE` here because we are testing `operator<` explicitly 57 | EXPECT_FALSE(py::object_map_key{nullptr} < a); 58 | EXPECT_FALSE(py::object_map_key{nullptr} < nullptr); 59 | } 60 | 61 | TEST_F(object_map_key, le) { 62 | py::object_map_key a{py::to_object(9000)}; 63 | ASSERT_TRUE(a); 64 | 65 | py::object_map_key b{py::to_object(9001)}; 66 | ASSERT_TRUE(b); 67 | 68 | py::object_map_key c{py::to_object(9001)}; 69 | ASSERT_TRUE(c); 70 | 71 | // check that we aren't just checking pointer equality 72 | EXPECT_NE(a.get(), b.get()); 73 | EXPECT_LE(a, b); 74 | 75 | EXPECT_NE(a.get(), c.get()); 76 | EXPECT_LE(a, c); 77 | 78 | EXPECT_LE(a, nullptr); 79 | 80 | // NOTE: don't test EXPECT_GE` here because we are testing `operator<` explicitly 81 | EXPECT_FALSE(py::object_map_key{nullptr} <= a); 82 | EXPECT_LE(py::object_map_key{nullptr}, nullptr); 83 | } 84 | 85 | TEST_F(object_map_key, ge) { 86 | py::object_map_key a{py::to_object(9000)}; 87 | ASSERT_TRUE(a); 88 | 89 | py::object_map_key b{py::to_object(9001)}; 90 | ASSERT_TRUE(b); 91 | 92 | py::object_map_key c{py::to_object(9001)}; 93 | ASSERT_TRUE(c); 94 | 95 | // check that we aren't just checking pointer equality 96 | EXPECT_NE(a.get(), b.get()); 97 | EXPECT_GE(b, a); 98 | 99 | EXPECT_NE(a.get(), c.get()); 100 | EXPECT_GE(b, c); 101 | 102 | // NOTE: don't test EXPECT_LT` here because we are testing `operator>=` explicitly 103 | EXPECT_FALSE(a >= nullptr); 104 | EXPECT_GE(py::object_map_key{nullptr}, a); 105 | EXPECT_GE(py::object_map_key{nullptr}, nullptr); 106 | } 107 | 108 | TEST_F(object_map_key, gt) { 109 | py::object_map_key a{py::to_object(9000)}; 110 | ASSERT_TRUE(a); 111 | 112 | py::object_map_key b{py::to_object(9001)}; 113 | ASSERT_TRUE(b); 114 | 115 | // check that we aren't just checking pointer equality 116 | EXPECT_NE(a.get(), b.get()); 117 | EXPECT_GT(b, a); 118 | 119 | // NOTE: don't test EXPECT_LE` here because we are testing `operator>` explicitly 120 | EXPECT_FALSE(a > nullptr); 121 | EXPECT_FALSE(py::object_map_key{nullptr} < nullptr); 122 | EXPECT_GT(py::object_map_key{nullptr}, a); 123 | } 124 | 125 | template 126 | void test_fails(const std::string& method, F f) { 127 | using namespace std::literals; 128 | 129 | py::owned_ref ns = RUN_PYTHON(R"( 130 | class C(object): 131 | def __)"s + method + R"(__(self, other): 132 | raise ValueError('ayy lmao') 133 | 134 | a = C() 135 | b = C() 136 | )"); 137 | ASSERT_TRUE(ns); 138 | 139 | py::borrowed_ref a_ob = PyDict_GetItemString(ns.get(), "a"); 140 | ASSERT_TRUE(a_ob); 141 | Py_INCREF(a_ob); 142 | py::object_map_key a{a_ob}; 143 | 144 | py::borrowed_ref b_ob = PyDict_GetItemString(ns.get(), "b"); 145 | ASSERT_TRUE(b_ob); 146 | Py_INCREF(b_ob); 147 | py::object_map_key b{b_ob}; 148 | 149 | EXPECT_THROW(static_cast(f(a, b)), py::exception); 150 | expect_pyerr_type_and_message(PyExc_ValueError, "ayy lmao"); 151 | PyErr_Clear(); 152 | } 153 | 154 | TEST_F(object_map_key, eq_fails) { 155 | test_fails("eq", py::meta::op::eq{}); 156 | } 157 | 158 | TEST_F(object_map_key, ne_fails) { 159 | test_fails("ne", py::meta::op::ne{}); 160 | } 161 | 162 | TEST_F(object_map_key, lt_fails) { 163 | test_fails("lt", py::meta::op::lt{}); 164 | } 165 | 166 | TEST_F(object_map_key, le_fails) { 167 | test_fails("le", py::meta::op::le{}); 168 | } 169 | 170 | TEST_F(object_map_key, ge_fails) { 171 | test_fails("ge", py::meta::op::ge{}); 172 | } 173 | 174 | TEST_F(object_map_key, gt_fails) { 175 | test_fails("gt", py::meta::op::gt{}); 176 | } 177 | 178 | TEST_F(object_map_key, hash) { 179 | int value = 10; 180 | py::object_map_key a{py::to_object(value)}; 181 | ASSERT_TRUE(a); 182 | 183 | EXPECT_EQ(std::hash{}(a), value); 184 | 185 | // nullptr just hashes to 0 186 | EXPECT_EQ(std::hash{}(py::object_map_key{}), 0); 187 | } 188 | 189 | TEST_F(object_map_key, hash_fails) { 190 | py::owned_ref ns = RUN_PYTHON(R"( 191 | class C(object): 192 | def __hash__(self, other): 193 | raise ValueError() 194 | 195 | a = C() 196 | )"); 197 | ASSERT_TRUE(ns); 198 | 199 | py::borrowed_ref a_ob = PyDict_GetItemString(ns.get(), "a"); 200 | ASSERT_TRUE(a_ob); 201 | Py_INCREF(a_ob); 202 | py::object_map_key a{a_ob}; 203 | 204 | EXPECT_THROW(static_cast(std::hash{}(a)), py::exception); 205 | PyErr_Clear(); 206 | } 207 | 208 | template 209 | void test_use_in_map(M map) { 210 | 211 | int a_value = 10; 212 | py::object_map_key a_key{py::to_object(a_value)}; 213 | ASSERT_TRUE(a_key); 214 | 215 | map[a_key] = a_value; 216 | 217 | EXPECT_EQ(map[a_key], a_value); 218 | 219 | int b_value = 20; 220 | // use a scoped ref to test implicit conversions 221 | py::owned_ref b_key = py::to_object(b_value); 222 | ASSERT_TRUE(b_key); 223 | 224 | map[b_key] = b_value; 225 | 226 | EXPECT_EQ(map[b_key], b_value); 227 | EXPECT_EQ(map[a_key], a_value); 228 | 229 | int c_value = 30; 230 | py::owned_ref c_key = py::to_object(c_value); 231 | ASSERT_TRUE(c_key); 232 | 233 | map.insert({c_key, c_value}); 234 | 235 | EXPECT_EQ(map[c_key], c_value); 236 | EXPECT_EQ(map[b_key], b_value); 237 | EXPECT_EQ(map[a_key], a_value); 238 | } 239 | 240 | TEST_F(object_map_key, use_in_map) { 241 | test_use_in_map(std::map{}); 242 | test_use_in_map(std::unordered_map{}); 243 | } 244 | 245 | TEST_F(object_map_key, convert) { 246 | py::owned_ref<> ob = py::to_object(1); 247 | ASSERT_TRUE(ob); 248 | 249 | py::object_map_key m = ob; 250 | EXPECT_EQ(m.get(), ob.get()); 251 | EXPECT_EQ(static_cast>(m).get(), ob.get()); 252 | } 253 | } // namespace test_object_hash_key 254 | -------------------------------------------------------------------------------- /tests/test_range.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "gtest/gtest.h" 5 | 6 | #include "libpy/detail/python.h" 7 | #include "libpy/exception.h" 8 | #include "libpy/range.h" 9 | #include "test_utils.h" 10 | 11 | namespace test_dict_range { 12 | using namespace std::literals; 13 | 14 | class range : public with_python_interpreter {}; 15 | 16 | TEST_F(range, iteration) { 17 | py::owned_ref ns = RUN_PYTHON(R"( 18 | it_0 = [] 19 | it_1 = [1, 2, 3] 20 | it_2 = (1, 2, 3) 21 | 22 | def gen(): 23 | yield 1 24 | yield 2 25 | yield 3 26 | 27 | it_3 = gen() 28 | )"); 29 | ASSERT_TRUE(ns); 30 | 31 | { 32 | py::borrowed_ref it_0 = PyDict_GetItemString(ns.get(), "it_0"); 33 | ASSERT_TRUE(it_0); 34 | 35 | for ([[maybe_unused]] auto item : py::range(it_0)) { 36 | FAIL() << "empty sequence should not enter the loop"; 37 | } 38 | } 39 | 40 | { 41 | std::vector expected = {1, 2, 3}; 42 | 43 | for (const auto name : {"it_1", "it_2", "it_3"}) { 44 | py::borrowed_ref it = PyDict_GetItemString(ns.get(), name); 45 | ASSERT_TRUE(it); 46 | 47 | std::vector actual; 48 | 49 | for (auto value : py::range(it)) { 50 | actual.emplace_back(py::from_object(value)); 51 | } 52 | 53 | EXPECT_EQ(actual, expected); 54 | } 55 | } 56 | } 57 | 58 | TEST_F(range, not_iterable) { 59 | EXPECT_THROW(py::range{Py_None}, py::exception); 60 | expect_pyerr_type_and_message(PyExc_TypeError, "'NoneType' object is not iterable"); 61 | PyErr_Clear(); 62 | } 63 | 64 | TEST_F(range, exception_in_next) { 65 | py::owned_ref ns = RUN_PYTHON(R"( 66 | def gen(): 67 | yield 1 68 | yield 2 69 | raise ValueError('die, you evil C++ program') 70 | 71 | it = gen() 72 | )"); 73 | ASSERT_TRUE(ns); 74 | 75 | py::borrowed_ref it = PyDict_GetItemString(ns.get(), "it"); 76 | ASSERT_TRUE(it); 77 | 78 | std::vector expected = {1, 2}; 79 | std::vector actual; 80 | EXPECT_THROW( 81 | { 82 | for (auto value : py::range(it)) { 83 | actual.emplace_back(py::from_object(value)); 84 | } 85 | }, 86 | py::exception); 87 | expect_pyerr_type_and_message(PyExc_ValueError, "die, you evil C++ program"); 88 | PyErr_Clear(); 89 | 90 | EXPECT_EQ(actual, expected); 91 | } 92 | } // namespace test_dict_range 93 | -------------------------------------------------------------------------------- /tests/test_scope_guard.cc: -------------------------------------------------------------------------------- 1 | #include "gtest/gtest.h" 2 | 3 | #include "libpy/scope_guard.h" 4 | 5 | namespace test_scope_guard { 6 | TEST(scope_guard, scope_exit) { 7 | bool fired = false; 8 | { 9 | py::util::scope_guard guard([&] { fired = true; }); 10 | EXPECT_FALSE(fired); 11 | } 12 | 13 | EXPECT_TRUE(fired); 14 | } 15 | 16 | TEST(scope_guard, throw_) { 17 | bool fired = false; 18 | auto f = [&] { 19 | py::util::scope_guard guard([&] { fired = true; }); 20 | EXPECT_FALSE(fired); 21 | throw std::runtime_error("boom"); 22 | }; 23 | EXPECT_FALSE(fired); 24 | EXPECT_THROW(f(), std::runtime_error); 25 | EXPECT_TRUE(fired); 26 | } 27 | 28 | TEST(scope_guard, dismiss) { 29 | bool fired = false; 30 | { 31 | py::util::scope_guard guard([&] { fired = true; }); 32 | EXPECT_FALSE(fired); 33 | guard.dismiss(); 34 | }; 35 | EXPECT_FALSE(fired); 36 | } 37 | } // namespace test_scope_guard 38 | -------------------------------------------------------------------------------- /tests/test_scoped_ref.cc: -------------------------------------------------------------------------------- 1 | #include "gtest/gtest.h" 2 | 3 | #include "libpy/call_function.h" 4 | #include "libpy/detail/python.h" 5 | #include "libpy/owned_ref.h" 6 | #include "test_utils.h" 7 | 8 | namespace test_owned_ref { 9 | class owned_ref : public with_python_interpreter {}; 10 | 11 | TEST_F(owned_ref, basic_lifetime) { 12 | PyObject* raw = Py_None; 13 | auto starting_ref_count = Py_REFCNT(raw); 14 | 15 | { 16 | Py_INCREF(raw); 17 | 18 | // wrapping an object in a scoped ref claims the reference, it should not incref 19 | // again 20 | py::owned_ref sr(raw); 21 | 22 | EXPECT_EQ(Py_REFCNT(raw), starting_ref_count + 1); 23 | } 24 | 25 | EXPECT_EQ(Py_REFCNT(raw), starting_ref_count); 26 | } 27 | 28 | TEST_F(owned_ref, copy_construct) { 29 | PyObject* raw = Py_None; 30 | auto starting_ref_count = Py_REFCNT(raw); 31 | 32 | { 33 | Py_INCREF(raw); 34 | py::owned_ref sr(raw); 35 | 36 | EXPECT_EQ(Py_REFCNT(raw), starting_ref_count + 1); 37 | 38 | EXPECT_EQ(sr.get(), raw); 39 | 40 | { 41 | // copy construct 42 | py::owned_ref<> copy(sr); 43 | 44 | // copy should take new ownership of the underlying object 45 | EXPECT_EQ(Py_REFCNT(raw), starting_ref_count + 2); 46 | 47 | EXPECT_EQ(copy, sr); 48 | EXPECT_EQ(copy.get(), sr.get()); 49 | } 50 | 51 | EXPECT_EQ(Py_REFCNT(raw), starting_ref_count + 1); 52 | } 53 | 54 | EXPECT_EQ(Py_REFCNT(raw), starting_ref_count); 55 | } 56 | 57 | TEST_F(owned_ref, self_assign) { 58 | py::owned_ref ob = nullptr; 59 | // we need to hold onto the ref when the namespace goes out of scope so that 60 | // the callback will still fire 61 | py::owned_ref ref = nullptr; 62 | py::owned_ref destructions = nullptr; 63 | { 64 | py::owned_ref ns = RUN_PYTHON(R"( 65 | import weakref 66 | 67 | class EmptyObject(object): 68 | pass 69 | 70 | def setup(): 71 | ob = EmptyObject() 72 | 73 | # use a list so we can see changes to this object without a cell 74 | destructions = [] 75 | 76 | def callback(wr): 77 | destructions.append(True) 78 | 79 | return ob, weakref.ref(ob, callback), destructions 80 | )"); 81 | ASSERT_TRUE(ns); 82 | ASSERT_TRUE(PyDict_CheckExact(ns.get())); 83 | 84 | PyObject* setup = PyDict_GetItemString(ns.get(), "setup"); 85 | ASSERT_TRUE(setup); 86 | 87 | auto res = py::call_function(setup); 88 | ASSERT_TRUE(res); 89 | ASSERT_TRUE(PyTuple_CheckExact(res.get())); 90 | ASSERT_EQ(PyTuple_GET_SIZE(res.get()), 3); 91 | 92 | ob = py::owned_ref(PyTuple_GET_ITEM(res.get(), 0)); 93 | Py_INCREF(ob); 94 | 95 | ref = py::owned_ref(PyTuple_GET_ITEM(res.get(), 1)); 96 | Py_INCREF(ref); 97 | 98 | destructions = py::owned_ref(PyTuple_GET_ITEM(res.get(), 2)); 99 | Py_INCREF(destructions); 100 | ASSERT_TRUE(PyList_CheckExact(destructions.get())); 101 | } 102 | 103 | auto starting_ref_count = Py_REFCNT(ob); 104 | EXPECT_EQ(starting_ref_count, 1); 105 | 106 | ob = ob; 107 | auto ending_ref_count = Py_REFCNT(ob); 108 | EXPECT_EQ(ending_ref_count, starting_ref_count); 109 | EXPECT_FALSE(PyList_GET_SIZE(destructions.get())); 110 | 111 | // explicitly kill ob now 112 | PyObject* escaped = std::move(ob).escape(); 113 | Py_DECREF(escaped); 114 | 115 | // make sure the callback fired 116 | EXPECT_EQ(PyList_GET_SIZE(destructions.get()), 1); 117 | } 118 | 119 | TEST_F(owned_ref, assign_same_underlying_pointer) { 120 | PyObject* raw = Py_None; 121 | auto start_ref_count = Py_REFCNT(raw); 122 | 123 | { 124 | Py_INCREF(raw); 125 | py::owned_ref a(raw); 126 | EXPECT_EQ(Py_REFCNT(raw), start_ref_count + 1); 127 | 128 | Py_INCREF(raw); 129 | py::owned_ref b(raw); 130 | EXPECT_EQ(Py_REFCNT(raw), start_ref_count + 2); 131 | 132 | a = b; 133 | 134 | EXPECT_EQ(Py_REFCNT(raw), start_ref_count + 2); 135 | } 136 | 137 | EXPECT_EQ(Py_REFCNT(raw), start_ref_count); 138 | } 139 | 140 | TEST_F(owned_ref, move_construct) { 141 | PyObject* raw = Py_None; 142 | auto starting_ref_count = Py_REFCNT(raw); 143 | 144 | { 145 | Py_INCREF(raw); 146 | py::owned_ref sr(raw); 147 | EXPECT_EQ(Py_REFCNT(raw), starting_ref_count + 1); 148 | 149 | { 150 | // move construct 151 | py::owned_ref moved(std::move(sr)); 152 | 153 | EXPECT_EQ(moved.get(), raw); 154 | 155 | // movement doesn't alter the refcount of the underlying 156 | EXPECT_EQ(Py_REFCNT(raw), starting_ref_count + 1); 157 | 158 | // movement resets the moved-from scoped ref 159 | EXPECT_EQ(sr.get(), nullptr); 160 | } 161 | 162 | EXPECT_EQ(Py_REFCNT(raw), starting_ref_count); 163 | } 164 | 165 | EXPECT_EQ(Py_REFCNT(raw), starting_ref_count); 166 | } 167 | 168 | TEST_F(owned_ref, move_assign) { 169 | PyObject* raw_rhs = Py_None; 170 | auto rhs_starting_ref_count = Py_REFCNT(raw_rhs); 171 | 172 | PyObject* raw_lhs = Py_Ellipsis; 173 | auto lhs_starting_ref_count = Py_REFCNT(raw_lhs); 174 | 175 | { 176 | Py_INCREF(raw_rhs); 177 | py::owned_ref rhs(raw_rhs); 178 | EXPECT_EQ(Py_REFCNT(raw_rhs), rhs_starting_ref_count + 1); 179 | 180 | { 181 | Py_INCREF(raw_lhs); 182 | py::owned_ref lhs(raw_lhs); 183 | EXPECT_EQ(Py_REFCNT(raw_lhs), lhs_starting_ref_count + 1); 184 | 185 | lhs = std::move(rhs); 186 | 187 | EXPECT_EQ(lhs.get(), raw_rhs); 188 | 189 | // movement doesn't alter the refcount of either underlying object 190 | EXPECT_EQ(Py_REFCNT(raw_lhs), lhs_starting_ref_count + 1); 191 | EXPECT_EQ(Py_REFCNT(raw_rhs), rhs_starting_ref_count + 1); 192 | 193 | // move assign swaps the values to ensure the old value is cleaned up 194 | EXPECT_EQ(rhs.get(), raw_lhs); 195 | } 196 | 197 | // rhs was cleaned up in the previous scope 198 | EXPECT_EQ(Py_REFCNT(raw_rhs), rhs_starting_ref_count); 199 | 200 | // lhs has been extended to the lifetime of this outer scope 201 | EXPECT_EQ(Py_REFCNT(raw_lhs), lhs_starting_ref_count + 1); 202 | } 203 | 204 | EXPECT_EQ(Py_REFCNT(raw_rhs), rhs_starting_ref_count); 205 | EXPECT_EQ(Py_REFCNT(raw_lhs), lhs_starting_ref_count); 206 | } 207 | 208 | TEST_F(owned_ref, escape) { 209 | PyObject* raw = Py_None; 210 | auto starting_ref_count = Py_REFCNT(raw); 211 | 212 | PyObject* escape_into = nullptr; 213 | 214 | { 215 | Py_INCREF(raw); 216 | py::owned_ref sr(raw); 217 | EXPECT_EQ(Py_REFCNT(raw), starting_ref_count + 1); 218 | 219 | escape_into = std::move(sr).escape(); 220 | 221 | // escaping from a scoped ref should reset the pointer 222 | EXPECT_EQ(sr.get(), nullptr); 223 | } 224 | 225 | EXPECT_EQ(escape_into, raw); 226 | 227 | // `sr` was moved from, it shouldn't decref the object 228 | EXPECT_EQ(Py_REFCNT(raw), starting_ref_count + 1); 229 | 230 | // keep the test refcount neutral 231 | Py_DECREF(raw); 232 | } 233 | 234 | TEST_F(owned_ref, operator_bool) { 235 | PyObject* raw = Py_None; 236 | 237 | Py_INCREF(raw); 238 | py::owned_ref truthy(raw); 239 | EXPECT_TRUE(truthy); 240 | 241 | py::owned_ref falsy(nullptr); 242 | EXPECT_FALSE(falsy); 243 | } 244 | } // namespace test_owned_ref 245 | -------------------------------------------------------------------------------- /tests/test_singletons.cc: -------------------------------------------------------------------------------- 1 | #include "gtest/gtest.h" 2 | 3 | #include "libpy/singletons.h" 4 | 5 | namespace test_singletons { 6 | TEST(none, is_none) { 7 | EXPECT_EQ(py::none, py::none); 8 | EXPECT_EQ(py::none.get(), Py_None); 9 | } 10 | 11 | TEST(none, refcnt) { 12 | Py_ssize_t start = Py_REFCNT(Py_None); 13 | { 14 | py::owned_ref<> none = py::none; 15 | EXPECT_EQ(Py_REFCNT(Py_None), start + 1); 16 | } 17 | EXPECT_EQ(Py_REFCNT(Py_None), start); 18 | } 19 | 20 | TEST(ellipsis, is_ellipsis) { 21 | EXPECT_EQ(py::ellipsis, py::ellipsis); 22 | EXPECT_EQ(py::ellipsis.get(), Py_Ellipsis); 23 | } 24 | 25 | TEST(ellipsis, refcnt) { 26 | Py_ssize_t start = Py_REFCNT(Py_Ellipsis); 27 | { 28 | py::owned_ref<> ellipsis = py::ellipsis; 29 | EXPECT_EQ(Py_REFCNT(Py_Ellipsis), start + 1); 30 | } 31 | EXPECT_EQ(Py_REFCNT(Py_Ellipsis), start); 32 | } 33 | 34 | TEST(not_implemented, is_not_implemented) { 35 | EXPECT_EQ(py::not_implemented, py::not_implemented); 36 | EXPECT_EQ(py::not_implemented.get(), Py_NotImplemented); 37 | } 38 | 39 | TEST(not_implemented, refcnt) { 40 | Py_ssize_t start = Py_REFCNT(Py_NotImplemented); 41 | { 42 | py::owned_ref<> not_implemented = py::not_implemented; 43 | EXPECT_EQ(Py_REFCNT(Py_NotImplemented), start + 1); 44 | } 45 | EXPECT_EQ(Py_REFCNT(Py_NotImplemented), start); 46 | } 47 | } // namespace test_singletons 48 | -------------------------------------------------------------------------------- /tests/test_str_convert.cc: -------------------------------------------------------------------------------- 1 | #include "gtest/gtest.h" 2 | 3 | #include "libpy/str_convert.h" 4 | #include "test_utils.h" 5 | 6 | namespace test_str_convert { 7 | 8 | using namespace py::cs::literals; 9 | 10 | class to_stringlike : public with_python_interpreter {}; 11 | 12 | TEST_F(to_stringlike, bytes) { 13 | auto s = "foobar"_cs; 14 | const char* expected = "foobar"; 15 | 16 | py::owned_ref<> s_py = py::to_stringlike(s, py::str_type::bytes); 17 | ASSERT_TRUE(s_py); 18 | 19 | ASSERT_TRUE(PyBytes_CheckExact(s_py.get())); 20 | EXPECT_STREQ(PyBytes_AS_STRING(s_py.get()), expected); 21 | } 22 | 23 | TEST_F(to_stringlike, str) { 24 | auto s = "foobar"_cs; 25 | const char* expected = "foobar"; 26 | 27 | py::owned_ref<> s_py = py::to_stringlike(s, py::str_type::str); 28 | 29 | ASSERT_TRUE(PyUnicode_CheckExact(s_py.get())); 30 | py::owned_ref<> decoded(PyUnicode_AsEncodedString(s_py.get(), "utf-8", "strict")); 31 | ASSERT_TRUE(decoded); 32 | EXPECT_STREQ(PyBytes_AS_STRING(decoded.get()), expected); 33 | } 34 | 35 | } // namespace test_str_convert 36 | -------------------------------------------------------------------------------- /tests/test_to_object.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "gtest/gtest.h" 6 | 7 | #include "libpy/any.h" 8 | #include "libpy/char_sequence.h" 9 | #include "libpy/itertools.h" 10 | #include "libpy/meta.h" 11 | #include "libpy/numpy_utils.h" 12 | #include "libpy/object_map_key.h" 13 | #include "libpy/str_convert.h" 14 | #include "libpy/to_object.h" 15 | #include "test_utils.h" 16 | 17 | namespace test_to_object { 18 | using namespace std::literals; 19 | using namespace py::cs::literals; 20 | 21 | class to_object : public with_python_interpreter {}; 22 | 23 | TEST_F(to_object, map_to_object) { 24 | auto map = std::unordered_map(); 25 | py_test::test_map_to_object_impl(map); 26 | } 27 | 28 | TEST_F(to_object, vector_to_object) { 29 | auto to_vec = [](const auto& arr) { return std::vector(arr.begin(), arr.end()); }; 30 | auto vectors = std::make_tuple(to_vec(py_test::examples()), 31 | to_vec(py_test::examples()), 32 | to_vec(py_test::examples>())); 33 | // Call test_sequence_to_object_impl on each entry in `vectors`. 34 | std::apply([&](auto... vec) { (py_test::test_sequence_to_object_impl(vec), ...); }, vectors); 35 | } 36 | 37 | TEST_F(to_object, array_to_object) { 38 | auto arrays = std::make_tuple(py_test::examples(), 39 | py_test::examples(), 40 | py_test::examples>()); 41 | // Call test_sequence_to_object_impl on each entry in `arrays`. 42 | std::apply([&](auto... arr) { (py_test::test_sequence_to_object_impl(arr), ...); }, arrays); 43 | } 44 | 45 | template 46 | auto test_any_ref_to_object(T value) { 47 | R ref(std::addressof(value), py::any_vtable::make()); 48 | 49 | auto ref_to_object = py::to_object(ref); 50 | ASSERT_TRUE(ref_to_object); 51 | auto value_to_object = py::to_object(value); 52 | ASSERT_TRUE(ref_to_object); 53 | 54 | int eq = PyObject_RichCompareBool(ref_to_object.get(), value_to_object.get(), Py_EQ); 55 | ASSERT_EQ(eq, 1); 56 | } 57 | 58 | TEST_F(to_object, any_ref) { 59 | test_any_ref_to_object(1); 60 | test_any_ref_to_object(1.5); 61 | test_any_ref_to_object("test"s); 62 | 63 | test_any_ref_to_object(1); 64 | test_any_ref_to_object(1.5); 65 | test_any_ref_to_object("test"s); 66 | } 67 | 68 | TEST_F(to_object, any_ref_of_object_refcnt) { 69 | PyObject* ob = Py_None; 70 | Py_ssize_t baseline_refcnt = Py_REFCNT(ob); 71 | { 72 | Py_INCREF(ob); 73 | py::owned_ref sr(ob); 74 | ASSERT_EQ(Py_REFCNT(ob), baseline_refcnt + 1); 75 | 76 | py::any_ref ref(&sr, py::any_vtable::make()); 77 | // `any_ref` is *non-owning* so the reference count doesn't change 78 | ASSERT_EQ(Py_REFCNT(ob), baseline_refcnt + 1); 79 | { 80 | auto to_object_result = py::to_object(ref); 81 | ASSERT_TRUE(to_object_result); 82 | // `to_object` returns a new owning reference 83 | ASSERT_EQ(Py_REFCNT(ob), baseline_refcnt + 2); 84 | 85 | EXPECT_EQ(to_object_result.get(), ob); 86 | } 87 | 88 | // `to_object_result` goes out of scope, releasing its reference 89 | ASSERT_EQ(Py_REFCNT(ob), baseline_refcnt + 1); 90 | } 91 | 92 | // `sr` goes out of scope, releasing its reference 93 | ASSERT_EQ(Py_REFCNT(ob), baseline_refcnt); 94 | } 95 | 96 | TEST_F(to_object, any_ref_non_convertible_object) { 97 | // The most simple type which can be put into an `any_ref`. There is no `to_object` 98 | // dispatch, so we expect `to_object(S{})` would throw a runtime exception. 99 | struct S { 100 | bool operator==(const S&) const { 101 | return true; 102 | } 103 | 104 | bool operator!=(const S&) const { 105 | return false; 106 | } 107 | }; 108 | 109 | S value; 110 | py::any_ref ref(&value, py::any_vtable::make()); 111 | 112 | EXPECT_THROW(py::to_object(ref), py::exception); 113 | PyErr_Clear(); 114 | } 115 | 116 | TEST_F(to_object, object_map_key) { 117 | py::object_map_key key{py::to_object(5)}; 118 | ASSERT_TRUE(key); 119 | Py_ssize_t starting_ref_count = Py_REFCNT(key.get()); 120 | 121 | py::owned_ref as_ob = py::to_object(key); 122 | // should be the same pointer 123 | EXPECT_EQ(key.get(), as_ob.get()); 124 | 125 | // now owned by both as_ob and key 126 | EXPECT_EQ(Py_REFCNT(key.get()), starting_ref_count + 1); 127 | } 128 | 129 | TEST_F(to_object, owned_ref_nonstandard) { 130 | py::owned_ref t = py::new_dtype(); 131 | 132 | py::owned_ref ob = py::to_object(t); 133 | ASSERT_TRUE(ob); 134 | EXPECT_EQ(static_cast(ob), static_cast(t)); 135 | } 136 | 137 | TEST_F(to_object, filesystem_path) { 138 | std::filesystem::path test_path = "/tmp/"; 139 | py::owned_ref ob = py::to_object(test_path); 140 | ASSERT_TRUE(ob); 141 | 142 | py::owned_ref ns = RUN_PYTHON(R"( 143 | from pathlib import Path 144 | py_path = Path("/tmp/") 145 | )"); 146 | ASSERT_TRUE(ns); 147 | 148 | py::borrowed_ref py_path_ob = PyDict_GetItemString(ns.get(), "py_path"); 149 | ASSERT_TRUE(py_path_ob); 150 | 151 | int eq = PyObject_RichCompareBool(ob.get(), py_path_ob.get(), Py_EQ); 152 | EXPECT_EQ(eq, 1); 153 | } 154 | 155 | } // namespace test_to_object 156 | -------------------------------------------------------------------------------- /tests/test_util.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "gtest/gtest.h" 4 | 5 | #include "libpy/util.h" 6 | 7 | namespace test_util { 8 | TEST(searchsorted_left, all) { 9 | std::vector vec{1, 2, 3, 5, 6}; 10 | 11 | // needle not in container. 12 | EXPECT_EQ(py::util::searchsorted_l(vec, 4), 3); 13 | 14 | // needle collides with value in container. 15 | EXPECT_EQ(py::util::searchsorted_l(vec, 3), 2); 16 | 17 | // needle greater than largest value in container. 18 | EXPECT_EQ(py::util::searchsorted_l(vec, 42), static_cast(vec.size())); 19 | 20 | // needle less than smallest value in container. 21 | EXPECT_EQ(py::util::searchsorted_l(vec, -1), 0); 22 | 23 | // needle equal to largest value in container. 24 | EXPECT_EQ(py::util::searchsorted_l(vec, 6), 4); 25 | 26 | // needle equal to smallest value in container. 27 | EXPECT_EQ(py::util::searchsorted_l(vec, 1), 0); 28 | } 29 | 30 | TEST(searchsorted_right, all) { 31 | std::vector vec{1, 2, 3, 5, 6}; 32 | 33 | // needle not in container. 34 | EXPECT_EQ(py::util::searchsorted_r(vec, 4), 3); 35 | 36 | // needle collides with value in container. 37 | EXPECT_EQ(py::util::searchsorted_r(vec, 3), 3); 38 | 39 | // needle greater than largest value in container. 40 | EXPECT_EQ(py::util::searchsorted_r(vec, 42), static_cast(vec.size())); 41 | 42 | // needle less than smallest value in container. 43 | EXPECT_EQ(py::util::searchsorted_r(vec, -1), 0); 44 | 45 | // needle equal to largest value in container. 46 | EXPECT_EQ(py::util::searchsorted_r(vec, 6), static_cast(vec.size())); 47 | 48 | // needle equal to smallest value in container. 49 | EXPECT_EQ(py::util::searchsorted_r(vec, 1), 1); 50 | } 51 | 52 | using t = std::tuple; 53 | 54 | class apply_to_groups_test_cases : public testing::Test { 55 | protected: 56 | // vector containing tuples of input sequences and expected outputs. 57 | std::vector, std::vector>> test_cases; 58 | 59 | void SetUp() override { 60 | 61 | // case 1: empty sequence 62 | test_cases.emplace_back(std::vector(), std::vector()); 63 | 64 | // case 2: sequence of length 1 65 | test_cases.emplace_back(std::vector({1}), 66 | std::vector({std::make_tuple(1, 0, 1)})); 67 | 68 | // case 3: sequence containing single unique value 69 | test_cases.emplace_back(std::vector({1, 1}), 70 | std::vector({std::make_tuple(1, 0, 2)})); 71 | 72 | // case 4: sequence with multiple unique values 1 73 | test_cases.emplace_back(std::vector({1, 1, 2}), 74 | std::vector({std::make_tuple(1, 0, 2), 75 | std::make_tuple(2, 2, 3)})); 76 | 77 | // case 5: sequence with multiple unique values 2 78 | test_cases.emplace_back(std::vector({1, 1, 2, 2}), 79 | std::vector({std::make_tuple(1, 0, 2), 80 | std::make_tuple(2, 2, 4)})); 81 | 82 | // case 6: sequence with multiple unique values 3 83 | test_cases.emplace_back(std::vector({1, 1, 2, 2, 2, 3, 4, 4}), 84 | std::vector({std::make_tuple(1, 0, 2), 85 | std::make_tuple(2, 2, 5), 86 | std::make_tuple(3, 5, 6), 87 | std::make_tuple(4, 6, 8)})); 88 | 89 | // case 7: non-sorted sequence with multiple unique values 90 | test_cases.emplace_back(std::vector({1, 1, 3, 4, 4, 7, 2, 2, 1}), 91 | std::vector({std::make_tuple(1, 0, 2), 92 | std::make_tuple(3, 2, 3), 93 | std::make_tuple(4, 3, 5), 94 | std::make_tuple(7, 5, 6), 95 | std::make_tuple(2, 6, 8), 96 | std::make_tuple(1, 8, 9)})); 97 | } 98 | }; 99 | 100 | TEST_F(apply_to_groups_test_cases, using_it_begin_end) { 101 | std::vector test_vec; 102 | 103 | auto f = [&](const auto& val, std::ptrdiff_t start_ix, std::ptrdiff_t stop_ix) { 104 | test_vec.emplace_back(val, start_ix, stop_ix); 105 | }; 106 | 107 | for (auto [input, expected_out] : test_cases) { 108 | py::util::apply_to_groups(input.begin(), input.end(), f); 109 | EXPECT_EQ(test_vec, expected_out); 110 | test_vec.clear(); 111 | } 112 | } 113 | 114 | TEST_F(apply_to_groups_test_cases, using_it_range) { 115 | std::vector test_vec; 116 | 117 | auto f = [&](const auto& val, std::ptrdiff_t start_ix, std::ptrdiff_t stop_ix) { 118 | test_vec.emplace_back(val, start_ix, stop_ix); 119 | }; 120 | 121 | for (auto [input, expected_out] : test_cases) { 122 | py::util::apply_to_groups(input, f); 123 | EXPECT_EQ(test_vec, expected_out); 124 | test_vec.clear(); 125 | } 126 | } 127 | } // namespace test_util 128 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist=py{35,36}-{sanitize,release} 3 | skip_missing_interpreters=True 4 | skipsdist=True 5 | 6 | [testenv] 7 | whitelist_externals=/usr/bin/make 8 | passenv=GTEST_OUTPUT CC CXX 9 | deps= 10 | numpy==1.11.3 11 | pytest==4.4.1 12 | commands= 13 | py{35,36}-sanitize: make -j2 test SANITIZE_ADDRESS=1 SANITIZE_UNDEFINED=1 14 | py{35,36}-release: make -j2 test GTEST_OUTPUT={env:GTEST_OUTPUT:} 15 | -------------------------------------------------------------------------------- /version: -------------------------------------------------------------------------------- 1 | 0.2.5 2 | --------------------------------------------------------------------------------