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>