test: assert the all extra stays the union of the runtime extras
Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 963527870
This commit is contained in:
committed by
Copybara-Service
parent
470d59e4a3
commit
b4dc92de57
@@ -27,6 +27,9 @@ regressions documented in the bare-install audit cannot silently re-emerge:
|
||||
objects while deserializing checkpoint data.
|
||||
* ``google-genai`` MUST exclude 2.11 and include 2.12.1, whose types module
|
||||
defers the optional MCP server stack instead of importing it at Agent startup.
|
||||
* The ``all`` extra MUST stay the union of every extra that unlocks a runtime
|
||||
feature, so that ``pip install "google-adk[all]"`` cannot silently stop
|
||||
installing a feature's dependencies.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -42,6 +45,7 @@ except ImportError:
|
||||
from packaging.requirements import Requirement
|
||||
from packaging.specifiers import SpecifierSet
|
||||
from packaging.utils import canonicalize_name
|
||||
from packaging.version import InvalidVersion
|
||||
from packaging.version import Version
|
||||
import pytest
|
||||
|
||||
@@ -52,6 +56,18 @@ _UNSAFE_CHECKPOINT_RELEASES = {
|
||||
'langgraph-checkpoint': (('2.1.0', '3.0.0', '4.0.0', '4.1.0'), '4.1.1'),
|
||||
}
|
||||
|
||||
# Extras that ``all`` deliberately leaves out, for the reason recorded in the
|
||||
# comment above ``optional-dependencies.all`` in pyproject.toml. Every other
|
||||
# extra is part of the ``all`` contract, so a new extra joins that contract
|
||||
# unless it is also listed here.
|
||||
_NON_RUNTIME_EXTRAS = frozenset({
|
||||
'benchmark',
|
||||
'community',
|
||||
'dev',
|
||||
'docs',
|
||||
'test',
|
||||
})
|
||||
|
||||
|
||||
def _find_pyproject() -> Path:
|
||||
"""Locates pyproject.toml by walking up from this file's directory.
|
||||
@@ -120,6 +136,58 @@ def _requirement_specifier(
|
||||
return None
|
||||
|
||||
|
||||
def _runtime_extra_requirements(
|
||||
pyproject: dict,
|
||||
) -> dict[str, list[tuple[str, Requirement]]]:
|
||||
"""Returns what the runtime extras require, keyed by distribution name.
|
||||
|
||||
Each value lists the extras that ask for the distribution, paired with the
|
||||
requirement that extra declares, so a failure can name the extra that ``all``
|
||||
drifted away from.
|
||||
"""
|
||||
contributors: dict[str, list[tuple[str, Requirement]]] = {}
|
||||
for extra, entries in pyproject['project']['optional-dependencies'].items():
|
||||
if extra == 'all' or extra in _NON_RUNTIME_EXTRAS:
|
||||
continue
|
||||
for entry in entries:
|
||||
requirement = Requirement(entry)
|
||||
key = canonicalize_name(requirement.name)
|
||||
contributors.setdefault(key, []).append((extra, requirement))
|
||||
return contributors
|
||||
|
||||
|
||||
def _all_extra_requirements(pyproject: dict) -> dict[str, Requirement]:
|
||||
"""Returns the ``all`` extra's requirements, keyed by distribution name."""
|
||||
entries = pyproject['project']['optional-dependencies']['all']
|
||||
requirements = (Requirement(entry) for entry in entries)
|
||||
return {canonicalize_name(req.name): req for req in requirements}
|
||||
|
||||
|
||||
def _specifier_versions(specifier: SpecifierSet) -> set[Version]:
|
||||
"""Returns the version literals a specifier mentions.
|
||||
|
||||
Wildcard clauses such as ``==1.2.*`` name no single version and are skipped.
|
||||
"""
|
||||
versions: set[Version] = set()
|
||||
for clause in specifier:
|
||||
try:
|
||||
versions.add(Version(clause.version))
|
||||
except InvalidVersion:
|
||||
continue
|
||||
return versions
|
||||
|
||||
|
||||
def _expected_marker(requirements: list[Requirement]) -> str:
|
||||
"""Returns the marker text the union of ``requirements`` should carry.
|
||||
|
||||
An empty string means the union must be unmarked. Contributors that disagree
|
||||
about a marker install the distribution between them on every environment any
|
||||
of them names, so the union goes unmarked rather than under-installing.
|
||||
"""
|
||||
markers = {str(req.marker) if req.marker else '' for req in requirements}
|
||||
return markers.pop() if len(markers) == 1 else ''
|
||||
|
||||
|
||||
def test_main_deps_include_packaging(pyproject: dict) -> None:
|
||||
"""``packaging`` is imported unguarded by core ADK; it must be a main dep."""
|
||||
main_deps = _requirement_names(pyproject['project']['dependencies'])
|
||||
@@ -181,6 +249,90 @@ def test_main_deps_require_lazy_mcp_google_genai_release(
|
||||
assert Version('2.12.1') in google_genai.specifier
|
||||
|
||||
|
||||
def test_all_extra_covers_every_runtime_extra(pyproject: dict) -> None:
|
||||
"""``all`` names exactly the distributions the runtime extras name.
|
||||
|
||||
This is the guard against the failure that motivated the union: an extra
|
||||
gains a dependency, nobody mirrors it into ``all``, and users who installed
|
||||
``google-adk[all]`` hit an ImportError for a feature they believed they had.
|
||||
"""
|
||||
contributors = _runtime_extra_requirements(pyproject)
|
||||
all_extra = _all_extra_requirements(pyproject)
|
||||
|
||||
missing = sorted(set(contributors) - set(all_extra))
|
||||
assert not missing, 'The all extra is missing ' + ', '.join(
|
||||
f'{name} (required by'
|
||||
f' {", ".join(sorted(e for e, _ in contributors[name]))})'
|
||||
for name in missing
|
||||
)
|
||||
|
||||
orphaned = sorted(set(all_extra) - set(contributors))
|
||||
assert not orphaned, (
|
||||
f'The all extra requires {", ".join(orphaned)}, which no runtime extra '
|
||||
'declares. Every entry in all belongs to the extra that owns its '
|
||||
'feature, so either declare it there or drop it from all.'
|
||||
)
|
||||
|
||||
|
||||
def test_all_extra_preserves_runtime_extra_constraints(pyproject: dict) -> None:
|
||||
"""``all`` asks for each distribution on the same terms its extras do.
|
||||
|
||||
Installing several extras at once yields the union of their distributions
|
||||
and the intersection of their version constraints, so ``all`` must request
|
||||
the union of the sub-extras named, admit a version exactly when every
|
||||
contributing extra admits it, and carry the environment marker its
|
||||
contributors agree on.
|
||||
"""
|
||||
contributors = _runtime_extra_requirements(pyproject)
|
||||
all_extra = _all_extra_requirements(pyproject)
|
||||
problems: list[str] = []
|
||||
|
||||
for name, sources in sorted(contributors.items()):
|
||||
combined = all_extra.get(name)
|
||||
if combined is None:
|
||||
continue # Already reported as missing by the coverage test.
|
||||
extras = sorted(extra for extra, _ in sources)
|
||||
requirements = [requirement for _, requirement in sources]
|
||||
|
||||
wanted_extras = set().union(*(req.extras for req in requirements))
|
||||
if combined.extras != wanted_extras:
|
||||
problems.append(
|
||||
f'{name}: all requests sub-extras {sorted(combined.extras)}, but '
|
||||
f'{extras} together require {sorted(wanted_extras)}'
|
||||
)
|
||||
|
||||
wanted_marker = _expected_marker(requirements)
|
||||
actual_marker = str(combined.marker) if combined.marker else ''
|
||||
if actual_marker != wanted_marker:
|
||||
problems.append(
|
||||
f'{name}: all is gated on {actual_marker or "nothing"}, but '
|
||||
f'{extras} require {wanted_marker or "no marker"}'
|
||||
)
|
||||
|
||||
candidates = _specifier_versions(combined.specifier)
|
||||
for requirement in requirements:
|
||||
candidates |= _specifier_versions(requirement.specifier)
|
||||
for version in sorted(candidates):
|
||||
admitted_by_all = combined.specifier.contains(version, prereleases=True)
|
||||
admitted_by_extras = all(
|
||||
req.specifier.contains(version, prereleases=True)
|
||||
for req in requirements
|
||||
)
|
||||
if admitted_by_all != admitted_by_extras:
|
||||
problems.append(
|
||||
f'{name}: all and {extras} disagree about version {version}. all '
|
||||
f'declares {combined.specifier or "no constraint"}, against '
|
||||
+ ', '.join(
|
||||
f'{extra}: {req.specifier or "no constraint"}'
|
||||
for extra, req in sources
|
||||
)
|
||||
)
|
||||
|
||||
assert not problems, 'The all extra diverges from its extras:\n' + '\n'.join(
|
||||
problems
|
||||
)
|
||||
|
||||
|
||||
def test_environment_simulation_config_imports_validation_error_from_pydantic() -> (
|
||||
None
|
||||
):
|
||||
|
||||
Reference in New Issue
Block a user