Skip to content

🚸 Set up Django on every import of lamindb via a "mock instance" #1063

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 41 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
af6b699
🚸 Always connect
falexwolf Jun 16, 2025
4b1f6eb
💚 Fixes
falexwolf Jun 16, 2025
7c9e90e
Merge branch 'main' into alwaysconnect
falexwolf Jun 16, 2025
33f5962
💚 Fix
falexwolf Jun 16, 2025
bd2959f
💚 Fix
falexwolf Jun 16, 2025
be500f9
♻️ Changes
falexwolf Jun 16, 2025
ee18546
💚 Fixes
falexwolf Jun 17, 2025
7784168
🐛 Auto-connect is always True now
falexwolf Jun 17, 2025
c5ea1d9
Merge branch 'main' into alwaysconnect
falexwolf Jun 30, 2025
0ed9adb
♻️ Also reset variables upon lamin init
falexwolf Jun 30, 2025
b917568
💚 Mock instance needs to have all modules
falexwolf Jul 1, 2025
93599e2
💚 Fixes
falexwolf Jul 1, 2025
8e8e930
🚧 Try outs along the lines of not setting up Django using lamindb as …
falexwolf Jul 1, 2025
8057031
Revert "🚧 Try outs along the lines of not setting up Django using lam…
falexwolf Jul 1, 2025
655f869
♻️ Configure apps_ready
falexwolf Jul 1, 2025
386f6ae
Merge branch 'main' into alwaysconnect
falexwolf Jul 1, 2025
7b42678
🚸 Raise CurrentInstanceNotConfigured error
falexwolf Jul 1, 2025
23ac680
♻️ Fix issue
falexwolf Jul 1, 2025
fad092a
💚 Fix
falexwolf Jul 1, 2025
31c3ea5
💚 Fix
falexwolf Jul 1, 2025
17cd576
📝 Document error
falexwolf Jul 1, 2025
7523b97
♻️ Also account for confest module
falexwolf Jul 1, 2025
6b7fd9a
♻️ Add a warning about auto-connect
falexwolf Jul 1, 2025
d6fcf72
♻️ Remove switching auto-connect in init
falexwolf Jul 1, 2025
233b829
♻️ We no longer need to gate on auto-connect
falexwolf Jul 1, 2025
eacbf28
♻️ Update is_connected()
falexwolf Jul 1, 2025
5b91fd8
🚸 Prettier error message
falexwolf Jul 1, 2025
57b6c7f
♻️ Move logging into more meaningful spot
falexwolf Jul 2, 2025
e9702c9
💚 Fix test
falexwolf Jul 2, 2025
00d6cef
💚 Fix
falexwolf Jul 2, 2025
dbce392
♻️ Auto-detect candidate modules
falexwolf Jul 2, 2025
ea8ffda
♻️ Refactor
falexwolf Jul 2, 2025
5b3b2a0
💚 Fix
falexwolf Jul 2, 2025
4f96599
💚 Fix
falexwolf Jul 2, 2025
a943bcc
♻️ Also update classes
falexwolf Jul 2, 2025
5db764a
Merge branch 'main' into alwaysconnect
falexwolf Jul 6, 2025
6b57b26
✨ Allow re-connecting through re-configuring Django in the same Pytho…
falexwolf Jul 6, 2025
afc7711
Merge branch 'main' into alwaysconnect
falexwolf Jul 20, 2025
5f05262
💚 Fix
falexwolf Jul 20, 2025
b2a1f92
🚨 Fix
falexwolf Jul 20, 2025
bd871f0
Merge branch 'main' into alwaysconnect
falexwolf Jul 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 10 additions & 37 deletions docs/hub-cloud/08-test-multi-session.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@
"ln_setup.init(storage=\"./testsetup\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"assert ln_setup.settings.instance.slug == \"testuser1/testsetup\""
]
},
{
"cell_type": "code",
"execution_count": null,
Expand Down Expand Up @@ -111,43 +120,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"Let us try to init another instance in the same Python session:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from lamindb_setup._init_instance import CannotSwitchDefaultInstance\n",
"\n",
"with pytest.raises(CannotSwitchDefaultInstance):\n",
" ln_setup.init(storage=\"./testsetup2\")\n",
"with pytest.raises(CannotSwitchDefaultInstance):\n",
" ln_setup.connect(\"testsetup\")\n",
"with pytest.raises(RuntimeError):\n",
" ln_setup.migrate.create()\n",
"with pytest.raises(RuntimeError):\n",
" ln_setup.migrate.deploy()\n",
"\n",
"assert ln_setup.settings.instance.slug == \"testuser1/testsetup\""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Reset `django` and connect to another instance:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"ln_setup.core.django.reset_django()"
"Connect to another instance in the same process:"
]
},
{
Expand Down
6 changes: 3 additions & 3 deletions lamindb_setup/_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from .core._settings_save import save_platform_user_storage_settings
from .core._settings_store import system_settings_file
from .errors import CurrentInstanceNotConfigured


def clear_cache_dir():
Expand All @@ -19,9 +20,8 @@ def clear_cache_dir():
"disconnecting the current instance to update the cloud sqlite database."
)
disconnect()
except SystemExit as e:
if str(e) != "No instance connected! Call `lamin connect` or `lamin init`":
raise e
except CurrentInstanceNotConfigured:
pass

