Jeremiah Lowin commited on
Commit
77b8c9c
·
unverified ·
2 Parent(s): 80e65e79c58e7f

Merge branch 'main' into windows2

Browse files
.github/workflows/run-tests.yml CHANGED
@@ -12,12 +12,14 @@ on:
12
  - "tests/**"
13
  - "uv.lock"
14
  - "pyproject.toml"
 
15
  pull_request:
16
  paths:
17
  - "src/**"
18
  - "tests/**"
19
  - "uv.lock"
20
  - "pyproject.toml"
 
21
 
22
  workflow_dispatch:
23
 
@@ -26,8 +28,13 @@ permissions:
26
 
27
  jobs:
28
  run_tests:
29
- name: Run tests
30
- runs-on: ubuntu-latest
 
 
 
 
 
31
 
32
  steps:
33
  - uses: actions/checkout@v4
@@ -35,11 +42,11 @@ jobs:
35
  - name: Install uv
36
  uses: astral-sh/setup-uv@v4
37
 
38
- - name: Set up Python
39
- run: uv python install 3.11
40
 
41
  - name: Install FastMCP
42
- run: uv sync --extra dev
43
 
44
  - name: Run tests
45
  run: uv run pytest -vv
 
12
  - "tests/**"
13
  - "uv.lock"
14
  - "pyproject.toml"
15
+ - ".github/workflows/**"
16
  pull_request:
17
  paths:
18
  - "src/**"
19
  - "tests/**"
20
  - "uv.lock"
21
  - "pyproject.toml"
22
+ - ".github/workflows/**"
23
 
24
  workflow_dispatch:
25
 
 
28
 
29
  jobs:
30
  run_tests:
31
+ name: "Run tests: Python ${{ matrix.python-version }} on ${{ matrix.os }}"
32
+ runs-on: ${{ matrix.os }}
33
+ strategy:
34
+ matrix:
35
+ os: [ubuntu-latest, windows-latest, macos-latest]
36
+ python-version: ["3.10"]
37
+ fail-fast: false
38
 
39
  steps:
40
  - uses: actions/checkout@v4
 
42
  - name: Install uv
43
  uses: astral-sh/setup-uv@v4
44
 
45
+ - name: Set up Python ${{ matrix.python-version }}
46
+ run: uv python install ${{ matrix.python-version }}
47
 
48
  - name: Install FastMCP
49
+ run: uv sync --extra tests
50
 
51
  - name: Run tests
52
  run: uv run pytest -vv
