repld-tool 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.
- {repld_tool-0.2.0 → repld_tool-0.2.2}/PKG-INFO +1 -1
- {repld_tool-0.2.0 → repld_tool-0.2.2}/pyproject.toml +1 -1
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/__init__.py +3 -1
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/bind.py +44 -1
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/help.py +60 -4
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/kernel.py +57 -11
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/runtime.py +11 -3
- {repld_tool-0.2.0 → repld_tool-0.2.2}/LICENSE +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/README.md +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/.ruff_cache/.gitignore +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/.ruff_cache/0.15.22/14226964882126921391 +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/.ruff_cache/CACHEDIR.TAG +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/__main__.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/bg.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/bridge.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/bridge_tools.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/browser/__init__.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/browser/browser.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/browser/capture.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/browser/cdp.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/browser/har.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/browser/observe.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/browser/pin.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/browser/png.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/browser/pool.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/browser/row.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/browser/selector.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/browser/session.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/browser/tab.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/browser/tab_query.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/browser/target.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/browser_dispatch.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/channel.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/cli.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/cli_args.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/core_schemas.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/dashboard.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/dashboard_cmd.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/dashboard_html.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/display.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/eventlog.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/events.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/exec_cmd.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/gate_cmd.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/gates.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/gist_api.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/gist_cmd.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/gist_deps.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/gist_links.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/gist_lint.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/gists.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/ipc.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/kernel_context.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/lifecycle_cmd.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/log_cmd.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/paths.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/protocol.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/relaunch.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/render.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/sessions.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/spawn.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/state.py +0 -0
- {repld_tool-0.2.0 → repld_tool-0.2.2}/src/repld/tasks.py +0 -0
|
@@ -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(
|
|
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: ...
|
|
@@ -161,12 +161,50 @@ def adopt(venv: Path) -> Path | None:
|
|
|
161
161
|
return sp
|
|
162
162
|
|
|
163
163
|
|
|
164
|
+
def has_browser_extra() -> bool:
|
|
165
|
+
"""Whether *this* interpreter can import the browser stack.
|
|
166
|
+
|
|
167
|
+
All three are load-bearing and imported eagerly, so the question is not
|
|
168
|
+
"which one" but "are they all here" — `browser/png.py` pulls in PIL at
|
|
169
|
+
module scope, and `kernel._inject_builtins` reads any one of them missing
|
|
170
|
+
as the whole extra being absent.
|
|
171
|
+
|
|
172
|
+
``find_spec`` rather than an import: this runs on every bound start, and
|
|
173
|
+
importing duckdb to find out whether we could is most of the cost the extra
|
|
174
|
+
exists to avoid paying.
|
|
175
|
+
"""
|
|
176
|
+
from importlib.util import find_spec
|
|
177
|
+
|
|
178
|
+
for mod in ("duckdb", "websockets", "PIL"):
|
|
179
|
+
try:
|
|
180
|
+
if find_spec(mod) is None:
|
|
181
|
+
return False
|
|
182
|
+
except (ImportError, ValueError):
|
|
183
|
+
return False
|
|
184
|
+
return True
|
|
185
|
+
|
|
186
|
+
|
|
164
187
|
def uv_run_argv(argv: list[str]) -> list[str] | None:
|
|
165
188
|
"""The ``uv run`` command that relaunches ``repld <argv>`` under the
|
|
166
189
|
project's interpreter, or None if uv isn't on PATH.
|
|
167
190
|
|
|
168
191
|
Reuses ``relaunch._editable_path()`` so a local checkout keeps winning over
|
|
169
192
|
the published package, exactly as it does for ``repld browser``.
|
|
193
|
+
|
|
194
|
+
**The browser extra is carried across the re-exec, or the rebind silently
|
|
195
|
+
takes it away.** ``uv run --with`` builds a fresh environment from the spec
|
|
196
|
+
it is given; the interpreter we are leaving does not contribute to it. So a
|
|
197
|
+
repld installed *with* the extra — the tool venv carrying duckdb,
|
|
198
|
+
websockets and PIL — re-execs into an environment that has none of them,
|
|
199
|
+
and the kernel comes up with no ``browser`` builtin, no browser MCP tools,
|
|
200
|
+
and nothing anywhere saying why. The observable shape of that bug is the
|
|
201
|
+
tell: browser worked in every project *without* a ``./.venv`` and in none
|
|
202
|
+
of the projects with one, since only the latter rebind at all.
|
|
203
|
+
|
|
204
|
+
Conditional on what we already have rather than unconditional, so a repld
|
|
205
|
+
installed without the extra doesn't start resolving duckdb behind the
|
|
206
|
+
user's back. The rule is that the re-exec must not *lose* a capability, not
|
|
207
|
+
that it should acquire one.
|
|
170
208
|
"""
|
|
171
209
|
import shutil
|
|
172
210
|
|
|
@@ -175,8 +213,13 @@ def uv_run_argv(argv: list[str]) -> list[str] | None:
|
|
|
175
213
|
uv = shutil.which("uv")
|
|
176
214
|
if uv is None:
|
|
177
215
|
return None
|
|
216
|
+
extra = "[browser]" if has_browser_extra() else ""
|
|
178
217
|
path = _editable_path()
|
|
179
|
-
with_arg =
|
|
218
|
+
with_arg = (
|
|
219
|
+
["--with-editable", f"{path}{extra}"]
|
|
220
|
+
if path
|
|
221
|
+
else ["--with", f"repld-tool{extra}"]
|
|
222
|
+
)
|
|
180
223
|
return [uv, "run", *with_arg, "repld", *argv]
|
|
181
224
|
|
|
182
225
|
|
|
@@ -96,8 +96,6 @@ _PLAYBOOK = (
|
|
|
96
96
|
"Read repld://docs/playbook for the full methodology."
|
|
97
97
|
)
|
|
98
98
|
|
|
99
|
-
_REFERENCE = "Reference: `repld help <topic>` — topics: exec, browser, gists, gates\nRead repld://docs/guide for exec patterns and gist conventions. Read repld://docs/browser for the full browser API and internals.\nRead repld://docs/production when graduating gists to FastMCP/FastAPI."
|
|
100
|
-
|
|
101
99
|
|
|
102
100
|
# ---------------------------------------------------------------------------
|
|
103
101
|
# PLAYBOOK (repld://docs/playbook resource — workflow methodology)
|
|
@@ -992,7 +990,9 @@ def static_instructions() -> str:
|
|
|
992
990
|
depends on kernel state — the browser model, the gist listing, and
|
|
993
991
|
registered gist tools.
|
|
994
992
|
"""
|
|
995
|
-
return "\n\n".join(
|
|
993
|
+
return "\n\n".join(
|
|
994
|
+
[_EXEC_MODEL, _GISTS_MODEL, _deps_hint(), _PLAYBOOK, _reference()]
|
|
995
|
+
)
|
|
996
996
|
|
|
997
997
|
|
|
998
998
|
def build_instructions() -> str:
|
|
@@ -1029,7 +1029,7 @@ def build_instructions() -> str:
|
|
|
1029
1029
|
|
|
1030
1030
|
parts.append(_deps_hint())
|
|
1031
1031
|
parts.append(_PLAYBOOK)
|
|
1032
|
-
parts.append(
|
|
1032
|
+
parts.append(_reference())
|
|
1033
1033
|
return "\n\n".join(parts)
|
|
1034
1034
|
|
|
1035
1035
|
|
|
@@ -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)
|
|
@@ -1366,10 +1369,55 @@ Emits awaiting_human channel while blocked.
|
|
|
1366
1369
|
|
|
1367
1370
|
notify(content, **meta)
|
|
1368
1371
|
One-shot channel push to all MCP sessions.
|
|
1372
|
+
""",
|
|
1373
|
+
"migration": """\
|
|
1374
|
+
Why a repld project has no state files (0.1.x → 0.2).
|
|
1375
|
+
|
|
1376
|
+
repld 0.2 writes nothing into a project directory. Files an older setup
|
|
1377
|
+
created are absent on purpose — none of these are missing, they are gone:
|
|
1378
|
+
|
|
1379
|
+
.pyrepl.lock/.sock/.dashboard runtime state; now
|
|
1380
|
+
$XDG_RUNTIME_DIR/repld/projects/<slug>/ as
|
|
1381
|
+
kernel.lock/.sock/.dashboard. `repld status`
|
|
1382
|
+
prints the live paths.
|
|
1383
|
+
.mcp.json never written now. Register the server with
|
|
1384
|
+
`claude mcp add repld -- repld bridge`.
|
|
1385
|
+
CLAUDE.md repld:start block gone; that content is the MCP `initialize`
|
|
1386
|
+
instructions, composed fresh each session.
|
|
1387
|
+
repl.py + --init now ./repld_init.py, auto-detected and run by
|
|
1388
|
+
every kernel, whoever started it.
|
|
1389
|
+
__repld_tools__ removed in 0.2 and ignored, so a file still
|
|
1390
|
+
declaring one has silently lost its tools.
|
|
1391
|
+
`repld gist lint` is what reports it.
|
|
1392
|
+
|
|
1393
|
+
Still in the project and yours to commit: ./gists/, ./gists/.links, ./.env,
|
|
1394
|
+
./repld_init.py.
|
|
1395
|
+
|
|
1396
|
+
If you FIND a .pyrepl.* file, it is 0.1.x leftover, not something this version
|
|
1397
|
+
made. .pyrepl.lock names a pid that may still be running — check it with
|
|
1398
|
+
`ps -p <pid> -o pid,command` before signalling, since a stale lockfile can name
|
|
1399
|
+
a reused pid. .pyrepl.dashboard holds a dead dashboard's API token; it was only
|
|
1400
|
+
gitignored where `repld init` ran, so check `git ls-files | grep pyrepl`.
|
|
1401
|
+
Cleanup: https://angelsen.github.io/repld/docs/guides/upgrading/
|
|
1369
1402
|
""",
|
|
1370
1403
|
}
|
|
1371
1404
|
|
|
1372
1405
|
|
|
1406
|
+
def _reference() -> str:
|
|
1407
|
+
"""The always-loaded pointer block. Topic list derives from _TOPICS.
|
|
1408
|
+
|
|
1409
|
+
It was a literal listing four topics by hand, which is a second copy of
|
|
1410
|
+
`sorted(_TOPICS)` in the one string every session loads — so adding a topic
|
|
1411
|
+
silently published a list that omitted it.
|
|
1412
|
+
"""
|
|
1413
|
+
return (
|
|
1414
|
+
f"Reference: `repld help <topic>` — topics: {', '.join(sorted(_TOPICS))}\n"
|
|
1415
|
+
"Read repld://docs/guide for exec patterns and gist conventions. "
|
|
1416
|
+
"Read repld://docs/browser for the full browser API and internals.\n"
|
|
1417
|
+
"Read repld://docs/production when graduating gists to FastMCP/FastAPI."
|
|
1418
|
+
)
|
|
1419
|
+
|
|
1420
|
+
|
|
1373
1421
|
# `== Builtins ==` below recaps _EXEC_MODEL's builtins — keep in sync.
|
|
1374
1422
|
GUIDE = """\
|
|
1375
1423
|
repld — working guide
|
|
@@ -1421,6 +1469,14 @@ For intentionally long work, use defer():
|
|
|
1421
1469
|
|
|
1422
1470
|
defer(download_all_invoices(), label="invoice sync")
|
|
1423
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
|
+
|
|
1424
1480
|
This returns the task_id immediately. The channel notification arrives
|
|
1425
1481
|
when the coroutine completes (or fails).
|
|
1426
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,
|
|
491
|
-
"""
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
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
|
|
538
|
+
if inspect.iscoroutine(coro):
|
|
539
|
+
thunk = lambda c=coro: c # noqa: E731
|
|
540
|
+
elif inspect.iscoroutinefunction(coro):
|
|
515
541
|
raise TypeError(
|
|
516
|
-
f"defer() expects a coroutine object, got
|
|
517
|
-
"Call it as: defer(
|
|
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:
|
|
548
|
+
raise TypeError(
|
|
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,
|
|
564
|
+
asyncio.run_coroutine_threadsafe(_run_deferred(task_id, thunk), loop)
|
|
531
565
|
return task_id
|
|
532
566
|
|
|
533
567
|
return defer
|
|
@@ -1017,8 +1051,20 @@ def _inject_builtins(loop: asyncio.AbstractEventLoop) -> None:
|
|
|
1017
1051
|
# themselves up anyway — the pill's staleness check removes it, and the
|
|
1018
1052
|
# beforeunload guard with it, once Python stops heartbeating, which is
|
|
1019
1053
|
# exactly what a stopped loop looks like from the page.
|
|
1020
|
-
except ImportError:
|
|
1021
|
-
|
|
1054
|
+
except ImportError as e:
|
|
1055
|
+
# Not an error — most projects never install the extra. But swallowing
|
|
1056
|
+
# it silently means the failure surfaces much later and somewhere else:
|
|
1057
|
+
# `NameError: name 'browser' is not defined` in a cell, or 21 tools
|
|
1058
|
+
# simply absent from tools/list. Both read as "this kernel has no
|
|
1059
|
+
# browser" and neither says *why*, which sends the reader looking at
|
|
1060
|
+
# the browser stack instead of at the environment. This is the only
|
|
1061
|
+
# place that knows which module was missing, so it says so.
|
|
1062
|
+
print(
|
|
1063
|
+
f"repld: browser disabled — cannot import {e.name or e}. "
|
|
1064
|
+
"Install the `browser` extra, or run `repld browser` instead of "
|
|
1065
|
+
"`repld`.",
|
|
1066
|
+
file=sys.stderr,
|
|
1067
|
+
)
|
|
1022
1068
|
|
|
1023
1069
|
|
|
1024
1070
|
def _start_services(
|
|
@@ -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
|
-
"
|
|
226
|
-
"
|
|
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
|
|
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
|
|
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
|
|
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
|