Repository: pypa/pipx (python)

Issue:
pipx crashes when a file named logging.py is in the current directory
<!--
Thank you for filing a bug! Please feel free to answer as much or as little of this template as you can.

Please check pipx's Troubleshooting page to see if any of those solutions help solve your issue:
https://pypa.github.io/pipx/troubleshooting/
-->

**Describe the bug**
<!-- Please be as detailed as possible! -->
When a file called `logging.py` exists in the directory that `pipx` is invoked from, `pipx` crashes.

**How to reproduce**
<!-- If possible, include output of `pipx --verbose ...` -->
On my Ubuntu 20.04 LTS machine:

```sh
cd $(mktemp -d)
pipx --version
```

```
1.2.0
```

Create a `logging.py` file:

```sh
cat > logging.py
print('hello world')
^d
```

Run pipx:

```sh
pipx --version
```

"hello world" appears, then a crash:

```
hello world
Traceback (most recent call last):
  File "/home/james/.local/bin/pipx", line 5, in <module>
    from pipx.main import cli
  File "/home/james/.local/lib/python3.8/site-packages/pipx/main.py", line 7, in <module>
    import logging.config
ModuleNotFoundError: No module named 'logging.config'; 'logging' is not a package
Error in sys.excepthook:
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/apport_python_hook.py", line 72, in apport_excepthook
    from apport.fileutils import likely_packaged, get_recent_crashes
  File "/usr/lib/python3/dist-packages/apport/__init__.py", line 5, in <module>
    from apport.report import Report
  File "/usr/lib/python3/dist-packages/apport/report.py", line 32, in <module>
    import apport.fileutils
  File "/usr/lib/python3/dist-packages/apport/fileutils.py", line 12, in <module>
    import os, glob, subprocess, os.path, time, pwd, sys, requests_unixsocket
  File "/usr/lib/python3/dist-packages/requests_unixsocket/__init__.py", line 1, in <module>
    import requests
  File "/usr/lib/python3/dist-packages/requests/__init__.py", line 43, in <module>
    import urllib3
  File "/usr/lib/python3/dist-packages/urllib3/__init__.py", line 7, in <module>
    from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool, connection_from_url
  File "/usr/lib/python3/dist-packages/urllib3/connectionpool.py", line 29, in <module>
    from .connection import (
  File "/usr/lib/python3/dist-packages/urllib3/connection.py", line 41, in <module>
    from .util.ssl_ import (
  File "/usr/lib/python3/dist-packages/urllib3/util/__init__.py", line 20, in <module>
    from .retry import Retry
  File "/usr/lib/python3/dist-packages/urllib3/util/retry.py", line 20, in <module>
    log = logging.getLogger(__name__)
AttributeError: module 'logging' has no attribute 'getLogger'

Original exception was:
Traceback (most recent call last):
  File "/home/james/.local/bin/pipx", line 5, in <module>
    from pipx.main import cli
  File "/home/james/.local/lib/python3.8/site-packages/pipx/main.py", line 7, in <module>
    import logging.config
ModuleNotFoundError: No module named 'logging.config'; 'logging' is not a package
```

**Expected behavior**
<!-- What should have happened? -->

pipx is not be affected by the presence of a `logging.py` file in the current directory - it shows the current version and does not crash.

**Checking on docker with py3.12**

I was concerned that this failure might be a consequence of my shaky Ubuntu install in the global environment, so I reproduced it in a docker Python 3.12 image. It's a bit hacky because I've run it as root, but I'm able to get the same crash when trying to install `tox`:

```sh
docker run -it python:3.12 /bin/bash
```

