Skip to content

Commit e57453d

Browse files
authored
Merge pull request #1831 from transformerlab/fix/cli-local-install-upgrade-hint
Warn when CLI is installed from a non-PyPI source
2 parents 5c8b8a7 + 6d95ecc commit e57453d

6 files changed

Lines changed: 131 additions & 19 deletions

File tree

cli/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "transformerlab-cli"
7-
version = "0.0.17"
7+
version = "0.0.18"
88
description = "Transformer Lab CLI"
99
requires-python = ">=3.10"
1010
authors = [{ name = "Transformer Lab", email = "hello@transformerlab.ai" }]

cli/src/transformerlab_cli/commands/version.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,17 @@
1111
@app.command()
1212
def version() -> None:
1313
"""Display the CLI version and check for updates."""
14-
from transformerlab_cli.util.pypi import fetch_latest_version, get_installed_version, _parse_version
14+
from transformerlab_cli.util.pypi import (
15+
_parse_version,
16+
describe_install_source,
17+
fetch_latest_version,
18+
get_install_source,
19+
get_installed_version,
20+
)
1521

1622
installed = get_installed_version()
1723
latest = fetch_latest_version()
24+
source = get_install_source()
1825

1926
# Determine state: update available, up to date, or check failed
2027
update_available = False
@@ -29,18 +36,33 @@ def version() -> None:
2936
data: dict[str, object] = {"installed_version": installed, "update_available": update_available}
3037
if update_available:
3138
data["latest_version"] = latest
32-
data["upgrade_command"] = "uv tool upgrade transformerlab-cli"
39+
data["upgrade_command"] = (
40+
"uv tool install --force transformerlab-cli"
41+
if source is not None
42+
else "uv tool upgrade transformerlab-cli"
43+
)
44+
if source is not None:
45+
data["install_source"] = source
3346
if not check_succeeded:
3447
data["check_succeeded"] = False
3548
print(json.dumps(data))
3649
else:
3750
console.print(f"v{installed}", highlight=False)
3851
if update_available:
39-
console.print(
40-
f"[yellow]Update available:[/yellow] v{latest}\n"
41-
f"Run [bold]uv tool upgrade transformerlab-cli[/bold] to upgrade."
42-
)
52+
console.print(f"[yellow]Update available:[/yellow] v{latest}")
53+
if source is None:
54+
console.print("Run [bold]uv tool upgrade transformerlab-cli[/bold] to upgrade.")
55+
else:
56+
console.print(
57+
f"[yellow]Installed from {describe_install_source(source)}[/yellow]\n"
58+
f"[dim]`uv tool upgrade` resolves against this source, not PyPI — "
59+
f"it won't fetch v{latest}.\n"
60+
f"Pull source updates, or run "
61+
f"[bold]uv tool install --force transformerlab-cli[/bold] to switch to PyPI.[/dim]"
62+
)
4363
elif check_succeeded:
4464
console.print("[green]You are up to date.[/green]")
65+
if source is not None:
66+
console.print(f"[dim]Installed from {describe_install_source(source)}[/dim]")
4567
else:
4668
console.print("[dim]Could not check for updates.[/dim]")

cli/src/transformerlab_cli/util/pypi.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import json
44
import os
55
import time
6-
from importlib.metadata import PackageNotFoundError
6+
from importlib.metadata import PackageNotFoundError, distribution
77
from importlib.metadata import version as pkg_version
88

99
import httpx
@@ -13,16 +13,52 @@
1313
PYPI_URL = "https://pypi.org/pypi/transformerlab-cli/json"
1414
CACHE_FILE = os.path.join(CONFIG_DIR, ".version_cache.json")
1515
CACHE_TTL_SECONDS = 4 * 60 * 60 # 4 hours
16+
PACKAGE_NAME = "transformerlab-cli"
1617

1718

1819
def get_installed_version() -> str:
1920
"""Return the installed CLI version from package metadata, or 'unknown'."""
2021
try:
21-
return pkg_version("transformerlab-cli")
22+
return pkg_version(PACKAGE_NAME)
2223
except PackageNotFoundError:
2324
return "unknown"
2425

2526

27+
def get_install_source() -> dict | None:
28+
"""Return PEP 610 direct_url.json contents for non-PyPI installs, else None.
29+
30+
PyPI installs have no direct_url.json. Local paths, editable installs, and
31+
VCS (git) installs do — so its presence means `uv tool upgrade` will resolve
32+
against that source rather than PyPI.
33+
"""
34+
try:
35+
dist = distribution(PACKAGE_NAME)
36+
raw = dist.read_text("direct_url.json")
37+
if raw is None:
38+
return None
39+
return json.loads(raw)
40+
except Exception:
41+
return None
42+
43+
44+
def describe_install_source(direct_url: dict) -> str:
45+
"""Human-readable description of a direct_url.json entry."""
46+
url = direct_url.get("url", "") or ""
47+
dir_info = direct_url.get("dir_info") or {}
48+
vcs_info = direct_url.get("vcs_info") or {}
49+
50+
if vcs_info:
51+
vcs = vcs_info.get("vcs", "vcs")
52+
commit = (vcs_info.get("commit_id") or "")[:7]
53+
suffix = f" @ {commit}" if commit else ""
54+
return f"{vcs}: {url}{suffix}"
55+
if url.startswith("file://"):
56+
path = url[len("file://") :]
57+
kind = "editable directory" if dir_info.get("editable") else "local directory"
58+
return f"{kind}: {path}"
59+
return url or "unknown source"
60+
61+
2662
def _parse_version(v: str) -> tuple[int, ...]:
2763
"""Parse a PEP 440 version string into a tuple of ints for comparison.
2864

cli/src/transformerlab_cli/util/version_check.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,32 @@
77
def check_for_update(console: Console) -> None:
88
"""Check if a newer CLI version is available on PyPI and print a banner if so."""
99
try:
10-
from transformerlab_cli.util.pypi import is_update_available
10+
from transformerlab_cli.util.pypi import (
11+
describe_install_source,
12+
get_install_source,
13+
is_update_available,
14+
)
1115

1216
installed, latest = is_update_available()
1317
if latest is None:
1418
return
1519

16-
console.print(
17-
Panel(
18-
f"[yellow]Update available![/yellow] "
19-
f"You are running [bold]v{installed}[/bold], but [bold]v{latest}[/bold] is available.\n"
20-
f"Run [bold]uv tool upgrade transformerlab-cli[/bold] to upgrade.",
21-
border_style="yellow",
22-
expand=False,
23-
)
20+
source = get_install_source()
21+
header = (
22+
f"[yellow]Update available![/yellow] "
23+
f"You are running [bold]v{installed}[/bold], but [bold]v{latest}[/bold] is available."
2424
)
25+
if source is None:
26+
body = f"{header}\nRun [bold]uv tool upgrade transformerlab-cli[/bold] to upgrade."
27+
else:
28+
body = (
29+
f"{header}\n"
30+
f"[dim]Installed from {describe_install_source(source)} — "
31+
f"`uv tool upgrade` won't fetch PyPI.\n"
32+
f"Run [bold]uv tool install --force transformerlab-cli[/bold] to switch to PyPI.[/dim]"
33+
)
34+
35+
console.print(Panel(body, border_style="yellow", expand=False))
2536
except Exception:
2637
# Never let version check failures interrupt CLI usage
2738
pass

cli/tests/test_pypi_cache.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
_parse_version,
1010
_read_cache,
1111
_write_cache,
12+
describe_install_source,
1213
fetch_latest_version,
14+
get_install_source,
1315
is_update_available,
1416
)
1517

@@ -144,3 +146,44 @@ def test_is_update_available_unknown_version():
144146
installed, latest = is_update_available()
145147
assert installed == "unknown"
146148
assert latest is None
149+
150+
151+
def test_get_install_source_pypi_returns_none():
152+
"""A PyPI install has no direct_url.json, so get_install_source returns None."""
153+
mock_dist = MagicMock()
154+
mock_dist.read_text.return_value = None
155+
with patch("transformerlab_cli.util.pypi.distribution", return_value=mock_dist):
156+
assert get_install_source() is None
157+
158+
159+
def test_get_install_source_local_path():
160+
"""A local-path install returns parsed direct_url.json contents."""
161+
payload = {"url": "file:///Users/alice/project/cli", "dir_info": {"editable": False}}
162+
mock_dist = MagicMock()
163+
mock_dist.read_text.return_value = json.dumps(payload)
164+
with patch("transformerlab_cli.util.pypi.distribution", return_value=mock_dist):
165+
assert get_install_source() == payload
166+
167+
168+
def test_get_install_source_package_not_found():
169+
"""Never raises — returns None if the package metadata lookup fails."""
170+
with patch("transformerlab_cli.util.pypi.distribution", side_effect=Exception("missing")):
171+
assert get_install_source() is None
172+
173+
174+
def test_describe_install_source_local():
175+
payload = {"url": "file:///Users/alice/project/cli", "dir_info": {"editable": False}}
176+
assert describe_install_source(payload) == "local directory: /Users/alice/project/cli"
177+
178+
179+
def test_describe_install_source_editable():
180+
payload = {"url": "file:///Users/alice/project/cli", "dir_info": {"editable": True}}
181+
assert describe_install_source(payload) == "editable directory: /Users/alice/project/cli"
182+
183+
184+
def test_describe_install_source_vcs():
185+
payload = {
186+
"url": "https://github.com/example/repo",
187+
"vcs_info": {"vcs": "git", "commit_id": "abcdef1234567890"},
188+
}
189+
assert describe_install_source(payload) == "git: https://github.com/example/repo @ abcdef1"

cli/uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)