Skip to content

Commit 6127249

Browse files
authored
build: enable mypyc (#247)
Signed-off-by: nstarman <nstarman@users.noreply.github.com>
1 parent 987ffea commit 6127249

File tree

9 files changed

+2056
-1279
lines changed

9 files changed

+2056
-1279
lines changed

.github/workflows/ci.yml

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jobs:
3737

3838
name: Test ${{ matrix.value.name }}
3939
steps:
40-
- uses: actions/checkout@v2
40+
- uses: actions/checkout@v4
4141
- name: Set up Python ${{ matrix.value.python-version }}
4242
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
4343
with:
@@ -51,14 +51,45 @@ jobs:
5151
- name: Run tests
5252
run: |
5353
PRAGMA_VERSION=`uv run python -c "import sys; print('.'.join(map(str, sys.version_info[:2])))"` \
54-
uv run --frozen pytest -v --cov=plum --cov-report term-missing --cov-report lcov:coverage.lcov
54+
uv run --frozen pytest -v --cov=src/plum --cov-report term-missing --cov-report lcov:coverage.lcov
5555
- name: Coveralls parallel
5656
uses: coverallsapp/github-action@v2
5757
with:
5858
flag-name: run-${{ matrix.value.name }}
5959
parallel: true
6060
file: coverage.lcov
6161

62+
# Test that mypyc-compiled wheels work
63+
test-mypyc:
64+
name: Test mypyc wheel
65+
runs-on: ubuntu-latest
66+
steps:
67+
- uses: actions/checkout@v4
68+
with:
69+
fetch-depth: 0
70+
71+
- name: Set up uv
72+
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
73+
with:
74+
python-version: "3.12"
75+
76+
- name: Install build & test dependencies
77+
run: uv sync --locked --no-install-project --no-default-groups --group build --group wheels --group test_static --group test_runtime
78+
79+
- name: Build mypyc wheel
80+
run: uv run --no-sync python -m build --wheel
81+
env:
82+
HATCH_BUILD_HOOKS_ENABLE: "1"
83+
84+
- name: Install wheel
85+
run: uv pip install dist/*.whl
86+
87+
- name: Verify compiled
88+
run: uv run --no-sync python -c "from plum import COMPILED; print(f'COMPILED={COMPILED}'); assert COMPILED"
89+
90+
- name: Run tests
91+
run: uv run --no-sync pytest tests/ -v -k "not incompatible_with_mypyc" --ignore=tests/advanced
92+
6293
finish:
6394
name: Finish coverage
6495
needs: test

.github/workflows/publish.yml

Lines changed: 122 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,126 @@
1-
# This workflow will upload a Python package using Twine when a release is
2-
# created. For more information see the following link:
3-
# https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
4-
5-
name: Publish to PyPI
1+
name: Build and Publish
62

73
on:
8-
release:
9-
types: [published]
4+
release:
5+
types: [published]
6+
workflow_dispatch:
7+
inputs:
8+
publish:
9+
description: "Publish to PyPI (only for releases)"
10+
required: false
11+
default: false
12+
type: boolean
13+
14+
# Cancel any in-progress build when a new commit is pushed
15+
concurrency:
16+
group: ${{ github.workflow }}-${{ github.ref }}
17+
cancel-in-progress: true
1018

1119
jobs:
12-
deploy:
13-
runs-on: ubuntu-latest
14-
15-
steps:
16-
- uses: actions/checkout@v2
17-
18-
# Make sure tags are fetched, so we can get a version.
19-
- run: |
20-
git fetch --prune --unshallow --tags
21-
22-
- name: Set up Python
23-
uses: actions/setup-python@v2
24-
with:
25-
python-version: '3.x'
26-
27-
- name: Install dependencies
28-
run: |
29-
python -m pip install --upgrade pip
30-
python -m pip install --upgrade build twine
31-
32-
- name: Build and publish
33-
env:
34-
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
35-
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
36-
37-
run: |
38-
python -m build
39-
twine upload dist/*
20+
# Build the sdist and pure Python wheel
21+
sdist-and-wheel:
22+
name: Build sdist and pure wheel
23+
runs-on: ubuntu-latest
24+
steps:
25+
- uses: actions/checkout@v4
26+
with:
27+
fetch-depth: 0 # Needed for hatch-vcs versioning
28+
29+
- name: Set up uv
30+
uses: astral-sh/setup-uv@v5
31+
with:
32+
python-version: "3.12"
33+
34+
- name: Build sdist and wheel
35+
run: uv build
36+
37+
- name: Upload sdist
38+
uses: actions/upload-artifact@v4
39+
with:
40+
name: sdist
41+
path: dist/*.tar.gz
42+
43+
- name: Upload wheel
44+
uses: actions/upload-artifact@v4
45+
with:
46+
name: wheel-pure
47+
path: dist/*.whl
48+
49+
# Generate the wheel build matrix dynamically
50+
generate-wheels-matrix:
51+
name: Generate wheels matrix
52+
runs-on: ubuntu-latest
53+
outputs:
54+
include: ${{ steps.set-matrix.outputs.include }}
55+
steps:
56+
- uses: actions/checkout@v4
57+
- name: Set up uv
58+
uses: astral-sh/setup-uv@v5
59+
- name: Install cibuildwheel and generate matrix
60+
run: |
61+
uvx cibuildwheel==2.22.0 --print-build-identifiers --platform linux \
62+
| jq -nRc '{"only": inputs, "os": "ubuntu-latest"}' > /tmp/linux_builds.json
63+
uvx cibuildwheel==2.22.0 --print-build-identifiers --platform macos \
64+
| jq -nRc '{"only": inputs, "os": "macos-14"}' > /tmp/macos_builds.json
65+
uvx cibuildwheel==2.22.0 --print-build-identifiers --platform windows \
66+
| jq -nRc '{"only": inputs, "os": "windows-latest"}' > /tmp/windows_builds.json
67+
- name: Merge matrices
68+
id: set-matrix
69+
run: |
70+
jq -sc 'add' /tmp/*_builds.json > /tmp/matrix.json
71+
MATRIX=$(cat /tmp/matrix.json)
72+
echo "include=$MATRIX" >> "$GITHUB_OUTPUT"
73+
74+
# Build mypyc-compiled wheels using cibuildwheel
75+
mypyc-wheels:
76+
name: Build ${{ matrix.only }}
77+
needs: generate-wheels-matrix
78+
runs-on: ${{ matrix.os }}
79+
strategy:
80+
fail-fast: false
81+
matrix:
82+
include: ${{ fromJson(needs.generate-wheels-matrix.outputs.include) }}
83+
steps:
84+
- uses: actions/checkout@v4
85+
with:
86+
fetch-depth: 0
87+
88+
- name: Build wheels
89+
uses: pypa/cibuildwheel@v2.22.0
90+
with:
91+
only: ${{ matrix.only }}
92+
env:
93+
CIBW_BUILD_VERBOSITY: 1
94+
95+
- name: Upload wheels
96+
uses: actions/upload-artifact@v4
97+
with:
98+
name: wheel-${{ matrix.only }}
99+
path: wheelhouse/*.whl
100+
101+
# Publish to PyPI using trusted publishing
102+
publish:
103+
name: Publish to PyPI
104+
needs: [sdist-and-wheel, mypyc-wheels]
105+
runs-on: ubuntu-latest
106+
# Only publish on release events or manual trigger with publish=true
107+
if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.publish)
108+
environment:
109+
name: pypi
110+
url: https://pypi.org/p/plum-dispatch
111+
permissions:
112+
id-token: write # Required for trusted publishing
113+
steps:
114+
- name: Download all artifacts
115+
uses: actions/download-artifact@v4
116+
with:
117+
path: dist
118+
merge-multiple: true
119+
120+
- name: List artifacts
121+
run: ls -la dist/
122+
123+
- name: Publish to PyPI
124+
uses: pypa/gh-action-pypi-publish@release/v1
125+
with:
126+
packages-dir: dist/

noxfile.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def precommit(s: nox.Session, /) -> None:
3131
@session(uv_groups=["lint"], reuse_venv=True)
3232
def pylint(s: nox.Session, /) -> None:
3333
"""Run PyLint."""
34-
s.run("pylint", "plum", *s.posargs)
34+
s.run("pylint", "src/plum", *s.posargs)
3535

3636

3737
# =============================================================================

pyproject.toml

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ dev = [
4141
{ include-group = "test_static" },
4242
{ include-group = "test_runtime" },
4343
]
44+
build = [
45+
"hatch>=1.15.1",
46+
"hatch-mypyc>=0.16.0",
47+
"hatch-vcs",
48+
]
49+
wheels = [
50+
"build>=1.4.0",
51+
"cibuildwheel>=2.22.0",
52+
]
4453
docs = [
4554
"jupyter-book>=1.0.0,<2.0.0",
4655
]
@@ -71,6 +80,61 @@ build.hooks.vcs.version-file = "src/plum/_version.py"
7180
build.targets.wheel.packages = ["src/plum"]
7281
version.source = "vcs"
7382

83+
84+
[tool.hatch.build.targets.wheel.hooks.mypyc]
85+
enable-by-default = false
86+
dependencies = ["hatch-mypyc>=0.16.0", "mypy>=1.18.2"]
87+
require-runtime-dependencies = true
88+
include = [ # TODO: include more files
89+
"src/plum/_bear.py",
90+
"src/plum/_dispatcher.py",
91+
"src/plum/_method.py",
92+
"src/plum/_overload.py",
93+
"src/plum/_resolver.py",
94+
]
95+
mypy-args = [
96+
"--ignore-missing-imports",
97+
"--disable-error-code=assignment",
98+
"--disable-error-code=attr-defined",
99+
"--disable-error-code=return-value",
100+
"--disable-error-code=arg-type",
101+
"--disable-error-code=type-var",
102+
"--disable-error-code=misc",
103+
"--disable-error-code=valid-type",
104+
"--disable-error-code=no-redef",
105+
]
106+
options = { debug_level = "0" }
107+
108+
[tool.cibuildwheel]
109+
build-verbosity = 1
110+
111+
# Target environments:
112+
# - Python: CPython 3.10+ only
113+
# - Architecture (64-bit only): amd64 / x86_64, universal2, and arm64
114+
# - OS: Linux (no musl), Windows, and macOS
115+
build = "cp310-* cp311-* cp312-* cp313-*"
116+
skip = [
117+
"*-manylinux_i686",
118+
"*-musllinux_*",
119+
"*-win32",
120+
"pp*",
121+
]
122+
123+
test-command = 'pytest {project}/tests -k "not incompatible_with_mypyc" --ignore={project}/tests/advanced'
124+
test-requires = ["pytest>=8.4.2"]
125+
# Skip testing arm64 builds on Intel Macs
126+
test-skip = ["*-macosx_arm64", "*-macosx_universal2:arm64"]
127+
128+
[tool.cibuildwheel.environment]
129+
HATCH_BUILD_HOOKS_ENABLE = "1"
130+
MYPYC_OPT_LEVEL = "3"
131+
MYPYC_DEBUG_LEVEL = "0"
132+
133+
[tool.cibuildwheel.linux]
134+
manylinux-x86_64-image = "manylinux_2_28"
135+
manylinux-aarch64-image = "manylinux_2_28"
136+
137+
74138
# Development tools
75139
[tool.coverage]
76140
run.branch = true
@@ -111,6 +175,9 @@ addopts = [
111175
"no:doctest",
112176
]
113177
minversion = "6.0"
178+
markers = [
179+
"incompatible_with_mypyc: marks tests as incompatible with mypyc-compiled code",
180+
]
114181

115182

116183
[tool.ruff]

src/plum/__init__.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,11 @@
5555
"get_class",
5656
"get_context",
5757
"argsort",
58+
# Compilation
59+
"COMPILED",
5860
)
5961

62+
import importlib.util
6063
from typing import TypeGuard, TypeVar
6164

6265
from beartype.door import TypeHint as _TypeHint
@@ -106,15 +109,23 @@
106109
)
107110
from ._version import __version__ # noqa: F401 # noqa: F401
108111

112+
# If mypyc compiled, one of the core modules will be a native extension
113+
spec = importlib.util.find_spec("plum._bear")
114+
COMPILED = (
115+
spec is not None
116+
and spec.origin is not None
117+
and spec.origin.endswith((".so", ".pyd", ".dylib"))
118+
)
119+
109120
# isort: split
110121
# Plum previously exported a number of types. As of recently, the user can use
111122
# the versions from `typing`. To not break backward compatibility, we still
112123
# export these types.
113-
from typing import Dict, List, Tuple, Union # noqa: F401, UP035
124+
from typing import Dict, List, Tuple, Union # noqa: E402, F401, UP035
114125

115126
# Deprecated
116127
# isort: split
117-
from ._parametric import Val as Val # noqa: F401, F403
128+
from ._parametric import Val as Val # noqa: E402, F401, F403
118129

119130
T = TypeVar("T")
120131
T2 = TypeVar("T2")

src/plum/_resolver.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -352,13 +352,13 @@ def resolve(self, target: tuple[object, ...] | Signature) -> Method:
352352

353353
def check(m: Method, /) -> bool:
354354
# `target` are concrete arguments.
355-
return m.signature.match(target)
355+
return bool(m.signature.match(target))
356356

357357
else:
358358

359359
def check(m: Method, /) -> bool:
360360
# `target` is a signature that must be encompassed.
361-
return target <= m.signature
361+
return bool(target <= m.signature)
362362

363363
candidates: list[Method] = []
364364
for method in [m for m in self.methods if check(m)]:

tests/test_method.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def f(x, *args) -> float:
112112
assert repr(m).startswith(result)
113113
# Also render the fully mismatched version. When rendered to text, that should
114114
# give the same.
115-
assert rich_render(m.repr_mismatch({0}, False)).startswith(result)
115+
assert rich_render(m.repr_mismatch(frozenset({0}), False)).startswith(result)
116116

117117

118118
def test_repr_complex():
@@ -135,7 +135,7 @@ def f(x, *, option, **kw_args) -> float:
135135
assert repr(m).startswith(result)
136136
# Also render the fully mismatched version. When rendered to text, that should
137137
# give the same.
138-
assert rich_render(m.repr_mismatch({0}, False)).startswith(result)
138+
assert rich_render(m.repr_mismatch(frozenset({0}), False)).startswith(result)
139139

140140

141141
def test_methodlist_repr(monkeypatch, dispatch: plum.Dispatcher):

tests/test_resolver.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ def f(x):
9090
assert _document(f, "f") == textwrap.dedent(expected_doc).strip()
9191

9292

93+
@pytest.mark.incompatible_with_mypyc
9394
def test_doc(monkeypatch):
9495
# Let the `pydoc` documenter simply return the docstring. This makes testing
9596
# simpler.

0 commit comments

Comments
 (0)