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
├── debian
│   └── changelog
├── setup.cfg
├── setup.py
├── c_src
│   └── ...
├── py_src
│   └── package1
│       ├── package2
│       │   ├── __init__.py
│       │   └── module1.py
│       ├── __init__.py
│       └── __main__.py
└── test
    └── ...

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, "debian/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,
    install_requires=['pybind11>=2.4'],
    setup_requires=['pybind11>=2.4'],
    cmdclass={'build_ext': BuildExt},
    zip_safe=False,
)

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, debian/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:

[options.packages.find]
where = py_src

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

MANIFEST.in

include LICENSE.txt
include README.md
include debian/*
include c_src/*
include test/*

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 sdist
python3 setup.py install --user

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]