Repository: pdm-project/pdm

Issue:
PDM doesn't find Python interpreters when PDM_IGNORE_ACTIVE_VENV is set
- [x] I have searched the issue tracker and believe that this is not a duplicate.

**Make sure you run commands with `-v` flag before pasting the output.**

## Steps to reproduce

<!--Describe the minimized example of how to reproduce the bug-->

1.  Have some Python version `>=3.10` installed
2. Use following minimal `pyproject.toml`
   
   ```toml
   [project]
   name = "test"
   version = "0.0.1"
   requires-python = ">=3.10"
   ```

3. Set environment variable `PDM_IGNORE_SAVED_PYTHON` and `PDM_IGNORE_ACTIVE_VENV`.
4. Try to install the project:

   ```shell
   pdm install --verbose
   ```

## Actual behavior

<!--A clear and concise description the result of the above steps-->

PDM does not find a Python version to use and errors out:

```
PS C:\Users\progalski\git\test> pdm install --verbose
WARNING: Lockfile does not exist
Updating the lock file...
[NoPythonVersion]: No Python that satisfies >=3.10 is found on the system.
```

## Expected behavior

<!--A clear and concise description of what you expected to happen.-->

According to [PDM Docs: Virtualenv auto-detection](https://pdm-project.org/latest/usage/venv/#virtualenv-auto-detection) I would expect that PDM creates a new virtual environment because virtual env auto-detection fails.

## Environment Information

```bash
# Paste the output of `pdm info && pdm info --env` below:
PS C:\Users\progalski\git\test> pdm info && pdm info --env
[NoPythonVersion]: No Python that satisfies >=3.10 is found on the system.
```

Environment Information without the problematic environment variables set:
```bash
# Paste the output of `pdm info && pdm info --env` below:
PDM version:
  2.13.2
Python Interpreter:
  C:\Users\progalski\git\test\.venv\Scripts\python.exe (3.11)
Project Root:
  C:/Users/progalski/git/test
Local Packages:

{
  "implementation_name": "cpython",
  "implementation_version": "3.11.4",
  "os_name": "nt",
  "platform_machine": "AMD64",
  "platform_release": "10",
  "platform_system": "Windows",
  "platform_version": "10.0.22631",
  "python_full_version": "3.11.4",
  "platform_python_implementation": "CPython",
  "python_version": "3.11",
  "sys_platform": "win32"
}
```


Discussion:
I had a look into the PDM source code, the logic of getting a valid interpreter can be found here: https://github.com/pdm-project/pdm/blob/881cd4e38d31663ae67bdae227ec1ccdfd5e2c77/src/pdm/project/core.py#L193

If `PDM_IGNORE_ACTIVE_VENV` is set, the logic which creates a new virtual environment is skipped completely. This is unexpected, because the env var name suggests that it just ignores the currently activated venv.

Instead, I would PDM expect to fall back to creating a new virtual environment when `python.use_venv` is `True`.
I just hit the same issue.
Working on a fix



Patch:
diff --git a/news/2779.bugfix.md b/news/2779.bugfix.md
new file mode 100644
index 0000000000..7ba5389148
--- /dev/null
+++ b/news/2779.bugfix.md
@@ -0,0 +1,1 @@
+Fix PDM unable to find Python interpreters when `PDM_IGNORE_ACTIVE_VENV` is set
diff --git a/src/pdm/project/core.py b/src/pdm/project/core.py
index da539c11d2..72218ca6f7 100644
--- a/src/pdm/project/core.py
+++ b/src/pdm/project/core.py
@@ -22,7 +22,7 @@
 from pdm.models.repositories import BaseRepository, LockedRepository
 from pdm.models.requirements import Requirement, parse_requirement, strip_extras
 from pdm.models.specifiers import PySpecSet
-from pdm.project.config import Config
+from pdm.project.config import Config, ensure_boolean
 from pdm.project.lockfile import Lockfile
 from pdm.project.project_file import PyProject
 from pdm.utils import (
@@ -33,6 +33,7 @@
     find_python_in_path,
     is_conda_base,
     is_conda_base_python,
+    is_path_relative_to,
     path_to_url,
 )
 
@@ -202,6 +203,11 @@ def note(message: str) -> None:
             if not self.is_global:
                 self.core.ui.info(message)
 
+        def is_active_venv(python: PythonInfo) -> bool:
+            if not (venv := os.getenv("VIRTUAL_ENV", os.getenv("CONDA_PREFIX"))):
+                return False
+            return is_path_relative_to(python.executable, venv)
+
         config = self.config
         saved_path = self._saved_python
         if saved_path and not os.getenv("PDM_IGNORE_SAVED_PYTHON"):
@@ -215,11 +221,12 @@ def note(message: str) -> None:
                 )
             self._saved_python = None  # Clear the saved path if it doesn't match
 
