Repository: pdm-project/pdm

Issue:
pdm venv activate: ShellDetectionFailure
- [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
```zsh
$ pdm venv create
[...]
$ pdm venv activate in-project
[ShellDetectionFailure]: 
Add '-v' to see the detailed traceback
```
I am not certain what environment steps are required to get a ShellDetectionFailure. I'm running zsh (v5.4.2) inside VSCode.

## Actual behavior
When shellingham fails to detect a shell, the `pdm venv activate` command fails.

## Expected behavior
`pdm venv activate` should try a plausible default (e.g. `source .venv/bin/activate`) instead of failing. From the Shellingham docs:
> Shellingham raises ShellDetectionFailure if there is no shell to detect, but your application should almost never do this to your user.

## Environment Information

```bash
PDM version:
  2.8.2
Python Interpreter:
  /home/philip.garrison/.pyenv/versions/3.8.13/bin/python3.8 (3.8)
Project Root:
  /home/philip.garrison/workspace/aics/nuc-morph-analysis
Local Packages:
  /home/philip.garrison/workspace/aics/nuc-morph-analysis/__pypackages__/3.8
{
  "implementation_name": "cpython",
  "implementation_version": "3.8.13",
  "os_name": "posix",
  "platform_machine": "x86_64",
  "platform_release": "4.15.0-210-generic",
  "platform_system": "Linux",
  "platform_version": "#221-Ubuntu SMP Tue Apr 18 08:32:52 UTC 2023",
  "python_full_version": "3.8.13",
  "platform_python_implementation": "CPython",
  "python_version": "3.8",
  "sys_platform": "linux"
}
```


Discussion:

Patch:
diff --git a/news/2182.bugfix.md b/news/2182.bugfix.md
new file mode 100644
index 00000000..7ef16f94
--- /dev/null
+++ b/news/2182.bugfix.md
@@ -0,0 +1,1 @@
+Makes `comarable_version("1.2.3+local1") == Version("1.2.3")`.
\ No newline at end of file
diff --git a/news/2187.bugfix.md b/news/2187.bugfix.md
new file mode 100644
index 00000000..137b3605
--- /dev/null
+++ b/news/2187.bugfix.md
@@ -0,0 +1,1 @@
+Default behavior for pdm venv activate when shell detection fails.
diff --git a/src/pdm/cli/commands/venv/activate.py b/src/pdm/cli/commands/venv/activate.py
index e1fb176f..3d04c593 100644
--- a/src/pdm/cli/commands/venv/activate.py
+++ b/src/pdm/cli/commands/venv/activate.py
@@ -45,7 +45,10 @@ class ActivateCommand(BaseCommand):
         project.core.ui.echo(self.get_activate_command(venv))
 
     def get_activate_command(self, venv: VirtualEnv) -> str:  # pragma: no cover
-        shell, _ = shellingham.detect_shell()
+        try:
+            shell, _ = shellingham.detect_shell()
+        except shellingham.ShellDetectionFailure:
+            shell = None
         if shell == "fish":
             command, filename = "source", "activate.fish"
         elif shell == "csh":
diff --git a/src/pdm/utils.py b/src/pdm/utils.py
index 72e4a60f..f903a461 100644
--- a/src/pdm/utils.py
+++ b/src/pdm/utils.py
@@ -19,7 +19,7 @@ import warnings
 from pathlib import Path
 from typing import TYPE_CHECKING
 
-from packaging.version import Version
+from packaging.version import Version, _cmpkey
 
 from pdm.compat import importlib_metadata
 
@@ -344,6 +344,17 @@ def comparable_version(version: str) -> Version:
     if parsed.local is not None:
         # strip the local part
         parsed._version = parsed._version._replace(local=None)
+
+        # To make comparable_version("1.2.3+local1") == Version("1.2.3")
+        parsed._key = _cmpkey(
+            parsed._version.epoch,
+            parsed._version.release,
+            parsed._version.pre,
+            parsed._version.post,
+            parsed._version.dev,
+            parsed._version.local,
+        )
+
     return parsed
 
 


Test patch:
diff --git a/tests/cli/test_venv.py b/tests/cli/test_venv.py
index 49139194..5d11a8d7 100644
--- a/tests/cli/test_venv.py
+++ b/tests/cli/test_venv.py
@@ -5,6 +5,7 @@ import sys
 from unittest.mock import ANY
 
 import pytest
+import shellingham
 
 from pdm.cli.commands.venv import backends
 from pdm.cli.commands.venv.utils import get_venv_prefix
@@ -168,6 +169,26 @@ def test_venv_activate_error(pdm, project):
     assert "Can't activate a non-venv Python" in result.stderr
 
 
+@pytest.mark.usefixtures("venv_backends")
+def test_venv_activate_no_shell(pdm, mocker, project):
+    project.project_config["venv.in_project"] = False
+    result = pdm(["venv", "create"], obj=project)
+    assert result.exit_code == 0, result.stderr
+    venv_path = re.match(r"Virtualenv (.+) is created successfully", result.output).group(1)
+    key = os.path.basename(venv_path)[len(get_venv_prefix(project)) :]
+
+    mocker.patch("shellingham.detect_shell", side_effect=shellingham.ShellDetectionFailure())
+    result = pdm(["venv", "activate", key], obj=project)
+    assert result.exit_code == 0, result.stderr
+    backend = project.config["venv.backend"]
+
+    if backend == "conda":
+        assert result.output.startswith("conda activate")
+    else:
+        assert result.output.strip("'\"\n").endswith("activate")
+        assert result.output.startswith("source")
+
+
 @pytest.mark.usefixtures("fake_create")
 @pytest.mark.parametrize("keep_pypackages", [True, False])
 def test_venv_auto_create(pdm, mocker, project, keep_pypackages):
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 218a3205..86976e26 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -3,6 +3,7 @@ import sys
 
 import pytest
 import tomlkit
+from packaging.version import Version
 
 from pdm import utils
 from pdm.cli import utils as cli_utils
@@ -145,3 +146,8 @@ def test_deprecation_warning():
 
     with pytest.raises(FutureWarning):
         utils.deprecation_warning("Test warning", raise_since="0.0")
+
+
+def test_comparable_version():
+    assert utils.comparable_version("1.2.3") == Version("1.2.3")
+    assert utils.comparable_version("1.2.3a1+local1") == Version("1.2.3a1")