feat: Support macOS for in-place self-update (#37)
Refactored the platform checks for the `colab update --install` command to enable support for macOS (Darwin) in addition to Linux. Added a helper `is_self_install_supported()` in `auto_update.py`.
Seth Troisi committed
Jun 2, 2026 at 09:58 UTC
ff13ccf3193cc0acd2612f7ca65388b596faf863
4 files changed
+66
-17
docs/04_automation_and_utility.md
+3
-3
@@ -1,6 +1,6 @@
1
---
2
log:
3
-2026-06-01: Improved `colab update` output. On Linux platforms, an additional message is shown recommending `colab update --install` to upgrade in place, positioned above the standard `pip`/`uv` installation command.
3
+2026-06-01: Enabled `colab update --install` self-update on macOS in addition to Linux. Refactored platform check logic to keep the implementation DRY and updated both tests and documentation. Also, on these platforms, an additional message is shown recommending `colab update --install` to upgrade in place, positioned above the standard `pip`/`uv` installation command.
4
2026-05-27: Refactored `colab README` and `colab AGENT` to bundle `README.md` and `AGENTS.md` via Hatchling's `force-include` and read them using `importlib.resources` instead of `importlib.metadata`. `colab AGENT` now correctly prints `AGENTS.md`.
5
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.
6
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.
@@ -186,12 +186,12 @@ remediation guidance) rather than silently after ~1 minute via the daemon.
186
the cache.
187
- **Notification**: If a new version is found, a non-intrusive message is
188
printed to the console with a `Run 'pip install --upgrade google-colab-cli' to
189
- update.` hint. On Linux platforms where `--install` self-update is supported,
189
+ update.` hint. On Linux and macOS platforms where `--install` self-update is supported,
190
an additional hint `You can run 'colab update --install' to upgrade in place.`
191
is displayed above the pip/uv install command. The cached banner shown between
192
fetches uses the generic `Run 'colab update' to update.` hint.
193
- **Self-install (`--install`)**: An opt-in `--install` flag (default
194
- `False`) makes `colab update` upgrade the CLI in place (**Linux only**).
194
+ `False`) makes `colab update` upgrade the CLI in place (**Linux and macOS**).
195
It detects how the CLI was installed:
196
- If `sys.executable` contains `/uv/tools` (indicating it was installed via
197
`uv tool install`), it runs `uv tool install -U google-colab-cli`.
src/colab_cli/auto_update.py
+7
-2
@@ -43,6 +43,11 @@ PYPI_PACKAGE_NAME = "google-colab-cli"
43
# ---------- Version detection -------------------------------------------
44
45
46
+def is_self_install_supported() -> bool:
47
+ """Return True if self-install (--install) is supported on the current platform."""
48
+ return platform.system() in ("Linux", "Darwin")
49
+
50
+
51
def get_app_version() -> str:
52
"""Return the installed package version, falling back to the git short hash."""
53
try:
@@ -113,7 +118,7 @@ def announce_upgrade(
118
typer.echo(
119
f"\n[colab] A new version of Colab CLI is available: {latest} (current: {current})"
120
)
116
- if platform.system() == "Linux" and ("pip" in install_cmd or "uv" in install_cmd):
121
+ if is_self_install_supported() and ("pip" in install_cmd or "uv" in install_cmd):
122
typer.echo("[colab] You can run 'colab update --install' to upgrade in place.")
123
typer.echo(f"[colab] Run '{install_cmd}' to update.")
124
if show_disable_hint:
@@ -132,7 +137,7 @@ def _get_install_command() -> str:
137
"""Return the recommended installation command based on the environment."""
138
import sys
139
135
- if platform.system() == "Linux" and "/uv/tools/" in sys.executable:
140
+ if is_self_install_supported() and "/uv/tools/" in sys.executable:
141
return f"uv tool install -U {PYPI_PACKAGE_NAME}"
142
return f"pip install --upgrade {PYPI_PACKAGE_NAME}"
143
src/colab_cli/commands/utility.py
+3
-3
@@ -12,7 +12,6 @@
12
# See the License for the specific language governing permissions and
13
# limitations under the License.
14
15
-import platform
15
from typing import Optional
16
17
import typer
@@ -362,9 +361,10 @@ def update_command(
361
if not install:
362
return
363
365
- if platform.system() != "Linux":
364
+ if not auto_update.is_self_install_supported():
365
typer.echo(
367
- "[colab] '--install' self-install is only supported on Linux.", err=True
366
+ "[colab] '--install' self-install is only supported on Linux and macOS.",
367
+ err=True,
368
)
369
raise typer.Exit(code=1)
370
tests/test_update.py
+53
-9
@@ -163,7 +163,7 @@ def test_pypi_upgrade_uses_uv_hint(mocker, app_version, fake_settings, mock_pypi
163
assert idx_install < idx_uv
164
165
166
-def test_pypi_upgrade_uses_pip_hint_non_linux(
166
+def test_pypi_upgrade_uses_pip_hint_macos(
167
mocker, app_version, fake_settings, mock_pypi
168
):
169
app_version("1.0.0")
@@ -172,6 +172,30 @@ def test_pypi_upgrade_uses_pip_hint_non_linux(
172
mocker.patch("sys.executable", "/usr/bin/python")
173
mocker.patch("colab_cli.auto_update.platform.system", return_value="Darwin")
174
175
+ result = runner.invoke(app, ["update"])
176
+ assert result.exit_code == 0
177
+ assert "available: 1.1.0 (current: 1.0.0)" in result.output
178
+ assert "You can run 'colab update --install' to upgrade in place." in result.output
179
+ assert "Run 'pip install --upgrade google-colab-cli' to update." in result.output
180
+
181
+ idx_install = result.output.find(
182
+ "You can run 'colab update --install' to upgrade in place."
183
+ )
184
+ idx_pip = result.output.find(
185
+ "Run 'pip install --upgrade google-colab-cli' to update."
186
+ )
187
+ assert idx_install < idx_pip
188
+
189
+
190
+def test_pypi_upgrade_uses_pip_hint_windows(
191
+ mocker, app_version, fake_settings, mock_pypi
192
+):
193
+ app_version("1.0.0")
194
+ mock_pypi({"info": {"version": "1.1.0"}})
195
+ fake_settings()
196
+ mocker.patch("sys.executable", "/usr/bin/python")
197
+ mocker.patch("colab_cli.auto_update.platform.system", return_value="Windows")
198
+
199
result = runner.invoke(app, ["update"])
200
assert result.exit_code == 0
201
assert "available: 1.1.0 (current: 1.0.0)" in result.output
@@ -448,7 +472,7 @@ def test_install_flag_runs_pip_install_upgrade(
472
app_version("1.0.0")
473
mock_pypi({"info": {"version": "1.1.0"}})
474
fake_settings()
451
- mocker.patch("colab_cli.commands.utility.platform.system", return_value="Linux")
475
+ mocker.patch("colab_cli.auto_update.platform.system", return_value="Linux")
476
mocker.patch("sys.executable", "/usr/bin/python")
477
run = mocker.patch(
478
"colab_cli.auto_update.subprocess.run",
@@ -472,7 +496,7 @@ def test_install_flag_runs_uv_tool_install(
496
app_version("1.0.0")
497
mock_pypi({"info": {"version": "1.1.0"}})
498
fake_settings()
475
- mocker.patch("colab_cli.commands.utility.platform.system", return_value="Linux")
499
+ mocker.patch("colab_cli.auto_update.platform.system", return_value="Linux")
500
mocker.patch(
501
"sys.executable", "/home/user/.local/share/uv/tools/google-colab-cli/bin/python"
502
)
@@ -489,21 +513,41 @@ def test_install_flag_runs_uv_tool_install(
513
assert cmd == ["uv", "tool", "install", "-U", "google-colab-cli"]
514
515
492
-def test_install_flag_errors_on_non_linux(
516
+def test_install_flag_errors_on_unsupported_platform(
517
mocker, app_version, fake_settings, mock_pypi
518
):
495
- """`--install` is gated to Linux; on other platforms the command must
519
+ """`--install` is gated to Linux and macOS; on other platforms the command must
520
exit non-zero with an explanatory message and skip the pip subprocess."""
521
app_version("1.0.0")
522
mock_pypi({"info": {"version": "1.1.0"}})
523
fake_settings()
500
- mocker.patch("colab_cli.commands.utility.platform.system", return_value="Darwin")
524
+ mocker.patch("colab_cli.auto_update.platform.system", return_value="Windows")
525
run = mocker.patch("colab_cli.auto_update.subprocess.run")
526
527
result = runner.invoke(app, ["update", "--install"])
528
assert result.exit_code != 0
529
assert run.call_count == 0
506
- assert "only supported on Linux" in result.output
530
+ assert "only supported on Linux and macOS" in result.output
531
+
532
+
533
+def test_install_flag_runs_on_macos(mocker, app_version, fake_settings, mock_pypi):
534
+ """`colab update --install` shells out to pip/uv when running on macOS."""
535
+ app_version("1.0.0")
536
+ mock_pypi({"info": {"version": "1.1.0"}})
537
+ fake_settings()
538
+ mocker.patch("colab_cli.auto_update.platform.system", return_value="Darwin")
539
+ mocker.patch("sys.executable", "/usr/bin/python")
540
+ run = mocker.patch(
541
+ "colab_cli.auto_update.subprocess.run",
542
+ return_value=mocker.Mock(returncode=0),
543
+ )
544
+
545
+ result = runner.invoke(app, ["update", "--install"])
546
+ assert result.exit_code == 0
547
+ assert run.call_count == 1
548
+ args, _ = run.call_args
549
+ cmd = args[0]
550
+ assert cmd == ["/usr/bin/python", "-m", "pip", "install", "-U", "google-colab-cli"]
551
552
553
def test_install_flag_no_op_when_already_up_to_date(
@@ -514,7 +558,7 @@ def test_install_flag_no_op_when_already_up_to_date(
558
app_version("1.1.0")
559
mock_pypi({"info": {"version": "1.1.0"}})
560
fake_settings()
517
- mocker.patch("colab_cli.commands.utility.platform.system", return_value="Linux")
561
+ mocker.patch("colab_cli.auto_update.platform.system", return_value="Linux")
562
run = mocker.patch("colab_cli.auto_update.subprocess.run")
563
564
result = runner.invoke(app, ["update", "--install"])
@@ -529,7 +573,7 @@ def test_install_flag_propagates_pip_failure(
573
app_version("1.0.0")
574
mock_pypi({"info": {"version": "1.1.0"}})
575
fake_settings()
532
- mocker.patch("colab_cli.commands.utility.platform.system", return_value="Linux")
576
+ mocker.patch("colab_cli.auto_update.platform.system", return_value="Linux")
577
mocker.patch(
578
"colab_cli.auto_update.subprocess.run",
579
return_value=mocker.Mock(returncode=2),