cache_dir = settings.cache_dir
if cache_dir.exists():
Expand Down
49 changes: 38 additions & 11 deletions lamindb_setup/_check_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import importlib as il
import inspect
import os
from importlib.metadata import distributions
from typing import TYPE_CHECKING
from uuid import UUID

from lamin_utils import logger

Expand All @@ -25,6 +27,7 @@


CURRENT_ISETTINGS: InstanceSettings | None = None
MODULE_CANDIDATES: set[str] | None = None
IS_LOADING: bool = False


Expand All @@ -42,7 +45,25 @@ def wrapper(*args, **kwargs):
return wrapper


def _get_current_instance_settings() -> InstanceSettings | None:
def find_module_candidates():
"""Find all local packages that depend on lamindb."""
global MODULE_CANDIDATES
if MODULE_CANDIDATES is not None:
return MODULE_CANDIDATES
all_dists = list(distributions())
lamindb_deps = {
dist.metadata["Name"].lower()
for dist in all_dists
if dist.requires and any("lamindb" in req.lower() for req in dist.requires)
}
lamindb_deps.remove("lamindb")
MODULE_CANDIDATES = lamindb_deps
return lamindb_deps


def _get_current_instance_settings(from_module: str | None = None) -> InstanceSettings:
from .core._settings_instance import InstanceSettings

global CURRENT_ISETTINGS

if CURRENT_ISETTINGS is not None:
Expand All @@ -60,9 +81,17 @@ def _get_current_instance_settings() -> InstanceSettings | None:
" command line: `lamin connect <instance>` or `lamin init <...>`"
)
raise e
return isettings
else:
return None
module_candidates = find_module_candidates()
isettings = InstanceSettings(
id=UUID("00000000-0000-0000-0000-000000000000"),
owner="none",
name="none",
storage=None,
modules=",".join(module_candidates),
)
CURRENT_ISETTINGS = isettings
return isettings


def _normalize_module_name(module_name: str) -> str:
Expand Down Expand Up @@ -132,12 +161,7 @@ def _check_instance_setup(from_module: str | None = None) -> bool:
return True
isettings = _get_current_instance_settings()
if isettings is not None:
if (
from_module is not None
and settings.auto_connect
and not django_lamin.IS_SETUP
and not IS_LOADING
):
if from_module is not None and not django_lamin.IS_SETUP and not IS_LOADING:
if from_module != "lamindb":
_check_module_in_instance_modules(from_module, isettings)

