From 1db6aa1f6eadf828c02fb75e3b6609054acae013 Mon Sep 17 00:00:00 2001 From: Ugo Date: Fri, 30 Apr 2021 16:25:19 +0200 Subject: [PATCH] alpha version --- .gitignore | 7 + README.md | 40 ++ examples/.gitignore | 1 + examples/main.py | 132 +++++++ examples/pathtocmake/CMakeLists.txt | 21 + examples/pathtocmake/cmake/FindLIBIGL.cmake | 34 ++ examples/test.ipynb | 409 ++++++++++++++++++++ pybindmagic/__init__.py | 184 +++++++++ setup.py | 35 ++ 9 files changed, 863 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 examples/.gitignore create mode 100644 examples/main.py create mode 100644 examples/pathtocmake/CMakeLists.txt create mode 100644 examples/pathtocmake/cmake/FindLIBIGL.cmake create mode 100644 examples/test.ipynb create mode 100644 pybindmagic/__init__.py create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e262701 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.ipynb_checkpoints +.ccls-cache +build +pbm_modules +*.pyc +__pycache__ +pybind11 diff --git a/README.md b/README.md new file mode 100644 index 0000000..4ab4143 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# pybindmagic + +An IPython kernel magic library. After `import pybindmagic` you can use the cell magic `%%cpp` + +In this cell you can write cpp code. + +## Requirements + +* pybind11 +* git +* gcc +* cmake (optional) + +## TODOS + +* [ ] add options for cmake flags +* [ ] add other compilers +* [ ] Better exception handling + * [ ] Better debugging output + +## Options + +### defs + +You need to specify all functions or classes you want to expose to python. Either by `%%cpp -f funcname` or by pybind11 syntax at the end of the cell. There is a shortcut by inserting a `defs` in the end of the cell that will add the correct module name. For an example take a look at the example notebook. + +### Compiler + +Currently only gcc is supported. The default compiler command is `c++`. You can change the compiler command with: +`pybindmagic.compiler_cmd = "gcc"` + + +### CMake + +With `%%cpp -c path/to/folder/with/CMakeList.txt/in` you can specify cmake compilation. After evaluation pybind11 will be downloaded and moved to this folder, a build folder is generated and the cell is compiled in this folder. +Makesure that you add pybind11 to your cmake file. + +### rebuild + +With the `%cpp -rebuild` flag you can enforce a complete rebuild. diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 0000000..038750f --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1 @@ +libigl diff --git a/examples/main.py b/examples/main.py new file mode 100644 index 0000000..29b1693 --- /dev/null +++ b/examples/main.py @@ -0,0 +1,132 @@ +import pybindmagic +import os +os.environ["PATH"] = "/home/ugo/anaconda3/bin" + os.pathsep + os.environ["PATH"] + +#%% +%%cpp -f generateMesh +#include +std::tuple generateMesh() +{ + // Inline mesh of a cube + const Eigen::MatrixXd V = (Eigen::MatrixXd(8,3)<< + 0.0,0.0,0.0, + 0.0,0.0,1.0, + 0.0,1.0,0.0, + 0.0,1.0,1.0, + 1.0,0.0,0.0, + 1.0,0.0,1.0, + 1.0,1.0,0.0, + 1.0,1.0,1.0).finished(); + const Eigen::MatrixXi F = (Eigen::MatrixXi(12,3)<< + 1,7,5, + 1,3,7, + 1,4,3, + 1,2,4, + 3,8,7, + 3,4,8, + 5,7,8, + 5,8,6, + 1,5,6, + 1,6,2, + 2,6,8, + 2,8,4).finished().array()-1; + return std::make_tuple(V,F); +} +#%% + +V,F = generateMesh() +print("Vertices:\n",V) +print("Faces:\n",F) + +#%% +%%cpp +#include + + +std::tuple generateMesh() +{ + // Inline mesh of a cube + const Eigen::MatrixXd V = (Eigen::MatrixXd(8,3)<< + 0.0,0.0,0.0, + 0.0,0.0,1.0, + 0.0,1.0,0.0, + 0.0,1.0,1.0, + 1.0,0.0,0.0, + 1.0,0.0,1.0, + 1.0,1.0,0.0, + 1.0,1.0,1.0).finished(); + const Eigen::MatrixXi F = (Eigen::MatrixXi(12,3)<< + 1,7,5, + 1,3,7, + 1,4,3, + 1,2,4, + 3,8,7, + 3,4,8, + 5,7,8, + 5,8,6, + 1,5,6, + 1,6,2, + 2,6,8, + 2,8,4).finished().array()-1; + return std::make_tuple(V,F); +} + +void printMesh(Eigen::MatrixXd V,Eigen::MatrixXi F) +{ + py::print("Vertices:\n", V); + py::print("Faces:\n", F); +} + +defs + m.def("generateMesh",&generateMesh); + m.def("printMesh",&printMesh); +#%% + +printMesh(*generateMesh()) + +#%% +%%cpp -c pathtocmake -f viewMesh + +#include +#include + + +void viewMesh(Eigen::Matrix V, Eigen::Matrix F) +{ + // Plot the mesh + igl::opengl::glfw::Viewer viewer; + viewer.data().set_mesh(V, F); + viewer.data().set_face_based(true); + viewer.launch(); +} +#%% + +import numpy as np + +V = np.array([ + [0.0,0.0,0.0], + [0.0,0.0,1.0], + [0.0,1.0,0.0], + [0.0,1.0,1.0], + [1.0,0.0,0.0], + [1.0,0.0,1.0], + [1.0,1.0,0.0], + [1.0,1.0,1.0]]) + +F = np.array( + [ + [1,7,5], + [1,3,7], + [1,4,3], + [1,2,4], + [3,8,7], + [3,4,8], + [5,7,8], + [5,8,6], + [1,5,6], + [1,6,2], + [2,6,8], + [2,8,4] + ], dtype=np.int32)-1 + +viewMesh(V,F) diff --git a/examples/pathtocmake/CMakeLists.txt b/examples/pathtocmake/CMakeLists.txt new file mode 100644 index 0000000..3d038b8 --- /dev/null +++ b/examples/pathtocmake/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.1) + +project({name} LANGUAGES CXX) + +find_package(Python COMPONENTS Interpreter Development REQUIRED) +add_subdirectory(pybind11) + + +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake) + +# libigl +option(LIBIGL_WITH_OPENGL "Use OpenGL" ON) +option(LIBIGL_WITH_OPENGL_GLFW "Use GLFW" ON) + +find_package(LIBIGL REQUIRED QUIET) + +# Add your project files +file(GLOB SRCFILES *.cpp) +# add_executable(${PROJECT_NAME} ${SRCFILES}) +pybind11_add_module(${PROJECT_NAME} ${SRCFILES}) +target_link_libraries(${PROJECT_NAME} PRIVATE igl::core igl::opengl_glfw) diff --git a/examples/pathtocmake/cmake/FindLIBIGL.cmake b/examples/pathtocmake/cmake/FindLIBIGL.cmake new file mode 100644 index 0000000..1017008 --- /dev/null +++ b/examples/pathtocmake/cmake/FindLIBIGL.cmake @@ -0,0 +1,34 @@ +# - Try to find the LIBIGL library +# Once done this will define +# +# LIBIGL_FOUND - system has LIBIGL +# LIBIGL_INCLUDE_DIR - **the** LIBIGL include directory +if(LIBIGL_FOUND) + return() +endif() + +find_path(LIBIGL_INCLUDE_DIR igl/readOBJ.h + HINTS + ${LIBIGL_DIR} + ENV LIBIGL_DIR + PATHS + ${CMAKE_SOURCE_DIR}/../.. + ${CMAKE_SOURCE_DIR}/.. + ${CMAKE_SOURCE_DIR} + ${CMAKE_SOURCE_DIR}/libigl + ${CMAKE_SOURCE_DIR}/../libigl + ${CMAKE_SOURCE_DIR}/../../libigl + /usr + /usr/local + /usr/local/igl/libigl + PATH_SUFFIXES include +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(LIBIGL + "\nlibigl not found --- You can download it using:\n\tgit clone https://github.com/libigl/libigl.git ${CMAKE_SOURCE_DIR}/../libigl" + LIBIGL_INCLUDE_DIR) +mark_as_advanced(LIBIGL_INCLUDE_DIR) + +list(APPEND CMAKE_MODULE_PATH "${LIBIGL_INCLUDE_DIR}/../cmake") +include(libigl) diff --git a/examples/test.ipynb b/examples/test.ipynb new file mode 100644 index 0000000..51497a5 --- /dev/null +++ b/examples/test.ipynb @@ -0,0 +1,409 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import pybindmagic" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "%%cpp -f generateMesh\n", + "#include \n", + "\n", + "\n", + "std::tuple generateMesh()\n", + "{\n", + " // Inline mesh of a cube\n", + " const Eigen::MatrixXd V = (Eigen::MatrixXd(8,3)<<\n", + " 0.0,0.0,0.0,\n", + " 0.0,0.0,1.0,\n", + " 0.0,1.0,0.0,\n", + " 0.0,1.0,1.0,\n", + " 1.0,0.0,0.0,\n", + " 1.0,0.0,1.0,\n", + " 1.0,1.0,0.0,\n", + " 1.0,1.0,1.0).finished();\n", + " const Eigen::MatrixXi F = (Eigen::MatrixXi(12,3)<<\n", + " 1,7,5,\n", + " 1,3,7,\n", + " 1,4,3,\n", + " 1,2,4,\n", + " 3,8,7,\n", + " 3,4,8,\n", + " 5,7,8,\n", + " 5,8,6,\n", + " 1,5,6,\n", + " 1,6,2,\n", + " 2,6,8,\n", + " 2,8,4).finished().array()-1;\n", + " return std::make_tuple(V,F);\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Vertices:\n", + " [[0. 0. 0.]\n", + " [0. 0. 1.]\n", + " [0. 1. 0.]\n", + " [0. 1. 1.]\n", + " [1. 0. 0.]\n", + " [1. 0. 1.]\n", + " [1. 1. 0.]\n", + " [1. 1. 1.]]\n", + "Faces:\n", + " [[0 6 4]\n", + " [0 2 6]\n", + " [0 3 2]\n", + " [0 1 3]\n", + " [2 7 6]\n", + " [2 3 7]\n", + " [4 6 7]\n", + " [4 7 5]\n", + " [0 4 5]\n", + " [0 5 1]\n", + " [1 5 7]\n", + " [1 7 3]]\n" + ] + } + ], + "source": [ + "V,F = generateMesh()\n", + "print(\"Vertices:\\n\",V)\n", + "print(\"Faces:\\n\",F)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "%%cpp\n", + "#include \n", + "\n", + "\n", + "std::tuple generateMesh()\n", + "{\n", + " // Inline mesh of a cube\n", + " const Eigen::MatrixXd V = (Eigen::MatrixXd(8,3)<<\n", + " 0.0,0.0,0.0,\n", + " 0.0,0.0,1.0,\n", + " 0.0,1.0,0.0,\n", + " 0.0,1.0,1.0,\n", + " 1.0,0.0,0.0,\n", + " 1.0,0.0,1.0,\n", + " 1.0,1.0,0.0,\n", + " 1.0,1.0,1.0).finished();\n", + " const Eigen::MatrixXi F = (Eigen::MatrixXi(12,3)<<\n", + " 1,7,5,\n", + " 1,3,7,\n", + " 1,4,3,\n", + " 1,2,4,\n", + " 3,8,7,\n", + " 3,4,8,\n", + " 5,7,8,\n", + " 5,8,6,\n", + " 1,5,6,\n", + " 1,6,2,\n", + " 2,6,8,\n", + " 2,8,4).finished().array()-1;\n", + " return std::make_tuple(V,F);\n", + "}\n", + "\n", + "void printMesh(Eigen::MatrixXd V,Eigen::MatrixXi F)\n", + "{\n", + " py::print(\"Vertices:\\n\", V);\n", + " py::print(\"Faces:\\n\", F);\n", + "}\n", + "\n", + "defs\n", + " m.def(\"generateMesh\",&generateMesh);\n", + " m.def(\"printMesh\",&printMesh);" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Vertices:\n", + " [[0. 0. 0.]\n", + " [0. 0. 1.]\n", + " [0. 1. 0.]\n", + " [0. 1. 1.]\n", + " [1. 0. 0.]\n", + " [1. 0. 1.]\n", + " [1. 1. 0.]\n", + " [1. 1. 1.]]\n", + "Faces:\n", + " [[0 6 4]\n", + " [0 2 6]\n", + " [0 3 2]\n", + " [0 1 3]\n", + " [2 7 6]\n", + " [2 3 7]\n", + " [4 6 7]\n", + " [4 7 5]\n", + " [0 4 5]\n", + " [0 5 1]\n", + " [1 5 7]\n", + " [1 7 3]]\n" + ] + } + ], + "source": [ + "printMesh(*generateMesh())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# CMAKE example\n", + "\n", + "Take a look at _pathtocmake/CMAKEList.txt_. Its a template cmake file. Make sure that the following lines are present:\n", + "\n", + "```\n", + "project({name} LANGUAGES CXX)\n", + "\n", + "find_package(Python COMPONENTS Interpreter Development REQUIRED)\n", + "add_subdirectory(pybind11)\n", + "pybind11_add_module(${PROJECT_NAME} ${SRCFILES})\n", + "```\n", + "\n", + "This example needs libigl in the pathtocmake folder. Clone it using:\n", + "\n", + "`git clone https://github.com/libigl/libigl.git`" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "cmake:\n", + "-- The CXX compiler identification is GNU 9.3.0\n", + "-- Check for working CXX compiler: /usr/bin/c++\n", + "-- Check for working CXX compiler: /usr/bin/c++ -- works\n", + "-- Detecting CXX compiler ABI info\n", + "-- Detecting CXX compiler ABI info - done\n", + "-- Detecting CXX compile features\n", + "-- Detecting CXX compile features - done\n", + "-- Found Python: /home/ugo/anaconda3/bin/python3.7 (found version \"3.7.6\") found components: Interpreter Development \n", + "-- pybind11 v2.6.3 dev1\n", + "-- Performing Test HAS_FLTO\n", + "-- Performing Test HAS_FLTO - Success\n", + "-- Looking for C++ include pthread.h\n", + "-- Looking for C++ include pthread.h - found\n", + "-- Performing Test CMAKE_HAVE_LIBC_PTHREAD\n", + "-- Performing Test CMAKE_HAVE_LIBC_PTHREAD - Failed\n", + "-- Looking for pthread_create in pthreads\n", + "-- Looking for pthread_create in pthreads - not found\n", + "-- Looking for pthread_create in pthread\n", + "-- Looking for pthread_create in pthread - found\n", + "-- Found Threads: TRUE \n", + "-- Creating target: igl::core (igl)\n", + "-- Creating target: igl::opengl (igl_opengl)\n", + "-- Found OpenGL: /usr/lib/x86_64-linux-gnu/libOpenGL.so found components: OpenGL \n", + "-- The C compiler identification is GNU 9.3.0\n", + "-- Check for working C compiler: /usr/bin/cc\n", + "-- Check for working C compiler: /usr/bin/cc -- works\n", + "-- Detecting C compiler ABI info\n", + "-- Detecting C compiler ABI info - done\n", + "-- Detecting C compile features\n", + "-- Detecting C compile features - done\n", + "-- Creating target: igl::opengl_glfw (igl_opengl_glfw)\n", + "-- Using X11 for window creation\n", + "-- Found X11: /usr/include \n", + "-- Looking for XOpenDisplay in /usr/lib/x86_64-linux-gnu/libX11.so;/usr/lib/x86_64-linux-gnu/libXext.so\n", + "-- Looking for XOpenDisplay in /usr/lib/x86_64-linux-gnu/libX11.so;/usr/lib/x86_64-linux-gnu/libXext.so - found\n", + "-- Looking for gethostbyname\n", + "-- Looking for gethostbyname - found\n", + "-- Looking for connect\n", + "-- Looking for connect - found\n", + "-- Looking for remove\n", + "-- Looking for remove - found\n", + "-- Looking for shmat\n", + "-- Looking for shmat - found\n", + "-- Looking for IceConnectionNumber in ICE\n", + "-- Looking for IceConnectionNumber in ICE - found\n", + "-- Configuring done\n", + "-- Generating done\n", + "-- Build files have been written to: /home/ugo/work/pybindipynb/examples/pathtocmake/build\n", + "\n", + "make:\n", + "Scanning dependencies of target glad\n", + "[ 4%] Building C object glad/CMakeFiles/glad.dir/src/glad.c.o\n", + "[ 9%] Linking C static library libglad.a\n", + "[ 9%] Built target glad\n", + "Scanning dependencies of target glfw\n", + "[ 14%] Building C object glfw/src/CMakeFiles/glfw.dir/context.c.o\n", + "[ 19%] Building C object glfw/src/CMakeFiles/glfw.dir/init.c.o\n", + "[ 23%] Building C object glfw/src/CMakeFiles/glfw.dir/input.c.o\n", + "[ 28%] Building C object glfw/src/CMakeFiles/glfw.dir/monitor.c.o\n", + "[ 33%] Building C object glfw/src/CMakeFiles/glfw.dir/vulkan.c.o\n", + "[ 38%] Building C object glfw/src/CMakeFiles/glfw.dir/window.c.o\n", + "[ 42%] Building C object glfw/src/CMakeFiles/glfw.dir/x11_init.c.o\n", + "[ 47%] Building C object glfw/src/CMakeFiles/glfw.dir/x11_monitor.c.o\n", + "[ 52%] Building C object glfw/src/CMakeFiles/glfw.dir/x11_window.c.o\n", + "[ 57%] Building C object glfw/src/CMakeFiles/glfw.dir/xkb_unicode.c.o\n", + "[ 61%] Building C object glfw/src/CMakeFiles/glfw.dir/posix_time.c.o\n", + "[ 66%] Building C object glfw/src/CMakeFiles/glfw.dir/posix_thread.c.o\n", + "[ 71%] Building C object glfw/src/CMakeFiles/glfw.dir/glx_context.c.o\n", + "[ 76%] Building C object glfw/src/CMakeFiles/glfw.dir/egl_context.c.o\n", + "[ 80%] Building C object glfw/src/CMakeFiles/glfw.dir/osmesa_context.c.o\n", + "[ 85%] Building C object glfw/src/CMakeFiles/glfw.dir/linux_joystick.c.o\n", + "[ 90%] Linking C static library libglfw3.a\n", + "[ 90%] Built target glfw\n", + "Scanning dependencies of target cpp_magic_5329071abe\n", + "[ 95%] Building CXX object CMakeFiles/cpp_magic_5329071abe.dir/cpp_magic_5329071abe.cpp.o\n", + "[100%] Linking CXX shared module cpp_magic_5329071abe.cpython-37m-x86_64-linux-gnu.so\n", + "[100%] Built target cpp_magic_5329071abe\n", + "\n" + ] + } + ], + "source": [ + "%%cpp -c pathtocmake -f viewMesh -rebuild\n", + "\n", + "#include \n", + "#include \n", + "\n", + "\n", + "void viewMesh(Eigen::Matrix V, Eigen::Matrix F)\n", + "{\n", + " // Plot the mesh\n", + " igl::opengl::glfw::Viewer viewer;\n", + " viewer.data().set_mesh(V, F);\n", + " viewer.data().set_face_based(true);\n", + " viewer.launch();\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "ename": "ImportError", + "evalue": "dynamic module does not define module export function (PyInit_cpp_magic_f96b7fce74)", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mImportError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0;32mimport\u001b[0m \u001b[0mcpp_magic_f96b7fce74\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 2\u001b[0m \u001b[0;31m#viewMesh(V,F)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mImportError\u001b[0m: dynamic module does not define module export function (PyInit_cpp_magic_f96b7fce74)" + ] + } + ], + "source": [ + "import cpp_magic_f96b7fce74\n", + "#viewMesh(V,F)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "V = np.array([\n", + " [0.0,0.0,0.0],\n", + " [0.0,0.0,1.0],\n", + " [0.0,1.0,0.0],\n", + " [0.0,1.0,1.0],\n", + " [1.0,0.0,0.0],\n", + " [1.0,0.0,1.0],\n", + " [1.0,1.0,0.0],\n", + " [1.0,1.0,1.0]])\n", + "\n", + "F = np.array(\n", + " [\n", + " [1,7,5],\n", + " [1,3,7],\n", + " [1,4,3],\n", + " [1,2,4],\n", + " [3,8,7],\n", + " [3,4,8],\n", + " [5,7,8],\n", + " [5,8,6],\n", + " [1,5,6],\n", + " [1,6,2],\n", + " [2,6,8],\n", + " [2,8,4]\n", + " ], dtype=np.int32)-1\n", + "viewMesh(V,F)" + ] + } + ], + "metadata": { + "@webio": { + "lastCommId": null, + "lastKernelId": null + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.6" + }, + "latex_envs": { + "LaTeX_envs_menu_present": true, + "autoclose": false, + "autocomplete": true, + "bibliofile": "biblio.bib", + "cite_by": "apalike", + "current_citInitial": 1, + "eqLabelWithNumbers": true, + "eqNumInitial": 1, + "hotkeys": { + "equation": "Ctrl-E", + "itemize": "Ctrl-I" + }, + "labels_anchors": false, + "latex_user_defs": false, + "report_style_numbering": false, + "user_envs_cfg": false + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/pybindmagic/__init__.py b/pybindmagic/__init__.py new file mode 100644 index 0000000..d9015e6 --- /dev/null +++ b/pybindmagic/__init__.py @@ -0,0 +1,184 @@ +from IPython.core.magic import register_cell_magic, needs_local_scope + +import importlib +import glob +import sys, os +import random +import string +import hashlib +import shutil +import subprocess + +compiler_cmd = "c++" + +cmd_flags = ["$(pkg-config --cflags eigen3)"] + +template = """ +#include +namespace py = pybind11; + +{cell_code} + +PYBIND11_MODULE({name}, m) + {defs} +""" + +def_template = '\nm.def("{function}", &{function});\n' + +tmp_path = "pbm_modules" + +# Make sure the correct python executeable is used using cmake +os.environ["PATH"] = os.path.dirname(sys.executable) + os.pathsep + os.environ["PATH"] + +def exec_command(cmd, cwd=None,**kwargs): + try: + output = subprocess.check_output(cmd,cwd=cwd,stderr=subprocess.STDOUT,**kwargs) + if isinstance(output,bytes): + return False, output.decode("utf-8") + return False, output + except subprocess.CalledProcessError as e: + if isinstance(e.output,bytes): + return True, e.output.decode("utf-8") + return True, e.output + + + +def ensure_tmp_folder(): + path = os.path.join(os.getcwd(), tmp_path) + if not os.path.exists(path): + os.makedirs(path) + if not path in sys.path: + sys.path.append(path) + +def ensure_pybind(path, force=False): + path = os.path.join(path,"pybind11") + exists = os.path.exists(path) + if exists and force: + shutil.rmtree(os.path.join(path,"pybind11")) + if not exists: + err, output = exec_command(["git", "clone","https://github.com/pybind/pybind11.git",path]) + +def ensure_build_dir(path, clear=False): + path = os.path.join(path, "build") + exists = os.path.exists(path) + if exists and clear: + shutil.rmtree(path) + if not exists or clear: + os.makedirs(path) + return exists and not clear + +@register_cell_magic +def cpp(line, cell, *what): + """Compile, execute C++ code wrapped with pybind11 and import it""" + args = line.split(" ") + rebuild = False + cmake_path = None + function = "" + run_cmake = False + # TODO: Add full argument parser + for i,arg in enumerate(args): + if arg == "-c": + cmake_path = args[i+1].strip() + if arg == "-f": + function = args[i+1].strip() + if arg == "-rebuild": + rebuild = True + + + # We first retrieve the current IPython interpreter + # instance. + ip = get_ipython() + + cmake = "" + if cmake_path is not None: + with open(os.path.join(cmake_path,"CMakeLists.txt"), 'r') as f: + cmake = f.read() + # name = ''.join(random.choices(string.ascii_lowercase, k=10)) + hash_object = hashlib.sha256((cmake+" ".join(line)+function+cell).encode()) + name = "cpp_magic_"+hash_object.hexdigest()[:10] + if cmake_path is None: + ensure_tmp_folder() + else: + run_cmake = not ensure_build_dir(cmake_path, clear=rebuild) + p = os.path.abspath(os.path.join(cmake_path,"build")) + if p not in sys.path: + sys.path.append(p) + ensure_pybind(cmake_path) + + if cmake_path is not None: + path = cmake_path + else: + path = tmp_path + + # We define the source and executable filenames. + try: + try: + if rebuild: + raise Exception() + mdl = importlib.import_module(name) + except: + exe = sys.executable + split = cell.split("defs") + if len(split) == 1 and function == "": + raise Exception("You have to name a function as argument ('%%cpp -f ') or manual set defs") + split.append("") + + curr_defs = split[1] + if function != "": + curr_defs += def_template.format(function=function) + + + # We write the code to the C++ file. + with open(os.path.join(path,f"{name}.cpp"), 'w') as f: + f.write(template.format(cell_code=split[0],name=name,defs="{"+curr_defs+"}")) + + + if cmake_path is None: + # We compile the C++ code into an executable. + command = f"{compiler_cmd} {' '.join(cmd_flags)} -O3 -Wall -shared -std=c++11 -fPIC $({exe} -m pybind11 --includes) {tmp_path}/{name}.cpp -o {tmp_path}/{name}.so" + compile = ip.getoutput(command) + if len(compile) != 0: + raise Exception("\n".join(compile)) + else: + with open(os.path.join(cmake_path,"CMakeLists.txt"), 'w') as f: + f.write(cmake.replace("{name}",name)) + # TODO: Add option to add cmake flags + if run_cmake: + command = ["cmake", ".."] + print("cmake:") + err, output = exec_command(command, cwd=os.path.join(cmake_path, "build")) + if err: + raise Exception(output) + print(output) + command = ["make"] + print("make:") + err, output = exec_command(command, cwd=os.path.join(cmake_path, "build")) + if err: + raise Exception(output) + print(output) + with open(os.path.join(cmake_path,"CMakeLists.txt"), 'w') as f: + f.write(cmake) + + + # We execute the executable and return the output. + # get a handle on the module + mdl = importlib.import_module(name) + + if os.path.exists(os.path.join(path,f"{name}.cpp")): + os.remove(os.path.join(path,f"{name}.cpp")) + + # is there an __all__? if so respect it + if "__all__" in mdl.__dict__: + names = mdl.__dict__["__all__"] + else: + # otherwise we import all names that don't begin with _ + names = [x for x in mdl.__dict__ if not x.startswith("_")] + except Exception as e: + with open(os.path.join(cmake_path,"CMakeLists.txt"), 'w') as f: + f.write(cmake) + if os.path.exists(os.path.join(path,f"{name}.cpp")): + os.remove(os.path.join(path,f"{name}.cpp")) + raise e + + # now drag them in + ip.push({k: getattr(mdl, k) for k in names}) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..31f76ef --- /dev/null +++ b/setup.py @@ -0,0 +1,35 @@ +from setuptools import setup, find_packages +import os + +# Utility function to read the README file. +# Used for the long_description. It's nice, because now 1) we have a top level +# README file and 2) it's easier to type in the README file than to put a raw +# string in below ... +def read(fname): + return open(os.path.join(os.path.dirname(__file__), fname)).read() + +setup( + name="pybindmagic", + version="0.1", + packages=find_packages(), + # requires=['pybind11'], + # scripts=['say_hello.py'], + # install_requires=['python_version>=3.5'], + + # metadata to display on PyPI + author="Ugo Finnendahl", + author_email="finnendahl@tu-berlin.de", + description="This is a simple pybind11 wrapper as ipython cellmagic.", + keywords="ipython, jupyter, cpp, c++, cell_magic", + # url="http://example.com/HelloWorld/", # project home page, if any + # project_urls={ + # "Bug Tracker": "https://bugs.example.com/HelloWorld/", + # "Documentation": "https://docs.example.com/HelloWorld/", + # "Source Code": "https://code.example.com/HelloWorld/", + # }, + # classifiers=[ + # 'License :: OSI Approved :: Python Software Foundation License' + # ], + python_requires=">=3.5", + long_description=read('README.md') +)