index carries the panel-entry contract; LazyTool serves it without materialising

ToolIndexEntry gains icon, xrefs, model_class, form_style and
is_workflow_compatible, derived at populate time the same way
Tool.__init__/to_dict derive them (tool_type class registry, root
workflow_compatible attribute, page count). EDAM fields now go through
expand_ontology_data, picking up the curated mapping overrides and
legacy bio.tools xrefs the direct parse_edam_* calls missed.

LazyTool.to_panel_entry emits the full client payload -- including
tool_shed_repository (the field the panel-view integration tests
assert), versions from the registered lineage, uuid, and the
admin-gated config_file from source_path.
This commit is contained in:
mvdbeek
2026-07-28 17:27:23 +02:00
parent 7af0f056cd
commit e16502d77a
4 changed files with 118 additions and 16 deletions
+15
View File
@@ -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", []),
+53 -13
View File
@@ -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:
+28 -3
View File
@@ -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.
+22
View File
@@ -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] = []