Skip to content

Commit 7d412f4

Browse files
Add option to disable core loading
1 parent 787712b commit 7d412f4

File tree

2 files changed

+105
-55
lines changed

2 files changed

+105
-55
lines changed

src/snowflake/connector/_utils.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import ctypes
44
import importlib
5+
import os
56
import string
67
import sys
78
import threading
@@ -135,6 +136,10 @@ def _load_minicore(path: str) -> ctypes.CDLL:
135136
core = ctypes.CDLL(str(lib_path))
136137
return core
137138

139+
def _is_core_disabled(self) -> bool:
140+
value = str(os.getenv("SNOWFLAKE_DISABLE_MINICORE", None)).lower()
141+
return value in ["1", "true"]
142+
138143
def _load(self) -> None:
139144
try:
140145
path = self._get_core_path()
@@ -147,6 +152,9 @@ def _load(self) -> None:
147152

148153
def load(self):
149154
"""Spawn a separate thread to load the minicore library (non-blocking)."""
155+
if self._is_core_disabled():
156+
self._error = "mini-core-disabled"
157+
return
150158
self._error = "still-loading"
151159
thread = threading.Thread(target=self._load, daemon=True)
152160
thread.start()

test/unit/test_util.py

Lines changed: 97 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import ctypes
2+
import os
23
import sys
34
from importlib import reload
45
from unittest import mock
@@ -62,9 +63,7 @@ def test_get_core_path_darwin(self):
6263
_CoreLoader._get_core_path()
6364

6465
mock_files.assert_called_once_with("snowflake.connector.minicore")
65-
mock_files_obj.joinpath.assert_called_once_with(
66-
"libsf_mini_core.dyldib"
67-
)
66+
mock_files_obj.joinpath.assert_called_once_with("libsf_mini_core.dylib")
6867

6968
def test_get_core_path_linux(self):
7069
"""Test _get_core_path returns correct path for Linux."""
@@ -111,6 +110,42 @@ def test_load_minicore(self):
111110
mock_cdll.assert_called_once_with(str(mock_lib_path))
112111
assert result == mock_core
113112

113+
@pytest.mark.parametrize("env_value", ["1", "true", "True", "TRUE"])
114+
def test_is_core_disabled_returns_true(self, env_value):
115+
"""Test that _is_core_disabled returns True when env var is '1' or 'true' (case-insensitive)."""
116+
loader = _CoreLoader()
117+
with mock.patch.dict(os.environ, {"SNOWFLAKE_DISABLE_MINICORE": env_value}):
118+
assert loader._is_core_disabled() is True
119+
120+
@pytest.mark.parametrize("env_value", ["0", "false", "False", "no", "other", ""])
121+
def test_is_core_disabled_returns_false(self, env_value):
122+
"""Test that _is_core_disabled returns False for other values."""
123+
loader = _CoreLoader()
124+
with mock.patch.dict(os.environ, {"SNOWFLAKE_DISABLE_MINICORE": env_value}):
125+
assert loader._is_core_disabled() is False
126+
127+
def test_is_core_disabled_returns_false_when_not_set(self):
128+
"""Test that _is_core_disabled returns False when env var is not set."""
129+
loader = _CoreLoader()
130+
with mock.patch.dict(os.environ, {}, clear=True):
131+
# Ensure the env var is not set
132+
os.environ.pop("SNOWFLAKE_DISABLE_CORE", None)
133+
assert loader._is_core_disabled() is False
134+
135+
def test_load_skips_loading_when_core_disabled(self):
136+
"""Test that load() returns early when core is disabled."""
137+
loader = _CoreLoader()
138+
139+
with mock.patch.dict(os.environ, {"SNOWFLAKE_DISABLE_MINICORE": "1"}):
140+
with mock.patch.object(loader, "_get_core_path") as mock_get_path:
141+
loader.load()
142+
143+
# Verify that _get_core_path was never called (loading was skipped)
144+
mock_get_path.assert_not_called()
145+
# Verify the error message is set correctly
146+
assert loader._error == "mini-core-disabled"
147+
assert loader._version is None
148+
114149
def test_load_success(self):
115150
"""Test successful load of the core library."""
116151
loader = _CoreLoader()
@@ -119,34 +154,38 @@ def test_load_success(self):
119154
mock_version = b"1.2.3"
120155
mock_core.sf_core_full_version = mock.MagicMock(return_value=mock_version)
121156

122-
with mock.patch.object(
123-
loader, "_get_core_path", return_value=mock_path
124-
) as mock_get_path:
157+
with mock.patch.object(loader, "_is_core_disabled", return_value=False):
125158
with mock.patch.object(
126-
loader, "_load_minicore", return_value=mock_core
127-
) as mock_load:
128-
with mock.patch.object(loader, "_register_functions") as mock_register:
129-
loader.load()
130-
131-
mock_get_path.assert_called_once()
132-
mock_load.assert_called_once_with(mock_path)
133-
mock_register.assert_called_once_with(mock_core)
134-
assert loader._version == mock_version
135-
assert loader._error is None
159+
loader, "_get_core_path", return_value=mock_path
160+
) as mock_get_path:
161+
with mock.patch.object(
162+
loader, "_load_minicore", return_value=mock_core
163+
) as mock_load:
164+
with mock.patch.object(
165+
loader, "_register_functions"
166+
) as mock_register:
167+
loader.load()
168+
169+
mock_get_path.assert_called_once()
170+
mock_load.assert_called_once_with(mock_path)
171+
mock_register.assert_called_once_with(mock_core)
172+
assert loader._version == mock_version
173+
assert loader._error is None
136174

137175
def test_load_failure(self):
138176
"""Test that load captures exceptions."""
139177
loader = _CoreLoader()
140178
test_error = Exception("Test error loading core")
141179

