Repository: pypa/pipenv
Pull request #6373: Refactor virtualenv `bin/` / `Scripts/` path resolution using `sysconfig` mechanism
Author: login: ByteB4rb1e
Created: 2025-04-10T19:19:28Z | Merged: 2025-04-21T08:37:35Z

This pull request refactors the logic for determining the venv's `scripts/`. or `bin/` directory location by leveraging `sysconfig`’s built-in mechanism for retrieving the complete global install directory path of scripts and retrieving its basename. This removes any platform-dependent logic and does not require an additional dependency.

Changes:

    feat: Introduced a new approach for venv scripts path lookup, eliminating reliance on platform identifiers and improving compatibility with POSIX-like environments such as Cygwin and MinGW.

    refactor: Updated both project-wide and routine-specific implementations to use the virtualenv mechanism for dynamic script location lookup, streamlining execution.

These changes mitigate execution inconsistencies in non-native Windows environments and improve portability across platforms.

Fixes:  https://github.com/pypa/pipenv/issues/5978


Diff:
diff --git a/news/6737.bugfix.rst b/news/6737.bugfix.rst
new file mode 100644
index 0000000000..f17f622271
--- /dev/null
+++ b/news/6737.bugfix.rst
@@ -0,0 +1,22 @@
+# Improved virtualenv scripts path resolution
+
+## Summary
+
+This PR refactors the logic for determining virtual environment script paths
+by leveraging ``sysconfig``'s built-in mechanisms. By removing
+platform-dependent logic, ``pipenv`` now offers enhanced compatibility with
+POSIX-like environments, including Cygwin and MinGW. The fix also mitigates
+execution inconsistencies in non-native Windows environments, improving
+portability across platforms.
+
+## Motivation
+
+The original logic for determining the scripts path was unable to handle the
+deviations of MSYS2 MinGW CPython identifying as ``nt`` platform, yet using a
+POSIX ``{base}/bin`` path, instead of ``{base}/Scripts``.
+
+## Changes
+
+Removed custom logic for determining virtualenv scripts path in favor of
+retrieving the basename of the path string returned by
+``sysconfig.get_path('scripts')```.
diff --git a/pipenv/environment.py b/pipenv/environment.py
index 71009d507d..5b6964d38e 100644
--- a/pipenv/environment.py
+++ b/pipenv/environment.py
@@ -26,6 +26,7 @@
 from pipenv.utils.indexes import prepare_pip_source_args
 from pipenv.utils.processes import subprocess_run
 from pipenv.utils.shell import temp_environ
+from pipenv.utils.virtualenv import virtualenv_scripts_dir
 from pipenv.vendor.importlib_metadata.compat.py39 import normalized_name
 from pipenv.vendor.pythonfinder.utils import is_in_path
 
@@ -246,16 +247,12 @@ def script_basedir(self) -> str:
     @property
     def python(self) -> str:
         """Path to the environment python"""
-        if self._python is not None:
-            return self._python
-        if os.name == "nt" and not self.is_venv:
-            py = Path(self.prefix).joinpath("python").absolute().as_posix()
-        else:
-            py = Path(self.script_basedir).joinpath("python").absolute().as_posix()
-        if not py:
-            py = Path(sys.executable).as_posix()
-        self._python = py
-        return py
+        if self._python is None:
+            self._python = (
+                (virtualenv_scripts_dir(self.prefix) / "python").absolute().as_posix()
+            )
+
+        return self._python
 
     @cached_property
     def sys_path(self) -> list[str]:
diff --git a/pipenv/project.py b/pipenv/project.py
index a256b842f0..dd2c3c0421 100644
--- a/pipenv/project.py
+++ b/pipenv/project.py
@@ -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:
@@ -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
@@ -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:
@@ -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
@@ -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:
diff --git a/pipenv/routines/shell.py b/pipenv/routines/shell.py
index 268f079dd4..c33fe8e98a 100644
--- a/pipenv/routines/shell.py
+++ b/pipenv/routines/shell.py
@@ -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
 
 
@@ -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
 
@@ -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)
diff --git a/pipenv/utils/virtualenv.py b/pipenv/utils/virtualenv.py
index b5a9c6a633..c7be3f46b6 100644
--- a/pipenv/utils/virtualenv.py
+++ b/pipenv/utils/virtualenv.py
@@ -3,6 +3,7 @@
 import re
 import shutil
 import sys
+import sysconfig
 from pathlib import Path
 
 from pipenv import environments, exceptions
@@ -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():
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
index 337b1816c8..9788c474ef 100644
--- a/tests/unit/test_utils.py
+++ b/tests/unit/test_utils.py
@@ -1,10 +1,11 @@
 import os
+import sys
 from unittest import mock
 
 import pytest
 
 from pipenv.exceptions import PipenvUsageError
-from pipenv.utils import dependencies, indexes, internet, shell, toml
+from pipenv.utils import dependencies, indexes, internet, shell, toml, virtualenv
 
 # Pipfile format <-> requirements.txt format.
 DEP_PIP_PAIRS = [
@@ -547,3 +548,17 @@ def test_is_env_truthy_does_not_exisxt(self, monkeypatch):
         name = "ZZZ"
         monkeypatch.delenv(name, raising=False)
         assert shell.is_env_truthy(name) is False
+
+    @pytest.mark.utils
+    # substring search in version handles special-case of MSYS2 MinGW CPython
+    # https://github.com/msys2/MINGW-packages/blob/master/mingw-w64-python/0017-sysconfig-treat-MINGW-builds-as-POSIX-builds.patch#L24
+    @pytest.mark.skipif(os.name != "nt" or "GCC" in sys.version, reason="Windows test only")
+    def test_virtualenv_scripts_dir_nt(self):
+        """
+        """
+        assert str(virtualenv.virtualenv_scripts_dir('foobar')) == 'foobar\\Scripts'
+
+    @pytest.mark.utils
+    @pytest.mark.skipif(os.name == "nt" and "GCC" not in sys.version, reason="POSIX test only")
+    def test_virtualenv_scripts_dir_posix(self):
+        assert str(virtualenv.virtualenv_scripts_dir('foobar')) == 'foobar/bin'


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.


<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**pipenv/utils/virtualenv.py:16**
* Consider adding tests for the new virtualenv_scripts_dir function to ensure that script path resolution works correctly across different environments.
```
PYINFO_CURRENT = PythonInfo.current_system()
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T04:21:27Z
user: login: matteius
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-15T08:06:22Z
user: login: matteius
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T22:53:02Z
user: login: ByteB4rb1e
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T13:10:59Z
user: login: ByteB4rb1e
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-19T17:56:17Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR refactors virtualenv path resolution using sysconfig to dynamically determine the scripts directory (either bin/ or Scripts/) and improves cross-platform compatibility. Key changes include introducing the virtualenv_scripts_dir function, updating tests to cover both Windows and POSIX environments, and refactoring usages in routines and project properties.

