feat: Detect uv installation in `colab update --install` (#31)
Seth Troisi committed
May 28, 2026 at 01:15 UTC
223df556fd4180e1704024d2b652e03a2dd9613f
4 files changed
+48
-12
docs/04_automation_and_utility.md
+12
-8
@@ -1,5 +1,6 @@
1
---
2
log:
3
+2026-05-27: Extended `colab update --install` to detect if the CLI was installed via `uv tool install` (by checking if `sys.executable` contains `/uv/`) and if so, use `uv tool install -U google-colab-cli` to upgrade.
4
2026-05-27: Updated auto-update upgrade hint to recommend `pip install --upgrade google-colab-cli` instead of `colab`, aligning with the PyPI package name.
5
2026-05-27: `colab url` now emits BOTH the `?dbu=<urlencoded path>` query parameter (existing) AND a new `#datalabBackendUrl=<full URL>` hash fragment (new). Format: `https://<host>/notebooks/empty.ipynb?dbu=%2Ftun%2Fm%2F<endpoint>#datalabBackendUrl=<host>/tun/m/<endpoint>`. Why both: some Colab frontend code paths consult the hash fragment first and ignore `dbu` entirely, so the previously-emitted query-only form failed silently for those users (the frontend fell through to allocating a fresh VM via `/tun/m/assign`). The fragment value is a FULL URL with scheme + host (NOT just the path) and is emitted RAW (no URL encoding) because browsers don't decode the fragment before passing `location.hash` to page JS — Colab's parser calls `new URL(rawString)` directly. The fragment host always matches `--host` so Colab's same-origin enforcement on embedded backend URLs doesn't block the connection, and sandbox/dev users (`--host https://colab.sandbox.google.com`) get a sandbox fragment automatically. Three new test cases in `tests/test_url.py` cover the raw-encoding requirement (`%3A`/`%2F` must NOT appear in the fragment), the both-signals-present invariant, and `--open` propagating the fragment to `webbrowser.open()`. Integration-verified live against synthetic session state with three host shapes (default, sandbox, trailing-slash); all produced correctly-shaped URLs with no `//` artifacts.
6
2026-05-07: Added a developer-only `colab whoami` subcommand (hidden from `colab --help`). Mints an access token via the same `auth.get_credentials(...)` path the rest of the CLI uses (honoring the global `--auth=...` flag), refreshes the credentials, then queries `https://oauth2.googleapis.com/tokeninfo` to print the email, scopes, audience, and expiry of whatever the CLI is about to send. Built specifically to short-circuit the "why is my call to colab.pa.googleapis.com 403-ing" debugging loop — the answer is almost always "missing scope" or "wrong identity", both of which `whoami` makes immediately visible. Hidden via `app.command(hidden=True)`; reachable via `colab whoami` or `colab whoami --help`. Suppressed from the daily-update banner check (added to `_AUTO_UPDATE_SUPPRESSED` in `cli.py`) so the banner doesn't obscure the auth output.
@@ -186,14 +187,17 @@ remediation guidance) rather than silently after ~1 minute via the daemon.
187
update.` hint. The cached banner shown between fetches uses the generic
188
`Run 'colab update' to update.` hint.
189
- **Self-install (`--install`)**: An opt-in `--install` flag (default
189
- `False`) makes `colab update` shell out to `pip install -U
190
- google-colab-cli` (using `sys.executable` so the upgrade lands in the
191
- same interpreter the CLI is running under). **Linux only**; on other
192
- platforms the command exits non-zero with an explanatory message. When
193
- the cached `latest_version` is already at or below the current install,
194
- the flag is a silent no-op so it is safe to wire into automation. If
195
- `pip` exits non-zero, `colab update --install` propagates the same
196
- exit code.
190
+ `False`) makes `colab update` upgrade the CLI in place (**Linux only**).
191
+ It detects how the CLI was installed:
192
+ - If `sys.executable` contains `/uv/tools` (indicating it was installed via
193
+ `uv tool install`), it runs `uv tool install -U google-colab-cli`.
194
+ - Otherwise, runs `pip install -U google-colab-cli` using `sys.executable`
195
+ to ensure the upgrade lands in the same interpreter.
196
+ On other platforms, the command exits non-zero with an explanatory
197
+ message. When the cached `latest_version` is already at or below the
198
+ current install, the flag is a silent no-op so it is safe to wire into
199
+ automation. If the upgrade command exits non-zero, `colab update --install`
200
+ propagates the same exit code.
201
202
### 8. Identity Inspection (`colab whoami`) [developer-only]
203
src/colab_cli/auto_update.py
+8
-2
@@ -216,10 +216,16 @@ PYPI_PACKAGE_NAME = "google-colab-cli"
216
217
218
def self_install() -> None:
219
- """Run ``pip install -U <PYPI_PACKAGE_NAME>`` to upgrade the CLI in place."""
219
+ """Upgrade the CLI in place, detecting uv vs pip."""
220
import sys
221
222
- cmd = [sys.executable, "-m", "pip", "install", "-U", PYPI_PACKAGE_NAME]
222
+ # If the executable path contains "/uv/", we assume it was installed via
223
+ # `uv tool install` and use `uv` to upgrade it.
224
+ if "/uv/tools/" in sys.executable:
225
+ cmd = ["uv", "tool", "install", "-U", PYPI_PACKAGE_NAME]
226
+ else:
227
+ cmd = [sys.executable, "-m", "pip", "install", "-U", PYPI_PACKAGE_NAME]
228
+
229
typer.echo(f"[colab] Running: {' '.join(cmd)}")
230
result = subprocess.run(cmd)
231
if result.returncode != 0:
src/colab_cli/commands/utility.py
+1
-1
@@ -369,7 +369,7 @@ def update_command(
369
raise typer.Exit(code=1)
370
371
# Skip the install when the current version already matches (or exceeds)
372
- # the latest known version, to avoid an unnecessary pip subprocess.
372
+ # the latest known version, to avoid an unnecessary subprocess call.
373
settings = state.settings_store.load()
374
if settings.latest_version and not auto_update._is_newer(
375
settings.latest_version, auto_update.get_app_version()
tests/test_update.py
+27
-1
@@ -398,6 +398,7 @@ def test_install_flag_runs_pip_install_upgrade(
398
mock_pypi({"info": {"version": "1.1.0"}})
399
fake_settings()
400
mocker.patch("colab_cli.commands.utility.platform.system", return_value="Linux")
401
+ mocker.patch("sys.executable", "/usr/bin/python")
402
run = mocker.patch(
403
"colab_cli.auto_update.subprocess.run",
404
return_value=mocker.Mock(returncode=0),
@@ -409,7 +410,32 @@ def test_install_flag_runs_pip_install_upgrade(
410
args, _ = run.call_args
411
# Use sys.executable to avoid PATH ambiguity / virtualenv mixups.
412
cmd = args[0]
412
- assert cmd[1:] == ["-m", "pip", "install", "-U", "google-colab-cli"]
413
+ assert cmd == ["/usr/bin/python", "-m", "pip", "install", "-U", "google-colab-cli"]
414
+
415
+
416
+def test_install_flag_runs_uv_tool_install(
417
+ mocker, app_version, fake_settings, mock_pypi
418
+):
419
+ """`colab update --install` shells out to `uv tool install -U google-colab-cli`
420
+ when sys.executable contains '/uv/'."""
421
+ app_version("1.0.0")
422
+ mock_pypi({"info": {"version": "1.1.0"}})
423
+ fake_settings()
424
+ mocker.patch("colab_cli.commands.utility.platform.system", return_value="Linux")
425
+ mocker.patch(
426
+ "sys.executable", "/home/user/.local/share/uv/tools/google-colab-cli/bin/python"
427
+ )
428
+ run = mocker.patch(
429
+ "colab_cli.auto_update.subprocess.run",
430
+ return_value=mocker.Mock(returncode=0),
431
+ )
432
+
433
+ result = runner.invoke(app, ["update", "--install"])
434
+ assert result.exit_code == 0
435
+ assert run.call_count == 1
436
+ args, _ = run.call_args
437
+ cmd = args[0]
438
+ assert cmd == ["uv", "tool", "install", "-U", "google-colab-cli"]
439
440
441
def test_install_flag_errors_on_non_linux(