142-
with mock.patch.object(
143-
loader, "_get_core_path", side_effect=test_error
144-
) as mock_get_path:
145-
loader.load()
180+
with mock.patch.object(loader, "_is_core_disabled", return_value=False):
181+
with mock.patch.object(
182+
loader, "_get_core_path", side_effect=test_error
183+
) as mock_get_path:
184+
loader.load()
146185

147-
mock_get_path.assert_called_once()
148-
assert loader._version is None
149-
assert loader._error == test_error
186+
mock_get_path.assert_called_once()
187+
assert loader._version is None
188+
assert loader._error == test_error
150189

151190
def test_get_load_error_with_error(self):
152191
"""Test get_load_error returns error message when error exists."""
@@ -190,7 +229,7 @@ def test_importing_snowflake_connector_triggers_core_loader_load():
190229
# core_loader.load() is called. Since snowflake.connector is already imported,
191230
# we need to reload it and mock the load method.
192231

193-
with mock.patch("snowflake.connector._utils.core_loader.load") as mock_load:
232+
with mock.patch("snowflake.connector._utils._core_loader.load") as mock_load:
194233
# Reload the connector module to trigger the __init__.py code again
195234
import snowflake.connector
196235

@@ -204,7 +243,7 @@ def test_snowflake_connector_loads_when_core_loader_fails():
204243
"""Test that snowflake.connector loads successfully even if core_loader.load() fails."""
205244
# Mock core_loader.load() to raise an exception
206245
with mock.patch(
207-
"snowflake.connector._utils.core_loader.load",
246+
"snowflake.connector._utils._core_loader.load",
208247
side_effect=Exception("Simulated core loading failure"),
209248
):
210249
import snowflake.connector
@@ -229,7 +268,7 @@ def test_snowflake_connector_usable_when_core_loader_fails():
229268
"""Test that snowflake.connector remains usable even if core_loader.load() fails."""
230269
# Mock core_loader.load() to raise an exception
231270
with mock.patch(
232-
"snowflake.connector._utils.core_loader.load",
271+
"snowflake.connector._utils._core_loader.load",
233272
side_effect=RuntimeError("Core library not found"),
234273
):
235274
import snowflake.connector
@@ -259,32 +298,34 @@ def test_core_loader_error_captured_when_load_fails():
259298
test_exception = FileNotFoundError("Library file not found")
260299

261300
# Mock _get_core_path to raise an exception
262-
with mock.patch.object(loader, "_get_core_path", side_effect=test_exception):
263-
# Call load - it should NOT raise an exception
264-
loader.load()
301+
with mock.patch.object(loader, "_is_core_disabled", return_value=False):
302+
with mock.patch.object(loader, "_get_core_path", side_effect=test_exception):
303+
# Call load - it should NOT raise an exception
304+
loader.load()
265305

266-
# Verify the error was captured
267-
assert loader._error is test_exception
268-
assert loader._version is None
269-
assert loader.get_load_error() == "Library file not found"
270-
assert loader.get_core_version() is None
306+
# Verify the error was captured
307+
assert loader._error is test_exception
308+
assert loader._version is None
309+
assert loader.get_load_error() == "Library file not found"
310+
assert loader.get_core_version() is None
271311

272312

273313
def test_core_loader_fails_gracefully_on_missing_library():
274314
"""Test that core_loader handles missing library files gracefully."""
275315
loader = _CoreLoader()
276316

277317
# Mock importlib.resources.files to simulate missing library
278-
with mock.patch("importlib.resources.files") as mock_files:
279-
mock_files.side_effect = FileNotFoundError("minicore module not found")
318+
with mock.patch.object(loader, "_is_core_disabled", return_value=False):
319+
with mock.patch("importlib.resources.files") as mock_files:
320+
mock_files.side_effect = FileNotFoundError("minicore module not found")
280321

281-
# Call load - it should NOT raise an exception
282-
loader.load()
322+
# Call load - it should NOT raise an exception
323+
loader.load()
283324

284-
# Verify the error was captured
285-
assert loader._error is not None
286-
assert loader._version is None
287-
assert "minicore module not found" in loader.get_load_error()
325+
# Verify the error was captured
326+
assert loader._error is not None
327+
assert loader._version is None
328+
assert "minicore module not found" in loader.get_load_error()
288329

289330

290331
def test_core_loader_fails_gracefully_on_incompatible_library():
@@ -293,16 +334,17 @@ def test_core_loader_fails_gracefully_on_incompatible_library():
293334
mock_path = mock.MagicMock()
294335

295336
# Mock the loading to simulate incompatible library (OSError is common for this)
296-
with mock.patch.object(loader, "_get_core_path", return_value=mock_path):
297-
with mock.patch.object(
298-
loader,
299-
"_load_minicore",
300-
side_effect=OSError("incompatible library version"),
301-
):
302-
# Call load - it should NOT raise an exception
303-
loader.load()
304-
305-
# Verify the error was captured
306-
assert loader._error is not None
307-
assert loader._version is None
308-
assert "incompatible library version" in loader.get_load_error()
337+
with mock.patch.object(loader, "_is_core_disabled", return_value=False):
338+
with mock.patch.object(loader, "_get_core_path", return_value=mock_path):
339+
with mock.patch.object(
340+
loader,
341+
"_load_minicore",
342+
side_effect=OSError("incompatible library version"),
343+
):
344+
# Call load - it should NOT raise an exception
345+
loader.load()
346+
347+
# Verify the error was captured
348+
assert loader._error is not None
349+
assert loader._version is None
350+
assert "incompatible library version" in loader.get_load_error()

0 commit comments

Comments
 (0)