-
Notifications
You must be signed in to change notification settings - Fork 0
Support windows #26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support windows #26
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,39 @@ | ||||||||||||||||||||||||||||||||||
| import os | ||||||||||||||||||||||||||||||||||
| import subprocess | ||||||||||||||||||||||||||||||||||
| import sys | ||||||||||||||||||||||||||||||||||
| from pathlib import Path | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| def is_windows() -> bool: | ||||||||||||||||||||||||||||||||||
| return sys.platform == "win32" or os.name == "nt" | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| def is_link_or_junction(path: Path) -> bool: | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
| def is_link_or_junction(path: Path) -> bool: | |
| def is_link_or_junction(path: Path) -> bool: | |
| """ | |
| Check if a path is a symlink or Windows junction. | |
| Args: | |
| path (Path): The path to check. | |
| Returns: | |
| bool: True if the path is a symlink or junction, False otherwise. | |
| """ |
Copilot
AI
Nov 28, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing docstring for path_exists() function. Consider adding documentation:
def path_exists(path: Path) -> bool:
\"\"\"Check if a path exists, including broken symlinks or junctions.
Args:
path: The path to check.
Returns:
bool: True if the path exists or is a symlink/junction (even if broken), False otherwise.
\"\"\"| def path_exists(path: Path) -> bool: | |
| def path_exists(path: Path) -> bool: | |
| """ | |
| Check if a path exists, including broken symlinks or junctions. | |
| Args: | |
| path (Path): The path to check. | |
| Returns: | |
| bool: True if the path exists or is a symlink/junction (even if broken), False otherwise. | |
| """ |
Copilot
AI
Nov 28, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Grammar error: "as symlink_to" should be "as symlink_to does" or "unlike symlink_to" for clarity. The sentence is missing a verb.
| Does not require admin privileges as symlink_to. | |
| Does not require admin privileges, unlike symlink_to. |
Copilot
AI
Nov 28, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Security risk: Using shell=True with f-string formatting can lead to command injection vulnerabilities if symlink or target paths contain shell metacharacters. Use subprocess.run() with a list of arguments instead:
subprocess.check_call(['cmd', '/c', 'mklink', '/J', str(symlink.absolute()), str(target.absolute())])| cmd = f'mklink /J "{symlink.absolute()}" "{target.absolute()}"' | |
| subprocess.check_call(cmd, shell=True) | |
| cmd = ['cmd', '/c', 'mklink', '/J', str(symlink.absolute()), str(target.absolute())] | |
| subprocess.check_call(cmd) |
Copilot
AI
Nov 28, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing docstring for create_symlink() function. Consider adding comprehensive documentation:
def create_symlink(symlink: Path, target: Path) -> None:
\"\"\"Create a symlink to the target directory, with Windows junction fallback.
Creates the target directory if it doesn't exist. On Windows, falls back to
creating a junction if symlink creation fails (e.g., due to permissions).
Args:
symlink: The path where the symlink should be created.
target: The target directory the symlink should point to.
Raises:
OSError: If symlink creation fails on non-Windows platforms.
\"\"\"| def create_symlink(symlink: Path, target: Path) -> None: | |
| def create_symlink(symlink: Path, target: Path) -> None: | |
| """ | |
| Create a symlink to the target directory, with Windows junction fallback. | |
| Ensures the target directory exists. On Windows, if symlink creation fails | |
| (e.g., due to insufficient permissions), falls back to creating a junction. | |
| Args: | |
| symlink (Path): The path where the symlink should be created. | |
| target (Path): The target directory the symlink should point to. | |
| Raises: | |
| OSError: If symlink creation fails on non-Windows platforms. | |
| ValueError: If the target is not a directory when creating a junction. | |
| """ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,6 +44,7 @@ def test_link_dry_run(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: | |
| # Verify that no symlink was actually created (dry-run should not create anything) | ||
| assert not expected_symlink.exists() | ||
| assert not expected_symlink.is_symlink() | ||
| assert not expected_symlink.is_junction() | ||
|
|
||
|
|
||
| def test_link_creation(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: | ||
|
|
@@ -67,7 +68,7 @@ def test_link_creation(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: | |
| assert expected_output in result.stdout | ||
|
|
||
| # Verify symlink exists | ||
| assert expected_symlink.is_symlink() | ||
| assert expected_symlink.is_symlink() or expected_symlink.is_junction() | ||
|
|
||
| # Verify the symlink actually points to the expected cache directory | ||
| assert expected_symlink.resolve() == expected_venv.resolve() | ||
|
|
@@ -99,8 +100,7 @@ def test_ls(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: | |
| p2 = Project(project_dir=project2_dir) | ||
| # Remove the symlink to make it unlinked | ||
| symlink2 = project2_dir / ".venv" | ||
| if symlink2.exists() or symlink2.is_symlink(): | ||
| symlink2.unlink() | ||
| symlink2.unlink(missing_ok=True) | ||
|
||
|
|
||
| # Use --cache-root to specify the cache directory explicitly | ||
| cache_dir = fake_home / "uvlink" / "cache" | ||
|
|
@@ -113,8 +113,7 @@ def test_ls(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: | |
| assert "Is Linked" in result.stdout | ||
|
|
||
| # Verify cache location message | ||
| assert "Cache Location:" in result.stdout | ||
| assert "uvlink/cache" in result.stdout | ||
| assert f"Cache Location: {cache_dir}" in result.stdout | ||
|
|
||
| # Verify both projects appear and have correct linked status | ||
| output_lines = result.stdout.split("\n") | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| from pathlib import Path | ||
|
|
||
| import pytest | ||
|
|
||
| from uvlink.path_utils import create_symlink, create_windows_junction, is_windows | ||
|
|
||
|
|
||
| def test_create_symlink(tmp_path: Path): | ||
| target_dir = tmp_path / "target" | ||
| symlink_dir = tmp_path / "symlink" | ||
| create_symlink(symlink=symlink_dir, target=target_dir) | ||
|
|
||
| assert symlink_dir.exists() | ||
| assert symlink_dir.is_symlink() or (is_windows() and symlink_dir.is_junction()) | ||
| assert symlink_dir.resolve() == target_dir.resolve() | ||
|
Comment on lines
+8
to
+15
|
||
|
|
||
|
|
||
| @pytest.mark.skipif( | ||
| not is_windows(), reason="Windows junctions are only applicable on Windows." | ||
| ) | ||
| def test_create_windows_junction(tmp_path: Path): | ||
| target_dir = tmp_path | ||
| symlink_dir = tmp_path / "symlink" | ||
| create_windows_junction(symlink=symlink_dir, target=target_dir) | ||
|
|
||
| assert symlink_dir.exists() | ||
| assert symlink_dir.is_junction() | ||
| assert symlink_dir.resolve() == target_dir.resolve() | ||
|
|
||
|
|
||
| def test_create_windows_junction_invalid_target(tmp_path: Path): | ||
| with pytest.raises(ValueError): | ||
| create_windows_junction( | ||
| symlink=tmp_path / "any", target=tmp_path / "nonexistent" | ||
| ) | ||
|
Comment on lines
+31
to
+35
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,24 +4,39 @@ | |
|
|
||
| import pytest | ||
|
|
||
| from uvlink.path_utils import create_symlink, is_windows | ||
| from uvlink.project import Project | ||
|
|
||
|
|
||
| class TestProject: | ||
| def test_hash_path(self): | ||
| assert Project.hash_path("/") == "8a5edab28263" | ||
| expected_hash = "d0023e7cb6a9" if is_windows() else "8a5edab28263" | ||
| assert Project.hash_path("/") == expected_hash | ||
|
|
||
| def test_project_init_path_resolution_with_tilde(self) -> None: | ||
| """Test with tilde expansion and parent directory (e.g., "~/../xxx").""" | ||
| user_home_path = Path.home() | ||
| test_dir = user_home_path / "test_project" | ||
|
|
||
| # Test ~/test_project resolves correctly | ||
| p2 = Project(project_dir="~/test_project") | ||
| assert p2.project_dir == test_dir.resolve() | ||
| assert p2.project_dir.is_absolute() | ||
|
Comment on lines
+16
to
+24
|
||
|
|
||
| # Test ~/.. resolves to parent of HOME | ||
| p3 = Project(project_dir="~/..") | ||
| assert p3.project_dir == user_home_path.parent.resolve() | ||
| assert p3.project_dir.is_absolute() | ||
|
|
||
| def test_project_init(self, tmp_path: Path) -> None: | ||
| p = Project(project_dir=tmp_path) | ||
| # Project.resolve() normalizes the path, so compare with resolved tmp_path | ||
| assert p.project_dir == tmp_path.resolve() | ||
| assert p.project_name == tmp_path.name | ||
| assert p.venv_type == ".venv" | ||
| assert "uvlink/cache" in str(p.project_cache_dir) | ||
| assert "uvlink/cache" in p.project_cache_dir.as_posix() | ||
|
|
||
| def test_project_init_path_resolution( | ||
| self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch | ||
| ) -> None: | ||
| def test_project_init_path_resolution(self, tmp_path: Path) -> None: | ||
| """Test that Project resolves relative paths and parent directory references.""" | ||
| # Create a subdirectory to test relative paths with parent directory references | ||
| subdir = tmp_path / "subdir" / "nested" | ||
|
|
@@ -36,28 +51,12 @@ def test_project_init_path_resolution( | |
| assert p1.project_dir.is_absolute() | ||
| assert p1.project_name == tmp_path.name | ||
|
|
||
| # Test with tilde expansion and parent directory (e.g., "~/../xxx") | ||
| # Mock HOME to be tmp_path for reliable testing | ||
| monkeypatch.setenv("HOME", str(tmp_path)) | ||
| test_dir = tmp_path / "test_project" | ||
| test_dir.mkdir(exist_ok=True) | ||
|
|
||
| # Test ~/test_project resolves correctly | ||
| p2 = Project(project_dir="~/test_project") | ||
| assert p2.project_dir == test_dir.resolve() | ||
| assert p2.project_dir.is_absolute() | ||
|
|
||
| # Test ~/.. resolves to parent of HOME | ||
| p3 = Project(project_dir="~/..") | ||
| assert p3.project_dir == tmp_path.parent.resolve() | ||
| assert p3.project_dir.is_absolute() | ||
|
|
||
| # Test symlink resolution | ||
| # Create a target directory and a symlink pointing to it | ||
| target_dir = tmp_path / "target" | ||
| target_dir.mkdir() | ||
| symlink_path = tmp_path / "symlink" | ||
| symlink_path.symlink_to(target_dir) | ||
| create_symlink(symlink_path, target_dir) | ||
|
|
||
| # Project should resolve the symlink to the actual target | ||
| p4 = Project(project_dir=symlink_path) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing docstring for
is_windows()function. Consider adding documentation explaining what this function checks and when it returns True: