Venv
venv 모듈은 자체 사이트 디렉터리를 갖는 경량 《가상 환경》을 만들고, 선택적으로 시스템 사이트 디렉터리에서 격리할 수 있도록 지원합니다. 각 가상 환경은 고유한 파이썬 바이너리(이 환경을 만드는 데 사용된 바이너리 버전과 일치함)를 가지며 자신의 사이트 디렉터리에 독립적으로 설치된 파이썬 패키지 집합을 가질 수 있습니다.
파이썬 가상 환경에 대한 자세한 내용은 PEP 405를 참조하십시오.
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
참고
참고 가상 환경은 파이썬 인터프리터, 라이브러리 및 스크립트가 다른 가상 환경에 설치된 것과 (기본적으로) 《시스템》 파이썬(즉, 여러분의 운영 체제 일부로 설치되어있는 것)에 설치된 모든 라이브러리와 격리되어있는 파이썬 환경입니다. 가상 환경은 파이썬 실행 파일과 가상 환경임을 나타내는 다른 파일을 포함하는 디렉터리 트리입니다.
setuptools나 pip와 같은 일반적인 설치 도구는 가상 환경에서 예상대로 작동합니다. 즉, 가상 환경이 활성화되면, 명시적으로 그렇게 지정하지 않아도 파이썬 패키지를 가상 환경에 설치합니다.
가상 환경이 활성일 때 (즉, 가상 환경의 파이썬 인터프리터가 실행 중일 때), 어트리뷰트 sys.prefix 와 sys.exec_prefix는 가상 환경의 베이스 디렉터리를 가리키지만, sys.base_prefix와 sys.base_exec_prefix는 가상 환경을 만들 때 사용한 가상이 아닌 환경의 파이썬을 가리킵니다. 가상 환경이 활성화되어 있지 않으면, sys.prefix는 sys.base_prefix와 같고, sys.exec_prefix는 sys.base_exec_prefix와 같습니다 (모두 가상 환경이 아닌 파이썬 설치를 가리킵니다).
가상 환경이 활성일 때, 설치 경로를 변경하는 모든 옵션이 모든 distutils 구성 파일에서 무시되어, 실수로 가상 환경 외부에 프로젝트가 설치되는 것을 방지합니다.
명령 셸에서 작업할 때, 사용자는 가상 환경의 실행 파일 디렉터리(정확한 파일 이름과 파일을 사용하는 명령은 셸 종속적입니다)에서 activate 스크립트를 실행하여 가상 환경을 활성화할 수 있습니다. 이 파일은 가상 환경의 실행 파일 디렉터리를 실행 중인 셸의 PATH 환경 변수 앞에 추가합니다. 다른 상황에서는 가상 환경을 활성화할 필요가 없습니다; 가상 환경에 설치된 스크립트에는 가상 환경의 파이썬 인터프리터를 가리키는 《셔뱅》 줄이 있습니다. 이는 스크립트가 PATH 값과 상관없이 해당 인터프리터로 실행됨을 뜻합니다. 윈도우에서는, Python Launcher for Windows를 설치하면 《셔뱅》 줄 처리가 지원됩니다 (이것은 파이썬 3.3에 추가되었습니다 - 자세한 내용은 PEP 397를 참조하십시오). 그래서, 윈도우 탐색기 창에서 설치된 스크립트를 더블 클릭하면 PATH에 가상 환경에 대한 참조가 없어도 올바른 인터프리터로 스크립트를 실행하게 됩니다.
작동 원리
- Stackoverflow - How does virtualenv work?
- sys — System-specific parameters and functions — Python 3.8.2 documentation
- site — 사이트별 구성 훅 — Python 3.8.2 문서
- PEP 370 -- Per user site-packages directory
- [추천] venv는 내부적으로 어떻게 작동할까?
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
Python#Bootstrap script 항목 참조.
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
중간에 포함되는 AsyncPythonSubprocess 는 해당 페이지 참조.
# -*- 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 버전 관리.
- Virtualenv: Python 패키지 의존성 관리.
- autoenv: 자동 환경(Environments) 설정.
- venv: Python venv
- Daytona - 오픈소스 개발환경 관리자