Skip to content

Commit ddd9eb9

Browse files
committed
fix(plugins): preserve PluginRegistry across DATALAB_PLUGINS unit tests (#311)
1 parent 9208d15 commit ddd9eb9

2 files changed

Lines changed: 88 additions & 53 deletions

File tree

datalab/config.py

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from __future__ import annotations
1111

12+
import logging
1213
import os
1314
import os.path as osp
1415
import sys
@@ -118,29 +119,53 @@ def is_frozen(module_name: str) -> bool:
118119
#: (subset of :data:`OTHER_PLUGINS_PATHLIST`, kept around so that consumers
119120
#: such as the plugin configuration dialog can flag them as user-provided).
120121
DATALAB_PLUGINS_ENV_PATHS: list[str] = []
121-
_env_plugins = os.environ.get(DATALAB_PLUGINS_ENV_VAR, "")
122-
if _env_plugins:
123-
import logging as _logging # local import to avoid polluting module namespace
124-
125-
_logger = _logging.getLogger(__name__)
126-
for _raw_path in _env_plugins.split(os.pathsep):
127-
_path = _raw_path.strip()
128-
if not _path:
122+
123+
124+
def parse_datalab_plugins_env_var(
125+
env_value: str | None,
126+
pathlist: list[str],
127+
env_paths: list[str],
128+
) -> None:
129+
"""Parse ``DATALAB_PLUGINS`` and append valid directories to ``pathlist``.
130+
131+
Args:
132+
env_value: Raw value of the ``DATALAB_PLUGINS`` environment variable
133+
(``None`` or empty string is a no-op).
134+
pathlist: Plugin search path list to extend in-place
135+
(typically :data:`OTHER_PLUGINS_PATHLIST`).
136+
env_paths: List of env-var-provided directories to extend in-place
137+
(typically :data:`DATALAB_PLUGINS_ENV_PATHS`), used by the GUI to
138+
flag entries originating from the environment variable.
139+
"""
140+
if not env_value:
141+
return
142+
143+
logger = logging.getLogger(__name__)
144+
for raw_path in env_value.split(os.pathsep):
145+
path = raw_path.strip()
146+
if not path:
129147
continue
130-
_path = osp.normpath(osp.expanduser(_path))
131-
if osp.isdir(_path):
132-
if _path not in OTHER_PLUGINS_PATHLIST:
133-
OTHER_PLUGINS_PATHLIST.append(_path)
134-
if _path not in DATALAB_PLUGINS_ENV_PATHS:
135-
DATALAB_PLUGINS_ENV_PATHS.append(_path)
148+
path = osp.normpath(osp.expanduser(path))
149+
if osp.isdir(path):
150+
if path not in pathlist:
151+
pathlist.append(path)
152+
if path not in env_paths:
153+
env_paths.append(path)
136154
else:
137-
_logger.warning(
155+
logger.warning(
138156
"%s: ignoring non-existent plugin directory '%s'",
139157
DATALAB_PLUGINS_ENV_VAR,
140-
_path,
158+
path,
141159
)
142160

143161

162+
parse_datalab_plugins_env_var(
163+
os.environ.get(DATALAB_PLUGINS_ENV_VAR),
164+
OTHER_PLUGINS_PATHLIST,
165+
DATALAB_PLUGINS_ENV_PATHS,
166+
)
167+
168+
144169
def get_mod_source_dir() -> str | None:
145170
"""Return module source directory
146171

datalab/tests/backbone/plugins_envvar_unit_test.py

Lines changed: 47 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,67 +7,72 @@
77
into entries of :data:`datalab.config.OTHER_PLUGINS_PATHLIST` and verifies
88
that :func:`datalab.plugins.discover_plugins` actually finds plugin modules
99
placed in those directories.
10+
11+
The parsing function is tested directly (without reloading
12+
``datalab.config``) to avoid corrupting global singletons (``Conf``,
13+
``PluginRegistry``, the ``PluginBase`` metaclass) shared with the rest of
14+
the test session.
1015
"""
1116

1217
# guitest: skip
1318

1419
from __future__ import annotations
1520

16-
import importlib
1721
import os
1822
import sys
1923
import textwrap
2024
from pathlib import Path
2125

2226
import pytest
2327

28+
from datalab import config as config_mod
29+
from datalab import plugins as plugins_mod
2430

25-
def _reload_config(monkeypatch: pytest.MonkeyPatch, env_value: str | None) -> object:
26-
"""Reload ``datalab.config`` with a controlled ``DATALAB_PLUGINS`` value."""
27-
if env_value is None:
28-
monkeypatch.delenv("DATALAB_PLUGINS", raising=False)
29-
else:
30-
monkeypatch.setenv("DATALAB_PLUGINS", env_value)
31-
import datalab.config as config_mod
3231

33-
return importlib.reload(config_mod)
32+
def test_env_var_unset_leaves_default_paths() -> None:
33+
"""When the env var is empty/unset, the path list is left untouched."""
34+
pathlist: list[str] = ["/initial/path"]
35+
env_paths: list[str] = []
3436

37+
config_mod.parse_datalab_plugins_env_var(None, pathlist, env_paths)
38+
config_mod.parse_datalab_plugins_env_var("", pathlist, env_paths)
3539

36-
def test_env_var_unset_leaves_default_paths(monkeypatch: pytest.MonkeyPatch) -> None:
37-
"""When the env var is unset, the default plugin path list is unchanged."""
38-
config_mod = _reload_config(monkeypatch, None)
39-
assert config_mod.OTHER_PLUGINS_PATHLIST # at least the bundled directory
40+
assert pathlist == ["/initial/path"]
41+
assert env_paths == []
4042

4143

42-
def test_env_var_adds_existing_directories(
43-
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
44-
) -> None:
45-
"""Existing directories listed in the env var are appended to the path list."""
44+
def test_env_var_adds_existing_directories(tmp_path: Path) -> None:
45+
"""Existing directories listed in the env var are appended to both lists."""
4646
dir1 = tmp_path / "plugins_a"
4747
dir2 = tmp_path / "plugins_b"
4848
dir1.mkdir()
4949
dir2.mkdir()
5050
env_value = os.pathsep.join([str(dir1), str(dir2)])
5151

52-
config_mod = _reload_config(monkeypatch, env_value)
53-
paths = [os.path.normpath(p) for p in config_mod.OTHER_PLUGINS_PATHLIST]
54-
assert os.path.normpath(str(dir1)) in paths
55-
assert os.path.normpath(str(dir2)) in paths
52+
pathlist: list[str] = []
53+
env_paths: list[str] = []
54+
config_mod.parse_datalab_plugins_env_var(env_value, pathlist, env_paths)
55+
56+
expected = [os.path.normpath(str(dir1)), os.path.normpath(str(dir2))]
57+
assert [os.path.normpath(p) for p in pathlist] == expected
58+
assert [os.path.normpath(p) for p in env_paths] == expected
5659

5760

5861
def test_env_var_skips_missing_directories(
59-
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, caplog: pytest.LogCaptureFixture
62+
tmp_path: Path, caplog: pytest.LogCaptureFixture
6063
) -> None:
61-
"""Missing directories are skipped silently and a warning is logged."""
64+
"""Missing directories are skipped and a warning is logged."""
6265
existing = tmp_path / "exists"
6366
existing.mkdir()
6467
missing = tmp_path / "does_not_exist"
6568
env_value = os.pathsep.join([str(existing), str(missing), " "])
6669

70+
pathlist: list[str] = []
71+
env_paths: list[str] = []
6772
with caplog.at_level("WARNING", logger="datalab.config"):
68-
config_mod = _reload_config(monkeypatch, env_value)
73+
config_mod.parse_datalab_plugins_env_var(env_value, pathlist, env_paths)
6974

70-
paths = [os.path.normpath(p) for p in config_mod.OTHER_PLUGINS_PATHLIST]
75+
paths = [os.path.normpath(p) for p in pathlist]
7176
assert os.path.normpath(str(existing)) in paths
7277
assert os.path.normpath(str(missing)) not in paths
7378
assert any("does_not_exist" in r.message for r in caplog.records)
@@ -76,7 +81,12 @@ def test_env_var_skips_missing_directories(
7681
def test_discover_plugins_finds_module_in_env_dir(
7782
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
7883
) -> None:
79-
"""``datalab_*`` module in a ``DATALAB_PLUGINS`` directory is discovered."""
84+
"""A ``datalab_*`` module in an env-var directory is discovered.
85+
86+
``OTHER_PLUGINS_PATHLIST`` is mutated in-place (and restored after) to
87+
avoid reloading ``datalab.config`` or ``datalab.plugins``, which would
88+
invalidate the ``PluginBase`` metaclass and break later tests.
89+
"""
8090
plugin_dir = tmp_path / "extra_plugins"
8191
plugin_dir.mkdir()
8292
plugin_name = "datalab_envvar_discovery_probe"
@@ -91,22 +101,22 @@ def test_discover_plugins_finds_module_in_env_dir(
91101
encoding="utf-8",
92102
)
93103

94-
monkeypatch.setenv("DATALAB_PLUGINS", str(plugin_dir))
95-
# Ensure config picks up the env var, then reload plugins module so it
96-
# imports the refreshed OTHER_PLUGINS_PATHLIST.
97-
importlib.reload(importlib.import_module("datalab.config"))
98-
plugins_mod = importlib.reload(importlib.import_module("datalab.plugins"))
99-
100-
# Make sure plugins are enabled for discovery
101-
from datalab.config import Conf
102-
103-
monkeypatch.setattr(Conf.main.plugins_enabled, "get", lambda *a, **kw: True)
104-
104+
extra_path = str(plugin_dir)
105+
config_mod.OTHER_PLUGINS_PATHLIST.append(extra_path)
106+
sys.modules.pop(plugin_name, None)
105107
try:
108+
# Ensure plugins are considered enabled regardless of user config
109+
monkeypatch.setattr(
110+
config_mod.Conf.main.plugins_enabled, "get", lambda *a, **kw: True
111+
)
106112
modules = plugins_mod.discover_plugins()
107113
discovered_names = {m.__name__ for m in modules}
108114
assert plugin_name in discovered_names
109115
finally:
116+
try:
117+
config_mod.OTHER_PLUGINS_PATHLIST.remove(extra_path)
118+
except ValueError:
119+
pass
110120
sys.modules.pop(plugin_name, None)
111121

112122

0 commit comments

Comments
 (0)