diff --git a/python/scripts/dependencies/README.md b/python/scripts/dependencies/README.md index 8c577d64299..5967dfc7076 100644 --- a/python/scripts/dependencies/README.md +++ b/python/scripts/dependencies/README.md @@ -22,6 +22,9 @@ Run the commands below from the `python/` directory. - Resolves published runtime dependencies and non-development extras independently of `uv.lock` with both `lowest-direct` and `highest` strategies. - Derives the minimum supported Python minor from each changed package's internal editable dependency closure. + - Re-declares any pre-release-bounded external requirement found in that closure as a direct probe requirement so + `prerelease = "if-necessary-or-explicit"` keeps allowing it (uv only enables pre-releases for direct requirements + that carry a pre-release specifier). - Imports each changed package and records resolved dependency versions in a JSON report. - Runs probes concurrently under one five-minute deadline. diff --git a/python/scripts/dependencies/_dependency_bounds_release_impl.py b/python/scripts/dependencies/_dependency_bounds_release_impl.py index b47d8eb85c6..377402ba043 100644 --- a/python/scripts/dependencies/_dependency_bounds_release_impl.py +++ b/python/scripts/dependencies/_dependency_bounds_release_impl.py @@ -50,6 +50,7 @@ class ReleaseProbePlan: import_modules: tuple[str, ...] reported_distributions: tuple[str, ...] python_version: str + prerelease_requirements: tuple[str, ...] = () def _utc_now() -> str: @@ -232,6 +233,33 @@ def _minimum_python_version(projects: list[ReleaseProject]) -> str: return python_version +def _prerelease_requirements( + projects: dict[str, ReleaseProject], + requested_extras: dict[str, set[str]], +) -> tuple[str, ...]: + """Collect external requirements in the closure that carry a pre-release specifier. + + ``uv`` enables pre-releases for a package only when a *direct* requirement carries a + pre-release specifier. Internal packages resolved as editables contribute their own + requirements transitively, so a pre-release floor declared by one of them would otherwise + be ignored. Re-declaring those requirements at the probe root keeps them explicit. + """ + collected: set[str] = set() + for package_name, extras in requested_extras.items(): + project = projects[package_name] + for requirement_text in _requirements_for_extras(project, extras): + try: + requirement = Requirement(requirement_text) + except InvalidRequirement: + continue + if canonicalize_name(requirement.name) in projects: + continue + if not requirement.specifier.prereleases: + continue + collected.add(str(requirement)) + return tuple(sorted(collected)) + + def _build_release_probe_plan( workspace_root: Path, target: ReleaseProject, @@ -295,6 +323,7 @@ def _build_release_probe_plan( import_modules=target.import_modules, reported_distributions=tuple(sorted(reported_distributions)), python_version=_minimum_python_version([projects[package_name] for package_name in requested_extras]), + prerelease_requirements=_prerelease_requirements(projects, requested_extras), ) @@ -337,6 +366,8 @@ def _build_release_probe_command( ] for editable_spec in plan.editable_specs: command.extend(["--with-editable", editable_spec]) + for requirement in plan.prerelease_requirements: + command.extend(["--with", requirement]) command.extend(["python", "-c", probe_script]) return command diff --git a/python/scripts/dependencies/tests/test_dependency_bounds_release.py b/python/scripts/dependencies/tests/test_dependency_bounds_release.py index 88913a10b09..100764b1eb5 100644 --- a/python/scripts/dependencies/tests/test_dependency_bounds_release.py +++ b/python/scripts/dependencies/tests/test_dependency_bounds_release.py @@ -103,6 +103,63 @@ def test_release_probe_uses_only_the_required_internal_dependency_closure(tmp_pa assert root_plan.python_version == "3.10" +def test_release_probe_repeats_internal_prerelease_requirements_at_the_probe_root(tmp_path: Path) -> None: + _write_project( + tmp_path, + """ +[project] +name = "agent-framework" +version = "1.2.0" +requires-python = ">=3.10" +dependencies = ["agent-framework-core[all]==1.2.0"] + +[tool.uv.workspace] +members = ["packages/*"] + +[tool.flit.module] +name = "agent_framework_meta" +""", + ) + _write_project( + tmp_path / "packages/core", + """ +[project] +name = "agent-framework-core" +version = "1.2.0" +requires-python = ">=3.10" +dependencies = ["pydantic>=2,<3"] + +[project.optional-dependencies] +all = ["agent-framework-hosting"] +dev = ["preview-only-dev>=1.0.0b1"] + +[tool.flit.module] +name = "agent_framework" +""", + ) + _write_project( + tmp_path / "packages/hosting", + """ +[project] +name = "agent-framework-hosting" +version = "1.0.0" +requires-python = ">=3.10" +dependencies = ["agent-framework-core>=1,<2", "server-responses>=2.2.0b1,<3", "httpx>=0.28,<1"] + +[tool.flit.module] +name = "agent_framework_hosting" +""", + ) + + projects = _build_release_project_map(tmp_path) + plan = _build_release_probe_plan(tmp_path, projects["agent-framework"], projects) + + assert plan.prerelease_requirements == ("server-responses<3,>=2.2.0b1",) + + command = _build_release_probe_command(plan, resolution="highest") + assert command[command.index("--with") + 1] == "server-responses<3,>=2.2.0b1" + + def test_release_probe_command_is_lock_independent_and_uses_bound_resolution(tmp_path: Path) -> None: plan = ReleaseProbePlan( project_path=Path("packages/openai"), @@ -120,6 +177,7 @@ def test_release_probe_command_is_lock_independent_and_uses_bound_resolution(tmp assert command[command.index("--python") + 1] == "3.11" assert command[command.index("--prerelease") + 1] == "if-necessary-or-explicit" assert command.count("--with-editable") == 2 + assert "--with" not in command assert "pytest" not in command assert "pyright" not in command