Skip to content

Commit e732854

Browse files
authored
Make pydantic-monty + conda-forge-metadata optional (#187)
1 parent 948b6bb commit e732854

7 files changed

Lines changed: 103 additions & 12 deletions

File tree

conda_meta_mcp/tools/import_mapping.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,35 @@
88
from __future__ import annotations
99

1010
import asyncio
11+
from collections.abc import Callable
1112
from functools import lru_cache
1213
from typing import Any
1314

14-
from conda_forge_metadata.autotick_bot.import_to_pkg import (
15-
get_pkgs_for_import,
16-
map_import_to_package,
17-
)
1815
from fastmcp.exceptions import ToolError
1916

2017
from .registry import register_tool
2118

19+
DISABLED_MESSAGE = "Disabled, enable via installing the package conda-forge-metadata"
20+
21+
GetPkgsForImport = Callable[[str], tuple[set[str] | None, str]]
22+
MapImportToPackage = Callable[[str], str]
23+
get_pkgs_for_import: GetPkgsForImport | None
24+
map_import_to_package: MapImportToPackage | None
25+
26+
try:
27+
from conda_forge_metadata.autotick_bot.import_to_pkg import (
28+
get_pkgs_for_import as _get_pkgs_for_import,
29+
)
30+
from conda_forge_metadata.autotick_bot.import_to_pkg import (
31+
map_import_to_package as _map_import_to_package,
32+
)
33+
except ImportError:
34+
get_pkgs_for_import = None
35+
map_import_to_package = None
36+
else:
37+
get_pkgs_for_import = _get_pkgs_for_import
38+
map_import_to_package = _map_import_to_package
39+
2240

2341
@lru_cache(maxsize=1024)
2442
def _map_import(import_name: str, get_keys: str = "") -> dict[str, Any]:
@@ -28,6 +46,8 @@ def _map_import(import_name: str, get_keys: str = "") -> dict[str, Any]:
2846
"""
2947
if not import_name or not import_name.strip():
3048
raise ValueError("import_name must be a non-empty string")
49+
if get_pkgs_for_import is None or map_import_to_package is None:
50+
raise ToolError(DISABLED_MESSAGE)
3151

3252
query = import_name.strip()
3353

@@ -105,6 +125,8 @@ async def import_mapping(import_name: str, get_keys: str = "") -> dict[str, Any]
105125
"""
106126
try:
107127
return await asyncio.to_thread(_map_import, import_name, get_keys)
128+
except ToolError:
129+
raise
108130
except ValueError as ve:
109131
raise ToolError(f"'import_mapping' invalid input: {ve}") from ve
110132
except Exception as e: # pragma: no cover - generic protection

conda_meta_mcp/tools/pypi_to_conda.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,28 @@
88
from __future__ import annotations
99

1010
import asyncio
11+
from collections.abc import Callable
1112
from functools import lru_cache
1213
from typing import Any
1314

14-
from conda_forge_metadata.autotick_bot.pypi_to_conda import map_pypi_to_conda
1515
from fastmcp.exceptions import ToolError
1616

1717
from .registry import register_tool
1818

19+
DISABLED_MESSAGE = "Disabled, enable via installing the package conda-forge-metadata"
20+
21+
MapPypiToConda = Callable[[str], str]
22+
map_pypi_to_conda: MapPypiToConda | None
23+
24+
try:
25+
from conda_forge_metadata.autotick_bot.pypi_to_conda import (
26+
map_pypi_to_conda as _map_pypi_to_conda,
27+
)
28+
except ImportError:
29+
map_pypi_to_conda = None
30+
else:
31+
map_pypi_to_conda = _map_pypi_to_conda
32+
1933

2034
@lru_cache(maxsize=4096)
2135
def _map_pypi_name(pypi_name: str) -> dict[str, Any]:
@@ -25,6 +39,8 @@ def _map_pypi_name(pypi_name: str) -> dict[str, Any]:
2539
"""
2640
if not pypi_name or not pypi_name.strip():
2741
raise ValueError("pypi_name must be a non-empty string")
42+
if map_pypi_to_conda is None:
43+
raise ToolError(DISABLED_MESSAGE)
2844

2945
original = pypi_name.strip()
3046
conda_name = map_pypi_to_conda(original)
@@ -51,6 +67,8 @@ async def pypi_to_conda(pypi_name: str) -> dict[str, Any]:
5167
"""
5268
try:
5369
return await asyncio.to_thread(_map_pypi_name, pypi_name)
70+
except ToolError:
71+
raise
5472
except ValueError as ve:
5573
raise ToolError(f"'pypi_to_conda' invalid input: {ve}") from ve
5674
except Exception as e: # pragma: no cover

pixi.lock

Lines changed: 12 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@ classifiers = [
1111
"Programming Language :: Python :: 3.13",
1212
]
1313
dependencies = [
14-
"fastmcp",
14+
"argparse-manpage>=4.7,<5",
15+
"conda>=26.1,<27",
16+
"conda-libmamba-solver",
17+
"conda-package-streaming>=0.12.0,<0.13",
18+
"fastmcp>=3.1.0",
19+
"pydantic>=2",
20+
"pyyaml>=6.0.3,<7",
21+
"requests>=2",
1522
]
1623
description = "A Model Context Protocol server providing conda eco system context"
1724
dynamic = ["version"]
@@ -20,6 +27,10 @@ name = "conda-meta-mcp"
2027
readme = "README.md"
2128
requires-python = ">=3.13"
2229

30+
[project.optional-dependencies]
31+
code-mode = ["pydantic-monty"]
32+
conda-forge-metadata = ["conda-forge-metadata>=0.13,<0.14"]
33+
2334
[project.scripts]
2435
cmm = "conda_meta_mcp.cli:main"
2536

@@ -39,11 +50,13 @@ source = "vcs"
3950
argparse-manpage = ">=4.7,<5"
4051
conda = ">=26.1,<27"
4152
conda-forge-metadata = ">=0.13,<0.14"
53+
conda-libmamba-solver = "*"
4254
conda-package-streaming = ">=0.12.0,<0.13"
43-
fastmcp = ">=3.2.4,<3.3.0"
55+
fastmcp = ">=3.1.0,<3.3.0"
4456
libmambapy = ">=2.4.0,<3"
4557
pydantic-monty = "*"
4658
pyyaml = ">=6.0.3,<7"
59+
requests = ">=2,<3"
4760

4861
[tool.pixi.environments]
4962
dev = ["test"]
@@ -67,7 +80,7 @@ prek = 'prek run --all-files --color=always'
6780
test = 'python -m pytest'
6881

6982
[tool.pixi.pypi-dependencies]
70-
conda-meta-mcp = {path = ".", editable = true}
83+
conda-meta-mcp = {path = ".", editable = true, extras = ["code-mode", "conda-forge-metadata"]}
7184

7285
[tool.pixi.workspace]
7386
channels = ["conda-forge"]

recipe/meta.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,12 @@ requirements:
2525
run:
2626
- python >=3.13
2727
- argparse-manpage >=4.7
28-
- fastmcp >=2.13.1
28+
- fastmcp >=3.1.0
2929
- conda >=26.1
3030
- conda-forge-metadata >=0.13.0
3131
- conda-package-streaming >=0.12.0
3232
- conda-libmamba-solver
33+
- pydantic-monty
3334
- pyyaml >=6.0.3
3435
- requests >=2.28.0
3536

tests/tools/test_import_mapping.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from fastmcp import Client
55
from fastmcp.exceptions import ToolError
66

7+
from conda_meta_mcp.tools import import_mapping as import_mapping_module
8+
79
# Heuristic labels the tool may legitimately emit. Keeping a central set makes the
810
# success test resilient to minor internal mapping changes upstream.
911
VALID_HEURISTICS = {
@@ -65,6 +67,18 @@ async def test_import_mapping__error_on_empty_input(server):
6567
assert "invalid input" in str(exc.value).lower()
6668

6769

70+
@pytest.mark.asyncio
71+
async def test_import_mapping__disabled_when_dependency_missing(monkeypatch):
72+
import_mapping_module._map_import.cache_clear()
73+
monkeypatch.setattr(import_mapping_module, "get_pkgs_for_import", None)
74+
75+
with pytest.raises(ToolError) as exc:
76+
await import_mapping_module.import_mapping("numpy")
77+
78+
assert str(exc.value) == "Disabled, enable via installing the package conda-forge-metadata"
79+
import_mapping_module._map_import.cache_clear()
80+
81+
6882
@pytest.mark.asyncio
6983
async def test_import_mapping__get_keys_empty_returns_all(server):
7084
"""Empty get_keys should return all fields (backward compatible)."""

tests/tools/test_pypi_to_conda.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from fastmcp import Client
55
from fastmcp.exceptions import ToolError
66

7+
from conda_meta_mcp.tools import pypi_to_conda as pypi_to_conda_module
8+
79

810
@pytest.mark.asyncio
911
@pytest.mark.parametrize(
@@ -47,3 +49,15 @@ async def test_pypi_to_conda__error_empty_input(server):
4749
async with Client(server) as client:
4850
with pytest.raises(ToolError):
4951
await client.call_tool("pypi_to_conda", {"pypi_name": ""})
52+
53+
54+
@pytest.mark.asyncio
55+
async def test_pypi_to_conda__disabled_when_dependency_missing(monkeypatch):
56+
pypi_to_conda_module._map_pypi_name.cache_clear()
57+
monkeypatch.setattr(pypi_to_conda_module, "map_pypi_to_conda", None)
58+
59+
with pytest.raises(ToolError) as exc:
60+
await pypi_to_conda_module.pypi_to_conda("authzed")
61+
62+
assert str(exc.value) == "Disabled, enable via installing the package conda-forge-metadata"
63+
pypi_to_conda_module._map_pypi_name.cache_clear()

0 commit comments

Comments
 (0)