Expand All @@ -146,10 +170,13 @@ def _check_instance_setup(from_module: str | None = None) -> bool:
il.reload(il.import_module(from_module))
else:
django_lamin.setup_django(isettings)
logger.important(f"connected lamindb: {isettings.slug}")
if isettings.slug != "none/none":
logger.important(f"connected lamindb: {isettings.slug}")
else:
logger.warning("not connected, call: ln.connect('account/name')")
return django_lamin.IS_SETUP
else:
if from_module is not None and settings.auto_connect:
if from_module is not None:
# the below enables users to auto-connect to an instance
# simply by setting an environment variable, bypassing the
# need of calling connect() manually
Expand Down
74 changes: 59 additions & 15 deletions lamindb_setup/_connect_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@

import importlib
import os
import sys
from typing import TYPE_CHECKING, Any
from uuid import UUID

from lamin_utils import logger

from ._check_setup import _check_instance_setup, _get_current_instance_settings
from ._disconnect import disconnect
from ._init_instance import (
MESSAGE_CANNOT_SWITCH_DEFAULT_INSTANCE,
load_from_isettings,
from ._check_setup import (
_check_instance_setup,
_get_current_instance_settings,
find_module_candidates,
)
from ._disconnect import disconnect
from ._init_instance import load_from_isettings
from ._silence_loggers import silence_loggers
from .core._hub_core import connect_instance_hub
from .core._hub_utils import (
Expand Down Expand Up @@ -188,6 +190,51 @@ def _connect_instance(
return isettings


def reset_django_module_variables():
import types

from django.apps import apps

app_names = {app.name for app in apps.get_app_configs()}

for name, module in sys.modules.items():
if (
module is not None
and (not name.startswith("__") or name == "__main__")
and name not in sys.builtin_module_names
and not (
hasattr(module, "__file__")
and module.__file__
and any(
path in module.__file__ for path in ["/lib/python", "\\lib\\python"]
)
)
):
try:
for k, v in vars(module).items():
if (
isinstance(v, types.ModuleType)
and not k.startswith("_")
and getattr(v, "__name__", None) in app_names
):
if v.__name__ in sys.modules:
vars(module)[k] = sys.modules[v.__name__]
# Also reset classes from Django apps - but check if the class module starts with any app name
elif hasattr(v, "__module__") and getattr(v, "__module__", None):
class_module = v.__module__
# Check if the class module starts with any of our app names
if any(
class_module.startswith(app_name) for app_name in app_names
):
if class_module in sys.modules:
fresh_module = sys.modules[class_module]
attr_name = getattr(v, "__name__", k)
if hasattr(fresh_module, attr_name):
vars(module)[k] = getattr(fresh_module, attr_name)
except (AttributeError, TypeError):
continue


def _connect_cli(instance: str) -> None:
from lamindb_setup import settings as settings_

Expand Down Expand Up @@ -244,12 +291,7 @@ def connect(instance: str | None = None, **kwargs: Any) -> str | tuple | None:

try:
if instance is None:
isettings_or_none = _get_current_instance_settings()
if isettings_or_none is None:
raise ValueError(
"No instance was connected through the CLI, pass a value to `instance` or connect via the CLI."
)
isettings = isettings_or_none
isettings = _get_current_instance_settings()
else:
owner, name = get_owner_name_from_identifier(instance)
if _check_instance_setup() and not _test:
Expand All @@ -260,9 +302,9 @@ def connect(instance: str | None = None, **kwargs: Any) -> str | tuple | None:
logger.important(f"connected lamindb: {settings.instance.slug}")
return None
else:
raise CannotSwitchDefaultInstance(
MESSAGE_CANNOT_SWITCH_DEFAULT_INSTANCE
)
from lamindb_setup.core.django import reset_django

reset_django()
elif (
_write_settings
and settings._instance_exists
Expand Down Expand Up @@ -315,7 +357,9 @@ def connect(instance: str | None = None, **kwargs: Any) -> 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}")
reset_django_module_variables()
if isettings.slug != "none/none":
logger.important(f"connected lamindb: {isettings.slug}")
except Exception as e:
if isettings is not None:
if _write_settings:
Expand Down
20 changes: 5 additions & 15 deletions lamindb_setup/_init_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,16 +215,6 @@ def validate_init_args(
return name_str, instance_id, instance_state, instance_slug


MESSAGE_CANNOT_SWITCH_DEFAULT_INSTANCE = """
You cannot write to different instances in the same Python session.

Do you want to read from another instance via `SQLRecord.using()`? For example:

ln.Artifact.using("laminlabs/cellxgene").filter()

Or do you want to switch off auto-connect via `lamin settings set auto-connect false`?
"""

DOC_STORAGE_ARG = "A local or remote folder (`'s3://...'` or `'gs://...'`). Defaults to current working directory."
DOC_INSTANCE_NAME = (
"Instance name. If not passed, it will equal the folder name passed to `storage`."
Expand Down Expand Up @@ -272,9 +262,12 @@ def init(
from ._check_setup import _check_instance_setup

if _check_instance_setup() and not _test:
raise CannotSwitchDefaultInstance(MESSAGE_CANNOT_SWITCH_DEFAULT_INSTANCE)
from lamindb_setup.core.django import reset_django

reset_django()
elif _write_settings:
disconnect(mute=True)
from ._connect_instance import reset_django_module_variables
from .core._hub_core import init_instance_hub

name_str, instance_id, instance_state, _ = validate_init_args(
Expand All @@ -287,8 +280,6 @@ def init(
_user=_user, # will get from settings.user if _user is None
)
if instance_state == "connected":
if _write_settings:
settings.auto_connect = True # we can also debate this switch here
return None
prevent_register_hub = is_local_db_url(db) if db is not None else False
ssettings, _ = init_storage(
Expand Down Expand Up @@ -344,9 +335,8 @@ def init(
from ._schema_metadata import update_schema_in_hub

update_schema_in_hub(access_token=access_token)
if _write_settings:
settings.auto_connect = True
importlib.reload(importlib.import_module("lamindb"))
reset_django_module_variables()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't think it should always reset, it should do the reset only on re-connect. Also not clear why importlib.reload deosn't do this already.

logger.important(f"initialized lamindb: {isettings.slug}")
except Exception as e:
from ._delete import delete_by_isettings
Expand Down
17 changes: 12 additions & 5 deletions lamindb_setup/core/_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

import os
import sys
import warnings
from typing import TYPE_CHECKING

from lamin_utils import logger
from platformdirs import user_cache_dir

from lamindb_setup.errors import CurrentInstanceNotConfigured

from ._settings_load import (
load_cache_path_from_settings,
load_instance_settings,
Expand Down Expand Up @@ -84,10 +87,13 @@ def auto_connect(self) -> bool:
- in Python: `ln.setup.settings.auto_connect = True/False`
- via the CLI: `lamin settings set auto-connect true/false`
"""
return self._auto_connect_path.exists()
return True

@auto_connect.setter
def auto_connect(self, value: bool) -> None:
logger.warning(
"setting auto_connect to `False` no longer has an effect and the setting will likely be removed in the future; since lamindb 1.7, auto_connect `True` no longer clashes with connecting in a Python session",
)
if value:
self._auto_connect_path.touch()
else:
Expand Down Expand Up @@ -188,9 +194,10 @@ def is_connected(self) -> bool:
If `True`, the current instance is connected, meaning that the db and other settings
are properly configured for use.
"""
from .django import IS_SETUP # always import to protect from assignment

return IS_SETUP
if self._instance_exists:
return self.instance.slug != "none/none"
else:
return False

@property
def private_django_api(self) -> bool:
Expand Down Expand Up @@ -253,7 +260,7 @@ def _instance_exists(self):
self.instance # noqa
return True
# this is implicit logic that catches if no instance is loaded
except SystemExit:
except CurrentInstanceNotConfigured:
return False

@property
Expand Down
Loading