Skip to content

Commit 2193c39

Browse files
Copilotmsyyc
andcommitted
Add uv package manager support alongside pip in typespec-python scripts
Co-authored-by: msyyc <70930885+msyyc@users.noreply.github.com>
1 parent 839d67f commit 2193c39

File tree

6 files changed

+165
-63
lines changed

6 files changed

+165
-63
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
3+
changeKind: feature
4+
packages:
5+
- "@azure-tools/typespec-python"
6+
---
7+
8+
[typespec-python] Add support for uv package manager alongside pip

packages/typespec-python/scripts/install.py

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,37 +11,33 @@
1111
raise Exception("Autorest for Python extension requires Python 3.9 at least")
1212

1313
try:
14-
import pip
15-
except ImportError:
16-
raise Exception("Your Python installation doesn't have pip available")
14+
from package_manager import detect_package_manager, PackageManagerNotFoundError
15+
detect_package_manager() # Just check if we have a package manager
16+
except (ImportError, ModuleNotFoundError, PackageManagerNotFoundError):
17+
raise Exception("Your Python installation doesn't have a suitable package manager (pip or uv) available")
1718

1819
try:
1920
import venv
2021
except ImportError:
2122
raise Exception("Your Python installation doesn't have venv available")
2223

2324

24-
# Now we have pip and Py >= 3.9, go to work
25+
# Now we have a package manager (uv or pip) and Py >= 3.9, go to work
2526

2627
from pathlib import Path
2728

28-
from venvtools import ExtendedEnvBuilder, python_run
29-
3029
_ROOT_DIR = Path(__file__).parent.parent
3130

3231

3332
def main():
3433
venv_path = _ROOT_DIR / "venv"
35-
if venv_path.exists():
36-
env_builder = venv.EnvBuilder(with_pip=True)
37-
venv_context = env_builder.ensure_directories(venv_path)
38-
else:
39-
env_builder = ExtendedEnvBuilder(with_pip=True, upgrade_deps=True)
40-
env_builder.create(venv_path)
41-
venv_context = env_builder.context
42-
43-
python_run(venv_context, "pip", ["install", "-U", "pip"])
44-
python_run(venv_context, "pip", ["install", "-U", "black"])
34+
35+
# Create virtual environment using package manager abstraction
36+
from package_manager import create_venv_with_package_manager, install_packages
37+
venv_context = create_venv_with_package_manager(venv_path)
38+
39+
# Install required packages - install_packages handles package manager logic
40+
install_packages(["-U", "black"], venv_context)
4541

4642

4743
if __name__ == "__main__":
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
#!/usr/bin/env python
2+
3+
# -------------------------------------------------------------------------
4+
# Copyright (c) Microsoft Corporation. All rights reserved.
5+
# Licensed under the MIT License. See License.txt in the project root for
6+
# license information.
7+
# --------------------------------------------------------------------------
8+
"""Package manager utilities for detecting and using pip or uv."""
9+
10+
import subprocess
11+
import sys
12+
import venv
13+
from pathlib import Path
14+
from venvtools import ExtendedEnvBuilder
15+
16+
17+
class PackageManagerNotFoundError(Exception):
18+
"""Raised when no suitable package manager is found."""
19+
pass
20+
21+
22+
def _check_command_available(command: str) -> bool:
23+
"""Check if a command is available in the environment."""
24+
try:
25+
subprocess.run([command, "--version"], capture_output=True, check=True)
26+
return True
27+
except (subprocess.CalledProcessError, FileNotFoundError):
28+
return False
29+
30+
31+
def detect_package_manager() -> str:
32+
"""Detect the best available package manager.
33+
34+
Returns:
35+
str: The package manager command ('uv' or 'pip')
36+
37+
Raises:
38+
PackageManagerNotFoundError: If no suitable package manager is found
39+
"""
40+
# Check for uv first since it's more modern and faster
41+
if _check_command_available("uv"):
42+
return "uv"
43+
44+
# Fall back to pip
45+
if _check_command_available("pip"):
46+
return "pip"
47+
48+
# As a last resort, try using python -m pip
49+
try:
50+
subprocess.run([sys.executable, "-m", "pip", "--version"],
51+
capture_output=True, check=True)
52+
return "python -m pip"
53+
except (subprocess.CalledProcessError, FileNotFoundError):
54+
pass
55+
56+
raise PackageManagerNotFoundError(
57+
"No suitable package manager found. Please install either uv or pip."
58+
)
59+
60+
61+
def get_install_command(package_manager: str, venv_context=None) -> list:
62+
"""Get the install command for the given package manager.
63+
64+
Args:
65+
package_manager: The package manager command ('uv', 'pip', or 'python -m pip')
66+
venv_context: The virtual environment context (optional, used for pip)
67+
68+
Returns:
69+
list: The base install command as a list
70+
"""
71+
if package_manager == "uv":
72+
cmd = ["uv", "pip", "install"]
73+
if venv_context:
74+
cmd.extend(["--python", venv_context.env_exe])
75+
return cmd
76+
elif package_manager == "pip":
77+
if venv_context:
78+
return [venv_context.env_exe, "-m", "pip", "install"]
79+
else:
80+
return ["pip", "install"]
81+
elif package_manager == "python -m pip":
82+
if venv_context:
83+
return [venv_context.env_exe, "-m", "pip", "install"]
84+
else:
85+
return [sys.executable, "-m", "pip", "install"]
86+
else:
87+
raise ValueError(f"Unknown package manager: {package_manager}")
88+
89+
90+
def install_packages(packages: list, venv_context=None, package_manager: str = None) -> None:
91+
"""Install packages using the available package manager.
92+
93+
Args:
94+
packages: List of packages to install
95+
venv_context: Virtual environment context (optional)
96+
package_manager: Package manager to use (auto-detected if None)
97+
"""
98+
if package_manager is None:
99+
package_manager = detect_package_manager()
100+
101+
install_cmd = get_install_command(package_manager, venv_context)
102+
103+
try:
104+
subprocess.check_call(install_cmd + packages)
105+
except subprocess.CalledProcessError as e:
106+
raise RuntimeError(f"Failed to install packages with {package_manager}: {e}")
107+
108+
109+
def create_venv_with_package_manager(venv_path):
110+
"""Create virtual environment using the best available package manager.
111+
112+
Args:
113+
venv_path: Path where to create the virtual environment
114+
115+
Returns:
116+
venv_context: Virtual environment context object
117+
"""
118+
package_manager = detect_package_manager()
119+
120+
if package_manager == "uv":
121+
# Use uv to create and manage the virtual environment
122+
if not venv_path.exists():
123+
subprocess.check_call(["uv", "venv", str(venv_path)])
124+
125+
# Create a mock venv_context for compatibility
126+
class MockVenvContext:
127+
def __init__(self, venv_path):
128+
self.env_exe = str(venv_path / "bin" / "python") if sys.platform != "win32" else str(venv_path / "Scripts" / "python.exe")
129+
130+
return MockVenvContext(venv_path)
131+
else:
132+
# Use standard venv for pip
133+
if venv_path.exists():
134+
env_builder = venv.EnvBuilder(with_pip=True)
135+
return env_builder.ensure_directories(venv_path)
136+
else:
137+
env_builder = ExtendedEnvBuilder(with_pip=True, upgrade_deps=True)
138+
env_builder.create(venv_path)
139+
return env_builder.context

