Skip to content

Commit 91d0d8d

Browse files
authored
feat(cli): Return more detailed vectorise stats in CLI and LSP server (#193)
* feat(cli): Return more detailed vectorise stats in CLI and LSP server * Auto generate docs * refactor(cli): Return dict instead of VectoriseStats in LSP server --------- Co-authored-by: Davidyz <Davidyz@users.noreply.github.com>
1 parent 145e318 commit 91d0d8d

7 files changed

Lines changed: 71 additions & 39 deletions

File tree

doc/VectorCode-cli.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -682,7 +682,10 @@ VECTORCODE VECTORISE
682682

683683
The output is in JSON format. It contains a dictionary with the following
684684
fields: - `"add"`number of added documents; - `"update"`number of updated
685-
documents; - `"removed"`number of removed documents;
685+
documents; - `"removed"`number of removed documents; - `"skipped"`number of
686+
skipped documents (because it’s empty or its hash matches the metadata saved
687+
in the database); - `"failed"`number of documents that failed to be vectorised.
688+
This is usually due to encoding issues.
686689

687690

688691
VECTORCODE LS

docs/cli.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,10 @@ The output is in JSON format. It contains a dictionary with the following fields
616616
- `"add"`: number of added documents;
617617
- `"update"`: number of updated documents;
618618
- `"removed"`: number of removed documents;
619+
- `"skipped"`: number of skipped documents (because it's empty or its hash
620+
matches the metadata saved in the database);
621+
- `"failed"`: number of documents that failed to be vectorised. This is usually
622+
due to encoding issues.
619623

620624
#### `vectorcode ls`
621625
A JSON array of collection information of the following format will be printed:

src/vectorcode/lsp_main.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import shtab
1111

1212
from vectorcode.subcommands.vectorise import (
13+
VectoriseStats,
1314
chunked_add,
1415
exclude_paths_by_spec,
1516
find_exclude_specs,
@@ -188,7 +189,7 @@ async def execute_command(ls: LanguageServer, args: list[str]):
188189
if os.path.isfile(spec):
189190
logger.info(f"Loading ignore specs from {spec}.")
190191
files = exclude_paths_by_spec((str(i) for i in files), spec)
191-
stats = {"add": 0, "update": 0, "removed": 0}
192+
stats = VectoriseStats()
192193
collection_lock = asyncio.Lock()
193194
stats_lock = asyncio.Lock()
194195
max_batch_size = await client.get_max_batch_size()
@@ -220,10 +221,10 @@ async def execute_command(ls: LanguageServer, args: list[str]):
220221
ls.progress.end(
221222
progress_token,
222223
types.WorkDoneProgressEnd(
223-
message=f"Vectorised {stats['add'] + stats['update']} files."
224+
message=f"Vectorised {stats.add + stats.update} files."
224225
),
225226
)
226-
return stats
227+
return stats.to_dict()
227228
case _ as c: # pragma: nocover
228229
error_message = f"Unsupported vectorcode subcommand: {str(c)}"
229230
logger.error(

src/vectorcode/subcommands/update.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from vectorcode.cli_utils import Config
1212
from vectorcode.common import get_client, get_collection, verify_ef
13-
from vectorcode.subcommands.vectorise import chunked_add, show_stats
13+
from vectorcode.subcommands.vectorise import VectoriseStats, chunked_add, show_stats
1414

1515
logger = logging.getLogger(name=__name__)
1616

@@ -43,7 +43,7 @@ async def update(configs: Config) -> int:
4343
else:
4444
orphanes.add(file)
4545

46-
stats = {"add": 0, "update": 0, "removed": len(orphanes)}
46+
stats = VectoriseStats(removed=len(orphanes))
4747
collection_lock = Lock()
4848
stats_lock = Lock()
4949
max_batch_size = await client.get_max_batch_size()

src/vectorcode/subcommands/vectorise.py

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import sys
77
import uuid
88
from asyncio import Lock
9+
from dataclasses import dataclass, fields
910
from typing import Iterable, Optional
1011

1112
import pathspec
@@ -27,6 +28,31 @@
2728
logger = logging.getLogger(name=__name__)
2829

2930

31+
@dataclass
32+
class VectoriseStats:
33+
add: int = 0
34+
update: int = 0
35+
removed: int = 0
36+
skipped: int = 0
37+
failed: int = 0
38+
39+
def to_json(self) -> str:
40+
return json.dumps(self.to_dict())
41+
42+
def to_dict(self) -> dict[str, int]:
43+
return {i.name: getattr(self, i.name) for i in fields(self)}
44+
45+
def to_table(self) -> str:
46+
_fields = fields(self)
47+
return tabulate.tabulate(
48+
[
49+
[i.name.capitalize() for i in _fields],
50+
[getattr(self, i.name) for i in _fields],
51+
],
52+
headers="firstrow",
53+
)
54+
55+
3056
def hash_str(string: str) -> str:
3157
"""Return the sha-256 hash of a string."""
3258
return hashlib.sha256(string.encode()).hexdigest()
@@ -53,7 +79,7 @@ async def chunked_add(
5379
file_path: str,
5480
collection: AsyncCollection,
5581
collection_lock: Lock,
56-
stats: dict[str, int],
82+
stats: VectoriseStats,
5783
stats_lock: Lock,
5884
configs: Config,
5985
max_batch_size: int,
@@ -74,6 +100,7 @@ async def chunked_add(
74100
logger.debug(
75101
f"Skipping {full_path_str} because it's unchanged since last vectorisation."
76102
)
103+
stats.skipped += 1
77104
return
78105

79106
if num_existing_chunks:
@@ -92,6 +119,7 @@ async def chunked_add(
92119
if len(chunks) == 0 or (len(chunks) == 1 and chunks[0] == ""):
93120
# empty file
94121
logger.debug(f"Skipping {full_path_str} because it's empty.")
122+
stats.skipped += 1
95123
return
96124
chunks.append(str(os.path.relpath(full_path_str, configs.project_root)))
97125
logger.debug(f"Chunked into {len(chunks)} pieces.")
@@ -116,29 +144,22 @@ async def chunked_add(
116144
)
117145
except (UnicodeDecodeError, UnicodeError): # pragma: nocover
118146
logger.warning(f"Failed to decode {full_path_str}.")
147+
stats.failed += 1
119148
return
120149

121150
if num_existing_chunks:
122151
async with stats_lock:
123-
stats["update"] += 1
152+
stats.update += 1
124153
else:
125154
async with stats_lock:
126-
stats["add"] += 1
155+
stats.add += 1
127156

128157

129-
def show_stats(configs: Config, stats):
158+
def show_stats(configs: Config, stats: VectoriseStats):
130159
if configs.pipe:
131-
print(json.dumps(stats))
160+
print(stats.to_json())
132161
else:
133-
print(
134-
tabulate.tabulate(
135-
[
136-
["Added", "Updated", "Removed"],
137-
[stats["add"], stats["update"], stats["removed"]],
138-
],
139-
headers="firstrow",
140-
)
141-
)
162+
print(stats.to_table())
142163

143164

144165
def exclude_paths_by_spec(
@@ -229,7 +250,7 @@ async def vectorise(configs: Config) -> int:
229250
else: # pragma: nocover
230251
logger.info("Ignoring exclude specs.")
231252

232-
stats = {"add": 0, "update": 0, "removed": 0}
253+
stats = VectoriseStats()
233254
collection_lock = Lock()
234255
stats_lock = Lock()
235256
max_batch_size = await client.get_max_batch_size()
@@ -270,7 +291,7 @@ async def vectorise(configs: Config) -> int:
270291
if isinstance(path, str) and not os.path.isfile(path):
271292
orphans.add(path)
272293
async with stats_lock:
273-
stats["removed"] = len(orphans)
294+
stats.removed = len(orphans)
274295
if len(orphans):
275296
logger.info(f"Removing {len(orphans)} orphaned files from database.")
276297
await collection.delete(where={"path": {"$in": list(orphans)}})

tests/subcommands/test_vectorise.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import asyncio
22
import hashlib
3-
import json
43
import os
54
import socket
65
import tempfile
@@ -15,6 +14,7 @@
1514
from vectorcode.chunking import Chunk
1615
from vectorcode.cli_utils import Config
1716
from vectorcode.subcommands.vectorise import (
17+
VectoriseStats,
1818
chunked_add,
1919
exclude_paths_by_spec,
2020
get_uuid,
@@ -74,7 +74,7 @@ async def test_chunked_add():
7474
file_path = "test_file.py"
7575
collection = AsyncMock()
7676
collection_lock = asyncio.Lock()
77-
stats = {"add": 0, "update": 0}
77+
stats = VectoriseStats()
7878
stats_lock = asyncio.Lock()
7979
configs = Config(chunk_size=100, overlap_ratio=0.2, project_root=".")
8080
max_batch_size = 50
@@ -97,8 +97,8 @@ async def test_chunked_add():
9797
semaphore,
9898
)
9999

100-
assert stats["add"] == 1
101-
assert stats["update"] == 0
100+
assert stats.add == 1
101+
assert stats.update == 0
102102
collection.add.assert_called()
103103
assert collection.add.call_count == 1
104104

@@ -110,7 +110,7 @@ async def test_chunked_add_with_existing():
110110
collection.get = AsyncMock()
111111
collection.get.return_value = {"ids": ["id1"], "metadatas": [{"sha256": "hash1"}]}
112112
collection_lock = asyncio.Lock()
113-
stats = {"add": 0, "update": 0}
113+
stats = VectoriseStats()
114114
stats_lock = asyncio.Lock()
115115
configs = Config(chunk_size=100, overlap_ratio=0.2, project_root=".")
116116
max_batch_size = 50
@@ -133,8 +133,8 @@ async def test_chunked_add_with_existing():
133133
semaphore,
134134
)
135135

136-
assert stats["add"] == 0
137-
assert stats["update"] == 0
136+
assert stats.add == 0
137+
assert stats.update == 0
138138
collection.add.assert_not_called()
139139

140140

@@ -145,7 +145,7 @@ async def test_chunked_add_update_existing():
145145
collection.get = AsyncMock()
146146
collection.get.return_value = {"ids": ["id1"], "metadatas": [{"sha256": "hash1"}]}
147147
collection_lock = asyncio.Lock()
148-
stats = {"add": 0, "update": 0}
148+
stats = VectoriseStats()
149149
stats_lock = asyncio.Lock()
150150
configs = Config(chunk_size=100, overlap_ratio=0.2, project_root=".")
151151
max_batch_size = 50
@@ -168,8 +168,8 @@ async def test_chunked_add_update_existing():
168168
semaphore,
169169
)
170170

171-
assert stats["add"] == 0
172-
assert stats["update"] == 1
171+
assert stats.add == 0
172+
assert stats.update == 1
173173
collection.add.assert_called()
174174

175175

@@ -178,7 +178,7 @@ async def test_chunked_add_empty_file():
178178
file_path = "test_file.py"
179179
collection = AsyncMock()
180180
collection_lock = asyncio.Lock()
181-
stats = {"add": 0, "update": 0}
181+
stats = VectoriseStats(**{"add": 0, "update": 0})
182182
stats_lock = asyncio.Lock()
183183
configs = Config(chunk_size=100, overlap_ratio=0.2, project_root=".")
184184
max_batch_size = 50
@@ -201,25 +201,25 @@ async def test_chunked_add_empty_file():
201201
semaphore,
202202
)
203203

204-
assert stats["add"] == 0
205-
assert stats["update"] == 0
204+
assert stats.add == 0
205+
assert stats.update == 0
206206
assert collection.add.call_count == 0
207207

208208

209209
@patch("tabulate.tabulate")
210210
def test_show_stats_pipe_false(mock_tabulate, capsys):
211211
configs = Config(pipe=False)
212-
stats = {"add": 1, "update": 2, "removed": 3}
212+
stats = VectoriseStats(**{"add": 1, "update": 2, "removed": 3})
213213
show_stats(configs, stats)
214214
mock_tabulate.assert_called_once()
215215

216216

217217
def test_show_stats_pipe_true(capsys):
218218
configs = Config(pipe=True)
219-
stats = {"add": 1, "update": 2, "removed": 3}
219+
stats = VectoriseStats(**{"add": 1, "update": 2, "removed": 3})
220220
show_stats(configs, stats)
221221
captured = capsys.readouterr()
222-
assert captured.out == json.dumps(stats) + "\n"
222+
assert captured.out.strip() == (stats.to_json())
223223

224224

225225
def test_exclude_paths_by_spec():

tests/test_lsp.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,10 @@ async def test_execute_command_vectorise(mock_language_server, mock_config: Conf
294294
result = await execute_command(
295295
mock_language_server, ["vectorise", "/test/project"]
296296
)
297-
assert isinstance(result, dict)
297+
assert isinstance(result, dict) and all(
298+
k in ("add", "update", "removed", "failed", "skipped")
299+
for k in result.keys()
300+
)
298301

299302
# Assertions
300303
mock_language_server.progress.create_async.assert_called_once()

0 commit comments

Comments
 (0)