Skip to content

Commit e1b2774

Browse files
committed
feat(cli): accept json5 config.
1 parent 141bf87 commit e1b2774

File tree

7 files changed

+65
-26
lines changed

7 files changed

+65
-26
lines changed

docs/cli.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,14 @@ default global configuration will be used. If `.git/` does not exist, VectorCode
202202
falls back to using the current working directory as the _project root_.
203203

204204
### Configuring VectorCode
205+
Since 0.6.4, VectorCode adapted a [json5 parser](https://github.com/dpranke/pyjson5)
206+
for loading configuration. VectorCode will now look for `config.json5` in
207+
configuration directories, and if it doesn't find one, it'll look for
208+
`config.json` too. Regardless of the filename extension, the json5 syntax will
209+
be accepted. This allows you to leave trailing comma in the config file, as well
210+
as writing comments (`//`). This can be very useful if you're experimenting with
211+
the configs.
212+
205213
The JSON configuration file may hold the following values:
206214
- `embedding_function`: string, one of the embedding functions supported by [Chromadb](https://www.trychroma.com/)
207215
(find more [here](https://docs.trychroma.com/docs/embeddings/embedding-functions) and

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ dependencies = [
1919
"wheel<0.46.0",
2020
"colorlog",
2121
"charset-normalizer>=3.4.1",
22+
"json5",
2223
]
2324
requires-python = ">=3.11,<3.14"
2425
readme = "README.md"

src/vectorcode/cli_utils.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import argparse
22
import atexit
33
import glob
4-
import json
54
import logging
65
import os
76
import sys
@@ -11,6 +10,7 @@
1110
from pathlib import Path
1211
from typing import Any, Optional, Sequence, Union
1312

13+
import json5
1414
import shtab
1515

1616
from vectorcode import __version__
@@ -415,9 +415,17 @@ async def load_config_file(path: Optional[PathLike] = None):
415415
if os.path.isfile(path):
416416
logger.debug(f"Loading config from {path}")
417417
with open(path) as fin:
418-
config = json.load(fin)
419-
expand_envs_in_dict(config)
420-
return await Config.import_from(config)
418+
content = fin.read()
419+
if content:
420+
config = json5.loads(content)
421+
if isinstance(config, dict):
422+
expand_envs_in_dict(config)
423+
return await Config.import_from(config)
424+
else:
425+
logger.error("Invalid configuration format!")
426+
raise ValueError("Invalid configuration format!")
427+
else:
428+
logger.debug("Skipping empty json file.")
421429
else:
422430
logger.warning("Loading default config.")
423431
return Config()
@@ -464,10 +472,14 @@ async def get_project_config(project_root: PathLike) -> Config:
464472
"""
465473
if not os.path.isabs(project_root):
466474
project_root = os.path.abspath(project_root)
467-
local_config_path = os.path.join(project_root, ".vectorcode", "config.json")
468-
if os.path.isfile(os.path.join(project_root, ".vectorcode", "config.json")):
469-
config = await load_config_file(local_config_path)
470-
else:
475+
exts = ("json5", "json")
476+
config = None
477+
for ext in exts:
478+
local_config_path = os.path.join(project_root, ".vectorcode", f"config.{ext}")
479+
if os.path.isfile(local_config_path):
480+
config = await load_config_file(local_config_path)
481+
break
482+
if config is None:
471483
config = await load_config_file()
472484
config.project_root = project_root
473485
return config

src/vectorcode/lsp_main.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
cleanup_path,
2323
config_logging,
2424
find_project_root,
25-
load_config_file,
25+
get_project_config,
2626
parse_cli_args,
2727
)
2828
from vectorcode.common import get_client, get_collection, try_server
@@ -37,10 +37,7 @@
3737
async def make_caches(project_root: str):
3838
assert os.path.isabs(project_root)
3939
if cached_project_configs.get(project_root) is None:
40-
config_file = os.path.join(project_root, ".vectorcode", "config.json")
41-
if not os.path.isfile(config_file):
42-
config_file = None
43-
cached_project_configs[project_root] = await load_config_file(config_file)
40+
cached_project_configs[project_root] = await get_project_config(project_root)
4441
config = cached_project_configs[project_root]
4542
config.project_root = project_root
4643
host, port = config.host, config.port

src/vectorcode/mcp_main.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,7 @@ async def mcp_server():
151151
logger.info("Found project config: %s", local_config_dir)
152152
project_root = str(Path(local_config_dir).parent.resolve())
153153

154-
default_config = await load_config_file(
155-
os.path.join(project_root, ".vectorcode", "config.json")
156-
)
154+
default_config = await get_project_config(project_root)
157155
default_config.project_root = project_root
158156
default_client = await get_client(default_config)
159157
try:

tests/test_cli_utils.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import json
21
import os
32
import tempfile
43
from typing import Any, Dict
@@ -201,7 +200,18 @@ async def test_load_config_file_invalid_json():
201200
with open(config_path, "w") as f:
202201
f.write("invalid json")
203202

204-
with pytest.raises(json.JSONDecodeError):
203+
with pytest.raises(ValueError):
204+
await load_config_file(config_path)
205+
206+
207+
@pytest.mark.asyncio
208+
async def test_load_config_file_invalid_config():
209+
with tempfile.TemporaryDirectory() as temp_dir:
210+
config_path = os.path.join(temp_dir, "config.json")
211+
with open(config_path, "w") as f:
212+
f.write('"hello world"')
213+
214+
with pytest.raises(ValueError):
205215
await load_config_file(config_path)
206216

207217

@@ -225,8 +235,7 @@ async def test_load_config_file_empty_file():
225235
with open(config_path, "w") as f:
226236
f.write("")
227237

228-
with pytest.raises(json.JSONDecodeError):
229-
await load_config_file(config_path)
238+
assert await load_config_file(config_path) == Config()
230239

231240

232241
@pytest.mark.asyncio
@@ -399,8 +408,22 @@ async def test_get_project_config_local_config(tmp_path):
399408
assert config.host == "test_host"
400409
assert config.port == 9999
401410

402-
# Clean up the temporary directory
403-
# shutil.rmtree(tmp_path) # Use tmp_path fixture, no need to remove manually
411+
412+
@pytest.mark.asyncio
413+
async def test_get_project_config_local_config_json5(tmp_path):
414+
# Create a temporary directory and a .vectorcode subdirectory
415+
project_root = tmp_path / "project"
416+
vectorcode_dir = project_root / ".vectorcode"
417+
vectorcode_dir.mkdir(parents=True)
418+
419+
# Create a config.json file inside .vectorcode with some custom settings
420+
config_file = vectorcode_dir / "config.json5"
421+
config_file.write_text('{"host": "test_host", "port": 9999}')
422+
423+
# Call get_project_config and check if it returns the custom settings
424+
config = await get_project_config(project_root)
425+
assert config.host == "test_host"
426+
assert config.port == 9999
404427

405428

406429
def test_find_project_root_file_input(tmp_path):

tests/test_lsp.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,16 @@ async def test_make_caches(tmp_path):
4848

4949
with (
5050
patch(
51-
"vectorcode.lsp_main.load_config_file", new_callable=AsyncMock
52-
) as mock_load_config_file,
51+
"vectorcode.lsp_main.get_project_config", new_callable=AsyncMock
52+
) as mock_get_project_config,
5353
patch(
5454
"vectorcode.lsp_main.try_server", new_callable=AsyncMock
5555
) as mock_try_server,
5656
):
5757
mock_try_server.return_value = True
5858
await make_caches(project_root)
5959

60-
mock_load_config_file.assert_called_once()
60+
mock_get_project_config.assert_called_once_with(project_root)
6161
assert project_root in cached_project_configs
6262

6363

@@ -69,7 +69,7 @@ async def test_make_caches_server_unavailable(tmp_path):
6969
config_file.write_text('{"host": "test_host", "port": 9999}')
7070

7171
with (
72-
patch("vectorcode.lsp_main.load_config_file", new_callable=AsyncMock),
72+
patch("vectorcode.lsp_main.get_project_config", new_callable=AsyncMock),
7373
patch(
7474
"vectorcode.lsp_main.try_server", new_callable=AsyncMock
7575
) as mock_try_server,

0 commit comments

Comments
 (0)