packages/typespec-python/scripts/prepare.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,12 @@
66
# license information.
77
# --------------------------------------------------------------------------
88
import sys
9-
import os
10-
import argparse
119

1210
if not sys.version_info >= (3, 9, 0):
1311
raise Exception("Autorest for Python extension requires Python 3.9 at least")
1412

1513
from pathlib import Path
16-
import venv
17-
18-
from venvtools import python_run
14+
from package_manager import create_venv_with_package_manager, install_packages
1915

2016
_ROOT_DIR = Path(__file__).parent.parent
2117

@@ -26,10 +22,10 @@ def main():
2622

2723
assert venv_preexists # Otherwise install was not done
2824

29-
env_builder = venv.EnvBuilder(with_pip=True)
30-
venv_context = env_builder.ensure_directories(venv_path)
25+
venv_context = create_venv_with_package_manager(venv_path)
26+
3127
try:
32-
python_run(venv_context, "pip", ["install", "-r", f"{_ROOT_DIR}/dev_requirements.txt"])
28+
install_packages(["-r", f"{_ROOT_DIR}/dev_requirements.txt"], venv_context)
3329
except FileNotFoundError as e:
3430
raise ValueError(e.filename)
3531

packages/typespec-python/scripts/run_tsp.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
try:
2828
import debugpy # pylint: disable=import-outside-toplevel
2929
except ImportError:
30-
raise SystemExit("Please pip install ptvsd in order to use VSCode debugging")
30+
raise SystemExit("Please install ptvsd in order to use VSCode debugging")
3131

3232
# 5678 is the default attach port in the VS Code debug configurations
3333
debugpy.listen(("localhost", 5678))

packages/typespec-python/scripts/venvtools.py

Lines changed: 1 addition & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
# Licensed under the MIT License. See License.txt in the project root for
44
# license information.
55
# --------------------------------------------------------------------------
6-
from contextlib import contextmanager
7-
import tempfile
86
import subprocess
97
import venv
108
import sys
@@ -28,45 +26,10 @@ def ensure_directories(self, env_dir):
2826
return self.context
2927

3028

31-
def create(
32-
env_dir, system_site_packages=False, clear=False, symlinks=False, with_pip=False, prompt=None, upgrade_deps=False
33-
):
34-
"""Create a virtual environment in a directory."""
35-
builder = ExtendedEnvBuilder(
36-
system_site_packages=system_site_packages,
37-
clear=clear,
38-
symlinks=symlinks,
39-
with_pip=with_pip,
40-
prompt=prompt,
41-
upgrade_deps=upgrade_deps,
42-
)
43-
builder.create(env_dir)
44-
return builder.context
45-
46-
47-
@contextmanager
48-
def create_venv_with_package(packages):
49-
"""Create a venv with these packages in a temp dir and yield the env.
50-
51-
packages should be an iterable of pip version instructions (e.g. package~=1.2.3)
52-
"""
53-
with tempfile.TemporaryDirectory() as tempdir:
54-
myenv = create(tempdir, with_pip=True, upgrade_deps=True)
55-
pip_call = [
56-
myenv.env_exe,
57-
"-m",
58-
"pip",
59-
"install",
60-
]
61-
subprocess.check_call(pip_call + ["-U", "pip"])
62-
if packages:
63-
subprocess.check_call(pip_call + packages)
64-
yield myenv
65-
66-
6729
def python_run(venv_context, module, command=None, *, additional_dir="."):
6830
try:
6931
cmd_line = [venv_context.env_exe, "-m", module] + (command if command else [])
32+
7033
print("Executing: {}".format(" ".join(cmd_line)))
7134
subprocess.run(
7235
cmd_line,

0 commit comments

Comments
 (0)