diff --git a/eng/pipelines/templates/jobs/ci.yml b/eng/pipelines/templates/jobs/ci.yml index 61d063daa808..2b61414675c2 100644 --- a/eng/pipelines/templates/jobs/ci.yml +++ b/eng/pipelines/templates/jobs/ci.yml @@ -64,9 +64,6 @@ parameters: - name: EnvVars type: object default: {} - - name: PythonVersionForAnalyze - type: string - default: '' jobs: - job: 'Build_Linux' @@ -212,10 +209,10 @@ jobs: steps: - task: UsePythonVersion@0 - displayName: "Use Python ${{ coalesce(parameters.PythonVersionForAnalyze, '$(PythonVersion)') }}" + displayName: "Use Python $(PythonVersion)" condition: succeededOrFailed() inputs: - versionSpec: ${{ coalesce(parameters.PythonVersionForAnalyze, '$(PythonVersion)') }} + versionSpec: $(PythonVersion) - template: /eng/pipelines/templates/steps/use-venv.yml - template: /eng/common/pipelines/templates/steps/check-spelling.yml diff --git a/eng/pipelines/templates/stages/archetype-sdk-client.yml b/eng/pipelines/templates/stages/archetype-sdk-client.yml index e8654863bbc5..03cad2d2fd0e 100644 --- a/eng/pipelines/templates/stages/archetype-sdk-client.yml +++ b/eng/pipelines/templates/stages/archetype-sdk-client.yml @@ -84,9 +84,6 @@ parameters: - name: EnvVars type: object default: {} - - name: PythonVersionForAnalyze - type: string - default: '' extends: template: /eng/pipelines/templates/stages/1es-redirect.yml @@ -123,7 +120,6 @@ extends: VerifyAutorest: ${{ parameters.VerifyAutorest }} TestProxy: ${{ parameters.TestProxy }} GenerateApiReviewForManualOnly: ${{ parameters.GenerateApiReviewForManualOnly }} - PythonVersionForAnalyze: ${{ parameters.PythonVersionForAnalyze }} variables: - template: /eng/pipelines/templates/variables/globals.yml diff --git a/eng/scripts/dispatch_checks.py b/eng/scripts/dispatch_checks.py index 8045c33abf9e..a86fc660e5ff 100644 --- a/eng/scripts/dispatch_checks.py +++ b/eng/scripts/dispatch_checks.py @@ -15,6 +15,7 @@ from ci_tools.scenario.generation import build_whl_for_req, replace_dev_reqs from ci_tools.logging import configure_logging, logger from ci_tools.environment_exclusions import is_check_enabled, CHECK_DEFAULTS +from ci_tools.parsing import get_config_setting from devtools_testutils.proxy_startup import prepare_local_tool from packaging.requirements import Requirement @@ -49,6 +50,7 @@ class ProxyProcess: "sdist", "devtest", "optional", + "import_all", "latestdependency", "mindependency", } @@ -84,9 +86,7 @@ def _compare_req_to_injected_reqs(parsed_req, injected_packages: List[str]) -> b return any(parsed_req.name in req for req in injected_packages) -def _inject_custom_reqs( - req_file: str, injected_packages: str, package_dir: str -) -> None: +def _inject_custom_reqs(req_file: str, injected_packages: str, package_dir: str) -> None: req_lines = [] injected_list = [p for p in re.split(r"[\s,]", injected_packages) if p] @@ -115,8 +115,7 @@ def _inject_custom_reqs( all_adjustments = installable + [ line_tuple[0].strip() for line_tuple in req_lines - if line_tuple[0].strip() - and not _compare_req_to_injected_reqs(line_tuple[1], all_filter_names) + if line_tuple[0].strip() and not _compare_req_to_injected_reqs(line_tuple[1], all_filter_names) ] else: all_adjustments = installable @@ -138,6 +137,7 @@ async def run_check( mark_arg: Optional[str], dest_dir: Optional[str] = None, service: Optional[str] = None, + python_version: Optional[str] = None, ) -> CheckResult: """Run a single check (subprocess) within a concurrency semaphore, capturing output and timing. @@ -161,6 +161,8 @@ async def run_check( async with semaphore: start = time.time() cmd = base_args + [check, "--isolate", package] + if python_version and check not in INSTALL_AND_TEST_CHECKS: + cmd += ["--python", python_version] if service: cmd += ["--service", service] if mark_arg: @@ -172,9 +174,7 @@ async def run_check( env["PROXY_URL"] = f"http://localhost:{proxy_port}" if in_ci(): - env["PROXY_ASSETS_FOLDER"] = os.path.join( - root_dir, ".assets_distributed", str(proxy_port) - ) + env["PROXY_ASSETS_FOLDER"] = os.path.join(root_dir, ".assets_distributed", str(proxy_port)) try: logger.info(" ".join(cmd)) proc = await asyncio.create_subprocess_exec( @@ -194,9 +194,7 @@ async def run_check( stderr = stderr_b.decode(errors="replace") exit_code = proc.returncode or 0 status = "OK" if exit_code == 0 else f"FAIL({exit_code})" - logger.info( - f"[END {idx}/{total}] {check} :: {package} -> {status} in {duration:.2f}s" - ) + logger.info(f"[END {idx}/{total}] {check} :: {package} -> {status} in {duration:.2f}s") # Print captured output after completion to avoid interleaving header = f"===== OUTPUT: {check} :: {package} (exit {exit_code}) =====" trailer = "=" * len(header) @@ -220,9 +218,7 @@ async def run_check( # finally, we need to clean up any temp dirs created by --isolate if in_ci(): package_name = os.path.basename(os.path.normpath(package)) - isolate_dir = os.path.join( - root_dir, ".venv", package_name, f".venv_{check}" - ) + isolate_dir = os.path.join(root_dir, ".venv", package_name, f".venv_{check}") ISOLATE_DIRS_TO_CLEAN.append(isolate_dir) return CheckResult(package, check, exit_code, duration, stdout, stderr) @@ -247,14 +243,10 @@ def summarize(results: List[CheckResult]) -> int: print("-" * len(header)) for r in sorted(results, key=lambda x: (x.exit_code != 0, x.package, x.check)): status = "OK" if r.exit_code == 0 else f"FAIL({r.exit_code})" - print( - f"{r.package.ljust(pkg_w)} {r.check.ljust(chk_w)} {status.ljust(8)} {r.duration:>10.2f}" - ) + print(f"{r.package.ljust(pkg_w)} {r.check.ljust(chk_w)} {status.ljust(8)} {r.duration:>10.2f}") worst = max((r.exit_code for r in results), default=0) failed = [r for r in results if r.exit_code != 0] - print( - f"\nTotal checks: {len(results)} | Failed: {len(failed)} | Worst exit code: {worst}" - ) + print(f"\nTotal checks: {len(results)} | Failed: {len(failed)} | Worst exit code: {worst}") return worst @@ -292,14 +284,10 @@ async def run_all_checks( dependency_tools_path = os.path.join(root_dir, "eng", "dependency_tools.txt") if in_ci(): - logger.info( - "Replacing relative requirements in eng/test_tools.txt with prebuilt wheels." - ) + logger.info("Replacing relative requirements in eng/test_tools.txt with prebuilt wheels.") replace_dev_reqs(test_tools_path, root_dir, wheel_dir) - logger.info( - "Replacing relative requirements in eng/dependency_tools.txt with prebuilt wheels." - ) + logger.info("Replacing relative requirements in eng/dependency_tools.txt with prebuilt wheels.") replace_dev_reqs(dependency_tools_path, root_dir, wheel_dir) for pkg in packages: @@ -321,15 +309,19 @@ async def run_all_checks( if not is_check_enabled(package, check, CHECK_DEFAULTS.get(check, True)): logger.warning(f"Skipping disabled check {check} for package {package}") continue - logger.info( - f"Assigning proxy port {next_proxy_port} to check {check} for package {package}" - ) - scheduled.append((package, check, next_proxy_port)) + logger.info(f"Assigning proxy port {next_proxy_port} to check {check} for package {package}") + + # Check if this package overrides the Python version for analysis + pkg_python_version = get_config_setting(package, "analyze_python_version", None) + if pkg_python_version: + logger.info(f"Package {package} overrides analyze Python version to {pkg_python_version}") + + scheduled.append((package, check, next_proxy_port, pkg_python_version)) next_proxy_port += 1 total = len(scheduled) - for idx, (package, check, proxy_port) in enumerate(scheduled, start=1): + for idx, (package, check, proxy_port, pkg_python_version) in enumerate(scheduled, start=1): tasks.append( asyncio.create_task( run_check( @@ -343,6 +335,7 @@ async def run_all_checks( mark_arg, dest_dir, service, + pkg_python_version, ) ) ) @@ -358,15 +351,13 @@ async def run_all_checks( raise # Normalize exceptions norm_results: List[CheckResult] = [] - for res, (package, check, _) in zip(results, scheduled): + for res, (package, check, _, _) in zip(results, scheduled): if isinstance(res, CheckResult): norm_results.append(res) elif isinstance(res, Exception): norm_results.append(CheckResult(package, check, 99, 0.0, "", str(res))) else: - norm_results.append( - CheckResult(package, check, 98, 0.0, "", f"Unknown result type: {res}") - ) + norm_results.append(CheckResult(package, check, 98, 0.0, "", f"Unknown result type: {res}")) return summarize(norm_results) @@ -442,15 +433,11 @@ def handler(signum, frame): ), ) - parser.add_argument( - "--disablecov", help=("Flag. Disables code coverage."), action="store_true" - ) + parser.add_argument("--disablecov", help=("Flag. Disables code coverage."), action="store_true") parser.add_argument( "--service", - help=( - "Name of service directory (under sdk/) to test. Example: --service applicationinsights" - ), + help=("Name of service directory (under sdk/) to test. Example: --service applicationinsights"), ) parser.add_argument( @@ -517,9 +504,7 @@ def handler(signum, frame): else: target_dir = root_dir - logger.info( - f"Beginning discovery for {args.service} and root dir {root_dir}. Resolving to {target_dir}." - ) + logger.info(f"Beginning discovery for {args.service} and root dir {root_dir}. Resolving to {target_dir}.") # ensure that recursive virtual envs aren't messed with by this call os.environ.pop("VIRTUAL_ENV", None) @@ -536,9 +521,7 @@ def handler(signum, frame): ) if len(targeted_packages) == 0: - logger.info( - f"No packages collected for targeting string {args.glob_string} and root dir {root_dir}. Exit 0." - ) + logger.info(f"No packages collected for targeting string {args.glob_string} and root dir {root_dir}. Exit 0.") exit(0) logger.info(f"Executing checks with the executable {sys.executable}.") @@ -570,9 +553,7 @@ def handler(signum, frame): try: proxy_executable = prepare_local_tool(root_dir) except Exception as exc: - logger.error( - f"Unable to prepare test proxy executable for recording restore: {exc}" - ) + logger.error(f"Unable to prepare test proxy executable for recording restore: {exc}") sys.exit(1) logger.info( @@ -583,9 +564,7 @@ def handler(signum, frame): proxy_processes: List[ProxyProcess] = [] try: if in_ci(): - logger.info( - f"Ensuring {len(checks)} test proxies are running for requested checks..." - ) + logger.info(f"Ensuring {len(checks)} test proxies are running for requested checks...") # Pass through service if set and not "auto" effective_service = args.service if (args.service and args.service != "auto") else None exit_code = asyncio.run( diff --git a/eng/tools/azure-sdk-tools/azpysdk/Check.py b/eng/tools/azure-sdk-tools/azpysdk/Check.py index 2d3748400ae4..2b59afadfa2a 100644 --- a/eng/tools/azure-sdk-tools/azpysdk/Check.py +++ b/eng/tools/azure-sdk-tools/azpysdk/Check.py @@ -14,11 +14,13 @@ from ci_tools.parsing import ParsedSetup from ci_tools.functions import ( discover_targeted_packages, + find_whl, +) +from ci_tools.venv import ( get_venv_call, install_into_venv, get_venv_python, get_pip_command, - find_whl, ) from ci_tools.variables import discover_repo_root, in_ci from ci_tools.logging import logger @@ -71,10 +73,14 @@ def run(self, args: argparse.Namespace) -> int: """ return 0 - def create_venv(self, isolate: bool, venv_location: str) -> str: - """Abstraction for creating a virtual environment.""" + def create_venv(self, isolate: bool, venv_location: str, python_version: Optional[str] = None) -> str: + """Abstraction for creating a virtual environment. + + :param python_version: If provided, passed to ``uv venv --python`` during initial creation only. + Has no effect on venv reuse or lookup. + """ if isolate: - venv_cmd = get_venv_call(sys.executable) + venv_cmd = get_venv_call(sys.executable, python_version=python_version) venv_python = get_venv_python(venv_location) if os.path.exists(venv_python): logger.info(f"Reusing existing venv at {venv_python}") @@ -109,16 +115,30 @@ def create_venv(self, isolate: bool, venv_location: str) -> str: # if we don't need to isolate, just return the python executable that we're invoking return sys.executable - def get_executable(self, isolate: bool, check_name: str, executable: str, package_folder: str) -> Tuple[str, str]: + def get_executable( + self, + isolate: bool, + check_name: str, + executable: str, + package_folder: str, + python_version: Optional[str] = None, + ) -> Tuple[str, str]: """Get the Python executable that should be used for this check.""" # Keep venvs under a shared repo-level folder to prevent nested import errors during pytest collection package_name = os.path.basename(os.path.normpath(package_folder)) shared_venv_root = os.path.join(REPO_ROOT, ".venv", package_name) os.makedirs(shared_venv_root, exist_ok=True) - venv_location = os.path.join(shared_venv_root, f".venv_{check_name}") + + # version-qualify the venv dir when a specific Python is requested to avoid cache collisions + venv_suffix = f".venv_{check_name}" + if python_version: + sanitized = python_version.replace(".", "").replace("-", "") + venv_suffix = f".venv_{check_name}_py{sanitized}" + venv_location = os.path.join(shared_venv_root, venv_suffix) + # if isolation is required, the executable we get back will align with the venv # otherwise we'll just get sys.executable and install in current - executable = self.create_venv(isolate, venv_location) + executable = self.create_venv(isolate, venv_location, python_version=python_version) staging_directory = os.path.join(venv_location, ".staging") os.makedirs(staging_directory, exist_ok=True) return executable, staging_directory diff --git a/eng/tools/azure-sdk-tools/azpysdk/apistub.py b/eng/tools/azure-sdk-tools/azpysdk/apistub.py index 0a023dd7c4ba..199b42f2eb8b 100644 --- a/eng/tools/azure-sdk-tools/azpysdk/apistub.py +++ b/eng/tools/azure-sdk-tools/azpysdk/apistub.py @@ -92,7 +92,13 @@ def run(self, args: argparse.Namespace) -> int: os.chdir(parsed.folder) package_dir = parsed.folder package_name = parsed.name - executable, staging_directory = self.get_executable(args.isolate, args.command, sys.executable, package_dir) + executable, staging_directory = self.get_executable( + args.isolate, + args.command, + sys.executable, + package_dir, + python_version=getattr(args, "python_version", None), + ) logger.info(f"Processing {package_name} for apistub check") # install dependencies diff --git a/eng/tools/azure-sdk-tools/azpysdk/bandit.py b/eng/tools/azure-sdk-tools/azpysdk/bandit.py index b4dc3c3e8f09..157073fe2b7b 100644 --- a/eng/tools/azure-sdk-tools/azpysdk/bandit.py +++ b/eng/tools/azure-sdk-tools/azpysdk/bandit.py @@ -42,7 +42,13 @@ def run(self, args: argparse.Namespace) -> int: os.chdir(parsed.folder) package_dir = parsed.folder package_name = parsed.name - executable, staging_directory = self.get_executable(args.isolate, args.command, sys.executable, package_dir) + executable, staging_directory = self.get_executable( + args.isolate, + args.command, + sys.executable, + package_dir, + python_version=getattr(args, "python_version", None), + ) logger.info(f"Processing {package_name} for bandit check") self.install_dev_reqs(executable, args, package_dir) diff --git a/eng/tools/azure-sdk-tools/azpysdk/black.py b/eng/tools/azure-sdk-tools/azpysdk/black.py index 039ca6a7bd9d..c901538c2c4e 100644 --- a/eng/tools/azure-sdk-tools/azpysdk/black.py +++ b/eng/tools/azure-sdk-tools/azpysdk/black.py @@ -45,7 +45,13 @@ def run(self, args: argparse.Namespace) -> int: package_dir = parsed.folder package_name = parsed.name - executable, staging_directory = self.get_executable(args.isolate, args.command, sys.executable, package_dir) + executable, staging_directory = self.get_executable( + args.isolate, + args.command, + sys.executable, + package_dir, + python_version=getattr(args, "python_version", None), + ) logger.info(f"Processing {package_name} for black check") self.install_dev_reqs(executable, args, package_dir) diff --git a/eng/tools/azure-sdk-tools/azpysdk/breaking.py b/eng/tools/azure-sdk-tools/azpysdk/breaking.py index df3429f0d6a4..ea1e60edaced 100644 --- a/eng/tools/azure-sdk-tools/azpysdk/breaking.py +++ b/eng/tools/azure-sdk-tools/azpysdk/breaking.py @@ -105,7 +105,13 @@ def run(self, args: argparse.Namespace) -> int: os.chdir(parsed.folder) package_dir = parsed.folder package_name = parsed.name - executable, staging_directory = self.get_executable(args.isolate, args.command, sys.executable, package_dir) + executable, staging_directory = self.get_executable( + args.isolate, + args.command, + sys.executable, + package_dir, + python_version=getattr(args, "python_version", None), + ) logger.info(f"Processing {package_name} for breaking check...") # install dependencies diff --git a/eng/tools/azure-sdk-tools/azpysdk/dependency_check.py b/eng/tools/azure-sdk-tools/azpysdk/dependency_check.py index 13d375ceabf2..a72181b7a1e8 100644 --- a/eng/tools/azure-sdk-tools/azpysdk/dependency_check.py +++ b/eng/tools/azure-sdk-tools/azpysdk/dependency_check.py @@ -66,7 +66,13 @@ def run(self, args: argparse.Namespace) -> int: package_dir = parsed.folder package_name = parsed.name - executable, staging_directory = self.get_executable(args.isolate, args.command, sys.executable, package_dir) + executable, staging_directory = self.get_executable( + args.isolate, + args.command, + sys.executable, + package_dir, + python_version=getattr(args, "python_version", None), + ) logger.info(f"Processing {package_name} using interpreter {executable}") try: diff --git a/eng/tools/azure-sdk-tools/azpysdk/generate.py b/eng/tools/azure-sdk-tools/azpysdk/generate.py index c132ccbb95ef..07845e8e3986 100644 --- a/eng/tools/azure-sdk-tools/azpysdk/generate.py +++ b/eng/tools/azure-sdk-tools/azpysdk/generate.py @@ -38,7 +38,13 @@ def run(self, args: argparse.Namespace) -> int: os.chdir(parsed.folder) package_dir = parsed.folder package_name = parsed.name - executable, staging_directory = self.get_executable(args.isolate, args.command, sys.executable, package_dir) + executable, staging_directory = self.get_executable( + args.isolate, + args.command, + sys.executable, + package_dir, + python_version=getattr(args, "python_version", None), + ) logger.info(f"Processing {package_name} for generate check") # install dependencies diff --git a/eng/tools/azure-sdk-tools/azpysdk/import_all.py b/eng/tools/azure-sdk-tools/azpysdk/import_all.py index 7c9ff8d181d8..ad647cef47ae 100644 --- a/eng/tools/azure-sdk-tools/azpysdk/import_all.py +++ b/eng/tools/azure-sdk-tools/azpysdk/import_all.py @@ -59,7 +59,9 @@ def run(self, args: argparse.Namespace) -> int: for parsed in targeted: pkg = parsed.folder - executable, staging_directory = self.get_executable(args.isolate, args.command, sys.executable, pkg) + executable, staging_directory = self.get_executable( + args.isolate, args.command, sys.executable, pkg, python_version=getattr(args, "python_version", None) + ) self.install_dev_reqs(executable, args, pkg) diff --git a/eng/tools/azure-sdk-tools/azpysdk/install_and_test.py b/eng/tools/azure-sdk-tools/azpysdk/install_and_test.py index 88d4a3c123ab..db2f7ef48be2 100644 --- a/eng/tools/azure-sdk-tools/azpysdk/install_and_test.py +++ b/eng/tools/azure-sdk-tools/azpysdk/install_and_test.py @@ -58,7 +58,13 @@ def run(self, args: argparse.Namespace) -> int: package_dir = parsed.folder package_name = parsed.name - executable, staging_directory = self.get_executable(args.isolate, args.command, sys.executable, package_dir) + executable, staging_directory = self.get_executable( + args.isolate, + args.command, + sys.executable, + package_dir, + python_version=getattr(args, "python_version", None), + ) logger.info(f"Processing {package_name} using interpreter {executable}") install_result = self.install_all_requirements( diff --git a/eng/tools/azure-sdk-tools/azpysdk/main.py b/eng/tools/azure-sdk-tools/azpysdk/main.py index af770f932e21..00f03d5fd9c1 100644 --- a/eng/tools/azure-sdk-tools/azpysdk/main.py +++ b/eng/tools/azure-sdk-tools/azpysdk/main.py @@ -56,6 +56,16 @@ def build_parser() -> argparse.ArgumentParser: parser.add_argument( "--isolate", action="store_true", default=False, help="If set, run in an isolated virtual environment." ) + parser.add_argument( + "--python", + default=None, + dest="python_version", + metavar="VERSION", + help=( + "Python version to use when creating the isolated venv (e.g. 3.13). " + "Passed through to 'uv venv --python'. Requires --isolate and uv." + ), + ) # mutually exclusive logging options log_group = parser.add_mutually_exclusive_group() @@ -77,8 +87,22 @@ def build_parser() -> argparse.ArgumentParser: help="Glob pattern for packages. Defaults to '**', but will match patterns below CWD if a value is provided.", ) # allow --isolate to be specified after the subcommand as well + # use SUPPRESS so the subparser default doesn't overwrite a value set by the global parser common.add_argument( - "--isolate", action="store_true", default=False, help="If set, run in an isolated virtual environment." + "--isolate", + action="store_true", + default=argparse.SUPPRESS, + help="If set, run in an isolated virtual environment.", + ) + common.add_argument( + "--python", + default=argparse.SUPPRESS, + dest="python_version", + metavar="VERSION", + help=( + "Python version to use when creating the isolated venv (e.g. 3.13). " + "Passed through to 'uv venv --python'. Requires --isolate and uv." + ), ) common.add_argument( "--service", @@ -147,6 +171,19 @@ def main(argv: Optional[Sequence[str]] = None) -> int: if uv_path: os.environ["TOX_PIP_IMPL"] = "uv" + # --python requires both --isolate and uv + python_version = getattr(args, "python_version", None) + if python_version: + isolate = getattr(args, "isolate", False) + if not isolate: + parser.error( + "--python requires --isolate to create a virtual environment with the specified Python version." + ) + + pip_impl = os.environ.get("TOX_PIP_IMPL", "pip").lower() + if pip_impl != "uv": + parser.error("--python requires uv as the backend. Install uv or set TOX_PIP_IMPL=uv.") + try: result = args.func(args) print(f"{args.command} check completed with exit code {result}") diff --git a/eng/tools/azure-sdk-tools/azpysdk/mypy.py b/eng/tools/azure-sdk-tools/azpysdk/mypy.py index 64b759a8ad53..154ab77e2a08 100644 --- a/eng/tools/azure-sdk-tools/azpysdk/mypy.py +++ b/eng/tools/azure-sdk-tools/azpysdk/mypy.py @@ -53,7 +53,13 @@ def run(self, args: argparse.Namespace) -> int: package_name = parsed.name additional_requirements = ADDITIONAL_LOCKED_DEPENDENCIES - executable, staging_directory = self.get_executable(args.isolate, args.command, sys.executable, package_dir) + executable, staging_directory = self.get_executable( + args.isolate, + args.command, + sys.executable, + package_dir, + python_version=getattr(args, "python_version", None), + ) logger.info(f"Processing {package_name} for mypy check") # # need to install dev_requirements to ensure that type-hints properly resolve diff --git a/eng/tools/azure-sdk-tools/azpysdk/optional.py b/eng/tools/azure-sdk-tools/azpysdk/optional.py index 53c05b538444..c7b95238d5ed 100644 --- a/eng/tools/azure-sdk-tools/azpysdk/optional.py +++ b/eng/tools/azure-sdk-tools/azpysdk/optional.py @@ -72,7 +72,13 @@ def run(self, args: argparse.Namespace) -> int: os.chdir(parsed.folder) package_dir = parsed.folder package_name = parsed.name - executable, staging_directory = self.get_executable(args.isolate, args.command, sys.executable, package_dir) + executable, staging_directory = self.get_executable( + args.isolate, + args.command, + sys.executable, + package_dir, + python_version=getattr(args, "python_version", None), + ) logger.info(f"Processing {package_name} using interpreter {executable}") try: diff --git a/eng/tools/azure-sdk-tools/azpysdk/pylint.py b/eng/tools/azure-sdk-tools/azpysdk/pylint.py index 5815d8bdb30e..49542b607173 100644 --- a/eng/tools/azure-sdk-tools/azpysdk/pylint.py +++ b/eng/tools/azure-sdk-tools/azpysdk/pylint.py @@ -50,7 +50,13 @@ def run(self, args: argparse.Namespace) -> int: os.chdir(parsed.folder) package_dir = parsed.folder package_name = parsed.name - executable, staging_directory = self.get_executable(args.isolate, args.command, sys.executable, package_dir) + executable, staging_directory = self.get_executable( + args.isolate, + args.command, + sys.executable, + package_dir, + python_version=getattr(args, "python_version", None), + ) logger.info(f"Processing {package_name} for pylint check") package_failed = False diff --git a/eng/tools/azure-sdk-tools/azpysdk/pyright.py b/eng/tools/azure-sdk-tools/azpysdk/pyright.py index 2be47bacc848..b09c3f129d4b 100644 --- a/eng/tools/azure-sdk-tools/azpysdk/pyright.py +++ b/eng/tools/azure-sdk-tools/azpysdk/pyright.py @@ -78,7 +78,13 @@ def run(self, args: argparse.Namespace) -> int: package_dir = parsed.folder package_name = parsed.name - executable, staging_directory = self.get_executable(args.isolate, args.command, sys.executable, package_dir) + executable, staging_directory = self.get_executable( + args.isolate, + args.command, + sys.executable, + package_dir, + python_version=getattr(args, "python_version", None), + ) logger.info(f"Processing {package_name} for pyright check") try: diff --git a/eng/tools/azure-sdk-tools/azpysdk/ruff.py b/eng/tools/azure-sdk-tools/azpysdk/ruff.py index 139106469eda..8f6cff0fa802 100644 --- a/eng/tools/azure-sdk-tools/azpysdk/ruff.py +++ b/eng/tools/azure-sdk-tools/azpysdk/ruff.py @@ -36,7 +36,13 @@ def run(self, args: argparse.Namespace) -> int: os.chdir(parsed.folder) package_dir = parsed.folder package_name = parsed.name - executable, staging_directory = self.get_executable(args.isolate, args.command, sys.executable, package_dir) + executable, staging_directory = self.get_executable( + args.isolate, + args.command, + sys.executable, + package_dir, + python_version=getattr(args, "python_version", None), + ) logger.info(f"Processing {package_name} for ruff check") self.install_dev_reqs(executable, args, package_dir) diff --git a/eng/tools/azure-sdk-tools/azpysdk/samples.py b/eng/tools/azure-sdk-tools/azpysdk/samples.py index 4db373cf043c..0ebf1f056725 100644 --- a/eng/tools/azure-sdk-tools/azpysdk/samples.py +++ b/eng/tools/azure-sdk-tools/azpysdk/samples.py @@ -316,7 +316,13 @@ def run(self, args: argparse.Namespace) -> int: os.chdir(parsed.folder) package_dir = parsed.folder package_name = parsed.name - executable, staging_directory = self.get_executable(args.isolate, args.command, sys.executable, package_dir) + executable, staging_directory = self.get_executable( + args.isolate, + args.command, + sys.executable, + package_dir, + python_version=getattr(args, "python_version", None), + ) logger.info(f"Processing {package_name} for samples check") # install dependencies diff --git a/eng/tools/azure-sdk-tools/azpysdk/sphinx.py b/eng/tools/azure-sdk-tools/azpysdk/sphinx.py index 53c91a6b8451..559b48d094a6 100644 --- a/eng/tools/azure-sdk-tools/azpysdk/sphinx.py +++ b/eng/tools/azure-sdk-tools/azpysdk/sphinx.py @@ -219,7 +219,13 @@ def run(self, args: argparse.Namespace) -> int: package_dir = parsed.folder package_name = parsed.name - executable, staging_directory = self.get_executable(args.isolate, args.command, sys.executable, package_dir) + executable, staging_directory = self.get_executable( + args.isolate, + args.command, + sys.executable, + package_dir, + python_version=getattr(args, "python_version", None), + ) logger.info(f"Processing {package_name} for sphinx check") # check Python version diff --git a/eng/tools/azure-sdk-tools/azpysdk/update_snippet.py b/eng/tools/azure-sdk-tools/azpysdk/update_snippet.py index d55b9db1aec6..465f5ce99296 100644 --- a/eng/tools/azure-sdk-tools/azpysdk/update_snippet.py +++ b/eng/tools/azure-sdk-tools/azpysdk/update_snippet.py @@ -206,6 +206,8 @@ def _run_black( self, pkg_dir: str, pkg_name: str, samples_dir: str, args: argparse.Namespace ) -> Optional[subprocess.CompletedProcess]: """Run black on the sample files of a package using the repo-wide config.""" - executable, _ = self.get_executable(args.isolate, args.command, sys.executable, pkg_dir) + executable, _ = self.get_executable( + args.isolate, args.command, sys.executable, pkg_dir, python_version=getattr(args, "python_version", None) + ) logger.info(f"Running black on samples for {pkg_name} to format before snippet extraction.") return black.format_directory(executable, samples_dir) diff --git a/eng/tools/azure-sdk-tools/azpysdk/verify_sdist.py b/eng/tools/azure-sdk-tools/azpysdk/verify_sdist.py index 7436079f3f43..9858a9b044f4 100644 --- a/eng/tools/azure-sdk-tools/azpysdk/verify_sdist.py +++ b/eng/tools/azure-sdk-tools/azpysdk/verify_sdist.py @@ -155,7 +155,13 @@ def run(self, args: argparse.Namespace) -> int: os.chdir(parsed.folder) package_dir = parsed.folder package_name = parsed.name - executable, staging_directory = self.get_executable(args.isolate, args.command, sys.executable, package_dir) + executable, staging_directory = self.get_executable( + args.isolate, + args.command, + sys.executable, + package_dir, + python_version=getattr(args, "python_version", None), + ) logger.info(f"Processing {package_name} for verify_sdist check") self.install_dev_reqs(executable, args, package_dir) diff --git a/eng/tools/azure-sdk-tools/azpysdk/verify_whl.py b/eng/tools/azure-sdk-tools/azpysdk/verify_whl.py index a5fda39aaecf..937cba64bb68 100644 --- a/eng/tools/azure-sdk-tools/azpysdk/verify_whl.py +++ b/eng/tools/azure-sdk-tools/azpysdk/verify_whl.py @@ -292,7 +292,13 @@ def run(self, args: argparse.Namespace) -> int: os.chdir(parsed.folder) package_dir = parsed.folder package_name = parsed.name - executable, staging_directory = self.get_executable(args.isolate, args.command, sys.executable, package_dir) + executable, staging_directory = self.get_executable( + args.isolate, + args.command, + sys.executable, + package_dir, + python_version=getattr(args, "python_version", None), + ) logger.info(f"Processing {package_name} for verify_whl check") top_level_module = parsed.namespace.split(".")[0] diff --git a/eng/tools/azure-sdk-tools/azpysdk/verifytypes.py b/eng/tools/azure-sdk-tools/azpysdk/verifytypes.py index baeae3d05e5c..040d563608d5 100644 --- a/eng/tools/azure-sdk-tools/azpysdk/verifytypes.py +++ b/eng/tools/azure-sdk-tools/azpysdk/verifytypes.py @@ -51,7 +51,13 @@ def run(self, args: argparse.Namespace) -> int: package_dir = parsed.folder package_name = parsed.name module = parsed.namespace - executable, staging_directory = self.get_executable(args.isolate, args.command, sys.executable, package_dir) + executable, staging_directory = self.get_executable( + args.isolate, + args.command, + sys.executable, + package_dir, + python_version=getattr(args, "python_version", None), + ) logger.info(f"Processing {package_name} for verifytypes check") self.install_dev_reqs(executable, args, package_dir) diff --git a/eng/tools/azure-sdk-tools/ci_tools/functions.py b/eng/tools/azure-sdk-tools/ci_tools/functions.py index 26504d3b0e4e..b043434a3d65 100644 --- a/eng/tools/azure-sdk-tools/ci_tools/functions.py +++ b/eng/tools/azure-sdk-tools/ci_tools/functions.py @@ -16,6 +16,20 @@ from pypi_tools.pypi import PyPIClient from ci_tools.logging import logger +# Re-export venv/pip helpers from their new home so existing imports keep working. +from ci_tools.venv import ( # noqa: F401 + get_venv_call, + get_pip_command, + get_venv_python, + install_into_venv, + uninstall_from_venv, + pip_install, + pip_uninstall, + pip_install_requirements_file, + run_pip_freeze, + get_pip_list_output, +) + import os, sys, platform, glob, re, logging from typing import List, Any, Optional, Tuple @@ -483,154 +497,6 @@ def find_sdist(dist_dir: str, pkg_name: str, pkg_version: str) -> Optional[str]: return packages[0] -def pip_install( - requirements: List[str], - include_dependencies: bool = True, - python_executable: Optional[str] = None, - cwd: Optional[str] = None, -) -> bool: - """ - Attempts to invoke an install operation using the invoking python's pip. Empty requirements are auto-success. - """ - - exe = get_pip_command(python_executable) - - command = exe + ["install"] - - if requirements: - command.extend([req.strip() for req in requirements]) - else: - return True - - try: - if cwd: - subprocess.check_call(command, cwd=cwd) - else: - subprocess.check_call(command) - except subprocess.CalledProcessError as f: - return False - - return True - - -def pip_uninstall(requirements: List[str], python_executable: str) -> bool: - """ - Attempts to invoke an install operation using the invoking python's pip. Empty requirements are auto-success. - """ - # use uninstall_from_venv() for uv venvs - exe = python_executable or sys.executable - command = [exe, "-m", "pip", "uninstall", "-y"] - - if requirements: - command.extend([req.strip() for req in requirements]) - else: - return True - - try: - result = subprocess.check_call(command) - return True - except subprocess.CalledProcessError as f: - return False - - -def get_venv_python(venv_path: str) -> str: - """ - Given a python venv path, identify the crossplat reference to the python executable. - """ - # if we already have a path to a python executable, return it - if os.path.isfile(venv_path) and os.access(venv_path, os.X_OK) and os.path.basename(venv_path).startswith("python"): - return venv_path - - # cross-platform python in a venv - bin_dir = "Scripts" if os.name == "nt" else "bin" - python_exe = "python.exe" if os.name == "nt" else "python" - return os.path.join(venv_path, bin_dir, python_exe) - - -def install_into_venv(venv_path_or_executable: str, requirements: List[str], working_directory: str) -> None: - """ - Install the requirements into an existing venv (venv_path) without activating it. - - - Uses get_pip_command(get_venv_python) per request. - - If get_pip_command returns the 'uv' wrapper, we fall back to get_venv_python -m pip - so installation goes into the target venv reliably. - """ - py = get_venv_python(venv_path_or_executable) - pip_cmd = get_pip_command(py) - - install_targets = [r.strip() for r in requirements] - cmd = pip_cmd + ["install"] + install_targets - - if pip_cmd[0] == "uv": - cmd += ["--python", py] - - # todo: clean this up so that we're using run_logged from #42862 - subprocess.check_call(cmd, cwd=working_directory) - - -def uninstall_from_venv(venv_path_or_executable: str, requirements: List[str], working_directory: str) -> None: - """ - Uninstalls the requirements from an existing venv (venv_path) without activating it. - """ - py = get_venv_python(venv_path_or_executable) - pip_cmd = get_pip_command(py) - - install_targets = [r.strip() for r in requirements] - cmd = pip_cmd + ["uninstall"] - if pip_cmd[0] != "uv": - cmd += ["-y"] - cmd.extend(install_targets) - - if pip_cmd[0] == "uv": - cmd += ["--python", py] - - subprocess.check_call(cmd, cwd=working_directory) - - -def pip_install_requirements_file(requirements_file: str, python_executable: Optional[str] = None) -> bool: - return pip_install(["-r", requirements_file], True, python_executable) - - -def run_pip_freeze(python_executable: Optional[str] = None) -> List[str]: - """Uses the invoking python executable to get the output from pip freeze.""" - exe = python_executable or sys.executable - - pip_cmd = get_pip_command(exe) - - # we use `freeze` because it is present on both pip and uv - out = subprocess.Popen( - pip_cmd + ["freeze", "--disable-pip-version-check"], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - ) - - stdout, stderr = out.communicate() - - collected_output = [] - - if stdout and (stderr is None): - for line in stdout.decode("utf-8").splitlines(): - if line: - collected_output.append(line) - else: - raise Exception(stderr) - - return collected_output - - -def get_pip_list_output(python_executable: Optional[str] = None): - """Uses the invoking python executable to get the output from pip list.""" - pip_output = run_pip_freeze(python_executable) - - collected_output = {} - for line in pip_output: - if "==" in line: - package, version = re.split("==", line) - collected_output[package] = version - - return collected_output - - def pytest(args: list, cwd: Optional[str] = None, python_executable: Optional[str] = None) -> bool: """ Invokes a set of tests, returns true if successful, false otherwise. @@ -1045,44 +911,6 @@ def verify_package_classifiers( return True, None -def get_venv_call(python_exe: Optional[str] = None) -> List[str]: - """ - Determine whether to use 'uv venv' or regular 'python -m venv' based on environment. - - :param str python_exe: The Python executable to use (if not using the default). - :return: List of command arguments for venv. - :rtype: List[str] - - """ - # Check TOX_PIP_IMPL environment variable (aligns with tox.ini configuration) - pip_impl = os.environ.get("TOX_PIP_IMPL", "pip").lower() - - # soon we will change this to default to uv - if pip_impl == "uv": - return ["uv", "venv"] - else: - return [python_exe if python_exe else sys.executable, "-m", "venv"] - - -def get_pip_command(python_exe: Optional[str] = None) -> List[str]: - """ - Determine whether to use 'uv pip' or regular 'pip' based on environment. - - :param str python_exe: The Python executable to use (if not using the default). - :return: List of command arguments for pip. - :rtype: List[str] - - """ - # Check TOX_PIP_IMPL environment variable (aligns with tox.ini configuration) - pip_impl = os.environ.get("TOX_PIP_IMPL", "pip").lower() - - # soon we will change this to default to uv - if pip_impl == "uv": - return ["uv", "pip"] - else: - return [python_exe if python_exe else sys.executable, "-m", "pip"] - - def is_error_code_5_allowed(target_pkg: str, pkg_name: str): """ Determine if error code 5 (no pytests run) is allowed for the given package. diff --git a/eng/tools/azure-sdk-tools/ci_tools/venv.py b/eng/tools/azure-sdk-tools/ci_tools/venv.py new file mode 100644 index 000000000000..95a9bb7b220c --- /dev/null +++ b/eng/tools/azure-sdk-tools/ci_tools/venv.py @@ -0,0 +1,196 @@ +"""Virtualenv and pip helpers for Azure SDK tooling. + +This module centralizes all venv creation, pip install/uninstall, and +related utilities. The backend (``uv`` vs stdlib ``pip``) is chosen +via the ``TOX_PIP_IMPL`` environment variable: + +* ``"uv"`` → uses ``uv venv`` / ``uv pip`` +* anything else (default ``"pip"``) → uses ``python -m venv`` / ``python -m pip`` +""" + +import logging +import os +import re +import subprocess +import sys +from typing import List, Optional + + +def get_venv_call(python_exe: Optional[str] = None, python_version: Optional[str] = None) -> List[str]: + """Determine whether to use 'uv venv' or regular 'python -m venv' based on environment. + + :param str python_exe: The Python executable to use (if not using the default). + :param str python_version: Optional Python version string to pass through to ``uv venv --python``. + Only valid when the backend is ``uv``; raises if used with the ``pip`` backend. + :return: List of command arguments for venv. + :rtype: List[str] + """ + pip_impl = os.environ.get("TOX_PIP_IMPL", "pip").lower() + + if python_version and pip_impl != "uv": + raise ValueError("--python requires uv as the backend. Install uv or set TOX_PIP_IMPL=uv.") + + # soon we will change this to default to uv + if pip_impl == "uv": + cmd = ["uv", "venv"] + if python_version: + cmd += ["--python", python_version] + return cmd + else: + return [python_exe if python_exe else sys.executable, "-m", "venv"] + + +def get_pip_command(python_exe: Optional[str] = None) -> List[str]: + """Determine whether to use 'uv pip' or regular 'pip' based on environment. + + :param str python_exe: The Python executable to use (if not using the default). + :return: List of command arguments for pip. + :rtype: List[str] + """ + # Check TOX_PIP_IMPL environment variable (aligns with tox.ini configuration) + pip_impl = os.environ.get("TOX_PIP_IMPL", "pip").lower() + + # soon we will change this to default to uv + if pip_impl == "uv": + return ["uv", "pip"] + else: + return [python_exe if python_exe else sys.executable, "-m", "pip"] + + +def get_venv_python(venv_path: str) -> str: + """Given a python venv path, identify the crossplat reference to the python executable.""" + # if we already have a path to a python executable, return it + if os.path.isfile(venv_path) and os.access(venv_path, os.X_OK) and os.path.basename(venv_path).startswith("python"): + return venv_path + + # cross-platform python in a venv + bin_dir = "Scripts" if os.name == "nt" else "bin" + python_exe = "python.exe" if os.name == "nt" else "python" + return os.path.join(venv_path, bin_dir, python_exe) + + +def install_into_venv(venv_path_or_executable: str, requirements: List[str], working_directory: str) -> None: + """Install the requirements into an existing venv (venv_path) without activating it. + + - Uses get_pip_command(get_venv_python) per request. + - If get_pip_command returns the 'uv' wrapper, we fall back to get_venv_python -m pip + so installation goes into the target venv reliably. + """ + py = get_venv_python(venv_path_or_executable) + pip_cmd = get_pip_command(py) + + install_targets = [r.strip() for r in requirements] + cmd = pip_cmd + ["install"] + install_targets + + if pip_cmd[0] == "uv": + cmd += ["--python", py] + + # todo: clean this up so that we're using run_logged from #42862 + subprocess.check_call(cmd, cwd=working_directory) + + +def uninstall_from_venv(venv_path_or_executable: str, requirements: List[str], working_directory: str) -> None: + """Uninstalls the requirements from an existing venv (venv_path) without activating it.""" + py = get_venv_python(venv_path_or_executable) + pip_cmd = get_pip_command(py) + + install_targets = [r.strip() for r in requirements] + cmd = pip_cmd + ["uninstall"] + if pip_cmd[0] != "uv": + cmd += ["-y"] + cmd.extend(install_targets) + + if pip_cmd[0] == "uv": + cmd += ["--python", py] + + subprocess.check_call(cmd, cwd=working_directory) + + +def pip_install( + requirements: List[str], + include_dependencies: bool = True, + python_executable: Optional[str] = None, + cwd: Optional[str] = None, +) -> bool: + """Attempts to invoke an install operation using the invoking python's pip. Empty requirements are auto-success.""" + + exe = get_pip_command(python_executable) + + command = exe + ["install"] + + if requirements: + command.extend([req.strip() for req in requirements]) + else: + return True + + try: + if cwd: + subprocess.check_call(command, cwd=cwd) + else: + subprocess.check_call(command) + except subprocess.CalledProcessError as f: + return False + + return True + + +def pip_uninstall(requirements: List[str], python_executable: str) -> bool: + """Attempts to invoke an uninstall operation using the invoking python's pip. Empty requirements are auto-success.""" + # use uninstall_from_venv() for uv venvs + exe = python_executable or sys.executable + command = [exe, "-m", "pip", "uninstall", "-y"] + + if requirements: + command.extend([req.strip() for req in requirements]) + else: + return True + + try: + result = subprocess.check_call(command) + return True + except subprocess.CalledProcessError as f: + return False + + +def pip_install_requirements_file(requirements_file: str, python_executable: Optional[str] = None) -> bool: + return pip_install(["-r", requirements_file], True, python_executable) + + +def run_pip_freeze(python_executable: Optional[str] = None) -> List[str]: + """Uses the invoking python executable to get the output from pip freeze.""" + exe = python_executable or sys.executable + + pip_cmd = get_pip_command(exe) + + # we use `freeze` because it is present on both pip and uv + out = subprocess.Popen( + pip_cmd + ["freeze", "--disable-pip-version-check"], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + + stdout, stderr = out.communicate() + + collected_output = [] + + if stdout and (stderr is None): + for line in stdout.decode("utf-8").splitlines(): + if line: + collected_output.append(line) + else: + raise Exception(stderr) + + return collected_output + + +def get_pip_list_output(python_executable: Optional[str] = None): + """Uses the invoking python executable to get the output from pip list.""" + pip_output = run_pip_freeze(python_executable) + + collected_output = {} + for line in pip_output: + if "==" in line: + package, version = re.split("==", line) + collected_output[package] = version + + return collected_output diff --git a/eng/tools/azure-sdk-tools/tests/test_build_interactions.py b/eng/tools/azure-sdk-tools/tests/test_build_interactions.py index 25e16dd5aa03..7bc1546390e8 100644 --- a/eng/tools/azure-sdk-tools/tests/test_build_interactions.py +++ b/eng/tools/azure-sdk-tools/tests/test_build_interactions.py @@ -18,3 +18,23 @@ def test_discover_targeted_packages(): def test_build_packages(): pass + + +def test_venv_helpers_importable(): + from ci_tools.venv import ( + get_venv_call, + get_pip_command, + get_venv_python, + install_into_venv, + uninstall_from_venv, + pip_install, + pip_uninstall, + pip_install_requirements_file, + run_pip_freeze, + get_pip_list_output, + ) + + # Verify re-exports from ci_tools.functions still work + from ci_tools.functions import get_venv_call as f_get_venv_call + + assert f_get_venv_call is get_venv_call diff --git a/sdk/agentserver/azure-ai-agentserver-agentframework/pyproject.toml b/sdk/agentserver/azure-ai-agentserver-agentframework/pyproject.toml index 814d1d6d1a1e..8e9d0af25bc2 100644 --- a/sdk/agentserver/azure-ai-agentserver-agentframework/pyproject.toml +++ b/sdk/agentserver/azure-ai-agentserver-agentframework/pyproject.toml @@ -58,6 +58,7 @@ known-first-party = ["azure.ai.agentserver.agentframework"] combine-as-imports = true [tool.azure-sdk-build] +analyze_python_version = "3.11" breaking = false # incompatible python version pyright = false verifytypes = false # incompatible python version for -core diff --git a/sdk/agentserver/azure-ai-agentserver-core/pyproject.toml b/sdk/agentserver/azure-ai-agentserver-core/pyproject.toml index f574360722bb..6e64cb740552 100644 --- a/sdk/agentserver/azure-ai-agentserver-core/pyproject.toml +++ b/sdk/agentserver/azure-ai-agentserver-core/pyproject.toml @@ -66,6 +66,7 @@ known-first-party = ["azure.ai.agentserver.core"] combine-as-imports = true [tool.azure-sdk-build] +analyze_python_version = "3.11" breaking = false # incompatible python version pyright = false verifytypes = false \ No newline at end of file diff --git a/sdk/agentserver/azure-ai-agentserver-githubcopilot/pyproject.toml b/sdk/agentserver/azure-ai-agentserver-githubcopilot/pyproject.toml index 3d8b3bd0cf6c..9a7c1e2a59d5 100644 --- a/sdk/agentserver/azure-ai-agentserver-githubcopilot/pyproject.toml +++ b/sdk/agentserver/azure-ai-agentserver-githubcopilot/pyproject.toml @@ -59,6 +59,7 @@ known-first-party = ["azure.ai.agentserver.githubcopilot"] combine-as-imports = true [tool.azure-sdk-build] +analyze_python_version = "3.11" breaking = false pyright = false verifytypes = false diff --git a/sdk/agentserver/azure-ai-agentserver-langgraph/pyproject.toml b/sdk/agentserver/azure-ai-agentserver-langgraph/pyproject.toml index 54bce651f401..f95d7e379c00 100644 --- a/sdk/agentserver/azure-ai-agentserver-langgraph/pyproject.toml +++ b/sdk/agentserver/azure-ai-agentserver-langgraph/pyproject.toml @@ -63,6 +63,7 @@ known-first-party = ["azure.ai.agentserver.langgraph"] combine-as-imports = true [tool.azure-sdk-build] +analyze_python_version = "3.11" breaking = false # incompatible python version pyright = false verifytypes = false # incompatible python version for -core diff --git a/sdk/agentserver/ci.yml b/sdk/agentserver/ci.yml index d9d8936842a3..a957a82ceae6 100644 --- a/sdk/agentserver/ci.yml +++ b/sdk/agentserver/ci.yml @@ -32,7 +32,6 @@ extends: TestProxy: true BuildDocs: true TestTimeoutInMinutes: 60 - PythonVersionForAnalyze: '3.11' # The job "Test ubuntu2404_pypy311" in the "python - agentserver" pipeline hangs and eventually times out. # Disable it until the issue is understood. MatrixConfigs: