diff --git a/.github/workflows/python-dependency-maintenance.yml b/.github/workflows/python-dependency-maintenance.yml index dfb7ca175d..bca0f6aeba 100644 --- a/.github/workflows/python-dependency-maintenance.yml +++ b/.github/workflows/python-dependency-maintenance.yml @@ -41,7 +41,7 @@ jobs: - name: Set dependency release cutoff run: | cutoff="$(date -u -d '7 days ago' '+%Y-%m-%dT%H:%M:%SZ')" - echo "UV_EXCLUDE_NEWER=${cutoff}" >> "$GITHUB_ENV" + echo "DEPENDENCY_RELEASE_CUTOFF=${cutoff}" >> "$GITHUB_ENV" echo "Using dependency release cutoff: ${cutoff}" - name: Repin dev dependency declarations diff --git a/python/packages/hyperlight/agent_framework_hyperlight/_execute_code_tool.py b/python/packages/hyperlight/agent_framework_hyperlight/_execute_code_tool.py index 304cdc095f..b4e5f8c9d3 100644 --- a/python/packages/hyperlight/agent_framework_hyperlight/_execute_code_tool.py +++ b/python/packages/hyperlight/agent_framework_hyperlight/_execute_code_tool.py @@ -90,6 +90,10 @@ class SandboxRuntime(Protocol): def execute(self, *, config: _RunConfig, code: str) -> list[Content]: ... +class _NamedDirectory(Protocol): + name: str + + _T = TypeVar("_T") @@ -725,7 +729,7 @@ def _collect_output_relative_paths(*, sandbox: Any, root: Path) -> set[str]: def _parse_output_files( *, sandbox: Any, - output_dir: TemporaryDirectory[str] | None, + output_dir: _NamedDirectory | None, expect_output_files: bool, ) -> list[Content]: if output_dir is None: diff --git a/python/scripts/dependencies/_dependency_bounds_lower_impl.py b/python/scripts/dependencies/_dependency_bounds_lower_impl.py index 3e272b3eed..36ac82bbeb 100644 --- a/python/scripts/dependencies/_dependency_bounds_lower_impl.py +++ b/python/scripts/dependencies/_dependency_bounds_lower_impl.py @@ -388,7 +388,7 @@ def _fetch(self, package_name: str) -> list[Version]: def _load_exclude_newer_from_env() -> datetime | None: - raw_value = os.environ.get("UV_EXCLUDE_NEWER") + raw_value = os.environ.get("DEPENDENCY_RELEASE_CUTOFF") or os.environ.get("UV_EXCLUDE_NEWER") if not raw_value: return None normalized = raw_value.removesuffix("Z") diff --git a/python/scripts/dependencies/_dependency_bounds_upper_impl.py b/python/scripts/dependencies/_dependency_bounds_upper_impl.py index d086643a65..66d68fd51d 100644 --- a/python/scripts/dependencies/_dependency_bounds_upper_impl.py +++ b/python/scripts/dependencies/_dependency_bounds_upper_impl.py @@ -37,6 +37,8 @@ logger = logging.getLogger(__name__) CHECK_TASK_PRIORITY = ("check", "typing", "pyright", "mypy", "lint") +AZURE_MONITOR_OPENTELEMETRY = "azure-monitor-opentelemetry" +OPENTELEMETRY_SDK = "opentelemetry-sdk" REQ_PATTERN = r"^\s*([A-Za-z0-9_.-]+(?:\[[^\]]+\])?)\s*(.*?)\s*$" SECTION_HEADER_PATTERN = re.compile(r"^\s*\[([^\]]+)\]\s*$") INLINE_ARRAY_ASSIGNMENT_PATTERN = re.compile( @@ -238,6 +240,18 @@ def _select_latest_dev_version(versions: list[Version]) -> Version | None: return versions[-1] +def _exact_pin_version(requirement: Requirement) -> Version | None: + """Return the exact pinned version from a requirement, if it has one.""" + for specifier in requirement.specifier: + if specifier.operator not in {"==", "==="} or "*" in specifier.version: + continue + try: + return Version(specifier.version) + except InvalidVersion: + return None + return None + + @lru_cache(maxsize=8) def _load_workspace_package_versions(workspace_root: str) -> dict[str, Version]: workspace_path = Path(workspace_root) @@ -283,6 +297,13 @@ def _collect_dev_pin_replacements( requirement for requirement in (dependency_groups.get("dev", []) or []) if isinstance(requirement, str) ) logger.debug(f"Found {len(dev_requirements)} dev requirements in {pyproject_file}") + parsed_dev_requirements: dict[str, Requirement] = {} + for requirement in dev_requirements: + try: + parsed_requirement = Requirement(requirement) + except InvalidRequirement: + continue + parsed_dev_requirements[parsed_requirement.name.lower()] = parsed_requirement seen_requirements: set[str] = set() replacements: dict[str, str] = {} @@ -309,6 +330,33 @@ def _collect_dev_pin_replacements( latest_version = _select_latest_dev_version(catalog.get(dependency_name)) if latest_version is None: continue + current_exact_version = _exact_pin_version(parsed_requirement) + if current_exact_version is None and not dependency_name.startswith("agent-framework"): + locked_version = _select_latest_dev_version(catalog.get_lock(dependency_name)) + if locked_version is not None: + latest_version = locked_version + if current_exact_version is not None and latest_version < current_exact_version: + logger.info( + "Skipping %s in %s because selected version %s is older than current pin %s.", + dependency_name, + pyproject_file, + latest_version, + current_exact_version, + ) + continue + if ( + dependency_name == OPENTELEMETRY_SDK + and AZURE_MONITOR_OPENTELEMETRY in parsed_dev_requirements + and current_exact_version is not None + and latest_version != current_exact_version + ): + logger.info( + "Skipping %s in %s because %s currently pins the SDK version.", + dependency_name, + pyproject_file, + AZURE_MONITOR_OPENTELEMETRY, + ) + continue extras = f"[{','.join(sorted(parsed_requirement.extras))}]" if parsed_requirement.extras else "" marker = f"; {parsed_requirement.marker}" if parsed_requirement.marker else "" @@ -486,7 +534,7 @@ def _fetch(self, package_name: str) -> list[Version]: def _load_exclude_newer_from_env() -> datetime | None: - raw_value = os.environ.get("UV_EXCLUDE_NEWER") + raw_value = os.environ.get("DEPENDENCY_RELEASE_CUTOFF") or os.environ.get("UV_EXCLUDE_NEWER") if not raw_value: return None normalized = raw_value.removesuffix("Z")