### Reviewed Changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated no comments.

<details>
<summary>Show a summary per file</summary>

| File                              | Description                                                              |
| --------------------------------- | ------------------------------------------------------------------------ |
| tests/unit/test_utils.py          | Added new test cases for asserting expected platform-based path results. |
| pipenv/utils/virtualenv.py        | Introduced virtualenv_scripts_dir to determine the scripts directory.    |
| pipenv/routines/shell.py          | Updated shell invocation to use virtualenv_scripts_dir for constructing paths. |
| pipenv/project.py                 | Refactored virtualenv existence check and Finder path resolution.        |
| pipenv/environment.py             | Updated python property to derive path using virtualenv_scripts_dir.       |
</details>


<details>
<summary>Files not reviewed (1)</summary>

* **news/6737.bugfix.rst**: Language not supported
</details>

<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**pipenv/utils/virtualenv.py:17**
* [nitpick] Consider renaming the parameter 'b' to 'base_path' for improved readability and clarity.
```
def virtualenv_scripts_dir(b):
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-20T13:36:48Z
user: login: matteius
body: Thanks for your hard work on this change!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-21T08:37:26Z

Review comments:
diff_hunk: @@ -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.endswith('Scripts'):
path: pipenv/project.py
user: login: Copilot
body: Using .endswith on a pathlib.Path object may not work as intended. Consider using scripts_dir.name == 'Scripts' to check the directory name.
```suggestion
            if scripts_dir.name == 'Scripts':
