diff --git a/lib/galaxy/tool_source_store/index.py b/lib/galaxy/tool_source_store/index.py index 6247416b543..97f826668f1 100644 --- a/lib/galaxy/tool_source_store/index.py +++ b/lib/galaxy/tool_source_store/index.py @@ -37,6 +37,11 @@ class ToolIndexEntry: description: str = "" # === Classification === + icon: str | None = None + xrefs: list[dict[str, Any]] = field(default_factory=list) + model_class: str = "Tool" + form_style: str = "regular" + is_workflow_compatible: bool = True panel_section_id: str | None = None panel_section_name: str | None = None labels: list[str] = field(default_factory=list) @@ -136,6 +141,11 @@ class ToolIndexEntry: "tool_shed_repository_id": self.tool_shed_repository_id, "name": self.name, "description": self.description, + "icon": self.icon, + "xrefs": self.xrefs, + "model_class": self.model_class, + "form_style": self.form_style, + "is_workflow_compatible": self.is_workflow_compatible, "panel_section_id": self.panel_section_id, "panel_section_name": self.panel_section_name, "labels": self.labels, @@ -174,6 +184,11 @@ class ToolIndexEntry: tool_shed_repository_id=data.get("tool_shed_repository_id"), name=data.get("name", ""), description=data.get("description", ""), + icon=data.get("icon"), + xrefs=data.get("xrefs", []), + model_class=data.get("model_class", "Tool"), + form_style=data.get("form_style", "regular"), + is_workflow_compatible=data.get("is_workflow_compatible", True), panel_section_id=data.get("panel_section_id"), panel_section_name=data.get("panel_section_name"), labels=data.get("labels", []), diff --git a/lib/galaxy/tool_source_store/populator.py b/lib/galaxy/tool_source_store/populator.py index 1d93484adc3..5d0bc9e796f 100644 --- a/lib/galaxy/tool_source_store/populator.py +++ b/lib/galaxy/tool_source_store/populator.py @@ -376,6 +376,7 @@ def build_index_entry_from_source( discovered, stored, tool_source, + biotools_metadata_source=None, ): """Assemble a :class:`ToolIndexEntry` from a populator triple. @@ -428,19 +429,48 @@ def build_index_entry_from_source( except Exception: pass - edam_operations: list[str] = [] - if hasattr(tool_source, "parse_edam_operations"): - try: - edam_operations = list(tool_source.parse_edam_operations() or ()) - except Exception: - pass + lowered = tool_id.lower() + all_ids = [lowered] + if "/repos/" in lowered: + all_ids = [lowered, lowered.rsplit("/", 1)[0], lowered.rsplit("/", 2)[-2]] + # Same ontology expansion as ``Tool.__init__`` — curated EDAM mapping + # overrides and legacy bio.tools xrefs included. + from galaxy.tool_util.ontologies.ontology_data import expand_ontology_data - edam_topics: list[str] = [] - if hasattr(tool_source, "parse_edam_topics"): - try: - edam_topics = list(tool_source.parse_edam_topics() or ()) - except Exception: - pass + ontology_data = expand_ontology_data(tool_source, all_ids, biotools_metadata_source) + edam_operations = list(ontology_data.edam_operations or ()) + edam_topics = list(ontology_data.edam_topics or ()) + xrefs: list[dict[str, Any]] = [dict(x) for x in ontology_data.xrefs or ()] + + icon = tool_source.parse_icon() if hasattr(tool_source, "parse_icon") else None + + # ``model_class`` / ``form_style`` mirror ``Tool.to_dict``'s ad-hoc + # class inspection via the same tool_type registry the eager path + # constructs tools with. Local import: galaxy.tools is the full tool + # machinery; only the registry mapping is needed. + from galaxy.tools import ( + DatabaseOperationTool, + InteractiveTool, + Tool, + tool_types, + ) + + tool_class = tool_types.get(tool_type, Tool) + regular_form = tool_class is Tool or issubclass(tool_class, (DatabaseOperationTool, InteractiveTool)) + + # ``Tool.check_workflow_compatible`` equivalents derivable at parse + # time: multi-page tools and data sources are incompatible; XML tools + # may opt out via workflow_compatible="false" on the root. + is_workflow_compatible = not tool_type.startswith("data_source") + try: + pages = tool_source.parse_input_pages() + if pages is not None and len(pages.page_sources) > 1: + is_workflow_compatible = False + except Exception: + pass + root = getattr(tool_source, "root", None) + if root is not None and str(root.get("workflow_compatible", "True")).lower() in ("false", "0", "no"): + is_workflow_compatible = False # Honour the same version-default rules as ``Tool.__init__``: empty # ``version`` on a pre-16.04-profile tool becomes "1.0.0"; on newer @@ -461,6 +491,11 @@ def build_index_entry_from_source( panel_section_id=discovered.section_id, panel_section_name=discovered.section_name, labels=list(discovered.labels or ()), + icon=icon, + xrefs=xrefs, + model_class=tool_class.__name__, + form_style="regular" if regular_form else "special", + is_workflow_compatible=is_workflow_compatible, edam_operations=edam_operations, edam_topics=edam_topics, source_hash=stored.hash, @@ -738,6 +773,11 @@ def populate_store_inline( # post-walk syncs (_stamp_panel_sections_onto_index, # _sync_tool_mutations_to_index) replaced ad-hoc. full_scan = paths is None or prune + # Local import: galaxy.tools.biotools executes the galaxy.tools + # package; only the metadata-source factory is needed, once per run. + from galaxy.tools.biotools import get_galaxy_biotools_metadata_source + + biotools_metadata_source = get_galaxy_biotools_metadata_source(config) for store_name in sorted(writable_names): triples = parsed_per_store[store_name] if full_scan: @@ -749,7 +789,7 @@ def populate_store_inline( # stays as-is (use reconcile_index for full prune). index = stores[store_name].load_index() or ToolIndex() for d, stored, tool_source in triples: - entry = build_index_entry_from_source(d, stored, tool_source) + entry = build_index_entry_from_source(d, stored, tool_source, biotools_metadata_source) if entry is not None: index.add_entry(entry) try: diff --git a/lib/galaxy/tools/lazy_toolbox.py b/lib/galaxy/tools/lazy_toolbox.py index 51c0f181f92..4cc1e9a536e 100644 --- a/lib/galaxy/tools/lazy_toolbox.py +++ b/lib/galaxy/tools/lazy_toolbox.py @@ -173,6 +173,9 @@ class LazyTool: require_login = _entry_attr("require_login") edam_operations = _entry_attr("edam_operations") edam_topics = _entry_attr("edam_topics") + icon = _entry_attr("icon") + xrefs = _entry_attr("xrefs") + is_workflow_compatible = _entry_attr("is_workflow_compatible") # --- forwarded mutable entry surface --- # Eager ``_load_tool_tag_set`` (base.py:964-987) mutates these post-create. @@ -317,20 +320,42 @@ class LazyTool: parse every tool on the first request. """ entry = self._entry - return { + if self._lineage is not None: + versions = list(self._lineage.tool_versions) + else: + versions = [entry.version] if entry.version else [] + payload = { + "model_class": entry.model_class, "id": self.id, "name": self.name, "version": self.version, "description": self.description, "labels": self.labels if self.labels else [], + "icon": entry.icon, "edam_operations": entry.edam_operations or [], "edam_topics": entry.edam_topics or [], "hidden": self.hidden, - "model_class": "Tool", + "is_workflow_compatible": entry.is_workflow_compatible, + "xrefs": entry.xrefs or [], + "versions": versions, + "hidden_versions": [], + "link": f"/tool_runner?tool_id={self.id}", "panel_section_id": entry.panel_section_id, "panel_section_name": entry.panel_section_name, - "link": f"/tool_runner?tool_id={self.id}", + "form_style": entry.form_style, } + if entry.uuid: + payload["uuid"] = entry.uuid + if entry.tool_shed: + payload["tool_shed_repository"] = { + "name": entry.repository_name, + "owner": entry.repository_owner, + "changeset_revision": entry.changeset_revision, + "tool_shed": entry.tool_shed, + } + if trans is not None and getattr(trans, "user_is_admin", False): + payload["config_file"] = entry.source_path + return payload def to_dict(self, trans=None, link_details: bool = False, tool_help: bool = False, **kw) -> dict[str, Any]: """Materialise and delegate — ``/api/tools/{id}`` contract. diff --git a/test/unit/app/tools/test_lazy_tool.py b/test/unit/app/tools/test_lazy_tool.py index 648a0c9e7b8..39e31750f43 100644 --- a/test/unit/app/tools/test_lazy_tool.py +++ b/test/unit/app/tools/test_lazy_tool.py @@ -133,6 +133,28 @@ def test_tool_tags_answered_without_materialise(): assert t.tool_tags == ["curated"] +def test_to_panel_entry_carries_client_contract_fields(): + e = _entry( + id="toolshed.example.com/repos/owner/repo/tool/1.0", + tool_shed="toolshed.example.com", + repository_name="repo", + repository_owner="owner", + changeset_revision="abc123", + model_class="Tool", + form_style="regular", + is_workflow_compatible=True, + xrefs=[{"value": "bwa", "reftype": "bio.tools"}], + ) + t = _stub(e) + d = t.to_panel_entry(trans=None) + assert d["is_workflow_compatible"] is True + assert d["form_style"] == "regular" + assert d["xrefs"] == [{"value": "bwa", "reftype": "bio.tools"}] + assert d["versions"] == ["2.5.0"] + assert d["tool_shed_repository"]["changeset_revision"] == "abc123" + assert "config_file" not in d + + def test_to_dict_materialises(): calls: list[Any] = []