README.md CHANGED
@@ -464,23 +464,20 @@ FastMCP requires Python 3.10+ and [uv](https://docs.astral.sh/uv/).
464
 
465
  ### Installation
466
 
467
- Create a fork of this repository, then clone it:
468
 
469
  ```bash
470
- git clone https://github.com/YouFancyUserYou/fastmcp.git
471
  cd fastmcp
 
472
  ```
473
 
474
- Next, create a virtual environment and install FastMCP:
475
 
476
  ```bash
477
- uv venv
478
- source .venv/bin/activate
479
- uv sync --frozen --all-extras --dev
480
  ```
481
 
482
-
483
-
484
  ### Testing
485
 
486
  Please make sure to test any new functionality. Your tests should be simple and atomic and anticipate change rather than cement complex patterns.
 
464
 
465
  ### Installation
466
 
467
+ For development, we recommend installing FastMCP with development dependencies, which includes various utilities the maintainers find useful.
468
 
469
  ```bash
470
+ git clone https://github.com/jlowin/fastmcp.git
471
  cd fastmcp
472
+ uv sync --frozen --extra dev
473
  ```
474
 
475
+ For running tests only (e.g., in CI), you only need the testing dependencies:
476
 
477
  ```bash
478
+ uv sync --frozen --extra tests
 
 
479
  ```
480
 
 
 
481
  ### Testing
482
 
483
  Please make sure to test any new functionality. Your tests should be simple and atomic and anticipate change rather than cement complex patterns.
pyproject.toml CHANGED
@@ -23,16 +23,14 @@ requires = ["hatchling>=1.21.0", "hatch-vcs>=0.4.0"]
23
  build-backend = "hatchling.build"
24
 
25
  [project.optional-dependencies]
26
- dev = [
27
- "copychat>=0.5.2",
28
- "ipython>=8.12.3",
29
- "pdbpp>=0.10.3",
30
  "pre-commit",
31
- "pytest-xdist>=3.6.1",
32
  "pytest>=8.3.3",
33
  "pytest-asyncio>=0.23.5",
 
34
  "ruff",
35
  ]
 
36
 
37
  [tool.pytest.ini_options]
38
  asyncio_mode = "auto"
 
23
  build-backend = "hatchling.build"
24
 
25
  [project.optional-dependencies]
26
+ tests = [
 
 
 
27
  "pre-commit",
 
28
  "pytest>=8.3.3",
29
  "pytest-asyncio>=0.23.5",
30
+ "pytest-xdist>=3.6.1",
31
  "ruff",
32
  ]
33
+ dev = ["fastmcp[tests]", "copychat>=0.5.2", "ipython>=8.12.3", "pdbpp>=0.10.3"]
34
 
35
  [tool.pytest.ini_options]
36
  asyncio_mode = "auto"
src/fastmcp/resources/base.py CHANGED
@@ -1,34 +1,17 @@
1
  """Base classes and interfaces for FastMCP resources."""
2
 
3
  import abc
4
- from typing import Annotated, Union
5
 
6
  from pydantic import (
7
  AnyUrl,
8
  BaseModel,
9
- BeforeValidator,
10
  ConfigDict,
11
  Field,
12
  FileUrl,
13
  ValidationInfo,
14
  field_validator,
15
  )
16
- from pydantic.networks import _BaseUrl # TODO: remove this once pydantic is updated
17
-
18
-
19
- def maybe_cast_str_to_any_url(x) -> AnyUrl:
20
- if isinstance(x, FileUrl):
21
- return x
22
- elif isinstance(x, AnyUrl):
23
- return x
24
- elif isinstance(x, str):
25
- if x.startswith("file://"):
26
- return FileUrl(x)
27
- return AnyUrl(x)
28
- raise ValueError(f"Expected str or AnyUrl, got {type(x)}")
29
-
30
-
31
- LaxAnyUrl = Annotated[_BaseUrl | str, BeforeValidator(maybe_cast_str_to_any_url)]
32
 
33
 
34
  class Resource(BaseModel, abc.ABC):
@@ -36,7 +19,8 @@ class Resource(BaseModel, abc.ABC):
36
 
37
  model_config = ConfigDict(validate_default=True)
38
 
39
- uri: LaxAnyUrl = Field(default=..., description="URI of the resource")
 
40
  name: str | None = Field(description="Name of the resource", default=None)
41
  description: str | None = Field(
42
  description="Description of the resource", default=None
@@ -47,6 +31,15 @@ class Resource(BaseModel, abc.ABC):
47
  pattern=r"^[a-zA-Z0-9]+/[a-zA-Z0-9\-+.]+$",
48
  )
49
 
 
 
 
 
 
 
 
 
 
50
  @field_validator("name", mode="before")
51
  @classmethod
52
  def set_default_name(cls, name: str | None, info: ValidationInfo) -> str:
 
1
  """Base classes and interfaces for FastMCP resources."""
2
 
3
  import abc
4
+ from typing import Union
5
 
6
  from pydantic import (
7
  AnyUrl,
8
  BaseModel,
 
9
  ConfigDict,
10
  Field,
11
  FileUrl,
12
  ValidationInfo,
13
  field_validator,
14
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
 
17
  class Resource(BaseModel, abc.ABC):
 
19
 
20
  model_config = ConfigDict(validate_default=True)
21
 
22
+ # uri: Annotated[AnyUrl, BeforeValidator(maybe_cast_str_to_any_url)] = Field(
23
+ uri: AnyUrl = Field(default=..., description="URI of the resource")
24
  name: str | None = Field(description="Name of the resource", default=None)
25
  description: str | None = Field(
26
  description="Description of the resource", default=None
 
31
  pattern=r"^[a-zA-Z0-9]+/[a-zA-Z0-9\-+.]+$",
32
  )
33
 
34
+ @field_validator("uri", mode="before")
35
+ def validate_uri(cls, uri: AnyUrl | str) -> AnyUrl:
36
+ if isinstance(uri, str):
37
+ # AnyUrl doesn't support triple-slashes, but files do ("file:///absolute/path")
38
+ if uri.startswith("file://"):
39
+ return FileUrl(uri)
40
+ return AnyUrl(uri)
41
+ return uri
42
+
43
  @field_validator("name", mode="before")
44
  @classmethod
45
  def set_default_name(cls, name: str | None, info: ValidationInfo) -> str: