mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-05-03 00:05:55 +00:00
feat(utils): add abbreviate_path for smart path/URL truncation
This commit is contained in:
parent
02597c3ec9
commit
f452af6c62
106
nanobot/utils/path.py
Normal file
106
nanobot/utils/path.py
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
"""Path abbreviation utilities for display."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
|
||||||
|
def abbreviate_path(path: str, max_len: int = 40) -> str:
|
||||||
|
"""Abbreviate a file path or URL, preserving basename and key directories.
|
||||||
|
|
||||||
|
Strategy:
|
||||||
|
1. Return as-is if short enough
|
||||||
|
2. Replace home directory with ~/
|
||||||
|
3. From right, keep basename + parent dirs until budget exhausted
|
||||||
|
4. Prefix with …/
|
||||||
|
"""
|
||||||
|
if not path:
|
||||||
|
return path
|
||||||
|
|
||||||
|
# Handle URLs: preserve scheme://domain + filename
|
||||||
|
if re.match(r"https?://", path):
|
||||||
|
return _abbreviate_url(path, max_len)
|
||||||
|
|
||||||
|
# Normalize separators to /
|
||||||
|
normalized = path.replace("\\", "/")
|
||||||
|
|
||||||
|
# Replace home directory
|
||||||
|
home = os.path.expanduser("~").replace("\\", "/")
|
||||||
|
if normalized.startswith(home + "/"):
|
||||||
|
normalized = "~" + normalized[len(home):]
|
||||||
|
elif normalized == home:
|
||||||
|
normalized = "~"
|
||||||
|
|
||||||
|
# Return early only after normalization and home replacement
|
||||||
|
if len(normalized) <= max_len:
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
# Split into segments
|
||||||
|
parts = normalized.rstrip("/").split("/")
|
||||||
|
if len(parts) <= 1:
|
||||||
|
return normalized[:max_len - 1] + "\u2026"
|
||||||
|
|
||||||
|
# Always keep the basename
|
||||||
|
basename = parts[-1]
|
||||||
|
# Budget: max_len minus "…/" prefix (2 chars) minus "/" separator minus basename
|
||||||
|
budget = max_len - len(basename) - 3 # -3 for "…/" + final "/"
|
||||||
|
|
||||||
|
# Walk backwards from parent, collecting segments
|
||||||
|
kept: list[str] = []
|
||||||
|
for seg in reversed(parts[:-1]):
|
||||||
|
needed = len(seg) + 1 # segment + "/"
|
||||||
|
if not kept and needed <= budget:
|
||||||
|
kept.append(seg)
|
||||||
|
budget -= needed
|
||||||
|
elif kept:
|
||||||
|
needed_with_sep = len(seg) + 1
|
||||||
|
if needed_with_sep <= budget:
|
||||||
|
kept.append(seg)
|
||||||
|
budget -= needed_with_sep
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
kept.reverse()
|
||||||
|
if kept:
|
||||||
|
return "\u2026/" + "/".join(kept) + "/" + basename
|
||||||
|
return "\u2026/" + basename
|
||||||
|
|
||||||
|
|
||||||
|
def _abbreviate_url(url: str, max_len: int = 40) -> str:
|
||||||
|
"""Abbreviate a URL keeping domain and filename."""
|
||||||
|
if len(url) <= max_len:
|
||||||
|
return url
|
||||||
|
|
||||||
|
parsed = urlparse(url)
|
||||||
|
domain = parsed.netloc # e.g. "example.com"
|
||||||
|
path_part = parsed.path # e.g. "/api/v2/resource.json"
|
||||||
|
|
||||||
|
# Extract filename from path
|
||||||
|
segments = path_part.rstrip("/").split("/")
|
||||||
|
basename = segments[-1] if segments else ""
|
||||||
|
|
||||||
|
if not basename:
|
||||||
|
# No filename, truncate URL
|
||||||
|
return url[: max_len - 1] + "\u2026"
|
||||||
|
|
||||||
|
budget = max_len - len(domain) - len(basename) - 4 # "…/" + "/"
|
||||||
|
if budget < 0:
|
||||||
|
return domain + "/\u2026" + basename[: max_len - len(domain) - 4]
|
||||||
|
|
||||||
|
# Build abbreviated path
|
||||||
|
kept: list[str] = []
|
||||||
|
for seg in reversed(segments[:-1]):
|
||||||
|
if len(seg) + 1 <= budget:
|
||||||
|
kept.append(seg)
|
||||||
|
budget -= len(seg) + 1
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
kept.reverse()
|
||||||
|
if kept:
|
||||||
|
return domain + "/\u2026/" + "/".join(kept) + "/" + basename
|
||||||
|
return domain + "/\u2026/" + basename
|
||||||
79
tests/utils/test_abbreviate_path.py
Normal file
79
tests/utils/test_abbreviate_path.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
"""Tests for abbreviate_path utility."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from nanobot.utils.path import abbreviate_path
|
||||||
|
|
||||||
|
|
||||||
|
class TestAbbreviatePathShort:
|
||||||
|
def test_short_path_unchanged(self):
|
||||||
|
assert abbreviate_path("/home/user/file.py") == "/home/user/file.py"
|
||||||
|
|
||||||
|
def test_exact_max_len_unchanged(self):
|
||||||
|
path = "/a/b/c" # 7 chars
|
||||||
|
assert abbreviate_path("/a/b/c", max_len=7) == "/a/b/c"
|
||||||
|
|
||||||
|
def test_basename_only(self):
|
||||||
|
assert abbreviate_path("file.py") == "file.py"
|
||||||
|
|
||||||
|
def test_empty_string(self):
|
||||||
|
assert abbreviate_path("") == ""
|
||||||
|
|
||||||
|
|
||||||
|
class TestAbbreviatePathHome:
|
||||||
|
def test_home_replacement(self):
|
||||||
|
home = os.path.expanduser("~")
|
||||||
|
result = abbreviate_path(f"{home}/project/file.py")
|
||||||
|
assert result.startswith("~/")
|
||||||
|
assert result.endswith("file.py")
|
||||||
|
|
||||||
|
def test_home_preserves_short_path(self):
|
||||||
|
home = os.path.expanduser("~")
|
||||||
|
result = abbreviate_path(f"{home}/a.py")
|
||||||
|
assert result == "~/a.py"
|
||||||
|
|
||||||
|
|
||||||
|
class TestAbbreviatePathLong:
|
||||||
|
def test_long_path_keeps_basename(self):
|
||||||
|
path = "/a/b/c/d/e/f/g/h/very_long_filename.py"
|
||||||
|
result = abbreviate_path(path, max_len=30)
|
||||||
|
assert result.endswith("very_long_filename.py")
|
||||||
|
assert "\u2026" in result
|
||||||
|
|
||||||
|
def test_long_path_keeps_parent_dir(self):
|
||||||
|
path = "/a/b/c/d/e/f/g/h/src/loop.py"
|
||||||
|
result = abbreviate_path(path, max_len=30)
|
||||||
|
assert "loop.py" in result
|
||||||
|
assert "src" in result
|
||||||
|
|
||||||
|
def test_very_long_path_just_basename(self):
|
||||||
|
path = "/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/file.py"
|
||||||
|
result = abbreviate_path(path, max_len=20)
|
||||||
|
assert result.endswith("file.py")
|
||||||
|
assert len(result) <= 20
|
||||||
|
|
||||||
|
|
||||||
|
class TestAbbreviatePathWindows:
|
||||||
|
def test_windows_drive_path(self):
|
||||||
|
path = "D:\\Documents\\GitHub\\nanobot\\src\\utils\\helpers.py"
|
||||||
|
result = abbreviate_path(path, max_len=40)
|
||||||
|
assert result.endswith("helpers.py")
|
||||||
|
assert "nanobot" in result
|
||||||
|
|
||||||
|
def test_windows_home(self):
|
||||||
|
home = os.path.expanduser("~")
|
||||||
|
path = os.path.join(home, ".nanobot", "workspace", "log.txt")
|
||||||
|
result = abbreviate_path(path)
|
||||||
|
assert result.startswith("~/")
|
||||||
|
assert "log.txt" in result
|
||||||
|
|
||||||
|
|
||||||
|
class TestAbbreviatePathURLs:
|
||||||
|
def test_url_keeps_domain_and_filename(self):
|
||||||
|
url = "https://example.com/api/v2/long/path/resource.json"
|
||||||
|
result = abbreviate_path(url, max_len=40)
|
||||||
|
assert "resource.json" in result
|
||||||
|
assert "example.com" in result
|
||||||
|
|
||||||
|
def test_short_url_unchanged(self):
|
||||||
|
url = "https://example.com/api"
|
||||||
|
assert abbreviate_path(url) == url
|
||||||
Loading…
x
Reference in New Issue
Block a user