```
root@8facc5020f37:/# python3 -m pip install pipx --user
Collecting pipx
  Downloading pipx-1.2.0-py3-none-any.whl (57 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 57.8/57.8 kB 2.6 MB/s eta 0:00:00
Collecting argcomplete>=1.9.4 (from pipx)
  Obtaining dependency information for argcomplete>=1.9.4 from https://files.pythonhosted.org/packages/1e/05/223116a4a5905d6b26bff334ffc7b74474fafe23fcb10532caf0ef86ca69/argcomplete-3.1.2-py3-none-any.whl.metadata
  Downloading argcomplete-3.1.2-py3-none-any.whl.metadata (16 kB)
Collecting packaging>=20.0 (from pipx)
  Obtaining dependency information for packaging>=20.0 from https://files.pythonhosted.org/packages/ec/1a/610693ac4ee14fcdf2d9bf3c493370e4f2ef7ae2e19217d7a237ff42367d/packaging-23.2-py3-none-any.whl.metadata
  Downloading packaging-23.2-py3-none-any.whl.metadata (3.2 kB)
Collecting userpath>=1.6.0 (from pipx)
  Obtaining dependency information for userpath>=1.6.0 from https://files.pythonhosted.org/packages/a2/28/20c7dcdb12681b2e12224184a8a158e5df51feb0c68116cb4b1b991a4aab/userpath-1.9.1-py3-none-any.whl.metadata
  Downloading userpath-1.9.1-py3-none-any.whl.metadata (3.0 kB)
Collecting click (from userpath>=1.6.0->pipx)
  Obtaining dependency information for click from https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl.metadata
  Downloading click-8.1.7-py3-none-any.whl.metadata (3.0 kB)
Downloading argcomplete-3.1.2-py3-none-any.whl (41 kB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 41.5/41.5 kB 10.1 MB/s eta 0:00:00
Downloading packaging-23.2-py3-none-any.whl (53 kB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 53.0/53.0 kB 7.0 MB/s eta 0:00:00
Downloading userpath-1.9.1-py3-none-any.whl (9.1 kB)
Downloading click-8.1.7-py3-none-any.whl (97 kB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 97.9/97.9 kB 6.4 MB/s eta 0:00:00
Installing collected packages: packaging, click, argcomplete, userpath, pipx
  WARNING: The script userpath is installed in '/root/.local/bin' which is not on PATH.
  Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.
  WARNING: The script pipx is installed in '/root/.local/bin' which is not on PATH.
  Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.
Successfully installed argcomplete-3.1.2 click-8.1.7 packaging-23.2 pipx-1.2.0 userpath-1.9.1
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv

[notice] A new release of pip is available: 23.2.1 -> 23.3
[notice] To update, run: pip install --upgrade pip
root@8facc5020f37:/# /root/.local/bin/pipx --version
1.2.0
root@8facc5020f37:/# cat > logging.py
print('hello world')
root@8facc5020f37:/# /root/.local/bin/pipx --version
1.2.0
root@8facc5020f37:/# /root/.local/bin/pipx install tox
hello world
Traceback (most recent call last):
  File "<frozen runpy>", line 189, in _run_module_as_main
  File "<frozen runpy>", line 148, in _get_module_details
  File "<frozen runpy>", line 112, in _get_module_details
  File "/usr/local/lib/python3.12/venv/__init__.py", line 17, in <module>
    logger = logging.getLogger(__name__)
             ^^^^^^^^^^^^^^^^^
AttributeError: module 'logging' has no attribute 'getLogger'

'/usr/local/bin/python3 -m venv --without-pip /root/.local/pipx/venvs/tox' failed
```

I'm probably getting something wrong with "ensurepath" or there's something going on that I'm missing - but hopefully this is helpful 🙏🏻 

