Skip to content

Latest commit

 

History

History
482 lines (385 loc) · 23.2 KB

APE17.rst

File metadata and controls

482 lines (385 loc) · 23.2 KB

A roadmap for package infrastructure without astropy-helpers

author: Stuart Mumford, Thomas Robitaille

date-created: 2019 March 7

date-last-revised: 2019 March 7

type: Process

status: Discussion

Abstract

The astropy-helpers package contains a collection of utilities intended to help build and distribute the core astropy package, as well as coordinated and affiliated packages in the Astropy ecosystem, and it is also used by packages outside of the ecosystem (at the time of writing, almost 400 repositories on GitHub appear to use astropy-helpers).

The original APE proposing the creation of astropy-helpers (APE 4) was accepted in June 2014. Since then, the state of the Python packaging ecosystem has changed significantly, with advances such as binary wheels that can be provided for all major platforms, tools such as tox and setuptools_scm, and more recently support for PEP 517 and PEP 518 (in pip 19.0 or later), and many incremental updates and new features to setuptools (in setuptools 30.3 or later). The purpose of this APE is to show how by relying on these tools, we can significantly simplify the packaging for astropy and all the affiliated packages and other packages using astropy-helpers, and remove the need for astropy-helpers altogether in pure-Python packages.

Detailed description

We now go through the various aspects of package infrastructure that astropy-helpers has solved in the past and how we can now address these instead using standard packaging tools.

Changes common to all packages

We first take a look at what changes are needed for all packages, regardless of whether they have compiled extensions or not. For pure-Python packages, if all these changes are made, astropy-helpers is no longer needed and can be removed from the repository.

Running tests

Astropy-helpers provides the ability for anyone to run tests in the source distribution to do this with the:

python setup.py test

command, which behind the scenes built the package, installed it into a temporary directory, installed pytest-astropy if needed, and ran the tests there.

However, this can now be done more cleanly using tox. Tox is a tool that makes is easy to install packages into a temporary clean Python environment along with the dependencies declared in setup.cfg, and run a set of custom commands - in a sense it does what the test command from astropy-helpers did, but in a more general way (including the use of an isolated test environment). For example, by putting the following into tox.ini:

[testenv:test]
deps =
  pytest-astropy
commands =
  pytest aplpy

developers will then easily be able to run the tests with:

tox -e test

The benefit of using tox over just running pytest directly is that since behind the scenes it creates a source distribution of the package and installs it into a clean environment, it ensures that source distributions for the package are fully functional and that all required files and dependencies are declared. For packages with C/Cython extensions it should also guarantee that they are always rebuilt before running the tests.

However, for pure-Python packages, developers can also opt to simply run the tests directly with pytest, e.g.:

pytest packagename

so using tox is optional.

We note that these changes have no impact on the availability of the package.test() function which is unrelated to astropy-helpers and relies instead on the astropy core package to provide a test runner.

Building documentation

Astropy-helpers provides a python setup.py build_docs command that behind-the-scenes built the package then added it to sys.path, then ran the documentation build. Having the package be importable is needed as documentation often includes API sections that are dynamically created based on the package.

As for testing, we can accomplish the same process more cleanly instead using tox - a minimal tox configuration might look like:

[testenv:build_docs]
deps =
  sphinx-astropy
commands =
  sphinx-build docs docs/_build/html -W -b html

and this would be run using:

tox -e build_docs

Developers not wishing to use tox could also accomplish the same by doing:

pip install -e .
cd docs
make html

These changes will have no impact on ReadTheDocs as that service never made use of the build_docs command, instead invoking sphinx directly with sphinx-build.

Version helpers

Currently, astropy-helpers includes version helpers that take the version defined in setup.cfg or setup.py and add a developer string when in a checked out version of a git repository. The developer string consists of version.devN where N is the number of commits since a release.

The Python Packaging Authority (PyPA) now provide a setuptools extension called setuptools_scm which can entirely replace the version helpers in astropy-helpers. The way this package works is that versions are no longer specified in setup.cfg or setup.py - instead the versions are taken from tags. Developer version strings produced in this way are much more sophisticated and can indicate for example if the working copy is clean or has local changes, and whether it is a stable tagged version or a developer version. We note that the default way of setting up setuptools_scm results in the version string not being updated automatically in ‘editable’ installs of a package (i.e. pip install -e .), however a workaround is available.

Switching to setuptools_scm requires minimal configuration, which is well described in its documentation and we therefore do not repeat here. However, one subtlety is that since it relies on tags to determine versions, for packages such as the core astropy package which have all their tags on branches (at least in recent years), we will need to add a ‘developer’ tag on master straight after branching, e.g. after creating a v4.0.x branch we should tag the next commit on master as v4.1.dev.

Package data and entry points

At the moment, package data and entry points for packages can be defined via get_package_data and get_entry_points in setup_package.py files. However, this adds unnecessary complexity, as even for the core package it is simple to define the data and entry points in setup.cfg using the [options.entry_points] and [options.package_data] sections, e.g.:

