timeout-iterator 0.2.2__py3-none-any.whl → 0.3.0__py3-none-any.whl
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.
- timeout_iterator/__init__.py +2 -2
- timeout_iterator/_seconds.py +9 -0
- timeout_iterator/_version.py +19 -11
- timeout_iterator/py.typed +0 -0
- timeout_iterator/terminate.py +78 -18
- timeout_iterator/without_terminate.py +42 -11
- {timeout_iterator-0.2.2.dist-info → timeout_iterator-0.3.0.dist-info}/METADATA +68 -7
- timeout_iterator-0.3.0.dist-info/RECORD +11 -0
- {timeout_iterator-0.2.2.dist-info → timeout_iterator-0.3.0.dist-info}/WHEEL +1 -1
- timeout_iterator-0.2.2.dist-info/RECORD +0 -9
- {timeout_iterator-0.2.2.dist-info → timeout_iterator-0.3.0.dist-info/licenses}/LICENSE +0 -0
- {timeout_iterator-0.2.2.dist-info → timeout_iterator-0.3.0.dist-info}/top_level.txt +0 -0
timeout_iterator/__init__.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
from timeout_iterator.without_terminate import without_terminate
|
|
2
1
|
from timeout_iterator.terminate import terminate
|
|
2
|
+
from timeout_iterator.without_terminate import without_terminate
|
|
3
3
|
|
|
4
4
|
# https://packaging-guide.openastronomy.org/en/latest/advanced/versioning.html
|
|
5
5
|
from ._version import __version__
|
|
6
6
|
|
|
7
|
-
__all__ = ["
|
|
7
|
+
__all__ = ["__version__", "terminate", "without_terminate"]
|
timeout_iterator/_version.py
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
|
-
# file generated by
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
2
|
# don't change, don't track in version control
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
9
13
|
|
|
10
14
|
version: str
|
|
11
15
|
__version__: str
|
|
12
|
-
__version_tuple__:
|
|
13
|
-
version_tuple:
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.3.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 3, 0)
|
|
14
23
|
|
|
15
|
-
|
|
16
|
-
__version_tuple__ = version_tuple = (0, 2, 2)
|
|
24
|
+
__commit_id__ = commit_id = 'g2389741fe'
|
|
File without changes
|
timeout_iterator/terminate.py
CHANGED
|
@@ -1,33 +1,93 @@
|
|
|
1
|
-
from typing import TypeVar, Iterable
|
|
2
|
-
import datetime
|
|
3
1
|
import signal
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
from collections.abc import Iterable, Iterator
|
|
5
|
+
from typing import TypeVar
|
|
4
6
|
|
|
7
|
+
from timeout_iterator._seconds import validate_timeout_seconds
|
|
5
8
|
|
|
6
9
|
T = TypeVar("T")
|
|
7
10
|
|
|
8
11
|
|
|
12
|
+
def _ensure_itimer_real_is_available() -> None:
|
|
13
|
+
current_timer = signal.getitimer(signal.ITIMER_REAL)
|
|
14
|
+
if current_timer == (0.0, 0.0):
|
|
15
|
+
return
|
|
16
|
+
|
|
17
|
+
message = "terminate() cannot run while ITIMER_REAL is already active"
|
|
18
|
+
raise RuntimeError(message)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _require_main_thread() -> None:
|
|
22
|
+
if threading.current_thread() is not threading.main_thread():
|
|
23
|
+
raise ValueError(
|
|
24
|
+
"terminate() must be called from the main thread; "
|
|
25
|
+
"signal.signal() and signal.setitimer() are main-thread-only "
|
|
26
|
+
"APIs. Use without_terminate() for thread-safe timeout iteration.",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
9
30
|
# NOTE: float type accepts int
|
|
10
31
|
# https://peps.python.org/pep-0484/#the-numeric-tower
|
|
11
|
-
def terminate(iterable: Iterable[T], seconds: float) ->
|
|
12
|
-
"""Timeout iterator
|
|
32
|
+
def terminate(iterable: Iterable[T], seconds: float) -> Iterator[T]:
|
|
33
|
+
"""Timeout iterator that raises TimeoutError when the timeout expires.
|
|
34
|
+
|
|
35
|
+
Unlike `without_terminate`, this function forcibly raises TimeoutError
|
|
36
|
+
after the specified number of seconds, even mid-iteration. It uses
|
|
37
|
+
``signal.SIGALRM`` / ``signal.setitimer()`` and is Unix-only.
|
|
38
|
+
It cannot be used while another ITIMER_REAL timer is active.
|
|
39
|
+
The timeout must be a positive finite number of seconds.
|
|
13
40
|
|
|
14
|
-
|
|
41
|
+
Caution: ``terminate`` installs a SIGALRM handler for its duration.
|
|
42
|
+
Any SIGALRM that arrives before the timeout elapses is silently
|
|
43
|
+
discarded; the previous handler is *not* called for those signals.
|
|
44
|
+
This means code running concurrently inside the iterator — or
|
|
45
|
+
libraries that also use ``signal.alarm()`` / ``signal.setitimer()``
|
|
46
|
+
— will have their signals suppressed until the iterator exits.
|
|
47
|
+
|
|
48
|
+
Must be called from the main thread of the main interpreter.
|
|
49
|
+
Calling from a worker thread raises ValueError.
|
|
50
|
+
|
|
51
|
+
**Main-thread only**: ``signal.signal()`` and ``signal.setitimer()``
|
|
52
|
+
are Python main-thread APIs. Calling ``terminate()`` from a worker
|
|
53
|
+
thread (e.g. inside ``threading.Thread`` or
|
|
54
|
+
``concurrent.futures.ThreadPoolExecutor``) raises ``ValueError``
|
|
55
|
+
immediately. Use ``without_terminate()`` for thread-safe timeout
|
|
56
|
+
iteration.
|
|
57
|
+
|
|
58
|
+
**Signal scope**: SIGALRM fires at any Python bytecode boundary in the
|
|
59
|
+
calling thread, not only during fetches from the upstream iterable.
|
|
60
|
+
The ``for`` loop body can also be interrupted and raise ``TimeoutError``.
|
|
61
|
+
Keep loop body code interrupt-safe, or use ``without_terminate`` if the
|
|
62
|
+
upstream must be fully consumed before yielding to the caller.
|
|
63
|
+
|
|
64
|
+
The timeout is measured using ``time.monotonic()``, so it counts
|
|
65
|
+
monotonic elapsed time and is unaffected by DST transitions or
|
|
66
|
+
wall-clock adjustments.
|
|
15
67
|
"""
|
|
16
|
-
|
|
17
|
-
|
|
68
|
+
_require_main_thread()
|
|
69
|
+
_ensure_itimer_real_is_available()
|
|
70
|
+
validate_timeout_seconds(seconds)
|
|
71
|
+
start = time.monotonic()
|
|
72
|
+
end = start + seconds
|
|
73
|
+
fired = False
|
|
18
74
|
|
|
19
|
-
def handler(signum: int,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if datetime.datetime.now() < end:
|
|
75
|
+
def handler(signum: int, _frame: object) -> None:
|
|
76
|
+
nonlocal fired
|
|
77
|
+
if fired or signum != signal.SIGALRM or time.monotonic() < end:
|
|
23
78
|
return
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
79
|
+
fired = True
|
|
80
|
+
elapsed = time.monotonic() - start
|
|
81
|
+
raise TimeoutError(
|
|
82
|
+
f"timed out after {elapsed:.3f}s (limit: {seconds}s)",
|
|
83
|
+
)
|
|
28
84
|
|
|
85
|
+
original_handler = signal.signal(signal.SIGALRM, handler)
|
|
29
86
|
try:
|
|
30
|
-
|
|
31
|
-
|
|
87
|
+
signal.setitimer(signal.ITIMER_REAL, seconds)
|
|
88
|
+
try:
|
|
89
|
+
yield from iterable
|
|
90
|
+
finally:
|
|
91
|
+
signal.setitimer(signal.ITIMER_REAL, 0)
|
|
32
92
|
finally:
|
|
33
|
-
signal.
|
|
93
|
+
signal.signal(signal.SIGALRM, original_handler)
|
|
@@ -1,22 +1,53 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
1
|
+
import time
|
|
2
|
+
from collections.abc import Iterable, Iterator
|
|
3
|
+
from typing import Protocol, TypeGuard, TypeVar
|
|
3
4
|
|
|
5
|
+
from timeout_iterator._seconds import validate_timeout_seconds
|
|
4
6
|
|
|
5
7
|
T = TypeVar("T")
|
|
6
8
|
|
|
7
9
|
|
|
10
|
+
class _Closeable(Protocol):
|
|
11
|
+
def close(self) -> None: ...
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _is_closeable(value: object) -> TypeGuard[_Closeable]:
|
|
15
|
+
return callable(getattr(value, "close", None))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _close_if_possible(value: object) -> None:
|
|
19
|
+
if _is_closeable(value):
|
|
20
|
+
value.close()
|
|
21
|
+
|
|
22
|
+
|
|
8
23
|
# NOTE: float type accepts int
|
|
9
24
|
# https://peps.python.org/pep-0484/#the-numeric-tower
|
|
10
|
-
def without_terminate(iterable: Iterable[T], seconds: float) ->
|
|
11
|
-
"""Timeout iterator
|
|
25
|
+
def without_terminate(iterable: Iterable[T], seconds: float) -> Iterator[T]:
|
|
26
|
+
"""Timeout iterator that stops yielding items once the deadline elapses.
|
|
27
|
+
|
|
28
|
+
Unlike `terminate`, this function does NOT raise TimeoutError when the
|
|
29
|
+
timeout expires and does not use signals, so it is safe to use in
|
|
30
|
+
environments that restrict SIGALRM. It simply stops yielding further
|
|
31
|
+
items after the deadline.
|
|
32
|
+
|
|
33
|
+
The timeout is checked *after* each item is fetched from the upstream
|
|
34
|
+
iterator, not before. This means that if the deadline expires while a
|
|
35
|
+
fetch is in progress (or just after one completes), the fetched item is
|
|
36
|
+
silently dropped — consumed from the upstream but not yielded to the
|
|
37
|
+
caller. At most one item may be lost this way at the boundary.
|
|
38
|
+
|
|
39
|
+
The trade-off is that it cannot forcibly interrupt a blocking upstream
|
|
40
|
+
fetch or a task that is running between yields.
|
|
12
41
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
42
|
+
The timeout is measured using ``time.monotonic()``, so it counts
|
|
43
|
+
monotonic elapsed time and is unaffected by DST transitions or
|
|
44
|
+
wall-clock adjustments.
|
|
16
45
|
"""
|
|
17
|
-
|
|
18
|
-
end =
|
|
19
|
-
|
|
20
|
-
|
|
46
|
+
validate_timeout_seconds(seconds)
|
|
47
|
+
end = time.monotonic() + seconds
|
|
48
|
+
iterator = iter(iterable)
|
|
49
|
+
for item in iterator:
|
|
50
|
+
if time.monotonic() > end:
|
|
51
|
+
_close_if_possible(iterator)
|
|
21
52
|
break
|
|
22
53
|
yield item
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: timeout-iterator
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Iterator with timeout
|
|
5
5
|
Author-email: Yui KITSU <kitsuyui+github@kitsuyui.com>
|
|
6
6
|
License: Copyright 2024-2025 Kitsu Yui
|
|
@@ -17,16 +17,19 @@ License: Copyright 2024-2025 Kitsu Yui
|
|
|
17
17
|
Project-URL: Homepage, https://github.com/kitsuyui/python-timeout-iterator
|
|
18
18
|
Classifier: Development Status :: 3 - Alpha
|
|
19
19
|
Classifier: License :: OSI Approved :: BSD License
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.10
|
|
22
21
|
Classifier: Programming Language :: Python :: 3.11
|
|
23
22
|
Classifier: Programming Language :: Python :: 3.12
|
|
24
23
|
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
25
25
|
Classifier: Intended Audience :: Developers
|
|
26
26
|
Classifier: Topic :: Software Development :: Libraries
|
|
27
|
-
|
|
27
|
+
Classifier: Operating System :: POSIX
|
|
28
|
+
Classifier: Operating System :: MacOS
|
|
29
|
+
Requires-Python: >=3.10
|
|
28
30
|
Description-Content-Type: text/markdown
|
|
29
31
|
License-File: LICENSE
|
|
32
|
+
Dynamic: license-file
|
|
30
33
|
|
|
31
34
|
# python-timeout-iterator
|
|
32
35
|
|
|
@@ -34,7 +37,7 @@ License-File: LICENSE
|
|
|
34
37
|
|
|
35
38
|
[](https://badge.fury.io/py/timeout-iterator)
|
|
36
39
|
[](https://pypi.python.org/pypi/timeout-iterator/)
|
|
37
|
-
|
|
40
|
+

|
|
38
41
|
[](https://opensource.org/licenses/BSD-3-Clause)
|
|
39
42
|
|
|
40
43
|
## Installation
|
|
@@ -47,10 +50,13 @@ $ pip install timeout-iterator
|
|
|
47
50
|
|
|
48
51
|
### without_terminate
|
|
49
52
|
|
|
50
|
-
`without_terminate`
|
|
51
|
-
|
|
53
|
+
`without_terminate` stops yielding items once the timeout elapses without
|
|
54
|
+
raising an exception. It is safe to use on all platforms because it does not
|
|
55
|
+
use signals.
|
|
52
56
|
|
|
53
57
|
```python
|
|
58
|
+
import time
|
|
59
|
+
|
|
54
60
|
from timeout_iterator import without_terminate
|
|
55
61
|
results = []
|
|
56
62
|
for i in without_terminate(range(10), seconds=0.3):
|
|
@@ -60,11 +66,28 @@ for i in without_terminate(range(10), seconds=0.3):
|
|
|
60
66
|
assert results == [0, 1, 2]
|
|
61
67
|
```
|
|
62
68
|
|
|
69
|
+
**Boundary behavior**: The timeout is checked *after* each item is fetched
|
|
70
|
+
from the upstream iterator. If the deadline passes between a fetch and its
|
|
71
|
+
yield, the fetched item is silently dropped. At most one item near the
|
|
72
|
+
boundary may be consumed from the upstream but not passed to the caller.
|
|
73
|
+
This differs from `terminate`, which delivers every item fetched before the
|
|
74
|
+
SIGALRM fires.
|
|
75
|
+
|
|
63
76
|
### terminate
|
|
64
77
|
|
|
65
78
|
`terminate` is a generator that it will raise an exception after the timeout.
|
|
79
|
+
This function uses `signal.SIGALRM` and `signal.setitimer()`, so it is only
|
|
80
|
+
supported on Unix-like platforms that provide those signal APIs.
|
|
81
|
+
|
|
82
|
+
**Signal interaction**: `terminate` installs a `SIGALRM` handler for its
|
|
83
|
+
duration. Any `SIGALRM` that arrives before the timeout elapses is silently
|
|
84
|
+
discarded — the previous handler is not called for those signals. Libraries or
|
|
85
|
+
code that also use `signal.alarm()` / `signal.setitimer()` will have their
|
|
86
|
+
signals suppressed until the iterator exits.
|
|
66
87
|
|
|
67
88
|
```python
|
|
89
|
+
import time
|
|
90
|
+
|
|
68
91
|
from timeout_iterator import terminate
|
|
69
92
|
try:
|
|
70
93
|
results = []
|
|
@@ -77,6 +100,44 @@ except TimeoutError:
|
|
|
77
100
|
assert results == [0, 1, 2]
|
|
78
101
|
```
|
|
79
102
|
|
|
103
|
+
**Main-thread only**: `signal.signal()` and `signal.setitimer()` are
|
|
104
|
+
main-thread APIs in CPython. Calling `terminate()` from a worker thread
|
|
105
|
+
(e.g. inside `threading.Thread` or `concurrent.futures.ThreadPoolExecutor`)
|
|
106
|
+
raises `ValueError` immediately. Use `without_terminate()` for thread-safe
|
|
107
|
+
timeout iteration.
|
|
108
|
+
|
|
109
|
+
**Signal scope**: `SIGALRM` is delivered at any Python bytecode boundary in
|
|
110
|
+
the calling thread — not only during fetches from the upstream iterator. The
|
|
111
|
+
loop body itself can be interrupted and raise `TimeoutError`. Keep loop body
|
|
112
|
+
code safe to interrupt at any point, or use `without_terminate` if you need the
|
|
113
|
+
upstream items to be fully consumed before yielding control.
|
|
114
|
+
## Development
|
|
115
|
+
|
|
116
|
+
This repository uses [lefthook](https://lefthook.dev/) to run the same checks as CI
|
|
117
|
+
locally, so problems surface before they reach CI.
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
# Install dependencies
|
|
121
|
+
uv sync
|
|
122
|
+
|
|
123
|
+
# Install the Git hooks (once; requires lefthook on your PATH)
|
|
124
|
+
lefthook install
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Once installed, the hooks run automatically:
|
|
128
|
+
|
|
129
|
+
- **pre-commit**: `uv run poe check`
|
|
130
|
+
- **pre-push**: `uv run poe check` and `uv run poe test`
|
|
131
|
+
|
|
132
|
+
You can also run the checks manually:
|
|
133
|
+
|
|
134
|
+
```sh
|
|
135
|
+
uv run poe check
|
|
136
|
+
uv run poe test
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
CI still runs the full matrix (see `.github/workflows/`); the hooks only bring that
|
|
140
|
+
feedback earlier on your machine.
|
|
80
141
|
# LICENSE
|
|
81
142
|
|
|
82
143
|
BSD 3-Clause License
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
timeout_iterator/__init__.py,sha256=FFOkrfbOPzqGZ9Tdky1Jh1AEGHzl93alRlKiOsrGAy8,289
|
|
2
|
+
timeout_iterator/_seconds.py,sha256=H8gVu4qvSvS4g6MuspdZ4Dpxn5cXI48kHU0uk8FvEpM,218
|
|
3
|
+
timeout_iterator/_version.py,sha256=rc1GsnWwMvRGh39VX1_37B4EoeA79pQ2v_jvz1p3zIw,528
|
|
4
|
+
timeout_iterator/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
timeout_iterator/terminate.py,sha256=V4LJ1bhFsb_KXNFrEP6fygnp3r7pRcNe-k0-IPcN73U,3647
|
|
6
|
+
timeout_iterator/without_terminate.py,sha256=RvHfToUweVvEHkvTUpalDTRbQfyZSX3E-0n8IA35XRQ,1866
|
|
7
|
+
timeout_iterator-0.3.0.dist-info/licenses/LICENSE,sha256=i_4Vm9uYuomNQI4x5y04-L0mrzPFBnioSHjfSsQHWa0,1458
|
|
8
|
+
timeout_iterator-0.3.0.dist-info/METADATA,sha256=lUPtFBy5M9j5DYCWa-mM_EBapc_PLMPLfJrCu6Qdt5U,5996
|
|
9
|
+
timeout_iterator-0.3.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
10
|
+
timeout_iterator-0.3.0.dist-info/top_level.txt,sha256=nvIqkq7driA1qK_Go6asLhagba5hwEx_mgvJNx7OUsg,17
|
|
11
|
+
timeout_iterator-0.3.0.dist-info/RECORD,,
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
timeout_iterator/__init__.py,sha256=ul6_kSumsg72svZShaWJQeafVkyo4JFnKKXGxOyfbA8,289
|
|
2
|
-
timeout_iterator/_version.py,sha256=RrHB9KG1O3GPm--rbTedqmZbdDrbgeRLXBmT4OBUqqI,411
|
|
3
|
-
timeout_iterator/terminate.py,sha256=fwsKwenq1aJxGj-bux8b_jee91lBRc-AlBNOI1WTZnU,869
|
|
4
|
-
timeout_iterator/without_terminate.py,sha256=-HQ7WfHdNcUdwpSYHVET95zQaQzq5ejgGKPWsoPGDr0,637
|
|
5
|
-
timeout_iterator-0.2.2.dist-info/LICENSE,sha256=i_4Vm9uYuomNQI4x5y04-L0mrzPFBnioSHjfSsQHWa0,1458
|
|
6
|
-
timeout_iterator-0.2.2.dist-info/METADATA,sha256=uDwEqmMvZAsSPTkWIBjGIf3NIjhti7Z9G-fHa3cYWh8,3663
|
|
7
|
-
timeout_iterator-0.2.2.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
8
|
-
timeout_iterator-0.2.2.dist-info/top_level.txt,sha256=nvIqkq7driA1qK_Go6asLhagba5hwEx_mgvJNx7OUsg,17
|
|
9
|
-
timeout_iterator-0.2.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|