```
created_at: 2025-04-14T04:21:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -248,14 +249,8 @@ def python(self) -> str:
         """Path to the environment python"""
         if self._python is not None:
             return self._python
-        if os.name == "nt" and not self.is_venv:
-            py = Path(self.prefix).joinpath("python").absolute().as_posix()
-        else:
-            py = Path(self.script_basedir).joinpath("python").absolute().as_posix()
-        if not py:
-            py = Path(sys.executable).as_posix()
-        self._python = py
-        return py
+
+        return (virtualenv_scripts_dir(self.prefix) / "python").absolute().as_posix()
path: pipenv/environment.py
user: login: matteius
body: I am actually surprised this doesn't break the `--system` flag tests because my understanding is this property would also be used for `--system` based installs which wouldn't be using a virtualenv.   Have you considered that angle at all or could do some testing of the `--system` flag on cli?
created_at: 2025-04-16T22:53:02Z
author_association: MEMBER
diff_hunk: @@ -248,14 +249,8 @@ def python(self) -> str:
         """Path to the environment python"""
         if self._python is not None:
             return self._python
-        if os.name == "nt" and not self.is_venv:
-            py = Path(self.prefix).joinpath("python").absolute().as_posix()
-        else:
-            py = Path(self.script_basedir).joinpath("python").absolute().as_posix()
-        if not py:
-            py = Path(sys.executable).as_posix()
-        self._python = py
-        return py
+
+        return (virtualenv_scripts_dir(self.prefix) / "python").absolute().as_posix()
path: pipenv/environment.py
user: login: ByteB4rb1e
body: https://github.com/pypa/pipenv/blob/29b4197693ad6d02bfe6c5b8034189ae345d35de/pipenv/environment.py#L247-L257

As far as I comprehended, the condition in line 255 would never be met, as the `joinpath('python')` in line 254 guarantees that `py` will never become a false-equivalent value, since its definition is in the else statement, hence catches the unmatched condition in line 251. That's why I disregarded it altogether.
I assumed `self.script_basedir` and `self.prefix` to be synonyms? They returned the same value in a Ubuntu VM. That's why I applied `virtualenv_scripts_dir` there and the `virtualenv_` prefix was just for namespacing sakes (didn't see any `as` import statements throughout, so thought this was unconventional to the project), the function accepts any base path. Should I move it elsewhere?

```python
@property
def python(self) -> str:
    """Path to the environment python"""
    if self._python is not None:
        return self._python

    return (virtualenv_scripts_dir(self.prefix) / "python").absolute().as_posix()
