mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-21 05:45:37 +08:00
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:
@@ -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}")
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 []
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -12,6 +12,11 @@ import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
from galaxy.queue_worker import reload_toolbox
|
||||
from galaxy.tools.lazy_toolbox import LazyToolBox
|
||||
from galaxy.tools.source_store import ToolIndex
|
||||
from galaxy.tools.source_store.composite import CompositeToolSourceStore
|
||||
from galaxy.tools.source_store.sqlalchemy import SqlAlchemyToolSourceStore
|
||||
from galaxy_test.base.populators import DatasetPopulator
|
||||
from galaxy_test.driver import integration_util
|
||||
|
||||
@@ -70,8 +75,6 @@ class TestSqliteToolSourceStorage(BaseToolSourceStorageIntegrationTestCase):
|
||||
self._test_api_tools_show()
|
||||
|
||||
def test_default_store_is_sqlalchemy_backend(self):
|
||||
from galaxy.tools.source_store.sqlalchemy import SqlAlchemyToolSourceStore
|
||||
|
||||
assert isinstance(self._app.tool_source_store, SqlAlchemyToolSourceStore)
|
||||
|
||||
|
||||
@@ -93,8 +96,6 @@ class TestCompositeToolSourceStorage(BaseToolSourceStorageIntegrationTestCase):
|
||||
cls._tmpdir = tempfile.mkdtemp(prefix="composite_tss_")
|
||||
cls._sqlite_path = os.path.join(cls._tmpdir, "sources.sqlite")
|
||||
|
||||
from galaxy.tools.source_store.sqlalchemy import SqlAlchemyToolSourceStore
|
||||
|
||||
SqlAlchemyToolSourceStore(url=f"sqlite:///{cls._sqlite_path}").count()
|
||||
|
||||
cls._conf_path = os.path.join(cls._tmpdir, "extra_tool_conf.xml")
|
||||
@@ -120,8 +121,6 @@ class TestCompositeToolSourceStorage(BaseToolSourceStorageIntegrationTestCase):
|
||||
# is enabled. Verifying the live app's store directly is more
|
||||
# robust than relying on /api/tools, which depends on whether the
|
||||
# store was populated in advance.
|
||||
from galaxy.tools.source_store.composite import CompositeToolSourceStore
|
||||
|
||||
assert isinstance(self._app.tool_source_store, CompositeToolSourceStore)
|
||||
|
||||
def test_api_tools_list_populated_via_bootstrap(self):
|
||||
@@ -165,8 +164,6 @@ class TestLazyToolBoxReload(BaseToolSourceStorageIntegrationTestCase):
|
||||
config["use_lazy_toolbox"] = True
|
||||
|
||||
def test_base_tools_survive_toolbox_reload(self):
|
||||
from galaxy.queue_worker import reload_toolbox
|
||||
|
||||
self._test_api_tools_show("cat1")
|
||||
assert self._app.toolbox.get_tool("upload1") is not None
|
||||
reload_toolbox(self._app)
|
||||
@@ -180,9 +177,6 @@ class TestLazyToolBoxReload(BaseToolSourceStorageIntegrationTestCase):
|
||||
# with its own, much smaller view, and the peer broadcast invalidates
|
||||
# our store cache. A subsequent reload must serve THIS instance's
|
||||
# tools from its inline repopulate, not the foreign index.
|
||||
from galaxy.tools.source_store import ToolIndex
|
||||
from galaxy.tools.source_store.sqlalchemy import SqlAlchemyToolSourceStore
|
||||
|
||||
store = self._app.tool_source_store
|
||||
assert store is not None
|
||||
connection = self._app.config.tool_source_database_connection
|
||||
@@ -195,8 +189,6 @@ class TestLazyToolBoxReload(BaseToolSourceStorageIntegrationTestCase):
|
||||
assert upload_stored
|
||||
store.delete(upload_stored[0].hash)
|
||||
|
||||
from galaxy.queue_worker import reload_toolbox
|
||||
|
||||
old_toolbox = self._app.toolbox
|
||||
reload_toolbox(self._app)
|
||||
assert self._app.toolbox is not old_toolbox
|
||||
@@ -211,8 +203,6 @@ class TestLazyToolBoxReload(BaseToolSourceStorageIntegrationTestCase):
|
||||
for source_hash in list(store.list_all()):
|
||||
store.delete(source_hash)
|
||||
store.invalidate_index_cache()
|
||||
from galaxy.queue_worker import reload_toolbox
|
||||
|
||||
reload_toolbox(self._app)
|
||||
assert self._app.toolbox.get_tool("upload1") is not None
|
||||
self._test_api_tools_show("cat1")
|
||||
@@ -390,8 +380,6 @@ class TestLazyToolBoxApi(BaseToolSourceStorageIntegrationTestCase):
|
||||
# batch surface instead. LAZY_TOOL_STRICT covers off-surface reads;
|
||||
# this also covers the ones strict can't (a filter that parses, an
|
||||
# _MATERIALIZE_OK attr read in a loop).
|
||||
from galaxy.tools.lazy_toolbox import LazyToolBox
|
||||
|
||||
toolbox = self._app.toolbox
|
||||
# This class self-enables lazy mode; assert it (and narrow the type so
|
||||
# mypy accepts the lazy-only _lazy_materialize_count counter).
|
||||
|
||||
@@ -8,6 +8,10 @@ from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
import galaxy.queue_worker as queue_worker_mod
|
||||
import galaxy.tools.lazy_toolbox as mod
|
||||
from galaxy.tool_util.toolbox.lineages.factory import LazyLineageMap
|
||||
from galaxy.tool_util.toolbox.panel import ToolPanelElements
|
||||
from galaxy.tools.lazy_toolbox import (
|
||||
LazyTool,
|
||||
LazyToolBox,
|
||||
@@ -230,8 +234,6 @@ def test_strict_getattr_raises_with_clear_message(monkeypatch):
|
||||
# Strict mode is opt-in via LAZY_TOOL_STRICT=1; permissive (materialise
|
||||
# on unknown attr with WARN) is the default. Flip the module-level flag
|
||||
# for this test so the strict path fires.
|
||||
import galaxy.tools.lazy_toolbox as mod
|
||||
|
||||
monkeypatch.setattr(mod, "_LAZY_TOOL_PERMISSIVE", False)
|
||||
t = _stub()
|
||||
with pytest.raises(NotImplementedError) as ei:
|
||||
@@ -255,8 +257,6 @@ def test_materialize_ok_set_forwards_to_real_tool(caplog):
|
||||
|
||||
|
||||
def test_permissive_flag_warns_and_materialises(monkeypatch, caplog):
|
||||
import galaxy.tools.lazy_toolbox as mod
|
||||
|
||||
monkeypatch.setattr(mod, "_LAZY_TOOL_PERMISSIVE", True)
|
||||
|
||||
class _Real:
|
||||
@@ -337,8 +337,6 @@ def test_create_tool_populates_adhoc_for_existing_file(tmp_path, monkeypatch):
|
||||
# Shed installs load cloned tools during metadata generation, before
|
||||
# any conf is persisted — a miss for an on-disk file populates that
|
||||
# path instead of raising.
|
||||
import galaxy.tools.lazy_toolbox as mod
|
||||
|
||||
guid = "toolshed.example.com/repos/owner/repo/cloned/1.0"
|
||||
tool_file = tmp_path / "cloned.xml"
|
||||
tool_file.write_text("<tool id='cloned' version='1.0'/>")
|
||||
@@ -451,9 +449,6 @@ def _registry_box():
|
||||
removal bookkeeping paths run against genuine data structures."""
|
||||
import threading
|
||||
|
||||
from galaxy.tool_util.toolbox.lineages.factory import LazyLineageMap
|
||||
from galaxy.tool_util.toolbox.panel import ToolPanelElements
|
||||
|
||||
box = _seam_box()
|
||||
box._tools_by_id = {}
|
||||
box._tool_versions_by_id = {}
|
||||
@@ -503,8 +498,6 @@ def test_invalidate_index_cache_keeps_unindexed_tools():
|
||||
|
||||
|
||||
def test_remove_tool_by_id_broadcasts_reload_to_peers(monkeypatch):
|
||||
import galaxy.queue_worker as queue_worker_mod
|
||||
|
||||
box = _registry_box()
|
||||
box._tool_index = ToolIndex()
|
||||
entry = _entry(id="doomed")
|
||||
|
||||
Reference in New Issue
Block a user