pymizu 0.0.1.dev0__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.
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+
3
+ Copyright (c) 2026 pymizu authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,65 @@
1
+ Metadata-Version: 2.4
2
+ Name: pymizu
3
+ Version: 0.0.1.dev0
4
+ Summary: Single-producer single-consumer channels
5
+ Author: pymizu authors
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/shikokuchuo/mizu-py
8
+ Project-URL: Issues, https://github.com/shikokuchuo/mizu-py/issues
9
+ Keywords: channel,concurrency,spsc
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Software Development :: Libraries
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Dynamic: license-file
19
+
20
+ # pymizu
21
+
22
+ Single-producer single-consumer channels for Python: a bounded ring
23
+ buffer with optional timeouts on receives and sentinel return values
24
+ for full, timeout, and closed states. The transport is in-process.
25
+
26
+ ## Installation
27
+
28
+ ```sh
29
+ pip install pymizu
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ```python
35
+ import pymizu
36
+
37
+ ch = pymizu.inproc_channel(2)
38
+
39
+ ch.send("a")
40
+ ch.send(42)
41
+ ch.send("b") # full: returns the pymizu.FULL sentinel
42
+
43
+ ch.recv()
44
+ # 'a'
45
+ ch.recv()
46
+ # 42
47
+ ch.recv(timeout=0) # empty: do not wait
48
+ # <pymizu.TIMEOUT>
49
+
50
+ ch.close()
51
+ ```
52
+
53
+ `timeout` is in seconds on a monotonic clock: `None` (the default) and
54
+ `math.inf` wait indefinitely, and `0` polls. The buffer is
55
+ lock-guarded, so a producer thread and a consumer thread may exchange
56
+ values without an external lock.
57
+
58
+ Terminal states surface as sentinel singletons, not exceptions:
59
+ `pymizu.FULL`, `pymizu.TIMEOUT`, and `pymizu.CLOSED`.
60
+ `pymizu.is_sentinel(x)` is an identity test for use where payloads are
61
+ untrusted.
62
+
63
+ ## License
64
+
65
+ MIT
@@ -0,0 +1,46 @@
1
+ # pymizu
2
+
3
+ Single-producer single-consumer channels for Python: a bounded ring
4
+ buffer with optional timeouts on receives and sentinel return values
5
+ for full, timeout, and closed states. The transport is in-process.
6
+
7
+ ## Installation
8
+
9
+ ```sh
10
+ pip install pymizu
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```python
16
+ import pymizu
17
+
18
+ ch = pymizu.inproc_channel(2)
19
+
20
+ ch.send("a")
21
+ ch.send(42)
22
+ ch.send("b") # full: returns the pymizu.FULL sentinel
23
+
24
+ ch.recv()
25
+ # 'a'
26
+ ch.recv()
27
+ # 42
28
+ ch.recv(timeout=0) # empty: do not wait
29
+ # <pymizu.TIMEOUT>
30
+
31
+ ch.close()
32
+ ```
33
+
34
+ `timeout` is in seconds on a monotonic clock: `None` (the default) and
35
+ `math.inf` wait indefinitely, and `0` polls. The buffer is
36
+ lock-guarded, so a producer thread and a consumer thread may exchange
37
+ values without an external lock.
38
+
39
+ Terminal states surface as sentinel singletons, not exceptions:
40
+ `pymizu.FULL`, `pymizu.TIMEOUT`, and `pymizu.CLOSED`.
41
+ `pymizu.is_sentinel(x)` is an identity test for use where payloads are
42
+ untrusted.
43
+
44
+ ## License
45
+
46
+ MIT
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pymizu"
7
+ version = "0.0.1.dev0"
8
+ description = "Single-producer single-consumer channels"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [{ name = "pymizu authors" }]
13
+ keywords = ["channel", "concurrency", "spsc"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "Operating System :: OS Independent",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: Software Development :: Libraries",
20
+ ]
21
+ dependencies = []
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/shikokuchuo/mizu-py"
25
+ Issues = "https://github.com/shikokuchuo/mizu-py/issues"
26
+
27
+ [tool.setuptools]
28
+ package-dir = { "" = "src" }
29
+ packages = ["pymizu"]
30
+
31
+ [tool.setuptools.package-data]
32
+ pymizu = ["py.typed"]
33
+
34
+ [tool.ruff]
35
+ target-version = "py310"
36
+ line-length = 79
37
+ src = ["src"]
38
+
39
+ [tool.ruff.lint]
40
+ select = ["E", "F", "W", "I", "UP", "B"]
41
+
42
+ [tool.pytest.ini_options]
43
+ testpaths = ["tests"]
44
+ pythonpath = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,186 @@
1
+ """Single-producer single-consumer channels for Python.
2
+
3
+ A bounded ring buffer with optional timeouts on receives and sentinel
4
+ return values for full, timeout, and closed states. The transport is
5
+ in-process: both ends of a channel live in the calling Python process,
6
+ which suits the channel to prototyping producer and consumer designs
7
+ and to testing channel-based logic.
8
+
9
+ This interface is experimental and may change in a future release.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import copy
15
+ import math
16
+ import threading
17
+ import time
18
+ from collections import deque
19
+ from typing import Any
20
+
21
+ __version__ = "0.0.1.dev0"
22
+
23
+ __all__ = [
24
+ "CLOSED",
25
+ "FULL",
26
+ "TIMEOUT",
27
+ "InprocChannel",
28
+ "Sentinel",
29
+ "__version__",
30
+ "inproc_channel",
31
+ "is_sentinel",
32
+ ]
33
+
34
+
35
+ class Sentinel:
36
+ """A channel terminal state as an ordinary value, not an exception.
37
+
38
+ The singletons FULL, TIMEOUT, and CLOSED are interned: identity is
39
+ the test, and is_sentinel() is provenance — True only for the
40
+ exact objects the channel verbs return. A sentinel forwarded as a
41
+ payload arrives as an ordinary copy.
42
+ """
43
+
44
+ __slots__ = ("_name",)
45
+
46
+ def __init__(self, name: str) -> None:
47
+ self._name = name
48
+
49
+ def __repr__(self) -> str:
50
+ return f"<pymizu.{self._name}>"
51
+
52
+ def __copy__(self) -> Sentinel:
53
+ return Sentinel(self._name)
54
+
55
+
56
+ FULL = Sentinel("FULL")
57
+ """The buffer is at capacity (InprocChannel.send())."""
58
+
59
+ TIMEOUT = Sentinel("TIMEOUT")
60
+ """No value arrived within timeout (InprocChannel.recv())."""
61
+
62
+ CLOSED = Sentinel("CLOSED")
63
+ """The channel is closed, and drained for InprocChannel.recv()."""
64
+
65
+ _SENTINELS = (FULL, TIMEOUT, CLOSED)
66
+
67
+
68
+ def is_sentinel(x: Any) -> bool:
69
+ """Test for a pymizu sentinel.
70
+
71
+ Identity comparison against the interned sentinel singletons —
72
+ True only for the exact objects the channel verbs themselves
73
+ return. A sentinel forwarded over a channel arrives as an ordinary
74
+ copy and fails this test, so code that relays untrusted values can
75
+ distinguish its own terminal states from look-alike payloads.
76
+ """
77
+ return any(x is s for s in _SENTINELS)
78
+
79
+
80
+ class InprocChannel:
81
+ """A single-producer single-consumer channel: a bounded ring buffer.
82
+
83
+ Sends and receives are O(1). Both ends of the channel live in the
84
+ calling process; the buffer is lock-guarded, so a producer thread
85
+ and a consumer thread may exchange values without an external
86
+ lock.
87
+ """
88
+
89
+ __slots__ = ("_buf", "_capacity", "_closed", "_cond")
90
+
91
+ def __init__(self, capacity: int) -> None:
92
+ if isinstance(capacity, bool) or not isinstance(capacity, int):
93
+ raise TypeError("pymizu: capacity must be a positive integer")
94
+ if capacity < 1:
95
+ raise ValueError("pymizu: capacity must be a positive integer")
96
+ self._buf: deque[Any] = deque()
97
+ self._capacity = capacity
98
+ self._closed = False
99
+ self._cond = threading.Condition()
100
+
101
+ def __repr__(self) -> str:
102
+ with self._cond:
103
+ state = "closed" if self._closed else "open"
104
+ return (
105
+ f"<pymizu.InprocChannel: {state}, "
106
+ f"{len(self._buf)}/{self._capacity} buffered>"
107
+ )
108
+
109
+ def send(self, x: Any) -> bool | Sentinel:
110
+ """Append x to the buffer.
111
+
112
+ Sends never block: a send on a full channel returns FULL
113
+ immediately, and a send on a closed channel returns CLOSED.
114
+ Returns True on success.
115
+ """
116
+ with self._cond:
117
+ if self._closed:
118
+ return CLOSED
119
+ if len(self._buf) == self._capacity:
120
+ return FULL
121
+ self._buf.append(x)
122
+ self._cond.notify()
123
+ return True
124
+
125
+ def recv(self, timeout: float | None = None) -> Any:
126
+ """Remove and return the oldest buffered value.
127
+
128
+ Waits up to timeout seconds for one to arrive: None (the
129
+ default) and math.inf wait indefinitely, and 0 polls. Returns
130
+ TIMEOUT when the wait expires. A closed channel drains its
131
+ buffered values before recv() reports CLOSED.
132
+
133
+ The wait is on a monotonic clock: system clock adjustments do
134
+ not affect it.
135
+ """
136
+ deadline = _recv_deadline(timeout)
137
+ with self._cond:
138
+ while not self._buf:
139
+ if self._closed:
140
+ return CLOSED
141
+ if deadline is None:
142
+ self._cond.wait()
143
+ continue
144
+ remaining = deadline - time.monotonic()
145
+ if remaining <= 0:
146
+ return TIMEOUT
147
+ self._cond.wait(remaining)
148
+ x = self._buf.popleft()
149
+ if is_sentinel(x):
150
+ # A forwarded sentinel crosses as an ordinary copy, so a
151
+ # returned singleton always tags a terminal state.
152
+ return copy.copy(x)
153
+ return x
154
+
155
+ def close(self) -> bool:
156
+ """Orderly shutdown.
157
+
158
+ Values buffered before the close remain receivable; further
159
+ sends return CLOSED, and a receive on a drained channel
160
+ returns the same. Closing an already-closed channel is a
161
+ no-op. Returns True.
162
+ """
163
+ with self._cond:
164
+ self._closed = True
165
+ self._cond.notify_all()
166
+ return True
167
+
168
+
169
+ def _recv_deadline(timeout: float | None) -> float | None:
170
+ if timeout is None or timeout == math.inf:
171
+ return None
172
+ if isinstance(timeout, bool) or not isinstance(timeout, (int, float)):
173
+ raise TypeError("pymizu: timeout must be a non-negative number")
174
+ if math.isnan(timeout) or timeout < 0:
175
+ raise ValueError("pymizu: timeout must be a non-negative number")
176
+ return time.monotonic() + timeout
177
+
178
+
179
+ def inproc_channel(capacity: int) -> InprocChannel:
180
+ """Create a single-producer single-consumer channel.
181
+
182
+ capacity is a positive integer: the number of values the channel
183
+ buffers before send() returns the FULL sentinel. Returns an
184
+ InprocChannel.
185
+ """
186
+ return InprocChannel(capacity)
@@ -0,0 +1 @@
1
+ # PEP 561 marker: this package ships inline type information.
@@ -0,0 +1,65 @@
1
+ Metadata-Version: 2.4
2
+ Name: pymizu
3
+ Version: 0.0.1.dev0
4
+ Summary: Single-producer single-consumer channels
5
+ Author: pymizu authors
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/shikokuchuo/mizu-py
8
+ Project-URL: Issues, https://github.com/shikokuchuo/mizu-py/issues
9
+ Keywords: channel,concurrency,spsc
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Software Development :: Libraries
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Dynamic: license-file
19
+
20
+ # pymizu
21
+
22
+ Single-producer single-consumer channels for Python: a bounded ring
23
+ buffer with optional timeouts on receives and sentinel return values
24
+ for full, timeout, and closed states. The transport is in-process.
25
+
26
+ ## Installation
27
+
28
+ ```sh
29
+ pip install pymizu
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ```python
35
+ import pymizu
36
+
37
+ ch = pymizu.inproc_channel(2)
38
+
39
+ ch.send("a")
40
+ ch.send(42)
41
+ ch.send("b") # full: returns the pymizu.FULL sentinel
42
+
43
+ ch.recv()
44
+ # 'a'
45
+ ch.recv()
46
+ # 42
47
+ ch.recv(timeout=0) # empty: do not wait
48
+ # <pymizu.TIMEOUT>
49
+
50
+ ch.close()
51
+ ```
52
+
53
+ `timeout` is in seconds on a monotonic clock: `None` (the default) and
54
+ `math.inf` wait indefinitely, and `0` polls. The buffer is
55
+ lock-guarded, so a producer thread and a consumer thread may exchange
56
+ values without an external lock.
57
+
58
+ Terminal states surface as sentinel singletons, not exceptions:
59
+ `pymizu.FULL`, `pymizu.TIMEOUT`, and `pymizu.CLOSED`.
60
+ `pymizu.is_sentinel(x)` is an identity test for use where payloads are
61
+ untrusted.
62
+
63
+ ## License
64
+
65
+ MIT
@@ -0,0 +1,10 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/pymizu/__init__.py
5
+ src/pymizu/py.typed
6
+ src/pymizu.egg-info/PKG-INFO
7
+ src/pymizu.egg-info/SOURCES.txt
8
+ src/pymizu.egg-info/dependency_links.txt
9
+ src/pymizu.egg-info/top_level.txt
10
+ tests/test_channel.py
@@ -0,0 +1 @@
1
+ pymizu
@@ -0,0 +1,185 @@
1
+ import gc
2
+ import math
3
+ import threading
4
+ import time
5
+
6
+ import pytest
7
+
8
+ from pymizu import (
9
+ CLOSED,
10
+ FULL,
11
+ TIMEOUT,
12
+ InprocChannel,
13
+ Sentinel,
14
+ inproc_channel,
15
+ is_sentinel,
16
+ )
17
+
18
+
19
+ def test_values_round_trip_in_fifo_order():
20
+ ch = inproc_channel(2)
21
+ assert isinstance(ch, InprocChannel)
22
+ ch.send("a")
23
+ ch.send(42)
24
+ assert ch.recv() == "a"
25
+ assert ch.recv() == 42
26
+
27
+
28
+ def test_none_is_a_legal_payload():
29
+ ch = inproc_channel(1)
30
+ ch.send(None)
31
+ assert ch.recv() is None
32
+
33
+
34
+ def test_the_ring_wraps():
35
+ ch = inproc_channel(2)
36
+ for i in range(5):
37
+ ch.send(i)
38
+ assert ch.recv() == i
39
+ ch.send(1)
40
+ ch.send(2)
41
+ assert ch.send(3) is FULL
42
+ assert ch.recv() == 1
43
+ ch.send(3)
44
+ assert ch.recv() == 2
45
+ assert ch.recv() == 3
46
+
47
+
48
+ def test_a_large_ring_preserves_order_and_integrity():
49
+ n = 10000
50
+ ch = inproc_channel(n)
51
+ for i in range(n):
52
+ ch.send(i)
53
+ assert ch.send(n) is FULL
54
+ assert [ch.recv() for _ in range(n)] == list(range(n))
55
+
56
+
57
+ def test_the_buffer_survives_garbage_collection():
58
+ ch = inproc_channel(2)
59
+ ch.send("a")
60
+ gc.collect()
61
+ assert ch.recv() == "a"
62
+
63
+
64
+ def test_send_on_a_full_channel_returns_full_without_waiting():
65
+ ch = inproc_channel(1)
66
+ assert ch.send("a") is True
67
+ assert ch.send("b") is FULL
68
+
69
+
70
+ def test_recv_on_an_empty_channel_returns_timeout():
71
+ ch = inproc_channel(1)
72
+ assert ch.recv(timeout=0) is TIMEOUT
73
+ assert ch.recv(timeout=0.01) is TIMEOUT
74
+
75
+
76
+ def test_the_timeout_is_respected():
77
+ ch = inproc_channel(1)
78
+ start = time.monotonic()
79
+ assert ch.recv(timeout=0.05) is TIMEOUT
80
+ assert time.monotonic() - start >= 0.04
81
+
82
+
83
+ def test_close_drains_then_reports_closed():
84
+ ch = inproc_channel(2)
85
+ ch.send("a")
86
+ assert ch.close() is True
87
+ assert ch.recv() == "a"
88
+ assert ch.recv() is CLOSED
89
+ assert ch.send("b") is CLOSED
90
+ assert ch.close() is True
91
+
92
+
93
+ def test_is_sentinel_is_an_identity_test():
94
+ assert is_sentinel(FULL)
95
+ assert is_sentinel(TIMEOUT)
96
+ assert is_sentinel(CLOSED)
97
+ assert not is_sentinel("a")
98
+ assert not is_sentinel(None)
99
+ assert not is_sentinel(Sentinel("TIMEOUT"))
100
+
101
+
102
+ def test_a_forwarded_sentinel_arrives_as_an_ordinary_copy():
103
+ ch = inproc_channel(1)
104
+ ch.send(TIMEOUT)
105
+ x = ch.recv()
106
+ assert isinstance(x, Sentinel)
107
+ assert repr(x) == repr(TIMEOUT)
108
+ assert not is_sentinel(x)
109
+
110
+
111
+ def test_a_sentinel_payload_drains_before_the_closed_sentinel():
112
+ ch = inproc_channel(1)
113
+ ch.send(FULL)
114
+ ch.close()
115
+ x = ch.recv()
116
+ assert isinstance(x, Sentinel)
117
+ assert not is_sentinel(x)
118
+ assert ch.recv() is CLOSED
119
+
120
+
121
+ def test_a_channel_repr_shows_state_and_occupancy():
122
+ ch = inproc_channel(2)
123
+ assert repr(ch) == "<pymizu.InprocChannel: open, 0/2 buffered>"
124
+ ch.send("a")
125
+ assert repr(ch) == "<pymizu.InprocChannel: open, 1/2 buffered>"
126
+ ch.close()
127
+ assert repr(ch) == "<pymizu.InprocChannel: closed, 1/2 buffered>"
128
+
129
+
130
+ def test_sentinels_repr_as_their_name():
131
+ assert repr(FULL) == "<pymizu.FULL>"
132
+ assert repr(TIMEOUT) == "<pymizu.TIMEOUT>"
133
+ assert repr(CLOSED) == "<pymizu.CLOSED>"
134
+
135
+
136
+ def test_misuse_is_an_error():
137
+ with pytest.raises(ValueError, match="capacity"):
138
+ inproc_channel(0)
139
+ with pytest.raises(TypeError, match="capacity"):
140
+ inproc_channel(1.5)
141
+ with pytest.raises(TypeError, match="capacity"):
142
+ inproc_channel(True)
143
+ ch = inproc_channel(1)
144
+ with pytest.raises(ValueError, match="timeout"):
145
+ ch.recv(timeout=-1)
146
+ with pytest.raises(ValueError, match="timeout"):
147
+ ch.recv(timeout=math.nan)
148
+ with pytest.raises(TypeError, match="timeout"):
149
+ ch.recv(timeout="1")
150
+
151
+
152
+ def test_recv_waits_indefinitely_by_default():
153
+ ch = inproc_channel(1)
154
+ threading.Timer(0.05, ch.send, args=("a",)).start()
155
+ assert ch.recv() == "a"
156
+
157
+
158
+ def test_an_infinite_timeout_waits_indefinitely():
159
+ ch = inproc_channel(1)
160
+ threading.Timer(0.05, ch.send, args=("a",)).start()
161
+ assert ch.recv(timeout=math.inf) == "a"
162
+
163
+
164
+ def test_close_wakes_a_blocked_recv():
165
+ ch = inproc_channel(1)
166
+ threading.Timer(0.05, ch.close).start()
167
+ assert ch.recv() is CLOSED
168
+
169
+
170
+ def test_producer_and_consumer_across_threads():
171
+ n = 1000
172
+ ch = inproc_channel(8)
173
+ out = []
174
+
175
+ def consume():
176
+ for _ in range(n):
177
+ out.append(ch.recv())
178
+
179
+ consumer = threading.Thread(target=consume, daemon=True)
180
+ consumer.start()
181
+ for i in range(n):
182
+ while ch.send(i) is FULL:
183
+ time.sleep(0)
184
+ consumer.join(timeout=10)
185
+ assert out == list(range(n))