```

EDIT: Though it's a valid point that I should probably set `self._python` before returning. Is there maybe a test case missing for that?

I will see if I can execute with the `--system` flag in Python distribution for native Windows, but honestly didn't consider it. In MSYS2 Python package management is handled by `pacman` and on Ubuntu by `apt-get`/`aptitude`, so I assumed that any sort of global options wouldn't work anyway with my setup.
created_at: 2025-04-17T13:10:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -248,14 +249,8 @@ def python(self) -> str:
         """Path to the environment python"""
         if self._python is not None:
             return self._python
-        if os.name == "nt" and not self.is_venv:
-            py = Path(self.prefix).joinpath("python").absolute().as_posix()
-        else:
-            py = Path(self.script_basedir).joinpath("python").absolute().as_posix()
-        if not py:
-            py = Path(sys.executable).as_posix()
-        self._python = py
-        return py
+
+        return (virtualenv_scripts_dir(self.prefix) / "python").absolute().as_posix()
path: pipenv/environment.py
user: login: ByteB4rb1e
body: Rebased the commit, assigning to `self._python` now and also inverted the first condition as to not require an intermediate assignment to `py`. I couldn't provoke an error using the `--system` flag. Looking forward to the test suite's results.
created_at: 2025-04-19T17:56:17Z
author_association: CONTRIBUTOR

PR comments:
user: login: matteius
created_at: 2025-04-14T04:31:25Z
body: @ByteB4rb1e please `pip install pre-commit` and then run `pre-commit install` in the pipenv dir, and check the lint changes for this PR so the tests can run.
author_association: MEMBER
user: login: ByteB4rb1e
created_at: 2025-04-14T19:09:06Z
body: Oh, that's unfortunate. My bad, I didn't check the requirements and just assumed `virtualenv` to already be a dependency. I don't want to introduce a new dependency for these small changes. So I'm reconsidering my approach, instead focusing on the built-in `venv`, which was introduced in v3.3, so would pass the *Vendoring* test.

I've tested `venv` in CPython 3.9 locally and am running into a similar issue as `pipenv` currently does, only that the wrong directories are being created. The issue was seemingly [fixed in CPython v3.11](https://github.com/python/cpython/commit/48d926269963cfe7a49c0a4f34af4fe9b832399b), interestingly [`pipenv` Issue #5109 (closed)](https://github.com/pypa/pipenv/issues/5109) references the [linked issue report (CPython #89576)](https://github.com/python/cpython/issues/89576). I don't yet know what the implications of that are.

A quick workaround that comes to mind would be:

```
>>> import sysconfig
>>> from pathlib import Path
>>> Path(sysconfig.get_path('scripts')).name
'bin'
```

So I will see how the internals of that work and compare it to how `venv::__main__` does the bootstrapping.

EDIT: The "workaround" might actually be the most pragmatic and sensible approach as [the underlying mechanism is not a public API](https://github.com/python/cpython/blob/3.11/Lib/sysconfig.py#L621) and I can see a few pitfalls mocking it.

@matteius, @oz123, is this approach fine with you?
author_association: CONTRIBUTOR
user: login: matteius
created_at: 2025-04-15T08:07:26Z
body: @ByteB4rb1e Would be great if we can get a new test to cover this.  Also everything else looked good--please add a news fragment too when you get a chance.
author_association: MEMBER
user: login: ByteB4rb1e
created_at: 2025-04-15T08:34:47Z
body: @matteius, will do! It'll probably have to wait for a couple of days though as I have to take a brief detour. I'm currently setting up another proposal for refactoring the test suite to work locally without the `run-tests.sh` wrapper script as I currently need a VM and also had to adapt the script to work. Doing that I ran into the same path issues with the `pre-commit` package, so I will file an issue report and create a PR there first.
author_association: CONTRIBUTOR
user: login: ByteB4rb1e
created_at: 2025-04-15T21:47:37Z
body: [0017-sysconfig-treat-MINGW-builds-as-POSIX-builds.patch](https://github.com/msys2/MINGW-packages/blob/master/mingw-w64-python/0017-sysconfig-treat-MINGW-builds-as-POSIX-builds.patch) is the MSYS2 specific patch of [CPython's sysconfig](https://github.com/python/cpython/blob/main/Lib/sysconfig/__init__.py) I aim to have test coverage for, as this is the strongest deviation from conventional CPython distributions. The patch is indeed a little quirky, but doesn't provoke any unintended behavior for what we're achieving with this refactoring, as we're utilizing a higher-level facility. Actually quite the opposite: The refactoring ensures, that the logic stays intact, even for other unconventional CPython builds that may not exist yet. Just stumbled upon this to figure out what's best to mock...

Sadly though, there was a [patch](https://github.com/msys2/MINGW-packages/blob/master/mingw-w64-python/0023-sysconfig-mingw-sysconfig-like-posix.patch) introduced which requires special handling in the test case. I've filed a [bug report](https://github.com/msys2/MINGW-packages/issues/23992) and am hopeful that this can be resolved soon!

I will now add parameterization for other platform combinations, then lint, then add the news fragment, as requested by @matteius. 
author_association: CONTRIBUTOR
user: login: matteius
created_at: 2025-04-16T00:57:58Z
body: looks like the tests are failing for a variety of reasons depending on platform and version.
author_association: MEMBER
user: login: ByteB4rb1e
created_at: 2025-04-16T10:00:33Z
body: Ok, wow... I didn't expect it to be this construed. What I'm taking from this is, while the `sysconfig::get_path` interface is constant, each distributor has different ways of patching things further down along the call tree. I've tested it locally with the embedded python distribution for Windows, MSYS2 MinGW distribution and Ubuntu distribution and they all passed.

It would be possible to define separate test cases for each platform and just ignore non-applicable ones, but that doesn't sit quite right with me as this wouldn't address testing any future deviations by distributors.

EDIT: Ok, half the test failures in regards to `_winapi` are plausible to me and can be resolved quickly, which actually do require the separation of test cases between Windows and POSIX. The macOS specific failures though...

I think I will push through with this, trying to analyze the deviations and make the test case work as I intended. It will take some time though as I first have to identify the distributions in use by the CI test and come up with a faster testing setup for debugging.

Bear with me, please. I'm hoping to get it done before the next release.
author_association: CONTRIBUTOR
user: login: matteius
created_at: 2025-04-16T14:30:31Z
body: > which actually do require the separation of test cases

We have this pattern with `pytest` markers, so you can specific which OS a test should run on.   That being said, as you pointed out, I've run into it too--the vendor implementations of sysconfig are always different, so relying on private `_` methods seems a bit risky.
author_association: MEMBER
user: login: ByteB4rb1e
created_at: 2025-04-16T16:14:45Z
body: You're right. I got carried away. I'm just trying to grasp the history of how this issue arose in the first place. But this is the wrong circumstance to do that. Mocking private API functions is a testament to that. Pardon...
There are now two separate test cases, authored like all the others, with the exception that I've added a secondary condition for skipping the test, so that they properly execute within MinGW CPython as well.

I'll create the news fragment now and then make sure the changes actually fixed the original problem.
author_association: CONTRIBUTOR
user: login: ByteB4rb1e
created_at: 2025-04-16T18:43:32Z
body: I've tested all interactive commands manually, they're working fine. In addition, fixed another fault during the uninstall routine, and fixed the faulty path in the Windows test case. All is seemingly working now. Don't know though why ruff didn't trigger locally...

```
$> cat /proc/version
MSYS_NT-10.0-26100 version 3.5.7-2644508f.x86_64 (runneradmin@fv-az1853-986) (gcc version 13.3.0 (GCC) ) 2025-01-30 09:08 UTC
$> which python3 && python3 -V && pacman -Q mingw-w64-x86_64-python
/mingw64/bin/python3
Python 3.12.9
mingw-w64-x86_64-python 3.12.9-1
$> test -d .venv || echo $?
1
$> python3 -m venv .venv
$> .venv/bin/pip install git+https://github.com/ByteB4rb1e/pipenv.git@bugfix/5978 2>&1 | tee pip-install-pipenv.txt
...
Successfully installed certifi-2025.1.31 distlib-0.3.9 filelock-3.18.0 packaging-24.2 pipenv-2024.4.1 platformdirs-4.3.7 setuptools-78.1.0 virtualenv-20.30.0

