Skip to content

Commit 9cea4e6

Browse files
committed
fix: address review comments (iteration #1)
1 parent d08fca1 commit 9cea4e6

File tree

4 files changed

+54
-61
lines changed

4 files changed

+54
-61
lines changed

sagemaker-core/pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ dependencies = [
1414
# Add your dependencies here (Include lower and upper bounds as applicable)
1515
"boto3>=1.42.2,<2.0.0",
1616
"pydantic>=2.10.0,<3.0.0",
17-
"pydantic-core>=2.27.0,<3.0.0",
1817
"PyYAML>=6.0, <7.0",
1918
"jsonschema<5.0.0",
2019
"platformdirs>=4.0.0, <5.0.0",

sagemaker-core/src/sagemaker/core/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
from sagemaker.core._pydantic_compat import check_pydantic_compatibility
44
check_pydantic_compatibility()
55
except ImportError as e:
6-
if "pydantic" in str(e).lower() and ("incompatible" in str(e).lower() or "mismatch" in str(e).lower()):
6+
if "pydantic" in str(e).lower():
77
raise
8-
# If it's a different ImportError (e.g., pydantic not installed yet), let it pass
9-
# and fail later with a more standard error
8+
# If it's a different ImportError (e.g., module not found for _pydantic_compat itself),
9+
# let it pass and fail later with a more standard error
1010
pass
1111

1212
from sagemaker.core.utils.utils import enable_textual_rich_console_and_traceback

sagemaker-core/src/sagemaker/core/_pydantic_compat.py

Lines changed: 12 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,24 @@
1515
This module provides an early check for pydantic/pydantic-core version
1616
compatibility to give users a clear error message with fix instructions
1717
instead of a cryptic SystemError.
18+
19+
When pydantic-core is upgraded independently of pydantic (e.g., via
20+
``pip install --force-reinstall --no-deps``), Python raises a ``SystemError``
21+
at import time. This module catches that error and converts it into a
22+
helpful ``ImportError`` with remediation steps.
1823
"""
1924

2025

2126
def check_pydantic_compatibility():
2227
"""Check that pydantic and pydantic-core versions are compatible.
2328
29+
Pydantic internally requires an exact matching pydantic-core version
30+
(e.g., pydantic 2.11.5 requires pydantic-core==2.41.5). If the two
31+
packages are out of sync, ``import pydantic`` raises a ``SystemError``.
32+
33+
This function catches that ``SystemError`` and re-raises it as an
34+
``ImportError`` with clear instructions on how to fix the issue.
35+
2436
Raises:
2537
ImportError: If pydantic and pydantic-core versions are incompatible,
2638
with instructions on how to fix the issue.
@@ -37,44 +49,3 @@ def check_pydantic_compatibility():
3749
" pip install pydantic pydantic-core --force-reinstall\n\n"
3850
"This will ensure both packages are installed at compatible versions."
3951
) from e
40-
41-
try:
42-
import pydantic_core # noqa: F401
43-
except ImportError:
44-
# pydantic_core not installed separately is fine;
45-
# pydantic manages it as a dependency
46-
return
47-
48-
# Additional version check: pydantic declares the exact pydantic-core
49-
# version it requires. Verify they match.
50-
try:
51-
pydantic_version = pydantic.VERSION
52-
pydantic_core_version = pydantic_core.VERSION
53-
54-
# pydantic >= 2.x stores the required core version
55-
expected_core_version = getattr(pydantic, '__pydantic_core_version__', None)
56-
if expected_core_version is None:
57-
# Try alternative attribute name used in some pydantic versions
58-
expected_core_version = getattr(
59-
pydantic, '_internal', None
60-
) and getattr(
61-
getattr(pydantic, '_internal', None),
62-
'_generate_schema',
63-
None,
64-
)
65-
# If we can't determine the expected version, skip the check
66-
return
67-
68-
if pydantic_core_version != expected_core_version:
69-
raise ImportError(
70-
f"Pydantic/pydantic-core version mismatch detected: "
71-
f"pydantic {pydantic_version} requires pydantic-core=={expected_core_version}, "
72-
f"but pydantic-core {pydantic_core_version} is installed.\n\n"
73-
"To fix this, run:\n"
74-
" pip install pydantic pydantic-core --force-reinstall\n\n"
75-
"This will ensure both packages are installed at compatible versions."
76-
)
77-
except (AttributeError, TypeError):
78-
# If we can't determine versions, skip the check
79-
# The SystemError catch above will handle the most common case
80-
pass

sagemaker-core/tests/unit/test_pydantic_compat.py

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# language governing permissions and limitations under the License.
1313
"""Tests for pydantic compatibility check."""
1414

15+
import builtins
1516
import sys
1617
from unittest import mock
1718

@@ -26,6 +27,18 @@ def test_check_pydantic_compatibility_passes_with_matching_versions():
2627
check_pydantic_compatibility()
2728

2829

30+
def _make_pydantic_system_error_import(error_msg):
31+
"""Create a mock import function that raises SystemError for pydantic."""
32+
_real_import = builtins.__import__
33+
34+
def _mock_import(name, *args, **kwargs):
35+
if name == "pydantic":
36+
raise SystemError(error_msg)
37+
return _real_import(name, *args, **kwargs)
38+
39+
return _mock_import
40+
41+
2942
def test_check_pydantic_compatibility_raises_on_system_error():
3043
"""Mock pydantic import to raise SystemError and verify a clear ImportError is raised."""
3144
from sagemaker.core._pydantic_compat import check_pydantic_compatibility
@@ -35,20 +48,15 @@ def test_check_pydantic_compatibility_raises_on_system_error():
3548
"with the current pydantic version, which requires 2.41.5."
3649
)
3750

