From 9796730d51b42986aa6354b2bfde65e906272ea2 Mon Sep 17 00:00:00 2001 From: Mohamed Zeidan Date: Fri, 25 Sep 2026 12:45:23 -0700 Subject: [PATCH] fix: stop sagemaker-train/serve/mlops from shipping sagemaker/__init__.py (#5795) sagemaker-core, -train, -serve, and -mlops each shipped an identical top-level sagemaker/__init__.py. Installing two of them writes the same file path, which strict installers and OS package managers (e.g. FreeBSD pkg) reject as a conflict. It also meant the last-installed wheel silently clobbered the others' init -- and sagemaker-core's is the only one that registers the v2->v3 removed- module migration finder, so that guidance could be lost depending on install order. Make sagemaker-core the sole owner of sagemaker/__init__.py (it is a required dependency of the other three, so the file is always present exactly once). The other three now contribute to the sagemaker namespace via PEP 420 / the pkgutil extend_path in core's init. Add per-distribution regression guards. --- sagemaker-mlops/src/sagemaker/__init__.py | 3 -- .../tests/unit/test_namespace_packaging.py | 38 +++++++++++++++++++ sagemaker-mlops/tox.ini | 4 +- sagemaker-serve/src/sagemaker/__init__.py | 3 -- .../tests/unit/test_namespace_packaging.py | 38 +++++++++++++++++++ sagemaker-serve/tox.ini | 4 +- sagemaker-train/src/sagemaker/__init__.py | 3 -- .../tests/unit/test_namespace_packaging.py | 38 +++++++++++++++++++ sagemaker-train/tox.ini | 4 +- 9 files changed, 120 insertions(+), 15 deletions(-) delete mode 100644 sagemaker-mlops/src/sagemaker/__init__.py create mode 100644 sagemaker-mlops/tests/unit/test_namespace_packaging.py delete mode 100644 sagemaker-serve/src/sagemaker/__init__.py create mode 100644 sagemaker-serve/tests/unit/test_namespace_packaging.py delete mode 100644 sagemaker-train/src/sagemaker/__init__.py create mode 100644 sagemaker-train/tests/unit/test_namespace_packaging.py diff --git a/sagemaker-mlops/src/sagemaker/__init__.py b/sagemaker-mlops/src/sagemaker/__init__.py deleted file mode 100644 index 33b1b0d2b8..0000000000 --- a/sagemaker-mlops/src/sagemaker/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Namespace package for SageMaker.""" - -__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/sagemaker-mlops/tests/unit/test_namespace_packaging.py b/sagemaker-mlops/tests/unit/test_namespace_packaging.py new file mode 100644 index 0000000000..6e6cfe1a6e --- /dev/null +++ b/sagemaker-mlops/tests/unit/test_namespace_packaging.py @@ -0,0 +1,38 @@ +# 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. +"""Regression guard for GH #5795. + +sagemaker-mlops must not ship its own top-level ``sagemaker/__init__.py``: +sagemaker-core owns that file, and having two distributions install the same +path makes strict installers / OS package managers refuse to co-install them. +The ``sagemaker`` package is a namespace shared across the sub-distributions. +""" + +from __future__ import absolute_import + +from pathlib import Path + +_SRC = Path(__file__).resolve().parents[2] / "src" + + +def test_does_not_ship_top_level_sagemaker_init(): + top_level_init = _SRC / "sagemaker" / "__init__.py" + assert not top_level_init.exists(), ( + f"{top_level_init} must not exist: sagemaker-core owns " + "sagemaker/__init__.py (see GH #5795)." + ) + + +def test_still_ships_the_subpackage_init(): + # The distribution's own sub-package must remain a real package. + assert (_SRC / "sagemaker" / "mlops" / "__init__.py").exists() diff --git a/sagemaker-mlops/tox.ini b/sagemaker-mlops/tox.ini index df0cde820c..a1b3d0a6f9 100644 --- a/sagemaker-mlops/tox.ini +++ b/sagemaker-mlops/tox.ini @@ -136,7 +136,7 @@ setenv = deps = -r ../requirements/tox/pylint_requirements.txt commands = - python -m pylint --rcfile=../.pylintrc -j 0 src/sagemaker --fail-under=9.9 + python -m pylint --rcfile=../.pylintrc -j 0 --recursive=y src/sagemaker --fail-under=9.9 [testenv:spelling] skipdist = true @@ -144,7 +144,7 @@ skip_install = true deps = -r ../requirements/tox/spelling_requirements.txt commands = - python -m pylint --rcfile=../.pylintrc --disable all --enable spelling --spelling-dict en_US src/sagemaker/{posargs} + python -m pylint --rcfile=../.pylintrc --disable all --enable spelling --spelling-dict en_US --recursive=y src/sagemaker/{posargs} [testenv:twine] # https://packaging.python.org/guides/making-a-pypi-friendly-readme/#validating-restructuredtext-markup diff --git a/sagemaker-serve/src/sagemaker/__init__.py b/sagemaker-serve/src/sagemaker/__init__.py deleted file mode 100644 index 33b1b0d2b8..0000000000 --- a/sagemaker-serve/src/sagemaker/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Namespace package for SageMaker.""" - -__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/sagemaker-serve/tests/unit/test_namespace_packaging.py b/sagemaker-serve/tests/unit/test_namespace_packaging.py new file mode 100644 index 0000000000..e23914301d --- /dev/null +++ b/sagemaker-serve/tests/unit/test_namespace_packaging.py @@ -0,0 +1,38 @@ +# 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. +"""Regression guard for GH #5795. + +sagemaker-serve must not ship its own top-level ``sagemaker/__init__.py``: +sagemaker-core owns that file, and having two distributions install the same +path makes strict installers / OS package managers refuse to co-install them. +The ``sagemaker`` package is a namespace shared across the sub-distributions. +""" + +from __future__ import absolute_import + +from pathlib import Path + +_SRC = Path(__file__).resolve().parents[2] / "src" + + +def test_does_not_ship_top_level_sagemaker_init(): + top_level_init = _SRC / "sagemaker" / "__init__.py" + assert not top_level_init.exists(), ( + f"{top_level_init} must not exist: sagemaker-core owns " + "sagemaker/__init__.py (see GH #5795)." + ) + + +def test_still_ships_the_subpackage_init(): + # The distribution's own sub-package must remain a real package. + assert (_SRC / "sagemaker" / "serve" / "__init__.py").exists() diff --git a/sagemaker-serve/tox.ini b/sagemaker-serve/tox.ini index c5282ddb37..79732382f8 100644 --- a/sagemaker-serve/tox.ini +++ b/sagemaker-serve/tox.ini @@ -139,7 +139,7 @@ setenv = deps = -r ../requirements/tox/pylint_requirements.txt commands = - python -m pylint --rcfile=../.pylintrc -j 0 src/sagemaker --fail-under=9.9 + python -m pylint --rcfile=../.pylintrc -j 0 --recursive=y src/sagemaker --fail-under=9.9 [testenv:spelling] skipdist = true @@ -147,7 +147,7 @@ skip_install = true deps = -r ../requirements/tox/spelling_requirements.txt commands = - python -m pylint --rcfile=../.pylintrc --disable all --enable spelling --spelling-dict en_US src/sagemaker/{posargs} + python -m pylint --rcfile=../.pylintrc --disable all --enable spelling --spelling-dict en_US --recursive=y src/sagemaker/{posargs} [testenv:twine] # https://packaging.python.org/guides/making-a-pypi-friendly-readme/#validating-restructuredtext-markup diff --git a/sagemaker-train/src/sagemaker/__init__.py b/sagemaker-train/src/sagemaker/__init__.py deleted file mode 100644 index 33b1b0d2b8..0000000000 --- a/sagemaker-train/src/sagemaker/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Namespace package for SageMaker.""" - -__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/sagemaker-train/tests/unit/test_namespace_packaging.py b/sagemaker-train/tests/unit/test_namespace_packaging.py new file mode 100644 index 0000000000..f3c96d8ea2 --- /dev/null +++ b/sagemaker-train/tests/unit/test_namespace_packaging.py @@ -0,0 +1,38 @@ +# 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. +"""Regression guard for GH #5795. + +sagemaker-train must not ship its own top-level ``sagemaker/__init__.py``: +sagemaker-core owns that file, and having two distributions install the same +path makes strict installers / OS package managers refuse to co-install them. +The ``sagemaker`` package is a namespace shared across the sub-distributions. +""" + +from __future__ import absolute_import + +from pathlib import Path + +_SRC = Path(__file__).resolve().parents[2] / "src" + + +def test_does_not_ship_top_level_sagemaker_init(): + top_level_init = _SRC / "sagemaker" / "__init__.py" + assert not top_level_init.exists(), ( + f"{top_level_init} must not exist: sagemaker-core owns " + "sagemaker/__init__.py (see GH #5795)." + ) + + +def test_still_ships_the_subpackage_init(): + # The distribution's own sub-package must remain a real package. + assert (_SRC / "sagemaker" / "train" / "__init__.py").exists() diff --git a/sagemaker-train/tox.ini b/sagemaker-train/tox.ini index b083a374e1..2b631d0bff 100644 --- a/sagemaker-train/tox.ini +++ b/sagemaker-train/tox.ini @@ -143,7 +143,7 @@ setenv = deps = -r ../requirements/tox/pylint_requirements.txt commands = - python -m pylint --rcfile=../.pylintrc -j 0 src/sagemaker --fail-under=9.9 + python -m pylint --rcfile=../.pylintrc -j 0 --recursive=y src/sagemaker --fail-under=9.9 [testenv:spelling] skipdist = true @@ -151,7 +151,7 @@ skip_install = true deps = -r ../requirements/tox/spelling_requirements.txt commands = - python -m pylint --rcfile=../.pylintrc --disable all --enable spelling --spelling-dict en_US src/sagemaker/{posargs} + python -m pylint --rcfile=../.pylintrc --disable all --enable spelling --spelling-dict en_US --recursive=y src/sagemaker/{posargs} [testenv:twine] # https://packaging.python.org/guides/making-a-pypi-friendly-readme/#validating-restructuredtext-markup