Python package using Pybind11

Ref

Installation

sudo apt install -y libffi-dev python3 python3-pip python3-dev \
&& python3 -m pip install -U --user pip setuptools wheel twine keyrings.alt pybind11

Package directory structure

workspace
β”œβ”€β”€ LICENSE.txt
β”œβ”€β”€ MANIFEST.in
β”œβ”€β”€ README.md
β”œβ”€β”€ CHANGELOG
β”œβ”€β”€ setup.cfg
β”œβ”€β”€ setup.py
β”œβ”€β”€ c_src
β”‚ └── ...
└── py_src
└── package
β”œβ”€β”€ test
| β”œβ”€β”€ __init__.py
| └── ...
β”œβ”€β”€ subPackage
β”‚ β”œβ”€β”€ __init__.py
β”‚ └── module1.py
β”œβ”€β”€ __init__.py
└── __main__.py

C/C++

function

#include <pybind11/pybind11.h>
namespace py = pybind11;
int add(int i, int j) { return i + j; }
PYBIND11_MODULE(_<package>, m) {
// m.def( "add", &add );
m.def("add",
&add,
"A function which adds two numbers",
py::arg("i") = 1,
py::arg("j") = 2);
}

variable

#include <pybind11/pybind11.h>
namespace py = pybind11;
PYBIND11_MODULE(_<package>, m) {
// Built-in types and general objects (more on that later)
// are automatically converted.
m.attr("var1") = 10;
// Can be explicitly converted using the function `py::cast`.
py::object var2 = py::cast("It is string");
m.attr("var2") = var2;
}

class

#include <pybind11/pybind11.h>
#include <string>
namespace py = pybind11;
class TestClass {
public:
TestClass(int a);
~TestClass();
int add(int a, int b);
int sub(int a, int b);
float sub(float a, float b);
std::string get_name(void);
void set_name(std::string name);
private:
std::string name;
};
PYBIND11_MODULE(_<package>, m) {
py::class_<TestClass>(m, "TestClass")
.def(py::init<int>())
.def("add", &TestClass::add)
.def("sub", (int (TestClass::*)(int, int)) & TestClass::sub)
.def("sub", (float (TestClass::*)(float, float)) & TestClass::sub)
// .def_readwrite( "name", &TestClass::name ) // public
.def_property(
"name", &TestClass::get_name, &TestClass::set_name); // private
}

Python

__init__.py

from <package>._<package> import *
...

Build

setup.py

Ref: https://github.com/pybind/python_example

from setuptools import setup, Extension
from setuptools.command.build_ext import build_ext
import sys
import setuptools
from os import path
BASE_DIR = path.dirname(path.abspath(__file__))
CHANGELOG_PATH = path.join(BASE_DIR, "CHANGELOG")
class get_pybind_include(object):
"""Helper class to determine the pybind11 include path
The purpose of this class is to postpone importing pybind11
until it is actually installed, so that the ``get_include()``
method can be invoked. """
def __init__(self, user=False):
self.user = user
def __str__(self):
import pybind11
return pybind11.get_include(self.user)
ext_modules = [
Extension(
"<package>._<package>",
sources=["c_src/gpio.cpp"],
include_dirs=[
# Path to pybind11 headers
get_pybind_include(),
get_pybind_include(user=True),
],
language="c++",
),
]
with open(CHANGELOG_PATH, "r") as f:
version = f.readline()
version = version.split()
version = version[1][1:-1]
def has_flag(compiler, flagname):
"""Return a boolean indicating whether a flag name is supported on
the specified compiler.
"""
import tempfile
with tempfile.NamedTemporaryFile("w", suffix=".cpp") as f:
f.write("int main (int argc, char **argv) { return 0; }")
try:
compiler.compile([f.name], extra_postargs=[flagname])
except setuptools.distutils.errors.CompileError:
return False
return True
def cpp_flag(compiler):
"""Return the -std=c++[11/14/17] compiler flag.
The newer version is prefered over c++11 (when it is available).
"""
flags = ["-std=c++17", "-std=c++14", "-std=c++11"]
for flag in flags:
if has_flag(compiler, flag):
return flag
raise RuntimeError(
"Unsupported compiler -- at least C++11 support " "is needed!"
)
class BuildExt(build_ext):
"""A custom build extension for adding compiler-specific options."""
c_opts = {
"msvc": ["/EHsc"],
"unix": [],
}
l_opts = {
"msvc": [],
"unix": [],
}
if sys.platform == "darwin":
darwin_opts = ["-stdlib=libc++", "-mmacosx-version-min=10.7"]
c_opts["unix"] += darwin_opts
l_opts["unix"] += darwin_opts
def build_extensions(self):
ct = self.compiler.compiler_type
opts = self.c_opts.get(ct, [])
link_opts = self.l_opts.get(ct, [])
if ct == "unix":
opts.append('-DVERSION_INFO="%s"' % self.distribution.get_version())
opts.append(cpp_flag(self.compiler))
if has_flag(self.compiler, "-fvisibility=hidden"):
opts.append("-fvisibility=hidden")
elif ct == "msvc":
opts.append(
'/DVERSION_INFO=\\"%s\\"' % self.distribution.get_version()
)
for ext in self.extensions:
ext.extra_compile_args = opts
ext.extra_link_args = link_opts
build_ext.build_extensions(self)
setup(
version=version, ext_modules=ext_modules, cmdclass={"build_ext": BuildExt},
)

setup.cfg

[metadata]
name =
url = https://github.com/loliot
project_urls =
Source =
author = Hyeonki Hong
author_email = hhk7734@gmail.com
description =
long-description = file: README.md, CHANGELOG
long_description_content_type = text/markdown
keywords =
license = MIT
classifiers =
Programming Language :: Python :: 3
License :: OSI Approved :: MIT License
Operating System :: POSIX :: Linux
Intended Audience :: Developers
Topic :: Software Development
Topic :: System :: Hardware
[options]
package_dir =
= py_src
packages = find:
zip_safe = False
setup_requires =
pybind11>=2.4
install_requires =
pybind11>=2.4
[options.packages.find]
where = py_src

Ref: https://pypi.org/classifiers/

MANIFEST.in

include LICENSE.txt
include README.md
include CHANGELOG
include c_src/*

Using setuptools

The preferred approach to building an extension module for python is to compile it with setuptools, which comes with all recent versions of python.

python3 setup.py install --user
python3 -m pip uninstall <package>

pip 등둝

dist

python3 setup.py sdist

Test 등둝/μ„€μΉ˜

python3 -m twine upload --repository-url https://test.pypi.org/legacy/ dist/*
python3 -m pip install --index-url https://test.pypi.org/simple/ --verbose --user <package>

정식 등둝/μ„€μΉ˜

python3 -m twine upload dist/*
python3 -m pip install <package>
Last updated on