diff --git a/lib/galaxy/app/__init__.py b/lib/galaxy/app/__init__.py index 69076214a51..d969a1d5368 100644 --- a/lib/galaxy/app/__init__.py +++ b/lib/galaxy/app/__init__.py @@ -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}") diff --git a/lib/galaxy/queue_worker/__init__.py b/lib/galaxy/queue_worker/__init__.py index 9224371fdf4..e71ca5f7c03 100644 --- a/lib/galaxy/queue_worker/__init__.py +++ b/lib/galaxy/queue_worker/__init__.py @@ -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. diff --git a/lib/galaxy/tool_shed/galaxy_install/tools/tool_panel_manager.py b/lib/galaxy/tool_shed/galaxy_install/tools/tool_panel_manager.py index 5883852ccb7..8f4aceaeaac 100644 --- a/lib/galaxy/tool_shed/galaxy_install/tools/tool_panel_manager.py +++ b/lib/galaxy/tool_shed/galaxy_install/tools/tool_panel_manager.py @@ -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 ```` elements or ``
`` elements with nested ```` 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), diff --git a/lib/galaxy/tools/lazy_toolbox.py b/lib/galaxy/tools/lazy_toolbox.py index ff4e32c2dcf..760c4dd633e 100644 --- a/lib/galaxy/tools/lazy_toolbox.py +++ b/lib/galaxy/tools/lazy_toolbox.py @@ -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: diff --git a/lib/galaxy/tools/search/__init__.py b/lib/galaxy/tools/search/__init__.py index ed6887ab4de..d207681ef39 100644 --- a/lib/galaxy/tools/search/__init__.py +++ b/lib/galaxy/tools/search/__init__.py @@ -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 [] diff --git a/lib/galaxy/tools/source_store/benchmarks.py b/lib/galaxy/tools/source_store/benchmarks.py index bc3ee867ab6..402d47857d6 100644 --- a/lib/galaxy/tools/source_store/benchmarks.py +++ b/lib/galaxy/tools/source_store/benchmarks.py @@ -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 diff --git a/lib/galaxy/webapps/galaxy/services/tools.py b/lib/galaxy/webapps/galaxy/services/tools.py index ce766024680..6c2f2797bff 100644 --- a/lib/galaxy/webapps/galaxy/services/tools.py +++ b/lib/galaxy/webapps/galaxy/services/tools.py @@ -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 diff --git a/lib/galaxy_test/driver/uses_shed.py b/lib/galaxy_test/driver/uses_shed.py index cc8cd1c9012..b061bdbb03c 100644 --- a/lib/galaxy_test/driver/uses_shed.py +++ b/lib/galaxy_test/driver/uses_shed.py @@ -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) diff --git a/test/integration/test_tool_source_storage.py b/test/integration/test_tool_source_storage.py index 9aa16a6afb3..b4fe1b511ff 100644 --- a/test/integration/test_tool_source_storage.py +++ b/test/integration/test_tool_source_storage.py @@ -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). diff --git a/test/unit/app/tools/test_lazy_tool.py b/test/unit/app/tools/test_lazy_tool.py index f38329c8983..44463a49d29 100644 --- a/test/unit/app/tools/test_lazy_tool.py +++ b/test/unit/app/tools/test_lazy_tool.py @@ -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("") @@ -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")