diff --git a/.chronus/changes/copilot-fix-3115-2025-6-1-3-6-11.md b/.chronus/changes/copilot-fix-3115-2025-6-1-3-6-11.md new file mode 100644 index 00000000000..351b3d97ece --- /dev/null +++ b/.chronus/changes/copilot-fix-3115-2025-6-1-3-6-11.md @@ -0,0 +1,7 @@ +--- +changeKind: internal +packages: + - "@autorest/python" +--- + +Regular update \ No newline at end of file diff --git a/.chronus/changes/copilot-fix-3115-2025-7-01-02-45-12.md b/.chronus/changes/copilot-fix-3115-2025-7-01-02-45-12.md new file mode 100644 index 00000000000..2dabf345751 --- /dev/null +++ b/.chronus/changes/copilot-fix-3115-2025-7-01-02-45-12.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: feature +packages: + - "@azure-tools/typespec-python" +--- + +[typespec-python] Add support for uv package manager alongside pip \ No newline at end of file diff --git a/packages/autorest.python/package.json b/packages/autorest.python/package.json index 8c9193665db..3fbece91863 100644 --- a/packages/autorest.python/package.json +++ b/packages/autorest.python/package.json @@ -29,7 +29,7 @@ }, "homepage": "https://github.com/Azure/autorest.python/blob/main/README.md", "dependencies": { - "@typespec/http-client-python": "~0.12.4", + "@typespec/http-client-python": "0.13.0-dev.2", "@autorest/system-requirements": "~1.0.2", "fs-extra": "~11.2.0", "tsx": "~4.19.1" diff --git a/packages/typespec-python/package.json b/packages/typespec-python/package.json index 7b09e3d4347..d22ec8dd6a9 100644 --- a/packages/typespec-python/package.json +++ b/packages/typespec-python/package.json @@ -67,7 +67,7 @@ "js-yaml": "~4.1.0", "semver": "~7.6.2", "tsx": "~4.19.1", - "@typespec/http-client-python": "~0.12.4", + "@typespec/http-client-python": "0.13.0-dev.2", "fs-extra": "~11.2.0" }, "devDependencies": { diff --git a/packages/typespec-python/scripts/install.py b/packages/typespec-python/scripts/install.py index 73d7060a639..5fa610349fb 100644 --- a/packages/typespec-python/scripts/install.py +++ b/packages/typespec-python/scripts/install.py @@ -11,9 +11,11 @@ raise Exception("Autorest for Python extension requires Python 3.9 at least") try: - import pip -except ImportError: - raise Exception("Your Python installation doesn't have pip available") + from package_manager import detect_package_manager, PackageManagerNotFoundError + + detect_package_manager() # Just check if we have a package manager +except (ImportError, ModuleNotFoundError, PackageManagerNotFoundError): + raise Exception("Your Python installation doesn't have a suitable package manager (pip or uv) available") try: import venv @@ -21,27 +23,23 @@ raise Exception("Your Python installation doesn't have venv available") -# Now we have pip and Py >= 3.9, go to work +# Now we have a package manager (uv or pip) and Py >= 3.9, go to work from pathlib import Path -from venvtools import ExtendedEnvBuilder, python_run - _ROOT_DIR = Path(__file__).parent.parent def main(): venv_path = _ROOT_DIR / "venv" - if venv_path.exists(): - env_builder = venv.EnvBuilder(with_pip=True) - venv_context = env_builder.ensure_directories(venv_path) - else: - env_builder = ExtendedEnvBuilder(with_pip=True, upgrade_deps=True) - env_builder.create(venv_path) - venv_context = env_builder.context - - python_run(venv_context, "pip", ["install", "-U", "pip"]) - python_run(venv_context, "pip", ["install", "-U", "black"]) + + # Create virtual environment using package manager abstraction + from package_manager import create_venv_with_package_manager, install_packages + + venv_context = create_venv_with_package_manager(venv_path) + + # Install required packages - install_packages handles package manager logic + install_packages(["-U", "black"], venv_context) if __name__ == "__main__": diff --git a/packages/typespec-python/scripts/package_manager.py b/packages/typespec-python/scripts/package_manager.py new file mode 100644 index 00000000000..6cb237b7d4b --- /dev/null +++ b/packages/typespec-python/scripts/package_manager.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python + +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +"""Package manager utilities for detecting and using pip or uv.""" + +import subprocess +import sys +import venv +from pathlib import Path +from venvtools import ExtendedEnvBuilder + + +class PackageManagerNotFoundError(Exception): + """Raised when no suitable package manager is found.""" + + pass + + +def _check_command_available(command: str) -> bool: + """Check if a command is available in the environment.""" + try: + subprocess.run([command, "--version"], capture_output=True, check=True) + return True + except (subprocess.CalledProcessError, FileNotFoundError): + return False + + +def detect_package_manager() -> str: + """Detect the best available package manager. + + Returns: + str: The package manager command ('uv' or 'pip') + + Raises: + PackageManagerNotFoundError: If no suitable package manager is found + """ + # Check for uv first since it's more modern and faster + if _check_command_available("uv"): + return "uv" + + # Fall back to pip + if _check_command_available("pip"): + return "pip" + + # As a last resort, try using python -m pip + try: + subprocess.run([sys.executable, "-m", "pip", "--version"], capture_output=True, check=True) + return "python -m pip" + except (subprocess.CalledProcessError, FileNotFoundError): + pass + + raise PackageManagerNotFoundError("No suitable package manager found. Please install either uv or pip.") + + +def get_install_command(package_manager: str, venv_context=None) -> list: + """Get the install command for the given package manager. + + Args: + package_manager: The package manager command ('uv', 'pip', or 'python -m pip') + venv_context: The virtual environment context (optional, used for pip) + + Returns: + list: The base install command as a list + """ + if package_manager == "uv": + cmd = ["uv", "pip", "install"] + if venv_context: + cmd.extend(["--python", venv_context.env_exe]) + return cmd + elif package_manager == "pip": + if venv_context: + return [venv_context.env_exe, "-m", "pip", "install"] + else: + return ["pip", "install"] + elif package_manager == "python -m pip": + if venv_context: + return [venv_context.env_exe, "-m", "pip", "install"] + else: + return [sys.executable, "-m", "pip", "install"] + else: + raise ValueError(f"Unknown package manager: {package_manager}") + + +def install_packages(packages: list, venv_context=None, package_manager: str = None) -> None: + """Install packages using the available package manager. + + Args: + packages: List of packages to install + venv_context: Virtual environment context (optional) + package_manager: Package manager to use (auto-detected if None) + """ + if package_manager is None: + package_manager = detect_package_manager() + + install_cmd = get_install_command(package_manager, venv_context) + + try: + subprocess.check_call(install_cmd + packages) + except subprocess.CalledProcessError as e: + raise RuntimeError(f"Failed to install packages with {package_manager}: {e}") + + +def create_venv_with_package_manager(venv_path): + """Create virtual environment using the best available package manager. + + Args: + venv_path: Path where to create the virtual environment + + Returns: + venv_context: Virtual environment context object + """ + package_manager = detect_package_manager() + + if package_manager == "uv": + # Use uv to create and manage the virtual environment + if not venv_path.exists(): + subprocess.check_call(["uv", "venv", str(venv_path)]) + + # Create a mock venv_context for compatibility + class MockVenvContext: + def __init__(self, venv_path): + self.env_exe = ( + str(venv_path / "bin" / "python") + if sys.platform != "win32" + else str(venv_path / "Scripts" / "python.exe") + ) + + return MockVenvContext(venv_path) + else: + # Use standard venv for pip + if venv_path.exists(): + env_builder = venv.EnvBuilder(with_pip=True) + return env_builder.ensure_directories(venv_path) + else: + env_builder = ExtendedEnvBuilder(with_pip=True, upgrade_deps=True) + env_builder.create(venv_path) + return env_builder.context diff --git a/packages/typespec-python/scripts/prepare.py b/packages/typespec-python/scripts/prepare.py index 848e1d6cc22..e15a83d27ac 100644 --- a/packages/typespec-python/scripts/prepare.py +++ b/packages/typespec-python/scripts/prepare.py @@ -6,16 +6,12 @@ # license information. # -------------------------------------------------------------------------- import sys -import os -import argparse if not sys.version_info >= (3, 9, 0): raise Exception("Autorest for Python extension requires Python 3.9 at least") from pathlib import Path -import venv - -from venvtools import python_run +from package_manager import create_venv_with_package_manager, install_packages _ROOT_DIR = Path(__file__).parent.parent @@ -26,10 +22,10 @@ def main(): assert venv_preexists # Otherwise install was not done - env_builder = venv.EnvBuilder(with_pip=True) - venv_context = env_builder.ensure_directories(venv_path) + venv_context = create_venv_with_package_manager(venv_path) + try: - python_run(venv_context, "pip", ["install", "-r", f"{_ROOT_DIR}/dev_requirements.txt"]) + install_packages(["-r", f"{_ROOT_DIR}/dev_requirements.txt"], venv_context) except FileNotFoundError as e: raise ValueError(e.filename) diff --git a/packages/typespec-python/scripts/run_tsp.py b/packages/typespec-python/scripts/run_tsp.py index 783b8472946..62f554606f4 100644 --- a/packages/typespec-python/scripts/run_tsp.py +++ b/packages/typespec-python/scripts/run_tsp.py @@ -4,11 +4,11 @@ # license information. # -------------------------------------------------------------------------- import sys -import venv import logging from pathlib import Path from pygen import preprocess, codegen from pygen.utils import parse_args +from package_manager import create_venv_with_package_manager _ROOT_DIR = Path(__file__).parent.parent @@ -20,14 +20,13 @@ assert venv_preexists # Otherwise install was not done - env_builder = venv.EnvBuilder(with_pip=True) - venv_context = env_builder.ensure_directories(venv_path) + venv_context = create_venv_with_package_manager(venv_path) if "--debug" in sys.argv or "--debug=true" in sys.argv: try: import debugpy # pylint: disable=import-outside-toplevel except ImportError: - raise SystemExit("Please pip install ptvsd in order to use VSCode debugging") + raise SystemExit("Please install ptvsd in order to use VSCode debugging") # 5678 is the default attach port in the VS Code debug configurations debugpy.listen(("localhost", 5678)) diff --git a/packages/typespec-python/scripts/venvtools.py b/packages/typespec-python/scripts/venvtools.py index 3b9eb69474d..f15abdd5250 100644 --- a/packages/typespec-python/scripts/venvtools.py +++ b/packages/typespec-python/scripts/venvtools.py @@ -3,8 +3,6 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- -from contextlib import contextmanager -import tempfile import subprocess import venv import sys @@ -28,45 +26,10 @@ def ensure_directories(self, env_dir): return self.context -def create( - env_dir, system_site_packages=False, clear=False, symlinks=False, with_pip=False, prompt=None, upgrade_deps=False -): - """Create a virtual environment in a directory.""" - builder = ExtendedEnvBuilder( - system_site_packages=system_site_packages, - clear=clear, - symlinks=symlinks, - with_pip=with_pip, - prompt=prompt, - upgrade_deps=upgrade_deps, - ) - builder.create(env_dir) - return builder.context - - -@contextmanager -def create_venv_with_package(packages): - """Create a venv with these packages in a temp dir and yield the env. - - packages should be an iterable of pip version instructions (e.g. package~=1.2.3) - """ - with tempfile.TemporaryDirectory() as tempdir: - myenv = create(tempdir, with_pip=True, upgrade_deps=True) - pip_call = [ - myenv.env_exe, - "-m", - "pip", - "install", - ] - subprocess.check_call(pip_call + ["-U", "pip"]) - if packages: - subprocess.check_call(pip_call + packages) - yield myenv - - def python_run(venv_context, module, command=None, *, additional_dir="."): try: cmd_line = [venv_context.env_exe, "-m", module] + (command if command else []) + print("Executing: {}".format(" ".join(cmd_line))) subprocess.run( cmd_line, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f06bf53a43..198cf7cfc14 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,8 +60,8 @@ importers: specifier: ~1.0.2 version: 1.0.2 '@typespec/http-client-python': - specifier: ~0.12.4 - version: 0.12.4(zqlw4u2idnbcqieabzlhuf7frq) + specifier: 0.13.0-dev.2 + version: 0.13.0-dev.2(zqlw4u2idnbcqieabzlhuf7frq) fs-extra: specifier: ~11.2.0 version: 11.2.0 @@ -82,8 +82,8 @@ importers: packages/typespec-python: dependencies: '@typespec/http-client-python': - specifier: ~0.12.4 - version: 0.12.4(zqlw4u2idnbcqieabzlhuf7frq) + specifier: 0.13.0-dev.2 + version: 0.13.0-dev.2(zqlw4u2idnbcqieabzlhuf7frq) fs-extra: specifier: ~11.2.0 version: 11.2.0 @@ -1684,8 +1684,8 @@ packages: peerDependencies: '@typespec/compiler': ^1.1.0 - '@typespec/http-client-python@0.12.4': - resolution: {integrity: sha512-jNS1zaqCgUbCfvf1N8AQb6w0XwrXgN95Z5CqUkmhzik7jAZY9OxFuIQbYGtv2yB4+K/cBgfuaNEc8T4j9t0hqQ==} + '@typespec/http-client-python@0.13.0-dev.2': + resolution: {integrity: sha512-fVWDyGSHgpvV7vvYlhjEecU7UPXj5Uw9mvVdOq9MXiioQ/+8wBmLwSeBKsSkQnZeEEN8RjNLzPc1g0xW4teScw==} engines: {node: '>=20.0.0'} peerDependencies: '@azure-tools/typespec-autorest': '>=0.56.0 <1.0.0' @@ -6437,7 +6437,7 @@ snapshots: dependencies: '@typespec/compiler': 1.1.0(@types/node@22.13.17) - '@typespec/http-client-python@0.12.4(zqlw4u2idnbcqieabzlhuf7frq)': + '@typespec/http-client-python@0.13.0-dev.2(zqlw4u2idnbcqieabzlhuf7frq)': dependencies: '@azure-tools/typespec-autorest': 0.57.0(y57gch43lkhene77dcdsarymam) '@azure-tools/typespec-azure-core': 0.57.0(@typespec/compiler@1.1.0(@types/node@22.13.17))(@typespec/http@1.1.0(@typespec/compiler@1.1.0(@types/node@22.13.17))(@typespec/streams@0.71.0(@typespec/compiler@1.1.0(@types/node@22.13.17))))(@typespec/rest@0.71.0(@typespec/compiler@1.1.0(@types/node@22.13.17))(@typespec/http@1.1.0(@typespec/compiler@1.1.0(@types/node@22.13.17))(@typespec/streams@0.71.0(@typespec/compiler@1.1.0(@types/node@22.13.17)))))