Skip to content

Commit

Permalink
Add wheel with a C extension to test mounting
Browse files Browse the repository at this point in the history
This recreates the source to the `tests/minimext*.whl` wheels
with a couple of differences:
* The C extension is placed in the minimext package rather than being
  a top level package.
* A Python implementation of the C extension is provided to make it
  easier for pure Python programmers to understand / modify.

This also adds a script `build_wheels.sh` that will install Python
interpreters and build the minimext wheel for different Python
versions with and without adding the `EXTENSIONS` metadata file.

Related to pypa#222
  • Loading branch information
stewartmiles committed Sep 20, 2024
1 parent 888c48b commit 4a67db8
Show file tree
Hide file tree
Showing 8 changed files with 304 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ distlib-vcsid
tests/passwords
tests/keys
tests/pypi-server-standalone.py
tests/minimext/*.egg-info
tests/minimext/.venv*
tests/minimext/minimext/*.pyd
tests/minimext/minimext/*.so
tests/minimext/wheels
dist
htmlcov
build
Expand Down
1 change: 1 addition & 0 deletions tests/minimext/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
BSD-licensed.
1 change: 1 addition & 0 deletions tests/minimext/README.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Distribution with a simple C extension that calculates Fibonacci numbers.
117 changes: 117 additions & 0 deletions tests/minimext/build_wheels.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#!/bin/bash -e
# Copyright (C) 2024 Stewart Miles
# Licensed to the Python Software Foundation under a contributor agreement.
# See LICENSE.txt and CONTRIBUTORS.txt.

readonly DEFAULT_PYTHON_VERSION="$(python --version |
cut -d ' ' -f 2 |
cut -d. -f 1,2)"
readonly DEFAULT_PYTHON_VERSIONS="2.7 3.5 ${DEFAULT_PYTHON_VERSION}"


help() {
echo "\
Builds Linux wheels for this package using a range of Python distributions.
This script requires a Ubuntu distribution and will leave the deadsnakes PPA
https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa
and Python packages used by this script installed.
$(basename "$0") [-d] [-I] [-S] [-p versions] [-h]
-d: Enable dry run mode, simply display rather than execute commands.
-S: Disable Python PPA installation.
-I: Disable Python apt package installation
-p: Space separated list of Python versions to install and build wheels for.
This defaults to \"${DEFAULT_PYTHON_VERSIONS}\".
-h: Display this help.
"
exit 1
}

main() {
readonly THIS_DIRECTORY="$(cd "$(dirname "${0}")"; pwd)"
local dryrun=
local install_python=1
local install_python_ppa=1
local selected_python_versions="${DEFAULT_PYTHON_VERSIONS}"

while getopts "dhISp:" OPTION; do
# shellcheck disable=SC2209
case "${OPTION}" in
d) dryrun=echo
;;
I) install_python=0
;;
S) install_python_ppa=0
;;
p) selected_python_versions="${OPTARG}"
;;
h|*) help
;;
esac
done

IFS=' ' read -r -a python_versions <<< "${selected_python_versions}"

if [[ $((install_python_ppa)) -eq 1 ]]; then
set -x
${dryrun} sudo add-apt-repository ppa:deadsnakes/ppa
set +x
fi

if [[ $((install_python)) -eq 1 ]]; then
# shellcheck disable=SC2207
readonly -a PYTHON_APT_PACKAGES=(
$(for version in "${python_versions[@]}"; do
echo "python${version}-dev";
done))
set -x
${dryrun} sudo apt install "${PYTHON_APT_PACKAGES[@]}"
set +x
fi

local wheels_directory="${THIS_DIRECTORY}/wheels"
mkdir -p "${wheels_directory}"

local venv_directory
local versioned_python
local version
for version in "${python_versions[@]}"; do
versioned_python="python${version}"
venv_directory="${THIS_DIRECTORY}/.venv${version}"

# Try to bootstrap pip if it isn't found.
if ! ${dryrun} "${versioned_python}" -c "import pip" 2> /dev/null; then
# shellcheck disable=SC2155
local temporary_directory="$(mktemp -d)"
local get_pip="${temporary_directory}/get-pip-${version}.py"
${dryrun} curl --output "${get_pip}" \
"https://bootstrap.pypa.io/pip/${version}/get-pip.py"
${dryrun} "${versioned_python}" "${get_pip}"
rm -rf "${temporary_directory}"
fi

# Install virtualenv as venv isn't available in all Python versions.
${dryrun} "${versioned_python}" -m pip install virtualenv
${dryrun} "${versioned_python}" -m virtualenv "${venv_directory}"
(
cd "${THIS_DIRECTORY}"
${dryrun} source "${venv_directory}/bin/activate"
# Upgrade pip and setuptools.
${dryrun} pip install -U pip
${dryrun} pip install -U setuptools
# Build wheels to the wheels subdirectory.
for embed_extension_metadata in 0 1; do
set -x
MINIMEXT_EMBED_EXTENSIONS_METADATA=${embed_extension_metadata} \
${dryrun} pip wheel . -w "${wheels_directory}"
set +x
done
)
done

cp "${wheels_directory}"/*.whl "${THIS_DIRECTORY}/.."
}

main "$@"
88 changes: 88 additions & 0 deletions tests/minimext/calculate.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright (C) 2024 Stewart Miles
// Licensed to the Python Software Foundation under a contributor agreement.
// See LICENSE.txt and CONTRIBUTORS.txt.

// Use the limited API to ensure ABI compatibility across all major Python
// versions starting from 3.2
// https://docs.python.org/3/c-api/stable.html#limited-c-api
#if !defined(Py_LIMITED_API)
#define Py_LIMITED_API 3
#endif // !defined(Py_LIMITED_API)
#define PY_SSIZE_T_CLEAN
#include <Python.h>

// Name and doc string for this module.
#define MODULE_NAME calculate
#define MODULE_DOCS "Calculates Fibonacci numbers."

// Convert the argument into a string.
#define _STRINGIFY(x) #x
#define STRINGIFY(x) _STRINGIFY(x)

// Calculate a Fibonacci number at the specified index of the sequence.
static PyObject *fib(PyObject *self, PyObject *args)
{
long int index;
if (!PyArg_ParseTuple(args, "l", &index)) {
PyErr_SetString(PyExc_ValueError, "An index must be specified.");
}

long int current_value = 1;
long int previous_value = 0;
index--;
for ( ; index > 0 ; --index) {
long int next_value = current_value + previous_value;
previous_value = current_value;
current_value = next_value;
}
return PyLong_FromLong(current_value);
}

// Exposes methods in this module.
static PyMethodDef methods[] =
{
{
"fib",
fib,
METH_VARARGS,
PyDoc_STR("Calculate a Fibonacci number.\n"
"\n"
":param index: Index of the number in the Fibonacci sequence\n"
" to calculate.\n"
"\n"
":returns: Fibonacci number at the specified index.\n"
" For example an index of 7 will return 13\n"),
},
};

#if PY_MAJOR_VERSION >= 3
// Defines the module.
static struct PyModuleDef module =
{
PyModuleDef_HEAD_INIT,
STRINGIFY(MODULE_NAME),
PyDoc_STR(MODULE_DOCS),
-1,
methods,
};
#endif // PY_MAJOR_VERSION >= 3

// Expands to the init function name.
#define _PYINIT_FUNCTION_NAME(prefix, name) prefix ## name
#define PYINIT_FUNCTION_NAME(prefix, name) _PYINIT_FUNCTION_NAME(prefix, name)

// Initialize this module.
#if PY_MAJOR_VERSION >= 3
PyMODINIT_FUNC
PYINIT_FUNCTION_NAME(PyInit_, MODULE_NAME)(void)
{
return PyModule_Create(&module);
}
#else
PyMODINIT_FUNC
PYINIT_FUNCTION_NAME(init, MODULE_NAME)(void)
{
// Ignore the returned module object.
(void)Py_InitModule(STRINGIFY(MODULE_NAME), methods);
}
#endif // PY_MAJOR_VERSION >= 3
Empty file.
25 changes: 25 additions & 0 deletions tests/minimext/minimext/calculate_py.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright (C) 2024 Stewart Miles
# Licensed to the Python Software Foundation under a contributor agreement.
# See LICENSE.txt and CONTRIBUTORS.txt.

"""Python implementation of the calculate extension module."""

def fib(index):
"""Calculate a Fibonacci number.
:param index: Index of the number in the Fibonacci sequence
to calculate.
:returns: Fibonacci number at the specified index.
For example an index of 7 will return 13
"""
current_value = 1
previous_value = 0
index -= 1
while index > 0:
next_value = current_value + previous_value
previous_value = current_value
current_value = next_value
index -= 1
return current_value

67 changes: 67 additions & 0 deletions tests/minimext/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Copyright (C) 2024 Stewart Miles
# Licensed to the Python Software Foundation under a contributor agreement.
# See LICENSE.txt and CONTRIBUTORS.txt.
import codecs
import os
import json
from setuptools import Extension, setup
from setuptools.command import egg_info
import sys


EMBED_EXTENSIONS_METADATA = (
int(os.getenv('MINIMEXT_EMBED_EXTENSIONS_METADATA', '0')))


class EggInfo(egg_info.egg_info):
"""egg_info command that optionally writes extensions metadata.
distlib.wheel.Wheel attempts to read the list of extensions from the
undocumented JSON EXTENSIONS metadata file.
This command will add the special file JSON EXTENSIONS metadata file to the
*.dist-info directory in the wheel if the
MINIMEXT_EMBED_EXTENSIONS_METADATA environment variable is set to 1.
"""

def run(self):
egg_info.egg_info.run(self)
if EMBED_EXTENSIONS_METADATA:
build_ext = self.get_finalized_command('build_ext')
extensions_dict = {
ext_module.name: build_ext.get_ext_filename(ext_module.name)
for ext_module in self.distribution.ext_modules
}
with open(os.path.join(self.egg_info, 'EXTENSIONS'), 'wb') as (
extensions_file):
json.dump(extensions_dict,
codecs.getwriter('utf-8')(extensions_file),
indent=2)


setup(
name='minimext' + ('_metadata' if EMBED_EXTENSIONS_METADATA else ''),
version='0.1',
description='Calculates Fibonacci numbers.',
long_description=(
'Distribution that provides calculate.fib() and calculate_py.fib() '
'which calculate Fibonacci numbers. minimext.calculate is implemented '
'as a C extension to test distlib.wheel.Wheel.mount().'),
packages=['minimext'],
ext_modules=[
Extension(name='minimext.calculate',
sources=['calculate.c'],
py_limited_api=True,
define_macros=[
('Py_LIMITED_API', str(sys.version_info.major)),
]),
],
# The extension uses the limited API so tag the wheel as compatible with
# Python 3.2 and later.
#
# Unfortunately the py_limited_api argument to Extension does not mark the
# wheel as supporting the limited API, so set the see compatibility
# manually.
options={'bdist_wheel': {'py_limited_api': 'cp32'}},
cmdclass={'egg_info': EggInfo},
)

0 comments on commit 4a67db8

Please sign in to comment.