Skip to content

Commit c55a9b2

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 930190c commit c55a9b2

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
@@ -721,6 +721,31 @@ upgrade that changes the derived value) triggers automatic recreation.
721721
This design mirrors tox's own auto-provisioning mechanism (``requires`` / ``min_version``), where tox bootstraps itself
722722
into a separate environment when the running installation doesn't meet the declared requirements.
723723

724+
***********************
725+
Environment discovery
726+
***********************
727+
728+
Editors need an interpreter path before they can offer completion, navigation or a debugger. tox keeps its environments
729+
under ``.tox``, a directory editors have no reason to search, so for years the answer was to copy a path out of ``tox
730+
devenv`` output and paste it into a settings dialog, then repeat it after every recreate.
731+
732+
:PEP:`832` standardizes where tools publish that answer. A project may hold a virtual environment at ``.venv``, and a
733+
``.python-envs`` file at the project root lists any further environments, one directory per line. The last line is the
734+
default, so a reader that supports only one environment still knows which to take. VS Code reads the file today and
735+
PyCharm honors the ``.venv`` half of the convention.
736+
737+
tox writes ``.python-envs`` at the end of a run, listing every environment that exists on disk. Ordering follows how
738+
useful an environment is to an editor rather than how tox happens to schedule it: an environment named ``dev`` wins,
739+
then one installing the project in development mode, then the earlier entries of :ref:`env_list`. Since the catalog
740+
lists the preferred environment last, removing lines from the bottom degrades to the next best choice.
741+
742+
Two rules keep the file honest. tox owns the lines under :ref:`work_dir` and rewrites them wholesale, while lines
743+
pointing elsewhere came from another tool and survive untouched, including the last line and the default it claims. And
744+
because a recreated environment is unusable between the moment tox empties the directory and the moment the new
745+
interpreter lands, tox takes the environment out of the catalog first and puts it back once the run ends.
746+
747+
Set :ref:`python_envs` to ``false`` if you would rather tox left the project root alone.
748+
724749
*******************
725750
Known limitations
726751
*******************

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
@@ -539,6 +539,21 @@ the top level of ``tox.toml``. Placing these options in an environment section (
539539
this directory for the project package. This ensures tox works correctly when having parallel runs (as each session
540540
will have its own copy of the project package - e.g. the source distribution).
541541

542+
.. conf::
543+
:keys: python_envs
544+
:default: true
545+
:version_added: 4.59.0
546+
547+
Catalog the created tox environments in a :pep:`832` ``.python-envs`` file next to the configuration file, so that
548+
editors such as VS Code or PyCharm offer them as interpreters. The file holds one environment directory per line,
549+
with the default one last. tox prefers an environment named ``dev`` for that spot, then one that installs the
550+
project in development mode, then the first entry of :ref:`env_list`. It lists only environments that exist, and
551+
takes one out of the catalog while it recreates it.
552+
553+
Lines pointing outside the :ref:`work_dir` come from another tool, so tox keeps them, including their claim on the
554+
last line. Commit the file when everyone working on the project has their environments in the same place, otherwise
555+
ignore it.
556+
542557
.. conf::
543558
:keys: no_package, skipsdist
544559
:default: false

docs/tutorial/getting-started.rst

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

src/tox/config/sets.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,12 @@ def register_config(self) -> None:
315315
default=self._default_temp_dir,
316316
desc="a folder for temporary files (is not cleaned at start)",
317317
)
318+
self.add_config(
319+
keys=["python_envs"],
320+
of_type=bool,
321+
default=True,
322+
desc="catalog the created tox environments in a PEP-832 .python-envs file, so editors can discover them",
323+
)
318324
self.add_constant("host_python", "the host python executable path", sys.executable)
319325

320326
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
@@ -25,6 +26,7 @@
2526
from tox.session.cmd.run.single import ToxEnvRunResult, run_one
2627
from tox.tox_env.errors import Fail
2728
from tox.util.graph import stable_topological_sort
29+
from tox.util.python_envs import record_python_envs
2830
from tox.util.spinner import MISS_DURATION, Spinner
2931

3032
if TYPE_CHECKING:
@@ -282,6 +284,8 @@ def _run_thread() -> tuple[Callable[[int, FrameType | None], Any] | int | Handle
282284
ordered_results = _order_results(state, results, to_run_list)
283285
# write the journal
284286
write_journal(state.conf.options.result_json, state._journal) # ruff:ignore[private-member-access]
287+
# let editors discover the environments
288+
_record_python_envs(state)
285289
# warn about unused config keys
286290
_warn_unused_config(state)
287291
# report the outcome
@@ -313,6 +317,22 @@ def _order_results(state: State, results: list[ToxEnvRunResult], to_run_list: li
313317
return ordered
314318

315319

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