From b43cd07ec70dad553bf41ed626d871b02def53a4 Mon Sep 17 00:00:00 2001
From: Ben Cumming <bcumming@cscs.ch>
Date: Tue, 25 Feb 2020 15:03:09 +0100
Subject: [PATCH] More robust Python installation (#971)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Improve the Python wrapper generation and installation:
  - install a proper module that can be extended with Python code;
  - give the user more control over where to install the module (e.g. as a user package or in a virtualenv).

During building, the following sub-directory is built in the build director (`CMAKE_BINARY_DIR`)
```
└── python
    └── arbor
            ├── __init__.py
            ├── arbor.so
            └── VERSION
```
This path can then be copied VERBATIM to the target installation path. By default this will be in `CMAKE_INSTALL_PREFIX/lib/python%d.%d/site-packages`.
An additional CMake parameter `ARB_PYTHON_PREFIX` can be used to specify an alternative destination for installing the Python module.

The Python part of the wrapper, implemented in `__init__.py` is currently very limited, only providing `__version__` and `__config__` variables.

The installation guide was updated to cover the Python installation.
---
 CMakeLists.txt                                |  7 +++
 doc/install.rst                               | 52 ++++++++++++++++---
 doc/python.rst                                | 41 +++++++++------
 python/CMakeLists.txt                         | 29 +++++++----
 python/__init__.py                            | 15 ++++++
 python/pyarb.cpp                              |  2 +-
 python/setup.py                               | 38 ++++++++++++++
 .../test/unit/test_domain_decompositions.py   |  3 +-
 python/test/unit_distributed/runner.py        |  5 +-
 .../unit_distributed/test_contexts_arbmpi.py  |  3 +-
 .../unit_distributed/test_contexts_mpi4py.py  |  5 +-
 .../test_domain_decompositions.py             |  5 +-
 scripts/travis/build.sh                       |  3 +-
 13 files changed, 159 insertions(+), 49 deletions(-)
 create mode 100644 python/__init__.py
 create mode 100644 python/setup.py

diff --git a/CMakeLists.txt b/CMakeLists.txt
index fc86e446..4ce372dd 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -53,6 +53,13 @@ option(ARB_WITH_ASSERTIONS "enable arb_assert() assertions in code" OFF)
 
 option(ARB_WITH_PYTHON "enable Python front end" OFF)
 
+# Path in which to install the Python module.
+# For Python 3.8, the module would ibe installed in
+#       ${ARB_PYTHON_PREFIX}/python3.8/site-packages
+# To install Arbor eqivalently to `pip install --user` on a linux system:
+#       -DARB_PYTHON_PREFIX="${HOME}/.local/lib""
+set(ARB_PYTHON_PREFIX ${CMAKE_INSTALL_PREFIX} CACHE PATH "path for installing Python module for Arbor.")
+
 #----------------------------------------------------------
 # Global CMake configuration
 #----------------------------------------------------------
diff --git a/doc/install.rst b/doc/install.rst
index 3ca16340..8de0b760 100644
--- a/doc/install.rst
+++ b/doc/install.rst
@@ -117,10 +117,11 @@ More information on building with MPI is in the `HPC cluster section <cluster_>`
 Python
 ~~~~~~
 
-Arbor has a Python frontend, for which Python 3.6 is required.
+Arbor has a Python frontend, for which a minimum of Python 3.6 is required.
 In order to use MPI in combination with the python frontend the
 `mpi4py <https://mpi4py.readthedocs.io/en/stable/install.html#>`_
-Python package is recommended.
+Python package is recommended. See :ref:`pythonfrontend` for more information.
+
 
 Documentation
 ~~~~~~~~~~~~~~
@@ -354,6 +355,8 @@ example:
     Arbor supports and has been tested on the Kepler (K20 & K80), Pascal (P100) and Volta (V100) GPUs
 
 
+.. _pythonfrontend:
+
 Python Frontend
 ----------------
 
@@ -362,13 +365,46 @@ CMake ``ARB_WITH_PYTHON`` option:
 
 .. code-block:: bash
 
-    cmake -ARB_WITH_PYTHON=ON
+    cmake -DARB_WITH_PYTHON=ON
+
+By default ``ARB_WITH_PYTHON=OFF``. When this option is turned on, a Python module called :py:mod:`arbor` is built.
+
+A specific version of Python can be set when configuring with CMake using the
+``PYTHON_EXECUTABLE`` variable. For example, to use Python 3.7 installed on a Linux
+system with the executable in ``/usr/bin/python3.7``:
+
+.. code-block:: bash
+
+    cmake .. -DARB_WITH_PYTHON=ON -DPYTHON_EXECUTABLE=/usr/bin/python3.7
+
+By default the Python module will be installed in the standard ``CMAKE_INSTALL_PREFIX``
+location. To install the module in a different location, for example as a
+user module or in a virtual environment, set ``ARB_PYTHON_PREFIX``.
+For example, the CMake configuration for targetting Python 3.8 and install as a
+user site package might look like the following:
+
+.. code-block:: bash
+
+    cmake .. -DARB_WITH_PYTHON=ON                   \
+             -DARB_PYTHON_PREFIX=${HOME}/.local     \
+             -DPYTHON_EXECUTABLE=/user/bin/python3.8
 
-By default ``ARB_WITH_PYTHON=OFF``. When this option is turned on, a python module called :py:mod:`arbor` is built.
+On the target LINUX system, the Arbor package was installed in
+``/home/$USER/.local/lib/python3.8/site-packages``.
 
-The Arbor Python wrapper has optional support for the ``mpi4py`` Python module
-for MPI. CMake will attempt to automatically detect ``mpi4py`` if configured
-with both ``-ARB_WITH_PYTHON=ON`` and MPI ``-DARB_WITH_MPI=ON``.
+.. Note::
+    By default CMake sets ``CMAKE_INSTALL_PREFIX`` to ``/usr/local`` on Linux and OS X.
+    The compiled libraries are installed in ``/usr/local/lib``, headers are installed in
+    ``/usr/local/include``, and the Python module will be installed in a path like
+    ``/usr/local/lib/python3.7/site-packages``.
+    Because ``/usr/local`` is a system path, the installation phase needs to be run as root,
+    i.e. ``sudo make install``, even if ``ARB_PYTHON_PREFIX`` is set to a user path
+    that does not require root to install.
+
+The Arbor Python wrapper has optional support for the mpi4py, though
+it is not required to use Arbor with Python and MPI.
+CMake will attempt to automatically detect ``mpi4py`` if configured
+with both ``-DARB_WITH_PYTHON=ON`` and MPI ``-DARB_WITH_MPI=ON``.
 If CMake fails to find ``mpi4py`` when it should, the easiest workaround is to
 add the path to the include directory for ``mpi4py`` to the ``CPATH`` environment
 variable before configuring and building Arbor:
@@ -384,7 +420,7 @@ variable before configuring and building Arbor:
     # set CPATH and run cmake
     export CPATH="/path/to/python3/site-packages/mpi4py/include/:$CPATH"
 
-    cmake -ARB_WITH_PYTHON=ON -DARB_WITH_MPI=ON
+    cmake -DARB_WITH_PYTHON=ON -DARB_WITH_MPI=ON
 
 .. _install:
 
diff --git a/doc/python.rst b/doc/python.rst
index d3238b34..94ef8d82 100644
--- a/doc/python.rst
+++ b/doc/python.rst
@@ -7,30 +7,38 @@ Arbor provides access to all of the C++ library's functionality in Python,
 which is the only interface for many users.
 The getting started guides will introduce Arbor via the Python interface.
 
-Before starting Arbor needs to be installed with the Python interface enabled,
-as per the `installation guide <_installarbor>`_.
-To test that Arbor is available, open `Python 3 <python2_>`_, and try the following:
+To test that Arbor is available, try the following in a `Python 3 <python2_>`_ interpreter:
 
 .. container:: example-code
 
     .. code-block:: python
 
-        import arbor
-        arbor.config()
-
-        {'mpi': True, 'mpi4py': True, 'gpu': False, 'version': '0.3'}
+        >>> import arbor
+        >>> print(arbor.__config__)
+        {'mpi': True, 'mpi4py': True, 'gpu': False, 'version': '0.2.3-dev'}
+        >>> print(arbor.__version__)
+        0.2.3-dev
 
-Calling ``arbor.config()`` returns a dictionary with information about the Arbor installation.
+The dictionary ``arbor.__config__`` contains information about the Arbor installation.
 This can be used to check that Arbor supports features that you require to run your model,
 or even to dynamically decide how to run a model.
-Single cell models like the one introduced here we do not require parallelism like
+Single cell models like do not require parallelism like
 that provided by MPI or GPUs, so the ``'mpi'`` and ``'gpu'`` fields can be ``False``.
 
+Installing
+-------------
+
+Before starting Arbor needs to be installed with the Python interface enabled,
+following the :ref:`Python configuration <pythonfrontend>` in
+the
+:ref:`installation guide <installarbor>`.
+
+
 Performance
 --------------
 
 The Python interface can incur significant performance overheads relative to C++
-during the *model building* phase, however simulation performance will be the same
+during the *model building* phase, however simulation performance is be the same
 for both interfaces.
 
 .. _python2:
@@ -38,11 +46,10 @@ for both interfaces.
 Python 2
 ----------
 
-Python 2.7 will reach `end of life <https://pythonclock.org/>`_ in January 2020.
-Arbor should be installed using Python 3, and all examples in the documentation are in
-Python 3. It might be possible to install and run Arbor using Python 2.7, however Arbor support
-will only be provided for Python 3.
-
-NEURON users that use Python 2 can take the opportunity of trying Arbor to make
-the move to Python 3.
+Python 2 reached `end of life <https://pythonclock.org/>`_ in January 2020.
+Arbor only officially supports Python 3.6 and later, and all examples in the
+documentation are in Python 3. While it is possible to install and run Arbor
+using Python 2.7 by setting the ``PYTHON_EXECUTABLE`` variable when
+configuring CMake, support is only provided for using Arbor with Python 3.6
+and later.
 
diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt
index bf4d5f00..12e7c388 100644
--- a/python/CMakeLists.txt
+++ b/python/CMakeLists.txt
@@ -46,13 +46,13 @@ target_link_libraries(pyarb_obj PRIVATE arbor pybind11::module)
 # The Python library. MODULE will make a Python-exclusive model.
 add_library(pyarb MODULE $<TARGET_OBJECTS:pyarb_obj>)
 
-# The output name of the pyarb .so file is "arbor", to facilitate "import arbor"
-set_target_properties(pyarb PROPERTIES OUTPUT_NAME arbor)
+# The output name of the pyarb .so file is "_arbor"
+set_target_properties(pyarb PROPERTIES OUTPUT_NAME _arbor)
 # With this, the full name of the library will be something like:
-#   arbor.cpython-37m-x86_64-linux-gnu.so
+#   arbor.cpython-36m-x86_64-linux-gnu.so
 set_target_properties(pyarb PROPERTIES PREFIX "${PYTHON_MODULE_PREFIX}" SUFFIX "${PYTHON_MODULE_EXTENSION}")
 
-# this dependency has to be spelt out again, despite being added to
+# This dependency has to be spelt out again, despite being added to
 # pyarb_obj because CMake.
 target_link_libraries(pyarb PRIVATE arbor pybind11::module)
 
@@ -65,10 +65,21 @@ if (ARB_WITH_MPI)
     endif()
 endif()
 
+# For unit tests on C++ side of Python wrappers
+add_subdirectory(test)
+
+# Create the Python module in the build directory.
+# The module contains the dynamic library, __init__.py and VERSION information.
+set(python_mod_path "${CMAKE_CURRENT_BINARY_DIR}/arbor")
+set_target_properties(pyarb PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${python_mod_path}")
+file(COPY "${CMAKE_SOURCE_DIR}/python/__init__.py" DESTINATION "${python_mod_path}")
+file(COPY "${CMAKE_SOURCE_DIR}/VERSION" DESTINATION "${python_mod_path}")
+
 # Determine the installation path, according to the Python version.
+# The installation for Python 3.8 would be:
+#  ${ARB_PYTHON_PREFIX}/python3.8/site-packages
+# By default ARB_PYTHON_PREFIX is set to CMAKE_INSTALL_PREFIX, and can be optionally
+# used to install the Python module as a user module, or in a virtualenv.
 find_package(PythonInterp REQUIRED)
-set(ARB_PYEXECDIR "${CMAKE_INSTALL_LIBDIR}/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages")
-install(TARGETS pyarb LIBRARY DESTINATION ${ARB_PYEXECDIR})
-
-# for unit tests on C++ side of Python wrappers
-add_subdirectory(test)
+set(arb_pyexecdir "${ARB_PYTHON_PREFIX}/lib/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages")
+install(DIRECTORY ${python_mod_path} DESTINATION ${arb_pyexecdir})
diff --git a/python/__init__.py b/python/__init__.py
new file mode 100644
index 00000000..dd137a74
--- /dev/null
+++ b/python/__init__.py
@@ -0,0 +1,15 @@
+# The Python wrapper generated using pybind11 is a compiled dynamic library,
+# with a name like _arbor.cpython-38-x86_64-linux-gnu.so
+#
+# The library will be installed in the same path as this file, which will imports
+# the compiled part of the wrapper from the _arbor namespace.
+
+from ._arbor import *
+
+import os
+
+here = os.path.abspath(os.path.dirname(__file__))
+with open(os.path.join(here, 'VERSION')) as version_file:
+    __version__ = version_file.read().strip()
+
+__config__ = config()
diff --git a/python/pyarb.cpp b/python/pyarb.cpp
index bab7005f..ae2dc9a0 100644
--- a/python/pyarb.cpp
+++ b/python/pyarb.cpp
@@ -27,7 +27,7 @@ void register_mpi(pybind11::module& m);
 #endif
 }
 
-PYBIND11_MODULE(arbor, m) {
+PYBIND11_MODULE(_arbor, m) {
     m.doc() = "arbor: multicompartment neural network models.";
     m.attr("__version__") = ARB_VERSION;
 
diff --git a/python/setup.py b/python/setup.py
new file mode 100644
index 00000000..efd8227e
--- /dev/null
+++ b/python/setup.py
@@ -0,0 +1,38 @@
+import setuptools
+import os
+
+here = os.path.abspath(os.path.dirname(__file__))
+with open(os.path.join(here, 'arbor/VERSION')) as version_file:
+    version_ = version_file.read().strip()
+
+setuptools.setup(
+    name='arbor',
+    packages=['arbor'],
+    version=version_,
+    author='CSCS and FSJ',
+    url='https://github.com/arbor-sim/arbor',
+    description='High performance simulation of networks of multicompartment neurons.',
+    long_description='',
+    classifiers=[
+        'Development Status :: 4 - Beta', # Upgrade to "5 - Production/Stable" on release.
+        'Intended Audience :: Science/Research',
+        'Topic :: Scientific/Engineering :: Build Tools',
+        'License :: OSI Approved :: BSD License'
+        'Programming Language :: Python :: 3.6',
+        'Programming Language :: Python :: 3.7',
+        'Programming Language :: Python :: 3.8',
+    ],
+    project_urls={
+        'Source': 'https://github.com/arbor-sim/arbor',
+        'Documentation': 'https://arbor.readthedocs.io',
+        'Bug Reports': 'https://github.com/arbor-sim/arbor/issues',
+    },
+    package_data={
+        'arbor': ['VERSION', '_arbor.*.so'],
+    },
+    python_requires='>=3.6',
+    install_requires=[],
+    setup_requires=[],
+    zip_safe=False,
+)
+
diff --git a/python/test/unit/test_domain_decompositions.py b/python/test/unit/test_domain_decompositions.py
index fc239f9c..eae6b2d4 100644
--- a/python/test/unit/test_domain_decompositions.py
+++ b/python/test/unit/test_domain_decompositions.py
@@ -16,8 +16,7 @@ except ModuleNotFoundError:
     from test import options
 
 # check Arbor's configuration of mpi and gpu
-config = arb.config()
-gpu_enabled = config["gpu"]
+gpu_enabled = arb.__config__["gpu"]
 
 """
 all tests for non-distributed arb.domain_decomposition
diff --git a/python/test/unit_distributed/runner.py b/python/test/unit_distributed/runner.py
index 8a913781..b887802b 100644
--- a/python/test/unit_distributed/runner.py
+++ b/python/test/unit_distributed/runner.py
@@ -6,9 +6,8 @@ import unittest
 import arbor as arb
 
 # check Arbor's configuration of mpi
-config = arb.config()
-mpi_enabled = config["mpi"]
-mpi4py_enabled = config["mpi4py"]
+mpi_enabled    = arb.__config__["mpi"]
+mpi4py_enabled = arb.__config__["mpi4py"]
 
 if (mpi_enabled and mpi4py_enabled):
     import mpi4py.MPI as mpi
diff --git a/python/test/unit_distributed/test_contexts_arbmpi.py b/python/test/unit_distributed/test_contexts_arbmpi.py
index 57d15bdb..30328cc7 100644
--- a/python/test/unit_distributed/test_contexts_arbmpi.py
+++ b/python/test/unit_distributed/test_contexts_arbmpi.py
@@ -16,8 +16,7 @@ except ModuleNotFoundError:
     from test import options
 
 # check Arbor's configuration of mpi
-config = arb.config()
-mpi_enabled = config["mpi"]
+mpi_enabled = arb.__config__["mpi"]
 
 """
 all tests for distributed arb.context using arbor mpi wrappers
diff --git a/python/test/unit_distributed/test_contexts_mpi4py.py b/python/test/unit_distributed/test_contexts_mpi4py.py
index 4dd225b6..82b3c345 100644
--- a/python/test/unit_distributed/test_contexts_mpi4py.py
+++ b/python/test/unit_distributed/test_contexts_mpi4py.py
@@ -16,9 +16,8 @@ except ModuleNotFoundError:
     from test import options
 
 # check Arbor's configuration of mpi
-config = arb.config()
-mpi_enabled = config["mpi"]
-mpi4py_enabled = config["mpi4py"]
+mpi_enabled    = arb.__config__["mpi"]
+mpi4py_enabled = arb.__config__["mpi4py"]
 
 if (mpi_enabled and mpi4py_enabled):
     import mpi4py.MPI as mpi
diff --git a/python/test/unit_distributed/test_domain_decompositions.py b/python/test/unit_distributed/test_domain_decompositions.py
index 17bd5252..f5a3a0df 100644
--- a/python/test/unit_distributed/test_domain_decompositions.py
+++ b/python/test/unit_distributed/test_domain_decompositions.py
@@ -16,9 +16,8 @@ except ModuleNotFoundError:
     from test import options
 
 # check Arbor's configuration of mpi and gpu
-config = arb.config()
-gpu_enabled = config["gpu"]
-mpi_enabled = config["mpi"]
+mpi_enabled = arb.__config__["mpi"]
+gpu_enabled = arb.__config__["gpu"]
 
 """
 all tests for distributed arb.domain_decomposition
diff --git a/scripts/travis/build.sh b/scripts/travis/build.sh
index 000e5e11..93b2a389 100755
--- a/scripts/travis/build.sh
+++ b/scripts/travis/build.sh
@@ -48,7 +48,8 @@ fi
 if [[ "${WITH_PYTHON}" == "true" ]]; then
     echo "python     : on"
     ARB_WITH_PYTHON="ON"
-    export PYTHONPATH=$PYTHONPATH:${base_path}/${build_path}/lib
+    # The build process creates the arbor module in build_path/python/arbor
+    export PYTHONPATH=$PYTHONPATH:${base_path}/${build_path}/python
     python_path=$base_path/python
     echo "python src : ${python_path}"
     echo "PYTHONPATH : ${PYTHONPATH}"
-- 
GitLab