diff --git a/docs/hub-cloud/01-init-local-instance.ipynb b/docs/hub-cloud/01-init-local-instance.ipynb index c023f2c2..55fff245 100644 --- a/docs/hub-cloud/01-init-local-instance.ipynb +++ b/docs/hub-cloud/01-init-local-instance.ipynb @@ -1,7 +1,6 @@ { "cells": [ { - "attachments": {}, "cell_type": "markdown", "metadata": { "tags": [] @@ -107,7 +106,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.13" + "version": "3.10.16" }, "vscode": { "interpreter": { diff --git a/docs/hub-cloud/02-connect-local-instance.ipynb b/docs/hub-cloud/02-connect-local-instance.ipynb index 5c2abb09..8f5b54fc 100644 --- a/docs/hub-cloud/02-connect-local-instance.ipynb +++ b/docs/hub-cloud/02-connect-local-instance.ipynb @@ -32,7 +32,9 @@ "metadata": {}, "outputs": [], "source": [ - "import lamindb_setup as ln_setup" + "import lamindb_setup as ln_setup\n", + "from lamindb_setup._check_setup import ModuleWasntConfigured\n", + "import pytest" ] }, { @@ -51,6 +53,20 @@ "If the user is the instance owner, load the instance by name:" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "1106eff3", + "metadata": {}, + "outputs": [], + "source": [ + "# bionty is not in the (schema) modules of mydata\n", + "# _check_instance_setup is called inside with from_module=None\n", + "# the branch where django is not setup yet\n", + "# as from_module=None it won't connect to an instance here\n", + "import bionty" + ] + }, { "cell_type": "code", "execution_count": null, @@ -58,7 +74,38 @@ "metadata": {}, "outputs": [], "source": [ - "ln_setup.connect(\"mydata\")" + "ln_setup.connect(\n", + " \"mydata\", _reload_lamindb=False\n", + ") # also test passing _reload_lamindb explicitly" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "86a1cb7c", + "metadata": {}, + "outputs": [], + "source": [ + "# wetlab is not in the (schema) modules of mydata\n", + "with pytest.raises(ModuleWasntConfigured):\n", + " # _check_instance_setup is called inside with from_module=None\n", + " # the branch where django is setup\n", + " import wetlab" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "799740b9", + "metadata": {}, + "outputs": [], + "source": [ + "# wetlab is not in the (schema) modules of mydata\n", + "with pytest.raises(ModuleWasntConfigured):\n", + " # _check_instance_setup is called inside with from_module=\"bionty\"\n", + " # the branch where django is setup\n", + " # in __getattr__ in __init__.py\n", + " bionty.CellType" ] }, { @@ -122,7 +169,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.17" + "version": "3.10.16" }, "vscode": { "interpreter": { diff --git a/lamindb_setup/_check_setup.py b/lamindb_setup/_check_setup.py index b341b4f2..0c2705f5 100644 --- a/lamindb_setup/_check_setup.py +++ b/lamindb_setup/_check_setup.py @@ -2,13 +2,14 @@ import functools import importlib as il +import inspect import os from typing import TYPE_CHECKING from lamin_utils import logger from ._silence_loggers import silence_loggers -from .core import django +from .core import django as django_lamin from .core._settings import settings from .core._settings_store import current_instance_settings_file from .core.exceptions import DefaultMessageException @@ -33,8 +34,12 @@ class InstanceNotSetupError(DefaultMessageException): IS_LOADING: bool = False +class ModuleWasntConfigured(SystemExit): + pass + + # decorator to disable auto-connect when importing a module such as lamindb -def _loading(func: Callable): +def disable_auto_connect(func: Callable): @functools.wraps(func) def wrapper(*args, **kwargs): global IS_LOADING @@ -70,14 +75,57 @@ def _get_current_instance_settings() -> InstanceSettings | None: return None +# checks that the provided modules is in the modules of the provided instance +# or in the apps setup by django +def _check_module_in_instance_modules( + module: str, isettings: InstanceSettings | None = None +) -> None: + not_in_instance_msg = ( + f"'{module}' is missing from this instance. " + "Please go to your instance settings page and add it under 'schema modules'." + ) + + if isettings is not None: + if module not in isettings.modules: + raise ModuleWasntConfigured(not_in_instance_msg) + else: + return + + from django.apps import apps + + for app in apps.get_app_configs(): + if module == app.name: + return + raise ModuleWasntConfigured(not_in_instance_msg) + + +# infer the name of the module that calls this function +def _infer_callers_module_name() -> str | None: + stack = inspect.stack() + if len(stack) < 3: + return None + module = inspect.getmodule(stack[2][0]) + return module.__name__.partition(".")[0] if module is not None else None + + # we make this a private function because in all the places it's used, # users should not see it def _check_instance_setup(from_module: str | None = None) -> bool: - if django.IS_SETUP: + if django_lamin.IS_SETUP: # reload logic here because module might not yet have been imported # upon first setup - if from_module is not None and from_module != "lamindb": - il.reload(il.import_module(from_module)) + if from_module is not None: + if from_module != "lamindb": + _check_module_in_instance_modules(from_module) + il.reload(il.import_module(from_module)) + else: + infer_module = _infer_callers_module_name() + if infer_module is not None and infer_module not in { + "lamindb", + "lamindb_setup", + "lamin_cli", + }: + _check_module_in_instance_modules(infer_module) return True silence_loggers() if os.environ.get("LAMINDB_MULTI_INSTANCE") == "true": @@ -91,17 +139,19 @@ def _check_instance_setup(from_module: str | None = None) -> bool: if ( from_module is not None and settings.auto_connect - and not django.IS_SETUP + and not django_lamin.IS_SETUP and not IS_LOADING ): - if not from_module == "lamindb": + if from_module != "lamindb": + _check_module_in_instance_modules(from_module, isettings) + import lamindb il.reload(il.import_module(from_module)) else: - django.setup_django(isettings) + django_lamin.setup_django(isettings) logger.important(f"connected lamindb: {isettings.slug}") - return django.IS_SETUP + return django_lamin.IS_SETUP else: if from_module is not None and settings.auto_connect: logger.warning(InstanceNotSetupError.default_message) diff --git a/lamindb_setup/_connect_instance.py b/lamindb_setup/_connect_instance.py index 922e1347..69861a35 100644 --- a/lamindb_setup/_connect_instance.py +++ b/lamindb_setup/_connect_instance.py @@ -312,7 +312,8 @@ def connect(instance: str | None = None, **kwargs) -> str | tuple | None: load_from_isettings(isettings, user=_user, write_settings=_write_settings) if _reload_lamindb: importlib.reload(importlib.import_module("lamindb")) - logger.important(f"connected lamindb: {isettings.slug}") + else: + logger.important(f"connected lamindb: {isettings.slug}") except Exception as e: if isettings is not None: if _write_settings: diff --git a/lamindb_setup/_migrate.py b/lamindb_setup/_migrate.py index 102a22ee..234e18d5 100644 --- a/lamindb_setup/_migrate.py +++ b/lamindb_setup/_migrate.py @@ -5,7 +5,7 @@ from lamin_utils import logger from packaging import version -from ._check_setup import _check_instance_setup, _loading +from ._check_setup import _check_instance_setup, disable_auto_connect from .core._settings import settings from .core.django import setup_django @@ -62,7 +62,7 @@ class migrate: """ @classmethod - @_loading + @disable_auto_connect def create(cls) -> None: """Create a migration.""" if _check_instance_setup(): @@ -70,7 +70,7 @@ def create(cls) -> None: setup_django(settings.instance, create_migrations=True) @classmethod - @_loading + @disable_auto_connect def deploy(cls) -> None: """Deploy a migration.""" from ._schema_metadata import update_schema_in_hub @@ -115,7 +115,7 @@ def deploy(cls) -> None: ) @classmethod - @_loading + @disable_auto_connect def check(cls) -> bool: """Check whether Registry definitions are in sync with migrations.""" from django.core.management import call_command @@ -132,7 +132,7 @@ def check(cls) -> bool: return True @classmethod - @_loading + @disable_auto_connect def squash( cls, package_name, migration_nr, start_migration_nr: str | None = None ) -> None: @@ -148,7 +148,7 @@ def squash( call_command("squashmigrations", package_name, migration_nr) @classmethod - @_loading + @disable_auto_connect def show(cls) -> None: """Show migrations.""" from django.core.management import call_command diff --git a/lamindb_setup/core/_settings_instance.py b/lamindb_setup/core/_settings_instance.py index c6cb0bd1..89f1ca12 100644 --- a/lamindb_setup/core/_settings_instance.py +++ b/lamindb_setup/core/_settings_instance.py @@ -466,13 +466,11 @@ def _persist(self, write_to_disk: bool = True) -> None: settings._instance_settings = self def _init_db(self): - from lamindb_setup import _check_setup + from lamindb_setup._check_setup import disable_auto_connect from .django import setup_django - _check_setup.IS_LOADING = True - setup_django(self, init=True) - _check_setup.IS_LOADING = False + disable_auto_connect(setup_django)(self, init=True) from lamindb.models import Space @@ -482,8 +480,6 @@ def _init_db(self): ) def _load_db(self) -> tuple[bool, str]: - from lamindb_setup import _check_setup - # Is the database available and initialized as LaminDB? # returns a tuple of status code and message if self.dialect == "sqlite" and not self._sqlite_file.exists(): @@ -494,15 +490,15 @@ def _load_db(self) -> tuple[bool, str]: f" {legacy_file} to {self._sqlite_file}" ) return False, f"SQLite file {self._sqlite_file} does not exist" - - from .django import setup_django - # we need the local sqlite to setup django self._update_local_sqlite_file() # setting up django also performs a check for migrations & prints them # as warnings # this should fail, e.g., if the db is not reachable - _check_setup.IS_LOADING = True - setup_django(self) - _check_setup.IS_LOADING = False + from lamindb_setup._check_setup import disable_auto_connect + + from .django import setup_django + + disable_auto_connect(setup_django)(self) + return True, ""