Skip to content

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

python3 -m venv /path/to/new/virtual/environment

참고

참고 가상 환경은 파이썬 인터프리터, 라이브러리 및 스크립트가 다른 가상 환경에 설치된 것과 (기본적으로) 《시스템》 파이썬(즉, 여러분의 운영 체제 일부로 설치되어있는 것)에 설치된 모든 라이브러리와 격리되어있는 파이썬 환경입니다. 가상 환경은 파이썬 실행 파일과 가상 환경임을 나타내는 다른 파일을 포함하는 디렉터리 트리입니다.

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에 가상 환경에 대한 참조가 없어도 올바른 인터프리터로 스크립트를 실행하게 됩니다.

작동 원리

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

Favorite site