Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions news/6389.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix for ``UnboundLocalError`` in ``ensure_python`` when ``SESSION_IS_INTERACTIVE=False``, using pyenv, and python version in Pipfile not available.
30 changes: 17 additions & 13 deletions pipenv/utils/virtualenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,21 +306,25 @@ def abort(msg=""):
else:
abort("Neither 'pyenv' nor 'asdf' could be found to install Python.")
else:
if environments.SESSION_IS_INTERACTIVE or project.s.PIPENV_YES:
try:
version = installer.find_version_to_install(python)
except ValueError:
abort()
except InstallerError as e:
abort(f"Something went wrong while installing Python:\n{e.err}")
s = (
"Would you like us to install ",
f"[green]CPython {version}[/green] ",
f"with {installer}?",
)
try:
version = installer.find_version_to_install(python)
except ValueError:
abort()
except InstallerError as e:
abort(f"Something went wrong while installing Python:\n{e.err}")

s = (
"Would you like us to install ",
f"[green]CPython {version}[/green] ",
f"with {installer}?",
)

# Prompt the user to continue...
if not (project.s.PIPENV_YES or Confirm.ask("".join(s), default=True)):
if environments.SESSION_IS_INTERACTIVE:
if not (project.s.PIPENV_YES or Confirm.ask("".join(s), default=True)):
abort()
elif not project.s.PIPENV_YES:
# Non-interactive session without PIPENV_YES, proceed with installation
abort()
else:
# Tell the user we're installing Python.
Expand Down
103 changes: 103 additions & 0 deletions tests/integration/test_python_version_mismatch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import pytest

from pipenv.utils.virtualenv import ensure_python


def test_ensure_python_non_interactive_no_yes(monkeypatch, project):
"""Test ensure_python when SESSION_IS_INTERACTIVE=False and PIPENV_YES=False."""
# Mock the environments.SESSION_IS_INTERACTIVE to be False
monkeypatch.setattr("pipenv.environments.SESSION_IS_INTERACTIVE", False)

# Mock project.s.PIPENV_YES to be False
monkeypatch.setattr(project.s, "PIPENV_YES", False)

# Mock find_version_to_install to return a version
class MockInstaller:
def __init__(self, *args, **kwargs):
self.cmd = "mock_installer"

def find_version_to_install(self, *args, **kwargs):
return "3.11.0"

monkeypatch.setattr("pipenv.installers.Pyenv", MockInstaller)

# Mock find_a_system_python to return None (Python not found)
monkeypatch.setattr("pipenv.utils.virtualenv.find_a_system_python", lambda x: None)

# Mock os.name to not be 'nt' to skip Windows-specific code
monkeypatch.setattr("os.name", "posix")

# Mock project.s.PIPENV_DONT_USE_PYENV to be False
monkeypatch.setattr(project.s, "PIPENV_DONT_USE_PYENV", False)

# The function should call sys.exit(1) when SESSION_IS_INTERACTIVE=False and PIPENV_YES=False
# We'll catch this with pytest.raises
with pytest.raises(SystemExit) as excinfo:
ensure_python(project, python="3.11.0")

# Verify that sys.exit was called with code 1
assert excinfo.value.code == 1


def test_ensure_python_non_interactive_with_yes(monkeypatch, project):
"""Test ensure_python when SESSION_IS_INTERACTIVE=False but PIPENV_YES=True."""
# Mock the environments.SESSION_IS_INTERACTIVE to be False
monkeypatch.setattr("pipenv.environments.SESSION_IS_INTERACTIVE", False)

# Mock project.s.PIPENV_YES to be True
monkeypatch.setattr(project.s, "PIPENV_YES", True)

# Mock find_version_to_install to return a version
class MockInstaller:
def __init__(self, *args, **kwargs):
self.cmd = "mock_installer"

def find_version_to_install(self, *args, **kwargs):
return "3.11.0"

def install(self, *args, **kwargs):
class Result:
stdout = "Installed successfully"
return Result()

monkeypatch.setattr("pipenv.installers.Pyenv", MockInstaller)

# Mock find_a_system_python to return None initially (Python not found)
# and then return a path after "installation"
find_python_calls = [None]

def mock_find_python(version):
if len(find_python_calls) == 1:
find_python_calls.append("/mock/path/to/python")
return find_python_calls[-1]
return find_python_calls[-1]

monkeypatch.setattr("pipenv.utils.virtualenv.find_a_system_python", mock_find_python)

# Mock python_version to return the expected version
monkeypatch.setattr("pipenv.utils.dependencies.python_version", lambda x: "3.11.0")

# Mock os.name to not be 'nt' to skip Windows-specific code
monkeypatch.setattr("os.name", "posix")

# Mock project.s.PIPENV_DONT_USE_PYENV to be False
monkeypatch.setattr(project.s, "PIPENV_DONT_USE_PYENV", False)

# Mock console.status to do nothing
def mock_status(*args, **kwargs):
class MockContextManager:
def __enter__(self):
return None

def __exit__(self, *args):
pass

return MockContextManager()

monkeypatch.setattr("pipenv.utils.console.status", mock_status)

# The function should proceed with installation when PIPENV_YES=True
result = ensure_python(project, python="3.11.0")

# Verify that the function returned the path to Python
assert result == "/mock/path/to/python"