Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option --pip-cmd to pass uv as an alternative package installer. #240

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ When invoked without arguments `pew` will output the list of all commands with e

Create a new environment, in the WORKON_HOME.

`usage: pew new [-hd] [-p PYTHON] [-i PACKAGES] [-a PROJECT] [-r REQUIREMENTS] envname`
`usage: pew new [-hd] [-p PYTHON] [-c {pip,uv}] [-i PACKAGES] [-a PROJECT] [-r REQUIREMENTS] envname`

The new environment is automatically activated after being initialized.

Expand All @@ -250,6 +250,8 @@ The `-i` option can be used to install one or more packages (by repeating the op

The `-r` option can be used to specify a text file listing packages to be installed. The argument value is passed to `pip -r` to be installed.

The `-c` option can be used to pass an alternative package installer. Currently supporting [uv](https://github.com/astral-sh/uv).

### workon ###

List or change working virtual environments.
Expand All @@ -262,7 +264,7 @@ If no `envname` is given the list of available environments is printed to stdout

Create a temporary virtualenv.

`usage: pew mktmpenv [-h] [-p PYTHON] [-i PACKAGES] [-a PROJECT] [-r REQUIREMENTS]`
`usage: pew mktmpenv [-h] [-p PYTHON] [-c {pip,uv}] [-i PACKAGES] [-a PROJECT] [-r REQUIREMENTS]`

### ls ###

Expand Down Expand Up @@ -362,7 +364,7 @@ Controls whether the active virtualenv will access the packages in the global Py

Create a new virtualenv in the `WORKON_HOME` and project directory in `PROJECT_HOME`.

`usage: pew mkproject [-hd] [-p PYTHON] [-i PACKAGES] [-a PROJECT] [-r REQUIREMENTS] [-t TEMPLATES] [-l] envname`
`usage: pew mkproject [-hd] [-p PYTHON] [-c {pip,uv}] [-i PACKAGES] [-a PROJECT] [-r REQUIREMENTS] [-t TEMPLATES] [-l] envname`

The template option may be repeated to have several templates used to create a new project. The templates are applied in the order named on the command line. All other options are passed to `pew new` to create a virtual environment with the same name as the project.

Expand Down
4 changes: 2 additions & 2 deletions pew/__main__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from pew.pew import pew
from pew.pew import main

pew()
main()
4 changes: 1 addition & 3 deletions pew/_utils.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import os
import sys
import locale
from codecs import getwriter
from contextlib import contextmanager
from subprocess import check_call, Popen, PIPE
from collections import namedtuple
from functools import partial, wraps
from functools import partial
from pathlib import Path
from tempfile import NamedTemporaryFile
from shutil import which

windows = sys.platform == 'win32'
Expand Down
97 changes: 71 additions & 26 deletions pew/pew.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@
from functools import partial
from subprocess import CalledProcessError
from pathlib import Path

from shutil import get_terminal_size
from tempfile import NamedTemporaryFile

from clonevirtualenv import clone_virtualenv

from pew._utils import (check_call, invoke, expandpath, own, env_bin_dir,
check_path, temp_environ)
from pew._print_utils import print_virtualenvs

windows = sys.platform == 'win32'

from clonevirtualenv import clone_virtualenv

if not windows:
try:
# Try importing these packages if available
Expand All @@ -29,7 +35,7 @@ def ListPythons():
except OSError:
pass
return ListCommand()
except:
except: # noqa: E722
# create mock commands
InstallCommand = ListPythons = LocatePython = UninstallCommand = \
lambda : sys.exit('You need to install the pythonz extra. pip install pew[pythonz]')
Expand All @@ -40,9 +46,6 @@ def ListPythons():

import shellingham

from pew._utils import (check_call, invoke, expandpath, own, env_bin_dir,
check_path, temp_environ, NamedTemporaryFile)
from pew._print_utils import print_virtualenvs

err = partial(print, file=sys.stderr)

Expand All @@ -55,6 +58,12 @@ def ListPythons():
os.environ.get('WORKON_HOME', default_home))


PIP_CMD_ARGS = {
'pip': ['pip'],
'uv': ['uv', 'pip'],
}


def makedirs_and_symlink_if_needed(workon_home):
if not workon_home.exists() and own(workon_home):
workon_home.mkdir(parents=True)
Expand Down Expand Up @@ -213,11 +222,11 @@ def shell(env, cwd=None):
return fork_shell(env, [shell], cwd)


def mkvirtualenv(envname, python=None, packages=[], project=None,
requirements=None, rest=[]):
def mkvirtualenv(envname, python=None, packages=None, project=None,
requirements=None, pip_cmd=None, rest=None):

if python:
rest = ["--python=%s" % python] + rest
rest = ["--python=%s" % python] + (rest or [])

path = (workon_home / envname).absolute()

Expand All @@ -227,12 +236,13 @@ def mkvirtualenv(envname, python=None, packages=[], project=None,
rmvirtualenvs([envname])
raise
else:
pip_command = PIP_CMD_ARGS.get(pip_cmd, ['pip'])
if project:
setvirtualenvproject(envname, project.absolute())
if requirements:
inve(envname, 'pip', 'install', '-r', str(expandpath(requirements)))
inve(envname, *(pip_command + ['install', '-r', str(expandpath(requirements))]))
if packages:
inve(envname, 'pip', 'install', *packages)
inve(envname, *(pip_command + ['install', *packages]))


def mkvirtualenv_argparser():
Expand All @@ -249,18 +259,33 @@ def mkvirtualenv_argparser():
return parser


def add_pip_cmd_argument(parser):
parser.add_argument('-c', '--pip-cmd', choices=list(PIP_CMD_ARGS.keys()), default='pip', dest='pip_cmd',
help='The pip command used to install packages (default: %(default)s). \
Possible values: "uv" - use "uv pip", "pip" - use pip')



def new_cmd(argv):
"""Create a new environment, in $WORKON_HOME."""
parser = mkvirtualenv_argparser()
parser.add_argument('-a', dest='project', help='Provide a full path to a \
project directory to associate with the new environment.')

parser.add_argument('envname')
add_pip_cmd_argument(parser)
args, rest = parser.parse_known_args(argv)
project = expandpath(args.project) if args.project else None

mkvirtualenv(args.envname, args.python, args.packages, project,
args.requirements, rest)
mkvirtualenv(
envname=args.envname,
python=args.python,
packages=args.packages,
project=project,
requirements=args.requirements,
pip_cmd=args.pip_cmd,
rest=rest,
)
if args.activate:
shell(args.envname)

Expand Down Expand Up @@ -564,6 +589,7 @@ def mkproject_cmd(argv):
return

parser = mkvirtualenv_argparser()
add_pip_cmd_argument(parser)
parser.add_argument('envname')
parser.add_argument(
'-t', action='append', default=[], dest='templates', help='Multiple \
Expand All @@ -583,8 +609,15 @@ def mkproject_cmd(argv):
if project.exists():
sys.exit('Project %s already exists.' % args.envname)

mkvirtualenv(args.envname, args.python, args.packages, project.absolute(),
args.requirements, rest)
mkvirtualenv(
envname=args.envname,
python=args.python,
packages=args.packages,
project=project.absolute(),
requirements=args.requirements,
pip_cmd=args.pip_cmd,
rest=rest,
)

project.mkdir()

Expand All @@ -598,14 +631,21 @@ def mkproject_cmd(argv):
def mktmpenv_cmd(argv):
"""Create a temporary virtualenv."""
parser = mkvirtualenv_argparser()
add_pip_cmd_argument(parser)
env = '.'
while (workon_home / env).exists():
env = hex(random.getrandbits(64))[2:-1]

args, rest = parser.parse_known_args(argv)

mkvirtualenv(env, args.python, args.packages, requirements=args.requirements,
rest=rest)
mkvirtualenv(
envname=env,
python=args.python,
packages=args.packages,
requirements=args.requirements,
pip_cmd=args.pip_cmd,
rest=rest,
)
print('This is a temporary environment. It will be deleted when you exit')
try:
if args.activate:
Expand Down Expand Up @@ -736,7 +776,7 @@ def prevent_path_errors():
In this case, for further details please see: https://github.com/berdario/pew#the-environment-doesnt-seem-to-be-activated''')


def first_run_setup():
def first_run_setup(cmd_args):
shell = supported_shell()
if shell:
if shell == 'fish':
Expand All @@ -752,7 +792,7 @@ def first_run_setup():
print("It seems that you're running pew for the first time\n"
"If you want source shell competions and update your prompt, "
"Add the following line to your shell config file:\n %s" % source_cmd)
print('\nWill now continue with the command:', *sys.argv[1:])
print('\nWill now continue with the command:', *cmd_args)
input('[enter]')

def update_config_file(rcpath, source_cmd):
Expand Down Expand Up @@ -787,25 +827,30 @@ def print_commands(cmds):
print(' ' + cmd)


def pew():
def pew(cmd_args):
first_run = makedirs_and_symlink_if_needed(workon_home)
if first_run and sys.stdin.isatty():
first_run_setup()
first_run_setup(cmd_args)

cmds = dict((cmd[:-4], fun)
for cmd, fun in globals().items() if cmd.endswith('_cmd'))
if sys.argv[1:]:
if sys.argv[1] in cmds:
command = cmds[sys.argv[1]]

if cmd_args:
if cmd_args[0] in cmds:
command = cmds[cmd_args[0]]
try:
return command(sys.argv[2:])
return command(cmd_args[1:])
except CalledProcessError as e:
return e.returncode
except KeyboardInterrupt:
pass
else:
err("ERROR: command", sys.argv[1], "does not exist.")
err("ERROR: command", cmd_args[0], "does not exist.")
print_commands(cmds)
sys.exit(1)
else:
print_commands(cmds)


def main():
return pew(sys.argv[1:])
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def run(self):
'deb': DebCommand
},
entry_points={
'console_scripts': ['pew = pew.pew:pew']},
'console_scripts': ['pew = pew.pew:main']},
classifiers=[
'Programming Language :: Python :: 3',
'Intended Audience :: Developers',
Expand Down
26 changes: 26 additions & 0 deletions tests/test_new.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from unittest.mock import patch
from uuid import uuid4

import pytest

from pew.pew import pew


@pytest.fixture
def mocked_check_call():
with patch("pew.pew.check_call") as mock:
yield mock


@pytest.mark.usefixtures("workon_home")
@pytest.mark.parametrize(
"pip_cmd,exp_call",
[
("pip", ["pip", "install", "pew"]),
("uv", ["uv", "pip", "install", "pew"]),
]
)
def test_pip_cmd_option(mocked_check_call, pip_cmd, exp_call):
pew(["new", "-d", "--pip-cmd", pip_cmd, "-i", "pew", str(uuid4())])
assert mocked_check_call.called
assert mocked_check_call.call_args[0][0] == exp_call
3 changes: 2 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tox]
skip_missing_interpreters=True
envlist = py{36,37,38,39,310}-{linux,windows}, pypy3
envlist = py{36,37,38,39,310,311}-{linux,windows}, pypy3

[gh-actions]
python =
Expand All @@ -9,6 +9,7 @@ python =
3.8: py38
3.9: py39
3.10: py310
3.11: py311

[gh-actions:env]
PLATFORM =
Expand Down