Packaging with setuptools¶
Tools for integrating with setuptools.
Integration with setuptools
is a somewhat messy affair. By putting a
cslug.CSlug
in a package, we now have to coerce our package setup to do
the following 5 things:
(Re)build all
cslug.CSlug
s onsetup.py build
.Include C source code in sdists but exclude them from wheels.
Include cslug type jsons and binariesNick-name for shared library. of the correct OS in wheels but exclude them from sdists.
Mark cslug as a build-time dependency to be installed before trying to run
setup.py build
.Mark wheels as platform dependent but Python version independent.
Quick setup¶
For the short way to setup your package you may use the following recipe (note all steps are mandatory). Some of these steps are a little drastic so I recommend you make a backup, or a git commit, so you can easily fix any mess you make:
Copy this MANIFEST.in into the root of your project (or merge with yours if you already have one).
Similarly, copy/merge this pyproject.toml.
Add all your dependencies to the [build-system] requires list in the pyproject.toml (keep the current contents of that list). If you already specify your dependencies elsewhere remove them.
Copy the following lines of setup.py into your setup.py (replacing
contains_slugs
with the name of your package).
from cslug.building import (build_slugs, bdist_wheel, CSLUG_SUFFIX,
copy_requirements)
install_requires=copy_requirements(),
package_data={
"contains_slugs": ["*" + CSLUG_SUFFIX, "*.json"],
},
cmdclass={
"build": build_slugs("contains_slugs:deep_thought"),
"bdist_wheel": bdist_wheel,
},
)
Edit the
"build": build_slugs("contains_slugs:deep_thought"),
line in the setup.py, replace"contains_slugs:deep_thought"
with the names of everycslug.CSlug
in your package (seemake()
for the name format).
For the why and the how to customise it, see the rest of this page.
contains-slugs¶
Throughout this document we'll be using the contains_slugs
package as an
example.
You can inspect its contents with the links listed below or download the full
project here
.
We'll start with the setup.py. Using a setuptools setup.py is currently the only supported way to use cslug. There is no way to use purely a setup.cfg or pyproject.toml or any of the alternatives to setuptools.
Apart from the cslug imports, the following lines are just generic setup.py and should already be familiar to you. (If it isn't, check out the Python packaging tutorial.)
from setuptools import setup, find_packages
from cslug.building import (build_slugs, bdist_wheel, CSLUG_SUFFIX,
copy_requirements)
setup(
name="contains_slugs",
version="1.0.0",
author="Mr Slug",
author_email="sluggy989@seaslugmail.com",
url="www.toomanyslugs.com",
packages=find_packages(),
Compile on setup.py build
.¶
Whilst you rarely use it directly, setup.py build
is called implicitly
whenever you:
pip install .
but notpip install -e .
pip wheel .
but notpython setup.py sdist
.pip install /path/to/source/distribution.tar.gz
but notpip install /path/to/binary/distribution.whl
.
To make package building as streamlined as possible,
hook setup.py build
to also call cslug.CSlug.make()
on every
cslug.CSlug
.
Calling cslug.building.make()
is a convenient way to call make throughout
a project
but to attach it to setup.py build
requires overloading the
run()
method of distutils.command.build.build
.
cslug.building.build_slugs()
does exactly this.
It creates a subclass of build
, prepending make()
to
the run()
method.
So finally, to create and register this custom build class, add the following option:
setup(
...
cmdclass={
"build": build_slugs("contains_slugs:deep_thought"),
},
)
See make()
for how the argument(s) to
build_slugs
should be formatted.
With this set up, you can now (re)compile all cslug components in a project with:
python setup.py build
Specify build-time dependencies¶
By putting a cslug import in our setup.py we've made cslug a build time dependency. And, because you need to import your project just to compile any cslug components, most or all of your run-time dependencies are now also build time dependencies.
The ability to specify build time dependencies is introduced by PEP 518. You require a pyproject.toml (a setup.cfg is not accepted).
If you don't like that your requirements are duplicated you may use
copy_requirements()
to extract your build time
requirements and pass them to the install_requires
option in your
setup.py:
from cslug.building import copy_requirements
setup(
...
install_requires=copy_requirements(),
...
)
If you have build time requirements that aren't runtime requirements you can exclude them with:
install_requires=copy_requirements(exclude=["build-time", "only", "packages"]),
setuptools, wheel and toml are excluded. If you want to include them or have other runtime-only dependencies then append them:
install_requires=copy_requirements() + ["toml", "some-other-library"],
Warning
pip builds packages in an isolated environment which ignores your currently
installed packages. If you forget a build requirement in the toml file, but
you will still a ModuleNotFoundError
even if have it installed
anyway.
Note
If you find the isolated build environment is maddeningly slow you can skip
it in pip using the --no-build-isolation
.
But only once your sure it works without it.
When the build dependencies get noticed¶
Build dependencies, being new, has a few holes in it. They are ignored (usually
resulting in ModuleNotFoundError
s) if you use:
python setup.py [bdist_wheel or install or anything else]
Using pip finds the build dependencies correctly so the solution is to the above is to:
pip install .
Or manually install the build dependencies before invoking any setup.py
commands.
You also require modern versions of pip, setuptools and wheel. If you get
ModuleNotFoundError
s for packages which are in your toml, trying
upgrading those.
For sdists to work the pyproject.toml must be included in the sdist. See Data files for source distributions (sdists).
Data files for source distributions (sdists)¶
An sdist should include source code but exclude anything cslug-generated. It's also crucial that pyproject.toml is included or build time dependencies don't get propagated.
This all happens in the MANIFEST.in. You can just copy/paste this file directly into your project's root:
recursive-include * *.c *.h
recursive-exclude * *.dll *.so *.dylib
recursive-exclude * *.json
include pyproject.toml
Data files for binary distributions (wheels)¶
A binary distribution is already compiled meaning that it doesn't need source code anymore. But it does need the compiled binariesNick-name for shared library. and type jsons.
We're back to the setup.py again, this time using the package_data
argument.
from cslug.building import CSLUG_SUFFIX
setup(
...,
include_package_data=False,
package_data={
"contains_slugs": ["*" + CSLUG_SUFFIX, "*.json"],
},
)
Make sure that you do not use include_package_data=True
. Using it causes
all files to be collected, including source and junk files, rather than only
those which are appropriate.
Note
The above filter would prevent binaries for the wrong platform from being
collected but for some unfortunate caching. If you compile for multiple
platforms which share a filesystem, run python setup.py clean
between
each switch.
This problem is rectified in v0.4.0.
Platform specific wheel tag¶
Because a cslug package contains binariesNick-name for shared library. but those binaries do not use the
Python ABI (i.e. don't include the line #include <Python.h>
anywhere in the
C code), a package is platform specific but independent of Python/ABI version.
i.e. You can't compile on Linux and run on Windows but you can compile using
Python 3.8 and run on Python 3.6.
We need to tell setuptools this, otherwise it incorrectly assumes that our
packages are Pure Python which would eventually lead to pip installing binaries
for the wrong operating system.
Unfortunately setuptools is heavily geared towards Python extension modules
exclusively and this is surprisingly fiddly.
But cslug.building.bdist_wheel
wraps it. Just add it as another
command class in your setup.py:
from cslug.building import bdist_wheel
setup(
...
cmdclass={
"bdist_wheel": bdist_wheel,
},
)
Now on running:
pip wheel --no-deps .
you should notice that the wheel produced has the name of your operating system in its filename.