38-
with mock.patch.dict(sys.modules, {"pydantic": None}):
39-
original_import = __builtins__.__import__ if hasattr(__builtins__, '__import__') else __import__
40-
41-
def mock_import(name, *args, **kwargs):
42-
if name == "pydantic":
43-
raise SystemError(error_msg)
44-
return original_import(name, *args, **kwargs)
51+
mock_import = _make_pydantic_system_error_import(error_msg)
4552

53+
with mock.patch.dict(sys.modules, {"pydantic": None}):
4654
with mock.patch("builtins.__import__", side_effect=mock_import):
4755
with pytest.raises(ImportError) as exc_info:
4856
check_pydantic_compatibility()
4957

50-
assert "incompatibility detected" in str(exc_info.value).lower() or \
51-
"incompatible" in str(exc_info.value).lower()
58+
error_str = str(exc_info.value).lower()
59+
assert "incompatibility detected" in error_str
5260

5361

5462
def test_pydantic_import_error_message_contains_instructions():
@@ -60,17 +68,32 @@ def test_pydantic_import_error_message_contains_instructions():
6068
"with the current pydantic version, which requires 2.41.5."
6169
)
6270

63-
with mock.patch.dict(sys.modules, {"pydantic": None}):
64-
original_import = __builtins__.__import__ if hasattr(__builtins__, '__import__') else __import__
65-
66-
def mock_import(name, *args, **kwargs):
67-
if name == "pydantic":
68-
raise SystemError(error_msg)
69-
return original_import(name, *args, **kwargs)
71+
mock_import = _make_pydantic_system_error_import(error_msg)
7072

73+
with mock.patch.dict(sys.modules, {"pydantic": None}):
7174
with mock.patch("builtins.__import__", side_effect=mock_import):
7275
with pytest.raises(ImportError) as exc_info:
7376
check_pydantic_compatibility()
7477

7578
error_str = str(exc_info.value)
7679
assert "pip install pydantic pydantic-core --force-reinstall" in error_str
80+
81+
82+
def test_pydantic_import_error_chains_original_system_error():
83+
"""Verify the ImportError chains the original SystemError as __cause__."""
84+
from sagemaker.core._pydantic_compat import check_pydantic_compatibility
85+
86+
error_msg = (
87+
"The installed pydantic-core version (2.42.0) is incompatible "
88+
"with the current pydantic version, which requires 2.41.5."
89+
)
90+
91+
mock_import = _make_pydantic_system_error_import(error_msg)
92+
93+
with mock.patch.dict(sys.modules, {"pydantic": None}):
94+
with mock.patch("builtins.__import__", side_effect=mock_import):
95+
with pytest.raises(ImportError) as exc_info:
96+
check_pydantic_compatibility()
97+
98+
assert isinstance(exc_info.value.__cause__, SystemError)
99+
assert "incompatible" in str(exc_info.value.__cause__).lower()

0 commit comments

Comments
 (0)