Files
microsoft--agent-framework/python/scripts/dependencies/upgrade_dev_dependencies.py
T
wehub-resource-sync db620d33df
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:39:25 +08:00

193 lines
6.8 KiB
Python

# Copyright (c) Microsoft. All rights reserved.
"""Refresh development dependency pins across the Python workspace."""
from __future__ import annotations
import argparse
import logging
from dataclasses import dataclass
from pathlib import Path
import tomli
from rich import print
from scripts.dependencies._dependency_bounds_upper_impl import (
VersionCatalog,
_apply_package_replacements,
_collect_development_pin_replacements,
_load_lock_versions,
)
from scripts.task_runner import discover_projects
logger = logging.getLogger(__name__)
@dataclass(frozen=True)
class WorkspaceProject:
"""Workspace project metadata used for development dependency pin refresh."""
name: str
project_path: str
pyproject_path: str
pyproject_file: Path
def _read_project_name(pyproject_file: Path) -> str:
"""Return the normalized project name declared in a pyproject file."""
with pyproject_file.open("rb") as f:
data = tomli.load(f)
project = data.get("project", {}) or {}
project_name = str(project.get("name", "")).strip()
return project_name or pyproject_file.parent.name
def _discover_workspace_projects(workspace_root: Path) -> list[WorkspaceProject]:
"""Return the root project plus all package projects in the workspace."""
workspace_pyproject = workspace_root / "pyproject.toml"
projects = [
WorkspaceProject(
name=_read_project_name(workspace_pyproject),
project_path=".",
pyproject_path="pyproject.toml",
pyproject_file=workspace_pyproject,
)
]
# The root project carries the repo-wide dev toolchain pins, while package pyprojects may
# carry package-specific development groups. Refresh both surfaces in one pass so the
# workspace stays internally consistent after a tooling bump.
# Reuse the shared workspace discovery logic so this script stays aligned with the rest
# of the repo-level task runners when packages are added or moved.
for project in sorted(discover_projects(workspace_pyproject), key=lambda value: str(value)):
pyproject_file = workspace_root / project / "pyproject.toml"
if not pyproject_file.exists():
continue
projects.append(
WorkspaceProject(
name=_read_project_name(pyproject_file),
project_path=str(project),
pyproject_path=str(project / "pyproject.toml"),
pyproject_file=pyproject_file,
)
)
return projects
def _normalize_filter(value: str) -> str:
"""Normalize a package filter for matching project names and paths."""
normalized = value.strip().strip("/").lower()
return normalized or "."
def _select_projects(projects: list[WorkspaceProject], package_filters: list[str] | None) -> list[WorkspaceProject]:
"""Filter workspace projects by package name or workspace path if requested."""
if not package_filters:
return projects
normalized_filters = {_normalize_filter(value) for value in package_filters if value.strip()}
selected: list[WorkspaceProject] = []
for project in projects:
normalized_path = _normalize_filter(project.project_path)
candidates = {project.name.lower(), normalized_path}
if normalized_path != ".":
candidates.add(f"./{normalized_path}")
if candidates & normalized_filters:
selected.append(project)
return selected
def main() -> None:
"""Refresh exact development dependency pins in workspace pyproject files."""
parser = argparse.ArgumentParser(
description=(
"Refresh development dependency pins across the workspace pyproject.toml files. "
"By default, resolves versions from PyPI and falls back to uv.lock when network access is unavailable."
)
)
parser.add_argument(
"--packages",
nargs="*",
default=None,
help="Optional project filters by workspace path (for example packages/core) or package name.",
)
parser.add_argument(
"--version-source",
choices=["pypi", "lock"],
default="pypi",
help="Version source for selecting the newest development dependency pin.",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Print planned replacements without updating files.",
)
parser.add_argument(
"--verbose",
action="store_true",
help="Show debug logging.",
)
args = parser.parse_args()
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO, format="%(message)s")
workspace_root = Path(__file__).resolve().parents[2]
lock_versions = _load_lock_versions(workspace_root)
# Reuse the same version catalog as the bound-expansion tooling so development pin refreshes choose
# versions with the same PyPI-vs-lock fallback behavior as the dependency validators.
catalog = VersionCatalog(lock_versions=lock_versions, source=args.version_source)
selected_projects = _select_projects(
_discover_workspace_projects(workspace_root),
package_filters=args.packages,
)
logger.debug(
"Selected projects for development dependency refresh: %s",
[project.pyproject_path for project in selected_projects],
)
if not selected_projects:
filters = ", ".join(args.packages or [])
raise SystemExit(f"No matching workspace projects found for: {filters}")
updated_projects = 0
updated_requirements = 0
for project in selected_projects:
# Keep the replacement logic centralized in the upper-bound helper so exact development pins are
# formatted consistently regardless of whether we update them directly here or while
# widening runtime dependency bounds.
replacements = _collect_development_pin_replacements(project.pyproject_file, catalog=catalog)
if not replacements:
continue
updated_projects += 1
updated_requirements += len(replacements)
if args.dry_run:
print(f"[yellow]Planned updates for {project.pyproject_path}[/yellow]")
for original, replacement in replacements.items():
print(f" - {original} -> {replacement}")
continue
_apply_package_replacements(project.pyproject_file, replacements)
print(
f"[green]Updated {project.pyproject_path}[/green] "
f"({project.name}) with {len(replacements)} development dependency pin refresh(es)."
)
if updated_projects == 0:
print("[green]No development dependency pin updates were needed.[/green]")
return
action = "Would update" if args.dry_run else "Updated"
print(
f"[green]{action} {updated_requirements} development dependency pin(s) "
f"across {updated_projects} workspace project(s).[/green]"
)
if __name__ == "__main__":
main()