Skip to content

Commit b6acd0e

Browse files
fix(venv): escape glob characters in the venv lookup prefix (#3869)
get_venv_prefix builds the prefix from the project directory name, and venv create writes that name to disk literally. iter_venvs fed the same string to Path.glob, where [ and ] are pattern metacharacters and are legal in Windows directory names. A project in my[project] or downloads[1] produced a pattern that never matched its own venv, so venv list, venv remove, venv activate and interpreter discovery all silently saw nothing. The ident slice keeps the unescaped length because it indexes the real on-disk name, not the pattern.
1 parent b1878e2 commit b6acd0e

2 files changed

Lines changed: 28 additions & 1 deletion

File tree

src/pdm/cli/commands/venv/utils.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import base64
4+
import glob
45
import hashlib
56
from pathlib import Path
67
from typing import TYPE_CHECKING
@@ -60,7 +61,10 @@ def iter_venvs(project: Project) -> Iterable[tuple[str, VirtualEnv]]:
6061
yield "in-project", in_project_venv
6162
venv_prefix = get_venv_prefix(project)
6263
venv_parent = get_venv_parent(project)
63-
for path in venv_parent.glob(f"{venv_prefix}*"):
64+
# The project directory name is part of the prefix and may legally contain
65+
# glob metacharacters (e.g. `my[project]`), which would otherwise be treated
66+
# as a pattern and fail to match the venv that `venv create` wrote to disk.
67+
for path in venv_parent.glob(f"{glob.escape(venv_prefix)}*"):
6468
ident = path.name[len(venv_prefix) :]
6569
venv = VirtualEnv.get(path)
6670
if venv is not None:

tests/cli/test_venv.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,29 @@ def test_venv_list_with_tilde_location(pdm, project, monkeypatch, tmp_path):
141141
assert result.output.strip() == venv_path
142142

143143

144+
@pytest.mark.parametrize("dirname", ["my[project]", "downloads[1]"])
145+
def test_iter_venvs_with_glob_chars_in_project_path(project, tmp_path, dirname):
146+
"""A project directory name may legally contain glob metacharacters.
147+
148+
``venv create`` names the venv with the literal directory name, so the
149+
lookup must match that same literal name rather than treating it as a
150+
pattern.
151+
"""
152+
from pdm.cli.commands.venv.utils import get_venv_parent, iter_venvs
153+
154+
project.root = tmp_path / dirname
155+
project.root.mkdir()
156+
157+
venv_parent = get_venv_parent(project)
158+
venv_parent.mkdir(parents=True, exist_ok=True)
159+
venv_dir = venv_parent / f"{get_venv_prefix(project)}3.13"
160+
bin_dir = venv_dir / ("Scripts" if sys.platform == "win32" else "bin")
161+
bin_dir.mkdir(parents=True)
162+
bin_dir.joinpath("python.exe" if sys.platform == "win32" else "python").touch()
163+
164+
assert {ident: venv.root for ident, venv in iter_venvs(project)} == {"3.13": venv_dir}
165+
166+
144167
@pytest.mark.usefixtures("fake_create")
145168
def test_venv_remove(pdm, project):
146169
project.project_config["venv.in_project"] = False

0 commit comments

Comments
 (0)