Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/changelog/3204.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Record the created environment as the default one of its parent folder within a ``.python-envs`` file, per `PEP 832
<https://peps.python.org/pep-0832/>`_, so editors and type checkers can find it. Parallel creations under one folder
serialize through a ``.python-envs.lock`` file next to it. Skip both with ``--no-python-envs`` - by :user:`gaborbernat`.
55 changes: 53 additions & 2 deletions docs/explanation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ virtualenv operates in two distinct phases:
CreatePython --> SeedPackages[Install seed packages: pip, setuptools, wheel]
SeedPackages --> ActivationScripts[Install activation scripts]
ActivationScripts --> VCSIgnore[Create VCS ignore files]
VCSIgnore --> Complete([Virtual environment ready])
VCSIgnore --> Record[Record in .python-envs]
Record --> Complete([Virtual environment ready])

style Start fill:#2563eb,stroke:#1d4ed8,color:#fff
style Phase1 fill:#6366f1,stroke:#4f46e5,color:#fff
Expand All @@ -161,12 +162,14 @@ virtualenv operates in two distinct phases:
flag to specify a different interpreter.

**Phase 2: Create the virtual environment**
Once the target interpreter is identified, virtualenv creates the environment in four steps:
Once the target interpreter is identified, virtualenv creates the environment in five steps:

