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.
- pymizu-0.0.1.dev0/LICENSE +21 -0
- pymizu-0.0.1.dev0/PKG-INFO +65 -0
- pymizu-0.0.1.dev0/README.md +46 -0
- pymizu-0.0.1.dev0/pyproject.toml +44 -0
- pymizu-0.0.1.dev0/setup.cfg +4 -0
- pymizu-0.0.1.dev0/src/pymizu/__init__.py +186 -0
- pymizu-0.0.1.dev0/src/pymizu/py.typed +1 -0
- pymizu-0.0.1.dev0/src/pymizu.egg-info/PKG-INFO +65 -0
- pymizu-0.0.1.dev0/src/pymizu.egg-info/SOURCES.txt +10 -0
- pymizu-0.0.1.dev0/src/pymizu.egg-info/dependency_links.txt +1 -0
- pymizu-0.0.1.dev0/src/pymizu.egg-info/top_level.txt +1 -0
- pymizu-0.0.1.dev0/tests/test_channel.py +185 -0
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -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))
|