Skip to content

Commit 342bb9e

Browse files
authored
Fix for UnboundLocalError in ensure_python SESSION_IS_INTERACTIVE=False (#6389)
* Fix for UnboundLocalError in ensure_python SESSION_IS_INTERACTIVE=False, using pyenv, and python version in Pipfile not available * Add tests * Add news
1 parent 000b5ba commit 342bb9e

File tree

3 files changed

+121
-13
lines changed

3 files changed

+121
-13
lines changed

news/6389.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix for ``UnboundLocalError`` in ``ensure_python`` when ``SESSION_IS_INTERACTIVE=False``, using pyenv, and python version in Pipfile not available.

pipenv/utils/virtualenv.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -306,21 +306,25 @@ def abort(msg=""):
306306
else:
307307
abort("Neither 'pyenv' nor 'asdf' could be found to install Python.")
308308
else:
309-
if environments.SESSION_IS_INTERACTIVE or project.s.PIPENV_YES:
310-
try:
311-
version = installer.find_version_to_install(python)
312-
except ValueError:
313-
abort()
314-
except InstallerError as e:
315-
abort(f"Something went wrong while installing Python:\n{e.err}")
316-
s = (
317-
"Would you like us to install ",
318-
f"[green]CPython {version}[/green] ",
319-
f"with {installer}?",
320-
)
309+
try:
310+
version = installer.find_version_to_install(python)
311+
except ValueError:
312+
abort()
313+
except InstallerError as e:
314+
abort(f"Something went wrong while installing Python:\n{e.err}")
315+
316+
s = (
317+
"Would you like us to install ",
318+
f"[green]CPython {version}[/green] ",
319+
f"with {installer}?",
320+
)
321321

322322
# Prompt the user to continue...
323-
if not (project.s.PIPENV_YES or Confirm.ask("".join(s), default=True)):
323+
if environments.SESSION_IS_INTERACTIVE:
324+
if not (project.s.PIPENV_YES or Confirm.ask("".join(s), default=True)):
325+
abort()
326+
elif not project.s.PIPENV_YES:
327+
# Non-interactive session without PIPENV_YES, proceed with installation
324328
abort()
325329
else:
326330
# Tell the user we're installing Python.
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import pytest
2+
3+
from pipenv.utils.virtualenv import ensure_python
4+
5+
6+
def test_ensure_python_non_interactive_no_yes(monkeypatch, project):
7+
"""Test ensure_python when SESSION_IS_INTERACTIVE=False and PIPENV_YES=False."""
8+
# Mock the environments.SESSION_IS_INTERACTIVE to be False
9+
monkeypatch.setattr("pipenv.environments.SESSION_IS_INTERACTIVE", False)
10+
11+
# Mock project.s.PIPENV_YES to be False
12+
monkeypatch.setattr(project.s, "PIPENV_YES", False)
13+
14+
# Mock find_version_to_install to return a version
15+
class MockInstaller:
16+
def __init__(self, *args, **kwargs):
17+
self.cmd = "mock_installer"
18+
19+
def find_version_to_install(self, *args, **kwargs):
20+
return "3.11.0"
21+
22+
monkeypatch.setattr("pipenv.installers.Pyenv", MockInstaller)
23+
24+
# Mock find_a_system_python to return None (Python not found)
25+
monkeypatch.setattr("pipenv.utils.virtualenv.find_a_system_python", lambda x: None)
26+
27+
# Mock os.name to not be 'nt' to skip Windows-specific code
28+
monkeypatch.setattr("os.name", "posix")
29+
30+
# Mock project.s.PIPENV_DONT_USE_PYENV to be False
31+
monkeypatch.setattr(project.s, "PIPENV_DONT_USE_PYENV", False)
32+
33+
# The function should call sys.exit(1) when SESSION_IS_INTERACTIVE=False and PIPENV_YES=False
34+
# We'll catch this with pytest.raises
35+
with pytest.raises(SystemExit) as excinfo:
36+
ensure_python(project, python="3.11.0")
37+
38+
# Verify that sys.exit was called with code 1
39+
assert excinfo.value.code == 1
40+
41+
42+
def test_ensure_python_non_interactive_with_yes(monkeypatch, project):
43+
"""Test ensure_python when SESSION_IS_INTERACTIVE=False but PIPENV_YES=True."""
44+
# Mock the environments.SESSION_IS_INTERACTIVE to be False
45+
monkeypatch.setattr("pipenv.environments.SESSION_IS_INTERACTIVE", False)
46+
47+
# Mock project.s.PIPENV_YES to be True
48+
monkeypatch.setattr(project.s, "PIPENV_YES", True)
49+
50+
# Mock find_version_to_install to return a version
51+
class MockInstaller:
52+
def __init__(self, *args, **kwargs):
53+
self.cmd = "mock_installer"
54+
55+
def find_version_to_install(self, *args, **kwargs):
56+
return "3.11.0"
57+
58+
def install(self, *args, **kwargs):
59+
class Result:
60+
stdout = "Installed successfully"
61+
return Result()
62+
63+
monkeypatch.setattr("pipenv.installers.Pyenv", MockInstaller)
64+
65+
# Mock find_a_system_python to return None initially (Python not found)
66+
# and then return a path after "installation"
67+
find_python_calls = [None]
68+
69+
def mock_find_python(version):
70+
if len(find_python_calls) == 1:
71+
find_python_calls.append("/mock/path/to/python")
72+
return find_python_calls[-1]
73+
return find_python_calls[-1]
74+
75+
monkeypatch.setattr("pipenv.utils.virtualenv.find_a_system_python", mock_find_python)
76+
77+
# Mock python_version to return the expected version
78+
monkeypatch.setattr("pipenv.utils.dependencies.python_version", lambda x: "3.11.0")
79+
80+
# Mock os.name to not be 'nt' to skip Windows-specific code
81+
monkeypatch.setattr("os.name", "posix")
82+
83+
# Mock project.s.PIPENV_DONT_USE_PYENV to be False
84+
monkeypatch.setattr(project.s, "PIPENV_DONT_USE_PYENV", False)
85+
86+
# Mock console.status to do nothing
87+
def mock_status(*args, **kwargs):
88+
class MockContextManager:
89+
def __enter__(self):
90+
return None
91+
92+
def __exit__(self, *args):
93+
pass
94+
95+
return MockContextManager()
96+
97+
monkeypatch.setattr("pipenv.utils.console.status", mock_status)
98+
99+
# The function should proceed with installation when PIPENV_YES=True
100+
result = ensure_python(project, python="3.11.0")
101+
102+
# Verify that the function returned the path to Python
103+
assert result == "/mock/path/to/python"

0 commit comments

Comments
 (0)