Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions python/scripts/dependencies/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
31 changes: 31 additions & 0 deletions python/scripts/dependencies/_dependency_bounds_release_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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),
)


Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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

Expand Down