repld-tool 0.2.1__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.
Files changed (63) hide show
  1. {repld_tool-0.2.1 → repld_tool-0.2.2}/PKG-INFO +1 -1
  2. {repld_tool-0.2.1 → repld_tool-0.2.2}/pyproject.toml +1 -1
  3. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/__init__.py +3 -1
  4. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/help.py +11 -0
  5. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/kernel.py +43 -9
  6. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/runtime.py +11 -3
  7. {repld_tool-0.2.1 → repld_tool-0.2.2}/LICENSE +0 -0
  8. {repld_tool-0.2.1 → repld_tool-0.2.2}/README.md +0 -0
  9. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/.ruff_cache/.gitignore +0 -0
  10. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/.ruff_cache/0.15.22/14226964882126921391 +0 -0
  11. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/.ruff_cache/CACHEDIR.TAG +0 -0
  12. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/__main__.py +0 -0
  13. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/bg.py +0 -0
  14. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/bind.py +0 -0
  15. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/bridge.py +0 -0
  16. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/bridge_tools.py +0 -0
  17. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/browser/__init__.py +0 -0
  18. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/browser/browser.py +0 -0
  19. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/browser/capture.py +0 -0
  20. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/browser/cdp.py +0 -0
  21. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/browser/har.py +0 -0
  22. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/browser/observe.py +0 -0
  23. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/browser/pin.py +0 -0
  24. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/browser/png.py +0 -0
  25. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/browser/pool.py +0 -0
  26. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/browser/row.py +0 -0
  27. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/browser/selector.py +0 -0
  28. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/browser/session.py +0 -0
  29. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/browser/tab.py +0 -0
  30. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/browser/tab_query.py +0 -0
  31. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/browser/target.py +0 -0
  32. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/browser_dispatch.py +0 -0
  33. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/channel.py +0 -0
  34. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/cli.py +0 -0
  35. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/cli_args.py +0 -0
  36. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/core_schemas.py +0 -0
  37. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/dashboard.py +0 -0
  38. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/dashboard_cmd.py +0 -0
  39. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/dashboard_html.py +0 -0
  40. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/display.py +0 -0
  41. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/eventlog.py +0 -0
  42. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/events.py +0 -0
  43. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/exec_cmd.py +0 -0
  44. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/gate_cmd.py +0 -0
  45. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/gates.py +0 -0
  46. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/gist_api.py +0 -0
  47. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/gist_cmd.py +0 -0
  48. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/gist_deps.py +0 -0
  49. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/gist_links.py +0 -0
  50. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/gist_lint.py +0 -0
  51. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/gists.py +0 -0
  52. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/ipc.py +0 -0
  53. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/kernel_context.py +0 -0
  54. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/lifecycle_cmd.py +0 -0
  55. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/log_cmd.py +0 -0
  56. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/paths.py +0 -0
  57. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/protocol.py +0 -0
  58. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/relaunch.py +0 -0
  59. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/render.py +0 -0
  60. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/sessions.py +0 -0
  61. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/spawn.py +0 -0
  62. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/state.py +0 -0
  63. {repld_tool-0.2.1 → repld_tool-0.2.2}/src/repld/tasks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: repld-tool
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Persistent Python runtime with MCP channel push. Dev shell and autonomous-agent substrate in one package.
5
5
  Keywords: repl,mcp,agent,browser
6
6
  Author: Fredrik Angelsen
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "repld-tool"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  description = "Persistent Python runtime with MCP channel push. Dev shell and autonomous-agent substrate in one package."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -11,7 +11,9 @@ if TYPE_CHECKING:
11
11
  from typing import Any, Callable, Coroutine
12
12
 
13
13
  def notify(content: Any, **meta: Any) -> None: ...
14
- def defer(coro: Coroutine, label: str | None = None) -> str: ...
14
+ def defer(
15
+ coro: Coroutine | Callable[[], Coroutine], label: str | None = None
16
+ ) -> str: ...
15
17
  def every(
16
18
  seconds: float, *, label: str | None = None, delay: float = 0.0
17
19
  ) -> Callable: ...
@@ -1113,6 +1113,9 @@ no_display(value) → value
1113
1113
 
1114
1114
  defer(coro, label=None) → task_id
1115
1115
  Fire-and-forget. Channel push on done. Visible to get_task/cancel.
1116
+ coro can also be a zero-arg callable that builds the awaitable —
1117
+ defer(lambda: asyncio.gather(a(), b())) — so gather()/create_task()
1118
+ construct on the kernel's loop instead of a sync cell's own thread.
1116
1119
 
1117
1120
  every(seconds, label=, delay=0)(fn) → fn periodic ticker; fn.cancel() stops
1118
1121
  delay= defers the first tick (default: tick now)
@@ -1466,6 +1469,14 @@ For intentionally long work, use defer():
1466
1469
 
1467
1470
  defer(download_all_invoices(), label="invoice sync")
1468
1471
 
1472
+ Fanning out with asyncio.gather()/create_task() inside defer() needs one
1473
+ more step: those reach for a running loop when *built*, not just when
1474
+ awaited, and a sync cell (no top-level await) runs in a worker thread with
1475
+ no loop of its own. Pass defer() a zero-arg callable instead of a
1476
+ pre-built awaitable, so the gather() call happens on the kernel's loop:
1477
+
1478
+ defer(lambda: asyncio.gather(one("a"), one("b"), return_exceptions=True))
1479
+
1469
1480
  This returns the task_id immediately. The channel notification arrives
1470
1481
  when the coroutine completes (or fails).
