diff --git a/lib/galaxy/tools/source_store/index.py b/lib/galaxy/tools/source_store/index.py index 2d1e88f7eaf..ce29a5b1ae0 100644 --- a/lib/galaxy/tools/source_store/index.py +++ b/lib/galaxy/tools/source_store/index.py @@ -80,6 +80,12 @@ class ToolIndexEntry(BaseModel): # ``interactive_tool``, ``data_source``, ...). Filter authors and # ``DataManagerTool.allow_user_access`` (admin-only) both branch on this. tool_type: str = "default" + # True for datatype-converter discoveries (``CONVERTER_TOOL_CONF``). The + # ``tool_type`` alone can't identify them — converters keep whatever type + # their XML declares — so the populator stamps this flag from the + # discovery source. ``tests_summary`` excludes them to mirror the eager + # ``if not tool.is_datatype_converter`` filter. + is_datatype_converter: bool = False # User-facing tags from ```` config (distinct from ``labels``). # Surfaced for custom tool filters that bucket tools by tag. tags: list[str] = Field(default_factory=list) @@ -396,8 +402,9 @@ class ToolIndex(BaseModel): summary: dict[str, dict[str, dict]] = {} for entry in self.entries.values(): # Match the eager fallback in services.tools.ToolsService.get_tests_summary: - # tools without tests are excluded entirely. - if not entry.test_count: + # tools without tests, and datatype converters, are excluded entirely + # (the eager loop skips ``tool.is_datatype_converter``). + if not entry.test_count or entry.is_datatype_converter: continue if entry.id not in summary: summary[entry.id] = {} diff --git a/lib/galaxy/tools/source_store/populator.py b/lib/galaxy/tools/source_store/populator.py index f4c8d001307..919ac09c027 100644 --- a/lib/galaxy/tools/source_store/populator.py +++ b/lib/galaxy/tools/source_store/populator.py @@ -498,6 +498,7 @@ def build_index_entry_from_source( version=version, name=tool_source.parse_name() or "", description=tool_source.parse_description() or "", + is_datatype_converter=discovered.tool_conf == CONVERTER_TOOL_CONF, icon=icon, xrefs=xrefs, is_workflow_compatible=is_workflow_compatible, diff --git a/lib/galaxy/webapps/galaxy/services/tools.py b/lib/galaxy/webapps/galaxy/services/tools.py index 5383d6edc5a..c1eb14c6cf7 100644 --- a/lib/galaxy/webapps/galaxy/services/tools.py +++ b/lib/galaxy/webapps/galaxy/services/tools.py @@ -575,7 +575,13 @@ class ToolsService(ServiceBase): """ lazy_toolbox = self._get_lazy_toolbox(trans) if lazy_toolbox and lazy_toolbox.tool_index: - return lazy_toolbox.tool_index.get_tests_summary() + # The index is store-wide and ``get_tests_summary`` already drops + # datatype converters; scope the result to tools this toolbox + # actually holds so ids present in the store but never loaded here + # don't leak into the summary (the eager loop below only ever sees + # loaded tools). + summary = lazy_toolbox.tool_index.get_tests_summary() + return {tool_id: versions for tool_id, versions in summary.items() if lazy_toolbox.has_tool(tool_id)} # Fallback to traditional toolbox iteration test_counts_by_tool: dict[str, dict] = {} diff --git a/test/unit/app/tools/source_store/test_index_versions.py b/test/unit/app/tools/source_store/test_index_versions.py index 4dde69b3f1c..e6dd23d7981 100644 --- a/test/unit/app/tools/source_store/test_index_versions.py +++ b/test/unit/app/tools/source_store/test_index_versions.py @@ -110,3 +110,11 @@ def test_add_entry_invalidates_derived_metadata(index_entry, tool_index): assert [requirement["name"] for requirement in index.get_all_requirements()] == ["one", "two"] assert set(index.get_tests_summary()) == {"one", "two"} + + +def test_tests_summary_excludes_datatype_converters(index_entry, tool_index): + index = tool_index( + index_entry("real_tool", version="1.0", test_count=2), + index_entry("convert_fasta", version="1.0", test_count=1, is_datatype_converter=True), + ) + assert set(index.get_tests_summary()) == {"real_tool"} diff --git a/test/unit/app/tools/source_store/test_populator_entry.py b/test/unit/app/tools/source_store/test_populator_entry.py new file mode 100644 index 00000000000..fd9a542c25c --- /dev/null +++ b/test/unit/app/tools/source_store/test_populator_entry.py @@ -0,0 +1,61 @@ +"""Pin ``build_index_entry_from_source`` metadata capture.""" + +from datetime import ( + datetime, + timezone, +) + +from galaxy.tool_util.parser import get_tool_source +from galaxy.tools.source_store.discover import ( + CONVERTER_TOOL_CONF, + DiscoveredTool, +) +from galaxy.tools.source_store.interface import StoredToolSource +from galaxy.tools.source_store.populator import ( + build_index_entry_from_source, + MAX_HELP_TEXT_CHARS, +) + +_TOOL_XML = """ + echo + + + This wraps the quaxifier subroutine. + +""" + +_TOOL_XML_NO_HELP = """ + echo + + + +""" + + +def _stored(path, tool_source): + return StoredToolSource( + hash="deadbeef", + tool_source_class=type(tool_source).__name__, + raw_source=tool_source.to_string(), + tool_id=tool_source.parse_id(), + tool_version=tool_source.parse_version(), + tool_dir=str(path.parent), + source_path=str(path), + stored_at=datetime.now(timezone.utc), + metadata={}, + ) + + +def _build(tmp_path, xml, tool_conf="tool_conf.xml"): + path = tmp_path / "tool.xml" + path.write_text(xml) + tool_source = get_tool_source(config_file=str(path)) + discovered = DiscoveredTool(path=str(path), tool_conf=tool_conf, tool_path=str(tmp_path)) + return build_index_entry_from_source(discovered, _stored(path, tool_source), tool_source) + + +def test_entry_marks_datatype_converter(tmp_path): + plain = _build(tmp_path, _TOOL_XML) + converter = _build(tmp_path, _TOOL_XML, tool_conf=CONVERTER_TOOL_CONF) + assert plain is not None and plain.is_datatype_converter is False + assert converter is not None and converter.is_datatype_converter is True