Skip to content

Commit a1da042

Browse files
authored
Merge pull request #42 from justjoehere/windows
Changes to accomodate windows users.
2 parents 15640d3 + f5bc5ce commit a1da042

File tree

4 files changed

+68
-6
lines changed

4 files changed

+68
-6
lines changed

Diff for: Windows_Notes.md

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Getting your development environment set up properly
2+
```bash
3+
uv venv
4+
.venv\Scripts\activate
5+
uv pip install -e ".[dev]"
6+
```
7+
8+
# Fixing `AttributeError: module 'collections' has no attribute 'Callable'`
9+
- open `.venv\Lib\site-packages\pyreadline\py3k_compat.py`
10+
- change `return isinstance(x, collections.Callable)` to
11+
```
12+
from collections.abc import Callable
13+
return isinstance(x, Callable)
14+
```
15+

Diff for: src/fastmcp/cli/cli.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,17 @@ def _parse_file_path(file_spec: str) -> Tuple[Path, Optional[str]]:
6767
Returns:
6868
Tuple of (file_path, server_object)
6969
"""
70-
if ":" in file_spec:
70+
# First check if we have a Windows path (e.g., C:\...)
71+
has_windows_drive = len(file_spec) > 1 and file_spec[1] == ":"
72+
73+
# Split on the last colon, but only if it's not part of the Windows drive letter
74+
# and there's actually another colon in the string after the drive letter
75+
if ":" in (file_spec[2:] if has_windows_drive else file_spec):
7176
file_str, server_object = file_spec.rsplit(":", 1)
7277
else:
7378
file_str, server_object = file_spec, None
7479

80+
# Resolve the file path
7581
file_path = Path(file_str).expanduser().resolve()
7682
if not file_path.exists():
7783
logger.error(f"File not found: {file_path}")

Diff for: tests/resources/test_file_resources.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import os
2+
13
import pytest
24
from pathlib import Path
35
from tempfile import NamedTemporaryFile
@@ -28,12 +30,12 @@ class TestFileResource:
2830
def test_file_resource_creation(self, temp_file: Path):
2931
"""Test creating a FileResource."""
3032
resource = FileResource(
31-
uri=f"file://{temp_file}",
33+
uri=temp_file.as_uri(),
3234
name="test",
3335
description="test file",
3436
path=temp_file,
3537
)
36-
assert str(resource.uri) == f"file://{temp_file}"
38+
assert str(resource.uri) == temp_file.as_uri()
3739
assert resource.name == "test"
3840
assert resource.description == "test file"
3941
assert resource.mime_type == "text/plain" # default
@@ -94,12 +96,15 @@ async def test_missing_file_error(self, temp_file: Path):
9496
with pytest.raises(ValueError, match="Error reading file"):
9597
await resource.read()
9698

99+
@pytest.mark.skipif(
100+
os.name == "nt", reason="File permissions behave differently on Windows"
101+
)
97102
async def test_permission_error(self, temp_file: Path):
98103
"""Test reading a file without permissions."""
99104
temp_file.chmod(0o000) # Remove all permissions
100105
try:
101106
resource = FileResource(
102-
uri=f"file://{temp_file}",
107+
uri=temp_file.as_uri(),
103108
name="test",
104109
path=temp_file,
105110
)

Diff for: tests/test_cli.py

+38-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
"""Tests for the FastMCP CLI."""
22

33
import json
4-
from unittest.mock import patch
4+
from pathlib import Path
5+
from unittest.mock import patch, call
56

67
import pytest
78
from typer.testing import CliRunner
89

9-
from fastmcp.cli.cli import app, _parse_env_var
10+
from fastmcp.cli.cli import app, _parse_env_var, _parse_file_path
1011

1112

1213
@pytest.fixture
@@ -91,6 +92,41 @@ def test_install_with_env_vars(mock_config, server_file, args, expected_env):
9192
assert server["env"] == expected_env
9293

9394

95+
def test_parse_file_path_windows_drive():
96+
"""Test parsing a Windows file path with a drive letter."""
97+
file_spec = r"C:\path\to\file.txt"
98+
with (
99+
patch("pathlib.Path.exists", return_value=True),
100+
patch("pathlib.Path.is_file", return_value=True),
101+
):
102+
file_path, server_object = _parse_file_path(file_spec)
103+
assert file_path == Path(r"C:\path\to\file.txt").resolve()
104+
assert server_object is None
105+
106+
107+
def test_parse_file_path_with_object():
108+
"""Test parsing a file path with an object specification."""
109+
file_spec = "/path/to/file.txt:object"
110+
with patch("sys.exit") as mock_exit:
111+
_parse_file_path(file_spec)
112+
113+
# Check that sys.exit was called twice with code 1
114+
assert mock_exit.call_count == 2
115+
mock_exit.assert_has_calls([call(1), call(1)])
116+
117+
118+
def test_parse_file_path_windows_with_object():
119+
"""Test parsing a Windows file path with an object specification."""
120+
file_spec = r"C:\path\to\file.txt:object"
121+
with (
122+
patch("pathlib.Path.exists", return_value=True),
123+
patch("pathlib.Path.is_file", return_value=True),
124+
):
125+
file_path, server_object = _parse_file_path(file_spec)
126+
assert file_path == Path(r"C:\path\to\file.txt").resolve()
127+
assert server_object == "object"
128+
129+
94130
def test_install_with_env_file(mock_config, server_file, mock_env_file):
95131
"""Test installing with environment variables from a file."""
96132
runner = CliRunner()

0 commit comments

Comments
 (0)