Skip to content

Commit

Permalink
Modern packaging
Browse files Browse the repository at this point in the history
Restructure the package to use modern Python packaging methods.

- Drop support for Python < 3.7
- Use pyproject.toml
- Use cibuildwheel to build wheels for many linux distros
- Introduce automation with GitHub actions for builds
- Use setuptools_scm to automate versioning
- Assume Cython is available when preparing a source distribution
  • Loading branch information
wmayner committed Feb 14, 2023
1 parent 12862c3 commit c35a511
Show file tree
Hide file tree
Showing 22 changed files with 226 additions and 245 deletions.
25 changes: 25 additions & 0 deletions .github/workflows/build_wheels.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Build

on: [push, pull_request]

jobs:
build_wheels:
name: Build wheels on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-20.04, windows-2019, macos-11]

steps:
- uses: actions/checkout@v3

- name: Build wheels
uses: pypa/[email protected]
with:
package-dir: .
output-dir: wheelhouse
config-file: "{package}/pyproject.toml"

- uses: actions/upload-artifact@v3
with:
path: ./wheelhouse/*.whl
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
__pycache__
.gitconfig
.cache
.pytest_cache
.tox
.env
.ropeproject
*.so
*.pyc
MANIFEST
*.egg*
src/pyemd/emd.cpp
src/pyemd/_version.py
build
dist
pyemd/emd.cpp
wheelhouse
ignore
2 changes: 1 addition & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
graft pyemd
graft src/pyemd
graft test

include README.rst
Expand Down
59 changes: 32 additions & 27 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,39 +1,44 @@
.PHONY: default test build clean upload-dist test-dist sign-dist check-dist build-dist clean-dist
.PHONY: default clean develop test dist-clean build-local build dist-upload dist-test-upload dist-sign dist-check

src = pyemd
dist_dir = dist
src = src/pyemd
test = test
dist = wheelhouse
readme = README.rst

default: build
default: develop

test: build
py.test
clean:
rm -rf $(shell find . -name '__pycache__')
rm -rf $(shell find . -name '*.so')
rm -rf .eggs
rm -rf pyemd.egg-info
rm -rf dist
rm -rf build

build: clean
develop: clean
python -m pip install -e .
# setup.py build_ext -b .

clean:
rm -f pyemd/*.so
rm -rf **/__pycache__
rm -rf build
rm -rf pyemd.egg-info
test: develop
py.test

upload-dist: sign-dist
twine upload $(dist_dir)/*
build-local: clean
python -m build

test-dist: check-dist
twine upload --repository testpypi $(dist_dir)/*
dist-clean:
rm -rf $(dist)

sign-dist: check-dist
gpg --detach-sign -a dist/*.tar.gz
gpg --detach-sign -a dist/*.whl
build: dist-clean
cibuildwheel --platform linux --config-file pyproject.toml --output-dir $(dist)

check-dist: build-dist
twine check --strict dist/*
dist-upload: dist-sign
twine upload $(dist)/*

build-dist: clean-dist
python -m build
# python -m setup.py sdist bdist_wheel --dist-dir=$(dist_dir)
dist-test-upload: dist-check
twine upload --repository testpypi $(dist)/*

dist-sign: dist-check
gpg --detach-sign -a $(dist)/*.tar.gz
gpg --detach-sign -a $(dist)/*.whl

clean-dist:
rm -r $(dist_dir)
dist-check: dist-build
twine check --strict $(dist)/*
6 changes: 0 additions & 6 deletions conftest.py

This file was deleted.

1 change: 0 additions & 1 deletion dev_requirements.txt

This file was deleted.

4 changes: 0 additions & 4 deletions dist_requirements.txt

This file was deleted.

20 changes: 0 additions & 20 deletions pyemd/__about__.py

This file was deleted.

35 changes: 27 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,24 +1,43 @@
[build-system]
requires = ["setuptools>=61.0", "cython", "wheel"]
requires = [
"cython",
"oldest-supported-numpy",
"setuptools >= 45",
"setuptools_scm",
"wheel",
]
build-backend = "setuptools.build_meta"

[project]
name = "pyemd"
version = "0.5.1"
authors = [{ name = "Will Mayner", email = "[email protected]" }]
description = "A Python wrapper for Ofir Pele and Michael Werman's implementation of the Earth Mover's Distance."
readme = "README.md"
license = { file = "LICENSE" }
description = "A Python wrapper for Ofir Pele and Michael Werman's implementation of the Earth Mover's Distance."
authors = [{ name = "Will Mayner", email = "[email protected]" }]
requires-python = ">=3.7"
dependencies = ["numpy >= 1.9.0"]
readme = "README.rst"
classifiers = [
'Development Status :: 3 - Alpha',
'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers',
'Natural Language :: English',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 3',
]
dynamic = ['dependencies']
dynamic = ['version']

[project.optional-dependencies]
test = ['pytest']
dist = ['cibuildwheel', 'setuptools_scm']

[project.urls]
"Homepage" = "https://github.com/wmayner/pyemd"
"Bug Tracker" = "https://github.com/wmayner/pyemd/issues"

[tool.setuptools_scm]
write_to = "src/pyemd/_version.py"

[tool.cibuildwheel]
skip = "cp36*"
build-verbosity = 2
test-requires = ["pytest"]
test-command = "py.test {project}"
6 changes: 5 additions & 1 deletion pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ addopts =
--tb=auto
--doctest-glob='*.rst'
--doctest-modules -vv
--ignore setup.py
norecursedirs =
src
dist
build
wheelhouse
ignore
.git
.tox
.eggs
4 changes: 0 additions & 4 deletions requirements.txt

This file was deleted.

104 changes: 32 additions & 72 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,34 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import io
import os
import platform
import sys
from warnings import warn
from distutils.sysconfig import get_config_var

from setuptools import Extension, setup
from setuptools.command.build_ext import build_ext as _build_ext
from setuptools.command.sdist import sdist as _sdist
from distutils.sysconfig import get_config_var
from distutils.version import LooseVersion

from packaging.version import Version, parse as parse_version


def is_platform_mac():
return sys.platform == "darwin"


# Alias ModuleNotFound for Python <= 3.5
if sys.version_info < (3, 6):
ModuleNotFoundError = ImportError

# For macOS, ensure extensions are built for macOS 10.9 when compiling on a
# 10.9 system or above, overriding distutils behaviour which is to target
# the version that Python was built for. This may be overridden by setting
# MACOSX_DEPLOYMENT_TARGET before calling setup.py
if is_platform_mac():
import packaging

if "MACOSX_DEPLOYMENT_TARGET" not in os.environ:
current_system = LooseVersion(platform.mac_ver()[0])
python_target = LooseVersion(get_config_var("MACOSX_DEPLOYMENT_TARGET"))
if python_target < "10.9" and current_system >= "10.9":
os.environ["MACOSX_DEPLOYMENT_TARGET"] = "10.9"
if is_platform_mac() and "MACOSX_DEPLOYMENT_TARGET" not in os.environ:
current_system = parse_version(platform.mac_ver()[0])
python_target = parse_version(get_config_var("MACOSX_DEPLOYMENT_TARGET"))
mac_deployment_target = Version("10.9")
if (
python_target < mac_deployment_target
and current_system >= mac_deployment_target
):
os.environ["MACOSX_DEPLOYMENT_TARGET"] = str(mac_deployment_target)

try:
from Cython.Build import cythonize as _cythonize
Expand All @@ -43,10 +38,10 @@ def is_platform_mac():
USE_CYTHON = False


def cythonize(extensions, **_ignore):
def cythonize(extensions, **kwargs):
# Attempt to use Cython
if USE_CYTHON:
return _cythonize(extensions)
return _cythonize(extensions, **kwargs)
# Cython is not available
for extension in extensions:
sources = []
Expand All @@ -63,25 +58,15 @@ def cythonize(extensions, **_ignore):
return extensions


EXTENSIONS = [Extension("pyemd.emd", sources=["pyemd/emd.pyx"], language="c++")]

EXT_MODULES = cythonize(EXTENSIONS)

EXTENSIONS = [
Extension(
"pyemd.emd",
sources=["src/pyemd/emd.pyx"],
language="c++",
)
]

class sdist(_sdist):
def run(self):
# Ensure the compiled Cython files in the distribution are up-to-date
if USE_CYTHON:
_cythonize(EXTENSIONS)
else:
warn(
"\n\n\033[91m\033[1m WARNING: "
"IF YOU A PREPARING A DISTRIBUTION: Cython is not available! "
"The cythonized `*.cpp` files may be out of date. Please "
"install Cython and run `sdist` again."
"\033[0m\n"
)
_sdist.run(self)
EXT_MODULES = cythonize(EXTENSIONS, language_level=3)


# See https://stackoverflow.com/a/21621689/1085344
Expand All @@ -96,46 +81,21 @@ def finalize_options(self):
self.include_dirs.append(numpy.get_include())


CMDCLASS = {"sdist": sdist, "build_ext": build_ext}

CMDCLASS = {"build_ext": build_ext}

with io.open("README.rst", encoding="utf-8") as f:
README = f.read()

ABOUT = {}
with open("./pyemd/__about__.py") as f:
exec(f.read(), ABOUT)


REQUIRES = ["packaging"]

NUMPY_REQUIREMENT = [
"numpy >=1.9.0, <1.20.0; python_version<='3.6'",
"numpy >=1.9.0, <2.0.0; python_version>'3.6'",
]

# Copied from scipy's installer, to solve the same issues they saw:

# Figure out whether to add ``*_requires = ['numpy']``.
# We don't want to do that unconditionally, because we risk updating
# an installed numpy which fails too often. Just if it's not installed, we
# may give it a try. See scipy gh-3379.
try:
import numpy
except ImportError: # We do not have numpy installed
REQUIRES += NUMPY_REQUIREMENT
else:
# If we're building a wheel, assume there already exist numpy wheels
# for this platform, so it is safe to add numpy to build requirements.
# See scipy gh-5184.
if "bdist_wheel" in sys.argv[1:]:
REQUIRES += NUMPY_REQUIREMENT

SETUP_REQUIRES = ["setuptools_scm", "packaging"]

setup(
name="pyemd",
packages=["pyemd", "pyemd.lib"],
install_requires=REQUIRES,
setup_requires=REQUIRES,
package_dir={
"pyemd": "src/pyemd",
"pyemd.lib": "src/pyemd/lib",
},
include_package_data=True,
ext_modules=EXT_MODULES,
cmdclass=CMDCLASS,
setup_requires=SETUP_REQUIRES,
use_scm_version=True,
)
6 changes: 5 additions & 1 deletion pyemd/__init__.py → src/pyemd/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,9 @@
:license: See the LICENSE file.
"""

from .__about__ import *
from .emd import emd, emd_with_flow, emd_samples

try:
from ._version import version as __version__
except ImportError:
__version__ = "unknown version"
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading

0 comments on commit c35a511

Please sign in to comment.