Skip to content

Commit 0d25fb3

Browse files
committed
✨ feat(config): catalog environments per PEP 832
Editors have no way to find the environments tox builds under .tox, so using one means copying a path out of tox devenv and pasting it into a settings dialog, again after every recreate. PEP 832 fixes that with a .python-envs file at the project root that any tool can write and any editor can read, with the last line naming the default environment. tox now writes that file at the end of a run. The ordering puts the environment a developer edits code against last: one named dev, then a develop install, then the earlier entries of env_list, so trimming lines from the bottom falls back to the next best choice. Lines outside the work dir belong to another tool and survive untouched, including their claim on the last line. A recreated environment leaves the catalog while it rebuilds so nothing points at a half-built interpreter. Writing into the project root is on by default, since the convention only pays off when tools follow it without being asked; python_envs turns it off.
1 parent 2e247dc commit 0d25fb3

14 files changed

Lines changed: 404 additions & 1 deletion

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ __pycache__
1212
/tests/demo_pkg_setuptools/build/lib/demo_pkg_setuptools/__init__.py
1313
/tests/demo_pkg_inline.lock
1414
/tests/demo_pkg_inline/.tox/
15+
.python-envs

docs/changelog/4013.feature.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Catalog the created tox environments in a :PEP:`832` ``.python-envs`` file next to the configuration file, so that
2+
editors such as VS Code or PyCharm offer them as interpreters instead of asking you for a path under ``.tox``. The
3+
default environment comes last, and tox prefers one named ``dev`` for that spot, then one that installs the project in
4+
development mode, then the first of :ref:`env_list`. tox leaves the lines other tools wrote alone, and takes an
5+
environment out of the catalog while it recreates it. Set :ref:`python_envs` to ``false`` to skip the file - by
6+
:user:`gaborbernat`. (:issue:`4013`)

docs/explanation.rst

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,31 @@ upgrade that changes the derived value) triggers automatic recreation.
700700
This design mirrors tox's own auto-provisioning mechanism (``requires`` / ``min_version``), where tox bootstraps itself
701701
into a separate environment when the running installation doesn't meet the declared requirements.
702702

703+
***********************
704+
Environment discovery
705+
***********************
706+
707+
Editors need an interpreter path before they can offer completion, navigation or a debugger. tox keeps its environments
708+
under ``.tox``, a directory editors have no reason to search, so for years the answer was to copy a path out of ``tox
709+
devenv`` output and paste it into a settings dialog, then repeat it after every recreate.
710+
711+
:PEP:`832` standardizes where tools publish that answer. A project may hold a virtual environment at ``.venv``, and a
712+
``.python-envs`` file at the project root lists any further environments, one directory per line. The last line is the
713+
default, so a reader that supports only one environment still knows which to take. VS Code reads the file today and
714+
PyCharm honors the ``.venv`` half of the convention.
715+
716+
tox writes ``.python-envs`` at the end of a run, listing every environment that exists on disk. Ordering follows how
717+
useful an environment is to an editor rather than how tox happens to schedule it: an environment named ``dev`` wins,
718+
then one installing the project in development mode, then the earlier entries of :ref:`env_list`. Since the catalog
719+
lists the preferred environment last, removing lines from the bottom degrades to the next best choice.
720+
721+
Two rules keep the file honest. tox owns the lines under :ref:`work_dir` and rewrites them wholesale, while lines
722+
pointing elsewhere came from another tool and survive untouched, including the last line and the default it claims. And
723+
because a recreated environment is unusable between the moment tox empties the directory and the moment the new
724+
interpreter lands, tox takes the environment out of the catalog first and puts it back once the run ends.
725+
726+
Set :ref:`python_envs` to ``false`` if you would rather tox left the project root alone.
727+
703728
*******************
704729
Known limitations
705730
*******************

docs/how-to/usage.rst

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,44 @@ The ``tox exec`` subcommand runs an arbitrary command inside a tox environment w
162162
The command must be in the environment's ``PATH`` or listed in :ref:`allowlist_externals`. ``tox exec`` is useful for
163163
debugging, running one-off scripts, or interactively exploring an environment without modifying your configuration.
164164

165+
.. _howto_editor_env:
166+
167+
***************************************
168+
Open a tox environment in your editor
169+
***************************************
170+
171+
Editors pick up interpreters through the :PEP:`832` ``.python-envs`` file, which tox writes next to your configuration
172+
file after each run. Name the environment you develop against ``dev`` and tox puts it on the last line, the one readers
173+
treat as the default:
174+
175+
.. code-block:: toml
176+
177+
[env.dev]
178+
description = "development environment"
179+
package = "editable"
180+
dependency_groups = [ "dev" ]
181+
182+
Create it, then reload your editor window:
183+
184+
.. code-block:: bash
185+
186+
tox run -e dev --notest
187+
188+
The file lists one directory per line, so you can check what an editor will see:
189+
190+
.. code-block:: bash
191+
192+
$ cat .python-envs
193+
.tox/3.13
194+
.tox/dev
195+
196+
Commit the file when your team keeps environments in the same place, otherwise add it to your ``.gitignore``. To keep
197+
tox from writing it at all, set :ref:`python_envs` in the core section:
198+
199+
.. code-block:: toml
200+
201+
python_envs = false
202+
165203
.. ------------------------------------------------------------------------------------------
166204
167205
.. Configuration (frequently needed)

