|
13 | 13 | """Module for deprecation abstractions.""" |
14 | 14 | from __future__ import absolute_import |
15 | 15 |
|
| 16 | +import importlib.abc |
16 | 17 | import logging |
| 18 | +import sys |
17 | 19 | import warnings |
18 | 20 |
|
19 | 21 | logger = logging.getLogger(__name__) |
20 | 22 |
|
21 | 23 | V2_URL = "https://sagemaker.readthedocs.io/en/stable/v2.html" |
22 | 24 |
|
| 25 | +# Migration guide for users moving from the v2 SDK to v3. |
| 26 | +V3_MIGRATION_URL = "https://github.com/aws/sagemaker-python-sdk/blob/master/migration.md" |
| 27 | + |
| 28 | +# Real top-level ``sagemaker.*`` names that ship in v3. The fallback finder must |
| 29 | +# never intercept these -- even when a package (e.g. sagemaker-train) is simply |
| 30 | +# not installed, its absence should surface the normal error, not a bogus |
| 31 | +# "removed in v3" message. |
| 32 | +_KNOWN_V3_TOPLEVEL = frozenset({"core", "train", "serve", "mlops", "lineage", "ai_registry"}) |
| 33 | + |
| 34 | + |
| 35 | +def raise_removed_in_v3(module, replacement=None, v3_import=None): |
| 36 | + """Warn and then raise an actionable error for a v2 module removed in v3. |
| 37 | +
|
| 38 | + The v2 SDK exposed top-level modules (e.g. ``sagemaker.estimator``) that no |
| 39 | + longer exist in v3. Importing one would otherwise fail with a bare |
| 40 | + ``ModuleNotFoundError: No module named 'sagemaker.estimator'`` that gives the |
| 41 | + caller no path forward. This helper is called from lightweight "tombstone" |
| 42 | + modules that stand in for those removed names: it emits a |
| 43 | + ``DeprecationWarning`` and then raises a ``ModuleNotFoundError`` whose message |
| 44 | + names the v3 replacement and links the migration guide. |
| 45 | +
|
| 46 | + Args: |
| 47 | + module (str): The removed v2 module path, e.g. ``"sagemaker.estimator"``. |
| 48 | + replacement (str): Human readable v3 replacement, e.g. |
| 49 | + ``"ModelTrainer in the sagemaker-train package"``. Optional. |
| 50 | + v3_import (str): The exact v3 import statement, e.g. |
| 51 | + ``"from sagemaker.train import ModelTrainer"``. Quoted verbatim so the |
| 52 | + caller can copy-paste it. Optional. |
| 53 | +
|
| 54 | + Raises: |
| 55 | + ModuleNotFoundError: always, after emitting the deprecation warning. |
| 56 | + """ |
| 57 | + msg = f"`{module}` was removed in the SageMaker Python SDK v3." |
| 58 | + if replacement: |
| 59 | + msg += f" Use {replacement}." |
| 60 | + if v3_import: |
| 61 | + msg += f" ({v3_import})" |
| 62 | + msg += f"\nSee {V3_MIGRATION_URL} for the migration guide." |
| 63 | + |
| 64 | + warnings.warn(msg, DeprecationWarning, stacklevel=2) |
| 65 | + logger.warning(msg) |
| 66 | + raise ModuleNotFoundError(msg, name=module) |
| 67 | + |
| 68 | + |
| 69 | +class _RemovedV2ModuleFinder(importlib.abc.MetaPathFinder): |
| 70 | + """Fallback finder that gives actionable guidance for removed v2 modules. |
| 71 | +
|
| 72 | + Curated removals ship as explicit "tombstone" modules (e.g. |
| 73 | + ``sagemaker/estimator.py``) that raise a precise, per-module message. This |
| 74 | + finder is the *catch-all* for every other ``sagemaker.<name>`` that existed |
| 75 | + in v2 but was not individually tombstoned: instead of a bare |
| 76 | + ``ModuleNotFoundError: No module named 'sagemaker.foo'``, the caller gets a |
| 77 | + message that says the module is not available in v3 and points to the |
| 78 | + migration guide. |
| 79 | +
|
| 80 | + It is registered by appending to ``sys.meta_path``, so it only runs *after* |
| 81 | + the normal import machinery has failed to locate the module. That ordering |
| 82 | + guarantees it never shadows: |
| 83 | + - real v3 packages (``sagemaker.core``, ``sagemaker.train``, ...), and |
| 84 | + - the curated tombstone modules, which resolve as ordinary files first. |
| 85 | + """ |
| 86 | + |
| 87 | + def find_spec(self, fullname, path=None, target=None): |
| 88 | + """Intercept only genuinely-missing top-level ``sagemaker.<name>`` imports.""" |
| 89 | + if not fullname.startswith("sagemaker."): |
| 90 | + return None |
| 91 | + leaf = fullname[len("sagemaker.") :] |
| 92 | + # Only guard top-level names; never touch real v3 subpackages. |
| 93 | + if "." in leaf or leaf in _KNOWN_V3_TOPLEVEL: |
| 94 | + return None |
| 95 | + |
| 96 | + msg = ( |
| 97 | + f"`{fullname}` is not available in the SageMaker Python SDK v3. " |
| 98 | + "It may have been removed or moved to a new location." |
| 99 | + f"\nSee {V3_MIGRATION_URL} for the migration guide." |
| 100 | + ) |
| 101 | + warnings.warn(msg, DeprecationWarning, stacklevel=2) |
| 102 | + logger.warning(msg) |
| 103 | + raise ModuleNotFoundError(msg, name=fullname) |
| 104 | + |
| 105 | + |
| 106 | +def register_removed_module_finder(): |
| 107 | + """Install the fallback finder for removed v2 modules (idempotent). |
| 108 | +
|
| 109 | + Appends a single ``_RemovedV2ModuleFinder`` to ``sys.meta_path`` so it acts |
| 110 | + as a last resort. Safe to call multiple times -- it installs at most one |
| 111 | + instance per process. |
| 112 | + """ |
| 113 | + if any(isinstance(f, _RemovedV2ModuleFinder) for f in sys.meta_path): |
| 114 | + return |
| 115 | + sys.meta_path.append(_RemovedV2ModuleFinder()) |
| 116 | + |
23 | 117 |
|
24 | 118 | def _warn(msg, sdk_version=None): |
25 | 119 | """Generic warning raiser referencing V2 |
|
0 commit comments