zloop 0.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zloop-0.0.0/.gitignore +17 -0
- zloop-0.0.0/LICENSE +28 -0
- zloop-0.0.0/PKG-INFO +85 -0
- zloop-0.0.0/README.md +71 -0
- zloop-0.0.0/build.zig +65 -0
- zloop-0.0.0/build_ext.sh +17 -0
- zloop-0.0.0/hatch_build.py +97 -0
- zloop-0.0.0/pyproject.toml +125 -0
- zloop-0.0.0/src/core/clock.zig +25 -0
- zloop-0.0.0/src/core/loop.zig +517 -0
- zloop-0.0.0/src/core/queue.zig +78 -0
- zloop-0.0.0/src/core/reactor.zig +323 -0
- zloop-0.0.0/src/core/root.zig +17 -0
- zloop-0.0.0/src/core/sys.zig +217 -0
- zloop-0.0.0/src/core/timers.zig +133 -0
- zloop-0.0.0/src/python/ft_atomics.c +20 -0
- zloop-0.0.0/src/python/handle.zig +244 -0
- zloop-0.0.0/src/python/loop_obj.zig +833 -0
- zloop-0.0.0/src/python/module.zig +58 -0
- zloop-0.0.0/src/python/py.zig +248 -0
- zloop-0.0.0/src/python/transport_obj.zig +894 -0
- zloop-0.0.0/zloop/__init__.py +13 -0
- zloop-0.0.0/zloop/_io.py +534 -0
- zloop-0.0.0/zloop/_support.py +34 -0
- zloop-0.0.0/zloop/_tls.py +45 -0
- zloop-0.0.0/zloop/_zloop.pyi +20 -0
- zloop-0.0.0/zloop/py.typed +0 -0
zloop-0.0.0/.gitignore
ADDED
zloop-0.0.0/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Marcelo Trylesinski
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
zloop-0.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zloop
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: An asyncio event loop with a Zig core.
|
|
5
|
+
Author-email: Marcelo Trylesinski <marcelotryle@gmail.com>
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Framework :: AsyncIO
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Zig
|
|
12
|
+
Requires-Python: >=3.12
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# zloop
|
|
16
|
+
|
|
17
|
+
> [!WARNING]
|
|
18
|
+
> zloop is experimental. The API and behaviour may change at any time, and it is not yet ready for production use.
|
|
19
|
+
|
|
20
|
+
A drop-in [asyncio](https://docs.python.org/3/library/asyncio.html) event loop
|
|
21
|
+
whose engine is written in [Zig](https://ziglang.org). It's to asyncio what
|
|
22
|
+
[uvloop](https://github.com/MagicStack/uvloop) is - a real
|
|
23
|
+
`asyncio.AbstractEventLoop` - except the engine is a hand-written kqueue/epoll
|
|
24
|
+
reactor in Zig rather than libuv wrapped in Cython.
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import asyncio
|
|
28
|
+
import zloop
|
|
29
|
+
|
|
30
|
+
print(asyncio.run(asyncio.sleep(0, "hello from a Zig loop"), loop_factory=zloop.new_event_loop))
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
With [uvicorn](https://www.uvicorn.org):
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
uvicorn app:app --loop zloop:new_event_loop
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Why
|
|
40
|
+
|
|
41
|
+
- **Drop-in.** A genuine `AbstractEventLoop`, so the asyncio ecosystem -
|
|
42
|
+
uvicorn, FastAPI, AnyIO, HTTPX - runs on it unchanged.
|
|
43
|
+
- **Correct.** Passes [uvicorn](https://github.com/encode/uvicorn)'s **entire**
|
|
44
|
+
test suite (1048 tests), identical to stock asyncio, plus its own suite at
|
|
45
|
+
**100%** coverage.
|
|
46
|
+
- **Fast.** Faster than uvloop on the workloads measured so far - scheduling,
|
|
47
|
+
timers, and small/medium-message socket throughput (e.g. `call_soon` +46%,
|
|
48
|
+
1 KiB echo +16% on CPython 3.14 / macOS arm64). `create_future` ties, because
|
|
49
|
+
all three loops reuse CPython's C-accelerated `_asyncio.Future`.
|
|
50
|
+
|
|
51
|
+
## Benchmarks
|
|
52
|
+
|
|
53
|
+
The fairest comparison is uvloop's *own* echo benchmark, run unchanged except
|
|
54
|
+
for a `--zloop` server flag mirroring `--uvloop` (the client is byte-for-byte
|
|
55
|
+
uvloop's). Requests/sec, higher is better - macOS arm64, CPython 3.14, 3
|
|
56
|
+
workers, best of 3:
|
|
57
|
+
|
|
58
|
+
| Message | Server mode | asyncio | uvloop | zloop | zloop vs uvloop |
|
|
59
|
+
| --- | --- | ---: | ---: | ---: | ---: |
|
|
60
|
+
| 1 KiB | proto | 113k | 113k | **121k** | **+7%** |
|
|
61
|
+
| 1 KiB | buffered | 115k | 115k | **123k** | **+7%** |
|
|
62
|
+
| 1 KiB | streams | 83k | 90k | **103k** | **+14%** |
|
|
63
|
+
| 10 KiB | proto | 105k | 110k | **113k** | **+3%** |
|
|
64
|
+
| 10 KiB | buffered | 105k | 105k | **124k** | **+18%** |
|
|
65
|
+
| 10 KiB | streams | 81k | 86k | **95k** | **+11%** |
|
|
66
|
+
|
|
67
|
+
For the 1-10 KiB messages common in HTTP, WebSocket frames, and RPC, zloop leads
|
|
68
|
+
uvloop in every cell. The 100 KiB row is omitted: at that size the test measures
|
|
69
|
+
loopback bandwidth, not the loop, and all three swing wildly run-to-run.
|
|
70
|
+
|
|
71
|
+
Reproduce it with `scripts/bench` (or `bash bench_uvloop/run_matrix.sh` for the
|
|
72
|
+
full matrix); the **Benchmark** CI workflow runs it on Linux and posts the table
|
|
73
|
+
to the run summary.
|
|
74
|
+
|
|
75
|
+
## How it works
|
|
76
|
+
|
|
77
|
+
The loop *engine* lives in Zig; CPython is reused only where reimplementing
|
|
78
|
+
would be reckless: driving coroutines (`asyncio.Future` / `asyncio.Task`) and
|
|
79
|
+
the TLS state machine (`asyncio.sslproto`). That's exactly uvloop's boundary.
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
zloop/ Python edge - new_event_loop() factory, connection setup
|
|
83
|
+
src/python/*.zig CPython C-API adapter - Loop, Handle, Transport
|
|
84
|
+
src/core/*.zig pure-Zig domain - run-once engine, kqueue/epoll reactor, timer heap
|
|
85
|
+
```
|
zloop-0.0.0/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# zloop
|
|
2
|
+
|
|
3
|
+
> [!WARNING]
|
|
4
|
+
> zloop is experimental. The API and behaviour may change at any time, and it is not yet ready for production use.
|
|
5
|
+
|
|
6
|
+
A drop-in [asyncio](https://docs.python.org/3/library/asyncio.html) event loop
|
|
7
|
+
whose engine is written in [Zig](https://ziglang.org). It's to asyncio what
|
|
8
|
+
[uvloop](https://github.com/MagicStack/uvloop) is - a real
|
|
9
|
+
`asyncio.AbstractEventLoop` - except the engine is a hand-written kqueue/epoll
|
|
10
|
+
reactor in Zig rather than libuv wrapped in Cython.
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
import asyncio
|
|
14
|
+
import zloop
|
|
15
|
+
|
|
16
|
+
print(asyncio.run(asyncio.sleep(0, "hello from a Zig loop"), loop_factory=zloop.new_event_loop))
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
With [uvicorn](https://www.uvicorn.org):
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uvicorn app:app --loop zloop:new_event_loop
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Why
|
|
26
|
+
|
|
27
|
+
- **Drop-in.** A genuine `AbstractEventLoop`, so the asyncio ecosystem -
|
|
28
|
+
uvicorn, FastAPI, AnyIO, HTTPX - runs on it unchanged.
|
|
29
|
+
- **Correct.** Passes [uvicorn](https://github.com/encode/uvicorn)'s **entire**
|
|
30
|
+
test suite (1048 tests), identical to stock asyncio, plus its own suite at
|
|
31
|
+
**100%** coverage.
|
|
32
|
+
- **Fast.** Faster than uvloop on the workloads measured so far - scheduling,
|
|
33
|
+
timers, and small/medium-message socket throughput (e.g. `call_soon` +46%,
|
|
34
|
+
1 KiB echo +16% on CPython 3.14 / macOS arm64). `create_future` ties, because
|
|
35
|
+
all three loops reuse CPython's C-accelerated `_asyncio.Future`.
|
|
36
|
+
|
|
37
|
+
## Benchmarks
|
|
38
|
+
|
|
39
|
+
The fairest comparison is uvloop's *own* echo benchmark, run unchanged except
|
|
40
|
+
for a `--zloop` server flag mirroring `--uvloop` (the client is byte-for-byte
|
|
41
|
+
uvloop's). Requests/sec, higher is better - macOS arm64, CPython 3.14, 3
|
|
42
|
+
workers, best of 3:
|
|
43
|
+
|
|
44
|
+
| Message | Server mode | asyncio | uvloop | zloop | zloop vs uvloop |
|
|
45
|
+
| --- | --- | ---: | ---: | ---: | ---: |
|
|
46
|
+
| 1 KiB | proto | 113k | 113k | **121k** | **+7%** |
|
|
47
|
+
| 1 KiB | buffered | 115k | 115k | **123k** | **+7%** |
|
|
48
|
+
| 1 KiB | streams | 83k | 90k | **103k** | **+14%** |
|
|
49
|
+
| 10 KiB | proto | 105k | 110k | **113k** | **+3%** |
|
|
50
|
+
| 10 KiB | buffered | 105k | 105k | **124k** | **+18%** |
|
|
51
|
+
| 10 KiB | streams | 81k | 86k | **95k** | **+11%** |
|
|
52
|
+
|
|
53
|
+
For the 1-10 KiB messages common in HTTP, WebSocket frames, and RPC, zloop leads
|
|
54
|
+
uvloop in every cell. The 100 KiB row is omitted: at that size the test measures
|
|
55
|
+
loopback bandwidth, not the loop, and all three swing wildly run-to-run.
|
|
56
|
+
|
|
57
|
+
Reproduce it with `scripts/bench` (or `bash bench_uvloop/run_matrix.sh` for the
|
|
58
|
+
full matrix); the **Benchmark** CI workflow runs it on Linux and posts the table
|
|
59
|
+
to the run summary.
|
|
60
|
+
|
|
61
|
+
## How it works
|
|
62
|
+
|
|
63
|
+
The loop *engine* lives in Zig; CPython is reused only where reimplementing
|
|
64
|
+
would be reckless: driving coroutines (`asyncio.Future` / `asyncio.Task`) and
|
|
65
|
+
the TLS state machine (`asyncio.sslproto`). That's exactly uvloop's boundary.
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
zloop/ Python edge - new_event_loop() factory, connection setup
|
|
69
|
+
src/python/*.zig CPython C-API adapter - Loop, Handle, Transport
|
|
70
|
+
src/core/*.zig pure-Zig domain - run-once engine, kqueue/epoll reactor, timer heap
|
|
71
|
+
```
|
zloop-0.0.0/build.zig
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
|
|
3
|
+
pub fn build(b: *std.Build) void {
|
|
4
|
+
const target = b.standardTargetOptions(.{});
|
|
5
|
+
const optimize = b.standardOptimizeOption(.{});
|
|
6
|
+
|
|
7
|
+
// Pure-Zig unit tests for the core data structures. Defined first so
|
|
8
|
+
// `zig build test` works without any Python configuration.
|
|
9
|
+
const core_tests = b.addTest(.{
|
|
10
|
+
.root_module = b.createModule(.{
|
|
11
|
+
.root_source_file = b.path("src/core/root.zig"),
|
|
12
|
+
.target = target,
|
|
13
|
+
.optimize = optimize,
|
|
14
|
+
.link_libc = true,
|
|
15
|
+
}),
|
|
16
|
+
});
|
|
17
|
+
const run_core_tests = b.addRunArtifact(core_tests);
|
|
18
|
+
const test_step = b.step("test", "Run pure-Zig core unit tests");
|
|
19
|
+
test_step.dependOn(&run_core_tests.step);
|
|
20
|
+
|
|
21
|
+
// Python build configuration is discovered by build_ext.sh and passed in as
|
|
22
|
+
// -D options or environment variables. Resolved lazily so the test step
|
|
23
|
+
// above never requires a Python toolchain.
|
|
24
|
+
const py_include = b.option([]const u8, "python-include", "Path to the CPython include dir") orelse
|
|
25
|
+
(b.graph.environ_map.get("ZLOOP_PYTHON_INCLUDE") orelse return);
|
|
26
|
+
const ext_suffix = b.option([]const u8, "ext-suffix", "Extension module suffix") orelse
|
|
27
|
+
(b.graph.environ_map.get("ZLOOP_EXT_SUFFIX") orelse return);
|
|
28
|
+
|
|
29
|
+
const core_mod = b.createModule(.{
|
|
30
|
+
.root_source_file = b.path("src/core/root.zig"),
|
|
31
|
+
.target = target,
|
|
32
|
+
.optimize = optimize,
|
|
33
|
+
.link_libc = true,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const mod = b.createModule(.{
|
|
37
|
+
.root_source_file = b.path("src/python/module.zig"),
|
|
38
|
+
.target = target,
|
|
39
|
+
.optimize = optimize,
|
|
40
|
+
.link_libc = true,
|
|
41
|
+
});
|
|
42
|
+
mod.addIncludePath(.{ .cwd_relative = py_include });
|
|
43
|
+
mod.addImport("core", core_mod);
|
|
44
|
+
// Provide atomic helpers the free-threaded headers declare but Zig's
|
|
45
|
+
// translate-c can't inline; harmless (unreferenced) on non-free-threaded builds.
|
|
46
|
+
mod.addCSourceFile(.{ .file = b.path("src/python/ft_atomics.c") });
|
|
47
|
+
|
|
48
|
+
const lib = b.addLibrary(.{
|
|
49
|
+
.name = "_zloop",
|
|
50
|
+
.root_module = mod,
|
|
51
|
+
.linkage = .dynamic,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// CPython extensions resolve interpreter symbols at load time. On macOS this
|
|
55
|
+
// requires undefined symbols to be allowed; on Linux they are global.
|
|
56
|
+
if (target.result.os.tag == .macos) {
|
|
57
|
+
lib.linker_allow_shlib_undefined = true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Install the shared object into the python package directory under the name
|
|
61
|
+
// CPython expects so it imports as `zloop._zloop`.
|
|
62
|
+
const out_name = b.fmt("_zloop{s}", .{ext_suffix});
|
|
63
|
+
const install = b.addInstallFileWithDir(lib.getEmittedBin(), .{ .custom = "../zloop" }, out_name);
|
|
64
|
+
b.getInstallStep().dependOn(&install.step);
|
|
65
|
+
}
|
zloop-0.0.0/build_ext.sh
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Build the zloop Zig extension against the interpreter that will import it.
|
|
3
|
+
# Usage: ./build_ext.sh [path-to-python] (defaults to the uvicorn venv python)
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
PY="${1:-/Users/marcelotryle/dev/encode/zloop/.uvicorn-upstream/.venv/bin/python}"
|
|
7
|
+
cd "$(dirname "$0")"
|
|
8
|
+
|
|
9
|
+
read -r INCLUDE SUFFIX < <("$PY" -c \
|
|
10
|
+
'import sysconfig as s; print(s.get_path("platinclude"), s.get_config_var("EXT_SUFFIX"))')
|
|
11
|
+
|
|
12
|
+
export ZLOOP_PYTHON_INCLUDE="$INCLUDE"
|
|
13
|
+
export ZLOOP_EXT_SUFFIX="$SUFFIX"
|
|
14
|
+
|
|
15
|
+
MODE="${ZLOOP_BUILD_MODE:-ReleaseFast}"
|
|
16
|
+
zig build "-Doptimize=$MODE" "$@" 2>/dev/null || zig build "-Doptimize=$MODE"
|
|
17
|
+
echo "built zloop/_zloop$SUFFIX against $("$PY" --version) ($INCLUDE)"
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import sysconfig
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
|
12
|
+
|
|
13
|
+
ROOT = Path(__file__).parent
|
|
14
|
+
|
|
15
|
+
# cibuildwheel builds every macOS wheel on the arm64 runner and asks for a
|
|
16
|
+
# specific arch through ARCHFLAGS; translate it into a Zig cross-compile target
|
|
17
|
+
# so the produced `.so` matches the wheel tag delocate enforces.
|
|
18
|
+
_MACOS_ZIG_ARCH = {"arm64": "aarch64-macos", "x86_64": "x86_64-macos"}
|
|
19
|
+
|
|
20
|
+
# Pin the binary's minimum macOS to match MACOSX_DEPLOYMENT_TARGET (set by
|
|
21
|
+
# cibuildwheel), so the wheel tag and the `.so`'s required OS version agree and
|
|
22
|
+
# delocate accepts the repaired wheel.
|
|
23
|
+
_MACOS_DEFAULT_MIN = "11.0"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _zig_target_args() -> list[str]:
|
|
27
|
+
archflags = os.environ.get("ARCHFLAGS", "")
|
|
28
|
+
arches = archflags.split()[1::2] # "-arch x86_64 -arch arm64" -> ["x86_64", "arm64"]
|
|
29
|
+
if len(arches) != 1 or sys.platform != "darwin":
|
|
30
|
+
return []
|
|
31
|
+
arch = _MACOS_ZIG_ARCH.get(arches[0])
|
|
32
|
+
if not arch:
|
|
33
|
+
return []
|
|
34
|
+
min_version = os.environ.get("MACOSX_DEPLOYMENT_TARGET", _MACOS_DEFAULT_MIN)
|
|
35
|
+
return [f"-Dtarget={arch}.{min_version}"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _zig_command() -> list[str]:
|
|
39
|
+
"""Resolve how to invoke Zig: a `zig` on PATH, else the `ziglang` pip package.
|
|
40
|
+
|
|
41
|
+
The pip fallback (`python -m ziglang`) works identically on the host and inside
|
|
42
|
+
cibuildwheel's manylinux containers, where a host-installed `zig` isn't visible.
|
|
43
|
+
"""
|
|
44
|
+
if shutil.which("zig"):
|
|
45
|
+
return ["zig"]
|
|
46
|
+
try:
|
|
47
|
+
import ziglang # noqa: F401
|
|
48
|
+
except ImportError:
|
|
49
|
+
raise RuntimeError(
|
|
50
|
+
"Zig toolchain not found: install Zig and put it on PATH, or `pip install ziglang`."
|
|
51
|
+
) from None
|
|
52
|
+
return [sys.executable, "-m", "ziglang"]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ZigBuildHook(BuildHookInterface):
|
|
56
|
+
"""Compile the Zig extension against the building interpreter during the wheel build.
|
|
57
|
+
|
|
58
|
+
This makes `uv build` / `pip wheel` / cibuildwheel produce a correct, platform-tagged
|
|
59
|
+
wheel with no out-of-band step: the `.so` is built here, against `sys.executable`, and
|
|
60
|
+
`build.zig` installs it into the `zloop/` package as `_zloop<EXT_SUFFIX>`.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
PLUGIN_NAME = "custom"
|
|
64
|
+
|
|
65
|
+
def initialize(self, version: str, build_data: dict[str, Any]) -> None:
|
|
66
|
+
if self.target_name != "wheel":
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
include = sysconfig.get_path("platinclude")
|
|
70
|
+
ext_suffix = sysconfig.get_config_var("EXT_SUFFIX")
|
|
71
|
+
if not include or not ext_suffix:
|
|
72
|
+
raise RuntimeError("could not resolve platinclude / EXT_SUFFIX from the building interpreter")
|
|
73
|
+
|
|
74
|
+
mode = os.environ.get("ZLOOP_BUILD_MODE", "ReleaseFast")
|
|
75
|
+
env = {**os.environ, "ZLOOP_PYTHON_INCLUDE": include, "ZLOOP_EXT_SUFFIX": ext_suffix}
|
|
76
|
+
subprocess.run(
|
|
77
|
+
[*_zig_command(), "build", f"-Doptimize={mode}", *_zig_target_args()],
|
|
78
|
+
cwd=ROOT,
|
|
79
|
+
env=env,
|
|
80
|
+
check=True,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
artifact = f"zloop/_zloop{ext_suffix}"
|
|
84
|
+
if not (ROOT / artifact).exists():
|
|
85
|
+
raise RuntimeError(f"zig build did not produce {artifact}")
|
|
86
|
+
|
|
87
|
+
# Tag the wheel for this interpreter + platform rather than py3-none-any.
|
|
88
|
+
build_data["pure_python"] = False
|
|
89
|
+
build_data["infer_tag"] = True
|
|
90
|
+
build_data["artifacts"].append(artifact)
|
|
91
|
+
|
|
92
|
+
def clean(self, versions: list[str]) -> None:
|
|
93
|
+
for path in ROOT.glob("zloop/_zloop*.so"):
|
|
94
|
+
path.unlink()
|
|
95
|
+
for path in ROOT.glob("zloop/_zloop*.pyd"):
|
|
96
|
+
path.unlink()
|
|
97
|
+
print(f"removed compiled extensions; building Zig core via {sys.executable}", file=sys.stderr)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling", "uv-dynamic-versioning>=0.8.0", "ziglang==0.16.0"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "zloop"
|
|
7
|
+
description = "An asyncio event loop with a Zig core."
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
license = "BSD-3-Clause"
|
|
10
|
+
license-files = ["LICENSE"]
|
|
11
|
+
requires-python = ">=3.12"
|
|
12
|
+
authors = [{ name = "Marcelo Trylesinski", email = "marcelotryle@gmail.com" }]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Framework :: AsyncIO",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Zig",
|
|
18
|
+
]
|
|
19
|
+
dynamic = ["version"]
|
|
20
|
+
|
|
21
|
+
[dependency-groups]
|
|
22
|
+
dev = [
|
|
23
|
+
"pytest>=8.0",
|
|
24
|
+
"pytest-asyncio>=0.24",
|
|
25
|
+
"coverage>=7.0",
|
|
26
|
+
"ruff",
|
|
27
|
+
"mypy",
|
|
28
|
+
"trustme",
|
|
29
|
+
]
|
|
30
|
+
docs = [
|
|
31
|
+
"zensical",
|
|
32
|
+
"matplotlib", # docs/gen_bench_chart.py renders the benchmark chart
|
|
33
|
+
]
|
|
34
|
+
bench = [
|
|
35
|
+
"uvloop", # the comparison baseline for bench_uvloop/bench_ci.py
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[tool.hatch.version]
|
|
39
|
+
source = "uv-dynamic-versioning"
|
|
40
|
+
|
|
41
|
+
[tool.uv-dynamic-versioning]
|
|
42
|
+
vcs = "git"
|
|
43
|
+
style = "pep440"
|
|
44
|
+
bump = true
|
|
45
|
+
metadata = false # drop the +<hash> local segment; PyPI rejects it on uploads
|
|
46
|
+
fallback-version = "0.0.0"
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.wheel]
|
|
49
|
+
packages = ["zloop"]
|
|
50
|
+
artifacts = ["zloop/*.so"]
|
|
51
|
+
|
|
52
|
+
[tool.hatch.build.targets.wheel.hooks.custom]
|
|
53
|
+
path = "hatch_build.py"
|
|
54
|
+
|
|
55
|
+
[tool.hatch.build.targets.sdist]
|
|
56
|
+
# Ship the sources needed to compile the extension from an sdist.
|
|
57
|
+
include = ["zloop", "src", "build.zig", "build_ext.sh", "hatch_build.py"]
|
|
58
|
+
|
|
59
|
+
[tool.cibuildwheel]
|
|
60
|
+
# Zig comes from the `ziglang` build requirement (build isolation), so no
|
|
61
|
+
# system toolchain install is needed. Skip 32-bit, musl, and PyPy: the Zig core
|
|
62
|
+
# targets glibc/macOS and CPython only.
|
|
63
|
+
build = "cp312-* cp313-* cp314-*"
|
|
64
|
+
skip = "*-win* *_i686 *-musllinux_*"
|
|
65
|
+
build-frontend = "build[uv]"
|
|
66
|
+
test-command = 'python -c "import asyncio, zloop; assert asyncio.run(asyncio.sleep(0, 1), loop_factory=zloop.new_event_loop) == 1"'
|
|
67
|
+
|
|
68
|
+
[tool.cibuildwheel.linux]
|
|
69
|
+
# manylinux container has no Zig; the ziglang wheel in the isolated build env
|
|
70
|
+
# provides it. Nothing extra to install. Each runner builds its native arch
|
|
71
|
+
# (x86_64 on ubuntu-latest, aarch64 on ubuntu-24.04-arm) - running the foreign
|
|
72
|
+
# manylinux container without QEMU hangs on `exec format error`.
|
|
73
|
+
archs = ["auto"]
|
|
74
|
+
|
|
75
|
+
[tool.cibuildwheel.macos]
|
|
76
|
+
archs = ["arm64", "x86_64"]
|
|
77
|
+
# Pin the wheel tag's minimum macOS; hatch_build.py passes the same version to
|
|
78
|
+
# Zig so the compiled `.so` and the tag agree and delocate accepts the wheel.
|
|
79
|
+
environment = { MACOSX_DEPLOYMENT_TARGET = "11.0" }
|
|
80
|
+
|
|
81
|
+
[tool.pytest.ini_options]
|
|
82
|
+
addopts = "-rxXs --strict-config --strict-markers"
|
|
83
|
+
asyncio_mode = "auto"
|
|
84
|
+
xfail_strict = true
|
|
85
|
+
testpaths = ["tests"]
|
|
86
|
+
|
|
87
|
+
[tool.coverage.run]
|
|
88
|
+
source_pkgs = ["zloop"]
|
|
89
|
+
branch = true
|
|
90
|
+
parallel = true
|
|
91
|
+
omit = ["zloop/_zloop*"]
|
|
92
|
+
|
|
93
|
+
[tool.coverage.report]
|
|
94
|
+
precision = 2
|
|
95
|
+
fail_under = 100
|
|
96
|
+
show_missing = true
|
|
97
|
+
skip_covered = true
|
|
98
|
+
exclude_lines = [
|
|
99
|
+
"pragma: no cover",
|
|
100
|
+
"if TYPE_CHECKING:",
|
|
101
|
+
"raise NotImplementedError",
|
|
102
|
+
"@overload",
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
[tool.ruff]
|
|
106
|
+
line-length = 120
|
|
107
|
+
|
|
108
|
+
[tool.ruff.lint]
|
|
109
|
+
select = ["E", "F", "I", "FA", "UP", "RUF100"]
|
|
110
|
+
|
|
111
|
+
[tool.ruff.lint.isort]
|
|
112
|
+
combine-as-imports = true
|
|
113
|
+
|
|
114
|
+
[tool.mypy]
|
|
115
|
+
strict = true
|
|
116
|
+
warn_unused_ignores = true
|
|
117
|
+
show_error_codes = true
|
|
118
|
+
files = ["zloop"]
|
|
119
|
+
|
|
120
|
+
[[tool.mypy.overrides]]
|
|
121
|
+
# The connection-setup layer deliberately widens the asyncio loop method
|
|
122
|
+
# signatures (Any / **_) the same way uvloop's stubs do; those intentional
|
|
123
|
+
# overrides and Any-returns aren't real type errors.
|
|
124
|
+
module = "zloop._io"
|
|
125
|
+
disable_error_code = ["override", "no-any-return"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//! Monotonic clock. asyncio's loop.time() is time.monotonic(); we read the same
|
|
2
|
+
//! CLOCK_MONOTONIC source so deadlines computed on the Python side and the Zig
|
|
3
|
+
//! side agree.
|
|
4
|
+
|
|
5
|
+
const std = @import("std");
|
|
6
|
+
const sys = @import("sys.zig");
|
|
7
|
+
|
|
8
|
+
/// Current monotonic time in nanoseconds.
|
|
9
|
+
pub fn nowNs() u64 {
|
|
10
|
+
return sys.monotonicNs();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/// Current monotonic time in seconds as f64, matching time.monotonic().
|
|
14
|
+
pub fn nowSeconds() f64 {
|
|
15
|
+
return @as(f64, @floatFromInt(nowNs())) / std.time.ns_per_s;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const testing = std.testing;
|
|
19
|
+
|
|
20
|
+
test "clock is monotonic non-decreasing" {
|
|
21
|
+
const a = nowNs();
|
|
22
|
+
const b = nowNs();
|
|
23
|
+
try testing.expect(b >= a);
|
|
24
|
+
try testing.expect(nowSeconds() > 0);
|
|
25
|
+
}
|