Skip to content

Commit

Permalink
Merge pull request #2 from nqminds/feat/add-irimager
Browse files Browse the repository at this point in the history
Add initial skeleton `nqm.irimager` C extension module
  • Loading branch information
aloisklink authored Jul 21, 2023
2 parents 15b29fd + 3a3d63e commit 2d9bd7e
Show file tree
Hide file tree
Showing 14 changed files with 241 additions and 8 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,9 @@ jobs:
# see https://github.com/pre-commit/pre-commit-hooks/issues/265
SKIP: "no-commit-to-branch"
run: poetry run pre-commit run --show-diff-on-failure --color=always
- name: Check whether nqm.irimager stubs match implementation
env:
MYPYPATH: src
run: poetry run stubtest nqm.irimager
- name: Test with pytest
run: poetry run pytest
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ ipython_config.py
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock

# The setup.py file is auto-generated by Poetry if you have
# `tool.poetry.build.generate-setup-file = true` set in your pyproject.toml file
setup.py

# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
Expand Down
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ repos:
[
"-rn", # Only display messages
"-sn", # Don't display the score
"--extension-pkg-whitelist=nqm.irimager", # import our custom C extension
]
- id: mypy
name: mypy
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,21 @@ Tests are written using [`pytest`](https://docs.pytest.org/en/7.2.x/), and can b
poetry run pytest
```

#### Mypy stubtest

You can use
[Mypy's `stubtest` tool](https://mypy.readthedocs.io/en/stable/stubtest.html)
to automatically check whether the types in a stub file match the
implementation.

For most Python code, we put the type information directly in the
implementation, so we only need this for C/C++ Python extensions, like the
`nqm.irimager` module.

```bash
MYPYPATH=src poetry run stubtest nqm.irimager
```

### Documentation

[Sphinx](https://www.sphinx-doc.org/en/master/index.html) is used to generate
Expand Down
23 changes: 23 additions & 0 deletions build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Poetry build script that builds the nqm.irimager Python C++ extension
"""

from setuptools.extension import Extension

nqm_irimager_extension = Extension(
"nqm.irimager",
sources=["src/nqm/irimager/irimager.cpp"],
define_macros=[("PY_SSIZE_T_CLEAN", None)],
)


def build(setup_kwargs):
"""
This is a callback for poetry used to hook in our extensions.
"""

setup_kwargs.update(
{
# declare the extension so that setuptools will compile it
"ext_modules": [nqm_irimager_extension],
}
)
14 changes: 13 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ license = "UNLICENSED"
readme = "README.md"
packages = [{include = "nqm", from = "src"}]


[tool.poetry.build]
# build a custom C++ extension using build.py
script = "build.py"
generate-setup-file = true

[tool.poetry.dependencies]
python = "^3.8"

Expand All @@ -21,9 +27,10 @@ pytest = "^7.2.1"
sphinx = "^5.3.0"
myst-parser = "^0.18.1"
isort = "^5.12.0"
types-setuptools = ">=45.2.0"

[build-system]
requires = ["poetry-core"]
requires = ["poetry-core", "setuptools>=45.2.0"]
build-backend = "poetry.core.masonry.api"

[tool.pylint.main]
Expand Down
1 change: 1 addition & 0 deletions src/nqm/_irimager/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# This is a dummy package just to make Poetry happy
1 change: 0 additions & 1 deletion src/nqm/irimager/__init__.py

This file was deleted.

16 changes: 16 additions & 0 deletions src/nqm/irimager/__init__.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# This file was auto-generated and modified by hand from
# `poetry run stubgen -m nqm.irimager`

"""Optris PI and XI imager IR camera controller
We use the IRImagerDirect SDK
(see http://documentation.evocortex.com/libirimager2/html/index.html)
to control these cameras.
"""

class IRImager:
"""IRImager object - interfaces with a camera."""

def __init__(self, *args, **kwargs) -> None: ...
def test(self) -> int:
"""Return the number 42"""
146 changes: 146 additions & 0 deletions src/nqm/irimager/irimager.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// contains example code adapted from https://opensource.com/article/22/11/extend-c-python

#include <Python.h>
#include "structmember.h"

#include <iostream>

// A struct contains the definition of a module
PyModuleDef irimager = {
PyModuleDef_HEAD_INIT,
"irimager", // Module name
R"(Optris PI and XI imager IR camera controller
We use the IRImagerDirect SDK
(see http://documentation.evocortex.com/libirimager2/html/index.html)
to control these cameras.)", // docstring
-1, // Optional size of the module state memory
nullptr, // Optional module methods
nullptr, // Optional slot definitions
nullptr, // Optional traversal function
nullptr, // Optional clear function
nullptr // Optional module deallocation function
};

class IRImager {
public:
int test() {
return 42;
}
};

// Python object that stores an IRImager
typedef struct {
PyObject_HEAD
IRImager* m_myclass;
} IRImagerObject;

PyObject* IRImager_test(PyObject *self, PyObject *args){
assert(self);

IRImagerObject* _self = reinterpret_cast<IRImagerObject*>(self);
int val = _self->m_myclass->test();
return PyLong_FromLong(val);
}

static PyMethodDef IRImager_methods[] = {
{"test", (PyCFunction)IRImager_test, METH_NOARGS, PyDoc_STR("Return the number 42")},
{0, nullptr} /* Sentinel */
};

static PyMemberDef IRImager_members[] = {
// currently no members
{nullptr, 0, 0, 0, nullptr} /* Sentinel */
};

PyObject *IRImager_new(PyTypeObject *type, PyObject *args, PyObject *kwds){
std::cout << "IRImager_new() called!" << std::endl;

IRImagerObject *self = (IRImagerObject*) type->tp_alloc(type, 0);
if(self != nullptr){ // -> allocation successfull
// assign initial values
self->m_myclass = nullptr;
}
return (PyObject*) self;
}

int IRImager_init(PyObject *self, PyObject *args, PyObject *kwds){
IRImagerObject* m = (IRImagerObject*)self;
m->m_myclass = (IRImager*)PyObject_Malloc(sizeof(IRImager));

if(!m->m_myclass){
PyErr_SetString(PyExc_RuntimeError, "Memory allocation failed");
return -1;
}

try {
new (m->m_myclass) IRImager();
} catch (const std::exception& ex) {
PyObject_Free(m->m_myclass);
m->m_myclass = nullptr;
PyErr_SetString(PyExc_RuntimeError, ex.what());
return -1;
} catch(...) {
PyObject_Free(m->m_myclass);
m->m_myclass = nullptr;
PyErr_SetString(PyExc_RuntimeError, "Initialization failed");
return -1;
}

return 0;
}

void IRImager_dealloc(IRImagerObject *self){
std::cout << "IRImager_dealloc() called!" << std::endl;
PyTypeObject *tp = Py_TYPE(self);

IRImagerObject* m = reinterpret_cast<IRImagerObject*>(self);

if(m->m_myclass){
m->m_myclass->~IRImager();
PyObject_Free(m->m_myclass);
}

tp->tp_free(self);
Py_DECREF(tp);
};

PyDoc_STRVAR(IRImager_doc, R"(IRImager object - interfaces with a camera.)");

static PyType_Slot IRImager_slots[] = {
{Py_tp_new, (void*)IRImager_new},
{Py_tp_init, (void*)IRImager_init},
{Py_tp_dealloc, (void*)IRImager_dealloc},
{Py_tp_members, IRImager_members},
{Py_tp_methods, IRImager_methods},
{Py_tp_doc, (void*)IRImager_doc},
{0, nullptr} /* Sentinel */
};

static PyType_Spec spec_IrImager = {
"nqm.irimager.IRImager", // name
sizeof(IRImagerObject) + sizeof(IRImager), // basicsize
0, // itemsize
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, // flags
IRImager_slots // slots
};

PyMODINIT_FUNC
PyInit_irimager(void) {
PyObject* module = PyModule_Create(&irimager);

PyObject *irimager_class = PyType_FromSpec(&spec_IrImager);
if (irimager_class == NULL){
std::cerr << "Failed to make irimager_class!" << std::endl;
return nullptr;
}
Py_INCREF(irimager_class);

if(PyModule_AddObject(module, "IRImager", irimager_class) < 0){
std::cerr << "Failed to add IRImager!" << std::endl;
Py_DECREF(irimager_class);
Py_DECREF(module);
return nullptr;
}
return module;
}
1 change: 1 addition & 0 deletions src/nqm/irimager/py.typed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# This file is left blank and is needed for PEP 561 (typing info)
5 changes: 0 additions & 5 deletions tests/test_init.py

This file was deleted.

9 changes: 9 additions & 0 deletions tests/test_irimager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""Tests for nqm.irimager.IRImager"""
from nqm.irimager import IRImager


def test_irimager_test():
"""Tests nqm.irimager.IRImager"""
irimager = IRImager()

assert irimager.test() == 42

0 comments on commit 2d9bd7e

Please sign in to comment.