1471
1482
 
@@ -487,14 +487,29 @@ async def _run_cell(task_id: str, src: str, n: int, *, wait_ready: bool = True)
487
487
  task["exception"] = type(exc).__name__
488
488
 
489
489
 
490
- async def _run_deferred(task_id: str, coro) -> None:
491
- """Await a user-supplied coroutine within the task lifecycle.
492
-
493
- Like _run_cell but skips compile/eval — just awaits the coroutine directly.
490
+ async def _run_deferred(task_id: str, thunk) -> None:
491
+ """Build and await a user-supplied awaitable within the task lifecycle.
492
+
493
+ `thunk` is always a zero-arg callable — defer() wraps a coroutine object
494
+ passed directly as ``lambda: coro``, so this only has one code path.
495
+ Calling it here, inside a coroutine already scheduled on the kernel's
496
+ loop (`asyncio.run_coroutine_threadsafe`), is what lets a factory like
497
+ ``lambda: asyncio.gather(...)`` build its awaitable with a loop actually
498
+ running: `asyncio.gather()`/`create_task()`/`ensure_future()` all reach
499
+ for the *current thread's* loop at construction time, not just to await,
500
+ and a sync cell's body runs off-loop (`asyncio.to_thread`) to keep the
501
+ kernel responsive — so building one directly in the cell raises "no
502
+ current event loop" before defer() ever gets a look at it.
494
503
  """
495
504
  async with _task_scope(task_id) as task:
496
505
  try:
497
- await coro
506
+ awaitable = thunk()
507
+ if not inspect.isawaitable(awaitable):
508
+ raise TypeError(
509
+ "defer() factory must return an awaitable, got "
510
+ f"{type(awaitable).__name__}"
511
+ )
512
+ await awaitable
498
513
  except asyncio.CancelledError:
499
514
  task["exception"] = "CancelledError"
500
515
  except BaseException as exc:
@@ -508,13 +523,32 @@ def _make_defer(loop: asyncio.AbstractEventLoop):
508
523
  def defer(coro, label: str | None = None) -> str:
509
524
  """Schedule a coroutine as a tracked task. Returns task_id immediately.
510
525
 
526
+ `coro` is normally a coroutine object (`defer(my_async_fn())`). It
527
+ may also be a zero-arg callable that *builds* one —
528
+ `defer(lambda: asyncio.gather(one(), two()))` — for constructs that
529
+ need a running loop to construct, not just to await. A sync cell's
530
+ body runs in a worker thread with no event loop set, so calling
531
+ `asyncio.gather(...)` there eagerly (as a `defer()` argument) fails
532
+ before `defer()` is even entered; wrapping it in a zero-arg callable
533
+ defers the call itself onto the kernel's loop, where it belongs.
534
+
511
535
  The task is visible to get_task and cancel. On completion, a task_done
512
536
  channel notification is pushed.
513
537
  """
514
- if not inspect.iscoroutine(coro):
538
+ if inspect.iscoroutine(coro):
539
+ thunk = lambda c=coro: c # noqa: E731
540
+ elif inspect.iscoroutinefunction(coro):
541
+ raise TypeError(
542
+ f"defer() expects a coroutine object, got the function "
543
+ f"{coro.__name__!r} itself. Call it as: defer({coro.__name__}())"
544
+ )
545
+ elif callable(coro):
546
+ thunk = coro
547
+ else:
515
548
  raise TypeError(
516
- f"defer() expects a coroutine object, got {type(coro).__name__}. "
517
- "Call it as: defer(my_async_fn())"
549
+ "defer() expects a coroutine object (or a zero-arg callable "
550
+ f"that builds one), got {type(coro).__name__}. Call it as: "
551
+ "defer(my_async_fn()) or defer(lambda: asyncio.gather(...))"
518
552
  )
519
553
  # Inherit the calling cell's originating session so a background task
520
554
  # reports back to whoever asked for it. defer() from an @every body or
@@ -527,7 +561,7 @@ def _make_defer(loop: asyncio.AbstractEventLoop):
527
561
  task["label"] = label
528
562
  src_label = label or "..."
529
563
  events.emit(CellStart(task_id, f"defer({src_label})", time.time()))
530
- asyncio.run_coroutine_threadsafe(_run_deferred(task_id, coro), loop)
564
+ asyncio.run_coroutine_threadsafe(_run_deferred(task_id, thunk), loop)
531
565
  return task_id
532
566
 
533
567
  return defer
@@ -221,8 +221,16 @@ def _format_user_traceback(exc: BaseException) -> str:
221
221
  )
222
222
  elif "no current event loop" in msg or "no running event loop" in msg:
223
223
  formatted += (
224
- "\nHint: this cell ran in a background thread "
225
- "(no 'await' detected). Use defer() to schedule async work:\n"
226
- " defer(some_coroutine())\n"
224
+ "\nHint: this cell ran in a background thread (no top-level "
225
+ "'await' in the cell), so asyncio.gather()/create_task()/"
226
+ "ensure_future() had no loop to attach to — they reach for "
227
+ "one at construction, not just to await it. Build it inside "
228
+ "an async def instead:\n"
229
+ " async def _run():\n"
230
+ " return await asyncio.gather(a(), b())\n"
231
+ " await _run()\n"
232
+ "or, if deferring it, pass defer() a zero-arg callable so it "
233
+ "builds on the kernel's loop rather than in this thread:\n"
234
+ " defer(lambda: asyncio.gather(a(), b()))\n"
227
235
  )
228
236
  return formatted
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes