Examples
In this section we walk through two simple examples, one a standalone project and the other a pair of package projects. These will introduce the basic features of SIP. Other sections of this documentation will contain complete descriptions of all available features.
A Standalone Project
This project implements a module called fib
that contains a function
fib_n()
which takes a single integer argument n
and returns the
n’th value of the Fibonacci series.
Note that the example does not wrap a separate C/C++ library that implements
fib_n()
. Instead it provides the C implementation within the
.sip
specification file itself. While this is not the way SIP is
normally used it means that the example is entirely self contained. Later in
this section we will describe the changes to the project that would be needed
if a separate library was being wrapped.
First of all is the project’s pyproject.toml
file (downloadable from
here
) which we show in its
entirety below.
# Specify sip v6 as the build system for the package.
[build-system]
requires = ["sip >=6, <7"]
build-backend = "sipbuild.api"
# Specify the PEP 621 metadata for the project.
[project]
name = "fib"
The file is in TOML format and the structure is defined in PEP 518.
The [build-system]
section is used by build frontends to determine what
version of what build backend is to be used. pip, for example, will
download, install and invoke an appropriate version automatically.
The [project]
section specified the name of the project (as it would appear
on PyPI). This is the minimum information needed to build a standalone
project.
Next is the module’s .sip
specification file (downloadable from
here
) which we also show in its
entirety below.
// Define the SIP wrapper to the (theoretical) fib library.
%Module(name=fib, language="C")
int fib_n(int n);
%MethodCode
if (a0 <= 0)
{
sipRes = 0;
}
else
{
int a = 0, b = 1, c, i;
for (i = 2; i <= a0; i++)
{
c = a + b;
a = b;
b = c;
}
sipRes = b;
}
%End
The first line of interest is the %Module
directive. This defines
the name of the extension module that will be created. In the case of
standalone projects this would normally be the same as the name defined in the
pyproject.toml
file. It also specifies that the code being wrapped is
implemented in C (as opposed to C++).
The next line of interest is the declaration of the fib_n()
function to
be wrapped.
The remainder of the file is the %MethodCode
directive attached to
the function declaration. This is used to provide the actual implementation of
the fib_n()
function and would not be needed if we were wrapping a
separate library. In this code a0
is the value of the first argument
passed to the function and converted from a Python int
object, and
sipRes
is the value that will be converted to a Python int
object and
returned by the function.
The project may be built and installed by running:
sip-install
It may also be built and installed by running:
pip install .
An sdist (to be installed by pip) may be created for the project by running:
sip-sdist
A wheel (to be installed by pip) may be created for the project by running:
sip-wheel
An installed project may be uninstalled by running:
pip uninstall fib
Using a Real Library
If there was a real fib
library to be wrapped, with a corresponding
fib.h
header file, then the pyproject.toml
file would look more
like that shown below.
# Specify sip v6 as the build system for the package.
[build-system]
requires = ["sip >=6, <7"]
build-backend = "sipbuild.api"
# Specify the PEP 621 metadata for the project.
[project]
name = "fib"
# Configure the building of the fib bindings.
[tool.sip.bindings.fib]
headers = ["fib.h"]
include-dirs = ["/path/to/headers"]
libraries = ["fib"]
library-dirs = ["/path/to/libraries"]
The include-dirs
and library-dirs
would only need to be specified if
they are not installed in standard locations and found automatically by the
compiler.
Note that POSIX path separators are used. SIP will automatically convert these to native path separators when required.
The .sip
file would look more like that shown below.
// Define the SIP wrapper to the (actual) fib library.
%Module(name=fib, language="C")
%ModuleCode
#include <fib.h>
%End
int fib_n(int n);
The %MethodCode
directive has been removed and the
%ModuleCode
directive has been added.
Configuring a Build
How should a project deal with the situation where, for example, the fib
library has been installed in a non-standard location? There are a couple of
possible approaches:
the user modifies
pyproject.toml
to set the values ofinclude-dirs
andlibrary-dirs
appropriatelythe project provides command line options to SIP’s build tools (i.e. sip-build, sip-install and sip-wheel) that allows the user to specify the locations.
The first approach, while not particularly user friendly, is legitimate so long as you document it. However note that it cannot work when building and installing directly from an sdist because pip does not currently fully implement PEP 517.
The second approach is the most flexible but requires code to implement it. If
SIP finds a file called (by default) project.py
in the same directory
as pyproject.toml
then it is assumed to be an extension to the build
system. Specifically it is expected to implement a class that is a sub-class
of SIP’s AbstractProject
class.
Below is a complete project.py
that adds options to allow the user to
specify the locations of the fib
header file and library.
import os
from sipbuild import Option, Project
class FibProject(Project):
""" A project that adds an additional configuration options to specify
the locations of the fib header file and library.
"""
def get_options(self):
""" Return the sequence of configurable options. """
# Get the standard options.
options = super().get_options()
# Add our new options.
inc_dir_option = Option('fib_include_dir',
help="the directory containing fib.h", metavar="DIR")
options.append(inc_dir_option)
lib_dir_option = Option('fib_library_dir',
help="the directory containing the fib library",
metavar="DIR")
options.append(lib_dir_option)
return options
def apply_user_defaults(self, tool):
""" Apply any user defaults. """
# Ensure any user supplied include directory is an absolute path.
if self.fib_include_dir is not None:
self.fib_include_dir = os.path.abspath(self.fib_include_dir)
# Ensure any user supplied library directory is an absolute path.
if self.fib_library_dir is not None:
self.fib_library_dir = os.path.abspath(self.fib_library_dir)
# Apply the defaults for the standard options.
super().apply_user_defaults(tool)
def update(self, tool):
""" Update the project configuration. """
# Get the fib bindings object.
fib_bindings = self.bindings['fib']
# Use any user supplied include directory.
if self.fib_include_dir is not None:
fib_bindings.include_dirs = [self.fib_include_dir]
# Use any user supplied library directory.
if self.fib_library_dir is not None:
fib_bindings.library_dirs = [self.fib_library_dir]
The get_options()
method is reimplemented to add two
new Option
instances. An Option
defines
a key that can be used in pyproject.toml
. Because these
Option
s are defined as part of the
Project
then the keys are used in the [tool.sip.project]
section of pyproject.toml
. In addition, because each
Option
has help text, these are defined as user options
and therefore are also added as command line options to each of SIP’s build
tools. Note that in both pyproject.toml
and the command line any
_
in the Option
name is converted to -
.
The apply_user_defaults()
method is reimplemented to
provide a default value for an Option
. Note that the value
is accessed as an instance attribute of the object for which the
Option
is defined. In this case there are no default values
but we want to make sure that any values that are provided are absolute path
names.
The update()
method is reimplemented to update the
Bindings
object for the fib
bindings with any values
provided by the user from the command line.
Package Projects
We now describe two package projects. The examples-core
project contains a
single set of bindings called core
. The examples-extras
project contains a single set of bindings called extras
. The
extras
module imports the core
module. Both
modules are part of the top-level examples
package. The
sip
module required by all related package projects is also
part of the top-level examples
package.
Again with this example, in order to make it self-contained, we are not
creating bindings for real libraries but instead embedding the implementation
within the .sip
files.
examples.sip
In order to create an sdist for the sip
module, run:
sip-module --sdist examples.sip
If you want to create a wheel from the sdist then run:
pip wheel examples_sip-X.Y.Z.tar.gz
X.Y.Z
is the version number of the ABI implemented by the
sip
module and it will default to the latest version.
examples.core
We now look at the pyproject.toml
file for the examples-core
project (downloadable from
here
) below.
# Specify sip v6 as the build system for the package.
[build-system]
requires = ["sip >=6, <7"]
build-backend = "sipbuild.api"
# Specify the PEP 621 metadata for the project.
[project]
name = "examples-core"
# Specify each set of bindings.
[tool.sip.bindings.core]
# Configure the project itself.
[tool.sip.project]
sip-module = "examples.sip"
dunder-init = true
Compared to the standalone project’s version of the file we have added the
[tool.sip.bindings.core]
section to specify that the project contains a
single set of bindings called core
. We need to do this
because the name is no longer the same as the name of the project itself as
defined by the name
key of the [project]
section. The bindings section
is empty because all the default values are appropriate in this case.
We have also added the [tool.sip.project]
section containing the
sip-module
key, which specifies the full package name of the
sip
module and the dunder-init
key, which specifies that
an __init__.py
file (empty by default) is created in the top-level
examples
package.
We next look at the core.sip
file (downloadable from
here
) below.
// Define the SIP wrapper to the core library.
%Module(name=examples.core, use_limited_api=True)
%DefaultEncoding "ASCII"
%Platforms {Linux macOS Windows}
%If (Linux)
const char *what_am_i();
%MethodCode
sipRes = "Linux";
%End
%End
%If (macOS)
const char *what_am_i();
%MethodCode
sipRes = "macOS";
%End
%End
%If (Windows)
const char *what_am_i();
%MethodCode
sipRes = "Windows";
%End
%End
The %Module
directive, as well as specifying the full package name
of the core
module, specifies that the bindings will use the
PEP 384 stable ABI.
The %DefaultEncoding
directive specifies that any character
conversions between C/C++ and Python str
objects will default to the ASCII
codec.
The %Platforms
directive defines three mutually exclusive tags
that can be used by %If
directives to select platform-specific
parts of the bindings.
The remaining parts of the file are three different platform-specific
implementations of a function called what_am_i()
which just returns a
name for the platform.
We are then left will the question as to how we specify which of the platform
tags should be selected for a particular build. For this we need the
project.py
file file (downloadable from
here
) shown below.
from sipbuild import Option, Project, PyProjectOptionException
class CoreProject(Project):
""" A project that adds an additional configuration option and introspects
the system to determine its value.
"""
def get_options(self):
""" Return the sequence of configurable options. """
# Get the standard options.
options = super().get_options()
# Add our new option.
options.append(Option('platform'))
return options
def apply_nonuser_defaults(self, tool):
""" Apply any non-user defaults. """
if self.platform is None:
# The option wasn't specified in pyproject.toml so we introspect
# the system.
from sys import platform
if platform == 'linux':
self.platform = 'Linux'
elif platform == 'darwin':
self.platform = 'macOS'
elif platform == 'win32':
self.platform = 'Windows'
else:
raise PyProjectOptionException('platform',
"the '{0}' platform is not supported".format(platform))
else:
# The option was set in pyproject.toml so we just verify the value.
if self.platform not in ('Linux', 'macOS', 'Windows'):
raise PyProjectOptionException('platform',
"'{0}' is not a valid platform".format(self.platform))
# Apply the defaults for the standard options.
super().apply_nonuser_defaults(tool)
def update(self, tool):
""" Update the project configuration. """
# Get the 'core' bindings and add the platform to the list of tags.
core_bindings = self.bindings['core']
core_bindings.tags.append(self.platform)
As before we reimplement the get_options()
method to
add a new Option
instance to specify the platform tag.
Because no help text has been specified the Option
can only
be used as a key in the [tool.sip.project]
section of
pyproject.toml
and will not be added to the command line options of the
build tools.
We reimplement the apply_nonuser_defaults()
method
provide a default value for the new Option
if it hasn’t been
specified in pyproject.toml
. If it has been specified in
pyproject.toml
then we validate it. (Why would be want to specify the
platform in pyproject.toml
? Perhaps we are cross-compiling and
introspecting the host platform would be inappropriate.)
The update()
method is reimplemented to update the
Bindings
object for the core
bindings with the validated
platform tag.
examples.extras
We now look at the pyproject.toml
file for the example-extras
project (downloadable from
here
) below.
# Specify sip v6 as the build system for the package.
[build-system]
requires = ["sip >=6, <7"]
build-backend = "sipbuild.api"
# Specify the PEP 621 metadata for the project.
[project]
name = "examples-extras"
dependencies = ["examples-core"]
# Specify each set of bindings.
[tool.sip.bindings.extras]
# Configure the project itself.
[tool.sip.project]
sip-module = "examples.sip"
Compared to the examples-core
project’s version of the file we have added
the dependencies
key to the [project]
section which will ensure that
the examples-core
project will be automatically installed as a prerequisite
of the examples-extras
project. SIP will automatically add a similar line
to ensure the sip
module is also installed.
Of course we have specifed an appropriately named bindings section.
We have also removed the dunder-init
key from the
[tool.sip.project.section]
section.
We next look at the extras.sip
file (downloadable from
here
) below.
// Define the SIP wrapper to the extras library.
%Module(name=examples.extras, use_limited_api=True)
%Import core/core.sip
%If (!Windows)
bool am_i_posix();
%MethodCode
sipRes = true;
%End
%End
%If (Windows)
bool am_i_posix();
%MethodCode
sipRes = false;
%End
%End
This is very similar to the core
module in that it implements
simple platform-specific functions. The key thing to notice is that there is
no need to specify the platform tag as part of the configuration as it is
obtained automatically from the installed examples-core
project.
The examples-extras
project has no need for a project.py
file.