processit 0.2.0__tar.gz → 0.2.2__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.
- {processit-0.2.0/src/processit.egg-info → processit-0.2.2}/PKG-INFO +31 -2
- {processit-0.2.0 → processit-0.2.2}/README.md +30 -1
- processit-0.2.2/src/processit/_version.py +2 -0
- {processit-0.2.0 → processit-0.2.2}/src/processit/progress.py +112 -20
- {processit-0.2.0 → processit-0.2.2/src/processit.egg-info}/PKG-INFO +31 -2
- {processit-0.2.0 → processit-0.2.2}/tests/test_progress.py +204 -6
- processit-0.2.0/src/processit/_version.py +0 -2
- {processit-0.2.0 → processit-0.2.2}/LICENSE +0 -0
- {processit-0.2.0 → processit-0.2.2}/pyproject.toml +0 -0
- {processit-0.2.0 → processit-0.2.2}/setup.cfg +0 -0
- {processit-0.2.0 → processit-0.2.2}/src/processit/__init__.py +0 -0
- {processit-0.2.0 → processit-0.2.2}/src/processit/py.typed +0 -0
- {processit-0.2.0 → processit-0.2.2}/src/processit.egg-info/SOURCES.txt +0 -0
- {processit-0.2.0 → processit-0.2.2}/src/processit.egg-info/dependency_links.txt +0 -0
- {processit-0.2.0 → processit-0.2.2}/src/processit.egg-info/requires.txt +0 -0
- {processit-0.2.0 → processit-0.2.2}/src/processit.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: processit
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Lightweight progress utilities for sync/async workloads
|
|
5
5
|
Author: Vicente Ruiz
|
|
6
6
|
License-Expression: MIT
|
|
@@ -79,8 +79,9 @@ def numbers():
|
|
|
79
79
|
|
|
80
80
|
async def main():
|
|
81
81
|
async with progress(numbers(), total=10, desc='Numbers') as p:
|
|
82
|
+
log = p.log_stream()
|
|
82
83
|
async for n in p:
|
|
83
|
-
|
|
84
|
+
print(f'value: {n}', file=log)
|
|
84
85
|
await asyncio.sleep(0.5)
|
|
85
86
|
|
|
86
87
|
asyncio.run(main())
|
|
@@ -181,6 +182,23 @@ Need concurrency limit `Semaphore` + `track_as_completed(...)`
|
|
|
181
182
|
|
|
182
183
|
---
|
|
183
184
|
|
|
185
|
+
### 3. TTY vs non-TTY output
|
|
186
|
+
|
|
187
|
+
When the output stream is a real TTY, `processit` redraws the same line.
|
|
188
|
+
|
|
189
|
+
When the output stream is not a TTY (for example CI logs, redirected output,
|
|
190
|
+
or `StringIO` in tests), `processit` emits line-based snapshots instead:
|
|
191
|
+
|
|
192
|
+
- It respects `refresh_interval` without periodic duplicate frames
|
|
193
|
+
- It prints a final `100%` snapshot before the summary when `total` is known
|
|
194
|
+
- Messages written with `p.write(...)` or `file=p.log_stream()` stay above the
|
|
195
|
+
live bar; the bar is re-rendered as the last line in TTY mode
|
|
196
|
+
|
|
197
|
+
Timing starts when iteration actually begins, not when the `Progress`
|
|
198
|
+
instance is created.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
184
202
|
## More Examples
|
|
185
203
|
|
|
186
204
|
### Asynchronous iteration over a data source
|
|
@@ -250,6 +268,17 @@ Name Type Description
|
|
|
250
268
|
|
|
251
269
|
---
|
|
252
270
|
|
|
271
|
+
### Progress helpers inside `async with progress(...) as p`
|
|
272
|
+
|
|
273
|
+
- `p.write("message")`: prints a message above the live bar and re-renders it
|
|
274
|
+
- `p.log_stream()`: returns a file-like stream for `print(..., file=...)` or
|
|
275
|
+
`logging.StreamHandler`
|
|
276
|
+
|
|
277
|
+
If a command mixes progress output with logs, prefer one of those two
|
|
278
|
+
interfaces instead of calling `print(...)` directly on stdout/stderr.
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
253
282
|
### track_as_completed(tasks, total=None, \*, desc='Processing', width=30, refresh_interval=0.1, show_summary=True)
|
|
254
283
|
|
|
255
284
|
Tracks a collection of awaitables or tasks as they complete.
|
|
@@ -58,8 +58,9 @@ def numbers():
|
|
|
58
58
|
|
|
59
59
|
async def main():
|
|
60
60
|
async with progress(numbers(), total=10, desc='Numbers') as p:
|
|
61
|
+
log = p.log_stream()
|
|
61
62
|
async for n in p:
|
|
62
|
-
|
|
63
|
+
print(f'value: {n}', file=log)
|
|
63
64
|
await asyncio.sleep(0.5)
|
|
64
65
|
|
|
65
66
|
asyncio.run(main())
|
|
@@ -160,6 +161,23 @@ Need concurrency limit `Semaphore` + `track_as_completed(...)`
|
|
|
160
161
|
|
|
161
162
|
---
|
|
162
163
|
|
|
164
|
+
### 3. TTY vs non-TTY output
|
|
165
|
+
|
|
166
|
+
When the output stream is a real TTY, `processit` redraws the same line.
|
|
167
|
+
|
|
168
|
+
When the output stream is not a TTY (for example CI logs, redirected output,
|
|
169
|
+
or `StringIO` in tests), `processit` emits line-based snapshots instead:
|
|
170
|
+
|
|
171
|
+
- It respects `refresh_interval` without periodic duplicate frames
|
|
172
|
+
- It prints a final `100%` snapshot before the summary when `total` is known
|
|
173
|
+
- Messages written with `p.write(...)` or `file=p.log_stream()` stay above the
|
|
174
|
+
live bar; the bar is re-rendered as the last line in TTY mode
|
|
175
|
+
|
|
176
|
+
Timing starts when iteration actually begins, not when the `Progress`
|
|
177
|
+
instance is created.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
163
181
|
## More Examples
|
|
164
182
|
|
|
165
183
|
### Asynchronous iteration over a data source
|
|
@@ -229,6 +247,17 @@ Name Type Description
|
|
|
229
247
|
|
|
230
248
|
---
|
|
231
249
|
|
|
250
|
+
### Progress helpers inside `async with progress(...) as p`
|
|
251
|
+
|
|
252
|
+
- `p.write("message")`: prints a message above the live bar and re-renders it
|
|
253
|
+
- `p.log_stream()`: returns a file-like stream for `print(..., file=...)` or
|
|
254
|
+
`logging.StreamHandler`
|
|
255
|
+
|
|
256
|
+
If a command mixes progress output with logs, prefer one of those two
|
|
257
|
+
interfaces instead of calling `print(...)` directly on stdout/stderr.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
232
261
|
### track_as_completed(tasks, total=None, \*, desc='Processing', width=30, refresh_interval=0.1, show_summary=True)
|
|
233
262
|
|
|
234
263
|
Tracks a collection of awaitables or tasks as they complete.
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import contextlib
|
|
5
|
+
import io
|
|
5
6
|
import sys
|
|
6
7
|
import time
|
|
7
8
|
|
|
@@ -18,14 +19,68 @@ if TYPE_CHECKING:
|
|
|
18
19
|
)
|
|
19
20
|
|
|
20
21
|
|
|
22
|
+
class _ProgressLogStream(io.TextIOBase):
|
|
23
|
+
__slots__ = ('_buffer', '_progress')
|
|
24
|
+
|
|
25
|
+
def __init__(self, progress: Progress[object]) -> None:
|
|
26
|
+
self._progress = progress
|
|
27
|
+
self._buffer = ''
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def encoding(self) -> str | None:
|
|
31
|
+
return getattr(self._progress.stream, 'encoding', None)
|
|
32
|
+
|
|
33
|
+
def isatty(self) -> bool:
|
|
34
|
+
return self._progress._is_tty
|
|
35
|
+
|
|
36
|
+
def writable(self) -> bool:
|
|
37
|
+
return True
|
|
38
|
+
|
|
39
|
+
def write(self, msg: str) -> int:
|
|
40
|
+
if not isinstance(msg, str):
|
|
41
|
+
msg = str(msg)
|
|
42
|
+
|
|
43
|
+
if not msg:
|
|
44
|
+
return 0
|
|
45
|
+
|
|
46
|
+
self._buffer += msg
|
|
47
|
+
self._flush_complete_lines()
|
|
48
|
+
return len(msg)
|
|
49
|
+
|
|
50
|
+
def flush(self) -> None:
|
|
51
|
+
self.flush_pending()
|
|
52
|
+
|
|
53
|
+
def close(self) -> None:
|
|
54
|
+
self.flush()
|
|
55
|
+
|
|
56
|
+
def flush_pending(self) -> None:
|
|
57
|
+
if not self._buffer:
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
pending = self._buffer.removesuffix('\r')
|
|
61
|
+
self._buffer = ''
|
|
62
|
+
self._progress._write_message(pending, allow_blank=True)
|
|
63
|
+
|
|
64
|
+
def _flush_complete_lines(self) -> None:
|
|
65
|
+
while '\n' in self._buffer:
|
|
66
|
+
line, self._buffer = self._buffer.split('\n', 1)
|
|
67
|
+
self._progress._write_message(
|
|
68
|
+
line.removesuffix('\r'),
|
|
69
|
+
allow_blank=True,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
21
73
|
class Progress[T]:
|
|
22
74
|
__slots__ = (
|
|
23
75
|
'_is_tty',
|
|
24
76
|
'_last_line_len',
|
|
25
77
|
'_last_refresh',
|
|
78
|
+
'_last_render_count',
|
|
79
|
+
'_log_stream',
|
|
26
80
|
'_next_render_at',
|
|
27
81
|
'_prefix',
|
|
28
82
|
'_refresh_task',
|
|
83
|
+
'_started',
|
|
29
84
|
'_stopped',
|
|
30
85
|
'_summary_printed',
|
|
31
86
|
'count',
|
|
@@ -65,22 +120,58 @@ class Progress[T]:
|
|
|
65
120
|
self._refresh_task: asyncio.Task[None] | None = None
|
|
66
121
|
self._summary_printed = False
|
|
67
122
|
self._last_line_len = 0
|
|
123
|
+
self._last_render_count = 0
|
|
124
|
+
self._started = False
|
|
68
125
|
self._stopped = False
|
|
69
126
|
self._is_tty = bool(getattr(self.stream, 'isatty', lambda: False)())
|
|
127
|
+
self._log_stream: _ProgressLogStream | None = None
|
|
70
128
|
self._prefix = f'{self.desc} '
|
|
71
129
|
|
|
130
|
+
def _start_rendering(self) -> None:
|
|
131
|
+
self._started = True
|
|
132
|
+
self._stopped = False
|
|
133
|
+
self.start_time = time.perf_counter()
|
|
134
|
+
self._last_refresh = 0.0
|
|
135
|
+
self._next_render_at = 0.0
|
|
136
|
+
self._last_line_len = 0
|
|
137
|
+
self._last_render_count = -1
|
|
138
|
+
self._render(force=True)
|
|
139
|
+
if self._is_tty:
|
|
140
|
+
self._refresh_task = asyncio.create_task(
|
|
141
|
+
self._refresh_periodically(),
|
|
142
|
+
)
|
|
143
|
+
|
|
72
144
|
def write(self, msg: str = '') -> None:
|
|
73
|
-
"""Print a message
|
|
145
|
+
"""Print a message above the live bar and re-render it."""
|
|
146
|
+
self._write_message(msg)
|
|
147
|
+
|
|
148
|
+
def log_stream(self) -> TextIO:
|
|
149
|
+
"""Return a file-like stream that logs above the live bar."""
|
|
150
|
+
if self._log_stream is None:
|
|
151
|
+
progress = cast('Progress[object]', self)
|
|
152
|
+
self._log_stream = _ProgressLogStream(progress)
|
|
153
|
+
return cast('TextIO', self._log_stream)
|
|
154
|
+
|
|
155
|
+
def _write_message(
|
|
156
|
+
self,
|
|
157
|
+
msg: str = '',
|
|
158
|
+
*,
|
|
159
|
+
allow_blank: bool = False,
|
|
160
|
+
) -> None:
|
|
74
161
|
if self._stopped:
|
|
75
162
|
return
|
|
76
163
|
self._clear_line()
|
|
77
|
-
if msg:
|
|
164
|
+
if msg or allow_blank:
|
|
78
165
|
if not msg.endswith('\n'):
|
|
79
166
|
msg += '\n'
|
|
80
167
|
self.stream.write(msg)
|
|
81
168
|
self.stream.flush()
|
|
82
169
|
self._render(force=True)
|
|
83
170
|
|
|
171
|
+
def _flush_log_stream(self) -> None:
|
|
172
|
+
if self._log_stream is not None:
|
|
173
|
+
self._log_stream.flush_pending()
|
|
174
|
+
|
|
84
175
|
async def amap[R](
|
|
85
176
|
self,
|
|
86
177
|
mapper: Callable[[T], Awaitable[R]],
|
|
@@ -143,12 +234,8 @@ class Progress[T]:
|
|
|
143
234
|
# Start the refresh task if it hasn't been started
|
|
144
235
|
# yet (same as in __aiter__)
|
|
145
236
|
started_here = False
|
|
146
|
-
if self.
|
|
147
|
-
self.
|
|
148
|
-
self._render(force=True)
|
|
149
|
-
self._refresh_task = asyncio.create_task(
|
|
150
|
-
self._refresh_periodically(),
|
|
151
|
-
)
|
|
237
|
+
if not self._started:
|
|
238
|
+
self._start_rendering()
|
|
152
239
|
started_here = True
|
|
153
240
|
|
|
154
241
|
try:
|
|
@@ -195,6 +282,7 @@ class Progress[T]:
|
|
|
195
282
|
await self._refresh_task
|
|
196
283
|
self._refresh_task = None
|
|
197
284
|
self._print_summary_if_needed()
|
|
285
|
+
self._started = False
|
|
198
286
|
|
|
199
287
|
def _format_elapsed(self, seconds: float) -> str:
|
|
200
288
|
"""Return hh:mm:ss, mm:ss or ss.s depending on duration."""
|
|
@@ -217,7 +305,7 @@ class Progress[T]:
|
|
|
217
305
|
# tests/logs
|
|
218
306
|
self.stream.write(text + '\n')
|
|
219
307
|
self.stream.flush()
|
|
220
|
-
self._last_line_len =
|
|
308
|
+
self._last_line_len = len(text)
|
|
221
309
|
|
|
222
310
|
def _clear_line(self) -> None:
|
|
223
311
|
if self._is_tty:
|
|
@@ -268,11 +356,19 @@ class Progress[T]:
|
|
|
268
356
|
)
|
|
269
357
|
|
|
270
358
|
self._write_line(line)
|
|
359
|
+
self._last_render_count = self.count
|
|
271
360
|
self._last_refresh = now
|
|
272
361
|
self._next_render_at = now + self.refresh_interval
|
|
273
362
|
|
|
274
363
|
def _print_summary_if_needed(self) -> None:
|
|
364
|
+
self._flush_log_stream()
|
|
275
365
|
if self.show_summary and not self._summary_printed:
|
|
366
|
+
if (
|
|
367
|
+
self.total is not None
|
|
368
|
+
and self.count == self.total
|
|
369
|
+
and self._last_render_count != self.count
|
|
370
|
+
):
|
|
371
|
+
self._render(force=True)
|
|
276
372
|
self._stopped = True
|
|
277
373
|
self._clear_line()
|
|
278
374
|
|
|
@@ -295,9 +391,8 @@ class Progress[T]:
|
|
|
295
391
|
self._render()
|
|
296
392
|
|
|
297
393
|
async def __aenter__(self) -> Progress[T]:
|
|
298
|
-
self.
|
|
299
|
-
|
|
300
|
-
self._refresh_task = asyncio.create_task(self._refresh_periodically())
|
|
394
|
+
if not self._started:
|
|
395
|
+
self._start_rendering() # feedback inmediato
|
|
301
396
|
return self
|
|
302
397
|
|
|
303
398
|
async def __aexit__(self, *_: object) -> None:
|
|
@@ -307,18 +402,14 @@ class Progress[T]:
|
|
|
307
402
|
await self._refresh_task
|
|
308
403
|
self._refresh_task = None
|
|
309
404
|
self._print_summary_if_needed()
|
|
405
|
+
self._started = False
|
|
310
406
|
|
|
311
407
|
async def __aiter__(self) -> AsyncIterator[T]:
|
|
312
408
|
started_here = False
|
|
313
409
|
|
|
314
|
-
if self.
|
|
315
|
-
|
|
316
|
-
self.
|
|
317
|
-
force=True,
|
|
318
|
-
) # feedback inmediato fuera de context manager
|
|
319
|
-
self._refresh_task = asyncio.create_task(
|
|
320
|
-
self._refresh_periodically(),
|
|
321
|
-
)
|
|
410
|
+
if not self._started:
|
|
411
|
+
# feedback inmediato fuera de context manager
|
|
412
|
+
self._start_rendering()
|
|
322
413
|
started_here = True
|
|
323
414
|
|
|
324
415
|
try:
|
|
@@ -341,6 +432,7 @@ class Progress[T]:
|
|
|
341
432
|
await self._refresh_task
|
|
342
433
|
self._refresh_task = None
|
|
343
434
|
self._print_summary_if_needed()
|
|
435
|
+
self._started = False
|
|
344
436
|
|
|
345
437
|
|
|
346
438
|
def progress[T]( # noqa: PLR0913
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: processit
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Lightweight progress utilities for sync/async workloads
|
|
5
5
|
Author: Vicente Ruiz
|
|
6
6
|
License-Expression: MIT
|
|
@@ -79,8 +79,9 @@ def numbers():
|
|
|
79
79
|
|
|
80
80
|
async def main():
|
|
81
81
|
async with progress(numbers(), total=10, desc='Numbers') as p:
|
|
82
|
+
log = p.log_stream()
|
|
82
83
|
async for n in p:
|
|
83
|
-
|
|
84
|
+
print(f'value: {n}', file=log)
|
|
84
85
|
await asyncio.sleep(0.5)
|
|
85
86
|
|
|
86
87
|
asyncio.run(main())
|
|
@@ -181,6 +182,23 @@ Need concurrency limit `Semaphore` + `track_as_completed(...)`
|
|
|
181
182
|
|
|
182
183
|
---
|
|
183
184
|
|
|
185
|
+
### 3. TTY vs non-TTY output
|
|
186
|
+
|
|
187
|
+
When the output stream is a real TTY, `processit` redraws the same line.
|
|
188
|
+
|
|
189
|
+
When the output stream is not a TTY (for example CI logs, redirected output,
|
|
190
|
+
or `StringIO` in tests), `processit` emits line-based snapshots instead:
|
|
191
|
+
|
|
192
|
+
- It respects `refresh_interval` without periodic duplicate frames
|
|
193
|
+
- It prints a final `100%` snapshot before the summary when `total` is known
|
|
194
|
+
- Messages written with `p.write(...)` or `file=p.log_stream()` stay above the
|
|
195
|
+
live bar; the bar is re-rendered as the last line in TTY mode
|
|
196
|
+
|
|
197
|
+
Timing starts when iteration actually begins, not when the `Progress`
|
|
198
|
+
instance is created.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
184
202
|
## More Examples
|
|
185
203
|
|
|
186
204
|
### Asynchronous iteration over a data source
|
|
@@ -250,6 +268,17 @@ Name Type Description
|
|
|
250
268
|
|
|
251
269
|
---
|
|
252
270
|
|
|
271
|
+
### Progress helpers inside `async with progress(...) as p`
|
|
272
|
+
|
|
273
|
+
- `p.write("message")`: prints a message above the live bar and re-renders it
|
|
274
|
+
- `p.log_stream()`: returns a file-like stream for `print(..., file=...)` or
|
|
275
|
+
`logging.StreamHandler`
|
|
276
|
+
|
|
277
|
+
If a command mixes progress output with logs, prefer one of those two
|
|
278
|
+
interfaces instead of calling `print(...)` directly on stdout/stderr.
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
253
282
|
### track_as_completed(tasks, total=None, \*, desc='Processing', width=30, refresh_interval=0.1, show_summary=True)
|
|
254
283
|
|
|
255
284
|
Tracks a collection of awaitables or tasks as they complete.
|
|
@@ -34,6 +34,49 @@ def _last_nonempty_line(buf: str) -> str:
|
|
|
34
34
|
return ''
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
class _TTYStringIO(io.StringIO):
|
|
38
|
+
def isatty(self) -> bool:
|
|
39
|
+
return True
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _render_tty_screen(buf: str) -> list[str]:
|
|
43
|
+
lines: list[str] = []
|
|
44
|
+
current: list[str] = []
|
|
45
|
+
cursor = 0
|
|
46
|
+
idx = 0
|
|
47
|
+
|
|
48
|
+
while idx < len(buf):
|
|
49
|
+
if buf.startswith('\x1b[2K', idx):
|
|
50
|
+
current = []
|
|
51
|
+
cursor = 0
|
|
52
|
+
idx += 4
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
char = buf[idx]
|
|
56
|
+
idx += 1
|
|
57
|
+
|
|
58
|
+
if char == '\r':
|
|
59
|
+
cursor = 0
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
if char == '\n':
|
|
63
|
+
lines.append(''.join(current))
|
|
64
|
+
current = []
|
|
65
|
+
cursor = 0
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
if cursor == len(current):
|
|
69
|
+
current.append(char)
|
|
70
|
+
else:
|
|
71
|
+
current[cursor] = char
|
|
72
|
+
cursor += 1
|
|
73
|
+
|
|
74
|
+
if current:
|
|
75
|
+
lines.append(''.join(current))
|
|
76
|
+
|
|
77
|
+
return [line for line in lines if line.strip()]
|
|
78
|
+
|
|
79
|
+
|
|
37
80
|
# -----------------------------
|
|
38
81
|
# Synchronous iterable cases
|
|
39
82
|
# -----------------------------
|
|
@@ -64,6 +107,107 @@ async def test_sync_iterable_basic_summary_only_once():
|
|
|
64
107
|
assert 'it/s' in out
|
|
65
108
|
|
|
66
109
|
|
|
110
|
+
@pytest.mark.asyncio
|
|
111
|
+
async def test_non_tty_respects_refresh_interval():
|
|
112
|
+
stream = io.StringIO()
|
|
113
|
+
expected_bar_frames = 2
|
|
114
|
+
|
|
115
|
+
def numbers() -> Iterator[int]:
|
|
116
|
+
for i in range(20):
|
|
117
|
+
time.sleep(0.005)
|
|
118
|
+
yield i
|
|
119
|
+
|
|
120
|
+
async for _ in progress(
|
|
121
|
+
numbers(),
|
|
122
|
+
desc='NonTTY',
|
|
123
|
+
total=20,
|
|
124
|
+
stream=stream,
|
|
125
|
+
refresh_interval=10.0,
|
|
126
|
+
):
|
|
127
|
+
await asyncio.sleep(0)
|
|
128
|
+
|
|
129
|
+
out = _strip_ansi(stream.getvalue())
|
|
130
|
+
assert out.count('NonTTY [') == expected_bar_frames
|
|
131
|
+
assert 'NonTTY [##############################] 100.00% (20/20)' in out
|
|
132
|
+
assert out.count('NonTTY: 20 it in ') == 1
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@pytest.mark.asyncio
|
|
136
|
+
async def test_non_tty_renders_final_complete_frame_before_summary():
|
|
137
|
+
stream = io.StringIO()
|
|
138
|
+
data = list(range(3))
|
|
139
|
+
|
|
140
|
+
async for _ in progress(
|
|
141
|
+
data,
|
|
142
|
+
desc='Final',
|
|
143
|
+
total=3,
|
|
144
|
+
stream=stream,
|
|
145
|
+
refresh_interval=10.0,
|
|
146
|
+
):
|
|
147
|
+
await asyncio.sleep(0)
|
|
148
|
+
|
|
149
|
+
lines = [
|
|
150
|
+
line for line in _strip_ansi(stream.getvalue()).splitlines() if line
|
|
151
|
+
]
|
|
152
|
+
expected_complete = 'Final [##############################] 100.00% (3/3)'
|
|
153
|
+
assert any(expected_complete in line for line in lines)
|
|
154
|
+
assert lines[-1].startswith('Final: 3 it in ')
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@pytest.mark.asyncio
|
|
158
|
+
async def test_non_tty_async_iterable_does_not_duplicate_periodic_frames():
|
|
159
|
+
stream = io.StringIO()
|
|
160
|
+
expected_lines = 5
|
|
161
|
+
|
|
162
|
+
async def agen() -> AsyncIterator[int]:
|
|
163
|
+
for i in range(3):
|
|
164
|
+
await asyncio.sleep(0.01)
|
|
165
|
+
yield i
|
|
166
|
+
|
|
167
|
+
async for _ in progress(
|
|
168
|
+
agen(),
|
|
169
|
+
desc='NoDupes',
|
|
170
|
+
total=3,
|
|
171
|
+
stream=stream,
|
|
172
|
+
refresh_interval=0.005,
|
|
173
|
+
):
|
|
174
|
+
await asyncio.sleep(0)
|
|
175
|
+
|
|
176
|
+
lines = [
|
|
177
|
+
line for line in _strip_ansi(stream.getvalue()).splitlines() if line
|
|
178
|
+
]
|
|
179
|
+
assert len(lines) == expected_lines
|
|
180
|
+
assert sum('(0/3)' in line for line in lines) == 1
|
|
181
|
+
assert sum('(1/3)' in line for line in lines) == 1
|
|
182
|
+
assert sum('(2/3)' in line for line in lines) == 1
|
|
183
|
+
assert sum('(3/3)' in line for line in lines) == 1
|
|
184
|
+
assert lines[-1].startswith('NoDupes: 3 it in ')
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@pytest.mark.asyncio
|
|
188
|
+
async def test_elapsed_starts_on_first_use_not_object_creation():
|
|
189
|
+
stream = io.StringIO()
|
|
190
|
+
p = progress(
|
|
191
|
+
[1],
|
|
192
|
+
total=1,
|
|
193
|
+
desc='Delay',
|
|
194
|
+
stream=stream,
|
|
195
|
+
refresh_interval=10.0,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
await asyncio.sleep(0.2)
|
|
199
|
+
|
|
200
|
+
async for _ in p:
|
|
201
|
+
await asyncio.sleep(0)
|
|
202
|
+
|
|
203
|
+
out = _strip_ansi(stream.getvalue())
|
|
204
|
+
expected_initial = (
|
|
205
|
+
'Delay [..............................] 0.00% (0/1) 0.00 it/s 00.0s'
|
|
206
|
+
)
|
|
207
|
+
assert expected_initial in out
|
|
208
|
+
assert 'Delay: 1 it in 00.0s' in out
|
|
209
|
+
|
|
210
|
+
|
|
67
211
|
@pytest.mark.asyncio
|
|
68
212
|
async def test_sync_iterable_infers_total_from_len():
|
|
69
213
|
stream = io.StringIO()
|
|
@@ -72,9 +216,9 @@ async def test_sync_iterable_infers_total_from_len():
|
|
|
72
216
|
data,
|
|
73
217
|
desc='Len inf',
|
|
74
218
|
stream=stream,
|
|
75
|
-
refresh_interval=
|
|
219
|
+
refresh_interval=0.001,
|
|
76
220
|
):
|
|
77
|
-
await asyncio.sleep(0)
|
|
221
|
+
await asyncio.sleep(0.01)
|
|
78
222
|
out = stream.getvalue()
|
|
79
223
|
# En algún render debe aparecer (4/4)
|
|
80
224
|
assert '(4/4)' in out
|
|
@@ -164,6 +308,60 @@ async def test_async_with_and_write_no_extra_bar_at_end():
|
|
|
164
308
|
assert 'With [' not in last
|
|
165
309
|
|
|
166
310
|
|
|
311
|
+
@pytest.mark.asyncio
|
|
312
|
+
async def test_log_stream_flushes_partial_output_before_summary_non_tty():
|
|
313
|
+
stream = io.StringIO()
|
|
314
|
+
|
|
315
|
+
async with progress(
|
|
316
|
+
[1, 2],
|
|
317
|
+
total=2,
|
|
318
|
+
desc='Stream',
|
|
319
|
+
stream=stream,
|
|
320
|
+
refresh_interval=10.0,
|
|
321
|
+
) as p:
|
|
322
|
+
log = p.log_stream()
|
|
323
|
+
print('alpha', file=log)
|
|
324
|
+
log.write('beta')
|
|
325
|
+
|
|
326
|
+
async for _ in p:
|
|
327
|
+
await asyncio.sleep(0)
|
|
328
|
+
|
|
329
|
+
lines = [
|
|
330
|
+
line for line in _strip_ansi(stream.getvalue()).splitlines() if line
|
|
331
|
+
]
|
|
332
|
+
assert 'alpha' in lines
|
|
333
|
+
assert 'beta' in lines
|
|
334
|
+
assert lines[-1].startswith('Stream: 2 it in ')
|
|
335
|
+
assert lines.index('alpha') < lines.index('beta') < len(lines) - 1
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
@pytest.mark.asyncio
|
|
339
|
+
async def test_log_stream_keeps_messages_above_bar_in_tty():
|
|
340
|
+
stream = _TTYStringIO()
|
|
341
|
+
|
|
342
|
+
async with progress(
|
|
343
|
+
[1, 2],
|
|
344
|
+
total=2,
|
|
345
|
+
desc='TTY',
|
|
346
|
+
stream=stream,
|
|
347
|
+
refresh_interval=10.0,
|
|
348
|
+
) as p:
|
|
349
|
+
log = p.log_stream()
|
|
350
|
+
print('alpha', file=log)
|
|
351
|
+
log.write('beta')
|
|
352
|
+
|
|
353
|
+
async for _ in p:
|
|
354
|
+
await asyncio.sleep(0)
|
|
355
|
+
|
|
356
|
+
raw = stream.getvalue()
|
|
357
|
+
screen = _render_tty_screen(raw)
|
|
358
|
+
|
|
359
|
+
assert 'alpha\n\r\x1b[2KTTY [' in raw
|
|
360
|
+
assert 'beta\n\r\x1b[2KTTY [' in raw
|
|
361
|
+
assert screen[:-1] == ['alpha', 'beta']
|
|
362
|
+
assert screen[-1].startswith('TTY: 2 it in ')
|
|
363
|
+
|
|
364
|
+
|
|
167
365
|
# -----------------------------
|
|
168
366
|
# track_as_completed cases
|
|
169
367
|
# -----------------------------
|
|
@@ -312,18 +510,18 @@ async def test_eta_only_when_total_known():
|
|
|
312
510
|
total=3,
|
|
313
511
|
desc='HasTotal',
|
|
314
512
|
stream=stream1,
|
|
315
|
-
refresh_interval=
|
|
513
|
+
refresh_interval=0.001,
|
|
316
514
|
):
|
|
317
|
-
|
|
515
|
+
await asyncio.sleep(0.01)
|
|
318
516
|
|
|
319
517
|
# Sin total -> preferimos que no aparezca "ETA" en la salida
|
|
320
518
|
async for _ in progress(
|
|
321
519
|
agen(),
|
|
322
520
|
desc='NoTotal',
|
|
323
521
|
stream=stream2,
|
|
324
|
-
refresh_interval=
|
|
522
|
+
refresh_interval=0.001,
|
|
325
523
|
):
|
|
326
|
-
|
|
524
|
+
await asyncio.sleep(0.01)
|
|
327
525
|
|
|
328
526
|
out1 = stream1.getvalue()
|
|
329
527
|
out2 = stream2.getvalue()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|