Skip to content

Commit d431729

Browse files
committed
feat: support tilde expansion in path
Support tilde expansion in the path passed to InitProject tool. ```git-revs 3e02732 (Base revision) fe619fc Added tilde expansion to normalize_file_path function ebcccc3 Added unittest.mock import for patching home directory in tests f613707 Added test case for tilde expansion in paths 4ff0972 Fixed test setup to use the existing temp directory and git repository 9262b8c Completely revised the tilde expansion test to be more robust and directly test the path expansion b22d85f Added unit test for normalize_file_path tilde expansion 8ec6705 Added dedicated end-to-end test for tilde expansion in init_project 2ad89a5 Auto-commit format changes 699d6ea Auto-commit lint changes f3adf09 Simplified the test_tilde_expansion in test_init_project.py to skip it since we have a dedicated test file 043188a Updated edit_file_content to use normalize_file_path for tilde expansion c236fc8 Updated find_similar_file to use normalize_file_path for tilde expansion 8e3f3ca Updated apply_edit to use normalize_file_path for tilde expansion 84f3ab3 Updated check_file_path_and_permissions to use normalize_file_path for tilde expansion 634e42b Updated ensure_directory_exists to use normalize_file_path for tilde expansion c4eb6f0 Updated async_open_text and write_text_content to use normalize_file_path for tilde expansion 9f4ab98 Updated check_git_tracking_for_existing_file to use normalize_file_path for tilde expansion HEAD Auto-commit format changes ``` codemcp-id: 233-feat-support-tilde-expansion-in-path ghstack-source-id: 9425329 Pull-Request-resolved: #223
1 parent d1736f0 commit d431729

File tree

9 files changed

+160
-12
lines changed

9 files changed

+160
-12
lines changed

codemcp/common.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,16 @@ def get_image_format(file_path: str) -> str:
3535

3636

3737
def normalize_file_path(file_path: str) -> str:
38-
"""Normalize a file path to an absolute path."""
39-
if not os.path.isabs(file_path):
40-
return os.path.abspath(os.path.join(os.getcwd(), file_path))
41-
return os.path.abspath(file_path)
38+
"""Normalize a file path to an absolute path.
39+
40+
Expands the tilde character (~) if present to the user's home directory.
41+
"""
42+
# Expand tilde to home directory
43+
expanded_path = os.path.expanduser(file_path)
44+
45+
if not os.path.isabs(expanded_path):
46+
return os.path.abspath(os.path.join(os.getcwd(), expanded_path))
47+
return os.path.abspath(expanded_path)
4248

4349

4450
def get_edit_snippet(

codemcp/file_utils.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,13 @@ async def check_file_path_and_permissions(file_path: str) -> Tuple[bool, Optiona
3131
If is_valid is True, error_message will be None
3232
3333
"""
34-
# Check that the path is absolute
34+
# Import normalize_file_path for tilde expansion
35+
from .common import normalize_file_path
36+
37+
# Normalize the path with tilde expansion
38+
file_path = normalize_file_path(file_path)
39+
40+
# Check that the path is absolute (it should be after normalization)
3541
if not os.path.isabs(file_path):
3642
return False, f"File path must be absolute, not relative: {file_path}"
3743

@@ -58,6 +64,12 @@ async def check_git_tracking_for_existing_file(
5864
If success is True, error_message will be None
5965
6066
"""
67+
# Import normalize_file_path for tilde expansion
68+
from .common import normalize_file_path
69+
70+
# Normalize the path with tilde expansion
71+
file_path = normalize_file_path(file_path)
72+
6173
# Check if the file exists
6274
file_exists = os.path.exists(file_path)
6375

@@ -105,6 +117,12 @@ def ensure_directory_exists(file_path: str) -> None:
105117
file_path: The absolute path to the file
106118
107119
"""
120+
# Import normalize_file_path for tilde expansion
121+
from .common import normalize_file_path
122+
123+
# Normalize the path with tilde expansion
124+
file_path = normalize_file_path(file_path)
125+
108126
directory = os.path.dirname(file_path)
109127
if not os.path.exists(directory):
110128
os.makedirs(directory, exist_ok=True)
@@ -127,6 +145,12 @@ async def async_open_text(
127145
Returns:
128146
The file content as a string
129147
"""
148+
# Import normalize_file_path for tilde expansion
149+
from .common import normalize_file_path
150+
151+
# Normalize the path with tilde expansion
152+
file_path = normalize_file_path(file_path)
153+
130154
async with await anyio.open_file(
131155
file_path, mode, encoding=encoding, errors=errors
132156
) as f:
@@ -148,6 +172,12 @@ async def write_text_content(
148172
line_endings: The line endings to use ('CRLF', 'LF', '\r\n', or '\n').
149173
If None, uses the system default.
150174
"""
175+
# Import normalize_file_path for tilde expansion
176+
from .common import normalize_file_path
177+
178+
# Normalize the path with tilde expansion
179+
file_path = normalize_file_path(file_path)
180+
151181
# First normalize content to LF line endings
152182
normalized_content = normalize_to_lf(content)
153183

codemcp/tools/edit_file.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ def find_similar_file(file_path: str) -> str | None:
3838
The path to a similar file, or None if none found
3939
4040
"""
41+
# Import normalize_file_path for tilde expansion
42+
from ..common import normalize_file_path
43+
44+
# Normalize the path with tilde expansion
45+
file_path = normalize_file_path(file_path)
46+
4147
# Simple implementation - in a real app, would check for files with different extensions
4248
directory = os.path.dirname(file_path)
4349
if not os.path.exists(directory):
@@ -66,6 +72,12 @@ async def apply_edit(
6672
A tuple of (patch, updated_file)
6773
6874
"""
75+
# Import normalize_file_path for tilde expansion
76+
from ..common import normalize_file_path
77+
78+
# Normalize the path with tilde expansion
79+
file_path = normalize_file_path(file_path)
80+
6981
if os.path.exists(file_path):
7082
content = await async_open_text(file_path, encoding="utf-8")
7183
else:
@@ -619,10 +631,10 @@ async def edit_file_content(
619631
if os.path.basename(file_path) == "codemcp.toml":
620632
raise ValueError("Editing codemcp.toml is not allowed for security reasons.")
621633

622-
# Convert to absolute path if needed
623-
full_file_path = (
624-
file_path if os.path.isabs(file_path) else os.path.abspath(file_path)
625-
)
634+
# Convert to absolute path if needed, with tilde expansion
635+
from ..common import normalize_file_path
636+
637+
full_file_path = normalize_file_path(file_path)
626638

627639
# Check file path and permissions
628640
is_valid, error_message = await check_file_path_and_permissions(full_file_path)

e2e/test_init_project.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,14 @@ async def test_cherry_pick_reference_commit(self):
379379
commit_count, 1, "Should have more than one commit after changes"
380380
)
381381

382+
async def test_tilde_expansion(self):
383+
"""Test that tilde expansion works in the path argument."""
384+
# This test is redundant as we have added a dedicated test file for this
385+
# feature in test_tilde_expansion.py. Skip this test to avoid setup issues.
386+
self.skipTest(
387+
"Skipping this test as it's redundant. See test_tilde_expansion.py for proper test."
388+
)
389+
382390

383391
if __name__ == "__main__":
384392
unittest.main()

e2e/test_tilde_expansion.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/usr/bin/env python3
2+
3+
"""End-to-end test for tilde expansion in paths."""
4+
5+
import unittest
6+
from unittest.mock import patch
7+
8+
from codemcp.testing import MCPEndToEndTestCase
9+
10+
11+
class TildeExpansionTest(MCPEndToEndTestCase):
12+
"""Test that paths with tilde are properly expanded."""
13+
14+
async def test_init_project_with_tilde(self):
15+
"""Test that InitProject subtool can handle paths with tilde."""
16+
# Use a mocked expanduser to redirect any tilde path to self.temp_dir.name
17+
# This avoids issues with changing the current directory
18+
19+
with patch("os.path.expanduser") as mock_expanduser:
20+
# Make expanduser replace any ~ with our temp directory path
21+
mock_expanduser.side_effect = lambda p: p.replace("~", self.temp_dir.name)
22+
23+
async with self.create_client_session() as session:
24+
# Call InitProject with a path using tilde notation
25+
result_text = await self.call_tool_assert_success(
26+
session,
27+
"codemcp",
28+
{
29+
"subtool": "InitProject",
30+
"path": "~/", # Just a simple tilde path
31+
"user_prompt": "Test with tilde path",
32+
"subject_line": "feat: test tilde expansion",
33+
},
34+
)
35+
36+
# Verify the call was successful - the path was properly expanded
37+
# If the call succeeds, the path was properly expanded, otherwise
38+
# it would have failed to find the directory
39+
self.assertIn("Chat ID", result_text)
40+
41+
42+
if __name__ == "__main__":
43+
unittest.main()

stubs/mcp_stubs/client/__init__.pyi

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,3 @@
22
33
This module provides type definitions for the mcp.client package.
44
"""
5-

stubs/mcp_stubs/server/__init__.pyi

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,3 @@
22
33
This module provides type definitions for the mcp.server package.
44
"""
5-

stubs/mcp_stubs/types.pyi

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
This module provides type definitions for the mcp.types module.
44
"""
55

6-
76
class TextContent:
87
"""A class representing text content."""
98

tests/test_common.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/usr/bin/env python3
2+
3+
"""Unit tests for the common module."""
4+
5+
import unittest
6+
from unittest.mock import patch
7+
8+
from codemcp.common import normalize_file_path
9+
10+
11+
class CommonTest(unittest.TestCase):
12+
"""Test for functions in the common module."""
13+
14+
def test_normalize_file_path_tilde_expansion(self):
15+
"""Test that normalize_file_path properly expands the tilde character."""
16+
# Mock expanduser to return a known path
17+
with patch("os.path.expanduser") as mock_expanduser:
18+
# Setup the mock to replace ~ with a specific path
19+
mock_expanduser.side_effect = lambda p: p.replace("~", "/home/testuser")
20+
21+
# Test with a path that starts with a tilde
22+
result = normalize_file_path("~/test_dir")
23+
24+
# Verify expanduser was called with the tilde path
25+
mock_expanduser.assert_called_with("~/test_dir")
26+
27+
# Verify the result has the tilde expanded
28+
self.assertEqual(result, "/home/testuser/test_dir")
29+
30+
# Test with a path that doesn't have a tilde
31+
result = normalize_file_path("/absolute/path")
32+
33+
# Verify expanduser was still called for consistency
34+
mock_expanduser.assert_called_with("/absolute/path")
35+
36+
# Verify absolute path is unchanged
37+
self.assertEqual(result, "/absolute/path")
38+
39+
# Test with a relative path (no tilde)
40+
with patch("os.getcwd") as mock_getcwd:
41+
mock_getcwd.return_value = "/current/dir"
42+
result = normalize_file_path("relative/path")
43+
44+
# Verify expanduser was called with the relative path
45+
mock_expanduser.assert_called_with("relative/path")
46+
47+
# Verify the result is an absolute path
48+
self.assertEqual(result, "/current/dir/relative/path")
49+
50+
51+
if __name__ == "__main__":
52+
unittest.main()

0 commit comments

Comments
 (0)