1d0b7a197f
Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 966245462
108 lines
3.4 KiB
Python
108 lines
3.4 KiB
Python
# Copyright 2026 Google LLC
|
|
#
|
|
# 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
|
|
#
|
|
# http://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.
|
|
|
|
"""Runs snippets in a fresh interpreter to assert import-time side effects.
|
|
|
|
Imports are process-global and irreversible, so any assertion about what a
|
|
package pulls in has to happen in a process that has not already imported it.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
SOURCE_ROOT = REPO_ROOT / 'src'
|
|
|
|
|
|
def run_isolated(source: str) -> subprocess.CompletedProcess[str]:
|
|
"""Runs source against this checkout in a fresh Python process."""
|
|
env = os.environ.copy()
|
|
source_path = str(SOURCE_ROOT)
|
|
current_pythonpath = env.get('PYTHONPATH')
|
|
env['PYTHONPATH'] = (
|
|
source_path
|
|
if not current_pythonpath
|
|
else os.pathsep.join((source_path, current_pythonpath))
|
|
)
|
|
# Run from an empty directory so the interpreter's implicit sys.path[0] entry
|
|
# cannot shadow a stdlib module: ADK ships a ``platform`` package that would
|
|
# otherwise mask stdlib ``platform`` (breaking uuid, pydantic, ...) here.
|
|
with tempfile.TemporaryDirectory() as isolated_cwd:
|
|
return subprocess.run(
|
|
[sys.executable, '-c', source],
|
|
cwd=isolated_cwd,
|
|
env=env,
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
|
|
def loaded_top_level_packages(source: str) -> frozenset[str]:
|
|
"""Returns the third-party top-level packages source leaves imported.
|
|
|
|
Standard-library modules, private modules and the pseudo-modules the
|
|
interpreter injects carry no install or startup cost of their own, so they
|
|
are dropped and only the distributions a caller pays for remain.
|
|
|
|
An empty namespace portion is dropped for the same reason: it holds no code
|
|
to run, and a failed probe for one of its submodules leaves it behind even
|
|
though nothing was loaded. A namespace whose submodule did load still counts,
|
|
because that submodule reports the same top-level name.
|
|
"""
|
|
result = run_isolated(f"""
|
|
import json
|
|
import sys
|
|
{source}
|
|
|
|
names = {{
|
|
name.partition('.')[0]
|
|
for name, module in sys.modules.items()
|
|
if getattr(module, '__spec__', None) is not None
|
|
and module.__spec__.origin is not None
|
|
}}
|
|
print(json.dumps(sorted(
|
|
name
|
|
for name in names - sys.stdlib_module_names
|
|
if not name.startswith('_')
|
|
)))
|
|
""")
|
|
assert result.returncode == 0, result.stderr
|
|
return frozenset(json.loads(result.stdout.splitlines()[-1]))
|
|
|
|
|
|
def assert_modules_unloaded(source: str, forbidden: tuple[str, ...]) -> None:
|
|
"""Asserts source leaves every forbidden module (and submodule) unimported."""
|
|
result = run_isolated(f"""
|
|
import sys
|
|
{source}
|
|
|
|
forbidden = {forbidden!r}
|
|
loaded = [
|
|
prefix
|
|
for prefix in forbidden
|
|
if any(
|
|
name == prefix or name.startswith(prefix + '.') for name in sys.modules
|
|
)
|
|
]
|
|
assert not loaded, loaded
|
|
""")
|
|
assert result.returncode == 0, result.stderr
|