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