Skip to content

Commit 33b8d8d

Browse files
committed
Add venv uninstall subcommand
1 parent 6985681 commit 33b8d8d

File tree

1 file changed

+156
-5
lines changed

1 file changed

+156
-5
lines changed

src/venv-cli/venv.sh

Lines changed: 156 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,155 @@ venv::_add_packages_to_requirements() {
386386
LC_COLLATE=C sort --ignore-case --stable -o "${requirements_file}" "${requirements_file}"
387387
}
388388

389+
venv::uninstall() {
390+
if venv::_check_if_help_requested "$1"; then
391+
echo "venv uninstall <package> [...] [OPTIONS]"
392+
echo
393+
echo "Remove one or more installed packages from a requirements file, then reinstall the environment from that requirements file."
394+
echo "The resulting environment will be locked into a .lock-file."
395+
echo
396+
echo "The packages to uninsall must be the canonical package names as specified in the requirements file, e.g. 'numpy' or 'scikit-learn'."
397+
echo "If specified, the requirements file must have file extension '.txt'."
398+
echo "If no requirements file is passed, the command will assume the file 'requirements.txt' is to be used, and will fail if this file cannot be found."
399+
echo
400+
echo "Options:"
401+
echo " -h, --help Show this help and exit."
402+
echo " -r, --requirement <requirements file> Remove the package(s) from the given requirements file, then reinstall the environment."
403+
echo " If not specified, will default to using 'requirements.txt' and will fail if this file does not exist."
404+
echo " -s, --skip-lock Skip locking packages to a .lock-file after reinstallation."
405+
echo " --pip-args <ARGS> Additional arguments to pass through to pip install on reinstallation."
406+
echo
407+
echo "Examples:"
408+
echo "$ venv uninstall numpy"
409+
echo "$ venv uninstall numpy -r requirements.txt"
410+
echo "This will remove the 'numpy' requirement from 'requirements.txt', then reinstall the environment from 'requirements.txt' and lock them into 'requirements.lock'."
411+
echo
412+
echo "$ venv uninstall numpy pandas -r requirements/dev-requirements.txt -s --pip-args='--no-cache --pre'"
413+
echo "This will remove 'numpy' and 'pandas' requirements from 'requirements/dev-requirements.txt', then reinstall"
414+
echo "all requirements from 'dev-requirements.txt' without locking them."
415+
echo "The arguments '--no-cache' and '--pre' are passed on to 'pip install' on reinstallation."
416+
return "${_success}"
417+
fi
418+
419+
# Parse arguments. Fail if invalid arguments are passed
420+
local TEMP=$(getopt -o 'r:s' --long 'requirement:,skip-lock,pip-args::' -n 'venv uninstall' -- "$@")
421+
local _exit="$?"
422+
if [ "${_exit}" -ne 0 ]; then
423+
return "${_exit}"
424+
fi
425+
426+
local package_names=() # List of packages to uninstall
427+
local requirements_file=""
428+
local skip_lock=""
429+
local pip_args=""
430+
431+
eval set -- "$TEMP" # Unpack the arguments in $TEMP into the positional parameters #1, #2, ...
432+
unset TEMP
433+
434+
# Parse arguments
435+
while true; do
436+
# -- marks the end of the options, and anything after it is treated as a positional argument.
437+
# If "$*" = "--", there are no optional parameters left, and we can break the loop
438+
if [ "$*" = "--" ]; then
439+
shift
440+
break
441+
fi
442+
443+
case "$1" in
444+
"-r" | "--requirement")
445+
requirements_file="$2"
446+
shift 2
447+
;;
448+
"-s" | "--skip-lock")
449+
skip_lock="--skip-lock"
450+
shift
451+
;;
452+
"--pip-args")
453+
pip_args="$2"
454+
shift 2
455+
;;
456+
--)
457+
# -- marks the end of the options, and anything after it is treated as a positional argument.
458+
# For venv uninstall, positional arguments are package names
459+
shift
460+
package_names+=( "$@" )
461+
break
462+
;;
463+
*)
464+
if [ -z "$1" ]; then
465+
break
466+
fi
467+
;;
468+
esac
469+
done
470+
471+
# Fail if no package names were specified
472+
if [ "${#package_names[@]}" -eq 0 ]; then
473+
venv::raise "No packages specified, nothing to uninstall. If you want to uninstall everything, use 'venv clear'."
474+
return "${_fail}"
475+
fi
476+
477+
# Check the specified requirements file
478+
if [ -z "${requirements_file}" ]; then
479+
requirements_file="requirements.txt"
480+
venv::color_echo "${_yellow}" "No requirements file specified, using requirements.txt"
481+
fi
482+
if ! venv::_check_install_requirements_file "${requirements_file}"; then
483+
# Fail if file name doesn't match required format
484+
return "${_fail}"
485+
fi
486+
487+
# Make a temporary backup of the requirements file, in case something fails
488+
venv::_create_backup_file "${requirements_file}"
489+
490+
# Remove package names from requirements file
491+
if ! venv::_remove_packages_from_requirements "${requirements_file}" "${package_names[@]}"; then
492+
return "${_fail}"
493+
fi
494+
if [ "$?" -eq 2 ]; then
495+
# If none of the packages were found in the requirements file, return without reinstalling
496+
venv::_remove_backup_file "${requirements_file}"
497+
return "${_success}"
498+
fi
499+
500+
# Remove the backup file if everything went well
501+
venv::_remove_backup_file "${requirements_file}"
502+
503+
# Reinstall the environment from the requirements file. Pass through additional arguments
504+
venv::color_echo "${_green}" "Reinstalling requirements from ${requirements_file}"
505+
if ! venv::install -r "${requirements_file}" ${skip_lock} --pip-args="${pip_args}"; then
506+
return "${_fail}"
507+
fi
508+
}
509+
510+
venv::_remove_packages_from_requirements() {
511+
local requirements_file="$1"
512+
local package_names=("${@:2}")
513+
514+
local package_name
515+
local packages_removed=0
516+
for package_name in "${package_names[@]}"; do
517+
# Check if the package is in the requirements file
518+
if ! command grep -q "^${package_name}" "${requirements_file}"; then
519+
venv::color_echo "${_yellow}" "Package '${package_name}' not found in ${requirements_file}, skipping"
520+
continue
521+
fi
522+
# Remove the package from the requirements file
523+
sed -i "/^${package_name}/d" "${requirements_file}"
524+
packages_removed=$((packages_removed+1))
525+
echo "Removed '${package_name}' from ${requirements_file}"
526+
done
527+
528+
if [ "${packages_removed}" -eq 0 ]; then
529+
venv::color_echo "${_yellow}" "None of the specified packages found in ${requirements_file}, nothing to remove"
530+
return 2
531+
fi
532+
533+
# Sort requirements file after removing packages. LC_COLLATE is set to C to ensure lines beginning with '-'
534+
# are sorted first, instad of being ignored
535+
LC_COLLATE=C sort --ignore-case --stable -o "${requirements_file}" "${requirements_file}"
536+
}
537+
389538
venv::_create_backup_file() {
390539
local file="$1"
391540
if [ -f "${file}" ]; then
@@ -488,7 +637,8 @@ venv::help() {
488637
echo "create Create a new virtual environment in the current folder"
489638
echo "activate Activate the virtual environment in the current folder"
490639
echo "delete Delete the virtual environment in the current folder"
491-
echo "install Install requirements from a requirements file in the current environment"
640+
echo "install Install individual packages, or requirements from a requirements file, in the current environment"
641+
echo "uninstall Uninstall packages from the current environment and reinstall from a requirements file"
492642
echo "lock Lock installed requirements in a '.lock'-file"
493643
echo "clear Remove all installed packages in the current environment"
494644
echo "deactivate Deactivate the currently activated virtual environment"
@@ -511,13 +661,14 @@ venv::main() {
511661
venv::_version
512662
;;
513663

514-
create \
515-
| activate \
664+
activate \
665+
| clear \
666+
| create \
667+
| deactivate \
516668
| delete \
517669
| install \
518670
| lock \
519-
| clear \
520-
| deactivate \
671+
| uninstall \
521672
)
522673
shift
523674
venv::"${subcommand}" "$@"

0 commit comments

Comments
 (0)