1. Create a Python executable matching the target interpreter
2. Install seed packages (pip, setuptools, wheel) to enable package installation
3. Install activation scripts for various shells
4. Create VCS ignore files (currently Git's ``.gitignore``, skip with ``--no-vcs-ignore``)
5. Record the environment in a ``.python-envs`` file so editors and type checkers can find it (`PEP 832
<https://peps.python.org/pep-0832/>`_, skip with ``--no-python-envs``)

An important design principle: virtual environments are not self-contained. A complete Python installation consists of
thousands of files, and copying all of them into every virtual environment would be wasteful. Instead, virtual
Expand Down Expand Up @@ -549,6 +552,54 @@ For a deeper dive into how activation works under the hood, see Allison Kaptur's
edition <https://www.recurse.com/blog/14-there-is-no-magic-virtualenv-edition>`_, which explains how virtualenv uses
``PATH`` and ``PYTHONHOME`` to isolate virtual environments.

***********************
Environment discovery
***********************

Editors, type checkers and task runners need to find a project's environments, and they cannot count on an activated
shell to tell them where those are. Launching an editor on a fresh checkout leaves it guessing. Each tool has answered
that by hard-coding a search per environment manager it wants to support, which is why editor support for any new tool
lags behind the tool itself.

`PEP 832 <https://peps.python.org/pep-0832/>`_ replaces the guessing with a file. A project may carry a ``.python-envs``
file listing one environment per line, and the tools that create environments keep it current. virtualenv creates
environments, so it writes the file; reading it belongs to the tools consuming it.

After creating ``<root>/<name>``, virtualenv records ``<name>`` in ``<root>/.python-envs``:

.. code-block:: console

$ virtualenv env
$ virtualenv other
$ cat .python-envs
env
other

**The last line is the default**
A tool asking "which environment should I use" takes the final entry, so the environment created last wins. An entry
pointing at the same destination moves to the end instead of being repeated.

**A ``.venv`` folder needs no line**
PEP 832 counts a ``.venv`` folder beside the file as its implicit last line. Creating ``.venv`` therefore writes
nothing, and where a ``.venv`` folder exists it stays the default whatever ``.python-envs`` lists. virtualenv logs a
debug message when it records an environment that a sibling ``.venv`` outranks.

**Recording never breaks creation**
A read or write failure logs a warning and leaves you with a working environment. Discovery is a convenience for
other tools, so it does not get to fail the job you asked for.

Why a lock file
===============

Recording rewrites the whole file, which loses entries when several environments land in one folder at once. A task
runner building a matrix does that. virtualenv serializes the read-modify-write through a ``.python-envs.lock`` file,
which is why a second file appears next to ``.python-envs`` on every platform except Windows, where the lock file goes
away as the lock releases.

The lock sits beside the file it guards rather than in the app data folder for two reasons. It keeps working when the
app data is read-only or disabled, and it gives any other PEP 832 writer a location to agree on, which a path private to
virtualenv could not.

**********
See also
**********
Expand Down
60 changes: 60 additions & 0 deletions docs/how-to/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,66 @@ Options are resolved in this order (highest to lowest priority):
style C fill:#d97706,stroke:#b45309,color:#fff
style D fill:#6366f1,stroke:#4f46e5,color:#fff

********************************
Make environments discoverable
********************************

virtualenv records every environment it creates in a ``.python-envs`` file next to it, so editors and type checkers can
find them without an activated shell. See `PEP 832 <https://peps.python.org/pep-0832/>`_ for the format and
:ref:`explanation:Environment discovery` for the reasoning.

Point a tool at the right environment
=====================================

The last line of ``.python-envs`` is the default environment, and the environment you create last takes that spot:

.. code-block:: console

$ virtualenv py313 --python 3.13
$ virtualenv py314 --python 3.14
$ cat .python-envs
py313
py314

To promote ``py313`` back to the default, create it again over the existing folder. Its entry moves to the end rather
than repeating:

.. code-block:: console

$ virtualenv py313 --python 3.13
$ cat .python-envs
py314
py313

A ``.venv`` folder outranks every line in the file, so delete or rename it if you want another environment to win.

Skip recording
==============

Pass ``--no-python-envs`` when you do not want the ``.python-envs`` and ``.python-envs.lock`` files:

.. code-block:: console

$ virtualenv env --no-python-envs

Set it once for every environment you create through the configuration file or an environment variable:

.. code-block:: ini

[virtualenv]
no_python_envs = true

.. code-block:: console

$ export VIRTUALENV_NO_PYTHON_ENVS=1

Commit the file or ignore it
============================

Commit ``.python-envs`` when the environment locations are the same for everyone on the project, such as a containerized
setup or a fixed tox layout. Add it to ``.gitignore`` when developers pick their own paths. Always ignore
``.python-envs.lock``, which is a local lock file rather than project configuration.

***********************
Control seed packages
***********************
Expand Down
2 changes: 2 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ into the standard library under the ``venv`` module. For how ``virtualenv`` comp

- :doc:`reference/compatibility` — Supported Python versions and operating systems
- :doc:`reference/cli` — Command line options and flags
- :doc:`reference/files` — Files written inside and beside a created environment
- :doc:`reference/api` — Programmatic Python API reference

**Explanation** - Understand the concepts
Expand Down Expand Up @@ -115,6 +116,7 @@ Learn more about virtualenv from these community resources:

reference/compatibility
reference/cli
reference/files
reference/api

.. toctree::
Expand Down
2 changes: 1 addition & 1 deletion docs/plugin/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ The output should list ``pyenv`` as an available discovery mechanism. You can no

$ virtualenv --discovery=pyenv myenv
created virtual environment CPython3.11.0.final.0-64 in 234ms
creator CPython3Posix(dest=/path/to/myenv, clear=False, no_vcs_ignore=False, global=False)
creator CPython3Posix(dest=/path/to/myenv, clear=False, no_vcs_ignore=False, no_python_envs=False, global=False)
seeder FromAppData(download=False, pip=bundle, setuptools=bundle, wheel=bundle, via=copy, app_data_dir=/path)
added seed packages: pip==23.0, setuptools==65.5.0, wheel==0.38.4
activators BashActivator,CShellActivator,FishActivator,NushellActivator,PowerShellActivator,PythonActivator
96 changes: 96 additions & 0 deletions docs/reference/files.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#################
Generated files
#################

Creating an environment writes files inside the destination folder and beside it. This page lists each one, what it
holds, and how to suppress it.

*******************************
Inside the environment folder
*******************************

``pyvenv.cfg``
==============

Marks the folder as a virtual environment and points the interpreter at the Python it was built from, per `PEP 405
<https://peps.python.org/pep-0405/>`_. Deleting it breaks the environment.

.. code-block:: ini

home = /usr/local/python-3.14/bin
implementation = CPython
version_info = 3.14.6.final.0
version = 3.14.6
executable = /usr/local/python-3.14/bin/python3.14
command = /usr/bin/python3 -m virtualenv /home/user/env
virtualenv = 21.7.1
include-system-site-packages = false
base-prefix = /usr/local/python-3.14
base-exec-prefix = /usr/local/python-3.14
base-executable = /usr/local/python-3.14/bin/python3.14

``prompt`` appears as an extra key when you pass ``--prompt``. The ``base-*`` keys come from the creator and vary by
creation method.

``CACHEDIR.TAG``
================

Marks the environment as regenerable cache content, following the `cache directory tagging specification
<https://bford.info/cachedir/>`_, so backup tools skip it. virtualenv leaves an existing file untouched.

``.gitignore``
==============

Holds ``*``, keeping the environment out of Git. Skip it with ``--no-vcs-ignore``. virtualenv leaves an existing file
untouched, and writes nothing for Mercurial, Bazaar or Subversion, none of which honor ignore files in a subdirectory.

``bin`` / ``Scripts``
=====================

The interpreter, the console scripts of any seeded package, and the activation scripts for each shell. See
:ref:`explanation:Activators` for the full list.

*******************************
Beside the environment folder
*******************************

``.python-envs``
================

Lists the environments of the parent folder, one per line, with the last line naming the default one, per `PEP 832
<https://peps.python.org/pep-0832/>`_. Skip it with ``--no-python-envs``.

.. code-block:: text

py313
py314

Format rules virtualenv follows when it rewrites the file:

.. list-table::
:header-rows: 1
:widths: 30 70

- - Rule
- Behavior
- - Encoding
- UTF-8.
- - Entry
- The destination folder name, since the file sits in the destination's parent. Absolute entries written by other
tools are read and preserved.
- - Default
- The last line. A new environment goes last, and an entry already pointing at it moves there rather than
repeating.
- - ``.venv``
- Never written, because PEP 832 counts a ``.venv`` folder beside the file as its implicit last line.
- - Blank lines
- Dropped on rewrite.
- - Failure
- Logged as a warning; the environment is still created.

``.python-envs.lock``
=====================

An empty lock file serializing concurrent rewrites of ``.python-envs``. It carries no content. On Windows it disappears
as the lock releases; on other platforms it stays behind, and you can delete it while no virtualenv is running.
Suppressed by ``--no-python-envs`` along with the file it guards, and worth adding to your VCS ignore list.
15 changes: 14 additions & 1 deletion docs/tutorial/getting-started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,24 @@ Let's create a virtual environment called ``myproject``:

$ virtualenv myproject
created virtual environment CPython3.13.2.final.0-64 in 200ms
creator CPython3Posix(dest=/home/user/myproject, clear=False, no_vcs_ignore=False, global=False)
creator CPython3Posix(dest=/home/user/myproject, clear=False, no_vcs_ignore=False, no_python_envs=False, global=False)
seeder FromAppData(download=False, pip=bundle, setuptools=bundle, via=copy, app_data_dir=/home/user/.cache/virtualenv)
activators BashActivator,CShellActivator,FishActivator,NushellActivator,PowerShellActivator,PythonActivator

This creates a new directory called ``myproject`` containing a complete, isolated Python environment with its own copy
of Python, pip, and other tools.

Alongside it you get a ``.python-envs`` file naming the environment you just made:

.. code-block:: console

$ cat .python-envs
myproject

Editors and type checkers read that file to find your environment, so they can offer the right interpreter before you
activate anything. Create a second environment here and its name joins the list, with the newest one last. Pass
``--no-python-envs`` if you would rather virtualenv left no trace outside the environment folder.

**************************
Activate the environment
**************************
Expand Down Expand Up @@ -232,6 +243,7 @@ In this tutorial, you learned how to:
- Install packages in isolation from your system Python.
- Save project dependencies with ``pip freeze``.
- Reproduce environments using ``requirements.txt``.
- Let editors find your environments through the ``.python-envs`` file.

************
Next steps
Expand All @@ -242,3 +254,4 @@ Now that you understand the basics, explore these topics:
- :doc:`../how-to/usage` for selecting specific Python versions, configuring defaults, and advanced usage patterns.
- :doc:`../explanation` for understanding how virtualenv works under the hood and how it compares to ``venv``.
- :doc:`../reference/cli` for all available command line options and flags.
- :doc:`../reference/files` for every file virtualenv writes inside and beside an environment.
34 changes: 34 additions & 0 deletions src/virtualenv/create/creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

from os.path import commonpath

from virtualenv.util.lock import ReentrantFileLock
from virtualenv.util.path import safe_delete
from virtualenv.util.subprocess import LogCmd, run_cmd
from virtualenv.version import __version__
Expand Down Expand Up @@ -54,6 +55,7 @@ def __init__(self, options: VirtualEnvOptions, interpreter: PythonInfo) -> None:
self.dest = Path(options.dest)
self.clear = options.clear
self.no_vcs_ignore = options.no_vcs_ignore
self.no_python_envs = options.no_python_envs
self.pyenv_cfg = PyEnvCfg.from_folder(self.dest)
self.app_data = options.app_data
self.env = options.env
Expand Down Expand Up @@ -90,6 +92,7 @@ def _args(self) -> list[tuple[str, Any]]:
("dest", str(self.dest)),
("clear", self.clear),
("no_vcs_ignore", self.no_vcs_ignore),
("no_python_envs", self.no_python_envs),
]

