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.
@@ -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__ = ["terminate", "without_terminate", "__version__"]
7
+ __all__ = ["__version__", "terminate", "without_terminate"]
@@ -0,0 +1,9 @@
1
+ import math
2
+
3
+
4
+ def validate_timeout_seconds(seconds: float) -> None:
5
+ if seconds > 0 and math.isfinite(seconds):
6
+ return
7
+
8
+ message = "seconds must be a positive finite number"
9
+ raise ValueError(message)
@@ -1,16 +1,24 @@
1
- # file generated by setuptools_scm
1
+ # file generated by vcs-versioning
2
2
  # don't change, don't track in version control
3
- TYPE_CHECKING = False
4
- if TYPE_CHECKING:
5
- from typing import Tuple, Union
6
- VERSION_TUPLE = Tuple[Union[int, str], ...]
7
- else:
8
- VERSION_TUPLE = object
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__: VERSION_TUPLE
13
- version_tuple: 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
- __version__ = version = '0.2.2'
16
- __version_tuple__ = version_tuple = (0, 2, 2)
24
+ __commit_id__ = commit_id = 'g2389741fe'
File without changes
@@ -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) -> Iterable[T]:
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
- This iterator forcibly terminates a running task after the specified number of seconds.
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
- now = datetime.datetime.now()
17
- end = now + datetime.timedelta(seconds=seconds)
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, frame: object) -> None:
20
- if signum != signal.SIGALRM:
21
- return
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
- raise TimeoutError
25
-
26
- signal.signal(signal.SIGALRM, handler)
27
- signal.setitimer(signal.ITIMER_REAL, seconds)
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
- for item in iterable:
31
- yield item
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.setitimer(signal.ITIMER_REAL, 0)
93
+ signal.signal(signal.SIGALRM, original_handler)
@@ -1,22 +1,53 @@
1
- from typing import TypeVar, Iterable
2
- import datetime
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) -> Iterable[T]:
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
- This iterator times out after the specified number of seconds.
14
- This iterator cannot forcibly terminate a running task.
15
- It just ends without executing the next task.
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
- now = datetime.datetime.now()
18
- end = now + datetime.timedelta(seconds=seconds)
19
- for item in iterable:
20
- if datetime.datetime.now() > end:
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.2
1
+ Metadata-Version: 2.4
2
2
  Name: timeout-iterator
3
- Version: 0.2.2
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
- Requires-Python: >=3.9
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
  [![Python](https://img.shields.io/pypi/pyversions/timeout-iterator.svg)](https://badge.fury.io/py/timeout-iterator)
36
39
  [![PyPI version](https://img.shields.io/pypi/v/timeout-iterator.svg)](https://pypi.python.org/pypi/timeout-iterator/)
37
- [![codecov](https://codecov.io/gh/kitsuyui/python-timeout-iterator/graph/badge.svg?token=D608CG7CUH)](https://codecov.io/gh/kitsuyui/python-timeout-iterator)
40
+ ![Coverage](https://raw.githubusercontent.com/kitsuyui/octocov-central/main/badges/kitsuyui/python-timeout-iterator/coverage.svg)
38
41
  [![License](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](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` is a generator that it will not yield after the timeout,
51
- but it will not raise an exception.
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,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.0)
2
+ Generator: setuptools (84.2.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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,,