[options.entry_points]
console_scripts =
    fits2bitmap = astropy.visualization.scripts.fits2bitmap:main
...

[options.package_data]
astropy = astropy.cfg, CITATION, **/data/**/*
...

Removing the ability to specify package data in setup_package.py files removes the dependence of the setup.py sdist command on astropy-helpers, which is essential to using the PEP 518 build time dependencies discussed below.

Using setup.cfg to define package data and other options does rely on setuptools 30.3 or later, which is now over two years old. Nevertheless, to minimize issues for users with older Python installations, we recommend including a version check for setuptools inside the setup.py file.

Removal of pre/post-processing hooks

A little-known feature of astropy-helpers is the ability to define hooks in setup_package.py files for steps to be carried out before/after specific setup.py commands. We propose removing this functionality since this feature was never well advertised and likely only used in the core astropy package, and our proof-of-concept implementation in the core package shows that we can easily remove this.

Changes for packages with compiled extensions

Build-time dependencies

One of the common issues that packages have regularly run into and which PEP 518 solves is how to define dependencies required for building a package. The setup() function provided by setuptools takes a setup_requires argument which can include dependencies for the build, but unfortunately the setup.py file has to be executed before the setup() function is run, which leads to the circular issue that any code in setup.py can’t rely on packages specified in setup_requires - thus, setup_requires does not properly solve the issue of build-time dependencies since those dependencies are only installed part way through the build process.

PEP 518 instead specifies that build-time dependencies can be specified in a file called pyproject.toml that contains for example:

[build-system]
requires = ["setuptools", "wheel", "numpy==1.13.3"]

Provided that the package is installed with a tool such as `pip`_ which understands this file, the build-time dependencies will be installed before the setup.py file is executed. PEP 517 takes this concept further by specifying that the build should happen in an isolated environment, which means that one could specify a pinned version of cython or jinja2 to use even if a different version is installed in the user’s environment.

With this in mind, if astropy-helpers was still needed, it could therefore now be included as a build-time dependency in pyproject.toml which removes the need for the ah_bootstrap.py file and the git submodule. Furthermore, astropy-helpers could be pinned to specific versions in pyproject.toml and different packages could use different versions (thanks to the build isolation, this will not be a problem).

For packages that need Numpy to be built, numpy should also be included in the list of build-time dependencies in pyproject.toml. Currently, when defining C extensions that need to use numpy, we need to add numpy.get_include() to the include_dirs argument of Extension. However, this can’t be done until numpy is installed, so astropy-helpers currently provides a way for packages to specify the string ‘numpy’ instead, and replacing it with numpy.get_include() on-the-fly. This workaround will no longer be needed once Numpy is specified in pyproject.toml since Numpy will be installed before the extensions are defined, and we suggest that packages should instead specify numpy.get_include() explicitly.

Note that Numpy should be pinned to the oldest compatible version in the pyproject.toml file - this is because if a user does pip install astropy numpy==1.14.2, the pinning of Numpy only applies to the version installed after astropy has been built, and the version taken to build astropy is taken from the pyproject.toml file. This means that packages such as astropy have to be built with the oldest compatible version of Numpy since the build will then be forward-compatible with any later version of Numpy (this is similar to the approach taken for conda packages). In addition, the oldest version should be that for which wheels are available so for packages where this depends on Python version, environment markers can be used (see PEP 508), e.g.:

"numpy==1.13.1; python_version<'3.7'",
"numpy==1.14.5; python_version>='3.7'",

A side benefit of this is that with these pinnings in place, building wheels with pip wheel . will automatically create wheels compatible with all available versions of Numpy.

Note that setup_requires should no longer be used in setup.py/setup.cfg, and install_requires should require numpy to be >= than the oldest version mentioned in pyproject.toml.

Cython extensions

Currently, astropy-helpers includes functionality to auto-generate C code from Cython extensions and ensure that when packages are released, the C code is the one used to compile extensions, even if Cython is installed. This was originally done to make sure that Cython was not required to install astropy, and to avoid issues due to differences in the generated C code from different Cython versions. When releasing stable versions of the core package for example, developers had to remember to run the build command before sdist to include the generated C code, otherwise this would cause issues for users that didn’t have Cython.

However, Cython should now be included as a build-time dependency in pyproject.toml and developers should not include generated C code in released packages. Build-time dependencies in pyproject.toml files are always installed from wheels, so this would not have a significant performance impact for source distributions - and since most users installing astropy with pip will be installing astropy wheels, this will have no impact for most users. Note that if needed, we can even pin the Cython version in pyproject.toml to ensure consistency across all builds. With this in place, the custom build_ext command in astropy-helpers can be removed.

Extensions and external libraries

Astropy-helpers provides a way for developers to use setup_package.py files throughout a package to define extensions and definitions for external libraries. This is one of the only parts of astropy-helpers which we think it makes sense to preserve, and we argue that it is so general that it should be released as a package with a more generic name than astropy-helpers, such as extension-helpers - this will allow us to also avoid breaking astropy-helpers and instead starting fresh with a clean package (although the git history could be preserved).

