diff --git a/sagemaker-core/src/sagemaker/__init__.py b/sagemaker-core/src/sagemaker/__init__.py index 71038bb89b..34badfccd5 100644 --- a/sagemaker-core/src/sagemaker/__init__.py +++ b/sagemaker-core/src/sagemaker/__init__.py @@ -1,2 +1,16 @@ """Namespace package for SageMaker.""" -__path__ = __import__('pkgutil').extend_path(__path__, __name__) + +__path__ = __import__("pkgutil").extend_path(__path__, __name__) + +# Register the fallback finder that gives actionable migration guidance for v2 +# modules removed in v3. Doing it here (in the namespace package init, which +# runs on any ``import sagemaker.*``) ensures the guidance is active even when a +# removed module is the very first sagemaker import in the process (e.g. +# ``import sagemaker.estimator``), before ``sagemaker.core`` is otherwise loaded. +# Fully guarded so it can never interfere with importing the package. +try: + from sagemaker.core.deprecations import register_removed_module_finder as _register + + _register() +except Exception: # pylint: disable=W0703 # noqa: E722 + pass diff --git a/sagemaker-core/src/sagemaker/algorithm.py b/sagemaker-core/src/sagemaker/algorithm.py new file mode 100644 index 0000000000..d1bfdcb444 --- /dev/null +++ b/sagemaker-core/src/sagemaker/algorithm.py @@ -0,0 +1,27 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Removed in v3. This module stood in the v2 SageMaker Python SDK. + +Importing ``sagemaker.algorithm`` raises an actionable ``ModuleNotFoundError`` that +points to the v3 replacement. See the migration guide for details. +""" +from __future__ import absolute_import + +from sagemaker.core.deprecations import raise_removed_in_v3 + +raise_removed_in_v3( + module="sagemaker.algorithm", + replacement="`ModelTrainer`", + v3_import="from sagemaker.train import ModelTrainer", + v3_docs="https://sagemaker.readthedocs.io/en/stable/api/generated/sagemaker.train.model_trainer.html", +) diff --git a/sagemaker-core/src/sagemaker/automl.py b/sagemaker-core/src/sagemaker/automl.py new file mode 100644 index 0000000000..509e46d839 --- /dev/null +++ b/sagemaker-core/src/sagemaker/automl.py @@ -0,0 +1,27 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Removed in v3. This module stood in the v2 SageMaker Python SDK. + +Importing ``sagemaker.automl`` raises an actionable ``ModuleNotFoundError`` that +points to the v3 replacement. See the migration guide for details. +""" +from __future__ import absolute_import + +from sagemaker.core.deprecations import raise_removed_in_v3 + +raise_removed_in_v3( + module="sagemaker.automl", + replacement="the `AutoMLJob` resource", + v3_import="from sagemaker.core.resources import AutoMLJob", + v3_docs="https://sagemaker.readthedocs.io/en/stable/api/generated/sagemaker.core.resources.html", +) diff --git a/sagemaker-core/src/sagemaker/base_predictor.py b/sagemaker-core/src/sagemaker/base_predictor.py new file mode 100644 index 0000000000..fb01410a23 --- /dev/null +++ b/sagemaker-core/src/sagemaker/base_predictor.py @@ -0,0 +1,27 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Removed in v3. This module stood in the v2 SageMaker Python SDK. + +Importing ``sagemaker.base_predictor`` raises an actionable ``ModuleNotFoundError`` that +points to the v3 replacement. See the migration guide for details. +""" +from __future__ import absolute_import + +from sagemaker.core.deprecations import raise_removed_in_v3 + +raise_removed_in_v3( + module="sagemaker.base_predictor", + replacement="the `Endpoint` resource", + v3_import="from sagemaker.core.resources import Endpoint", + v3_docs="https://sagemaker.readthedocs.io/en/stable/api/generated/sagemaker.core.resources.html", +) diff --git a/sagemaker-core/src/sagemaker/core/__init__.py b/sagemaker-core/src/sagemaker/core/__init__.py index 498c85390a..ad6225aa63 100644 --- a/sagemaker-core/src/sagemaker/core/__init__.py +++ b/sagemaker-core/src/sagemaker/core/__init__.py @@ -1,9 +1,17 @@ """SageMaker Core package for low-level resource management and SDK foundations.""" + from sagemaker.core.utils.utils import enable_textual_rich_console_and_traceback +from sagemaker.core.deprecations import register_removed_module_finder enable_textual_rich_console_and_traceback() +# Install the fallback finder that gives actionable migration guidance for v2 +# modules removed in v3 but not individually tombstoned. sagemaker-core is the +# universal dependency of every v3 package, so registering here guarantees the +# finder is active whenever the SDK is used. +register_removed_module_finder() + # Job management from sagemaker.core.job import _Job # noqa: F401 from sagemaker.core.processing import ( # noqa: F401 diff --git a/sagemaker-core/src/sagemaker/core/deprecations.py b/sagemaker-core/src/sagemaker/core/deprecations.py index a8ba298082..38500ad6f3 100644 --- a/sagemaker-core/src/sagemaker/core/deprecations.py +++ b/sagemaker-core/src/sagemaker/core/deprecations.py @@ -13,13 +13,236 @@ """Module for deprecation abstractions.""" from __future__ import absolute_import +import importlib.abc import logging +import sys import warnings logger = logging.getLogger(__name__) V2_URL = "https://sagemaker.readthedocs.io/en/stable/v2.html" +# Migration guide for users moving from the v2 SDK to v3. +V3_MIGRATION_URL = "https://github.com/aws/sagemaker-python-sdk/blob/master/migration.md" + +# Real top-level ``sagemaker.*`` names that ship in v3. The fallback finder must +# never intercept these -- even when a package (e.g. sagemaker-train) is simply +# not installed, its absence should surface the normal error, not a bogus +# "removed in v3" message. +_KNOWN_V3_TOPLEVEL = frozenset({"core", "train", "serve", "mlops", "lineage", "ai_registry"}) + +# Top-level ``sagemaker.`` modules that existed in v2 but were removed in +# v3 (some relocated under ``sagemaker.core.*``). Derived from the v2 top-level +# module surface (the ``master-v2`` branch), minus names that still exist in v3. +# The fallback finder only emits migration guidance for THESE names, so a typo +# or hallucinated import (e.g. ``sagemaker.foobar``) gets a plain +# ``ModuleNotFoundError`` rather than a misleading "was removed" message. V2 is +# in maintenance, so this surface is effectively frozen. +_REMOVED_V2_MODULES = frozenset( + { + "_studio", + "accept_types", + "algorithm", + "amazon", + "amtviz", + "analytics", + "apiutils", + "async_inference", + "automl", + "aws_batch", + "base_deserializers", + "base_predictor", + "base_serializers", + "batch_inference", + "chainer", + "clarify", + "cli", + "collection", + "compute_resource_requirements", + "config", + "container_base_model", + "content_types", + "dataset_definition", + "debugger", + "deprecations", + "deserializers", + "djl_inference", + "drift_check_baselines", + "enums", + "environment_variables", + "estimator", + "exceptions", + "experiments", + "explainer", + "feature_store", + "fw_utils", + "git_utils", + "huggingface", + "hyperparameters", + "image_uri_config", + "image_uris", + "inference_recommender", + "inputs", + "instance_group", + "instance_types", + "instance_types_gpu_info", + "interactive_apps", + "iterators", + "job", + "jumpstart", + "lambda_helper", + "local", + "logs", + "metadata_properties", + "metric_definitions", + "mlflow", + "model", + "model_card", + "model_life_cycle", + "model_metrics", + "model_monitor", + "model_uris", + "modules", + "multidatamodel", + "mxnet", + "network", + "parameter", + "partner_app", + "payloads", + "pipeline", + "predictor", + "predictor_async", + "processing", + "pytorch", + "remote_function", + "resource_requirements", + "rl", + "s3", + "s3_utils", + "script_uris", + "serializer_utils", + "serializers", + "serverless", + "session", + "session_settings", + "sklearn", + "spark", + "sparkml", + "stabilityai", + "telemetry", + "tensorflow", + "training_compiler", + "transformer", + "tuner", + "user_agent", + "utilities", + "utils", + "vpc_utils", + "workflow", + "wrangler", + "xgboost", + } +) + + +def raise_removed_in_v3(module, replacement=None, v3_import=None, v3_docs=None): + """Warn and then raise an actionable error for a v2 module removed in v3. + + The v2 SDK exposed top-level modules (e.g. ``sagemaker.estimator``) that no + longer exist in v3. Importing one would otherwise fail with a bare + ``ModuleNotFoundError: No module named 'sagemaker.estimator'`` that gives the + caller no path forward. This helper is called from lightweight "tombstone" + modules that stand in for those removed names: it emits a + ``DeprecationWarning`` and then raises a ``ModuleNotFoundError`` whose message + names the exact v3 replacement, the import to copy-paste, and a direct link + to that replacement's API docs (plus the migration guide). + + Args: + module (str): The removed v2 module path, e.g. ``"sagemaker.estimator"``. + replacement (str): Human readable v3 replacement, e.g. ``"ModelTrainer"``. + Optional. + v3_import (str): The exact v3 import statement, e.g. + ``"from sagemaker.train import ModelTrainer"``. Quoted verbatim so the + caller can copy-paste it. Optional. + v3_docs (str): Direct URL to the v3 replacement's API documentation, e.g. + the generated ``sagemaker.train.model_trainer`` page. Optional. + + Raises: + ModuleNotFoundError: always, after emitting the deprecation warning. + """ + msg = f"`{module}` was removed in the SageMaker Python SDK v3." + if replacement: + msg += f" Use {replacement}." + if v3_import: + msg += f" ({v3_import})" + if v3_docs: + msg += f"\nDocs: {v3_docs}" + msg += f"\nSee {V3_MIGRATION_URL} for the migration guide." + + warnings.warn(msg, DeprecationWarning, stacklevel=2) + # The raised ModuleNotFoundError below is the loud, authoritative signal + # (it stops execution and carries the full message). Log at debug only, to + # leave a breadcrumb for log-captured environments without duplicating the + # message at WARNING level. + logger.debug(msg) + raise ModuleNotFoundError(msg, name=module) + + +class _RemovedV2ModuleFinder(importlib.abc.MetaPathFinder): + """Fallback finder that gives actionable guidance for removed v2 modules. + + Curated removals ship as explicit "tombstone" modules (e.g. + ``sagemaker/estimator.py``) that raise a precise, per-module message. This + finder is the *catch-all* for every other ``sagemaker.`` that existed + in v2 but was not individually tombstoned: instead of a bare + ``ModuleNotFoundError: No module named 'sagemaker.foo'``, the caller gets a + message that says the module is not available in v3 and points to the + migration guide. + + It is registered by appending to ``sys.meta_path``, so it only runs *after* + the normal import machinery has failed to locate the module. That ordering + guarantees it never shadows: + - real v3 packages (``sagemaker.core``, ``sagemaker.train``, ...), and + - the curated tombstone modules, which resolve as ordinary files first. + """ + + def find_spec(self, fullname, path=None, target=None): + """Emit guidance only for known removed v2 top-level ``sagemaker`` modules.""" + if not fullname.startswith("sagemaker."): + return None + leaf = fullname[len("sagemaker.") :] + # Only guard top-level names; never touch real v3 subpackages. + if "." in leaf or leaf in _KNOWN_V3_TOPLEVEL: + return None + # Only guard names that were actually v2 modules. Unknown names (typos, + # hallucinated imports) fall through to a plain ModuleNotFoundError so we + # never claim something "was removed" when it never existed. + if leaf not in _REMOVED_V2_MODULES: + return None + + msg = ( + f"`{fullname}` was removed in the SageMaker Python SDK v3. " + "It may have moved to a new location." + f"\nSee {V3_MIGRATION_URL} for the migration guide." + ) + warnings.warn(msg, DeprecationWarning, stacklevel=2) + # See raise_removed_in_v3: the raised error is the loud signal; log at + # debug to avoid duplicating the message at WARNING level. + logger.debug(msg) + raise ModuleNotFoundError(msg, name=fullname) + + +def register_removed_module_finder(): + """Install the fallback finder for removed v2 modules (idempotent). + + Appends a single ``_RemovedV2ModuleFinder`` to ``sys.meta_path`` so it acts + as a last resort. Safe to call multiple times -- it installs at most one + instance per process. + """ + if any(isinstance(f, _RemovedV2ModuleFinder) for f in sys.meta_path): + return + sys.meta_path.append(_RemovedV2ModuleFinder()) + def _warn(msg, sdk_version=None): """Generic warning raiser referencing V2 diff --git a/sagemaker-core/src/sagemaker/estimator.py b/sagemaker-core/src/sagemaker/estimator.py new file mode 100644 index 0000000000..4cc11a8ed7 --- /dev/null +++ b/sagemaker-core/src/sagemaker/estimator.py @@ -0,0 +1,27 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Removed in v3. This module stood in the v2 SageMaker Python SDK. + +Importing ``sagemaker.estimator`` raises an actionable ``ModuleNotFoundError`` that +points to the v3 replacement. See the migration guide for details. +""" +from __future__ import absolute_import + +from sagemaker.core.deprecations import raise_removed_in_v3 + +raise_removed_in_v3( + module="sagemaker.estimator", + replacement="`ModelTrainer`", + v3_import="from sagemaker.train import ModelTrainer", + v3_docs="https://sagemaker.readthedocs.io/en/stable/api/generated/sagemaker.train.model_trainer.html", +) diff --git a/sagemaker-core/src/sagemaker/model.py b/sagemaker-core/src/sagemaker/model.py new file mode 100644 index 0000000000..541a728937 --- /dev/null +++ b/sagemaker-core/src/sagemaker/model.py @@ -0,0 +1,27 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Removed in v3. This module stood in the v2 SageMaker Python SDK. + +Importing ``sagemaker.model`` raises an actionable ``ModuleNotFoundError`` that +points to the v3 replacement. See the migration guide for details. +""" +from __future__ import absolute_import + +from sagemaker.core.deprecations import raise_removed_in_v3 + +raise_removed_in_v3( + module="sagemaker.model", + replacement="`ModelBuilder`", + v3_import="from sagemaker.serve import ModelBuilder", + v3_docs="https://sagemaker.readthedocs.io/en/stable/api/generated/sagemaker.serve.model_builder.html", +) diff --git a/sagemaker-core/src/sagemaker/multidatamodel.py b/sagemaker-core/src/sagemaker/multidatamodel.py new file mode 100644 index 0000000000..4e4e6db648 --- /dev/null +++ b/sagemaker-core/src/sagemaker/multidatamodel.py @@ -0,0 +1,27 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Removed in v3. This module stood in the v2 SageMaker Python SDK. + +Importing ``sagemaker.multidatamodel`` raises an actionable ``ModuleNotFoundError`` that +points to the v3 replacement. See the migration guide for details. +""" +from __future__ import absolute_import + +from sagemaker.core.deprecations import raise_removed_in_v3 + +raise_removed_in_v3( + module="sagemaker.multidatamodel", + replacement="`ModelBuilder`", + v3_import="from sagemaker.serve import ModelBuilder", + v3_docs="https://sagemaker.readthedocs.io/en/stable/api/generated/sagemaker.serve.model_builder.html", +) diff --git a/sagemaker-core/src/sagemaker/pipeline.py b/sagemaker-core/src/sagemaker/pipeline.py new file mode 100644 index 0000000000..dbe7c6a2d7 --- /dev/null +++ b/sagemaker-core/src/sagemaker/pipeline.py @@ -0,0 +1,27 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Removed in v3. This module stood in the v2 SageMaker Python SDK. + +Importing ``sagemaker.pipeline`` raises an actionable ``ModuleNotFoundError`` that +points to the v3 replacement. See the migration guide for details. +""" +from __future__ import absolute_import + +from sagemaker.core.deprecations import raise_removed_in_v3 + +raise_removed_in_v3( + module="sagemaker.pipeline", + replacement="`Pipeline`", + v3_import="from sagemaker.mlops.workflow.pipeline import Pipeline", + v3_docs="https://sagemaker.readthedocs.io/en/stable/api/generated/sagemaker.mlops.workflow.pipeline.html", +) diff --git a/sagemaker-core/src/sagemaker/predictor.py b/sagemaker-core/src/sagemaker/predictor.py new file mode 100644 index 0000000000..5509a81253 --- /dev/null +++ b/sagemaker-core/src/sagemaker/predictor.py @@ -0,0 +1,27 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Removed in v3. This module stood in the v2 SageMaker Python SDK. + +Importing ``sagemaker.predictor`` raises an actionable ``ModuleNotFoundError`` that +points to the v3 replacement. See the migration guide for details. +""" +from __future__ import absolute_import + +from sagemaker.core.deprecations import raise_removed_in_v3 + +raise_removed_in_v3( + module="sagemaker.predictor", + replacement="the `Endpoint` resource", + v3_import="from sagemaker.core.resources import Endpoint", + v3_docs="https://sagemaker.readthedocs.io/en/stable/api/generated/sagemaker.core.resources.html", +) diff --git a/sagemaker-core/src/sagemaker/predictor_async.py b/sagemaker-core/src/sagemaker/predictor_async.py new file mode 100644 index 0000000000..5da343837b --- /dev/null +++ b/sagemaker-core/src/sagemaker/predictor_async.py @@ -0,0 +1,27 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Removed in v3. This module stood in the v2 SageMaker Python SDK. + +Importing ``sagemaker.predictor_async`` raises an actionable ``ModuleNotFoundError`` that +points to the v3 replacement. See the migration guide for details. +""" +from __future__ import absolute_import + +from sagemaker.core.deprecations import raise_removed_in_v3 + +raise_removed_in_v3( + module="sagemaker.predictor_async", + replacement="`ModelBuilder` (async deploy)", + v3_import="from sagemaker.serve import ModelBuilder", + v3_docs="https://sagemaker.readthedocs.io/en/stable/api/generated/sagemaker.serve.model_builder.html", +) diff --git a/sagemaker-core/src/sagemaker/processing.py b/sagemaker-core/src/sagemaker/processing.py new file mode 100644 index 0000000000..638767e985 --- /dev/null +++ b/sagemaker-core/src/sagemaker/processing.py @@ -0,0 +1,27 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Removed in v3. This module stood in the v2 SageMaker Python SDK. + +Importing ``sagemaker.processing`` raises an actionable ``ModuleNotFoundError`` that +points to the v3 replacement. See the migration guide for details. +""" +from __future__ import absolute_import + +from sagemaker.core.deprecations import raise_removed_in_v3 + +raise_removed_in_v3( + module="sagemaker.processing", + replacement="the `ProcessingJob` resource", + v3_import="from sagemaker.core.resources import ProcessingJob", + v3_docs="https://sagemaker.readthedocs.io/en/stable/api/generated/sagemaker.core.resources.html", +) diff --git a/sagemaker-core/src/sagemaker/transformer.py b/sagemaker-core/src/sagemaker/transformer.py new file mode 100644 index 0000000000..1a40226c6e --- /dev/null +++ b/sagemaker-core/src/sagemaker/transformer.py @@ -0,0 +1,27 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Removed in v3. This module stood in the v2 SageMaker Python SDK. + +Importing ``sagemaker.transformer`` raises an actionable ``ModuleNotFoundError`` that +points to the v3 replacement. See the migration guide for details. +""" +from __future__ import absolute_import + +from sagemaker.core.deprecations import raise_removed_in_v3 + +raise_removed_in_v3( + module="sagemaker.transformer", + replacement="the `TransformJob` resource", + v3_import="from sagemaker.core.resources import TransformJob", + v3_docs="https://sagemaker.readthedocs.io/en/stable/api/generated/sagemaker.core.resources.html", +) diff --git a/sagemaker-core/src/sagemaker/tuner.py b/sagemaker-core/src/sagemaker/tuner.py new file mode 100644 index 0000000000..6ebfda507a --- /dev/null +++ b/sagemaker-core/src/sagemaker/tuner.py @@ -0,0 +1,27 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Removed in v3. This module stood in the v2 SageMaker Python SDK. + +Importing ``sagemaker.tuner`` raises an actionable ``ModuleNotFoundError`` that +points to the v3 replacement. See the migration guide for details. +""" +from __future__ import absolute_import + +from sagemaker.core.deprecations import raise_removed_in_v3 + +raise_removed_in_v3( + module="sagemaker.tuner", + replacement="the `HyperParameterTuningJob` resource", + v3_import="from sagemaker.core.resources import HyperParameterTuningJob", + v3_docs="https://sagemaker.readthedocs.io/en/stable/api/generated/sagemaker.core.resources.html", +) diff --git a/sagemaker-core/tests/unit/test_removed_v2_modules.py b/sagemaker-core/tests/unit/test_removed_v2_modules.py new file mode 100644 index 0000000000..19a389c699 --- /dev/null +++ b/sagemaker-core/tests/unit/test_removed_v2_modules.py @@ -0,0 +1,244 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Tests that removed v2 modules raise actionable guidance in v3.""" +from __future__ import absolute_import + +import importlib +import warnings + +import pytest + +from sagemaker.core.deprecations import ( + raise_removed_in_v3, + register_removed_module_finder, + _RemovedV2ModuleFinder, + V3_MIGRATION_URL, +) + +# Removed v2 module -> a substring expected in the guidance message. The values +# are verified-importable v3 symbols (see the tombstone modules under +# src/sagemaker/); the message also carries the exact import and a docs URL. +REMOVED_MODULES = { + "sagemaker.estimator": "ModelTrainer", + "sagemaker.model": "ModelBuilder", + "sagemaker.predictor": "Endpoint", + "sagemaker.base_predictor": "Endpoint", + "sagemaker.predictor_async": "ModelBuilder", + "sagemaker.transformer": "TransformJob", + "sagemaker.tuner": "HyperParameterTuningJob", + "sagemaker.processing": "ProcessingJob", + "sagemaker.pipeline": "Pipeline", + "sagemaker.multidatamodel": "ModelBuilder", + "sagemaker.automl": "AutoMLJob", + "sagemaker.algorithm": "ModelTrainer", +} + + +def _fresh_import(module): + """Import a module fresh, bypassing any cached (failed) import state.""" + import sys + + sys.modules.pop(module, None) + return importlib.import_module(module) + + +@pytest.mark.parametrize("module,expected", sorted(REMOVED_MODULES.items())) +def test_removed_module_raises_actionable_error(module, expected): + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + with pytest.raises(ModuleNotFoundError) as exc: + _fresh_import(module) + message = str(exc.value) + assert module in message + assert "removed in the SageMaker Python SDK v3" in message + assert expected in message + # Every tombstone points at a copy-pasteable import and a docs URL. + assert "from sagemaker." in message + assert "sagemaker.readthedocs.io/en/stable/api/generated/" in message + assert V3_MIGRATION_URL in message + + +@pytest.mark.parametrize("module", sorted(REMOVED_MODULES)) +def test_removed_module_emits_deprecation_warning(module): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + with pytest.raises(ModuleNotFoundError): + _fresh_import(module) + assert any(issubclass(x.category, DeprecationWarning) for x in w) + + +def test_helper_includes_import_and_docs_when_provided(): + docs = "https://sagemaker.readthedocs.io/en/stable/api/generated/sagemaker.train.model_trainer.html" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + with pytest.raises(ModuleNotFoundError) as exc: + raise_removed_in_v3( + module="sagemaker.estimator", + replacement="`ModelTrainer`", + v3_import="from sagemaker.train import ModelTrainer", + v3_docs=docs, + ) + message = str(exc.value) + assert "from sagemaker.train import ModelTrainer" in message + assert docs in message + # ModuleNotFoundError carries the module name for tooling. + assert exc.value.name == "sagemaker.estimator" + + +def test_helper_without_replacement_still_actionable(): + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + with pytest.raises(ModuleNotFoundError) as exc: + raise_removed_in_v3(module="sagemaker.something") + message = str(exc.value) + assert "sagemaker.something" in message + assert V3_MIGRATION_URL in message + + +# --- Fallback finder (catch-all for non-tombstoned removed modules) --- + + +def test_finder_is_registered_on_meta_path(): + import sys + import sagemaker.core # noqa: F401 -- import registers the finder + + assert any(isinstance(f, _RemovedV2ModuleFinder) for f in sys.meta_path) + + +def test_finder_registered_on_bare_sagemaker_import(): + import sys + import importlib + + # The namespace package init registers the finder, so it is active even + # without importing sagemaker.core explicitly. + for name in [m for m in list(sys.modules) if m == "sagemaker" or m.startswith("sagemaker.")]: + pass # (do not evict already-imported real modules; just assert state) + import sagemaker # noqa: F401 -- runs the namespace __init__ + + importlib.import_module("sagemaker") + assert any(isinstance(f, _RemovedV2ModuleFinder) for f in sys.meta_path) + + +def test_removed_module_guidance_on_first_import_in_fresh_process(): + # Regression: a removed module as the VERY FIRST sagemaker import (before + # sagemaker.core is otherwise loaded) must still get guidance, not a bare + # error. Run in a fresh interpreter so import state cannot leak in, and + # capture the message from the exception itself (the console traceback + # formatter does not always write plain text to stderr). + import subprocess + import sys + + code = ( + "import sys\n" + "try:\n" + " import sagemaker.estimator\n" + "except ModuleNotFoundError as e:\n" + " sys.stdout.write(str(e))\n" + ) + proc = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True) + assert "was removed in the SageMaker Python SDK v3" in proc.stdout + assert "from sagemaker.train import ModelTrainer" in proc.stdout + + +def test_register_is_idempotent(): + import sys + + register_removed_module_finder() + register_removed_module_finder() + finders = [f for f in sys.meta_path if isinstance(f, _RemovedV2ModuleFinder)] + assert len(finders) == 1 + + +def test_finder_is_appended_not_prepended(): + # As a last resort it must sit at the end so real/tombstone modules resolve + # first. Being in the second half of meta_path is a sufficient proxy. + import sys + + register_removed_module_finder() + idx = next(i for i, f in enumerate(sys.meta_path) if isinstance(f, _RemovedV2ModuleFinder)) + assert idx >= len(sys.meta_path) // 2 + + +@pytest.mark.parametrize("module", ["sagemaker.clarify", "sagemaker.session", "sagemaker.network"]) +def test_uncovered_module_falls_back_to_guidance(module): + import sagemaker.core # noqa: F401 -- ensure finder registered + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + with pytest.raises(ModuleNotFoundError) as exc: + _fresh_import(module) + message = str(exc.value) + assert module in message + assert "was removed in the SageMaker Python SDK v3" in message + assert V3_MIGRATION_URL in message + assert any(issubclass(x.category, DeprecationWarning) for x in w) + + +@pytest.mark.parametrize("module", ["sagemaker.foobar", "sagemaker.not_a_module", "sagemaker.zzz"]) +def test_unknown_name_gets_plain_error_not_guidance(module): + # Names that never existed in v2 (typos, hallucinated imports) must fall + # through to a plain ModuleNotFoundError -- no "was removed" claim, no + # migration-guide link, no deprecation warning. + import sagemaker.core # noqa: F401 -- ensure finder registered + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + with pytest.raises(ModuleNotFoundError) as exc: + _fresh_import(module) + message = str(exc.value) + assert "was removed" not in message + assert V3_MIGRATION_URL not in message + assert not any(issubclass(x.category, DeprecationWarning) for x in w) + + +def test_finder_passes_through_unknown_names(): + import sagemaker.core # noqa: F401 + + finder = _RemovedV2ModuleFinder() + # Known removed v2 module -> guarded (raises); unknown -> pass through (None). + assert finder.find_spec("sagemaker.foobar") is None + assert finder.find_spec("sagemaker.definitely_not_real") is None + + +@pytest.mark.parametrize( + "module", + [ + "sagemaker.core", + "sagemaker.train", + "sagemaker.serve", + "sagemaker.mlops", + "sagemaker.lineage", + ], +) +def test_finder_does_not_shadow_real_v3_packages(module): + import sagemaker.core # noqa: F401 -- ensure finder registered + + # The fallback finder must not intercept real v3 packages: find_spec must + # return None (pass-through), regardless of whether the package happens to + # be installed in the current environment. (The sagemaker-core unit-test job + # installs core only, so sagemaker.train/serve/mlops are not importable + # there -- asserting find_spec pass-through avoids that false dependency.) + finder = _RemovedV2ModuleFinder() + assert finder.find_spec(module) is None + + +def test_finder_ignores_non_sagemaker_and_nested(): + import sagemaker.core # noqa: F401 + + finder = _RemovedV2ModuleFinder() + # Not a sagemaker module -> pass through (None). + assert finder.find_spec("numpy") is None + # Nested path under a (removed) top-level -> only the top-level is guarded. + assert finder.find_spec("sagemaker.workflow.steps") is None + # Real v3 top-level -> never guarded. + assert finder.find_spec("sagemaker.train") is None