I spotted this when I tried to view a Markdown file with [frogmouth](https://github.com/Textualize/frogmouth/) in a project that contained a `logging.py` file and the frogmouth package kept crashing, so this might affect installed packages too.


Pull request description:
Run subprocesses in isolated directory
<!-- add an 'x' in the brackets below -->

- [x] I have added a news fragment under `changelog.d/` (if the patch affects the end users)

## Summary of changes

Redux PR for #1127. Will hopefully finally close #1091.

## Test plan

<!-- provide evidence of testing, preferably with command(s) that can be copy+pasted by others -->

Tested by running

```
touch argparse.py
python -m pipx install frogmouth
```


Patch:
diff --git a/changelog.d/1091.bugfix.md b/changelog.d/1091.bugfix.md
new file mode 100644
index 0000000..67f22bc
--- /dev/null
+++ b/changelog.d/1091.bugfix.md
@@ -0,0 +1,1 @@
+Fix installation issues when files in the working directory interfere with venv creation process.
diff --git a/src/pipx/shared_libs.py b/src/pipx/shared_libs.py
index 9429b01..eaa4920 100644
--- a/src/pipx/shared_libs.py
+++ b/src/pipx/shared_libs.py
@@ -43,7 +43,9 @@ class _SharedLibs:
     def create(self, verbose: bool = False) -> None:
         if not self.is_valid:
             with animate("creating shared libraries", not verbose):
-                create_process = run_subprocess([DEFAULT_PYTHON, "-m", "venv", "--clear", self.root])
+                create_process = run_subprocess(
+                    [DEFAULT_PYTHON, "-m", "venv", "--clear", self.root], run_dir=str(self.root)
+                )
             subprocess_post_check(create_process)
 
             # ignore installed packages to ensure no unexpected patches from the OS vendor
diff --git a/src/pipx/util.py b/src/pipx/util.py
index fad47c8..8a498e8 100644
--- a/src/pipx/util.py
+++ b/src/pipx/util.py
@@ -158,6 +158,7 @@ def run_subprocess(
     log_cmd_str: Optional[str] = None,
     log_stdout: bool = True,
     log_stderr: bool = True,
+    run_dir: Optional[str] = None,
 ) -> "subprocess.CompletedProcess[str]":
     """Run arbitrary command as subprocess, capturing stderr and stout"""
     env = dict(os.environ)
@@ -166,6 +167,8 @@ def run_subprocess(
     if log_cmd_str is None:
         log_cmd_str = " ".join(str(c) for c in cmd)
     logger.info(f"running {log_cmd_str}")
+    if run_dir:
+        os.makedirs(run_dir, exist_ok=True)
     # windows cannot take Path objects, only strings
     cmd_str_list = [str(c) for c in cmd]
     # Make sure to call the binary using its real path. This matters especially on Windows when using the packaged app
@@ -176,6 +179,9 @@ def run_subprocess(
     # symlink in argv[0] so that it can locate the venv.
     if not os.path.islink(cmd_str_list[0]) and WINDOWS:
         cmd_str_list[0] = os.path.realpath(cmd_str_list[0])
+
+    # TODO: Switch to using `-P` / PYTHONSAFEPATH instead of running in
+    # separate directory in Python 3.11
     completed_process = subprocess.run(
         cmd_str_list,
         env=env,
@@ -184,6 +190,7 @@ def run_subprocess(
         encoding="utf-8",
         universal_newlines=True,
         check=False,
+        cwd=run_dir,
     )
 
     if capture_stdout and log_stdout:
diff --git a/src/pipx/venv.py b/src/pipx/venv.py
index fc33016..96de4e3 100644
--- a/src/pipx/venv.py
+++ b/src/pipx/venv.py
@@ -157,7 +157,7 @@ class Venv:
             cmd = [self.python, "-m", "venv"]
             if not override_shared:
                 cmd.append("--without-pip")
-            venv_process = run_subprocess(cmd + venv_args + [str(self.root)])
+            venv_process = run_subprocess(cmd + venv_args + [str(self.root)], run_dir=str(self.root))
         subprocess_post_check(venv_process)
 
         shared_libs.create(self.verbose)
@@ -248,7 +248,7 @@ class Venv:
             ]
             # no logging because any errors will be specially logged by
             #   subprocess_post_check_handle_pip_error()
-            pip_process = run_subprocess(cmd, log_stdout=False, log_stderr=False)
+            pip_process = run_subprocess(cmd, log_stdout=False, log_stderr=False, run_dir=str(self.root))
         subprocess_post_check_handle_pip_error(pip_process)
         if pip_process.returncode:
             raise PipxError(f"Error installing {full_package_description(package_name, package_or_url)}.")
@@ -292,7 +292,7 @@ class Venv:
             ]
             # no logging because any errors will be specially logged by
             #   subprocess_post_check_handle_pip_error()
-            pip_process = run_subprocess(cmd, log_stdout=False, log_stderr=False)
+            pip_process = run_subprocess(cmd, log_stdout=False, log_stderr=False, run_dir=str(self.root))
         subprocess_post_check_handle_pip_error(pip_process)
         if pip_process.returncode:
             raise PipxError(f"Error installing {', '.join(requirements)}.")
@@ -454,7 +454,7 @@ class Venv:
         cmd = [str(self.python_path), "-m", "pip"] + cmd
         if not self.verbose:
             cmd.append("-q")
-        return run_subprocess(cmd)
+        return run_subprocess(cmd, run_dir=str(self.root))
 
     def run_pip_get_exit_code(self, cmd: List[str]) -> ExitCode:
         cmd = [str(self.python_path), "-m", "pip"] + cmd


Test patch:
diff --git a/tests/test_install.py b/tests/test_install.py
index b814b73..23cfc52 100644
--- a/tests/test_install.py
+++ b/tests/test_install.py
@@ -337,3 +337,11 @@ def test_passed_python_and_force_flag_warning(pipx_temp_env, capsys):
     assert not run_pipx_cli(["install", "pycowsay", "--force"])
     captured = capsys.readouterr()
     assert "--python is ignored when --force is passed." not in captured.out
+
+
+def test_install_run_in_separate_directory(caplog, capsys, pipx_temp_env, monkeypatch, tmp_path):
+    monkeypatch.chdir(tmp_path)
+    f = Path("argparse.py")
+    f.touch()
+
+    install_packages(capsys, pipx_temp_env, caplog, ["pycowsay"], ["pycowsay"])