Skip to content

Commit cbd3ded

Browse files
committed
feat: Add runtime warnings for loader path configuration in embedded mode
1 parent 10ec6e1 commit cbd3ded

8 files changed

Lines changed: 174 additions & 5 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ before Python starts. `iris.connect(path=...)` can configure Python import paths
114114
at runtime, but it cannot repair Unix dynamic loader resolution after the
115115
process has already started. If `pythonint` is found but its dependent shared
116116
libraries are not, the runtime error names the loader-path variable that must
117+
include the IRIS `bin` directory. When embedded-local is configured through
118+
`IRISINSTALLDIR`, `ISC_PACKAGE_INSTALLDIR`, or `path=...`, the wrapper also
119+
emits a `RuntimeWarning` on Unix if that loader-path variable does not already
117120
include the IRIS `bin` directory.
118121

119122
#### Linux and macOS

_iris_ep/_bootstrap.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import importlib
22
import os
33
import sys
4+
import warnings
45

56
from iris_utils import update_dynalib_path
67

8+
_LOADER_PATH_WARNINGS_EMITTED = set()
79
_SHARED_LIBRARY_ERROR_MARKERS = (
810
"cannot open shared object file",
911
"dlopen(",
@@ -35,7 +37,51 @@ def _push_sys_paths_front(paths):
3537
return original
3638

3739

38-
def configure_install_dir(path):
40+
def _env_path_contains(env_var, path):
41+
target = os.path.normcase(os.path.realpath(os.fspath(path)))
42+
current_paths = os.environ.get(env_var, "")
43+
for entry in current_paths.split(os.pathsep):
44+
if not entry:
45+
continue
46+
candidate = os.path.normcase(os.path.realpath(entry))
47+
if candidate == target:
48+
return True
49+
return False
50+
51+
52+
def format_loader_path_warning(install_dir):
53+
bin_dir = os.path.join(install_dir, 'bin')
54+
env_var = get_loader_path_env_var()
55+
return (
56+
f"IRIS embedded-local loading may fail because {env_var} does not "
57+
f"include {bin_dir}. Set {env_var} to include the IRIS bin directory "
58+
f"before Python starts; changing it after startup may be too late for "
59+
f"the dynamic loader."
60+
)
61+
62+
63+
def warn_if_loader_path_unconfigured(install_dir):
64+
if sys.platform.startswith('win'):
65+
return
66+
67+
bin_dir = os.path.join(install_dir, 'bin')
68+
env_var = get_loader_path_env_var()
69+
if _env_path_contains(env_var, bin_dir):
70+
return
71+
72+
warning_key = (env_var, os.path.normcase(os.path.realpath(bin_dir)))
73+
if warning_key in _LOADER_PATH_WARNINGS_EMITTED:
74+
return
75+
_LOADER_PATH_WARNINGS_EMITTED.add(warning_key)
76+
77+
warnings.warn(
78+
format_loader_path_warning(install_dir),
79+
RuntimeWarning,
80+
stacklevel=3,
81+
)
82+
83+
84+
def configure_install_dir(path, *, warn_loader_path=False):
3985
if not path:
4086
raise ValueError("path must be a non-empty IRIS installation directory")
4187

@@ -59,6 +105,9 @@ def configure_install_dir(path):
59105
_append_sys_path(bin_dir)
60106
_append_sys_path(python_dir)
61107

108+
if warn_loader_path:
109+
warn_if_loader_path_unconfigured(install_dir)
110+
62111
update_dynalib_path(bin_dir)
63112
return install_dir
64113

_iris_ep/_runtime_facade.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,10 @@ def configure_from_environment(self) -> None:
235235
logging.warning("IRISINSTALLDIR or ISC_PACKAGE_INSTALLDIR environment variable is not set")
236236
logging.warning("Embedded Python not configured; call iris.connect(path=...) to configure it")
237237
else:
238-
_bootstrap.configure_install_dir(installdir)
238+
_bootstrap.configure_install_dir(
239+
installdir,
240+
warn_loader_path=not _bootstrap.is_embedded_kernel(),
241+
)
239242

240243
if _bootstrap.is_embedded_kernel():
241244
module = _bootstrap.import_embedded_kernel_module()
@@ -348,7 +351,7 @@ def sync_public_modules(self):
348351
setattr(module, "__getattr__", self.module_globals["__getattr__"])
349352

350353
def load_embedded_backend(self, path):
351-
install_dir = _bootstrap.configure_install_dir(path)
354+
install_dir = _bootstrap.configure_install_dir(path, warn_loader_path=True)
352355
module = _bootstrap.import_pythonint_module_from_install_dir(install_dir)
353356
self.install_embedded_module(module)
354357
return self.runtime_manager.configure(mode='embedded', install_dir=install_dir)

docs/index.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ Set the following environment variables :
3030
Embedded-local execution from regular `python3` on Unix needs the loader path
3131
configured before Python starts. `iris.connect(path=...)` can configure Python
3232
import paths at runtime, but it cannot repair Unix dynamic loader resolution
33-
after startup.
33+
after startup. When embedded-local is configured through `IRISINSTALLDIR`,
34+
`ISC_PACKAGE_INSTALLDIR`, or `path=...`, the wrapper emits a `RuntimeWarning`
35+
on Unix if the loader-path variable does not already include the IRIS `bin`
36+
directory.
3437

3538
### For Linux and MacOS
3639

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ requires = ["setuptools", "wheel"]
33

44
[project]
55
name = "iris-embedded-python-wrapper"
6-
version = "0.5.17"
6+
version = "0.5.18"
77
description = "Wrapper for embedded python on InterSystems IRIS"
88
readme = "README.md"
99
authors = [

tests/iris/test_dbapi_runtime_modes.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ def _install_fake_embedded_runtime(monkeypatch, tmp_path, fake_statement):
1717
(install_dir / "bin").mkdir(parents=True)
1818
(install_dir / "lib" / "python").mkdir(parents=True)
1919
dynalib_paths = []
20+
if not sys.platform.startswith("win"):
21+
env_var = _iris_ep._bootstrap.get_loader_path_env_var()
22+
monkeypatch.setenv(env_var, str(install_dir / "bin"))
2023

2124
fake_module = types.SimpleNamespace(
2225
__file__=str(install_dir / "bin" / "pythonint.so"),
@@ -341,6 +344,9 @@ def test_dbapi_connect_path_surfaces_loader_path_import_error(monkeypatch, tmp_p
341344
install_dir = tmp_path / "iris"
342345
(install_dir / "bin").mkdir(parents=True)
343346
(install_dir / "lib" / "python").mkdir(parents=True)
347+
if not sys.platform.startswith("win"):
348+
env_var = _iris_ep._bootstrap.get_loader_path_env_var()
349+
monkeypatch.setenv(env_var, str(install_dir / "bin"))
344350

345351
def fake_import_module(name):
346352
raise ImportError("libirisdb.so: cannot open shared object file")

tests/iris/test_import_contracts.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,38 @@ def tracking_import_module(name, *args, **kwargs):
101101
assert "NO_PYTHONINT_PROBE_OK" in result.stdout
102102

103103

104+
@pytest.mark.skipif(sys.platform.startswith("win"), reason="Unix loader-path warning")
105+
@pytest.mark.parametrize("install_env", ["IRISINSTALLDIR", "ISC_PACKAGE_INSTALLDIR"])
106+
def test_import_iris_with_install_env_warns_when_loader_path_wrong(tmp_path, install_env):
107+
install_dir = tmp_path / "iris"
108+
other_dir = tmp_path / "other"
109+
(install_dir / "bin").mkdir(parents=True)
110+
(install_dir / "lib" / "python").mkdir(parents=True)
111+
other_dir.mkdir()
112+
env_var = "DYLD_LIBRARY_PATH" if sys.platform == "darwin" else "LD_LIBRARY_PATH"
113+
114+
result = _fresh_python(
115+
"""
116+
import warnings
117+
118+
warnings.simplefilter("always", RuntimeWarning)
119+
import iris
120+
121+
assert iris.runtime.get().install_dir
122+
print("INSTALL_ENV_LOADER_WARNING_OK")
123+
""",
124+
tmp_path,
125+
extra_env={
126+
install_env: str(install_dir),
127+
env_var: str(other_dir),
128+
},
129+
)
130+
131+
_assert_fresh_python_ok(result)
132+
assert "INSTALL_ENV_LOADER_WARNING_OK" in result.stdout
133+
assert f"{env_var} does not include {install_dir / 'bin'}" in result.stderr
134+
135+
104136
def test_native_dbapi_import_contract_preserves_wrapper_parent(tmp_path):
105137
try:
106138
importlib.metadata.distribution("intersystems-irispython")

tests/iris/test_iris.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ def _restore_module_attrs(snapshot):
3636
else:
3737
setattr(module, name, value)
3838

39+
40+
def _set_loader_path(monkeypatch, install_dir):
41+
if not sys.platform.startswith("win"):
42+
env_var = _iris_ep._bootstrap.get_loader_path_env_var()
43+
monkeypatch.setenv(env_var, str(install_dir / "bin"))
44+
45+
3946
def test_import_iris():
4047
import iris
4148

@@ -101,6 +108,7 @@ def test_connect_path_enables_embedded_runtime(monkeypatch, tmp_path):
101108
(install_dir / "bin").mkdir(parents=True)
102109
(install_dir / "lib" / "python").mkdir(parents=True)
103110
dynalib_paths = []
111+
_set_loader_path(monkeypatch, install_dir)
104112

105113
class FakeVersion:
106114
@staticmethod
@@ -179,6 +187,7 @@ def test_connect_path_reports_loader_path_import_error(monkeypatch, tmp_path):
179187
(install_dir / "bin").mkdir(parents=True)
180188
(install_dir / "lib" / "python").mkdir(parents=True)
181189
dynalib_paths = []
190+
_set_loader_path(monkeypatch, install_dir)
182191

183192
def fake_import_module(name):
184193
raise ImportError("dlopen(pythonint.so): Library not loaded: libirisdb.dylib")
@@ -196,13 +205,74 @@ def fake_import_module(name):
196205
iris.runtime.reset()
197206

198207

208+
@pytest.mark.skipif(sys.platform.startswith("win"), reason="Unix loader-path warning")
209+
def test_connect_path_warns_when_loader_path_missing(monkeypatch, tmp_path):
210+
iris.runtime.reset()
211+
install_dir = tmp_path / "iris"
212+
(install_dir / "bin").mkdir(parents=True)
213+
(install_dir / "lib" / "python").mkdir(parents=True)
214+
env_var = _iris_ep._bootstrap.get_loader_path_env_var()
215+
monkeypatch.delenv(env_var, raising=False)
216+
217+
fake_module = types.SimpleNamespace(
218+
__file__=str(install_dir / "bin" / "pythonint.so"),
219+
cls=lambda name: {"class": name},
220+
connect=lambda *args, **kwargs: {"ok": True},
221+
)
222+
223+
def fake_import_module(name):
224+
if name == "pythonint":
225+
return fake_module
226+
raise ModuleNotFoundError(name)
227+
228+
monkeypatch.setattr(_iris_ep._bootstrap.importlib, "import_module", fake_import_module)
229+
monkeypatch.setattr(_iris_ep._bootstrap, "update_dynalib_path", lambda path: None)
230+
231+
with pytest.warns(RuntimeWarning, match=f"{env_var} does not include"):
232+
iris.connect(path=install_dir)
233+
234+
iris.runtime.reset()
235+
236+
237+
@pytest.mark.skipif(sys.platform.startswith("win"), reason="Unix loader-path warning")
238+
def test_connect_path_warns_when_loader_path_points_elsewhere(monkeypatch, tmp_path):
239+
iris.runtime.reset()
240+
install_dir = tmp_path / "iris"
241+
other_dir = tmp_path / "other"
242+
(install_dir / "bin").mkdir(parents=True)
243+
(install_dir / "lib" / "python").mkdir(parents=True)
244+
other_dir.mkdir()
245+
env_var = _iris_ep._bootstrap.get_loader_path_env_var()
246+
monkeypatch.setenv(env_var, str(other_dir))
247+
248+
fake_module = types.SimpleNamespace(
249+
__file__=str(install_dir / "bin" / "pythonint.so"),
250+
cls=lambda name: {"class": name},
251+
connect=lambda *args, **kwargs: {"ok": True},
252+
)
253+
254+
def fake_import_module(name):
255+
if name == "pythonint":
256+
return fake_module
257+
raise ModuleNotFoundError(name)
258+
259+
monkeypatch.setattr(_iris_ep._bootstrap.importlib, "import_module", fake_import_module)
260+
monkeypatch.setattr(_iris_ep._bootstrap, "update_dynalib_path", lambda path: None)
261+
262+
with pytest.warns(RuntimeWarning, match=f"{env_var} does not include"):
263+
iris.connect(path=install_dir)
264+
265+
iris.runtime.reset()
266+
267+
199268
def test_connect_path_rejects_pythonint_from_other_install(monkeypatch, tmp_path):
200269
iris.runtime.reset()
201270
install_dir = tmp_path / "iris"
202271
other_dir = tmp_path / "other-iris"
203272
(install_dir / "bin").mkdir(parents=True)
204273
(install_dir / "lib" / "python").mkdir(parents=True)
205274
(other_dir / "bin").mkdir(parents=True)
275+
_set_loader_path(monkeypatch, install_dir)
206276

207277
fake_module = types.SimpleNamespace(
208278
__file__=str(other_dir / "bin" / "pythonint.so"),
@@ -233,6 +303,7 @@ def test_connect_path_ignores_stale_pythonint_module(monkeypatch, tmp_path):
233303
(install_dir / "bin").mkdir(parents=True)
234304
(install_dir / "lib" / "python").mkdir(parents=True)
235305
(other_dir / "bin").mkdir(parents=True)
306+
_set_loader_path(monkeypatch, install_dir)
236307

237308
stale_module = types.SimpleNamespace(
238309
__file__=str(other_dir / "bin" / "pythonint.so"),
@@ -272,6 +343,7 @@ def test_connect_path_only_prioritizes_install_dir_during_pythonint_import(monke
272343
(install_dir / "bin").mkdir(parents=True)
273344
(install_dir / "lib" / "python").mkdir(parents=True)
274345
seen_import_path = []
346+
_set_loader_path(monkeypatch, install_dir)
275347

276348
fake_module = types.SimpleNamespace(
277349
__file__=str(install_dir / "bin" / "pythonint.so"),
@@ -308,6 +380,7 @@ def test_connect_path_warns_when_backend_has_no_connect(monkeypatch, tmp_path):
308380
install_dir = tmp_path / "iris"
309381
(install_dir / "bin").mkdir(parents=True)
310382
(install_dir / "lib" / "python").mkdir(parents=True)
383+
_set_loader_path(monkeypatch, install_dir)
311384

312385
fake_module = types.SimpleNamespace(
313386
__file__=str(install_dir / "bin" / "pythonint.so"),

0 commit comments

Comments
 (0)