Warning
This document is for an in-development version of Galaxy. You can alternatively view this page in the latest release if it exists or view the top of the latest release's documentation.
Source code for galaxy.tool_util.toolbox.lineages.factory
from collections.abc import (
Callable,
Iterable,
)
from typing import (
TYPE_CHECKING,
)
from galaxy.util.tool_version import remove_version_from_guid
from .interface import ToolLineage
if TYPE_CHECKING:
from galaxy.tools import Tool
from ..base import AbstractToolBox
[docs]
class LineageMap:
"""Map each unique tool id to a lineage object."""
[docs]
def __init__(self, toolbox: "AbstractToolBox"):
self.lineage_map: dict[str, ToolLineage] = {}
self.toolbox = toolbox
[docs]
def register(self, tool: "Tool") -> ToolLineage:
tool_id = tool.id
assert tool_id
# An existing lineage may not have the current tool's version yet, so
# register it either way. The map entry can be an older, unshared
# lineage (`get` aliases a tool_id without its versionless key), and
# that is what callers have always been handed back.
lineage = self._shared_lineage(tool_id, lambda: ToolLineage.from_tool(tool))
lineage.register_version(tool.version)
return self.lineage_map[tool_id]
def _shared_lineage(self, tool_id: str, build: Callable[[], ToolLineage]) -> ToolLineage:
"""Return the lineage `tool_id` contributes its versions to.
Every tool_id sharing a versionless guid resolves to one lineage
object, and the map is keyed under both. Callers depend on that
sharing: `ToolSection.copy(merge_tools=True)` dedups panel entries by
lineage, so two installed revisions of the same tool must collapse.
The versionless key wins, so a lineage reached by any one version
accumulates them all; `build` runs only when neither key is mapped.
"""
versionless_tool_id = remove_version_from_guid(tool_id)
lineage = self.lineage_map.get(versionless_tool_id) if versionless_tool_id else None
if lineage is None:
lineage = self.lineage_map.get(tool_id) or build()
if versionless_tool_id:
self.lineage_map.setdefault(versionless_tool_id, lineage)
self.lineage_map.setdefault(tool_id, lineage)
return lineage
[docs]
def get(self, tool_id: str) -> ToolLineage | None:
"""
Get lineage for `tool_id`.
By preference the lineage for a version-agnostic tool_id is returned.
Falls back to fetching the lineage only when this fails.
This happens when the tool_id does not contain a version.
"""
lineage = self._get_versionless(tool_id)
if lineage:
return lineage
if tool_id not in self.lineage_map:
# Not every tool reaches the toolbox through `__add_tool`, which is
# what registers a lineage: built-in converters, hidden tools and
# single-tool reloads all go straight to `register_tool`. Derive the
# lineage for those on first lookup.
if tool := self.toolbox._tools_by_id.get(tool_id):
return self.register(tool)
return self.lineage_map.get(tool_id)
def _get_versionless(self, tool_id: str) -> ToolLineage | None:
versionless_tool_id = remove_version_from_guid(tool_id)
if not versionless_tool_id:
return None
return self.lineage_map.get(versionless_tool_id)
[docs]
class CachedLineageMap(LineageMap):
"""Lineage map that derives versions from a callable on first access.
Used by ``galaxy.tools.cached_toolbox.CachedToolBox`` so the lineage view
over ``ToolIndex.entries_by_version`` doesn't need a boot-time pass to
seed every tool's version set into ``ToolLineage.tool_versions``.
Lineage data is already serialised inside the index
(``ToolIndex.to_dict``); this class just exposes it as a ``LineageMap``
on demand.
"""
[docs]
def __init__(self, toolbox: "AbstractToolBox", versions_for: Callable[[str], Iterable[str]] | None = None):
super().__init__(toolbox)
self._versions_for = versions_for
[docs]
def get(self, tool_id: str) -> ToolLineage | None:
# An eager toolbox seeds every version through ``register`` as it
# loads tools, so ``LineageMap.get`` is only ever a lookup. Nothing
# walks the tools here, so ``get`` is the construction path and has
# to source versions from the index — the inherited fallback would
# build a lineage from a single just-loaded ``Tool`` and memoise it,
# freezing ``tool_versions`` at one entry and hiding the rest. That
# breaks ``get_safe_version`` (``ToolModule.__init__`` maps a
# pinned-but-missing tool_version onto the nearest safe upgrade,
# e.g. ``__BUILD_LIST__`` 1.0.0 → 1.1.0): a one-element ``[1.2.0]``
# lineage misses 1.1.0, so the workflow binds to 1.2.0 with state
# shaped for 1.0.0.
if self._versions_for is not None:
try:
versions = list(self._versions_for(tool_id))
except Exception:
versions = []
if versions:
lineage = self._shared_lineage(tool_id, lambda: ToolLineage(tool_id))
for version in versions:
if version:
lineage.register_version(version)
return lineage
# Index has no entry for this tool_id — fall back to whatever the
# parent class can derive (a registered Tool, etc.).
return super().get(tool_id)
__all__ = ("CachedLineageMap", "LineageMap")