Repository: pypa/pipx
Pull request #972 (2023-04-23 18:06:54)

📝 Update installation method for linux users
<!-- add an 'x' in the brackets below -->
* [ ] I have added an entry to `docs/changelog.md`

## Summary of changes

## Test plan
<!-- provide evidence of testing, preferably with command(s) that can be copy+pasted by others -->
Tested by running
```
# command(s) to exercise these changes
```


chrysle: Gentle ping @oviniciusfeitosa 

Patch:
diff --git a/.github/workflows/publish_docs.yml b/.github/workflows/publish_docs.yml
deleted file mode 100644
index ba85acd..0000000
--- a/.github/workflows/publish_docs.yml
+++ /dev/null
@@ -1,32 +0,0 @@
-name: Publish docs via GitHub Pages
-on:
-  workflow_dispatch:
-  release:
-    types: [published]
-
-env:
-  default-python: "3.11"
-
-jobs:
-  build:
-    name: Deploy docs
-    runs-on: ubuntu-latest
-    steps:
-      - name: Checkout main
-        uses: actions/checkout@v3
-      - name: Set up Python ${{ env.default-python }}
-        uses: actions/setup-python@v4
-        with:
-          python-version: ${{ env.default-python }}
-      - name: Install nox
-        run: |
-          python -m pip install --upgrade pip
-          pip install nox
-      - name: Build Docs
-        run: |
-          nox --error-on-missing-interpreters --non-interactive --session build_docs
-      - name: Deploy docs
-        if: ${{ success() }}
-        uses: mhausenblas/mkdocs-deploy-gh-pages@1.26
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
index fdeef22..8233c4d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,7 @@ dist
 activate
 __pypackages__
 venv
+.venv
 .DS_Store
 .tox
 __pycache__
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 5929e97..649d6a5 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -6,18 +6,18 @@
 
 repos:
 - repo: https://github.com/pre-commit/pre-commit-hooks
-  rev: v4.4.0
+  rev: v4.5.0
   hooks:
   - id: end-of-file-fixer
   - id: check-added-large-files
   - id: trailing-whitespace
   - id: check-yaml
-- repo: https://github.com/charliermarsh/ruff-pre-commit
-  rev: v0.0.254
+- repo: https://github.com/astral-sh/ruff-pre-commit
+  rev: v0.1.3
   hooks:
     - id: ruff
 - repo: https://github.com/psf/black
-  rev: 23.1.0
+  rev: 23.10.1
   hooks:
   - id: black
 # mypy args:
@@ -27,8 +27,8 @@ repos:
 #   cannot use --warn-unused-ignores because it conflicts with
 #     --ignore-missing-imports
 - repo: https://github.com/pre-commit/mirrors-mypy
-  rev: v1.1.1
+  rev: v1.6.1
   hooks:
   - id: mypy
-    args: ['--ignore-missing-imports', '--strict-equality','--no-implicit-optional']
+    args: ['--warn-unused-ignores', '--strict-equality','--no-implicit-optional']
     exclude: 'testdata/test_package_specifier/local_extras/setup.py'
diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml
new file mode 100644
index 0000000..6407f36
--- /dev/null
+++ b/.pre-commit-hooks.yaml
@@ -0,0 +1,6 @@
+-   id: pipx
+    name: pipx
+    entry: pipx run
+    require_serial: true
+    language: python
+    minimum_pre_commit_version: '2.9.2'
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 0000000..3930655
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,11 @@
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+
+version: 2
+
+build:
+  os: ubuntu-22.04
+  tools:
+    python: "3.11"
+  commands:
+  - pip install nox
+  - nox --session build_docs -- "${READTHEDOCS_OUTPUT}"/html
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5ccf749..9ca9c78 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,31 @@
 ## dev
 
