SpECTRE Documentation Coverage Report
Current view: top level - __w/spectre/spectre/docs/DevGuide - PythonBindings.md Hit Total Coverage
Commit: 3c072f0ce967e2e56649d3fa12aa2a0e4fe2a42e Lines: 0 1 0.0 %
Date: 2024-04-23 20:50:18
Legend: Lines: hit not hit

          Line data    Source code
       1           0 : \cond NEVER
       2             : Distributed under the MIT License.
       3             : See LICENSE.txt for details.
       4             : \endcond
       5             : # Writing Python Bindings {#spectre_writing_python_bindings}
       6             : 
       7             : \tableofcontents
       8             : 
       9             : ## CMake and Directory Layout
      10             : 
      11             : To allow users to analyze output from simulations and take advantage of
      12             : SpECTRE's data structures and functions in python, bindings must sometimes be
      13             : written. SpECTRE uses [pybind11](https://pybind11.readthedocs.io/)
      14             : to aid with generating the bindings. The C++ code for the bindings should
      15             : generally go in a `Python` subdirectory. For example, the bindings for the
      16             : DataStructures library would go in `src/DataStructures/Python/`. SpECTRE
      17             : provides the `spectre_python_add_module` CMake function to make adding a new
      18             : python module, be it with or without bindings, easy.  The python bindings are
      19             : built only if `-D BUILD_PYTHON_BINDINGS=ON` is passed when invoking cmake
      20             : (enabled by default).
      21             : You can specify the Python version, interpreter and libraries used for compiling
      22             : and testing the bindings by setting the `-D Python_EXECUTABLE` to an absolute
      23             : path such as `/usr/bin/python3`.
      24             : 
      25             : The function `spectre_python_add_module` takes as its first argument the module,
      26             : in our case `DataStructures`. Optionally, a list of `SOURCES` can be passed to
      27             : the CMake function. If you specify `SOURCES`, you must also specify a
      28             : `LIBRARY_NAME`. A good `LIBRARY_NAME` is the name of the C++ library for which
      29             : bindings are being built prefixed with `Py`, e.g. `PyDataStructures`. If the
      30             : Python module will only consist of Python files, then the `SOURCES` option
      31             : should not be specified. Python files that should be part of the module can be
      32             : passed with the keyword `PYTHON_FILES`. Finally, the `MODULE_PATH`
      33             : named argument can be passed with a string that is the path to where the module
      34             : should be. For example, `MODULE_PATH "submodule0/submodule1/"` would mean the
      35             : module is accessed from python using
      36             : `import spectre.submodule0.submodule1.MODULE_NAME`.
      37             : 
      38             : Here is a complete example of how to call the `spectre_python_add_module`
      39             : function:
      40             : 
      41             : \code
      42             : spectre_python_add_module(
      43             :   Extra
      44             :   LIBRARY_NAME "PyExtraDataStructures"
      45             :   MODULE_PATH "DataStructures/"
      46             :   SOURCES Bindings.cpp MyCoolDataStructure.cpp
      47             :   PYTHON_FILES CoolPythonDataStructure.py
      48             :   )
      49             : \endcode
      50             : 
      51             : The library that is added has the name `PyExtraDataStructures`. Make sure to
      52             : call `spectre_python_link_libraries` for every Python module that compiles
      53             : `SOURCES`. For example,
      54             : 
      55             : \code
      56             : spectre_python_link_libraries(
      57             :   PyExtraDataStructures
      58             :   PRIVATE
      59             :   ExtraDataStructures
      60             :   pybind11::module
      61             :   )
      62             : \endcode
      63             : 
      64             : You may also call `spectre_python_add_dependencies` for Python modules that
      65             : have `SOURCES`, e.g.
      66             : 
      67             : \code
      68             : spectre_python_add_dependencies(
      69             :   PyExtraDataStructures
      70             :   PyDataStructures
      71             :   )
      72             : \endcode
      73             : 
      74             : Note that these functions will skip adding or configure any C++ libraries if
      75             : the `BUILD_PYTHON_BINDINGS` flag is `OFF`.
      76             : 
      77             : ## Writing Bindings
      78             : 
      79             : Once a python module has been added you can write the actual bindings. You
      80             : should structure your bindings directory to reflect the structure of the library
      81             : you're writing bindings for. For example, say we want bindings for `DataVector`
      82             : and `Matrix` then we should have one source file for each class's bindings
      83             : inside `src/DataStructures/Python`. The functions that generate the bindings
      84             : should be in the `py_bindings` namespace and have a reasonable name such as
      85             : `bind_datavector`. There should be a file named `Bindings.cpp` which calls all
      86             : the `bind_*` functions. The `Bindings.cpp` file is quite simple and should
      87             : `include <pybind11/pybind11.h>`, forward declare the `bind_*` functions, and
      88             : then have a `PYBIND11_MODULE` function. For example,
      89             : 
      90             : \code{.cpp}
      91             : #include <pybind11/pybind11.h>
      92             : 
      93             : namespace py = pybind11;
      94             : 
      95             : namespace py_bindings {
      96             : void bind_datavector(py::module& m);
      97             : }  // namespace py_bindings
      98             : 
      99             : PYBIND11_MODULE(_Pybindings, m) {
     100             :   py_bindings::bind_datavector(m);
     101             : }
     102             : \endcode
     103             : 
     104             : Note that the library name is passed to `PYBIND11_MODULE` and is prefixed
     105             : with an underscore. The underscore is important and the library name must be the
     106             : same that is passed as `LIBRARY_NAME` to `spectre_python_add_module` (see
     107             : above).
     108             : 
     109             : The `DataVector` bindings serve as an example with code comments on how to write
     110             : bindings for a class. There is also extensive documentation available directly
     111             : from [pybind11](https://pybind11.readthedocs.io/).
     112             : 
     113             : If you are binding a library full of similarly structured free functions, such
     114             : as libraries in `src/PointwiseFunctions/`, you can bind all functions directly
     115             : in the `Bindings.cpp` file to avoid unnecessary boilerplate code. See
     116             : `src/PointwiseFunctions/GeneralRelativity/Python/Bindings.cpp` for an example.
     117             : 
     118             : \note Exceptions should be allowed to propagate through the bindings so that
     119             : error handling via exceptions is possible from python rather than having the
     120             : python interpreter being killed with a call to `abort`.
     121             : 
     122             : ## Testing Python Bindings and Code
     123             : 
     124             : All the python bindings must be tested. SpECTRE uses the
     125             : [unittest](https://docs.python.org/3/library/unittest.html) framework
     126             : provided as part of python. To register a test file with CMake use the
     127             : SpECTRE-provided function `spectre_add_python_test` passing as the first
     128             : argument the test name (e.g. `"Unit.DataStructures.Python.DataVector"`), the
     129             : file as the second argument (e.g. `Test_DataVector.py`), and a semicolon
     130             : separated list of labels as the last (e.g. `"unit;datastructures;python"`).
     131             : All the test cases should be in a single class so that the python unit testing
     132             : framework will run all test functions on a single invocation to avoid startup
     133             : cost.
     134             : 
     135             : Below is an example of registering a python test file for bindings:
     136             : 
     137             : \snippet tests/Unit/DataStructures/CMakeLists.txt example_add_pybindings_test
     138             : 
     139             : Python code that does not use bindings must also be tested. You can register the
     140             : test file using the `spectre_add_python_test` CMake function with the same
     141             : signature as shown above.
     142             : 
     143             : ## Using The Bindings
     144             : 
     145             : See \ref spectre_using_python "Using SpECTRE's Python"
     146             : 
     147             : ## Notes:
     148             : 
     149             : - All python libraries are dynamic/shared libraries.
     150             : - Exceptions should be allowed to propagate through the bindings so that
     151             :   error handling via exceptions is possible from python rather than having the
     152             :   python interpreter being killed with a call to `abort`.
     153             : - All function arguments in Python bindings should be named using `py::arg`.
     154             :   See the Python bindings in `IO/H5/` for examples. Using the named arguments in
     155             :   Python code is optional, but preferred when it makes code more readable.
     156             :   In particular, use the argument names in the tests for the Python bindings so
     157             :   they are being tested as well.
     158             : 
     159             : ## Guidelines for writing command-line interfaces (CLIs)
     160             : 
     161             : - List all CLI endpoints in `support/Python/__main__.py`.
     162             : - Follow the recommendations in the
     163             :   [click](https://click.palletsprojects.com/en/8.1.x/) documentation.
     164             : - Split your code into free functions that know nothing about the CLI and can
     165             :   just as well be called independently from Python, and the CLI commands that
     166             :   call the functions. Test both.
     167             : - Take only input files that the script operates on as positional arguments
     168             :   (like H5 data files or YAML input files) and everything else as options.
     169             : - Choose option names and shorthands consistent with other CLI endpoints in the
     170             :   repository. For example, H5 subfile names are specified with '--subfile-name'
     171             :   / '-d' and output files are specified with '--output' / '-o'. Look at other
     172             :   CLI endpoints before making choices for option names.
     173             : - Never read or write files to or from "default" locations. Instead, take all
     174             :   input files as arguments and write all output files to locations specified
     175             :   explicitly by the user. This is important so users are not afraid of moving
     176             :   and renaming files, and are not left wondering where the script wrote its
     177             :   output. Examples:
     178             :   - Don't try to read a file like "spectre.out" from the current directory just
     179             :     because it might be there by convention. Instead, add an argument or option
     180             :     like `--out-filename` so the user can specify it.
     181             :   - Don't write a file like "plot.pdf" to the current directory without telling
     182             :     the user. Instead, add an option like `--output` / `-o` for the user to
     183             :     specify explicitly so they know exactly where output is written to.
     184             : - Operate on files instead of directories when possible. For example, prefer
     185             :   taking many H5 volume data files as arguments instead of the directory that
     186             :   contains them. This helps with operating on H5 files in segments or other
     187             :   subdirectory structures. Passing many files to a script is easy for the user
     188             :   by using a glob (note: don't take the glob as a string argument, take the
     189             :   expanded list of files directly using `click.argument(..., nargs=-1,
     190             :   type=click.Path(...))`).
     191             : - Never overwrite or delete files without prompting the user or asking them to
     192             :   run with `--force`.
     193             : - When the input to a script is empty, [gracefully degrade to a
     194             :   noop](https://click.palletsprojects.com/en/8.1.x/arguments/#variadic-arguments).
     195             : - When the user did not specify an option, print possible values for it and
     196             :   return instead of raising an exception. For example, print the subfile names
     197             :   in an H5 file if no subfile name was specified. This allows the user to make
     198             :   selections incrementally.
     199             : - When the user did not specify an output file, write the output to `sys.stdout`
     200             :   if possible instead of raising an exception. This allows the user to use pipes
     201             :   and chain commands if they want, or add a quick `-o` option to write to a
     202             :   file.
     203             : - Always use Python's `logging` module over plain `print` statements. This
     204             :   allows the user to control the verbosity.

Generated by: LCOV version 1.14