Skip to content
12 changes: 11 additions & 1 deletion src/python/devcontainer-feature.json
Original file line number Diff line number Diff line change
@@ -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.",
Expand All @@ -22,6 +22,11 @@
"default": "os-provided",
"description": "Select a Python version to install."
Comment thread
alexander-smolyakov marked this conversation as resolved.
},
"additionalVersions": {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We intentionally did not publicize this option just because it was experimental. Also, it was meant only to be used by universal image.

However, if it works great in different test scenarios then it is worth publicizing 🎉

"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,
Expand All @@ -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": "",
Expand Down
54 changes: 32 additions & 22 deletions src/python/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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/*

Expand Down
31 changes: 31 additions & 0 deletions src/python/utils.sh
Original file line number Diff line number Diff line change
@@ -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"
}
23 changes: 23 additions & 0 deletions test/python/install_packages_mixed.sh
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions test/python/install_packages_non_root.sh
Original file line number Diff line number Diff line change
@@ -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
19 changes: 19 additions & 0 deletions test/python/install_packages_root.sh
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions test/python/install_packages_versions_locked.sh
Original file line number Diff line number Diff line change
@@ -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
39 changes: 39 additions & 0 deletions test/python/scenarios.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
}