docs/reference/config.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,21 @@ the top level of ``tox.toml``. Placing these options in an environment section (
525525
this directory for the project package. This ensures tox works correctly when having parallel runs (as each session
526526
will have its own copy of the project package - e.g. the source distribution).
527527

528+
.. conf::
529+
:keys: python_envs
530+
:default: true
531+
:version_added: 4.59.0
532+
533+
Catalog the created tox environments in a :pep:`832` ``.python-envs`` file next to the configuration file, so that
534+
editors such as VS Code or PyCharm offer them as interpreters. The file holds one environment directory per line,
535+
with the default one last. tox prefers an environment named ``dev`` for that spot, then one that installs the
536+
project in development mode, then the first entry of :ref:`env_list`. It lists only environments that exist, and
537+
takes one out of the catalog while it recreates it.
538+
539+
Lines pointing outside the :ref:`work_dir` come from another tool, so tox keeps them, including their claim on the
540+
last line. Commit the file when everyone working on the project has their environments in the same place, otherwise
541+
ignore it.
542+
528543
.. conf::
529544
:keys: no_package, skipsdist
530545
:default: false

docs/tutorial/getting-started.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,23 @@ has changed), use ``--skip-env-install``:
289289
290290
tox run -e 3.13 --skip-env-install
291291
292+
******************************
293+
Using an environment at hand
294+
******************************
295+
296+
After a run, tox records the environments it created in a ``.python-envs`` file next to your configuration, following
297+
:PEP:`832`. Editors such as VS Code read it and offer those interpreters, so you can pick one instead of hunting for a
298+
path under ``.tox``:
299+
300+
.. code-block:: bash
301+
302+
$ tox run -e 3.13
303+
$ cat .python-envs
304+
.tox/3.13
305+
306+
The last line is the default. Name an environment ``dev`` and tox puts it there, which is what you want for the
307+
environment you edit code against - see :ref:`howto_editor_env`.
308+
292309
********************************
293310
Listing available environments
294311
********************************

src/tox/config/sets.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,12 @@ def register_config(self) -> None:
249249
default=self._default_temp_dir,
250250
desc="a folder for temporary files (is not cleaned at start)",
251251
)
252+
self.add_config(
253+
keys=["python_envs"],
254+
of_type=bool,
255+
default=True,
256+
desc="catalog the created tox environments in a PEP-832 .python-envs file, so editors can discover them",
257+
)
252258
self.add_constant("host_python", "the host python executable path", sys.executable)
253259

254260
def _on_duplicate_conf(self, key: str, definition: ConfigDefinition[V]) -> None:

src/tox/pytest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ def __init__( # ruff:ignore[too-many-arguments]
147147
@staticmethod
148148
def _setup_files(dest: Path, base: Path | None, content: dict[str, Any]) -> None:
149149
if base is not None:
150-
shutil.copytree(str(base), str(dest), ignore=shutil.ignore_patterns(".tox"))
150+
shutil.copytree(str(base), str(dest), ignore=shutil.ignore_patterns(".tox", ".python-envs"))
151151
dest.mkdir(exist_ok=True)
152152
for key, value in content.items():
153153
if not isinstance(key, str):

src/tox/session/cmd/run/common.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from concurrent.futures import FIRST_COMPLETED, CancelledError, Future, ThreadPoolExecutor
1010
from concurrent.futures import wait as wait_futures
1111
from fnmatch import fnmatchcase
12+
from operator import itemgetter
1213
from pathlib import Path
1314
from signal import SIGINT, Handlers, signal
1415
from threading import Event, Thread
@@ -24,6 +25,7 @@
2425
from tox.session.cmd.run.single import ToxEnvRunResult, run_one
2526
from tox.tox_env.errors import Fail
2627
from tox.util.graph import stable_topological_sort
28+
from tox.util.python_envs import record_python_envs
2729
from tox.util.spinner import MISS_DURATION, Spinner
2830

2931
if TYPE_CHECKING:
@@ -280,6 +282,8 @@ def _run_thread() -> tuple[Any, bool]:
280282
ordered_results = _order_results(state, results, to_run_list)
281283
# write the journal
282284
write_journal(getattr(state.conf.options, "result_json", None), state._journal) # ruff:ignore[private-member-access]
285+
# let editors discover the environments
286+
_record_python_envs(state)
283287
# warn about unused config keys
284288
_warn_unused_config(state)
285289
# report the outcome
@@ -311,6 +315,22 @@ def _order_results(state: State, results: list[ToxEnvRunResult], to_run_list: li
311315
return ordered
312316

313317

318+
def _record_python_envs(state: State) -> None:
319+
core = state.conf.core
320+
if not core["python_envs"]:
321+
return
322+
ranked: list[tuple[tuple[bool, bool, int], Path]] = []
323+
for at, name in enumerate(state.envs.iter(only_active=False)):
324+
env = state.envs[name]
325+
if not (env.env_dir / "pyvenv.cfg").exists(): # not a Python environment a reader could use
326+
continue
327+
develop = "package" in env.conf and env.conf["package"] in {"editable", "editable-legacy"}
328+
# the default environment goes last: prefer one named dev, then a develop install, then the env list order
329+
ranked.append(((name == "dev", develop, -at), env.env_dir))
330+
envs = [env_dir for _, env_dir in sorted(ranked, key=itemgetter(0))]
331+
record_python_envs(cast("Path", core["tox_root"]), cast("Path", core["work_dir"]), envs)
332+
333+
314334
class ToxSpinner(Spinner):
315335
def __init__(self, enabled: bool, state: State, total: int) -> None: # ruff:ignore[boolean-type-hint-positional-argument]
316336
stream = state._options.log_handler.stdout # ruff:ignore[private-member-access]

src/tox/tox.schema.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
"type": "string",
3333
"description": "a folder for temporary files (is not cleaned at start)"
3434
},
35+
"python_envs": {
36+
"type": "boolean",
37+
"description": "catalog the created tox environments in a PEP-832 .python-envs file, so editors can discover them"
38+
},
3539
"env_list": {
3640
"type": "array",
3741
"items": {

0 commit comments

Comments
 (0)