From 4d947d4443aef1e2d45a97159bc82a6d0384f310 Mon Sep 17 00:00:00 2001
From: Sankalp Gilda <23521054+astrogilda@users.noreply.github.com>
Date: Tue, 6 Oct 2026 06:01:11 +0000
Subject: [PATCH 1/5] fix(packaging): preserve editable namespace sibling
imports
Remove the legacy namespace bootstrap and add maintained PEP 660 sibling checks in both installation orders for Python 3.10 through 3.14.
---
.github/workflows/namespace-install.yml | 71 +++++++++
setup.py | 1 -
tests/packaging/check_namespace_install.py | 163 +++++++++++++++++++++
3 files changed, 234 insertions(+), 1 deletion(-)
create mode 100644 .github/workflows/namespace-install.yml
create mode 100644 tests/packaging/check_namespace_install.py
diff --git a/.github/workflows/namespace-install.yml b/.github/workflows/namespace-install.yml
new file mode 100644
index 0000000000..85d608d219
--- /dev/null
+++ b/.github/workflows/namespace-install.yml
@@ -0,0 +1,71 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License 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.
+
+name: Namespace package installation
+
+on:
+ push:
+ branches: [main]
+ paths:
+ - 'setup.py'
+ - 'setup.cfg'
+ - 'MANIFEST.in'
+ - 'google/**'
+ - 'vertexai/**'
+ - 'agentplatform/**'
+ - 'tests/packaging/**'
+ - '.github/workflows/namespace-install.yml'
+ pull_request:
+ branches: [main]
+ paths:
+ - 'setup.py'
+ - 'setup.cfg'
+ - 'MANIFEST.in'
+ - 'google/**'
+ - 'vertexai/**'
+ - 'agentplatform/**'
+ - 'tests/packaging/**'
+ - '.github/workflows/namespace-install.yml'
+
+permissions:
+ contents: read
+
+jobs:
+ namespace-install:
+ name: Namespace install (Python ${{ matrix.python-version }})
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
+ steps:
+ - name: Checkout source
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ with:
+ persist-credentials: false
+ - name: Set up Python
+ uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install uv
+ uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
+ - name: Build and check installed namespace packages
+ run: |
+ set -euo pipefail
+ export UV_EXCLUDE_NEWER=$(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ)
+ uv venv --python '${{ matrix.python-version }}' .venv
+ uv pip install --python .venv/bin/python \
+ 'build==1.3.0' 'setuptools==78.1.1' 'wheel==0.45.1' 'flit-core==3.12.0'
+ uv pip freeze --python .venv/bin/python | tee "$RUNNER_TEMP/namespace-build-dependencies.txt"
+ .venv/bin/python -I -B -m build --wheel --no-isolation --outdir dist .
+ .venv/bin/python -I -B tests/packaging/check_namespace_install.py dist/*.whl
diff --git a/setup.py b/setup.py
index 08c5a4ced4..c10e2704cd 100644
--- a/setup.py
+++ b/setup.py
@@ -310,7 +310,6 @@
"tb-gcp-uploader=google.cloud.aiplatform.tensorboard.uploader_main:run_main"
],
},
- namespace_packages=("google", "google.cloud"),
author="Google LLC",
author_email="googleapis-packages@google.com",
license="Apache 2.0",
diff --git a/tests/packaging/check_namespace_install.py b/tests/packaging/check_namespace_install.py
new file mode 100644
index 0000000000..e626d8a4dc
--- /dev/null
+++ b/tests/packaging/check_namespace_install.py
@@ -0,0 +1,163 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License 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.
+
+"""Check installed provider and native editable namespace package coexistence."""
+
+import argparse
+import hashlib
+import json
+from pathlib import Path
+import subprocess
+import sys
+import tempfile
+import venv
+import zipfile
+
+
+def run(arguments, **kwargs):
+ result = subprocess.run(arguments, capture_output=True, text=True, **kwargs)
+ if result.returncode:
+ sys.stdout.write(result.stdout)
+ sys.stderr.write(result.stderr)
+ result.check_returncode()
+ return result
+
+
+def check(provider_wheel, directory, install_order):
+ sibling = directory / "sibling"
+ package = sibling / "src" / "google" / "namespace_install_probe"
+ package.mkdir(parents=True)
+ (package / "__init__.py").write_text(
+ '"""A native editable namespace sibling."""\nVALUE = "editable sibling"\n'
+ )
+ (sibling / "pyproject.toml").write_text(
+ '[build-system]\nrequires = ["flit_core>=3.12,<4"]\n'
+ 'build-backend = "flit_core.buildapi"\n'
+ '[project]\nname = "google-namespace-install-probe"\nversion = "0.0.0"\n'
+ 'description = "Namespace installation control"\n'
+ '[tool.flit.module]\nname = "google.namespace_install_probe"\n'
+ )
+ editable_output = directory / "editable-wheel"
+ editable_output.mkdir()
+ run(
+ [
+ sys.executable,
+ "-I",
+ "-B",
+ "-c",
+ "import sys; from flit_core.buildapi import build_editable; "
+ "print(build_editable(sys.argv[1]))",
+ str(editable_output),
+ ],
+ cwd=sibling,
+ )
+ editable_wheels = list(editable_output.glob("*.whl"))
+ assert len(editable_wheels) == 1, editable_wheels
+ editable_wheel = editable_wheels[0]
+ environment = directory / "environment"
+ venv.EnvBuilder(with_pip=True).create(environment)
+ python = environment / (
+ "Scripts/python.exe" if sys.platform == "win32" else "bin/python"
+ )
+ wheels = {"provider": provider_wheel, "sibling": editable_wheel}
+ for selected in install_order:
+ run(
+ [
+ str(python),
+ "-I",
+ "-B",
+ "-m",
+ "pip",
+ "install",
+ "--no-deps",
+ str(wheels[selected]),
+ ]
+ )
+ expected = {}
+ with zipfile.ZipFile(provider_wheel) as archive:
+ for name in archive.namelist():
+ if name.endswith((".py", ".pth")):
+ assert not name.startswith("/") and ".." not in Path(name).parts
+ expected[name] = hashlib.sha256(archive.read(name)).hexdigest()
+ run(
+ [
+ str(python),
+ "-I",
+ "-B",
+ "-c",
+ """
+import hashlib, importlib.metadata, json, sys
+from pathlib import Path
+distribution = importlib.metadata.distribution("google-cloud-aiplatform")
+expected = json.load(sys.stdin)
+for name, digest in expected.items():
+ path = Path(distribution.locate_file(name))
+ assert path.is_file() and not path.is_symlink(), name
+ assert hashlib.sha256(path.read_bytes()).hexdigest() == digest, name
+""",
+ ],
+ input=json.dumps(expected),
+ )
+ probe = """
+import importlib.util, json
+import google.namespace_install_probe as sibling
+assert sibling.VALUE == "editable sibling"
+provider = importlib.util.find_spec("google.cloud.aiplatform")
+assert provider is not None and provider.origin is not None
+print(json.dumps({"sibling": sibling.__file__, "provider": provider.origin}))
+"""
+ normal = run([str(python), "-B", "-c", probe], cwd=directory)
+ isolated = run([str(python), "-I", "-B", "-c", probe], cwd=directory)
+ normal_result = json.loads(normal.stdout)
+ isolated_result = json.loads(isolated.stdout)
+ for result in [normal_result, isolated_result]:
+ assert Path(result["sibling"]).resolve() == (package / "__init__.py").resolve()
+ assert Path(result["provider"]).resolve().is_relative_to(environment.resolve())
+ return {
+ "installOrder": install_order,
+ "normalImport": normal_result,
+ "isolatedImport": isolated_result,
+ "providerRuntimeAndStartupFilesVerified": len(expected),
+ "editableWheelSHA256": hashlib.sha256(editable_wheel.read_bytes()).hexdigest(),
+ }
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("provider_wheel", type=Path)
+ arguments = parser.parse_args()
+ provider_wheel = arguments.provider_wheel.resolve(strict=True)
+ results = []
+ with tempfile.TemporaryDirectory(prefix="namespace-install-") as temporary:
+ for index, order in enumerate(
+ [("provider", "sibling"), ("sibling", "provider")]
+ ):
+ directory = Path(temporary) / str(index)
+ directory.mkdir()
+ results.append(check(provider_wheel, directory, order))
+ print(
+ json.dumps(
+ {
+ "python": sys.version,
+ "providerWheel": provider_wheel.name,
+ "providerWheelSHA256": hashlib.sha256(
+ provider_wheel.read_bytes()
+ ).hexdigest(),
+ "results": results,
+ },
+ indent=2,
+ )
+ )
+
+
+if __name__ == "__main__":
+ main()
From 93374b2a84483e8e8e0307e8d8f4e58e9d9b228b Mon Sep 17 00:00:00 2001
From: Sankalp Gilda <23521054+astrogilda@users.noreply.github.com>
Date: Tue, 6 Oct 2026 06:22:35 +0000
Subject: [PATCH 2/5] docs: align contribution guide with declared Python
sessions
---
CONTRIBUTING.rst | 31 +++++++++++--------------------
owlbot.py | 2 ++
2 files changed, 13 insertions(+), 20 deletions(-)
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index ff71365e69..1d0557075e 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -1,4 +1,3 @@
-.. Generated by synthtool. DO NOT EDIT!
############
Contributing
############
@@ -22,7 +21,7 @@ In order to add a feature:
documentation.
- The feature must work fully on the following CPython versions:
- 3.9, 3.10, 3.11, 3.12, and 3.13 on both UNIX and Windows.
+ 3.10, 3.11, 3.12, 3.13, and 3.14 on both UNIX and Windows.
- The feature must not add unnecessary dependencies (where
"unnecessary" is of course subjective, but new dependencies should
@@ -68,6 +67,7 @@ Using ``nox``
We use `nox `__ to instrument our tests.
- To test your changes, run unit tests with ``nox``::
+
$ nox -s unit
- To run a single unit test::
@@ -143,13 +143,13 @@ Running System Tests
$ nox -s system
# Run a single system test
- $ nox -s system-3.9 -- -k
+ $ nox -s system-3.10 -- -k
.. note::
- System tests are only configured to run under Python 3.9.
- For expediency, we do not run them in older versions of Python 3.
+ System tests are only configured to run under Python 3.10.
+ See ``SYSTEM_TEST_PYTHON_VERSIONS`` in ``noxfile.py``.
This alone will not run the tests. You'll need to change some local
auth settings and change some configuration in your project to
@@ -195,11 +195,11 @@ configure them just like the System Tests.
# Run all tests in a folder
$ cd samples/snippets
- $ nox -s py-3.9
+ $ nox -s py-3.10
# Run a single sample test
$ cd samples/snippets
- $ nox -s py-3.9 -- -k
+ $ nox -s py-3.10 -- -k
********************************************
Note About ``README`` as it pertains to PyPI
@@ -221,17 +221,17 @@ Supported Python Versions
We support:
-- `Python 3.9`_
- `Python 3.10`_
- `Python 3.11`_
- `Python 3.12`_
- `Python 3.13`_
+- `Python 3.14`_
-.. _Python 3.9: https://docs.python.org/3.9/
.. _Python 3.10: https://docs.python.org/3.10/
.. _Python 3.11: https://docs.python.org/3.11/
.. _Python 3.12: https://docs.python.org/3.12/
.. _Python 3.13: https://docs.python.org/3.13/
+.. _Python 3.14: https://docs.python.org/3.14/
Supported versions can be found in our ``noxfile.py`` `config`_.
@@ -239,17 +239,8 @@ Supported versions can be found in our ``noxfile.py`` `config`_.
.. _config: https://github.com/googleapis/python-aiplatform/blob/main/noxfile.py
-We also explicitly decided to support Python 3 beginning with version 3.9.
-Reasons for this include:
-
-- Encouraging use of newest versions of Python 3
-- Taking the lead of `prominent`_ open-source `projects`_
-- `Unicode literal support`_ which allows for a cleaner codebase that
- works in both Python 2 and Python 3
-
-.. _prominent: https://docs.djangoproject.com/en/1.9/faq/install/#what-python-version-can-i-use-with-django
-.. _projects: http://flask.pocoo.org/docs/0.10/python3/
-.. _Unicode literal support: https://www.python.org/dev/peps/pep-0414/
+The minimum supported Python version is defined by ``python_requires`` in
+``setup.py``.
**********
Versioning
diff --git a/owlbot.py b/owlbot.py
index 7e9f3ada9d..bf2a51015a 100644
--- a/owlbot.py
+++ b/owlbot.py
@@ -65,6 +65,7 @@
".pre-commit-config.yaml",
"setup.py",
"README.rst",
+ "CONTRIBUTING.rst",
"docs/index.rst",
"docs/summary_overview.md",
f"docs/definition_{library.name}/services.rst",
@@ -134,6 +135,7 @@
".github/PULL_REQUEST_TEMPLATE.md",
".github/workflows", # exclude gh actions as credentials are needed for tests
"README.rst",
+ "CONTRIBUTING.rst",
".github/release-please.yml", # use release please manifest
"noxfile.py",
"testing",
From 3a4e5c52a760aea6be53f96e5653312034de661f Mon Sep 17 00:00:00 2001
From: Sankalp Gilda <23521054+astrogilda@users.noreply.github.com>
Date: Tue, 6 Oct 2026 07:24:37 +0000
Subject: [PATCH 3/5] fix(packaging): use native interpreter environment
bootstrap
---
tests/packaging/check_namespace_install.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/tests/packaging/check_namespace_install.py b/tests/packaging/check_namespace_install.py
index e626d8a4dc..850adf3d20 100644
--- a/tests/packaging/check_namespace_install.py
+++ b/tests/packaging/check_namespace_install.py
@@ -19,7 +19,6 @@
import subprocess
import sys
import tempfile
-import venv
import zipfile
@@ -64,7 +63,7 @@ def check(provider_wheel, directory, install_order):
assert len(editable_wheels) == 1, editable_wheels
editable_wheel = editable_wheels[0]
environment = directory / "environment"
- venv.EnvBuilder(with_pip=True).create(environment)
+ run([sys.executable, "-I", "-B", "-m", "venv", str(environment)])
python = environment / (
"Scripts/python.exe" if sys.platform == "win32" else "bin/python"
)
From ddb0e03ad8aeeefc6e0d3e994d5bb993da47de4a Mon Sep 17 00:00:00 2001
From: Sankalp Gilda <23521054+astrogilda@users.noreply.github.com>
Date: Tue, 6 Oct 2026 07:48:34 +0000
Subject: [PATCH 4/5] fix(packaging): declare Python 3 wheel support
---
owlbot.py | 4 ++++
setup.cfg | 2 +-
tests/packaging/check_namespace_install.py | 17 +++++++++++++++++
3 files changed, 22 insertions(+), 1 deletion(-)
diff --git a/owlbot.py b/owlbot.py
index bf2a51015a..76fec737b7 100644
--- a/owlbot.py
+++ b/owlbot.py
@@ -143,6 +143,10 @@
],
) # the microgenerator has a good coveragerc file
+ # This package requires Python 3.10 or later. Generated configuration
+ # must not advertise Python 2 compatibility.
+ s.replace("setup.cfg", r"^universal[ \t]*=[ \t]*1[ \t]*$", "universal = 0")
+
python.py_samples(skip_readmes=True)
python.configure_previous_major_version_branches()
diff --git a/setup.cfg b/setup.cfg
index 0523500895..3e7fd44706 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -16,4 +16,4 @@
# Generated by synthtool. DO NOT EDIT!
[bdist_wheel]
-universal = 1
+universal = 0
diff --git a/tests/packaging/check_namespace_install.py b/tests/packaging/check_namespace_install.py
index 850adf3d20..3bb4869036 100644
--- a/tests/packaging/check_namespace_install.py
+++ b/tests/packaging/check_namespace_install.py
@@ -135,6 +135,23 @@ def main():
parser.add_argument("provider_wheel", type=Path)
arguments = parser.parse_args()
provider_wheel = arguments.provider_wheel.resolve(strict=True)
+ if not provider_wheel.name.endswith("-py3-none-any.whl"):
+ raise ValueError(
+ f"Wheel filename does not declare Python 3: {provider_wheel.name}"
+ )
+ with zipfile.ZipFile(provider_wheel) as archive:
+ wheel_metadata = [
+ name for name in archive.namelist() if name.endswith(".dist-info/WHEEL")
+ ]
+ if len(wheel_metadata) != 1:
+ raise ValueError(f"Expected one WHEEL metadata member: {wheel_metadata}")
+ tags = [
+ line.removeprefix("Tag: ")
+ for line in archive.read(wheel_metadata[0]).decode().splitlines()
+ if line.startswith("Tag: ")
+ ]
+ if tags != ["py3-none-any"]:
+ raise ValueError(f"Wheel metadata does not declare only Python 3: {tags}")
results = []
with tempfile.TemporaryDirectory(prefix="namespace-install-") as temporary:
for index, order in enumerate(
From 0ff246a3e760a68947616b316b4c149ea5feb3b9 Mon Sep 17 00:00:00 2001
From: Sankalp Gilda <23521054+astrogilda@users.noreply.github.com>
Date: Tue, 6 Oct 2026 08:04:07 +0000
Subject: [PATCH 5/5] fix(packaging): make wheel dependency order reproducible
---
.github/workflows/namespace-install.yml | 19 +++++++++++++++++--
setup.py | 10 +++++-----
2 files changed, 22 insertions(+), 7 deletions(-)
diff --git a/.github/workflows/namespace-install.yml b/.github/workflows/namespace-install.yml
index 85d608d219..200431dcc1 100644
--- a/.github/workflows/namespace-install.yml
+++ b/.github/workflows/namespace-install.yml
@@ -18,6 +18,7 @@ on:
paths:
- 'setup.py'
- 'setup.cfg'
+ - 'owlbot.py'
- 'MANIFEST.in'
- 'google/**'
- 'vertexai/**'
@@ -29,6 +30,7 @@ on:
paths:
- 'setup.py'
- 'setup.cfg'
+ - 'owlbot.py'
- 'MANIFEST.in'
- 'google/**'
- 'vertexai/**'
@@ -67,5 +69,18 @@ jobs:
uv pip install --python .venv/bin/python \
'build==1.3.0' 'setuptools==78.1.1' 'wheel==0.45.1' 'flit-core==3.12.0'
uv pip freeze --python .venv/bin/python | tee "$RUNNER_TEMP/namespace-build-dependencies.txt"
- .venv/bin/python -I -B -m build --wheel --no-isolation --outdir dist .
- .venv/bin/python -I -B tests/packaging/check_namespace_install.py dist/*.whl
+ export SOURCE_DATE_EPOCH=946684800
+ PYTHONHASHSEED=1 PYTHONPATH= .venv/bin/python -B -m build \
+ --wheel --no-isolation --outdir dist/seed-1 .
+ PYTHONHASHSEED=2 PYTHONPATH= .venv/bin/python -B -m build \
+ --wheel --no-isolation --outdir dist/seed-2 .
+ .venv/bin/python -I -B - <<'PY'
+ import hashlib
+ from pathlib import Path
+ first, second = [list(Path(f"dist/seed-{seed}").glob("*.whl")) for seed in [1, 2]]
+ assert len(first) == len(second) == 1
+ assert first[0].name == second[0].name
+ assert first[0].read_bytes() == second[0].read_bytes(), "hash seed changed wheel bytes"
+ print("Hash-seed reproducibility:", hashlib.sha256(first[0].read_bytes()).hexdigest())
+ PY
+ .venv/bin/python -I -B tests/packaging/check_namespace_install.py dist/seed-1/*.whl
diff --git a/setup.py b/setup.py
index c10e2704cd..9391cbf8a4 100644
--- a/setup.py
+++ b/setup.py
@@ -174,7 +174,7 @@
"aiohttp", # for ADK users to use aiohttp rather than httpx client
]
-adk_testing_extra_require = list(
+adk_testing_extra_require = sorted(
set(
adk_extra_require + reasoning_engine_extra_require + ["absl-py", "pytest-xdist"]
)
@@ -203,7 +203,7 @@
"openinference-instrumentation-langchain >= 0.1.19, < 0.2",
]
-langchain_testing_extra_require = list(
+langchain_testing_extra_require = sorted(
set(
langchain_extra_require
+ reasoning_engine_extra_require
@@ -216,7 +216,7 @@
"openinference-instrumentation-autogen >= 0.1.6, < 0.2",
]
-ag2_testing_extra_require = list(
+ag2_testing_extra_require = sorted(
set(
ag2_extra_require + reasoning_engine_extra_require + ["absl-py", "pytest-xdist"]
)
@@ -228,7 +228,7 @@
"openinference-instrumentation-llama-index >= 3.0, < 4.0",
]
-llama_index_testing_extra_require = list(
+llama_index_testing_extra_require = sorted(
set(
llama_index_extra_require
+ reasoning_engine_extra_require
@@ -239,7 +239,7 @@
tokenization_extra_require = ["sentencepiece >= 0.2.0"]
tokenization_testing_extra_require = tokenization_extra_require + ["nltk"]
-full_extra_require = list(
+full_extra_require = sorted(
set(
tensorboard_extra_require
+ metadata_extra_require