click-async-plugins 0.8.0__tar.gz → 0.9.0__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.
- {click_async_plugins-0.8.0 → click_async_plugins-0.9.0}/PKG-INFO +1 -1
- {click_async_plugins-0.8.0 → click_async_plugins-0.9.0}/_version.py +2 -2
- {click_async_plugins-0.8.0 → click_async_plugins-0.9.0}/click_async_plugins/debug.py +7 -2
- {click_async_plugins-0.8.0 → click_async_plugins-0.9.0}/click_async_plugins/itc.py +45 -2
- {click_async_plugins-0.8.0 → click_async_plugins-0.9.0}/tests/test_debug.py +9 -3
- {click_async_plugins-0.8.0 → click_async_plugins-0.9.0}/tests/test_itc.py +114 -0
- {click_async_plugins-0.8.0 → click_async_plugins-0.9.0}/.gitignore +0 -0
- {click_async_plugins-0.8.0 → click_async_plugins-0.9.0}/.pre-commit-config.yaml +0 -0
- {click_async_plugins-0.8.0 → click_async_plugins-0.9.0}/LICENSE +0 -0
- {click_async_plugins-0.8.0 → click_async_plugins-0.9.0}/README.md +0 -0
- {click_async_plugins-0.8.0 → click_async_plugins-0.9.0}/click_async_plugins/__init__.py +0 -0
- {click_async_plugins-0.8.0 → click_async_plugins-0.9.0}/click_async_plugins/command.py +0 -0
- {click_async_plugins-0.8.0 → click_async_plugins-0.9.0}/click_async_plugins/core.py +0 -0
- {click_async_plugins-0.8.0 → click_async_plugins-0.9.0}/click_async_plugins/dependencies.py +0 -0
- {click_async_plugins-0.8.0 → click_async_plugins-0.9.0}/click_async_plugins/group.py +0 -0
- {click_async_plugins-0.8.0 → click_async_plugins-0.9.0}/click_async_plugins/py.typed +0 -0
- {click_async_plugins-0.8.0 → click_async_plugins-0.9.0}/click_async_plugins/typedefs.py +0 -0
- {click_async_plugins-0.8.0 → click_async_plugins-0.9.0}/click_async_plugins/util.py +0 -0
- {click_async_plugins-0.8.0 → click_async_plugins-0.9.0}/demo.py +0 -0
- {click_async_plugins-0.8.0 → click_async_plugins-0.9.0}/pyproject.toml +0 -0
- {click_async_plugins-0.8.0 → click_async_plugins-0.9.0}/tests/test_command_group_core.py +0 -0
- {click_async_plugins-0.8.0 → click_async_plugins-0.9.0}/tests/test_dependencies.py +0 -0
- {click_async_plugins-0.8.0 → click_async_plugins-0.9.0}/tests/test_typedefs.py +0 -0
- {click_async_plugins-0.8.0 → click_async_plugins-0.9.0}/tests/test_util.py +0 -0
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.
|
|
22
|
-
__version_tuple__ = version_tuple = (0,
|
|
21
|
+
__version__ = version = '0.9.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 9, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -10,7 +10,7 @@ from collections.abc import AsyncGenerator, Callable
|
|
|
10
10
|
from contextlib import asynccontextmanager
|
|
11
11
|
from dataclasses import dataclass
|
|
12
12
|
from functools import partial
|
|
13
|
-
from typing import Any, Coroutine, cast
|
|
13
|
+
from typing import Any, Coroutine, Never, cast
|
|
14
14
|
|
|
15
15
|
from click_async_plugins.util import CliContext
|
|
16
16
|
|
|
@@ -44,6 +44,10 @@ def _name_for_coro(coro: Coroutine[Any, Any, Any] | None) -> str:
|
|
|
44
44
|
return "(unknown)"
|
|
45
45
|
|
|
46
46
|
|
|
47
|
+
def interrupt(clictx: CliContext) -> Never:
|
|
48
|
+
raise KeyboardInterrupt
|
|
49
|
+
|
|
50
|
+
|
|
47
51
|
def debug_info(clictx: CliContext) -> str:
|
|
48
52
|
"""Prints debugging information on tasks and CliContext"""
|
|
49
53
|
ret = "*** BEGIN DEBUG INFO: ***\n"
|
|
@@ -58,7 +62,7 @@ def debug_info(clictx: CliContext) -> str:
|
|
|
58
62
|
ret += "CliContext:\n"
|
|
59
63
|
maxlen = max([len(k) for k in clictx.__dict__.keys()])
|
|
60
64
|
for attr, value in clictx.__dict__.items():
|
|
61
|
-
ret += f" {attr:>{maxlen}s} = {value!r}\n"
|
|
65
|
+
ret += f" {attr:>{maxlen}s} = {value!r} (id:{id(value)})\n"
|
|
62
66
|
return ret + "*** END DEBUG INFO: ***"
|
|
63
67
|
|
|
64
68
|
|
|
@@ -188,6 +192,7 @@ async def monitor_stdin_for_debug_commands[ContextT: CliContext](
|
|
|
188
192
|
decrease_loglevel.__doc__ = "Decrease the logging level"
|
|
189
193
|
|
|
190
194
|
map = {
|
|
195
|
+
0x3: KeyAndFunc(r"^C", interrupt),
|
|
191
196
|
0xA: KeyAndFunc(r"\n", echo_newline),
|
|
192
197
|
0xD: KeyAndFunc(r"\n", echo_newline),
|
|
193
198
|
0x1B: KeyAndFunc("<Esc>", terminal_block),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import inspect
|
|
2
3
|
import logging
|
|
3
4
|
import time
|
|
4
5
|
from collections import defaultdict
|
|
@@ -8,6 +9,31 @@ from typing import Any
|
|
|
8
9
|
logger = logging.getLogger(__name__)
|
|
9
10
|
|
|
10
11
|
|
|
12
|
+
def _caller(depth: int = 1) -> str:
|
|
13
|
+
"""Return a description of a calling frame, ``depth`` frames up.
|
|
14
|
+
|
|
15
|
+
``depth`` counts frames starting at this helper, so ``depth=1`` (the
|
|
16
|
+
default) reports the direct caller of this helper, ``depth=2`` the frame
|
|
17
|
+
above that, and so on. Returns a ``file:lineno in function`` string, or
|
|
18
|
+
a fallback marker if the frame cannot be determined.
|
|
19
|
+
|
|
20
|
+
Raises:
|
|
21
|
+
ValueError: If ``depth`` is less than 1.
|
|
22
|
+
"""
|
|
23
|
+
if depth < 1:
|
|
24
|
+
raise ValueError(f"depth must be >= 1, got {depth}")
|
|
25
|
+
|
|
26
|
+
frame = inspect.currentframe()
|
|
27
|
+
try:
|
|
28
|
+
for _ in range(depth):
|
|
29
|
+
frame = frame.f_back if frame is not None else None
|
|
30
|
+
if frame is None:
|
|
31
|
+
return "<unknown>"
|
|
32
|
+
return f"{frame.f_code.co_filename}:{frame.f_lineno} in {frame.f_code.co_name}"
|
|
33
|
+
finally:
|
|
34
|
+
del frame
|
|
35
|
+
|
|
36
|
+
|
|
11
37
|
class ITC:
|
|
12
38
|
def __init__(self) -> None:
|
|
13
39
|
self._events: dict[str, list[asyncio.Event]] = defaultdict(list[asyncio.Event])
|
|
@@ -25,13 +51,20 @@ class ITC:
|
|
|
25
51
|
return f"{ret}{' ' if i > 0 else ''})"
|
|
26
52
|
|
|
27
53
|
def set(self, key: str, obj: Any) -> None:
|
|
54
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
55
|
+
logger.debug(f"set('{key}', {obj!r}) called from {_caller(2)}")
|
|
28
56
|
self._objects[key] = obj
|
|
29
57
|
self.fire(key)
|
|
30
58
|
|
|
31
59
|
def get(self, key: str, default: Any = None) -> Any:
|
|
32
|
-
|
|
60
|
+
value = self._objects.get(key, default)
|
|
61
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
62
|
+
logger.debug(f"get('{key}') -> {value!r} called from {_caller(2)}")
|
|
63
|
+
return value
|
|
33
64
|
|
|
34
65
|
def fire(self, key: str) -> None:
|
|
66
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
67
|
+
logger.debug(f"fire('{key}') called from {_caller(2)}")
|
|
35
68
|
logger.debug(f"Notifying subscribers of update to '{key}'…")
|
|
36
69
|
for event in self._events.get(key) or []:
|
|
37
70
|
event.set()
|
|
@@ -47,6 +80,9 @@ class ITC:
|
|
|
47
80
|
) -> AsyncGenerator[Any | None]:
|
|
48
81
|
at_most_every = 0 if at_most_every is None else at_most_every
|
|
49
82
|
|
|
83
|
+
caller = _caller(2) if logger.isEnabledFor(logging.DEBUG) else ""
|
|
84
|
+
logger.debug(f"updates('{key}') subscribing from {caller}")
|
|
85
|
+
|
|
50
86
|
if timeout is not None and timeout <= 0:
|
|
51
87
|
logger.warning("Updates timeout <= 0, disabling timeout…")
|
|
52
88
|
timeout = None
|
|
@@ -60,7 +96,12 @@ class ITC:
|
|
|
60
96
|
|
|
61
97
|
try:
|
|
62
98
|
if yield_immediately:
|
|
63
|
-
|
|
99
|
+
value = self._objects.get(key, yield_for_no_value)
|
|
100
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
101
|
+
logger.debug(
|
|
102
|
+
f"updates('{key}') yielding initial {value!r} to {caller}"
|
|
103
|
+
)
|
|
104
|
+
yield value
|
|
64
105
|
|
|
65
106
|
timestamp = 0.0
|
|
66
107
|
while True:
|
|
@@ -81,6 +122,8 @@ class ITC:
|
|
|
81
122
|
# instead of being wiped out.
|
|
82
123
|
event.clear()
|
|
83
124
|
value = self._objects.get(key, yield_for_no_value)
|
|
125
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
126
|
+
logger.debug(f"updates('{key}') yielding {value!r} to {caller}")
|
|
84
127
|
yield value
|
|
85
128
|
timestamp = time.monotonic()
|
|
86
129
|
|
|
@@ -4,8 +4,8 @@ import os
|
|
|
4
4
|
import pty
|
|
5
5
|
import sys
|
|
6
6
|
import types
|
|
7
|
-
from collections.abc import AsyncGenerator, Iterator
|
|
8
|
-
from typing import Any
|
|
7
|
+
from collections.abc import AsyncGenerator, Coroutine, Iterator
|
|
8
|
+
from typing import Any, cast
|
|
9
9
|
|
|
10
10
|
import pytest
|
|
11
11
|
from click.testing import CliRunner
|
|
@@ -21,6 +21,7 @@ from click_async_plugins.debug import (
|
|
|
21
21
|
debug_info,
|
|
22
22
|
echo_newline,
|
|
23
23
|
getch,
|
|
24
|
+
interrupt,
|
|
24
25
|
monitor_stdin_for_debug_commands,
|
|
25
26
|
print_help,
|
|
26
27
|
puts,
|
|
@@ -52,6 +53,11 @@ def test_echo_newline(clictx: CliContext) -> None:
|
|
|
52
53
|
assert echo_newline(clictx) == ""
|
|
53
54
|
|
|
54
55
|
|
|
56
|
+
def test_interrupt(clictx: CliContext) -> None:
|
|
57
|
+
with pytest.raises(KeyboardInterrupt):
|
|
58
|
+
interrupt(clictx)
|
|
59
|
+
|
|
60
|
+
|
|
55
61
|
def test_terminal_block(clictx: CliContext) -> None:
|
|
56
62
|
block = terminal_block(clictx)
|
|
57
63
|
assert block.startswith("\n" * 8 + "The time is now: ")
|
|
@@ -79,7 +85,7 @@ def test_name_for_coro_unknown() -> None:
|
|
|
79
85
|
|
|
80
86
|
def test_name_for_coro_falls_back_to_name() -> None:
|
|
81
87
|
# an object with __name__ but no __qualname__
|
|
82
|
-
obj = types.SimpleNamespace(__name__="named")
|
|
88
|
+
obj = cast(Coroutine[Any, Any, Any], types.SimpleNamespace(__name__="named"))
|
|
83
89
|
assert _name_for_coro(obj) == "named"
|
|
84
90
|
|
|
85
91
|
|
|
@@ -207,3 +207,117 @@ async def test_updates_does_not_lose_update_during_consumer_work(itc: ITC) -> No
|
|
|
207
207
|
|
|
208
208
|
assert await asyncio.wait_for(anext(gen), 1) == 2
|
|
209
209
|
await gen.aclose()
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def test_set_logs_caller_and_value(itc: ITC, caplog: pytest.LogCaptureFixture) -> None:
|
|
213
|
+
caplog.set_level(logging.DEBUG)
|
|
214
|
+
itc.set("key", 7)
|
|
215
|
+
record = next(r for r in caplog.records if r.message.startswith("set("))
|
|
216
|
+
assert "set('key', 7) called from" in record.message
|
|
217
|
+
assert "test_itc.py" in record.message
|
|
218
|
+
assert "test_set_logs_caller_and_value" in record.message
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_get_logs_caller_and_value(itc: ITC, caplog: pytest.LogCaptureFixture) -> None:
|
|
222
|
+
caplog.set_level(logging.DEBUG)
|
|
223
|
+
itc.set("key", 7)
|
|
224
|
+
caplog.clear()
|
|
225
|
+
assert itc.get("key") == 7
|
|
226
|
+
record = next(r for r in caplog.records if r.message.startswith("get("))
|
|
227
|
+
assert "get('key') -> 7 called from" in record.message
|
|
228
|
+
assert "test_get_logs_caller_and_value" in record.message
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def test_fire_logs_caller(itc: ITC, caplog: pytest.LogCaptureFixture) -> None:
|
|
232
|
+
caplog.set_level(logging.DEBUG)
|
|
233
|
+
itc.fire("key")
|
|
234
|
+
record = next(r for r in caplog.records if r.message.startswith("fire("))
|
|
235
|
+
assert "fire('key') called from" in record.message
|
|
236
|
+
assert "test_fire_logs_caller" in record.message
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@pytest.mark.asyncio
|
|
240
|
+
async def test_updates_logs_caller_and_values(
|
|
241
|
+
itc: ITC, caplog: pytest.LogCaptureFixture
|
|
242
|
+
) -> None:
|
|
243
|
+
caplog.set_level(logging.DEBUG)
|
|
244
|
+
itc.set("key", 1)
|
|
245
|
+
caplog.clear()
|
|
246
|
+
|
|
247
|
+
gen = itc.updates("key")
|
|
248
|
+
assert await anext(gen) == 1
|
|
249
|
+
await gen.aclose()
|
|
250
|
+
|
|
251
|
+
assert any(
|
|
252
|
+
"updates('key') subscribing from" in r.message
|
|
253
|
+
and "test_updates_logs_caller_and_values" in r.message
|
|
254
|
+
for r in caplog.records
|
|
255
|
+
)
|
|
256
|
+
assert any(
|
|
257
|
+
"updates('key') yielding initial 1 to" in r.message for r in caplog.records
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@pytest.mark.asyncio
|
|
262
|
+
async def test_updates_logs_subsequent_values(
|
|
263
|
+
itc: ITC, caplog: pytest.LogCaptureFixture
|
|
264
|
+
) -> None:
|
|
265
|
+
caplog.set_level(logging.DEBUG)
|
|
266
|
+
gen = itc.updates("key", yield_immediately=False)
|
|
267
|
+
pending = asyncio.ensure_future(anext(gen))
|
|
268
|
+
await asyncio.sleep(0)
|
|
269
|
+
itc.set("key", 42)
|
|
270
|
+
assert await asyncio.wait_for(pending, 1) == 42
|
|
271
|
+
await gen.aclose()
|
|
272
|
+
|
|
273
|
+
assert any("updates('key') yielding 42 to" in r.message for r in caplog.records)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def test_caller_defaults_to_direct_caller() -> None:
|
|
277
|
+
from click_async_plugins.itc import _caller
|
|
278
|
+
|
|
279
|
+
def helper() -> str:
|
|
280
|
+
return _caller()
|
|
281
|
+
|
|
282
|
+
assert helper().endswith(" in helper")
|
|
283
|
+
assert "test_itc.py" in helper()
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def test_caller_depth_walks_up_stack() -> None:
|
|
287
|
+
from click_async_plugins.itc import _caller
|
|
288
|
+
|
|
289
|
+
def inner() -> str:
|
|
290
|
+
return _caller(2)
|
|
291
|
+
|
|
292
|
+
def outer() -> str:
|
|
293
|
+
return inner()
|
|
294
|
+
|
|
295
|
+
assert outer().endswith(" in outer")
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def test_caller_zero_depth_raises() -> None:
|
|
299
|
+
from click_async_plugins.itc import _caller
|
|
300
|
+
|
|
301
|
+
with pytest.raises(ValueError, match="depth must be >= 1"):
|
|
302
|
+
_caller(0)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def test_caller_negative_depth_raises() -> None:
|
|
306
|
+
from click_async_plugins.itc import _caller
|
|
307
|
+
|
|
308
|
+
with pytest.raises(ValueError, match="depth must be >= 1"):
|
|
309
|
+
_caller(-1)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def test_caller_depth_beyond_stack_returns_unknown() -> None:
|
|
313
|
+
from click_async_plugins.itc import _caller
|
|
314
|
+
|
|
315
|
+
assert _caller(10_000) == "<unknown>"
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def test_get_does_not_log_when_debug_disabled(
|
|
319
|
+
itc: ITC, caplog: pytest.LogCaptureFixture
|
|
320
|
+
) -> None:
|
|
321
|
+
caplog.set_level(logging.INFO)
|
|
322
|
+
itc.get("key")
|
|
323
|
+
assert not any(r.message.startswith("get(") for r in caplog.records)
|
|
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
|
|
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
|