[notice] A new release of pip is available: 24.3.1 -> 25.0.1
[notice] To update, run: C:/Users/me/Repositories/local/pipenv-debug/.venv/bin/python3.exe -m pip install --upgrade pip
$> .venv/bin/pipenv install cowsay && .venv/bin/cowsay -t works
To activate this project's virtualenv, run pipenv shell.
Alternatively, run a command inside the virtualenv with pipenv run.
Installing cowsay...
Installation Succeeded
To activate this project's virtualenv, run pipenv shell.
Alternatively, run a command inside the virtualenv with pipenv run.
Installing dependencies from Pipfile.lock (b54bd8)...
All dependencies are now up-to-date!
Upgrading cowsay in  dependencies.
Building requirements...
Resolving dependencies...
Success!
Building requirements...
Resolving dependencies...
Success!
To activate this project's virtualenv, run pipenv shell.
Alternatively, run a command inside the virtualenv with pipenv run.
Installing dependencies from Pipfile.lock (c93707)...
All dependencies are now up-to-date!
Installing dependencies from Pipfile.lock (c93707)...
  _____
| works |
  =====
     \
      \
        ^__^
        (oo)\_______
        (__)\       )\/\
            ||----w |
            ||     ||
$> .venv/bin/pipenv uninstall cowsay && .venv/bin/cowsay || echo 'removed'
Removed cowsay from Pipfile.
Uninstalling cowsay...
Found existing installation: cowsay 6.1
Uninstalling cowsay-6.1:
  Successfully uninstalled cowsay-6.1

-bash: .venv/bin/cowsay: No such file or directory
removed
```

[pip-install-pipenv.txt](https://github.com/user-attachments/files/19782790/pip-install-pipenv.txt)

author_association: CONTRIBUTOR