diff --git a/src/python/devcontainer-feature.json b/src/python/devcontainer-feature.json index 7ebdad38a..247808ab0 100644 --- a/src/python/devcontainer-feature.json +++ b/src/python/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "python", - "version": "1.3.1", + "version": "1.4.1", "name": "Python", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/python", "description": "Installs the provided version of Python, as well as PIPX, and other common Python utilities. JupyterLab is conditionally installed with the python feature. Note: May require source code compilation.", @@ -22,6 +22,11 @@ "default": "os-provided", "description": "Select a Python version to install." }, + "additionalVersions": { + "type": "string", + "default": "", + "description": "Enter additional Python versions, separated by commas. Use 'X.Y' or 'X.Y.Z' for a specific version" + }, "installTools": { "type": "boolean", "default": true, @@ -47,6 +52,11 @@ "default": "", "description": "Configure JupyterLab to accept HTTP requests from the specified origin" }, + "packages": { + "type": "string", + "default": "", + "description": "Optional comma separated list of Python packages to install with pip" + }, "httpProxy": { "type": "string", "default": "", diff --git a/src/python/install.sh b/src/python/install.sh index e6eefd055..1366e8f37 100755 --- a/src/python/install.sh +++ b/src/python/install.sh @@ -26,6 +26,13 @@ CONFIGURE_JUPYTERLAB_ALLOW_ORIGIN="${CONFIGUREJUPYTERLABALLOWORIGIN:-""}" # alongside PYTHON_VERSION, but not set as default. ADDITIONAL_VERSIONS="${ADDITIONALVERSIONS:-""}" +# Comma-separated list of packages to be installed +# to Python version specified in PYTHON_VERSION +PYTHON_PACKAGES="${PACKAGES:-""}" + +# Import common utils +. ./utils.sh + DEFAULT_UTILS=("pylint" "flake8" "autopep8" "black" "yapf" "mypy" "pydocstyle" "pycodestyle" "bandit" "pipenv" "virtualenv" "pytest") PYTHON_SOURCE_GPG_KEYS="64E628F8D684696D B26995E310250568 2D347EA6AA65421D FB9921286F5E1540 3A5CA953F73C700D 04C367C218ADD4FF 0EDDC5F26A45C816 6AF053F07D9DC8D2 C9BE28DEE6DF025C 126EB563A74B06BF D9866941EA5BBD71 ED9D77D5 A821E680E5FA6305" GPG_KEY_SERVERS="keyserver hkp://keyserver.ubuntu.com @@ -300,26 +307,6 @@ install_using_oryx() { add_symlink } -sudo_if() { - COMMAND="$*" - if [ "$(id -u)" -eq 0 ] && [ "$USERNAME" != "root" ]; then - su - "$USERNAME" -c "$COMMAND" - else - $COMMAND - fi -} - -install_user_package() { - INSTALL_UNDER_ROOT="$1" - PACKAGE="$2" - - if [ "$INSTALL_UNDER_ROOT" = true ]; then - sudo_if "${PYTHON_SRC}" -m pip install --upgrade --no-cache-dir "$PACKAGE" - else - sudo_if "${PYTHON_SRC}" -m pip install --user --upgrade --no-cache-dir "$PACKAGE" - fi -} - add_user_jupyter_config() { CONFIG_DIR="$1" CONFIG_FILE="$2" @@ -473,8 +460,8 @@ if [ "${INSTALL_JUPYTERLAB}" = "true" ]; then INSTALL_UNDER_ROOT=false fi - install_user_package $INSTALL_UNDER_ROOT jupyterlab - install_user_package $INSTALL_UNDER_ROOT jupyterlab-git + install_python_package $INSTALL_UNDER_ROOT $PYTHON_SRC jupyterlab + install_python_package $INSTALL_UNDER_ROOT $PYTHON_SRC jupyterlab-git # Configure JupyterLab if needed if [ -n "${CONFIGURE_JUPYTERLAB_ALLOW_ORIGIN}" ]; then @@ -491,6 +478,29 @@ if [ "${INSTALL_JUPYTERLAB}" = "true" ]; then fi fi +# Install pacakages if needed +if [ ! -z "${PYTHON_PACKAGES}" ]; then + if [ -z "${PYTHON_SRC}" ]; then + echo "(!) Could not install packages. Python not found." + exit 1 + fi + + INSTALL_UNDER_ROOT=true + if [ "$(id -u)" -eq 0 ] && [ "$USERNAME" != "root" ]; then + INSTALL_UNDER_ROOT=false + fi + + OLDIFS=$IFS + IFS="," + read -a python_packages <<< "$PYTHON_PACKAGES" + for package in "${python_packages[@]}"; do + name=$(echo ${package} | awk -F == '{ print $1 }') + version=$(echo ${package} | awk -F == '{ print $2 }') + install_python_package $INSTALL_UNDER_ROOT $PYTHON_SRC $name $version + done + IFS=$OLDIFS +fi + # Clean up rm -rf /var/lib/apt/lists/* diff --git a/src/python/utils.sh b/src/python/utils.sh new file mode 100644 index 000000000..2ca00d0de --- /dev/null +++ b/src/python/utils.sh @@ -0,0 +1,31 @@ +sudo_if() { + COMMAND="$*" + if [ "$(id -u)" -eq 0 ] && [ "$USERNAME" != "root" ]; then + su - "$USERNAME" -c "$COMMAND" + else + $COMMAND + fi +} + +install_python_package() { + INSTALL_UNDER_ROOT="$1" + PYTHON_PATH="$2" + PACKAGE="$3" + VERSION="${4:-""}" + + sudo_if "$PYTHON_PATH" -m pip uninstall --yes "$PACKAGE" + + install_package="${PACKAGE}" + + if [ ! -z "${VERSION}" ]; then + install_package+="==${VERSION}" + fi + + if [ "$INSTALL_UNDER_ROOT" = true ]; then + sudo_if "$PYTHON_PATH" -m pip install --upgrade --no-cache-dir "$install_package" + else + sudo_if "$PYTHON_PATH" -m pip install --upgrade --user --no-cache-dir "$install_package" + fi + + sudo_if "$PYTHON_PATH" -m pip --no-python-version-warning show "$PACKAGE" +} diff --git a/test/python/install_packages_mixed.sh b/test/python/install_packages_mixed.sh new file mode 100644 index 000000000..594e68b99 --- /dev/null +++ b/test/python/install_packages_mixed.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +set -e + +# Optional: Import test library +source dev-container-features-test-lib + +# Always run these checks as the non-root user +user="$(whoami)" +check "user" grep vscode <<< "$user" + +# Check for an installation of JupyterLab +check "version" jupyter lab --version + +# Check location of JupyterLab installation +packages="$(python3 -m pip list)" +check "location" grep jupyter <<< "$packages" + +# Check for git extension +check "jupyterlab_git" grep jupyterlab_git <<< "$packages" + +# Report result +reportResults diff --git a/test/python/install_packages_non_root.sh b/test/python/install_packages_non_root.sh new file mode 100644 index 000000000..594e68b99 --- /dev/null +++ b/test/python/install_packages_non_root.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +set -e + +# Optional: Import test library +source dev-container-features-test-lib + +# Always run these checks as the non-root user +user="$(whoami)" +check "user" grep vscode <<< "$user" + +# Check for an installation of JupyterLab +check "version" jupyter lab --version + +# Check location of JupyterLab installation +packages="$(python3 -m pip list)" +check "location" grep jupyter <<< "$packages" + +# Check for git extension +check "jupyterlab_git" grep jupyterlab_git <<< "$packages" + +# Report result +reportResults diff --git a/test/python/install_packages_root.sh b/test/python/install_packages_root.sh new file mode 100644 index 000000000..7d55cb44d --- /dev/null +++ b/test/python/install_packages_root.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -e + +# Optional: Import test library +source dev-container-features-test-lib + +# Check for an installation of JupyterLab +check "version" jupyter lab --version + +# Check location of JupyterLab installation +packages="$(python3 -m pip list)" +check "location" grep jupyter <<< "$packages" + +# Check for git extension +check "jupyterlab_git" grep jupyterlab_git <<< "$packages" + +# Report result +reportResults diff --git a/test/python/install_packages_versions_locked.sh b/test/python/install_packages_versions_locked.sh new file mode 100644 index 000000000..594e68b99 --- /dev/null +++ b/test/python/install_packages_versions_locked.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +set -e + +# Optional: Import test library +source dev-container-features-test-lib + +# Always run these checks as the non-root user +user="$(whoami)" +check "user" grep vscode <<< "$user" + +# Check for an installation of JupyterLab +check "version" jupyter lab --version + +# Check location of JupyterLab installation +packages="$(python3 -m pip list)" +check "location" grep jupyter <<< "$packages" + +# Check for git extension +check "jupyterlab_git" grep jupyterlab_git <<< "$packages" + +# Report result +reportResults diff --git a/test/python/scenarios.json b/test/python/scenarios.json index 23b6cfc92..1e53ae7d4 100644 --- a/test/python/scenarios.json +++ b/test/python/scenarios.json @@ -81,5 +81,44 @@ "version": "3.12" } } + }, + "install_packages_root": { + "image": "debian:bullseye-slim", + "features": { + "python": { + "version": "3.11", + "packages": "jupyterlab,jupyterlab-git" + } + } + }, + "install_packages_non_root": { + "image": "mcr.microsoft.com/devcontainers/base:focal", + "remoteUser": "vscode", + "features": { + "python": { + "version": "3.11", + "packages": "jupyterlab,jupyterlab-git" + } + } + }, + "install_packages_versions_locked": { + "image": "mcr.microsoft.com/devcontainers/base:focal", + "remoteUser": "vscode", + "features": { + "python": { + "version": "3.11", + "packages": "cryptography==41.0.5,jupyterlab==4.0.8,jupyterlab-git==0.43.0" + } + } + }, + "install_packages_mixed": { + "image": "mcr.microsoft.com/devcontainers/base:focal", + "remoteUser": "vscode", + "features": { + "python": { + "version": "3.11", + "packages": "cryptography==41.0.5,jupyterlab,jupyterlab-git==0.43.0" + } + } } }