However, we note that for small packages, developers can also simply define extensions inside setup.py, which would mean that astropy-helpers (or extension-helpers) would not needed for these packages either.

Branches and pull requests

Implementation

All the changes to packages are already described above, but to summarize, the following table shows the correspondence between old astropy-helpers features and their proposed replacement:

Feature Replacement
astropy.version_helpers setuptools_scm
Package data in specification in setup_package.py files All package data should be specified in setup.cfg
python setup.py test tox or direct use of pytest
python setup.py build_docs tox or make html or sphinx-build
Delayed import of Numpy when building C extensions Replaced by build dependencies and isolation in PEP 517 and PEP 518
Transpilation of Cython to C before sdist
Package specific versions of astropy-helpers provided by git submodule

We propose that a new package called extension-helpers be created starting from astropy-helpers but with only the minimal amount of functionality needed to handle the definition of compiled extensions, external libraries, and the auto-discovery of Cython extensions. This package would need to be declared as a build-time dependency in pyproject.toml.

In addition to the changes described here, we also recommend moving all or as many as possible of the options for the setup() function in setup.py to the setup.cfg file as described in the setuptools documentation.

Timeline for changes

Many of the changes described here can already be safely made now. The only change that requires some consideration in terms of timeline is the use of pyproject.toml (which is only needed for package with compiled extensions). Full support for this file, including the environment markers (which allows different Numpy dependencies for different Python versions for example) and the build isolation, only became available in pip 19.0 onwards.

Therefore, we recommend that Pure-python packages can make all the changes described here now. On the other hand, packages that need to rely on pyproject.toml for building C extensions should make the switch at the time they are happy to rely on pip 19.0 (released 2019-01-22) for installs from source distributions, but they can still make other changes now, e.g. using setuptools_scm or making a greater use of setup.cfg.

We note however that provided that wheels are available for a package with compiled extensions, it may be acceptable to transition to using pyproject.toml sooner rather than later because pip 19.0 would only be required for source installs, but since most users that pip install would be using wheels, this may not be an issue.

Impact for users

If done properly, these changes should have no noticeable impact for users. Users will still be able to pip install (or conda install when available) packages and for large packages such as the core astropy package, which contains a lot of compiled extensions, most users will not see any difference since they will be installing astropy from pre-built packages (wheels or conda packages). Running tests using package.test() will still be supported.

Impact for non-core developers

The immediate impact for developers of packages that use astropy-helpers is having to update the layout and infrastructure in their packages to follow the new guidelines presented here. However, we emphasise that these changes are recommendations and not mandatory, and astropy-helpers will continue to work as expected as long as it is included as a submodule. However, we recommend that astropy-helpers no longer be actively developed, in which case guarantees could not be made that some aspects of the astropy-helpers infrastructure will not break with future releases of Python, setuptools, or sphinx for example.

However, we believe that the initial effort to switch over to the new guidelines will be a worthwhile investment - in all cases it will mean being able to get rid of astropy-helpers as a submodule and the confusion and headaches this can cause, and it will make for example the definition of package data much simpler and not have to worry about setup_package.py files in most cases.

The astropy package-template will be updated to reflect the latest recommendations, which will make it easier for developers to update their packages.

Impact for contributors

Users who wish to contribute fixes to the core astropy package or other packages will be encouraged to have tox installed if they want to easily run tests or build documentation locally. However, this is an easy package to install with `pip`_ and we could also add code in setup.py so that running e.g. python setup.py test or python setup.py build_docs gives a helpful error message with instructions on updating and using tox to run tests and build the documentation.

We note however that using tox is just a convenience and will not be compulsory, especially for pure-Python packages where running pytest directly will work.

Impact on core astropy developers

One of the main benefits of these changes will be to not have to maintain astropy-helpers any longer. Some parts of astropy-helpers have relied on hacks that can be brittle and break with new setuptools or Sphinx releases. Maintenance will still be needed for the proposed extension-helpers package but this will be a much smaller package than astropy-helpers, and by making it more generic and usable by any package, we hope to attract contributions from beyond the Astropy team.

Impact on package managers

By relying on standard packaging infrastructure, this should facilitate the job of people involved in package managers - for example, the conda-forge infrastructure currently recommends that recipes should use pip to build packages, but this is not possible or easy at the moment for packages using astropy-helpers since they need to pass custom flags to setup.py to prevent the default behavior of checking for astropy-helpers releases online.

Backward compatibility

The changes described here are opt-in, so barring any breaking changes in Python, setuptools, or Sphinx, everything should continue to work as expected if developers do not make any changes.

Alternatives

In the words of Stuart: Do nothing, suffer submodules and maintaining the spaghetti code of astropy-helpers until we demoralise the whole community and Julia takes over.

Decision rationale

<To be filled in by the coordinating committee when the APE is accepted or rejected>