Hoist lazy-branch local imports to module level

The only local import left is lazy_toolbox's send_control_task, which
is a genuine cycle: galaxy.queue_worker imports LazyToolBox at module
level. Everything else - app boot, queue_worker, tool_panel_manager,
services/tools, search, populator, benchmarks, uses_shed and the test
modules - now imports at the top; the claimed app/tools circularity
never existed (galaxy.app already imports galaxy.tools).

Claude-Session: https://claude.ai/code/session_018L7ZmCv2ubKA3JNeSL8Pkr
This commit is contained in:
mvdbeek
2026-07-28 17:27:28 +02:00
parent 0b0eb9d3b5
commit 26249d3da6
10 changed files with 36 additions and 88 deletions
+9 -16
View File
@@ -176,10 +176,15 @@ from galaxy.tools.data import ToolDataTableManager
from galaxy.tools.data_manager.manager import DataManagers
from galaxy.tools.error_reports import ErrorReports
from galaxy.tools.evaluation import ToolTemplatingException
from galaxy.tools.lazy_toolbox import LazyToolBox
from galaxy.tools.search import (
LazyToolboxSearch,
ToolBoxSearch,
)
from galaxy.tools.source_store import (
build_tool_source_store,
ToolSourceStore,
)
from galaxy.tools.special_tools import load_lib_tools
from galaxy.tours import (
build_tours_registry,
@@ -425,12 +430,6 @@ class MinimalGalaxyApplication(BasicSharedApp, HaltableContainer, SentryClientMi
from ``build_tool_source_store`` — we let it propagate so the
operator sees the failure at startup.
"""
# Local import: avoids a circular import between galaxy.app and galaxy.tools.
from galaxy.tools.source_store import (
build_tool_source_store,
ToolSourceStore,
)
self.tool_source_store: ToolSourceStore | None = None
if not self.config.use_lazy_toolbox:
return
@@ -459,14 +458,11 @@ class MinimalGalaxyApplication(BasicSharedApp, HaltableContainer, SentryClientMi
def _shutdown_lazy_toolbox(self) -> None:
toolbox = getattr(self, "_toolbox", None)
# Lazy import: only relevant when the lazy path is in use.
try:
from galaxy.tools.lazy_toolbox import LazyToolBox
if isinstance(toolbox, LazyToolBox):
if isinstance(toolbox, LazyToolBox):
try:
toolbox.close()
except Exception as e:
log.debug(f"_shutdown_lazy_toolbox: {e}")
except Exception as e:
log.debug(f"_shutdown_lazy_toolbox: {e}")
def _use_lazy_toolbox(self) -> bool:
"""Determine whether to use LazyToolBox instead of regular ToolBox.
@@ -482,9 +478,6 @@ class MinimalGalaxyApplication(BasicSharedApp, HaltableContainer, SentryClientMi
def _create_lazy_toolbox(self) -> "tools.ToolBox":
"""Create a LazyToolBox instance."""
# Lazy import: avoids circular import between galaxy.app and galaxy.tools.
from galaxy.tools.lazy_toolbox import LazyToolBox
cache_size = self.config.lazy_toolbox_cache_size
log.info(f"Using LazyToolBox with cache_size={cache_size}")
+1 -5
View File
@@ -37,6 +37,7 @@ from galaxy.managers.sse import (
from galaxy.model import User
from galaxy.tools import ToolBox
from galaxy.tools.data_manager.manager import DataManagers
from galaxy.tools.lazy_toolbox import LazyToolBox
from galaxy.tools.special_tools import load_lib_tools
logging.getLogger("kombu").setLevel(logging.WARNING)
@@ -290,9 +291,6 @@ def _get_new_toolbox(app: "UniverseApplication", save_integrated_tool_panel: boo
new_toolbox: ToolBox
if getattr(app.config, "use_lazy_toolbox", False) and getattr(app, "tool_source_store", None) is not None:
# Lazy import: avoids circular import between galaxy.queue_worker and galaxy.tools.
from galaxy.tools.lazy_toolbox import LazyToolBox
new_toolbox = LazyToolBox(
config_filenames=tool_configs,
tool_root_dir=app.config.tool_path,
@@ -399,8 +397,6 @@ def reload_tool_source_cache(app, **kwargs):
This is typically triggered by an external process (like populate_store.py --watch)
when tool files change on disk.
"""
from galaxy.tools.lazy_toolbox import LazyToolBox
log.debug("Executing tool source cache reload on '%s'", app.config.server_name)
# Invalidate the lazy toolbox cache if the active toolbox is a LazyToolBox.
@@ -11,6 +11,8 @@ from galaxy.tool_shed.galaxy_install.client import InstallationTarget
from galaxy.tool_shed.util.basic_util import strip_path
from galaxy.tool_shed.util.repository_util import get_repository_owner
from galaxy.tool_shed.util.shed_util_common import get_tool_panel_config_tool_path_install_dir
from galaxy.tool_util.toolbox.base import resolve_tool_path
from galaxy.tools.source_store.populator import populate_for_paths
from galaxy.util import (
Element,
parse_xml_string,
@@ -31,14 +33,9 @@ def _collect_new_tool_paths(elem_list, tool_path: str, shed_tool_conf: str) -> d
— either top-level ``<tool>`` elements or ``<section>`` elements with
nested ``<tool>`` children. Paths must match what
``galaxy.tools.source_store.discover.discover_tools`` yields for the
rewritten conf byte-for-byte (the partial populate filters on the string):
a relative ``tool_path`` resolves against the conf file's directory, not
the process cwd, so route through the same ``resolve_tool_path``.
rewritten conf byte-for-byte (the partial populate filters on the string),
so route through the same ``resolve_tool_path`` discover uses.
"""
# Local import: keeps galaxy.tools.source_store out of the eager
# tool-shed install path's module graph.
from galaxy.tools.source_store.discover import resolve_tool_path
resolved_base = resolve_tool_path(tool_path, shed_tool_conf)
path_guids: dict[str, str | None] = {}
@@ -178,8 +175,6 @@ class ToolPanelManager:
load_elem_list, tool_path, shed_tool_conf_dict["config_filename"]
)
if new_path_guids:
from galaxy.tools.source_store.populator import populate_for_paths
populate_for_paths(
self.app.config,
paths=list(new_path_guids),
+2
View File
@@ -1323,6 +1323,8 @@ class LazyToolBox(ToolBox):
# broadcasts an invalidation; removals must broadcast too or
# peer web workers keep serving the uninstalled tool until an
# unrelated populate happens to run.
# Local import: genuine circularity — galaxy.queue_worker
# imports LazyToolBox at module level.
from galaxy.queue_worker import send_control_task
try:
+8 -10
View File
@@ -57,6 +57,14 @@ from whoosh.scoring import (
from whoosh.writing import AsyncWriter
from galaxy.config import GalaxyAppConfiguration
from galaxy.tools.source_store.populator import (
DEFAULT_STORE_NAME,
whoosh_dir_for_store,
)
from galaxy.tools.source_store.search import (
ToolSearchTuning,
ToolWhooshIndex,
)
from galaxy.util import (
ExecutionTimer,
unicodify,
@@ -156,16 +164,6 @@ class LazyToolboxSearch(ToolBoxSearch):
self.index_count += 1
def search(self, q: str, panel_view: str, config: GalaxyAppConfiguration) -> list[str]:
# Lazy import: avoids pulling populator's deps into module load.
from galaxy.tools.source_store.populator import (
DEFAULT_STORE_NAME,
whoosh_dir_for_store,
)
from galaxy.tools.source_store.search import (
ToolSearchTuning,
ToolWhooshIndex,
)
if not config.tool_search_index_dir:
# No index dir means whoosh search is off entirely.
return []
+1 -12
View File
@@ -12,6 +12,7 @@ from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path
from galaxy.tool_util.parser.factory import TOOL_SOURCE_FACTORIES
from .index import (
ToolIndex,
ToolIndexEntry,
@@ -162,12 +163,6 @@ class ToolSourceBenchmarks:
print("No tools found for XML parsing benchmark")
return None
try:
from galaxy.tool_util.parser.factory import TOOL_SOURCE_FACTORIES
except ImportError:
print("Could not import TOOL_SOURCE_FACTORIES")
return None
# Use a representative tool
path, content = tools[0]
factory = TOOL_SOURCE_FACTORIES.get("XmlToolSource")
@@ -188,12 +183,6 @@ class ToolSourceBenchmarks:
print("No tools found for deserialization benchmark")
return None
try:
from galaxy.tool_util.parser.factory import TOOL_SOURCE_FACTORIES
except ImportError:
print("Could not import TOOL_SOURCE_FACTORIES")
return None
path, content = tools[0]
# Simulate DB storage format
+1 -6
View File
@@ -8,7 +8,6 @@ from typing import (
cast,
get_args,
Optional,
TYPE_CHECKING,
)
from uuid import UUID
@@ -62,14 +61,12 @@ from galaxy.tool_util_models.parameters import (
)
from galaxy.tools import Tool
from galaxy.tools._types import InputFormatT
from galaxy.tools.lazy_toolbox import LazyToolBox
from galaxy.tools.search import ToolBoxSearch
from galaxy.util.path import safe_contains
from galaxy.webapps.galaxy.services._fetch_util import validate_and_normalize_targets
from galaxy.webapps.galaxy.services.base import ServiceBase
if TYPE_CHECKING:
from galaxy.tools.lazy_toolbox import LazyToolBox
log = logging.getLogger(__name__)
ToolRunPayload = dict[str, Any]
@@ -563,8 +560,6 @@ class ToolsService(ServiceBase):
def _get_lazy_toolbox(self, trans: ProvidesUserContext) -> Optional["LazyToolBox"]:
"""Return the active toolbox if it's a LazyToolBox, else None."""
from galaxy.tools.lazy_toolbox import LazyToolBox
toolbox = trans.app.toolbox
return toolbox if isinstance(toolbox, LazyToolBox) else None
+1 -2
View File
@@ -10,6 +10,7 @@ from unittest import SkipTest
log = logging.getLogger(__name__)
from galaxy.app import UniverseApplication
from galaxy.tools.source_store.populator import reconcile_index
from galaxy.util.tool_shed.tool_shed_registry import DEFAULT_TOOL_SHED_URL
from galaxy.util.unittest_utils import is_site_up
from galaxy_test.base.populators import DEFAULT_TIMEOUT
@@ -90,8 +91,6 @@ class UsesShed(UsesShedApi):
# would otherwise survive the ``reload_toolbox`` below: ``LazyToolBox``
# only re-runs the populator when discovery turns up a *new* path.
try:
from galaxy.tools.source_store.populator import reconcile_index
reconcile_index(self._app.config, rebuild_whoosh=True)
except Exception as e:
log.warning("reset_shed_tools: reconcile_index raised (continuing): %s", e)