ЗАМЕР
bench/venv/basics.py
Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.
- Цитируется в статье
- /ru/interview/python/venv-dependencies
- Как запустить
python3.11 bench/venv/basics.py python3.12 bench/venv/basics.py python3.13 bench/venv/basics.py python3.14 bench/venv/basics.py python3.11 bench/venv/practice.py python3.12 bench/venv/practice.py python3.13 bench/venv/practice.py python3.14 bench/venv/practice.py
Запись прогона
Замеры для урока «Окружения и зависимости»
| Скрипт | Что делает |
|---|---|
basics.py |
создаёт окружение во временной папке и смотрит на него: что в папке и в pyvenv.cfg, sys.prefix без активации, пакеты в свежем окружении, изоляция поставленного пакета и pip freeze, что меняет activate, что ломается в копии окружения, что пишет pip freeze для пакетов из PyPI, из локальной папки и editable, и что видит env/bin/python -S |
practice.py |
источник ответов двух практических задач урока |
python3.11 bench/venv/basics.py
python3.12 bench/venv/basics.py
python3.13 bench/venv/basics.py
python3.14 bench/venv/basics.py
python3.11 bench/venv/practice.py
python3.12 bench/venv/practice.py
python3.13 bench/venv/practice.py
python3.14 bench/venv/practice.py
Запускать самим интерпретатором, а не символической ссылкой на него: venv
определяет базовую установку по пути интерпретатора. Раздел 4 basics.py
ставит tomli-w==1.0.0 из PyPI, раздел 7 скачивает setuptools для сборки
двух локальных пакетов — нужна сеть.
Записи в runs/: без суффикса — CPython 3.13.7, остальные — с суффиксом
версии (3.11.15, 3.12.3, 3.14.7). Путь временной папки в выводе заменён на
<папка>, путь базовой установки — на <python>.
Что получилось
Папка окружения: bin/, include/, lib/, lib64 (символическая
ссылка на lib), pyvenv.cfg; с 3.13 — ещё .gitignore (его создаёт
командная строка python -m venv). bin/python — символическая ссылка на
базовый интерпретатор; абсолютный путь окружения записан в bin/activate и в
первой строке bin/pip (#!<папка>/env/bin/python3.13). На 3.14 в bin/
есть ещё имя 𝜋thon.
Без активации: sys.prefix — папка окружения, sys.base_prefix —
базовая установка, пакеты ставятся в site-packages окружения.
Свежее окружение: 3.11 — pip==24.0 и setuptools==79.0.1; 3.12 —
pip==24.0; 3.13 — pip==25.2; 3.14 — pip==26.2.1. Наполняет его
ensurepip; набор пакетов задан в Lib/ensurepip/__init__.py каждой версии.
Изоляция: tomli_w импортируется интерпретатором окружения и не
импортируется базовым (ModuleNotFoundError). У базового sys.prefix —
<python>, пакеты — в <python>/lib/python3.13/site-packages (у сборок 3.11
и 3.12 из Ubuntu — <python>/local/lib/python3.1X/dist-packages). В
метаданных пакета Name: tomli_w; pip freeze — tomli_w==1.0.0, без
самого pip (на 3.11 — и без setuptools).
activate: ставит env/bin первым в PATH, задаёт VIRTUAL_ENV и
VIRTUAL_ENV_PROMPT (env на 3.13 и 3.14, (env) на 3.11 и 3.12);
deactivate возвращает PATH прежним. Что ещё делает шаблон activate —
приглашение PS1, снятие PYTHONHOME — прогон не печатает.
Копия в другую папку: moved/bin/python ставит sys.prefix на
<папка>/moved, а moved/bin/pip по шебангу работает со старым окружением
(<папка>/env/lib/…/pip); moved/bin/python -m pip — с копией. Когда старой
папки нет, moved/bin/pip не запускается: FileNotFoundError. Одинаково на
3.11–3.14.
pip freeze (раздел 7): снимок в формате requirements — tomli_w==1.0.0
для пакета из PyPI, localpkg @ file://<папка>/localpkg для пакета из
локальной папки, # Editable install with no version control (editpkg==0.1)
и -e <папка>/editpkg для editable. --all добавляет то, что freeze
пропускает: на 3.11 — pip==24.0 и setuptools==79.0.1, на 3.12–3.14 —
только pip. Правило пропуска — pip/_internal/commands/freeze.py (pip 24.0:
строки 12–20, по sys.version_info < (3, 12) к pip добавляются
setuptools, distribute, wheel).
env/bin/python -S (раздел 8): на 3.11–3.13 sys.prefix — <python>,
prefix != base_prefix — False; на 3.14 — <папка>/env и True. С 3.14
префиксы окружения выставляет инициализация путей, а не site (документация
site 3.14, «Changed in version 3.14»). site-packages окружения в
sys.path под -S нет ни на одной версии.
Скрипт
192 строк"""Виртуальные окружения и зависимости: что делает venv и что — pip.
Скрипт создаёт окружение во временной папке тем интерпретатором, которым
запущен, и смотрит на него снаружи и изнутри. Путь временной папки в
выводе заменён на `<папка>`, путь к базовому интерпретатору — на `<python>`.
Шаг 4 ставит пакет из PyPI — нужен доступ к сети. Пакет выбран такой,
которого нет в базовых интерпретаторах машины записи: tomli-w 1.0.0.
ВОСЕМЬ СЮЖЕТОВ:
1. ЧТО ТАКОЕ ОКРУЖЕНИЕ. Папка со своим bin/python и файлом pyvenv.cfg —
и больше почти ничего.
2. КАК ИНТЕРПРЕТАТОР ПОНИМАЕТ, ЧТО ОН В ОКРУЖЕНИИ. sys.prefix указывает на
окружение, sys.base_prefix — на базовую установку. Активировать для
этого ничего не нужно.
3. ЧТО ЛЕЖИТ В СВЕЖЕМ ОКРУЖЕНИИ. pip list сразу после создания.
4. ИЗОЛЯЦИЯ. Пакет, поставленный в окружение, виден его интерпретатору и не
виден базовому; pip freeze печатает его с точной версией.
5. АКТИВАЦИЯ. Скрипт activate ставит bin/ окружения первым в PATH и задаёт
VIRTUAL_ENV и VIRTUAL_ENV_PROMPT; deactivate всё возвращает.
6. КОПИЯ. Окружение, скопированное в другую папку: его python видит новую
папку, а bin/pip по шебангу запускает интерпретатор старого окружения —
и без старой папки не запускается вовсе.
7. ЧТО ПИШЕТ PIP FREEZE. Снимок окружения в формате requirements: пакет из
PyPI — строкой имя==версия, пакет из локальной папки — строкой с прямой
ссылкой (direct URL), editable-установка — строкой -e с путём. Сам pip
freeze пропускает (на 3.11 — ещё setuptools), freeze --all — нет. Шаг
ставит два крошечных пакета из временной папки; для их сборки pip
скачивает setuptools из PyPI — нужна сеть.
8. ПРЕФИКСЫ БЕЗ МОДУЛЯ SITE. env/bin/python -S: до 3.13 включительно
sys.prefix окружения ставил модуль site, и -S это отменял; с 3.14 его
ставит инициализация путей, и -S на него не влияет. site-packages
окружения в sys.path без site не попадает ни на одной версии.
Запускать: python3.11 / 3.12 / 3.13 / 3.14 (не символической ссылкой на
интерпретатор, а им самим — иначе venv найдёт не ту базовую установку).
"""
import os
import subprocess
import sys
import tempfile
print("PY", sys.version.split()[0])
print()
tmp = os.path.realpath(tempfile.mkdtemp())
env_dir = os.path.join(tmp, "env")
env_py = os.path.join(env_dir, "bin", "python")
base_prefix = sys.base_prefix
def clean(text):
return text.replace(tmp, "<папка>").replace(base_prefix, "<python>").rstrip()
def run(cmd, **kw):
r = subprocess.run(cmd, capture_output=True, text=True, **kw)
return clean(r.stdout + r.stderr)
subprocess.run([sys.executable, "-m", "venv", env_dir], check=True)
# ── 1 ─────────────────────────────────────────────────────────────────────
print("1) что внутри папки окружения (верхний уровень и bin/)")
print(" ", sorted(os.listdir(env_dir)))
print(" bin/:", sorted(n for n in os.listdir(os.path.join(env_dir, "bin")) if not n.startswith("activate")),
"+ скрипты activate*")
py_link = os.path.join(env_dir, "bin", "python")
print(" bin/python — символическая ссылка:", os.path.islink(py_link), "->", clean(os.path.realpath(py_link)))
with open(os.path.join(env_dir, "bin", "activate"), encoding="utf-8") as f:
print(" абсолютный путь окружения записан в bin/activate:", env_dir in f.read())
with open(os.path.join(env_dir, "bin", "pip"), encoding="utf-8") as f:
print(" первая строка bin/pip:", clean(f.readline().strip()))
print(" pyvenv.cfg:")
with open(os.path.join(env_dir, "pyvenv.cfg"), encoding="utf-8") as f:
for line in f:
key = line.split("=")[0].strip()
if key in ("home", "include-system-site-packages", "version"):
print(" ", clean(line.strip()))
print()
# ── 2 ─────────────────────────────────────────────────────────────────────
print("2) интерпретатор окружения, без активации")
probe = "import sys; print(sys.prefix); print(sys.base_prefix); print(sys.prefix != sys.base_prefix)"
lines = run([env_py, "-c", probe]).splitlines()
print(" sys.prefix ->", lines[0])
print(" sys.base_prefix ->", lines[1])
print(" prefix != base_prefix ->", lines[2])
site = run([env_py, "-c", "import sysconfig; print(sysconfig.get_paths()['purelib'])"])
print(" куда ставятся пакеты ->", site)
print()
# ── 3 ─────────────────────────────────────────────────────────────────────
print("3) pip list в свежем окружении")
for line in run([env_py, "-m", "pip", "list", "--format=freeze", "--disable-pip-version-check"]).splitlines():
print(" ", line)
print()
# ── 4 ─────────────────────────────────────────────────────────────────────
print("4) изоляция: pip install tomli-w==1.0.0 в окружение")
subprocess.run([env_py, "-m", "pip", "install", "-q", "--disable-pip-version-check", "tomli-w==1.0.0"],
check=True, capture_output=True)
check = "import tomli_w; print('импортирован, версия', tomli_w.__version__)"
print(" интерпретатор окружения ->", run([env_py, "-c", check]).splitlines()[-1])
print(" базовый интерпретатор ->", run([sys.executable, "-c", check]).splitlines()[-1])
base_paths = run([sys.executable, "-c", "import sys, sysconfig; print(sys.prefix); print(sysconfig.get_paths()['purelib'])"]).splitlines()
print(" у базового sys.prefix ->", base_paths[0])
print(" у базового пакеты в ->", base_paths[1])
meta = run([env_py, "-c", "from importlib.metadata import metadata; print(metadata('tomli-w')['Name'])"])
print(" Name в метаданных пакета ->", meta)
freeze = run([env_py, "-m", "pip", "freeze", "--disable-pip-version-check"]).splitlines()
print(" pip freeze ->", freeze)
print()
# ── 5 ─────────────────────────────────────────────────────────────────────
print("5) активация в bash: что она меняет")
script = f'''
before_path="$PATH"
source "{env_dir}/bin/activate"
echo "VIRTUAL_ENV=$VIRTUAL_ENV"
echo "VIRTUAL_ENV_PROMPT=$VIRTUAL_ENV_PROMPT"
echo "первый элемент PATH: ${{PATH%%:*}}"
echo "python в PATH: $(command -v python)"
deactivate
echo "после deactivate PATH прежний: $([ "$PATH" = "$before_path" ] && echo да || echo нет)"
'''
for line in run(["bash", "-c", script]).splitlines():
print(" ", line)
print()
# ── 6 ─────────────────────────────────────────────────────────────────────
print("6) копия окружения в другую папку той же машины")
import shutil
moved = os.path.join(tmp, "moved")
shutil.copytree(env_dir, moved, symlinks=True)
moved_py = os.path.join(moved, "bin", "python")
moved_pip = os.path.join(moved, "bin", "pip")
print(" moved/bin/python: sys.prefix ->", run([moved_py, "-c", "import sys; print(sys.prefix)"]))
print(" moved/bin/pip --version ->", run([moved_pip, "--version", "--disable-pip-version-check"]).split(" from ")[1].split(" (")[0])
print(" moved/bin/python -m pip --version ->", run([moved_py, "-m", "pip", "--version", "--disable-pip-version-check"]).split(" from ")[1].split(" (")[0])
away = os.path.join(tmp, "env-away")
os.rename(env_dir, away)
try:
r = subprocess.run([moved_pip, "--version"], capture_output=True, text=True)
print(" без старой папки moved/bin/pip -> запустился, код", r.returncode)
except OSError as e:
print(" без старой папки moved/bin/pip ->", type(e).__name__ + ":", e.strerror)
finally:
os.rename(away, env_dir)
# ── 7 ─────────────────────────────────────────────────────────────────────
print()
print("7) pip freeze: что попадает в снимок")
for name in ("localpkg", "editpkg"):
pkg_dir = os.path.join(tmp, name)
os.makedirs(os.path.join(pkg_dir, name))
with open(os.path.join(pkg_dir, "pyproject.toml"), "w", encoding="utf-8") as f:
f.write('[build-system]\nrequires = ["setuptools>=61"]\nbuild-backend = "setuptools.build_meta"\n\n'
f'[project]\nname = "{name}"\nversion = "0.1"\n')
with open(os.path.join(pkg_dir, name, "__init__.py"), "w", encoding="utf-8") as f:
f.write("X = 1\n")
pip_q = [env_py, "-m", "pip", "install", "-q", "--disable-pip-version-check"]
subprocess.run(pip_q + [os.path.join(tmp, "localpkg")], check=True, capture_output=True)
subprocess.run(pip_q + ["-e", os.path.join(tmp, "editpkg")], check=True, capture_output=True)
frozen = run([env_py, "-m", "pip", "freeze", "--disable-pip-version-check"]).splitlines()
print(" python -m pip freeze:")
for line in frozen:
print(" ", line)
frozen_all = run([env_py, "-m", "pip", "freeze", "--all", "--disable-pip-version-check"]).splitlines()
print(" только в python -m pip freeze --all:", [line for line in frozen_all if line not in frozen])
# ── 8 ─────────────────────────────────────────────────────────────────────
print()
print("8) env/bin/python -S: модуль site не импортируется")
probe_s = ("import sys; print(sys.prefix); print(sys.base_prefix); "
"print(sys.prefix != sys.base_prefix); print('site' in sys.modules); "
"print(any('site-packages' in p for p in sys.path))")
lines = run([env_py, "-S", "-c", probe_s]).splitlines()
print(" sys.prefix ->", lines[0])
print(" sys.base_prefix ->", lines[1])
print(" prefix != base_prefix ->", lines[2])
print(" site в sys.modules ->", lines[3])
print(" site-packages в sys.path ->", lines[4])