-        if config.get("python.use_venv") and not self.is_global and not os.getenv("PDM_IGNORE_ACTIVE_VENV"):
+        if config.get("python.use_venv") and not self.is_global:
             # Resolve virtual environments from env-vars
+            ignore_active_venv = ensure_boolean(os.getenv("PDM_IGNORE_ACTIVE_VENV"))
             venv_in_env = os.getenv("VIRTUAL_ENV", os.getenv("CONDA_PREFIX"))
             # We don't auto reuse conda's base env since it may cause breakage when removing packages.
-            if venv_in_env and not is_conda_base():
+            if not ignore_active_venv and venv_in_env and not is_conda_base():
                 python = PythonInfo.from_path(get_venv_python(Path(venv_in_env)))
                 if match_version(python):
                     note(
@@ -230,7 +237,7 @@ def note(message: str) -> None:
             # otherwise, get a venv associated with the project
             for _, venv in iter_venvs(self):
                 python = PythonInfo.from_path(venv.interpreter)
-                if match_version(python):
+                if match_version(python) and not (ignore_active_venv and is_active_venv(python)):
                     note(f"Virtualenv [success]{venv.root}[/] is reused.")
                     self.python = python
                     return python


Test patch:
diff --git a/tests/test_project.py b/tests/test_project.py
index 8a5db208c7..634a070c26 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -4,6 +4,7 @@
 import sys
 import venv
 from pathlib import Path
+from typing import TYPE_CHECKING
 
 import pytest
 from pytest_httpserver import HTTPServer
@@ -13,7 +14,11 @@
 from pdm.models.requirements import parse_requirement
 from pdm.models.specifiers import PySpecSet
 from pdm.models.venv import get_venv_python
-from pdm.utils import cd, parse_version
+from pdm.utils import cd, is_path_relative_to, parse_version
+
+if TYPE_CHECKING:
+    from pdm.project.core import Project
+    from pdm.pytest import PDMCallable
 
 
 def test_project_python_with_pyenv_support(project, mocker, monkeypatch):
@@ -400,3 +405,36 @@ def test_preserve_log_file(project, pdm, tmp_path, mocker):
     assert result.exit_code != 0
     install_log = next(tmp_path.joinpath("logs").glob("pdm-install-*.log"))
     assert install_log.exists()
+
+
+@pytest.mark.parametrize("use_venv", [True, False])
+def test_find_interpreters_with_PDM_IGNORE_ACTIVE_VENV(
+    pdm: PDMCallable,
+    project: Project,
+    monkeypatch: pytest.MonkeyPatch,
+    use_venv: bool,
+):
+    project._saved_python = None
+    project._python = None
+    project.project_config["python.use_venv"] = use_venv
+    venv.create(venv_path := project.root / "venv", symlinks=True)
+    monkeypatch.setenv("VIRTUAL_ENV", str(venv_path))
+    monkeypatch.setenv("PDM_IGNORE_ACTIVE_VENV", "1")
+
+    venv_python = get_venv_python(venv_path)
+    pythons = list(project.find_interpreters())
+
+    assert pythons, "PDM should find interpreters with PDM_IGNORE_ACTIVE_VENV"
+    # Test requires that some interpreters are available outside the venv
+    assert any(venv_python != p.executable for p in project.find_interpreters())
+    # No need to assert, exception raised if not found
+    interpreter = project.resolve_interpreter()
+    assert interpreter.executable != venv_python
+
+    if use_venv:
+        project.project_config["venv.in_project"] = True
+        pdm("install", strict=True, obj=project)
+        assert project._saved_python
+        python = Path(project._saved_python)
+        assert is_path_relative_to(python, project.root)
+        assert not is_path_relative_to(python, venv_path)