+- Support PEP 723 run requirements in `pipx run`.
+- Imply `--include-apps` when running `pipx inject --include-deps`
+- Add `--with-suffix` for `pipx inject` command
+- `pipx install`: emit a warning when `--force` and `--python` were passed at the same time
+- Drop support for Python 3.7
+- Make usage message in `pipx run` show `package_or_url`, so extra will be printed out as well
+- Add `--force-reinstall` to pip arguments when `--force` was passed
+- Use the py launcher, if available, to select Python version with the `--python` option
 - Support including requirements in scripts run using `pipx run` (#916)
 - Pass `pip_args` to `shared_libs.upgrade()`
 - Fallback to user's log path if the default log path (`$PIPX_HOME/logs`) is not writable to aid with pipx being used for multi-user (e.g. system-wide) installs of applications
 - Fix wrong interpreter usage when injecting local pip-installable dependencies into venvs
+- add pre-commit hook support
+- Don't show escaped backslashes for paths in console output
+- Move `pipx` paths to ensure compatibility with the platform-specific user directories
+- [docs] Add more examples for `pipx run`
+- [docs] Add subsection to make README easier to read
+- Add `pipx install --preinstall` to support preinstalling build requirements
+- Pass `--no-input` to pip when output is not piped to parent stdout
+- Fix program name in generated manual page
+- Print all environment variables in `pipx environment`
+- Return an error message when directory can't be added to PATH successfully
+
+## 1.2.1
+
+- Fix compatibility to packaging 23.2+ by removing reliance on packaging's requirement validation logic and detecting a URL-based requirement in pipx. (#1070)
 
 ## 1.2.0
 
@@ -13,7 +35,6 @@
 - [docs] Add an example for installation from source with extras
 - Match pip's behaviour when package name ends with archive extension (treat it as a path)
 - Ship a [zipapp](https://docs.python.org/3/library/zipapp.html) of pipx
-
 - Change the program name to `path/to/python -m pipx` when running as `python -m pipx`
 - Improve the detection logic for MSYS2 to avoid entering infinite loop (#908) (#938)
 - Remove extra trailing quote from exception message
diff --git a/README.md b/README.md
index 0872eae..cd28d02 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@
 
 <p align="center">
 <a href="https://github.com/pypa/pipx/actions">
-<img src="https://github.com/pypa/pipx/workflows/tests/badge.svg?branch=main" alt="image" /></a> <a href="https://badge.fury.io/py/pipx"><img src="https://badge.fury.io/py/pipx.svg" alt="PyPI version"></a> <a href="https://badge.fury.io/py/pipx"><img src="https://pepy.tech/badge/pipx"></a>
+<img src="https://github.com/pypa/pipx/workflows/tests/badge.svg?branch=main" alt="image" /></a> <a href="https://badge.fury.io/py/pipx"><img src="https://badge.fury.io/py/pipx.svg" alt="PyPI version"></a> <a href="https://badge.fury.io/py/pipx"><img src="https://static.pepy.tech/badge/pipx"></a>
 
 </p>
 
@@ -26,6 +26,12 @@ _For comparison to other tools including pipsi, see [Comparison to Other Tools](
 
 ## Install pipx
 
+> [!NOTE]
+> It is not recommended to install `pipx` via `pipx`. If you'd like
+> to do this anyway, take a look at the
+> [`pipx-in-pipx`](https://github.com/mattsb42-meta/pipx-in-pipx) project and
+> read about the limitations there.
+
 ### On macOS
 
 ```
@@ -35,7 +41,17 @@ pipx ensurepath
 
 Upgrade pipx with `brew update && brew upgrade pipx`.
 
-### On Linux, install via pip (requires pip 19.0 or later)
+### On Linux
+
+-  Ubuntu 23.04 or above
+
+```
+sudo apt update
+sudo apt install pipx
+pipx ensurepath
+```
+
+-  Ubuntu 22.04 or below
 
 ```
 python3 -m pip install --user pipx
@@ -47,8 +63,8 @@ Upgrade pipx with `python3 -m pip install --user --upgrade pipx`.
 ### On Windows, install via pip (requires pip 19.0 or later)
 
 ```
-# If you installed python using the app-store, replace `python` with `python3` in the next line.
-python -m pip install --user pipx
+# If you installed python using Microsoft Store, replace `py` with `python3` in the next line.
+py -m pip install --user pipx
 ```
 
 It is possible (even most likely) the above finishes with a WARNING looking similar to this:
@@ -61,15 +77,15 @@ If so, go to the mentioned folder, allowing you to run the pipx executable direc
 Enter the following line (even if you did not get the warning):
 
 ```
-pipx ensurepath
+.\pipx.exe ensurepath
 ```
 
 This will add both the above mentioned path and the `%USERPROFILE%\.local\bin` folder to your search path.
 Restart your terminal session and verify `pipx` does run.
 
-Upgrade pipx with `python3 -m pip install --user --upgrade pipx`.
+Upgrade pipx with `py -m pip install --user --upgrade pipx`.
 
-### Via zipapp
+### Using pipx without installing (via zipapp)
 
 You can also use pipx without installing it.
 The zipapp can be downloaded from [Github releases](https://github.com/pypa/pipx/releases) and you can invoke it with a Python 3.7+ interpreter:
@@ -78,6 +94,10 @@ The zipapp can be downloaded from [Github releases](https://github.com/pypa/pipx
 python pipx.pyz ensurepath
 ```
 
+### Use with pre-commit
+
+pipx [has pre-commit support](installation.md#pre-commit).
+
 ### Shell completions
 
 Shell completions are available by following the instructions printed with this command:
@@ -141,7 +161,7 @@ done! ✨ 🌟 ✨
 
 
 >> pipx list
-venvs are in /home/user/.local/pipx/venvs
+venvs are in /home/user/.local/share/pipx/venvs
 apps are exposed on your $PATH at /home/user/.local/bin
    package pycowsay 2.0.3, Python 3.7.3
     - pycowsay
@@ -234,6 +254,8 @@ Any arguments after the application name will be passed directly to the applicat
 
 ```
 
+### Ambiguous arguments
+
 Sometimes pipx can consume arguments provided for the application:
 
 ```
@@ -265,17 +287,66 @@ To prevent this put double dash `--` before APP. It will make pipx to forward th
 
 Re-running the same app is quick because pipx caches Virtual Environments on a per-app basis. The caches only last a few days, and when they expire, pipx will again use the latest version of the package. This way you can be sure you're always running a new version of the package without having to manually upgrade.
 
-If the app name does not match that package name, you can use the `--spec` argument to specify the package to install and app to run separately:
+### Package with multiple apps, or the app name doesn't match the package name
+
+If the app name does not match the package name, you can use the `--spec` argument to specify the `PACKAGE` name, and provide the `APP` to run separately:
 
 ```
 pipx run --spec PACKAGE APP
 ```
 
-You can also specify specific versions, version ranges, or extras:
+For example, the [esptool](https://github.com/espressif/esptool) package doesn't provide an executable with the same name:
 
 ```
-pipx run APP==1.0.0
+>> pipx run esptool
+'esptool' executable script not found in package 'esptool'.
+Available executable scripts:
+    esp_rfc2217_server.py - usage: 'pipx run --spec esptool esp_rfc2217_server.py [arguments?]'
+    espefuse.py - usage: 'pipx run --spec esptool espefuse.py [arguments?]'
+    espsecure.py - usage: 'pipx run --spec esptool espsecure.py [arguments?]'
+    esptool.py - usage: 'pipx run --spec esptool esptool.py [arguments?]'
+```
+
+You can instead run the executables that this package provides by using `--spec`:
+
 ```
+pipx run --spec esptool esp_rfc2217_server.py
+pipx run --spec esptool espefuse.py
+pipx run --spec esptool espsecure.py
+pipx run --spec esptool esptool.py
+```
+
+Note that the `.py` extension is not something you append to the executable name. It is part of the executable name, as provided by the package. This can be anything. For example, when working with the [pymodbus](https://github.com/pymodbus-dev/pymodbus) package:
+
+```
+>> pipx run pymodbus[repl]
+'pymodbus' executable script not found in package 'pymodbus'.
+Available executable scripts:
+    pymodbus.console - usage: 'pipx run --spec pymodbus pymodbus.console [arguments?]'
+    pymodbus.server - usage: 'pipx run --spec pymodbus pymodbus.server [arguments?]'
+    pymodbus.simulator - usage: 'pipx run --spec pymodbus pymodbus.simulator [arguments?]'
+```
+
+You can run the executables like this:
+
+```
+pipx run --spec pymodbus[repl] pymodbus.console
+pipx run --spec pymodbus[repl] pymodbus.server
+pipx run --spec pymodbus[repl] pymodbus.simulator
+```
+
+### Running a specific version of a package
+
+The `PACKAGE` argument above is actually a [requirement specifier](https://packaging.python.org/en/latest/glossary/#term-Requirement-Specifier). Therefore, you can also specify specific versions, version ranges, or extras. For example:
+
+```
+pipx run mpremote==1.20.0
+pipx run --spec esptool==4.6.2 esptool.py
+pipx run --spec 'esptool>=4.5' esptool.py
+pipx run --spec 'esptool >= 4.5' esptool.py
+```
+
+Notice that some requirement specifiers have to be enclosed in quotes or escaped.
 
 ### Running from Source Control
 
diff --git a/docs/comparisons.md b/docs/comparisons.md
index bc4740f..8727288 100644
--- a/docs/comparisons.md
+++ b/docs/comparisons.md
@@ -85,3 +85,15 @@ pip-run poetry -- -m poetry --help
 
 Example interaction:
 None.
+
+## pipx vs fades
+
+[fades](https://github.com/PyAr/fades) is a tool to run **individual** Python scripts inside automatically provisioned virtualenvs with their dependencies installed.
+
+* Both [fades](https://github.com/PyAr/fades#how-to-mark-the-dependencies-to-be-installed) and [pipx run](examples.md#pipx-run-examples) allow specifying a script's dependencies in specially formatted comments, but the exact syntax differs (see also [PEP 722](https://peps.python.org/pep-0722/) which seeks to standardize such syntax).
+* Both tools automatically set up reusable virtualenvs containing the necessary dependencies.
+* Both can download Python scripts/packages to execute from remote resources.
+* fades can only run individual script files while pipx can also run packages.
+
+Example interaction:
+None.
diff --git a/docs/examples.md b/docs/examples.md
index cae08fb..61bed69 100644
--- a/docs/examples.md
+++ b/docs/examples.md
@@ -2,8 +2,8 @@
 
 ```
 pipx install pycowsay
-pipx install --python python3.7 pycowsay
-pipx install --python python3.8 pycowsay
+pipx install --python python3.10 pycowsay
+pipx install --python 3.11 pycowsay
 pipx install git+https://github.com/psf/black
 pipx install git+https://github.com/psf/black.git@branch-name
 pipx install git+https://github.com/psf/black.git@git-hash
diff --git a/docs/how-pipx-works.md b/docs/how-pipx-works.md
index 744948d..622543d 100644
--- a/docs/how-pipx-works.md
+++ b/docs/how-pipx-works.md
@@ -1,18 +1,18 @@
 ## How it Works
 
-When installing a package and its binaries (`pipx install package`) pipx will
+When installing a package and its binaries on linux (`pipx install package`) pipx will
 
-- create directory `~/.local/pipx/venvs/PACKAGE`
-- create or re-use a shared virtual environment that contains shared packaging libraries `pip`, `setuptools` and `wheel` in `~/.local/pipx/shared/`
+- create directory `~/.local/share/pipx/venvs/PACKAGE`
+- create or re-use a shared virtual environment that contains shared packaging libraries `pip`, `setuptools` and `wheel` in `~/.local/share/pipx/shared/`
 - ensure all packaging libraries are updated to their latest versions
-- create a Virtual Environment in `~/.local/pipx/venvs/PACKAGE` that uses the shared pip mentioned above but otherwise is isolated (pipx uses a [.pth file]( https://docs.python.org/3/library/site.html) to do this)
+- create a Virtual Environment in `~/.local/share/pipx/venvs/PACKAGE` that uses the shared pip mentioned above but otherwise is isolated (pipx uses a [.pth file]( https://docs.python.org/3/library/site.html) to do this)
 - install the desired package in the Virtual Environment
-- expose binaries at `~/.local/bin` that point to new binaries in `~/.local/pipx/venvs/PACKAGE/bin` (such as `~/.local/bin/black` -> `~/.local/pipx/venvs/black/bin/black`)
+- expose binaries at `~/.local/bin` that point to new binaries in `~/.local/share/pipx/venvs/PACKAGE/bin` (such as `~/.local/bin/black` -> `~/.local/share/pipx/venvs/black/bin/black`)
 - As long as `~/.local/bin/` is on your PATH, you can now invoke the new binaries globally
 
 When running a binary (`pipx run BINARY`), pipx will
 
-- create or re-use a shared virtual environment that contains shared packaging libraries `pip`, `setuptools` and `wheel` in `~/.local/pipx/shared/`
+- create or re-use a shared virtual environment that contains shared packaging libraries `pip`, `setuptools` and `wheel` in `~/.local/share/pipx/shared/`
 - ensure all packaging libraries are updated to their latest versions
 - create a temporary directory (or reuse a cached virtual environment for this package) with a name based on a hash of the attributes that make the run reproducible. This includes things like the package name, spec, python version, and pip arguments.
 - create a Virtual Environment inside it with `python -m venv`
diff --git a/docs/installation.md b/docs/installation.md
index 2d34d9c..d559104 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -6,7 +6,7 @@ You also need to have `pip` installed on your machine for `python3`. Installing
 
 pipx works on macOS, linux, and Windows.
 
-## Install pipx
+## Installing pipx
 
 On macOS:
 
@@ -18,8 +18,8 @@ pipx ensurepath
 On Windows (requires pip 19.0 or later):
 
 ```
-py -3 -m pip install --user pipx
-py -3 -m pipx ensurepath
+py -m pip install --user pipx
+py -m pipx ensurepath
 ```
 
 Otherwise, install via pip (requires pip 19.0 or later):
@@ -29,20 +29,46 @@ python3 -m pip install --user pipx
 python3 -m pipx ensurepath
 ```
 
-Or via zipapp:
+!!!caution
+    It is not recommended to install `pipx` via `pipx`. If you'd like
+    to do this anyway, take a look at the
+    [`pipx-in-pipx`](https://github.com/mattsb42-meta/pipx-in-pipx) project and
+    read about the limitations there.
 
-You can also use pipx without installing it.
+
+### Using pipx without installing (via zipapp)
 The zipapp can be downloaded from [Github releases](https://github.com/pypa/pipx/releases) and you can invoke it with a Python 3.7+ interpreter:
 
 ```
 python pipx.pyz ensurepath
 ```
 
-### Installation Options
+### <a name="pre-commit"></a> Using pipx with pre-commit
+
+Pipx has [pre-commit](https://pre-commit.com/) support. This lets you run applications:
+
+* That can be run using `pipx run` but don't have native pre-commit support.
+* Using its prebuilt wheel from pypi.org instead of building it from source.
+* Using pipx's `--spec` and `--index-url` flags.
+
+Example configuration for use of the code linter [yapf](https://github.com/google/yapf/). This is to be added to your `.pre-commit-config.yaml`.
+
+```yaml
+- repo: https://github.com/pypa/pipx
+  rev: 53e7f27
+  hooks:
+  - id: pipx
+    alias: yapf
+    name: yapf
+    args: ['yapf', '-i']
+    types: ['python']
+```
+
+## Installation Options
 
 The default binary location for pipx-installed apps is `~/.local/bin`. This can be overridden with the environment variable `PIPX_BIN_DIR`.
 
-pipx's default virtual environment location is `~/.local/pipx`. This can be overridden with the environment variable `PIPX_HOME`.
+pipx's default virtual environment location is typically `~/.local/share/pipx` on Linux/Unix, `%USERPROFILE%\AppData\Local\pipx` on Windows and `~/Library/Application Support/pipx` on macOS, and for compatibility reasons, if `~/.local/pipx` exists, it will be used as the default location instead. This can be overridden with the `PIPX_HOME` environment variable.
 
 As an example, you can install global apps accessible by all users on your system with the following command (on MacOS, Linux, and Windows WSL):
 
@@ -51,6 +77,21 @@ sudo PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install PACKAGE
 # Example: $ sudo PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install cowsay
 ```
 
+!!! note
+
+    After version 1.2.0, the default pipx paths have been moved from `~/.local/pipx` to specific user data directories on each platform using [platformdirs](https://pypi.org/project/platformdirs/) library
+
+    | Old Path               | New Path                                   |
+    | ---------------------- | ------------------------------------------ |
+    | `~/.local/pipx/.trash` | `platformdirs.user_data_dir()/pipx/trash`  |
+    | `~/.local/pipx/shared` | `platformdirs.user_data_dir()/pipx/shared` |
+    | `~/.local/pipx/venvs`  | `platformdirs.user_data_dir()/pipx/venv`   |
+    | `~/.local/pipx/.cache` | `platformdirs.user_cache_dir()/pipx`       |
+    | `~/.local/pipx/logs`   | `platformdirs.user_log_dir()/pipx/log`     |
+
+    `user_data_dir()`, `user_cache_dir()` and `user_log_dir()` resolve to appropriate platform-specific user data, cache and log directories.
+    See the [platformdirs documentation](https://platformdirs.readthedocs.io/en/latest/api.html#platforms) for details.
+
 ## Upgrade pipx
 
 On macOS:
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
index 1cbb7d4..4246a8e 100644
--- a/docs/troubleshooting.md
+++ b/docs/troubleshooting.md
@@ -74,10 +74,10 @@ Reference: [pip Environment Variables](https://pip.pypa.io/en/stable/user_guide/
 
 ## `pipx` log files
 Pipx records a verbose log file for every `pipx` command invocation.  The logs
-for the last 10 `pipx` commands can be found in `$PIPX_HOME/logs` or user's log path
+for the last 10 `pipx` commands can be found in `$XDG_STATE_HOME/pipx/logs` or user's log path
 if the former is not writable by the user.
 
-For most users this location is `~/.local/pipx/logs`, where `~` is your home
+For most users this location is `~/.local/state/pipx/logs`, where `~` is your home
 directory.
 
 ## Debian, Ubuntu issues
@@ -119,3 +119,21 @@ To clean up after this experiment:
 ```
 rm -rf test_venv
 ```
+
+## Pipx files not in expected locations according to documentation
+
+The default PIPX_HOME is `~/.local/pipx`, prior to the adoption of the XDG base
+directory specification after version 1.2.0. To maintain compatibility with older
+versions, pipx will automatically detect the old paths and use them accordingly.
+For a map of old and new paths, See [Installation](installation.md#installation-options)
+
+To migrate from the old path to the new path, you can remove the `~/.local/pipx` directory and
+reinstall all packages.
+
+For example, on Linux systems, you could read out `pipx`'s package information in JSON via `jq` (which you might need to install first):
+
+```
+packages=($(pipx list --json | jq '.venvs | keys[]' -r))
+rm -rf ~/.local/pipx
+for p in ${packages[@]}; do pipx install "$p"; done
+```
diff --git a/installation.md b/installation.md
new file mode 120000
index 0000000..ce2cdfe
--- /dev/null
+++ b/installation.md
@@ -0,0 +1,1 @@
+docs/installation.md
\ No newline at end of file
diff --git a/mkdocs.yml b/mkdocs.yml
index bdd1256..c31254a 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -3,12 +3,27 @@ site_description: execute binaries from Python packages in isolated environments
 
 theme:
   name: "material"
-google_analytics:
-  - 'UA-90243909-2'
-  - 'auto'
+  palette:
+    - media: "(prefers-color-scheme: light)"
+      scheme: default
+      toggle:
+        icon: material/brightness-7
+        name: Switch to dark mode
+
+    - media: "(prefers-color-scheme: dark)"
+      scheme: slate
+      toggle:
+        icon: material/brightness-4
+        name: Switch to light mode
+
 repo_name: pypa/pipx
 repo_url: https://github.com/pypa/pipx
 edit_uri: edit/main/docs/
+extra:
+  analytics:
+    provider: 'google'
+    property: 'UA-90243909-2'
+
 
 nav:
   - Home: "index.md"
diff --git a/noxfile.py b/noxfile.py
index cb9b68a..6beb7bc 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -4,15 +4,15 @@ from pathlib import Path
 
 import nox  # type: ignore
 
-PYTHON_ALL_VERSIONS = ["3.7", "3.8", "3.9", "3.10", "3.11"]
+PYTHON_ALL_VERSIONS = ["3.8", "3.9", "3.10", "3.11"]
 PYTHON_DEFAULT_VERSION = "3.11"
 DOC_DEPENDENCIES = [".", "jinja2", "mkdocs", "mkdocs-material"]
 MAN_DEPENDENCIES = [".", "argparse-manpage[setuptools]"]
 LINT_DEPENDENCIES = [
-    "black==22.8.0",
-    "mypy==0.930",
+    "black==23.10.1",
+    "mypy==1.6.1",
     "packaging>=20.0",
-    "ruff==0.0.254",
+    "ruff==0.1.3",
     "types-jinja2",
 ]
 # Packages whose dependencies need an intact system PATH to compile
@@ -103,7 +103,7 @@ def tests_with_options(session, net_pypiserver):
     if net_pypiserver:
         pypiserver_option = ["--net-pypiserver"]
     else:
-        session.install("pypiserver")
+        session.install("pypiserver[passlib]")
         refresh_packages_cache(session)
         pypiserver_option = []
 
@@ -193,27 +193,21 @@ def publish(session):
 
 @nox.session(python=PYTHON_DEFAULT_VERSION)
 def build_docs(session):
+    site_dir = session.posargs or ["site/"]
     session.run("python", "-m", "pip", "install", "--upgrade", "pip")
     session.install(*DOC_DEPENDENCIES)
     session.env[
         "PIPX__DOC_DEFAULT_PYTHON"
     ] = "typically the python used to execute pipx"
     session.run("python", "scripts/generate_docs.py")
-    session.run("mkdocs", "build")
-
-
-@nox.session(python=PYTHON_DEFAULT_VERSION)
-def publish_docs(session):
-    session.run("python", "-m", "pip", "install", "--upgrade", "pip")
-    session.install(*DOC_DEPENDENCIES)
-    build_docs(session)
-    session.run("mkdocs", "gh-deploy")
+    session.run("mkdocs", "build", "--strict", "--site-dir", *site_dir)
 
 
 @nox.session(python=PYTHON_DEFAULT_VERSION)
 def watch_docs(session):
     session.install(*DOC_DEPENDENCIES)
-    session.run("mkdocs", "serve")
+    session.run("python", "scripts/generate_docs.py")
+    session.run("mkdocs", "serve", "--strict")
 
 
 @nox.session(python=PYTHON_DEFAULT_VERSION)
diff --git a/pyproject.toml b/pyproject.toml
index 7032dcf..950f0f5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,7 +7,7 @@ name = "pipx"
 description = "Install and Run Python Applications in Isolated Environments"
 readme = "README.md"
 license = "MIT"
-requires-python = ">=3.7"
+requires-python = ">=3.8"
 keywords = ["pip", "install", "cli", "workflow", "Virtual Environment"]
 authors = [{ name = "Chad Smith", email = "chadsmith.software@gmail.com" }]
 classifiers = [
@@ -15,7 +15,6 @@ classifiers = [
   "Operating System :: OS Independent",
   "Programming Language :: Python",
   "Programming Language :: Python :: 3 :: Only",
-  "Programming Language :: Python :: 3.7",
   "Programming Language :: Python :: 3.8",
   "Programming Language :: Python :: 3.9",
   "Programming Language :: Python :: 3.10",
@@ -24,10 +23,10 @@ classifiers = [
 dependencies = [
   "argcomplete>=1.9.4",
   "colorama>=0.4.4; sys_platform == 'win32'",
-  "importlib-metadata>=3.3.0; python_version < '3.8'",
   "packaging>=20.0",
   "platformdirs>=2.1.0",
-  "userpath>=1.6.0",
+  "tomli; python_version < '3.11'",
+  "userpath>=1.6.0,!=1.9.0",
 ]
 dynamic = ["version"]
 
@@ -50,6 +49,9 @@ include = ["/src", "/logo.png", "/pipx_demo.gif", "/*.md"]
 skip-magic-trailing-comma = true
 
 [tool.ruff]
+line-length = 121
+
+[tool.ruff.lint]
 select = [
   "A",
   "B",
@@ -67,14 +69,24 @@ select = [
 ignore = [
   "B904",
 ]
-line-length = 121
-show-source = true
 
-[tool.ruff.isort]
+[tool.ruff.lint.isort]
 known-first-party = ["helpers", "package_info", "pipx"]
 
-[tool.ruff.mccabe]
+[tool.ruff.lint.mccabe]
 max-complexity = 15
 
 [tool.pytest.ini_options]
 markers = ["all_packages: test install with maximum number of packages"]
+
+[tool.mypy]
+show_error_codes = true
+
+[[tool.mypy.overrides]]
+module = [
+  "packaging.*",
+  "platformdirs",
+  "pycowsay.*",
+  "jinja2",
+]
+ignore_missing_imports = true
diff --git a/scripts/generate_docs.py b/scripts/generate_docs.py
index 26266c9..e65f7c4 100644
--- a/scripts/generate_docs.py
+++ b/scripts/generate_docs.py
@@ -1,5 +1,4 @@
 #!/usr/bin/env python3
-# -*- coding: utf-8 -*-
 
 import os
 import subprocess
diff --git a/scripts/generate_man.py b/scripts/generate_man.py
index 93adb33..cd63e04 100644
--- a/scripts/generate_man.py
+++ b/scripts/generate_man.py
@@ -1,7 +1,7 @@
 #!/usr/bin/env python3
-# -*- coding: utf-8 -*-
 
 import os.path
+import sys
 import textwrap
 
 from build_manpages.manpage import Manpage  # type: ignore
@@ -10,6 +10,7 @@ from pipx.main import get_command_parser
 
 
 def main():
+    sys.argv[0] = "pipx"
     parser = get_command_parser()
     parser.man_short_description = parser.description.splitlines()[1]
 
diff --git a/scripts/migrate_pipsi_to_pipx.py b/scripts/migrate_pipsi_to_pipx.py
index bf81fa1..74df56e 100644
--- a/scripts/migrate_pipsi_to_pipx.py
+++ b/scripts/migrate_pipsi_to_pipx.py
@@ -38,7 +38,7 @@ def main():
 
     error = False
     for package in packages:
-        ret = subprocess.run(["pipsi", "uninstall", "--yes", package])
+        ret = subprocess.run(["pipsi", "uninstall", "--yes", package], check=False)
         if ret.returncode:
             error = True
             print(
@@ -49,7 +49,7 @@ def main():
             print(
                 f"uninstalled {package!r} with pipsi. Now attempting to install with pipx."
             )
-            ret = subprocess.run(["pipx", "install", package])
+            ret = subprocess.run(["pipx", "install", package], check=False)
             if ret.returncode:
                 error = True
                 print(f"Failed to install {package!r} with pipx.")
diff --git a/scripts/pipx_release.py b/scripts/pipx_release.py
index 989c9b7..90151a8 100644
--- a/scripts/pipx_release.py
+++ b/scripts/pipx_release.py
@@ -4,7 +4,7 @@ from pathlib import Path
 
 
 def python_mypy_ok(filepath: Path) -> bool:
-    mypy_proc = subprocess.run(["mypy", filepath])
+    mypy_proc = subprocess.run(["mypy", filepath], check=False)
     return True if mypy_proc.returncode == 0 else False
 
 
diff --git a/scripts/update_package_cache.py b/scripts/update_package_cache.py
index 69155fa..ee0ad07 100644
--- a/scripts/update_package_cache.py
+++ b/scripts/update_package_cache.py
@@ -49,8 +49,7 @@ def process_command_line(argv: List[str]) -> argparse.Namespace:
         "download or delete files.",
     )
 
-    args = parser.parse_args(argv)
-    return args
+    return parser.parse_args(argv)
 
 
 def update_test_packages_cache(
@@ -92,7 +91,7 @@ def update_test_packages_cache(
 
     try:
         platform_package_list_fh = platform_package_list_path.open("r")
-    except IOError:
+    except OSError:
         print(
             f"ERROR.  File {str(platform_package_list_path)}\n"
             "    is not readable.  Cannot continue.\n",
@@ -135,7 +134,9 @@ def update_test_packages_cache(
                 packages_dir_hits.append(matches[0])
                 continue
             elif len(matches) > 1:
-                print("ERROR: more than one match for {package_spec}.", file=sys.stderr)
+                print(
+                    f"ERROR: more than one match for {package_spec}.", file=sys.stderr
+                )
                 print(f"    {matches}", file=sys.stderr)
                 exit_code = 1
                 continue
@@ -159,9 +160,9 @@ def update_test_packages_cache(
                     "-d",
                     str(packages_dir_path),
                 ],
-                stdout=subprocess.PIPE,
-                stderr=subprocess.PIPE,
-                universal_newlines=True,
+                capture_output=True,
+                text=True,
+                check=False,
             )
             if pip_download_process.returncode == 0:
                 print(f"Successfully downloaded {package_spec}")
diff --git a/src/pipx/__init__.py b/src/pipx/__init__.py
index ec98d9c..c4155c4 100644
--- a/src/pipx/__init__.py
+++ b/src/pipx/__init__.py
@@ -1,8 +1,8 @@
 import sys
 
-if sys.version_info < (3, 6, 0):
+if sys.version_info < (3, 8, 0):
     sys.exit(
-        "Python 3.6 or later is required. "
+        "Python 3.8 or later is required. "
         "See https://github.com/pypa/pipx "
         "for installation instructions."
     )
diff --git a/src/pipx/animate.py b/src/pipx/animate.py
index 4fe2038..1561cbb 100644
--- a/src/pipx/animate.py
+++ b/src/pipx/animate.py
@@ -101,11 +101,13 @@ def print_animation(
 # for Windows pre-ANSI-terminal-support (before Windows 10 TH2 (v1511))
 # https://stackoverflow.com/a/10455937
 def win_cursor(visible: bool) -> None:
+    if sys.platform != "win32":  # hello mypy
+        return
     ci = _CursorInfo()
-    handle = ctypes.windll.kernel32.GetStdHandle(-11)  # type: ignore[attr-defined]
-    ctypes.windll.kernel32.GetConsoleCursorInfo(handle, ctypes.byref(ci))  # type: ignore[attr-defined]
+    handle = ctypes.windll.kernel32.GetStdHandle(-11)
+    ctypes.windll.kernel32.GetConsoleCursorInfo(handle, ctypes.byref(ci))
     ci.visible = visible
-    ctypes.windll.kernel32.SetConsoleCursorInfo(handle, ctypes.byref(ci))  # type: ignore[attr-defined]
+    ctypes.windll.kernel32.SetConsoleCursorInfo(handle, ctypes.byref(ci))
 
 
 def hide_cursor() -> None:
diff --git a/src/pipx/commands/common.py b/src/pipx/commands/common.py
index 55e87d4..8666324 100644
--- a/src/pipx/commands/common.py
+++ b/src/pipx/commands/common.py
@@ -350,6 +350,13 @@ def run_post_install_actions(
 
     display_name = f"{package_name}{package_metadata.suffix}"
 
+    if (
+        not venv.main_package_name == package_name
+        and venv.package_metadata[venv.main_package_name].suffix
+        == package_metadata.suffix
+    ):
+        package_name = display_name
+
     if not package_metadata.apps:
         if not package_metadata.apps_of_dependencies:
             if venv.safe_to_remove():
@@ -409,7 +416,7 @@ def warn_if_not_on_path(local_bin_dir: Path) -> None:
         logger.warning(
             pipx_wrap(
                 f"""
-                {hazard}  Note: {str(local_bin_dir)!r} is not on your PATH
+                {hazard}  Note: '{local_bin_dir}' is not on your PATH
                 environment variable. These apps will not be globally
                 accessible until your PATH is updated. Run `pipx ensurepath` to
                 automatically add it, or manually modify your PATH in your
diff --git a/src/pipx/commands/ensure_path.py b/src/pipx/commands/ensure_path.py
index 8151177..6e3de6f 100644
--- a/src/pipx/commands/ensure_path.py
+++ b/src/pipx/commands/ensure_path.py
@@ -61,14 +61,22 @@ def ensure_path(location: Path, *, force: bool) -> Tuple[bool, bool]:
     in_current_path = userpath.in_current_path(location_str)
 
     if force or (not in_current_path and not need_shell_restart):
-        userpath.append(location_str, "pipx")
-        print(
-            pipx_wrap(
-                f"Success! Added {location_str} to the PATH environment variable.",
-                subsequent_indent=" " * 4,
+        path_added = userpath.append(location_str, "pipx")
+        if not path_added:
+            print(
+                pipx_wrap(
+                    f"{hazard}  {location_str} is not added to the PATH environment variable successfully. "
+                    f"You may need to add it to PATH manually.",
+                    subsequent_indent=" " * 4,
+                )
+            )
+        else:
+            print(
+                pipx_wrap(
+                    f"Success! Added {location_str} to the PATH environment variable.",
+                    subsequent_indent=" " * 4,
+                )
             )
-        )
-        path_added = True
         need_shell_restart = userpath.need_shell_restart(location_str)
     elif not in_current_path and need_shell_restart:
         print(
diff --git a/src/pipx/commands/environment.py b/src/pipx/commands/environment.py
index 461bcb7..155311e 100644
--- a/src/pipx/commands/environment.py
+++ b/src/pipx/commands/environment.py
@@ -1,3 +1,5 @@
+import os
+
 from pipx.constants import (
     EXIT_CODE_OK,
     LOCAL_BIN_DIR,
@@ -9,12 +11,21 @@ from pipx.constants import (
     PIPX_VENV_CACHEDIR,
     ExitCode,
 )
+from pipx.emojis import EMOJI_SUPPORT
+from pipx.interpreter import DEFAULT_PYTHON
 from pipx.util import PipxError
 
 
 def environment(value: str) -> ExitCode:
-    """Print a list of variables used in `pipx.constants`"""
-    environment_variables = {
+    """Print a list of environment variables and paths used by pipx"""
+    environment_variables = [
+        "PIPX_HOME",
+        "PIPX_BIN_DIR",
+        "PIPX_SHARED_LIBS",
+        "PIPX_DEFAULT_PYTHON",
+        "USE_EMOJI",
+    ]
+    derived_values = {
         "PIPX_HOME": PIPX_HOME,
         "PIPX_BIN_DIR": LOCAL_BIN_DIR,
         "PIPX_SHARED_LIBS": PIPX_SHARED_LIBS,
@@ -22,14 +33,22 @@ def environment(value: str) -> ExitCode:
         "PIPX_LOG_DIR": PIPX_LOG_DIR,
         "PIPX_TRASH_DIR": PIPX_TRASH_DIR,
         "PIPX_VENV_CACHEDIR": PIPX_VENV_CACHEDIR,
+        "PIPX_DEFAULT_PYTHON": DEFAULT_PYTHON,
+        "USE_EMOJI": str(EMOJI_SUPPORT).lower(),
     }
     if value is None:
+        print("Environment variables (set by user):")
+        print("")
         for env_variable in environment_variables:
-            print(f"{env_variable}={environment_variables[env_variable]}")
+            env_value = os.getenv(env_variable, "")
+            print(f"{env_variable}={env_value}")
+        print("")
+        print("Derived values (computed by pipx):")
         print("")
-        print("Only PIPX_HOME and PIPX_BIN_DIR can be set by users in the above list.")
-    elif value in environment_variables:
-        print(environment_variables[value])
+        for env_variable in derived_values:
+            print(f"{env_variable}={derived_values[env_variable]}")
+    elif value in derived_values:
+        print(derived_values[value])
     else:
         raise PipxError("Variable not found.")
 
diff --git a/src/pipx/commands/inject.py b/src/pipx/commands/inject.py
index 68a2243..f967e00 100644
--- a/src/pipx/commands/inject.py
+++ b/src/pipx/commands/inject.py
@@ -22,6 +22,7 @@ def inject_dep(
     include_apps: bool,
     include_dependencies: bool,
     force: bool,
+    suffix: bool = False,
 ) -> bool:
     if not venv_dir.exists() or not next(venv_dir.iterdir()):
         raise PipxError(
@@ -53,7 +54,10 @@ def inject_dep(
             pip_args=pip_args,
             verbose=verbose,
         )
-
+    if suffix:
+        venv_suffix = venv.package_metadata[venv.main_package_name].suffix
+    else:
+        venv_suffix = ""
     venv.install_package(
         package_name=package_name,
         package_or_url=package_spec,
@@ -61,6 +65,7 @@ def inject_dep(
         include_dependencies=include_dependencies,
         include_apps=include_apps,
         is_main_package=False,
+        suffix=venv_suffix,
     )
     if include_apps:
         run_post_install_actions(
@@ -89,12 +94,11 @@ def inject(
     include_apps: bool,
     include_dependencies: bool,
     force: bool,
+    suffix: bool = False,
 ) -> ExitCode:
     """Returns pipx exit code."""
     if not include_apps and include_dependencies:
-        raise PipxError(
-            "Cannot pass --include-deps if --include-apps is not passed as well"
-        )
+        include_apps = True
     all_success = True
     for dep in package_specs:
         all_success &= inject_dep(
@@ -106,6 +110,7 @@ def inject(
             include_apps=include_apps,
             include_dependencies=include_dependencies,
             force=force,
+            suffix=suffix,
         )
 
     # Any failure to install will raise PipxError, otherwise success
diff --git a/src/pipx/commands/install.py b/src/pipx/commands/install.py
index ddd230f..218bf7d 100644
--- a/src/pipx/commands/install.py
+++ b/src/pipx/commands/install.py
@@ -4,6 +4,7 @@ from typing import List, Optional
 from pipx import constants
 from pipx.commands.common import package_name_from_spec, run_post_install_actions
 from pipx.constants import EXIT_CODE_INSTALL_VENV_EXISTS, EXIT_CODE_OK, ExitCode
+from pipx.interpreter import DEFAULT_PYTHON
 from pipx.util import pipx_wrap
 from pipx.venv import Venv, VenvContainer
 
@@ -13,18 +14,23 @@ def install(
     package_name: Optional[str],
     package_spec: str,
     local_bin_dir: Path,
-    python: str,
+    python: Optional[str],
     pip_args: List[str],
     venv_args: List[str],
     verbose: bool,
     *,
     force: bool,
+    reinstall: bool,
     include_dependencies: bool,
+    preinstall_packages: Optional[List[str]],
     suffix: str = "",
 ) -> ExitCode:
     """Returns pipx exit code."""
     # package_spec is anything pip-installable, including package_name, vcs spec,
     #   zip file, or tar.gz file.
+    python_flag_was_passed = python is not None
+
+    python = python or DEFAULT_PYTHON
 
     if package_name is None:
         package_name = package_name_from_spec(
@@ -41,14 +47,25 @@ def install(
 
     venv = Venv(venv_dir, python=python, verbose=verbose)
     if exists:
+        if not reinstall and force and python_flag_was_passed:
+            print(
+                pipx_wrap(
+                    f"""
+                    --python is ignored when --force is passed.
+                    If you want to reinstall {package_name} with {python},
+                    run `pipx reinstall {package_spec} --python {python}` instead.
+                    """
+                )
+            )
         if force:
             print(f"Installing to existing venv {venv.name!r}")
+            pip_args += ["--force-reinstall"]
         else:
             print(
                 pipx_wrap(
                     f"""
                     {venv.name!r} already seems to be installed. Not modifying
-                    existing installation in {str(venv_dir)!r}. Pass '--force'
+                    existing installation in '{venv_dir}'. Pass '--force'
                     to force installation.
                     """
                 )
@@ -57,6 +74,11 @@ def install(
 
     try:
         venv.create_venv(venv_args, pip_args)
+        for dep in preinstall_packages or []:
+            dep_name = package_name_from_spec(
+                dep, python, pip_args=pip_args, verbose=verbose
+            )
+            venv.upgrade_package_no_metadata(dep_name, [])
         venv.install_package(
             package_name=package_name,
             package_or_url=package_spec,
diff --git a/src/pipx/commands/reinstall.py b/src/pipx/commands/reinstall.py
index 4271588..a15bb29 100644
--- a/src/pipx/commands/reinstall.py
+++ b/src/pipx/commands/reinstall.py
@@ -69,7 +69,9 @@ def reinstall(
         venv.pipx_metadata.venv_args,
         verbose,
         force=True,
+        reinstall=True,
         include_dependencies=venv.pipx_metadata.main_package.include_dependencies,
+        preinstall_packages=[],
         suffix=venv.pipx_metadata.main_package.suffix,
     )
 
diff --git a/src/pipx/commands/run.py b/src/pipx/commands/run.py
index b4d1b90..575565c 100644
--- a/src/pipx/commands/run.py
+++ b/src/pipx/commands/run.py
@@ -1,6 +1,8 @@
 import datetime
 import hashlib
 import logging
+import re
+import sys
 import time
 import urllib.parse
 import urllib.request
@@ -24,6 +26,11 @@ from pipx.util import (
 )
 from pipx.venv import Venv
 
+if sys.version_info < (3, 11):
+    import tomli as tomllib
+else:
+    import tomllib
+
 logger = logging.getLogger(__name__)
 
 
@@ -74,7 +81,7 @@ def run_script(
 ) -> NoReturn:
     requirements = _get_requirements_from_script(content)
     if requirements is None:
-        exec_app([str(python), "-c", content, *app_args])
+        exec_app([python, "-c", content, *app_args])
     else:
         # Note that the environment name is based on the identified
         # requirements, and *not* on the script name. This is deliberate, as
@@ -106,7 +113,6 @@ def run_package(
     verbose: bool,
     use_cache: bool,
 ) -> NoReturn:
-
     if which(app):
         logger.warning(
             pipx_wrap(
@@ -127,13 +133,13 @@ def run_package(
     pypackage_bin_path = get_pypackage_bin_path(app)
     if pypackage_bin_path.exists():
         logger.info(
-            f"Using app in local __pypackages__ directory at {str(pypackage_bin_path)}"
+            f"Using app in local __pypackages__ directory at '{pypackage_bin_path}'"
         )
         run_pypackage_bin(pypackage_bin_path, app_args)
     if pypackages:
         raise PipxError(
             f"""
-            '--pypackages' flag was passed, but {str(pypackage_bin_path)!r} was
+            '--pypackages' flag was passed, but '{pypackage_bin_path}' was
             not found. See https://github.com/cs01/pythonloc to learn how to
             install here, or omit the flag.
             """
@@ -180,7 +186,6 @@ def run(
     package
     """
 
-    package_or_url = spec if spec is not None else app
     # For any package, we need to just use the name
     try:
         package_name = Requirement(app).name
@@ -189,14 +194,11 @@ def run(
         # we can't parse this as a package
         package_name = app
 
-    if spec is not None:
-        content = None
-    else:
-        content = maybe_script_content(app, is_path)
-
+    content = None if spec is not None else maybe_script_content(app, is_path)
     if content is not None:
         run_script(content, app_args, python, pip_args, venv_args, verbose, use_cache)
     else:
+        package_or_url = spec if spec is not None else app
         run_package(
             package_name,
             package_or_url,
@@ -255,7 +257,7 @@ def _download_and_run(
                 app_filename = app
         else:
             all_apps = (
-                f"{a} - usage: 'pipx run --spec {package_name} {a} [arguments?]'"
+                f"{a} - usage: 'pipx run --spec {package_or_url} {a} [arguments?]'"
                 for a in apps
             )
             raise PipxError(
@@ -287,7 +289,7 @@ def _get_temporary_venv_path(
     m.update(python.encode())
     m.update("".join(pip_args).encode())
     m.update("".join(venv_args).encode())
-    venv_folder_name = m.hexdigest()[0:15]  # 15 chosen arbitrarily
+    venv_folder_name = m.hexdigest()[:15]  # 15 chosen arbitrarily
     return Path(constants.PIPX_VENV_CACHEDIR) / venv_folder_name
 
 
@@ -321,45 +323,49 @@ def _http_get_request(url: str) -> str:
         return res.read().decode(charset)
     except Exception as e:
         logger.debug("Uncaught Exception:", exc_info=True)
-        raise PipxError(str(e))
+        raise PipxError(str(e)) from e
+
+
+# This regex comes from PEP 723
+PEP723 = re.compile(
+    r"(?m)^# /// (?P<type>[a-zA-Z0-9-]+)$\s(?P<content>(^#(| .*)$\s)+)^# ///$"
+)
 
 
 def _get_requirements_from_script(content: str) -> Optional[List[str]]:
+    """
+    Supports PEP 723.
+    """
 
-    # An iterator over the lines in the script. We will
-    # read through this in sections, so it needs to be an
-    # iterator, not just a list.
-    lines = iter(content.splitlines())
-
-    for line in lines:
-        if not line.startswith("#"):
-            continue
-        line_content = line[1:].strip()
-        if line_content == "Requirements:":
-            break
-    else:
-        # No "Requirements:" line in the file
+    name = "pyproject"
+
+    # Windows is currently getting un-normalized line endings, so normalize
+    content = content.replace("\r\n", "\n")
+
+    matches = [m for m in PEP723.finditer(content) if m.group("type") == name]
+
+    if not matches:
         return None
 
-    # We are now at the first requirement
-    requirements = []
-    for line in lines:
-        # Stop at the end of the comment block
-        if not line.startswith("#"):
-            break
-        line_content = line[1:].strip()
-        # Stop at a blank comment line
-        if not line_content:
-            break
+    if len(matches) > 1:
+        raise ValueError(f"Multiple {name} blocks found")
 
+    content = "".join(
+        line[2:] if line.startswith("# ") else line[1:]
+        for line in matches[0].group("content").splitlines(keepends=True)
+    )
+
+    pyproject = tomllib.loads(content)
+
+    requirements = []
+    for requirement in pyproject.get("run", {}).get("requirements", []):
         # Validate the requirement
         try:
-            req = Requirement(line_content)
+            req = Requirement(requirement)
         except InvalidRequirement as e:
-            raise PipxError(f"Invalid requirement {line_content}: {str(e)}")
+            raise PipxError(f"Invalid requirement {requirement}: {e}") from e
 
-        # Use the normalised form of the requirement,
-        # not the original line.
+        # Use the normalised form of the requirement
         requirements.append(str(req))
 
     return requirements
diff --git a/src/pipx/commands/uninstall.py b/src/pipx/commands/uninstall.py
index d338d3c..bbca0f4 100644
--- a/src/pipx/commands/uninstall.py
+++ b/src/pipx/commands/uninstall.py
@@ -91,7 +91,7 @@ def _get_venv_bin_dir_app_paths(venv: Venv, local_bin_dir: Path) -> Set[Path]:
         # We'll take our best guess on what to uninstall here based on symlink
         # location for symlink-capable systems.
         # The heuristic here is any symlink in ~/.local/bin pointing to
-        # .local/pipx/venvs/VENV_NAME/{bin,Scripts} should be uninstalled.
+        # .local/share/pipx/venvs/VENV_NAME/{bin,Scripts} should be uninstalled.
 
         # For non-symlink systems we give up and return an empty set.
         if not can_symlink(local_bin_dir):
diff --git a/src/pipx/constants.py b/src/pipx/constants.py
index bdc87a7..786e04e 100644
--- a/src/pipx/constants.py
+++ b/src/pipx/constants.py
@@ -5,20 +5,34 @@ from pathlib import Path
 from textwrap import dedent
 from typing import NewType, Optional
 
-DEFAULT_PIPX_HOME = Path.home() / ".local/pipx"
+from platformdirs import user_cache_path, user_data_path, user_log_path
+
+DEFAULT_PIPX_HOME = user_data_path("pipx")
+FALLBACK_PIPX_HOME = Path.home() / ".local/pipx"
 DEFAULT_PIPX_BIN_DIR = Path.home() / ".local/bin"
-PIPX_HOME = Path(os.environ.get("PIPX_HOME", DEFAULT_PIPX_HOME)).resolve()
-PIPX_LOCAL_VENVS = PIPX_HOME / "venvs"
-PIPX_LOG_DIR = PIPX_HOME / "logs"
-DEFAULT_PIPX_SHARED_LIBS = PIPX_HOME / "shared"
-PIPX_TRASH_DIR = PIPX_HOME / ".trash"
+
+if FALLBACK_PIPX_HOME.exists() or os.environ.get("PIPX_HOME") is not None:
+    PIPX_HOME = Path(os.environ.get("PIPX_HOME", FALLBACK_PIPX_HOME)).resolve()
+    PIPX_LOCAL_VENVS = PIPX_HOME / "venvs"
+    PIPX_LOG_DIR = PIPX_HOME / "logs"
+    DEFAULT_PIPX_SHARED_LIBS = PIPX_HOME / "shared"
+    PIPX_TRASH_DIR = PIPX_HOME / ".trash"
+    PIPX_VENV_CACHEDIR = PIPX_HOME / ".cache"
+else:
+    PIPX_HOME = DEFAULT_PIPX_HOME
+    PIPX_LOCAL_VENVS = PIPX_HOME / "venvs"
+    PIPX_LOG_DIR = user_log_path("pipx")
+    DEFAULT_PIPX_SHARED_LIBS = PIPX_HOME / "shared"
+    PIPX_TRASH_DIR = PIPX_HOME / "trash"
+    PIPX_VENV_CACHEDIR = user_cache_path("pipx")
+
 PIPX_SHARED_LIBS = Path(
     os.environ.get("PIPX_SHARED_LIBS", DEFAULT_PIPX_SHARED_LIBS)
 ).resolve()
 PIPX_SHARED_PTH = "pipx_shared.pth"
 LOCAL_BIN_DIR = Path(os.environ.get("PIPX_BIN_DIR", DEFAULT_PIPX_BIN_DIR)).resolve()
-PIPX_VENV_CACHEDIR = PIPX_HOME / ".cache"
 TEMP_VENV_EXPIRATION_THRESHOLD_DAYS = 14
+MINIMUM_PYTHON_VERSION = "3.8"
 
 ExitCode = NewType("ExitCode", int)
 # pipx shell exit codes
diff --git a/src/pipx/interpreter.py b/src/pipx/interpreter.py
index 331caac..8eb0f72 100644
--- a/src/pipx/interpreter.py
+++ b/src/pipx/interpreter.py
@@ -2,6 +2,7 @@ import os
 import shutil
 import subprocess
 import sys
+from typing import Optional
 
 from pipx.constants import WINDOWS
 from pipx.util import PipxError
@@ -26,14 +27,23 @@ def has_venv() -> bool:
 # so we try to locate the system Python and use that instead.
 
 
+def find_py_launcher_python(python_version: Optional[str] = None) -> Optional[str]:
+    py = shutil.which("py")
+    if py and python_version:
+        py = subprocess.run(
+            [py, f"-{python_version}", "-c", "import sys; print(sys.executable)"],
+            capture_output=True,
+            text=True,
+            check=True,
+        ).stdout.strip()
+    return py
+
+
 def _find_default_windows_python() -> str:
     if has_venv():
         return sys.executable
+    python = find_py_launcher_python() or shutil.which("python")
 
-    py = shutil.which("py")
-    if py:
-        return py
-    python = shutil.which("python")
     if python is None:
         raise PipxError("No suitable Python found")
 
@@ -45,7 +55,7 @@ def _find_default_windows_python() -> str:
     # https://twitter.com/zooba/status/1212454929379581952
 
     proc = subprocess.run(
-        [python, "-V"], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL
+        [python, "-V"], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, check=False
     )
     if proc.returncode != 0:
         # Cover the 9009 return code pre-emptively.
@@ -66,7 +76,7 @@ def _get_sys_executable() -> str:
 def _get_absolute_python_interpreter(env_python: str) -> str:
     which_python = shutil.which(env_python)
     if not which_python:
-        raise PipxError(f"Default python interpreter {repr(env_python)} is invalid.")
+        raise PipxError(f"Default python interpreter '{env_python}' is invalid.")
     return which_python
 
 
diff --git a/src/pipx/main.py b/src/pipx/main.py
index 59f6222..25f9ad0 100644
--- a/src/pipx/main.py
+++ b/src/pipx/main.py
@@ -23,9 +23,9 @@ import pipx.constants
 from pipx import commands, constants
 from pipx.animate import hide_cursor, show_cursor
 from pipx.colors import bold, green
-from pipx.constants import WINDOWS, ExitCode
+from pipx.constants import MINIMUM_PYTHON_VERSION, WINDOWS, ExitCode
 from pipx.emojis import hazard
-from pipx.interpreter import DEFAULT_PYTHON
+from pipx.interpreter import DEFAULT_PYTHON, find_py_launcher_python
 from pipx.util import PipxError, mkdir, pipx_wrap, rmdir
 from pipx.venv import VenvContainer
 from pipx.version import __version__
@@ -181,6 +181,10 @@ def run_pipx_command(args: argparse.Namespace) -> ExitCode:  # noqa: C901
         logger.info(f"Virtual Environment location is {venv_dir}")
     if "skip" in args:
         skip_list = [canonicalize_name(x) for x in args.skip]
+    if "python" in args and not Path(args.python).is_file():
+        py_launcher_python = find_py_launcher_python(args.python)
+        if py_launcher_python:
+            args.python = py_launcher_python
 
     if args.command == "run":
         commands.run(
@@ -208,7 +212,9 @@ def run_pipx_command(args: argparse.Namespace) -> ExitCode:  # noqa: C901
             venv_args,
             verbose,
             force=args.force,
+            reinstall=False,
             include_dependencies=args.include_deps,
+            preinstall_packages=args.preinstall,
             suffix=args.suffix,
         )
     elif args.command == "inject":
@@ -221,6 +227,7 @@ def run_pipx_command(args: argparse.Namespace) -> ExitCode:  # noqa: C901
             include_apps=args.include_apps,
             include_dependencies=args.include_deps,
             force=args.force,
+            suffix=args.with_suffix,
         )
     elif args.command == "uninject":
         return commands.uninject(
@@ -339,10 +346,19 @@ def _add_install(subparsers: argparse._SubParsersAction) -> None:
     )
     p.add_argument(
         "--python",
-        default=DEFAULT_PYTHON,
+        # Don't pass a default Python here so we know whether --python flag was passed
+        help=(
+            "Python to install with. Possible values can be the executable name (python3.11), "
+            "the version to pass to py launcher (3.11), or the full path to the executable."
+            f"Requires Python {MINIMUM_PYTHON_VERSION} or above."
+        ),
+    )
+    p.add_argument(
+        "--preinstall",
+        action="append",
         help=(
-            "The Python executable used to create the Virtual Environment and run the "
-            "associated app/apps. Must be v3.6+."
+            "Optional packages to be installed into the Virtual Environment before "
+            "installing the main package."
         ),
     )
     add_pip_venv_args(p)
@@ -368,7 +384,11 @@ def _add_inject(subparsers, venv_completer: VenvCompleter) -> None:
         action="store_true",
         help="Add apps from the injected packages onto your PATH",
     )
-    add_include_dependencies(p)
+    p.add_argument(
+        "--include-deps",
+        help="Include apps of dependent packages. Implies --include-apps",
+        action="store_true",
+    )
     add_pip_venv_args(p)
     p.add_argument(
         "--force",
@@ -377,6 +397,11 @@ def _add_inject(subparsers, venv_completer: VenvCompleter) -> None:
         help="Modify existing virtual environment and files in PIPX_BIN_DIR",
     )
     p.add_argument("--verbose", action="store_true")
+    p.add_argument(
+        "--with-suffix",
+        action="store_true",
+        help="Add the suffix (if given) of the Virtual Environment to the packages to inject",
+    )
 
 
 def _add_uninject(subparsers, venv_completer: VenvCompleter):
@@ -484,8 +509,9 @@ def _add_reinstall(subparsers, venv_completer: VenvCompleter) -> None:
         "--python",
         default=DEFAULT_PYTHON,
         help=(
-            "The Python executable used to recreate the Virtual Environment "
-            "and run the associated app/apps. Must be v3.6+."
+            "Python to reinstall with. Possible values can be the executable name (python3.11), "
+            "the version to pass to py launcher (3.11), or the full path to the executable."
+            f"Requires Python {MINIMUM_PYTHON_VERSION} or above."
         ),
     )
     p.add_argument("--verbose", action="store_true")
@@ -512,8 +538,9 @@ def _add_reinstall_all(subparsers: argparse._SubParsersAction) -> None:
         "--python",
         default=DEFAULT_PYTHON,
         help=(
-            "The Python executable used to recreate the Virtual Environment "
-            "and run the associated app/apps. Must be v3.6+."
+            "Python to reinstall with. Possible values can be the executable name (python3.11), "
+            "the version to pass to py launcher (3.11), or the full path to the executable."
+            f"Requires Python {MINIMUM_PYTHON_VERSION} or above."
         ),
     )
     p.add_argument("--skip", nargs="+", default=[], help="skip these packages")
@@ -588,7 +615,11 @@ def _add_run(subparsers: argparse._SubParsersAction) -> None:
     p.add_argument(
         "--python",
         default=DEFAULT_PYTHON,
-        help="The Python version to run package's CLI app with. Must be v3.6+.",
+        help=(
+            "Python to run with. Possible values can be the executable name (python3.11), "
+            "the version to pass to py launcher (3.11), or the full path to the executable. "
+            f"Requires Python {MINIMUM_PYTHON_VERSION} or above."
+        ),
     )
     add_pip_venv_args(p)
     p.set_defaults(subparser=p)
@@ -648,15 +679,16 @@ def _add_environment(subparsers: argparse._SubParsersAction) -> None:
     p = subparsers.add_parser(
         "environment",
         formatter_class=LineWrapRawTextHelpFormatter,
-        help=("Print a list of variables used in pipx.constants."),
+        help="Print a list of environment variables and paths used by pipx.",
         description=textwrap.dedent(
             """
+            Prints the names and current values of environment variables used by pipx,
+            followed by internal pipx variables which are derived from the environment
+            variables and platform specific default values.
+
             Available variables:
             PIPX_HOME, PIPX_BIN_DIR, PIPX_SHARED_LIBS, PIPX_LOCAL_VENVS, PIPX_LOG_DIR,
-            PIPX_TRASH_DIR, PIPX_VENV_CACHEDIR
-
-            Only PIPX_HOME and PIPX_BIN_DIR can be set by users in the above list.
-
+            PIPX_TRASH_DIR, PIPX_VENV_CACHEDIR, PIPX_DEFAULT_PYTHON, USE_EMOJI
             """
         ),
     )
@@ -797,7 +829,7 @@ def setup(args: argparse.Namespace) -> None:
     logger.debug(f"{time.strftime('%Y-%m-%d %H:%M:%S')}")
     logger.debug(f"{' '.join(sys.argv)}")
     logger.info(f"pipx version is {__version__}")
-    logger.info(f"Default python interpreter is {repr(DEFAULT_PYTHON)}")
+    logger.info(f"Default python interpreter is '{DEFAULT_PYTHON}'")
 
     mkdir(constants.PIPX_LOCAL_VENVS)
     mkdir(constants.LOCAL_BIN_DIR)
diff --git a/src/pipx/package_specifier.py b/src/pipx/package_specifier.py
index ad48018..c4f47bc 100644
--- a/src/pipx/package_specifier.py
+++ b/src/pipx/package_specifier.py
@@ -8,6 +8,7 @@
 
 import logging
 import re
+import urllib.parse
 from pathlib import Path
 from typing import List, NamedTuple, Optional, Set, Tuple
 
@@ -73,24 +74,17 @@ def _parse_specifier(package_spec: str) -> ParsedPackage:
         else:
             raise PipxError(f"{package_path} does not exist")
 
-    # packaging currently (2020-07-19) only does basic syntax checks on URL.
-    #   Some examples of what it will not catch:
-    #       - invalid RCS string (e.g. "gat+https://...")
-    #       - non-existent scheme (e.g. "zzzzz://...")
+    # If this looks like a URL, treat it as such.
     if not valid_pep508:
-        try:
-            package_req = Requirement("notapackagename @ " + package_spec)
-        except InvalidRequirement:
-            # not a valid url
-            pass
-        else:
+        parsed_url = urllib.parse.urlsplit(package_spec)
+        if parsed_url.scheme and parsed_url.netloc:
             valid_url = package_spec
 
+    # Treat the input as a local path if it does not look like a PEP 508
+    # specifier nor a URL. In this case we want to split out the extra part.
     if not valid_pep508 and not valid_url:
         (package_path_str, package_extras_str) = _split_path_extras(package_spec)
-
         (package_path, package_path_exists) = _check_package_path(package_path_str)
-
         if package_path_exists:
             valid_local_path = str(package_path.resolve()) + package_extras_str
 
diff --git a/src/pipx/pipx_metadata_file.py b/src/pipx/pipx_metadata_file.py
index b3db6f7..54b4247 100644
--- a/src/pipx/pipx_metadata_file.py
+++ b/src/pipx/pipx_metadata_file.py
@@ -133,7 +133,7 @@ class PipxMetadata:
                     sort_keys=True,
                     cls=JsonEncoderHandlesPath,
                 )
-        except IOError:
+        except OSError:
             logger.warning(
                 pipx_wrap(
                     f"""
@@ -152,7 +152,7 @@ class PipxMetadata:
                 self.from_dict(
                     json.load(pipx_metadata_fh, object_hook=_json_decoder_object_hook)
                 )
-        except IOError:  # Reset self if problem reading
+        except OSError:  # Reset self if problem reading
             if verbose:
                 logger.warning(
                     pipx_wrap(
diff --git a/src/pipx/shared_libs.py b/src/pipx/shared_libs.py
index 5fc78f5..5a752c9 100644
--- a/src/pipx/shared_libs.py
+++ b/src/pipx/shared_libs.py
@@ -101,6 +101,7 @@ class _SharedLibs:
                         self.python_path,
                         "-m",
                         "pip",
+                        "--no-input",
                         "--disable-pip-version-check",
                         "install",
                         *_pip_args,
diff --git a/src/pipx/util.py b/src/pipx/util.py
index b390fff..c7a7670 100644
--- a/src/pipx/util.py
+++ b/src/pipx/util.py
@@ -180,6 +180,7 @@ def run_subprocess(
         stderr=subprocess.PIPE if capture_stderr else None,
         encoding="utf-8",
         universal_newlines=True,
+        check=False,
     )
 
     if capture_stdout and log_stdout:
@@ -396,6 +397,7 @@ def exec_app(
                 stderr=None,
                 encoding="utf-8",
                 universal_newlines=True,
+                check=False,
             ).returncode
         )
     else:
diff --git a/src/pipx/venv.py b/src/pipx/venv.py
index b6d01ce..28189cc 100644
--- a/src/pipx/venv.py
+++ b/src/pipx/venv.py
@@ -201,7 +201,7 @@ class Venv:
         else:
             # TODO: setuptools and wheel? Original code didn't bother
             # but shared libs code does.
-            self._upgrade_package_no_metadata("pip", pip_args)
+            self.upgrade_package_no_metadata("pip", pip_args)
 
     def uninstall_package(self, package: str, was_injected: bool = False):
         try:
@@ -240,11 +240,15 @@ class Venv:
         ):
             # do not use -q with `pip install` so subprocess_post_check_pip_errors
             #   has more information to analyze in case of failure.
-            cmd = (
-                [str(self.python_path), "-m", "pip", "install"]
-                + pip_args
-                + [package_or_url]
-            )
+            cmd = [
+                str(self.python_path),
+                "-m",
+                "pip",
+                "--no-input",
+                "install",
+                *pip_args,
+                package_or_url,
+            ]
             # 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)
@@ -284,11 +288,15 @@ class Venv:
         with animate(f"installing {', '.join(requirements)}", self.do_animation):
             # do not use -q with `pip install` so subprocess_post_check_pip_errors
             #   has more information to analyze in case of failure.
-            cmd = (
-                [str(self.python_path), "-m", "pip", "install"]
-                + pip_args
-                + requirements
-            )
+            cmd = [
+                str(self.python_path),
+                "-m",
+                "pip",
+                "--no-input",
+                "install",
+                *pip_args,
+                *requirements,
+            ]
             # 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)
@@ -301,7 +309,13 @@ class Venv:
             f"determining package name from {package_or_url!r}", self.do_animation
         ):
             old_package_set = self.list_installed_packages()
-            cmd = ["install"] + ["--no-dependencies"] + pip_args + [package_or_url]
+            cmd = [
+                "--no-input",
+                "install",
+                "--no-dependencies",
+                *pip_args,
+                package_or_url,
+            ]
             pip_process = self._run_pip(cmd)
         subprocess_post_check(pip_process, raise_error=False)
         if pip_process.returncode:
@@ -421,7 +435,7 @@ class Venv:
             return True
         return (self.bin_path / filename).is_file()
 
-    def _upgrade_package_no_metadata(
+    def upgrade_package_no_metadata(
         self, package_name: str, pip_args: List[str]
     ) -> None:
         with animate(
@@ -429,7 +443,7 @@ class Venv:
             self.do_animation,
         ):
             pip_process = self._run_pip(
-                ["install"] + pip_args + ["--upgrade", package_name]
+                ["--no-input", "install"] + pip_args + ["--upgrade", package_name]
             )
         subprocess_post_check(pip_process)
 
@@ -448,7 +462,7 @@ class Venv:
             self.do_animation,
         ):
             pip_process = self._run_pip(
-                ["install"] + pip_args + ["--upgrade", package_or_url]
+                ["--no-input", "install"] + pip_args + ["--upgrade", package_or_url]
             )
         subprocess_post_check(pip_process)
 
diff --git a/src/pipx/venv_inspect.py b/src/pipx/venv_inspect.py
index 3099c3c..3b794cb 100644
--- a/src/pipx/venv_inspect.py
+++ b/src/pipx/venv_inspect.py
@@ -2,7 +2,7 @@ import json
 import logging
 import textwrap
 from pathlib import Path
-from typing import Dict, List, NamedTuple, Optional, Set, Tuple
+from typing import Collection, Dict, List, NamedTuple, Optional, Set, Tuple
 
 from packaging.requirements import Requirement
 from packaging.utils import canonicalize_name
@@ -19,7 +19,7 @@ logger = logging.getLogger(__name__)
 
 
 class VenvInspectInformation(NamedTuple):
-    distributions: List[metadata.Distribution]
+    distributions: Collection[metadata.Distribution]
     env: Dict[str, str]
     bin_path: Path
 
@@ -34,7 +34,7 @@ class VenvMetadata(NamedTuple):
 
 
 def get_dist(
-    package: str, distributions: List[metadata.Distribution]
+    package: str, distributions: Collection[metadata.Distribution]
 ) -> Optional[metadata.Distribution]:
     """Find matching distribution in the canonicalized sense."""
     for dist in distributions:
@@ -130,7 +130,7 @@ def _dfs_package_apps(
         dep_dist = get_dist(dep_req.name, venv_inspect_info.distributions)
         if dep_dist is None:
             raise PipxError(
-                "Pipx Internal Error: cannot find package {dep_req.name!r} metadata."
+                f"Pipx Internal Error: cannot find package {dep_req.name!r} metadata."
             )
         app_names = get_apps(dep_dist, venv_inspect_info.bin_path)
         if app_names:
@@ -237,16 +237,22 @@ def inspect_venv(
         venv_python_path
     )
 
+    # Collect the generator created from metadata.distributions()
+    # (see `itertools.chain.from_iterable`) into a tuple because we
+    # need to iterate over it multiple times in `_dfs_package_apps`.
+
+    # Tuple is chosen over a list because the program only iterate over
+    # the distributions and never modify it.
+    distributions = tuple(metadata.distributions(path=venv_sys_path))
+
     venv_inspect_info = VenvInspectInformation(
-        bin_path=venv_bin_path,
-        env=venv_env,
-        distributions=list(metadata.distributions(path=venv_sys_path)),
+        bin_path=venv_bin_path, env=venv_env, distributions=distributions
     )
 
     root_dist = get_dist(root_req.name, venv_inspect_info.distributions)
     if root_dist is None:
         raise PipxError(
-            "Pipx Internal Error: cannot find package {root_req.name!r} metadata."
+            f"Pipx Internal Error: cannot find package {root_req.name!r} metadata."
         )
     app_paths_of_dependencies = _dfs_package_apps(
         root_dist, root_req, venv_inspect_info, app_paths_of_dependencies


Test patch:
diff --git a/.github/workflows/create_tests_package_lists.yml b/.github/workflows/create_tests_package_lists.yml
index b6f23e6..27a9b52 100644
--- a/.github/workflows/create_tests_package_lists.yml
+++ b/.github/workflows/create_tests_package_lists.yml
@@ -9,10 +9,10 @@ jobs:
     strategy:
       matrix:
         os: [ubuntu-latest, macos-latest, windows-latest]
-        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
+        python-version: ["3.8", "3.9", "3.10", "3.11"]
 
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
     - name: Set up Python ${{ matrix.python-version }}
       uses: actions/setup-python@v4
       with:
diff --git a/.github/workflows/exhaustive_package_test.yml b/.github/workflows/exhaustive_package_test.yml
index cff504f..09885bb 100644
--- a/.github/workflows/exhaustive_package_test.yml
+++ b/.github/workflows/exhaustive_package_test.yml
@@ -10,10 +10,10 @@ jobs:
     strategy:
       matrix:
         os: [ubuntu-latest, macos-latest, windows-latest]
-        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
+        python-version: ["3.8", "3.9", "3.10", "3.11"]
 
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
     - name: Set up Python ${{ matrix.python-version }}
       uses: actions/setup-python@v4
       with:
diff --git a/.github/workflows/publish-testpypi.yml b/.github/workflows/publish-testpypi.yml
index 35a02f9..ea04d7e 100644
--- a/.github/workflows/publish-testpypi.yml
+++ b/.github/workflows/publish-testpypi.yml
@@ -19,7 +19,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - name: Checkout ${{ github.ref }}
-      uses: actions/checkout@v3
+      uses: actions/checkout@v4
     - name: Set up Python ${{ env.default-python }}
       uses: actions/setup-python@v4
       with:
@@ -32,7 +32,7 @@ jobs:
       run: |
         nox --error-on-missing-interpreters --non-interactive --session build
     - name: Publish to Test PyPi
-      uses: pypa/gh-action-pypi-publish@v1.8.3
+      uses: pypa/gh-action-pypi-publish@v1.8.11
       with:
         user: __token__
         password: ${{ secrets.test_pypi_password }}
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 82bf875..6a7b9e1 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -20,7 +20,7 @@ jobs:
   lint:
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
     - name: Set up Python ${{ env.default-python }}
       uses: actions/setup-python@v4
       with:
@@ -36,7 +36,7 @@ jobs:
   verify-docs:
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
     - name: Set up Python ${{ env.default-python }}
       uses: actions/setup-python@v4
       with:
@@ -55,7 +55,7 @@ jobs:
       fail-fast: false
       matrix:
         os: [ubuntu-latest]
-        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
+        python-version: ["3.8", "3.9", "3.10", "3.11"]
         include:
           - os: windows-latest
             python-version: "3.11"
@@ -63,7 +63,7 @@ jobs:
             python-version: "3.11"
 
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
     - name: Set up Python ${{ matrix.python-version }}
       uses: actions/setup-python@v4
       with:
@@ -97,7 +97,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - name: Checkout ${{ github.ref }}
-      uses: actions/checkout@v3
+      uses: actions/checkout@v4
     - name: Set up Python ${{ env.default-python }}
       uses: actions/setup-python@v4
       with:
@@ -110,7 +110,7 @@ jobs:
       run: |
         nox --error-on-missing-interpreters --non-interactive --session build
     - name: Publish to PyPi
-      uses: pypa/gh-action-pypi-publish@v1.8.3
+      uses: pypa/gh-action-pypi-publish@v1.8.11
       with:
         user: __token__
         password: ${{ secrets.pypi_password }}
@@ -122,7 +122,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout ${{ github.ref }}
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       - name: Set up Python ${{ env.default-python }}
         uses: actions/setup-python@v4
         with:
diff --git a/scripts/list_test_packages.py b/scripts/list_test_packages.py
index 550a2e0..139552d 100644
--- a/scripts/list_test_packages.py
+++ b/scripts/list_test_packages.py
@@ -76,7 +76,7 @@ def parse_package_list(package_list_file: Path) -> List[Dict[str, Any]]:
                         f"ERROR: Unable to parse primary package list line:\n    {line.strip()}"
                     )
                     return []
-    except IOError:
+    except OSError:
         print("ERROR: File problem reading primary package list.")
         return []
     return output_list
@@ -111,10 +111,7 @@ def create_test_packages_list(
             if verbose:
                 print(f"CMD: {' '.join(cmd_list)}")
             pip_download_process = subprocess.run(
-                cmd_list,
-                stdout=subprocess.PIPE,
-                stderr=subprocess.PIPE,
-                universal_newlines=True,
+                cmd_list, capture_output=True, text=True, check=False
             )
             if pip_download_process.returncode == 0:
                 print(f"Examined {test_package['spec']}{test_package_option_string}")
@@ -133,7 +130,8 @@ def create_test_packages_list(
     all_packages = []
     for downloaded_filename in downloaded_list:
         wheel_re = re.search(
-            r"(.+)\-([^-]+)\-([^-]+)\-([^-]+)\-([^-]+)\.whl$", downloaded_filename
+            r"([^-]+)\-([^-]+)\-([^-]+)\-([^-]+)\-([^-]+)(-[^-]+)?\.whl$",
+            downloaded_filename,
         )
         src_re = re.search(r"(.+)\-([^-]+)\.(?:tar.gz|zip)$", downloaded_filename)
         if wheel_re:
@@ -149,7 +147,6 @@ def create_test_packages_list(
         all_packages.append(f"{package_name}=={package_version}")
 
     with platform_package_list_path.open("w") as package_list_fh:
-        "scripts/list_test_packages.py",
         for package in sorted(all_packages):
             print(package, file=package_list_fh)
 
diff --git a/testdata/tests_packages/macos21-python3.7.txt b/testdata/tests_packages/macos21-python3.7.txt
index 4bc92d6..7a61081 100644
--- a/testdata/tests_packages/macos21-python3.7.txt
+++ b/testdata/tests_packages/macos21-python3.7.txt
@@ -20,7 +20,7 @@ attrs==22.1.0
 awscli==1.18.168
 backcall==0.2.0
 beautifulsoup4==4.11.1
-black-22.10.0==1fixedarch
+black==22.10.0
 black==22.8.0
 bleach==5.0.1
 boto3==1.26.20
diff --git a/testdata/tests_packages/macos21-python3.8.txt b/testdata/tests_packages/macos21-python3.8.txt
index 0ffdc9f..3094f29 100644
--- a/testdata/tests_packages/macos21-python3.8.txt
+++ b/testdata/tests_packages/macos21-python3.8.txt
@@ -21,7 +21,7 @@ attrs==22.1.0
 awscli==1.18.168
 backcall==0.2.0
 beautifulsoup4==4.11.1
-black-22.10.0==1fixedarch
+black==22.10.0
 black==22.8.0
 bleach==5.0.1
 boto3==1.26.20
diff --git a/tests/conftest.py b/tests/conftest.py
index cc32319..a765420 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,4 +1,5 @@
 import os
+import shutil
 import subprocess
 import sys
 from pathlib import Path
@@ -6,7 +7,7 @@ from pathlib import Path
 import pytest  # type: ignore
 
 from helpers import WIN
-from pipx import commands, constants, shared_libs, venv
+from pipx import commands, constants, interpreter, shared_libs, venv
 
 PIPX_TESTS_DIR = Path(".pipx_tests")
 PIPX_TESTS_PACKAGE_LIST_DIR = Path("testdata/tests_packages")
@@ -40,7 +41,9 @@ def pytest_configure(config):
     config.option.markexpr = new_markexpr
 
 
-def pipx_temp_env_helper(pipx_shared_dir, tmp_path, monkeypatch, request):
+def pipx_temp_env_helper(
+    pipx_shared_dir, tmp_path, monkeypatch, request, utils_temp_dir
+):
     home_dir = Path(tmp_path) / "subdir" / "pipxhome"
     bin_dir = Path(tmp_path) / "otherdir" / "pipxbindir"
 
@@ -54,12 +57,17 @@ def pipx_temp_env_helper(pipx_shared_dir, tmp_path, monkeypatch, request):
     monkeypatch.setattr(constants, "PIPX_VENV_CACHEDIR", home_dir / ".cache")
     monkeypatch.setattr(constants, "PIPX_LOG_DIR", home_dir / "logs")
 
+    monkeypatch.setattr(interpreter, "DEFAULT_PYTHON", sys.executable)
+
+    if "PIPX_DEFAULT_PYTHON" in os.environ:
+        monkeypatch.delenv("PIPX_DEFAULT_PYTHON")
+
     # macOS needs /usr/bin in PATH to compile certain packages, but
     #   applications in /usr/bin cause test_install.py tests to raise warnings
     #   which make tests fail (e.g. on Github ansible apps exist in /usr/bin)
     monkeypatch.setenv("PATH_ORIG", str(bin_dir) + os.pathsep + os.getenv("PATH"))
     monkeypatch.setenv("PATH_TEST", str(bin_dir))
-    monkeypatch.setenv("PATH", str(bin_dir))
+    monkeypatch.setenv("PATH", str(bin_dir) + os.pathsep + str(utils_temp_dir))
     # On Windows, monkeypatch pipx.commands.common._can_symlink_cache to
     #   indicate that constants.LOCAL_BIN_DIR cannot use symlinks, even if
     #   we're running as administrator and symlinks are actually possible.
@@ -71,7 +79,9 @@ def pipx_temp_env_helper(pipx_shared_dir, tmp_path, monkeypatch, request):
         # IMPORTANT: use 127.0.0.1 not localhost
         #   Using localhost on Windows creates enormous slowdowns
         #   (for some reason--perhaps IPV6/IPV4 tries, timeouts?)
-        monkeypatch.setenv("PIP_INDEX_URL", "http://127.0.0.1:8080/simple")
+        monkeypatch.setenv(
+            "PIP_INDEX_URL", "http://username:password@127.0.0.1:8080/simple"
+        )
 
 
 @pytest.fixture(scope="session", autouse=True)
@@ -87,19 +97,19 @@ def pipx_local_pypiserver(request):
         request.config.invocation_params.dir / PIPX_TESTS_DIR / "package_cache"
     )
     check_test_packages_cmd = [
-        "python3",
+        sys.executable,
         "scripts/update_package_cache.py",
         "--check-only",
         str(PIPX_TESTS_PACKAGE_LIST_DIR),
         str(pipx_cache_dir),
     ]
     update_test_packages_cmd = [
-        "python3",
+        sys.executable,
         "scripts/update_package_cache.py",
         str(PIPX_TESTS_PACKAGE_LIST_DIR),
         str(pipx_cache_dir),
     ]
-    check_test_packages_process = subprocess.run(check_test_packages_cmd)
+    check_test_packages_process = subprocess.run(check_test_packages_cmd, check=False)
     if check_test_packages_process.returncode != 0:
         raise Exception(
             f"Directory {str(pipx_cache_dir)} does not contain all "
@@ -108,24 +118,36 @@ def pipx_local_pypiserver(request):
             f"{' '.join(update_test_packages_cmd)}"
         )
 
-    pypiserver_err_fh = open(
+    with open(
         request.config.invocation_params.dir / PIPX_TESTS_DIR / "pypiserver.log", "w"
-    )
-    pypiserver_process = subprocess.Popen(
-        [
-            "pypi-server",
-            "--authenticate=update",
-            "--disable-fallback",
-            str(pipx_cache_dir / f"{sys.version_info[0]}.{sys.version_info[1]}"),
-        ],
-        universal_newlines=True,
-        stderr=pypiserver_err_fh,
-    )
+    ) as pypiserver_err_fh:
+        pypiserver_htpasswd = str(
+            request.config.invocation_params.dir / PIPX_TESTS_DIR / "htpasswd"
+        )
 
-    yield
+        from passlib.apache import HtpasswdFile  # type: ignore
+
+        ht = HtpasswdFile(pypiserver_htpasswd, new=True)
+        ht.set_password("username", "password")
+        ht.save()
+
+        pypiserver_process = subprocess.Popen(
+            [
+                "pypi-server",
+                "run",
+                "--verbose",
+                "--authenticate=update,download,list",
+                f"--passwords={pypiserver_htpasswd}",
+                "--disable-fallback",
+                str(pipx_cache_dir / f"{sys.version_info[0]}.{sys.version_info[1]}"),
+            ],
+            universal_newlines=True,
+            stderr=pypiserver_err_fh,
+        )
 
-    pypiserver_process.terminate()
-    pypiserver_err_fh.close()
+        yield
+
+        pypiserver_process.terminate()
 
 
 @pytest.fixture(scope="session")
@@ -134,8 +156,23 @@ def pipx_session_shared_dir(tmp_path_factory):
     return tmp_path_factory.mktemp("session_shareddir")
 
 
+@pytest.fixture(scope="session")
+def utils_temp_dir(tmp_path_factory):
+    tmp_path = tmp_path_factory.mktemp("session_utilstempdir")
+    utils = ["git"]
+    for util in utils:
+        util_path = Path(shutil.which(util))
+        try:
+            (tmp_path / util_path.name).symlink_to(util_path)
+        except FileExistsError:
+            pass
+    return tmp_path
+
+
 @pytest.fixture
-def pipx_temp_env(tmp_path, monkeypatch, pipx_session_shared_dir, request):
+def pipx_temp_env(
+    tmp_path, monkeypatch, pipx_session_shared_dir, request, utils_temp_dir
+):
     """Sets up temporary paths for pipx to install into.
 
     Shared libs are setup once per session, all other pipx dirs, constants are
@@ -144,11 +181,13 @@ def pipx_temp_env(tmp_path, monkeypatch, pipx_session_shared_dir, request):
     Also adds environment variables as necessary to make pip installations
     seamless.
     """
-    pipx_temp_env_helper(pipx_session_shared_dir, tmp_path, monkeypatch, request)
+    pipx_temp_env_helper(
+        pipx_session_shared_dir, tmp_path, monkeypatch, request, utils_temp_dir
+    )
 
 
 @pytest.fixture
-def pipx_ultra_temp_env(tmp_path, monkeypatch, request):
+def pipx_ultra_temp_env(tmp_path, monkeypatch, request, utils_temp_dir):
     """Sets up temporary paths for pipx to install into.
 
     Fully temporary environment, every test function starts as if pipx has
@@ -158,4 +197,4 @@ def pipx_ultra_temp_env(tmp_path, monkeypatch, request):
     seamless.
     """
     shared_dir = Path(tmp_path) / "shareddir"
-    pipx_temp_env_helper(shared_dir, tmp_path, monkeypatch, request)
+    pipx_temp_env_helper(shared_dir, tmp_path, monkeypatch, request, utils_temp_dir)
diff --git a/tests/test_environment.py b/tests/test_environment.py
index d15cabe..8c55d94 100644
--- a/tests/test_environment.py
+++ b/tests/test_environment.py
@@ -11,10 +11,9 @@ def test_cli(monkeypatch, capsys):
     assert "PIPX_LOG_DIR" in captured.out
     assert "PIPX_TRASH_DIR" in captured.out
     assert "PIPX_VENV_CACHEDIR" in captured.out
-    assert (
-        "Only PIPX_HOME and PIPX_BIN_DIR can be set by users in the above list."
-        in captured.out
-    )
+    assert "PIPX_DEFAULT_PYTHON" in captured.out
+    assert "USE_EMOJI" in captured.out
+    assert "Environment variables (set by user):" in captured.out
 
 
 def test_cli_with_args(monkeypatch, capsys):
@@ -25,6 +24,8 @@ def test_cli_with_args(monkeypatch, capsys):
     assert not run_pipx_cli(["environment", "--value", "PIPX_LOG_DIR"])
     assert not run_pipx_cli(["environment", "--value", "PIPX_TRASH_DIR"])
     assert not run_pipx_cli(["environment", "--value", "PIPX_VENV_CACHEDIR"])
+    assert not run_pipx_cli(["environment", "--value", "PIPX_DEFAULT_PYTHON"])
+    assert not run_pipx_cli(["environment", "--value", "USE_EMOJI"])
 
     assert run_pipx_cli(["environment", "--value", "SSS"])
     captured = capsys.readouterr()
diff --git a/tests/test_inject.py b/tests/test_inject.py
index 2e891db..4cf262e 100644
--- a/tests/test_inject.py
+++ b/tests/test_inject.py
@@ -41,27 +41,15 @@ def test_inject_include_apps(pipx_temp_env, capsys, with_suffix):
         install_args = [f"--suffix={suffix}"]
 
     assert not run_pipx_cli(["install", "pycowsay", *install_args])
-    assert run_pipx_cli(
+    assert not run_pipx_cli(
         ["inject", f"pycowsay{suffix}", PKG["black"]["spec"], "--include-deps"]
     )
 
     if suffix:
         assert run_pipx_cli(
-            [
-                "inject",
-                "pycowsay",
-                PKG["black"]["spec"],
-                "--include-deps",
-                "--include-apps",
-            ]
+            ["inject", "pycowsay", PKG["black"]["spec"], "--include-deps"]
         )
 
     assert not run_pipx_cli(
-        [
-            "inject",
-            f"pycowsay{suffix}",
-            PKG["black"]["spec"],
-            "--include-deps",
-            "--include-apps",
-        ]
+        ["inject", f"pycowsay{suffix}", PKG["black"]["spec"], "--include-deps"]
     )
diff --git a/tests/test_install.py b/tests/test_install.py
index a135b86..87fa2a8 100644
--- a/tests/test_install.py
+++ b/tests/test_install.py
@@ -71,11 +71,10 @@ def test_install_tricky_packages(
     install_package(capsys, pipx_temp_env, caplog, package_spec, package_name)
 
 
-# TODO: Add git+... spec when git is in binpath of tests (Issue #303)
 @pytest.mark.parametrize(
     "package_name, package_spec",
     [
-        # ("nox", "git+https://github.com/cs01/nox.git@5ea70723e9e6"),
+        ("pycowsay", "git+https://github.com/cs01/pycowsay.git@master"),
         ("pylint", PKG["pylint"]["spec"]),
         ("nox", "https://github.com/wntrblm/nox/archive/2022.1.7.zip"),
     ],
@@ -144,7 +143,7 @@ def test_extra(pipx_temp_env, capsys):
 
 def test_install_local_extra(pipx_temp_env, capsys):
     assert not run_pipx_cli(
-        ["install", TEST_DATA_PATH + "/local_extras[cow]", "--include-deps"]
+        ["install", f"{TEST_DATA_PATH}/local_extras[cow]", "--include-deps"]
     )
     captured = capsys.readouterr()
     assert f"- {app_name('pycowsay')}\n" in captured.out
@@ -250,7 +249,7 @@ def test_install_pip_failure(pipx_temp_env, capsys):
         r"Full pip output in file:\s+(\S.+)$", captured.err, re.MULTILINE
     )
     assert pip_log_file_match
-    assert Path(pip_log_file_match.group(1)).exists()
+    assert Path(pip_log_file_match[1]).exists()
 
     assert re.search(r"pip (failed|seemed to fail) to build package", captured.err)
 
@@ -258,7 +257,41 @@ def test_install_pip_failure(pipx_temp_env, capsys):
 def test_install_local_archive(pipx_temp_env, monkeypatch, capsys):
     monkeypatch.chdir(Path(TEST_DATA_PATH) / "local_extras")
 
-    subprocess.run([sys.executable, "-m", "pip", "wheel", "."])
+    subprocess.run([sys.executable, "-m", "pip", "wheel", "."], check=True)
     assert not run_pipx_cli(["install", "repeatme-0.1-py3-none-any.whl"])
     captured = capsys.readouterr()
     assert f"- {app_name('repeatme')}\n" in captured.out
+
+
+def test_force_install_changes(pipx_temp_env, capsys):
+    assert not run_pipx_cli(
+        ["install", "https://github.com/wntrblm/nox/archive/2022.1.7.zip"]
+    )
+    captured = capsys.readouterr()
+    assert "2022.1.7" in captured.out
+
+    assert not run_pipx_cli(["install", "nox", "--force"])
+    captured = capsys.readouterr()
+    assert "2022.1.7" not in captured.out
+
+
+def test_preinstall(pipx_temp_env, caplog):
+    assert not run_pipx_cli(["install", "--preinstall", "black", "nox"])
+    assert "black" in caplog.text
+
+
+@pytest.mark.xfail
+def test_do_not_wait_for_input(pipx_temp_env, pipx_session_shared_dir, monkeypatch):
+    monkeypatch.setenv("PIP_INDEX_URL", "http://127.0.0.1:8080/simple")
+    run_pipx_cli(["install", "pycowsay"])
+
+
+def test_passed_python_and_force_flag_warning(pipx_temp_env, capsys):
+    assert not run_pipx_cli(["install", "black"])
+    assert not run_pipx_cli(["install", "--python", sys.executable, "--force", "black"])
+    captured = capsys.readouterr()
+    assert "--python is ignored when --force is passed." in captured.out
+
+    assert not run_pipx_cli(["install", "pycowsay", "--force"])
+    captured = capsys.readouterr()
+    assert "--python is ignored when --force is passed." not in captured.out
diff --git a/tests/test_install_all_packages.py b/tests/test_install_all_packages.py
index 626d86b..9c84496 100644
--- a/tests/test_install_all_packages.py
+++ b/tests/test_install_all_packages.py
@@ -163,7 +163,7 @@ def module_globals() -> ModuleGlobalsData:
 
 
 def pip_cache_purge() -> None:
-    subprocess.run([sys.executable, "-m", "pip", "cache", "purge"])
+    subprocess.run([sys.executable, "-m", "pip", "cache", "purge"], check=True)
 
 
 def write_report_legend(report_legend_path: Path) -> None:
diff --git a/tests/test_interpreter.py b/tests/test_interpreter.py
index 167c6c3..1c2f3c9 100644
--- a/tests/test_interpreter.py
+++ b/tests/test_interpreter.py
@@ -8,26 +8,41 @@ import pipx.interpreter
 from pipx.interpreter import (
     _find_default_windows_python,
     _get_absolute_python_interpreter,
+    find_py_launcher_python,
 )
 from pipx.util import PipxError
 
 
-def test_windows_python_venv_present(monkeypatch):
+@pytest.mark.skipif(not sys.platform.startswith("win"), reason="Looks for Python.exe")
+@pytest.mark.parametrize("venv", [True, False])
+def test_windows_python_with_version(monkeypatch, venv):
+    def which(name):
+        return "py"
+
+    major = sys.version_info.major
+    minor = sys.version_info.minor
+    monkeypatch.setattr(pipx.interpreter, "has_venv", lambda: venv)
+    monkeypatch.setattr(shutil, "which", which)
+    python_path = find_py_launcher_python(f"{major}.{minor}")
+    assert f"{major}.{minor}" in python_path or f"{major}{minor}" in python_path
+    assert python_path.endswith("python.exe")
+
+
+def test_windows_python_no_version_with_venv(monkeypatch):
     monkeypatch.setattr(pipx.interpreter, "has_venv", lambda: True)
     assert _find_default_windows_python() == sys.executable
 
 
-def test_windows_python_no_venv_py_present(monkeypatch):
+def test_windows_python_no_version_no_venv_with_py(monkeypatch):
     def which(name):
-        if name == "py":
-            return "py"
+        return "py"
 
     monkeypatch.setattr(pipx.interpreter, "has_venv", lambda: False)
     monkeypatch.setattr(shutil, "which", which)
     assert _find_default_windows_python() == "py"
 
 
-def test_windows_python_no_venv_python_present(monkeypatch):
+def test_windows_python_no_version_no_venv_python_present(monkeypatch):
     def which(name):
         if name == "python":
             return "python"
@@ -38,7 +53,7 @@ def test_windows_python_no_venv_python_present(monkeypatch):
     assert _find_default_windows_python() == "python"
 
 
-def test_windows_python_no_venv_no_python(monkeypatch):
+def test_windows_python_no_version_no_venv_no_python(monkeypatch):
     def which(name):
         return None
 
diff --git a/tests/test_package_specifier.py b/tests/test_package_specifier.py
index 154416e..6351d20 100644
--- a/tests/test_package_specifier.py
+++ b/tests/test_package_specifier.py
@@ -52,7 +52,6 @@ def test_fix_package_name(package_spec_in, package_name, package_spec_out):
     assert fix_package_name(package_spec_in, package_name) == package_spec_out
 
 
-# TODO: Make sure git+ works with tests, correct in test_install as well
 @pytest.mark.parametrize(
     "package_spec_in,package_or_url_correct,valid_spec",
     [
@@ -97,7 +96,7 @@ def test_fix_package_name(package_spec_in, package_name, package_spec_out):
             True,
         ),
         (
-            'my-project[cli] @ git+ssh://git@bitbucket.org/my-company/myproject.git ; python_version<"3.7"',
+            'my-project[cli] @ git+ssh://git@bitbucket.org/my-company/myproject.git ; python_version<"3.8"',
             "my-project[cli]@ git+ssh://git@bitbucket.org/my-company/myproject.git",
             True,
         ),
@@ -166,7 +165,7 @@ def test_parse_specifier_for_metadata(
             True,
         ),
         (
-            'my-project[cli] @ git+ssh://git@bitbucket.org/my-company/myproject.git ; python_version<"3.7"',
+            'my-project[cli] @ git+ssh://git@bitbucket.org/my-company/myproject.git ; python_version<"3.8"',
             "my-project[cli]@ git+ssh://git@bitbucket.org/my-company/myproject.git",
             True,
         ),
diff --git a/tests/test_run.py b/tests/test_run.py
index 6d5e4e1..6a6708d 100644
--- a/tests/test_run.py
+++ b/tests/test_run.py
@@ -28,10 +28,10 @@ def execvpe_mock(cmd_path, cmd_args, env):
     return_code = subprocess.run(
         [str(x) for x in cmd_args],
         env=env,
-        stdout=None,
-        stderr=None,
+        capture_output=False,
         encoding="utf-8",
-        universal_newlines=True,
+        text=True,
+        check=False,
     ).returncode
     sys.exit(return_code)
 
@@ -143,10 +143,10 @@ def test_run_ensure_null_pythonpath():
                 "-c",
                 "import os; print(os.environ.get('PYTHONPATH'))",
             ],
-            universal_newlines=True,
             env=env,
-            stdout=subprocess.PIPE,
-            stderr=subprocess.PIPE,
+            capture_output=True,
+            text=True,
+            check=True,
         ).stdout
     )
 
@@ -208,8 +208,9 @@ def test_run_with_requirements(caplog, pipx_temp_env, tmp_path):
     script.write_text(
         textwrap.dedent(
             f"""
-                # Requirements:
-                # requests==2.28.1
+                # /// pyproject
+                # run.requirements = ["requests==2.28.1"]
+                # ///
 
                 # Check requests can be imported
                 import requests
@@ -219,7 +220,8 @@ def test_run_with_requirements(caplog, pipx_temp_env, tmp_path):
                 from pathlib import Path
                 Path({repr(str(out))}).write_text(requests.__version__)
             """
-        ).strip()
+        ).strip(),
+        encoding="utf-8",
     )
     run_pipx_cli_exit(["run", script.as_uri()])
     assert out.read_text() == "2.28.1"
@@ -249,9 +251,9 @@ def test_run_with_requirements_and_args(caplog, pipx_temp_env, tmp_path):
     script.write_text(
         textwrap.dedent(
             f"""
-                # Requirements:
-                # packaging
-
+                # /// pyproject
+                # run.requirements = ["packaging"]
+                # ///
                 import packaging
                 import sys
                 from pathlib import Path
@@ -269,8 +271,9 @@ def test_run_with_invalid_requirement(capsys, pipx_temp_env, tmp_path):
     script.write_text(
         textwrap.dedent(
             """
-                # Requirements:
-                # this is an invalid requirement
+                # /// pyproject
+                # run.requirements = ["this is an invalid requirement"]
+                # ///
                 print()
             """
         ).strip()
@@ -316,3 +319,23 @@ def test_run_script_by_relative_name(caplog, pipx_temp_env, monkeypatch, tmp_pat
         m.chdir(tmp_path)
         run_pipx_cli_exit(["run", "test.py"])
     assert out.read_text() == test_str
+
+
+@pytest.mark.skipif(
+    not sys.platform.startswith("win"), reason="uses windows version format"
+)
+@mock.patch("os.execvpe", new=execvpe_mock)
+def test_run_with_windows_python_version(caplog, pipx_temp_env, tmp_path):
+    script = tmp_path / "test.py"
+    out = tmp_path / "output.txt"
+    script.write_text(
+        textwrap.dedent(
+            f"""
+                import sys
+                from pathlib import Path
+                Path({repr(str(out))}).write_text(sys.version)
+            """
+        ).strip()
+    )
+    run_pipx_cli_exit(["run", script.as_uri(), "--python", "3.11"])
+    assert "3.11" in out.read_text()