@classmethod
Expand Down Expand Up @@ -139,6 +142,13 @@ def add_parser_arguments(
help="don't create VCS ignore directive in the destination directory",
default=False,
)
parser.add_argument(
"--no-python-envs",
dest="no_python_envs",
action="store_true",
help="don't record the created environment within a PEP-832 .python-envs file next to the destination",
default=False,
)

@abstractmethod
def create(self) -> None:
Expand Down Expand Up @@ -204,6 +214,8 @@ def run(self) -> None:
self.set_pyenv_cfg()
if not self.no_vcs_ignore:
self.setup_ignore_vcs()
if not self.no_python_envs:
self.record_python_envs()

def add_cachedir_tag(self) -> None:
"""Generate a file indicating that this is not meant to be backed up."""
Expand Down Expand Up @@ -244,6 +256,28 @@ def setup_ignore_vcs(self) -> None:
# Bazaar - does not support ignore files in sub-directories, only at root level via .bzrignore
# Subversion - does not support ignore files, requires direct manipulation with the svn tool

def record_python_envs(self) -> None:
"""Register the environment as the default one of its parent folder, per PEP-832."""
if self.dest.name == ".venv": # implicitly the last line of .python-envs, no need to record it
return
root = self.dest.parent
if (root / ".venv" / "pyvenv.cfg").exists():
LOGGER.debug(".venv keeps being the default environment of %s", root)
envs_file = root / ".python-envs"
target = os.path.normcase(os.path.normpath(str(self.dest)))
try:
# concurrent creations under one folder rewrite the same file, so serialize them across processes
with ReentrantFileLock(root).lock_for_key(envs_file.name):
# the last entry is the default environment, so an entry already pointing at us must move to the end
keep = [
i
for i in (envs_file.read_text(encoding="utf-8") if envs_file.exists() else "").splitlines()
if i.strip() and os.path.normcase(os.path.normpath(os.path.join(str(root), i))) != target
]
envs_file.write_text("".join(f"{i}\n" for i in [*keep, self.dest.name]), encoding="utf-8")
except OSError as exc:
LOGGER.warning("could not record %s within %s - %s", self.dest, envs_file, exc)

@property
def debug(self) -> dict[str, Any] | None:
"""Debug information about the virtual environment (only valid after :meth:`create` has run)."""
Expand Down
Loading
Loading