Venv
lastmod 2024-09-25created 2020-04-0918 edits
The venv module supports creating lightweight "virtual environments" with their own site directories, optionally isolated from system site directories. Each virtual environment has its own Python binary (which matches the version of the binary that was used to create this environment) and can have its own independent set of Python packages installed in its site directories.
See PEP 405 for more information about Python virtual environments.
Help message
usage: venv [-h] [--system-site-packages] [--symlinks | --copies] [--clear]
[--upgrade] [--without-pip] [--prompt PROMPT] [--upgrade-deps]
ENV_DIR [ENV_DIR ...]
Creates virtual Python environments in one or more target directories.
positional arguments:
ENV_DIR A directory to create the environment in.
optional arguments:
-h, --help show this help message and exit
--system-site-packages
Give the virtual environment access to the system
site-packages dir.
--symlinks Try to use symlinks rather than copies, when symlinks
are not the default for the platform.
--copies Try to use copies rather than symlinks, even when
symlinks are the default for the platform.
--clear Delete the contents of the environment directory if it
already exists, before environment creation.
--upgrade Upgrade the environment directory to use this version
of Python, assuming Python has been upgraded in-place.
--without-pip Skips installing or upgrading pip in the virtual
environment (pip is bootstrapped by default)
--prompt PROMPT Provides an alternative prompt prefix for this
environment.
--upgrade-deps Upgrade core dependencies: pip setuptools to the
latest version in PyPI
Once an environment has been created, you may wish to activate it, e.g. by
sourcing an activate script in its bin directory.
Example
Note
Note: A virtual environment is a Python environment such that the Python interpreter, libraries and scripts installed into it are isolated from those installed in other virtual environments, and (by default) from any libraries installed in a "system" Python, i.e., one which is installed as part of your operating system. A virtual environment is a directory tree which contains Python executable files and other files which indicate that it is a virtual environment.
Common installation tools such as setuptools and pip work as expected with virtual environments. In other words, when a virtual environment is active, they install Python packages into the virtual environment without needing to be told to do so explicitly.
When a virtual environment is active (i.e., the virtual environment's Python interpreter is running), the attributes sys.prefix and sys.exec_prefix point to the base directory of the virtual environment, whereas sys.base_prefix and sys.base_exec_prefix point to the non-virtual-environment Python used to create the virtual environment. If a virtual environment is not active, then sys.prefix is the same as sys.base_prefix and sys.exec_prefix is the same as sys.base_exec_prefix (they all point to a non-virtual-environment Python installation).
When a virtual environment is active, any options that change the installation path will be ignored from all distutils configuration files to prevent projects being inadvertently installed outside of the virtual environment.
When working in a command shell, users can make a virtual environment active by running an activate script in the virtual environment's executables directory (the precise filename and command to use the file is shell-dependent), which prepends the virtual environment's directory for executables to the PATH environment variable for the running shell. There should be no need in other circumstances to activate a virtual environment; scripts installed into virtual environments have a "shebang" line which points to the virtual environment's Python interpreter. This means that the script will run with that interpreter regardless of the value of PATH. On Windows, "shebang" line processing is supported if you have the Python Launcher for Windows installed (this was added to Python in 3.3 - see PEP 397 for more details). Thus, double-clicking an installed script in a Windows Explorer window should run the script with the correct interpreter without there needing to be any reference to its virtual environment in PATH.
How it works
- Stackoverflow - How does virtualenv work?
- sys — System-specific parameters and functions — Python 3.8.2 documentation
- site — Site-specific configuration hook — Python 3.8.2 documentation
- PEP 370 -- Per user site-packages directory
- [Recommended] How does venv work internally?
I will describe the basic process, which I learned from the presentation @jcollado linked to.
When Python starts, it looks at the path of the binary, and the prefixes thereof.
So let's say your virtualenv is /home/blah/scratch. The Python process knows it was executed from /home/blah/scratch/bin/python (which is usually just a copy of your system python binary /usr/bin/python) and it knows its own version X.Y because it's compiled into it. Then Python looks for lib/pythonX.Y/os.py in this order:
/home/blah/scratch/bin/lib/pythonX.Y/os.py
/home/blah/scratch/lib/pythonX.Y/os.py <-- this file should exist
/home/blah/lib/pythonX.Y/os.py
/home/lib/pythonX.Y/os.py
/lib/pythonX.Y/os.py
It stops at /home/blah/scratch/lib/pythonX.Y/os.py because it's the first file that actually exists. If it didn't, Python would keep looking. It then sets sys.prefix based on this. It uses a similar process to set sys.exec_prefix, and then sys.path is constructed based on these.
Bootstrap script
See Python#Bootstrap script for details.
pyvenv.cfg pserser
# -*- coding: utf-8 -*-
import os
from typing import Tuple
from configparser import ConfigParser
PYVENV_CFG_FILENAME = "pyvenv.cfg"
CFG_KEY_HOME = "home"
CFG_KEY_INCLUDE_SYSTEM = "include-system-site-packages"
CFG_KEY_VERSION = "version"
_FAKE_SECTION_NAME = "default"
_FAKE_DEFAULT_SECTION = f"[{_FAKE_SECTION_NAME}]"
class PyvenvCfg:
home: str
include_system_site_packages: bool
version: str
def to_version_tuple(self) -> Tuple[int, ...]:
return tuple([int(d) for d in self.version.split("-")[0].split(".")])
def exists_pyvenv_cfg(env_root: str) -> bool:
return os.path.exists(os.path.join(env_root, PYVENV_CFG_FILENAME))
def read_pyvenv_cfg(env_root: str) -> PyvenvCfg:
if not env_root:
raise ValueError("Empty argument")
cfg_path = os.path.join(env_root, PYVENV_CFG_FILENAME)
if not os.path.isfile(cfg_path):
raise FileNotFoundError(f"Not found '{cfg_path}' file")
if not os.access(cfg_path, os.R_OK):
raise PermissionError(f"Not readable '{cfg_path}' file")
with open(cfg_path) as f:
content = _FAKE_DEFAULT_SECTION + "\n" + f.read()
parser = ConfigParser()
parser.read_string(content, cfg_path)
home = parser.get(_FAKE_SECTION_NAME, CFG_KEY_HOME)
include_system = parser.getboolean(_FAKE_SECTION_NAME, CFG_KEY_INCLUDE_SYSTEM)
version = parser.get(_FAKE_SECTION_NAME, CFG_KEY_VERSION)
result = PyvenvCfg()
result.home = home
result.include_system_site_packages = include_system
result.version = version
return result
def read_site_packages_dir(env_root: str) -> str:
cfg = read_pyvenv_cfg(env_root)
versions = cfg.to_version_tuple()
if len(versions) < 2:
raise RuntimeError(f"Wrong python version: {versions}")
major = versions[0]
minor = versions[1]
return os.path.join(env_root, "lib", f"python{major}.{minor}", "site-packages")
TestCase
# -*- coding: utf-8 -*-
import os
import tempfile
from unittest import TestCase, main
from recc.venv.pyvenv_cfg import read_pyvenv_cfg, read_site_packages_dir
_SAMPLE_PYENV_CFG = """home = /unknown/venv/root
include-system-site-packages = true
version = 3.7.3
"""
class PyvenvCfgTestCase(TestCase):
def setUp(self):
self.temp_dir = tempfile.TemporaryDirectory()
self.pyenv_cfg_path = os.path.join(self.temp_dir.name, "pyvenv.cfg")
with open(self.pyenv_cfg_path, "w") as f:
f.write(_SAMPLE_PYENV_CFG)
self.assertTrue(os.path.isfile(self.pyenv_cfg_path))
def tearDown(self):
self.temp_dir.cleanup()
def test_read_pyvenv_cfg(self):
cfg = read_pyvenv_cfg(self.temp_dir.name)
self.assertEqual("/unknown/venv/root", cfg.home)
self.assertTrue(cfg.include_system_site_packages)
self.assertEqual("3.7.3", cfg.version)
def test_read_site_packages_dir(self):
path = read_site_packages_dir(self.temp_dir.name)
self.assertTrue(path.endswith("lib/python3.7/site-packages"))
if __name__ == "__main__":
main()
AsyncVirtualEnvironment class
For the AsyncPythonSubprocess used along the way, see its page.
# -*- coding: utf-8 -*-
import sys
import os
from types import SimpleNamespace
from typing import Optional
from venv import EnvBuilder
from shutil import rmtree
from recc.subprocess.async_python_subprocess import AsyncPythonSubprocess
from recc.venv.venv_context_changer import VenvContextChanger
def _get_site_packages_dir(env_dir) -> str:
if sys.platform == "win32":
return os.path.join(env_dir, "Lib", "site-packages")
else:
return os.path.join(
env_dir, "lib", "python%d.%d" % sys.version_info[:2], "site-packages"
)
class AsyncVirtualEnvironment:
def __init__(
self,
root_directory: str,
system_site_packages=False,
pip_timeout: Optional[float] = None,
*,
isolate_ensure_pip=True,
):
self._root_directory = root_directory
self._pip_timeout = pip_timeout if pip_timeout else 0.0
self._isolate_ensure_pip = isolate_ensure_pip
self._venv = EnvBuilder(
system_site_packages=system_site_packages,
clear=False,
symlinks=False,
upgrade=False,
with_pip=True,
prompt=None,
)
self._context = self._venv.ensure_directories(os.path.abspath(root_directory))
if not hasattr(self._context, "pip_exe"):
self._context.pip_exe = os.path.join(
self._context.bin_path, "pip%d.%d" % sys.version_info[:2]
)
if not hasattr(self._context, "site_packages_dir"):
self._context.site_packages_dir = _get_site_packages_dir(root_directory)
@property
def root(self) -> str:
"""
The original venv root directory.
Preserves the original value passed to the constructor argument.
"""
return self._root_directory
@property
def context(self) -> SimpleNamespace:
return self._context
@property
def bin_name(self) -> str:
return self.context.bin_name
@property
def bin_path(self) -> str:
return self.context.bin_path
@property
def env_dir(self) -> str:
"""
Absolute path to the venv root directory.
"""
return self.context.env_dir
@property
def env_exe(self) -> str:
return self.context.env_exe
@property
def env_name(self) -> str:
return self.context.env_name
@property
def executable(self) -> str:
return self.context.executable
@property
def inc_path(self) -> str:
return self.context.inc_path
@property
def prompt(self) -> str:
return self.context.prompt
@property
def python_dir(self) -> str:
return self.context.python_dir
@property
def python_exe(self) -> str:
return self.context.python_exe
@property
def pip_exe(self) -> str:
return self._context.pip_exe
@property
def site_packages_dir(self) -> str:
return self._context.site_packages_dir
@property
def exists(self) -> bool:
return os.path.exists(self.env_exe)
def _create(self) -> None:
self._venv.create(self._root_directory)
def _create_if_not_exists(self) -> None:
if not self.exists:
self._venv.create(self._root_directory)
def _clear(self) -> None:
self._venv.clear_directory(self._root_directory)
def _create_configuration(self) -> None:
self._venv.create_configuration(self._context)
def _setup_python(self) -> None:
self._venv.setup_python(self._context)
def _setup_scripts(self) -> None:
self._venv.setup_scripts(self._context)
def _upgrade_dependencies(self) -> None:
func = getattr(self._venv, "upgrade_dependencies")
if func is None:
raise NotImplementedError
assert sys.version_info >= (3, 9)
func(self._context)
def _post_setup(self) -> None:
self._venv.post_setup(self._context)
def _install_scripts(self, path: str) -> None:
self._venv.install_scripts(self._context, path)
async def _setup_pip(self) -> None:
await AsyncPythonSubprocess(self.env_exe).ensure_pip(
isolate=self._isolate_ensure_pip
)
async def create(self) -> None:
# See issue 24875. We need system_site_packages to be False
# until after pip is installed.
true_system_site_packages = self._venv.system_site_packages
self._venv.system_site_packages = False
self._create_configuration()
self._setup_python()
if self._venv.with_pip:
await self._setup_pip()
if not self._venv.upgrade:
self._setup_scripts()
self._post_setup()
if true_system_site_packages:
# We had set it to False before, now
# restore it and rewrite the configuration
self._venv.system_site_packages = True
self._create_configuration()
async def create_if_not_exists(self, remove_if_raised=True) -> None:
if not self.exists:
try:
await self.create()
except BaseException as e:
if remove_if_raised:
rmtree(self.env_dir, ignore_errors=True)
raise RuntimeError(
f"Failed to create virtual environment: '{self.env_dir}'"
) from e
def create_python_subprocess(self) -> AsyncPythonSubprocess:
return AsyncPythonSubprocess(self.env_exe, self._pip_timeout)
def create_context_changer(self):
return VenvContextChanger(
env_dir=self.env_dir,
env_exe=self.env_exe,
bin_path=self.bin_path,
site_packages_dir=self.site_packages_dir,
)
ContextChanger class
# -*- coding: utf-8 -*-
import os
import sys
import site
from abc import abstractmethod
from typing import List, Optional
from overrides import overrides
real_prefix = "real_prefix"
X = sys.version_info[0]
Y = sys.version_info[1]
XY = f"{X}{Y}"
X_Y = f"{X}.{Y}"
LIB_PYTHON_ZIP = f"{sys.base_prefix}/{sys.platlibdir}/python{XY}.zip"
LIB_PYTHON = f"{sys.base_prefix}/{sys.platlibdir}/python{X_Y}"
LIB_PYTHON_LIB_DYNLOAD = f"{sys.base_prefix}/{sys.platlibdir}/python{X_Y}/lib-dynload"
PATH = "PATH"
VIRTUAL_ENV = "VIRTUAL_ENV"
class ContextChanger:
@abstractmethod
def open(self) -> None:
raise NotImplementedError
@abstractmethod
def close(self) -> None:
raise NotImplementedError
def __enter__(self):
self.open()
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
async def __aenter__(self):
raise RuntimeError(
"Accessing the sys package while the event loop is running causes problems"
)
async def __aexit__(self, exc_type, exc_val, exc_tb):
raise RuntimeError(
"Accessing the sys package while the event loop is running causes problems"
)
class FakeContextChanger(ContextChanger):
@overrides
def open(self) -> None:
pass
@overrides
def close(self) -> None:
pass
class VenvContextChanger(ContextChanger):
"""
References:
- https://docs.python.org/3/library/sys.html
- https://docs.python.org/3/library/venv.html
- https://peps.python.org/pep-0405/
"""
_sys_prefix: str
_sys_exec_prefix: str
_sys_executable: str
_sys_path: List[str]
_sys_real_prefix: Optional[str]
_env_path: Optional[str]
_env_virtual_env: Optional[str]
def __init__(
self,
env_dir: str,
env_exe: str,
bin_path: str,
site_packages_dir: str,
):
self.env_dir = env_dir
self.env_exe = env_exe
self.bin_path = bin_path
self.site_packages_dir = site_packages_dir
@overrides
def open(self) -> None:
self._sys_prefix = sys.prefix
self._sys_exec_prefix = sys.exec_prefix
self._sys_executable = sys.executable
self._sys_path = sys.path
sys.prefix = self.env_dir
sys.exec_prefix = self.env_dir
sys.executable = self.env_exe
sys.path = [
LIB_PYTHON_ZIP,
LIB_PYTHON,
LIB_PYTHON_LIB_DYNLOAD,
]
site.addsitedir(self.site_packages_dir)
self._sys_real_prefix = getattr(sys, real_prefix, None)
setattr(sys, real_prefix, self.env_dir)
self._env_path = os.environ.get(PATH, None)
self._env_virtual_env = os.environ.get(VIRTUAL_ENV, None)
os.environ[PATH] = os.pathsep.join(
[self.bin_path] + os.environ.get(PATH, "").split(os.pathsep)
)
os.environ[VIRTUAL_ENV] = self.env_dir
@staticmethod
def _restore_env(key: str, original: Optional[str]):
if original is not None:
os.environ[key] = original
else:
os.environ.pop(key)
@overrides
def close(self) -> None:
sys.prefix = self._sys_prefix
sys.exec_prefix = self._sys_exec_prefix
sys.executable = self._sys_executable
sys.path = self._sys_path
assert hasattr(sys, real_prefix)
if self._sys_real_prefix is not None:
setattr(sys, real_prefix, self._sys_real_prefix)
else:
delattr(sys, real_prefix)
self._restore_env(PATH, self._env_path)
self._restore_env(VIRTUAL_ENV, self._env_virtual_env)
See also
- Python
- pyenv: Python version management.
- Virtualenv: Python package dependency management.
- autoenv: Automatic environment (Environments) setup.
- venv: Python venv
- Daytona - Open-source development environment manager
Favorite site
- venv — Creation of virtual environments — Python 3.8.2 documentation
- 12. Virtual Environments and Packages — Python 3.8.2 documentation