Skip to content

Refactor virtualenv bin/ / Scripts/ path resolution using sysconfig mechanism #6373

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 21, 2025
Merged
Show file tree
Hide file tree
Changes from 3 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
31 changes: 23 additions & 8 deletions pipenv/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
system_which,
)
from pipenv.utils.toml import cleanup_toml, convert_toml_outline_tables
from pipenv.utils.virtualenv import virtualenv_scripts_dir
from pipenv.vendor import plette, tomlkit

try:
Expand Down Expand Up @@ -411,11 +412,19 @@ def is_venv_in_project(self) -> bool:
@property
def virtualenv_exists(self) -> bool:
venv_path = Path(self.virtualenv_location)

scripts_dir = self.virtualenv_scripts_location

if venv_path.exists():
if os.name == "nt":
activate_path = venv_path / "Scripts" / "activate.bat"

# existence of active.bat is dependent on the platform path prefix
# scheme, not platform itself. This handles special cases such as
# Cygwin/MinGW identifying as 'nt' platform, yet preferring a
# 'posix' path prefix scheme.
if scripts_dir.name == "Scripts":
activate_path = scripts_dir / "activate.bat"
else:
activate_path = venv_path / "bin" / "activate"
activate_path = scripts_dir / "activate"
return activate_path.is_file()

return False
Expand Down Expand Up @@ -612,6 +621,10 @@ def virtualenv_src_location(self) -> Path:
loc.mkdir(parents=True, exist_ok=True)
return loc

@property
def virtualenv_scripts_location(self) -> Path:
return virtualenv_scripts_dir(self.virtualenv_location)

@property
def download_location(self) -> Path:
if self._download_location is None:
Expand Down Expand Up @@ -1422,10 +1435,10 @@ def proper_case_section(self, section):
def finders(self):
from .vendor.pythonfinder import Finder

scripts_dirname = "Scripts" if os.name == "nt" else "bin"
scripts_dir = Path(self.virtualenv_location) / scripts_dirname
finders = [
Finder(path=str(scripts_dir), global_search=gs, system=False)
Finder(
path=str(self.virtualenv_scripts_location), global_search=gs, system=False
)
for gs in (False, True)
]
return finders
Expand Down Expand Up @@ -1463,12 +1476,14 @@ def _which(self, command, location=None, allow_global=False):
is_python = command in ("python", Path(sys.executable).name, version_str)

if not allow_global:
scripts_location = virtualenv_scripts_dir(location_path)

if os.name == "nt":
p = find_windows_executable(str(location_path / "Scripts"), command)
p = find_windows_executable(str(scripts_location), command)
# Convert to Path object if it's a string
p = Path(p) if isinstance(p, str) else p
else:
p = location_path / "bin" / command
p = scripts_location / command
elif is_python:
p = Path(sys.executable)
else:
Expand Down
9 changes: 2 additions & 7 deletions pipenv/routines/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from pipenv.utils.project import ensure_project
from pipenv.utils.shell import cmd_list_to_shell, system_which
from pipenv.utils.virtualenv import virtualenv_scripts_dir
from pipenv.vendor import click


Expand Down Expand Up @@ -60,7 +61,6 @@ def do_run(project, command, args, python=False, pypi_mirror=None):
Args are appended to the command in [scripts] section of project if found.
"""
from pathlib import Path

from pipenv.cmdparse import ScriptEmptyError

Expand All @@ -79,12 +79,7 @@ def do_run(project, command, args, python=False, pypi_mirror=None):
# Get the exact string representation of virtualenv_location
virtualenv_location = str(project.virtualenv_location)

# Use pathlib for path construction but convert back to string
from pathlib import Path

virtualenv_path = Path(virtualenv_location)
bin_dir = "Scripts" if os.name == "nt" else "bin"
new_path = str(virtualenv_path / bin_dir)
new_path = str(virtualenv_scripts_dir(virtualenv_location))

# Update PATH
paths = path.split(os.pathsep)
Expand Down
15 changes: 15 additions & 0 deletions pipenv/utils/virtualenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import re
import shutil
import sys
import sysconfig
from pathlib import Path

from pipenv import environments, exceptions
Expand All @@ -13,6 +14,20 @@
from pipenv.utils.shell import find_python, shorten_path


def virtualenv_scripts_dir(b):
"""returns a system-dependent scripts path

POSIX environments (including Cygwin/MinGW64) will result in
`{base}/bin/`, native Windows environments will result in
`{base}/Scripts/`.

:param b: base path
:type b: str
:returns: pathlib.Path
"""
return Path(f"{b}/{Path(sysconfig.get_path('scripts')).name}")


def warn_in_virtualenv(project):
# Only warn if pipenv isn't already active.
if environments.is_in_virtualenv() and not project.s.is_quiet():
Expand Down