Repository: pypa/pip
Pull request #9639 (2021-02-20 23:57:42)

Change VCS tooling verbosity along with pip's verbosity
## Overview
This PR is responding to (part of) the feature request represented by #8819, passing the verbosity of `pip` down to a VCS used to satisfy a requirement when calling `fetch_new()` (i.e. executing a clone). 

~I haven't contributed to `pip` before, so **I'm opening this as a WIP to get some feedback on my changes to the internal plumbing**, namely how `verbose` is passed down to the `VersionControl` instance. It seems like there are probably other ways to approach this problem, so I figured it's better to just ask about what I've done for `git` alone before proceeding to the other backends, in case there is some better way to pass this information down to the tool level.~

~Only the set of changes necessary to get the desired behavior when installing from a `git` repository is included at the moment.~

Note: I discovered this feature request because of a user who wanted to see the actual progress of the clone, which requires passing `--progress` to `git` when not attached to a terminal. I don't think this is essential to the reporting ticket, but it's nice to have and produced reasonable output, so I've included it in these changes.

## Output
Truncated output of typical usage follows:

**default:**
```
$ python3 -m pip install --target /tmp/otherpip git+https://github.com/pypa/pip
Collecting git+https://github.com/pypa/pip
  Cloning https://github.com/pypa/pip to /tmp/pip-req-build-i3w858wj
  Running command git clone --quiet https://github.com/pypa/pip /tmp/pip-req-build-i3w858wj
  Installing build dependencies: started
  Installing build dependencies: finished with status 'done'
  Getting requirements to build wheel: started
  Getting requirements to build wheel: finished with status 'done'
    Preparing wheel metadata: started
    Preparing wheel metadata: finished with status 'done'
Building wheels for collected packages: pip
  Building wheel for pip (PEP 517): started
  Building wheel for pip (PEP 517): finished with status 'done'
  Created wheel for pip: filename=pip-21.1.dev0-py3-none-any.whl size=1522347 sha256=67fcfe60e62d64239ec2725e7233db2f25d5d52fc00d781e006becc53b
  Stored in directory: /tmp/pip-ephem-wheel-cache-7eghaoih/wheels/48/9d/74/8de2038273187621570de5b4d06275d64086de14c84da727ff
Successfully built pip
Installing collected packages: pip
Successfully installed pip-21.1.dev0
```

**verbose:**
```
$ python3 -m pip -vvv install --target /tmp/otherpip git+https://github.com/pypa/pip
Using pip 21.1.dev0 from /home/snoopjedi/repos/pip-src/src/pip (python 3.6)
Non-user install due to --prefix or --target option
Created temporary directory: /tmp/pip-target-dherg1bq
Created temporary directory: /tmp/pip-ephem-wheel-cache-4fvg0opc
Created temporary directory: /tmp/pip-req-tracker-wtcxtdds
Initialized build tracking at /tmp/pip-req-tracker-wtcxtdds
Created build tracker: /tmp/pip-req-tracker-wtcxtdds
Entered build tracker: /tmp/pip-req-tracker-wtcxtdds
Created temporary directory: /tmp/pip-install-n4r1uosw
Collecting git+https://github.com/pypa/pip
  Created temporary directory: /tmp/pip-req-build-m1u4sukt
  Cloning https://github.com/pypa/pip to /tmp/pip-req-build-m1u4sukt
  Running command git clone --verbose --progress https://github.com/pypa/pip /tmp/pip-req-build-m1u4sukt
  Cloning into '/tmp/pip-req-build-m1u4sukt'...
  POST git-upload-pack (gzip 5065 to 2595 bytes)
  remote: Enumerating objects: 53, done.
  remote: Counting objects: 100% (53/53), done.
  remote: Compressing objects: 100% (44/44), done.
  remote: Total 71289 (delta 17), reused 24 (delta 7), pack-reused 71236
  Receiving objects: 100% (71289/71289), 59.23 MiB | 22.36 MiB/s, done.
  Resolving deltas: 100% (47677/47677), done.
  Added git+https://github.com/pypa/pip to build tracker '/tmp/pip-req-tracker-wtcxtdds'
  Created temporary directory: /tmp/pip-build-env-8y3yii2n
  Installing build dependencies: started

...verbose build output...

Successfully built pip
Installing collected packages: pip
  Running command git rev-parse HEAD
  8a949a1c52ae47584242e5bb0155d1d1018275ae

  Creating /tmp/pip-target-dherg1bq/bin
  changing mode of /tmp/pip-target-dherg1bq/bin/pip to 775
  changing mode of /tmp/pip-target-dherg1bq/bin/pip3 to 775
  changing mode of /tmp/pip-target-dherg1bq/bin/pip3.6 to 775
Successfully installed pip-21.1.dev0
 ```


### TODO
- [ ] Pass `pip` test suite
- [ ] Needs news fragment


BrownTruck: Hello!

I am an automated bot and I have noticed that this pull request is not currently able to be merged. If you are able to either merge the ``master`` branch into this pull request or rebase this pull request against ``master`` then it will be eligible for code review and hopefully merging!
pradyunsg: @SnoopJeDi Are you still interested in moving this forward? If so, could you [update this PR](https://www.digitalocean.com/community/tutorials/how-to-rebase-and-update-a-pull-request) and resolve the merge conflicts?
SnoopJeDi: > @SnoopJeDi Are you still interested in moving this forward? If so, could you [update this PR](https://www.digitalocean.com/community/tutorials/how-to-rebase-and-update-a-pull-request) and resolve the merge conflicts?

@pradyunsg Yes and yes. Will do, thanks for following up.

Can you comment on the question I asked about the plumbing changes? I.e. is it okay to add a `verbose` kwarg to the relevant functions to pass that info down to where it's needed to couple it to the VCS behavior? I don't know the `pip` codebase well enough to know if there's some nicer way to access config-at-invocation information like this, but didn't find one when I wrote the PR.

Summary of the changed signatures, most abstract to least:

```python
# src/pip/_internal/operations/prepare.py
unpack_vcs_link(link, location) → unpack_vcs_link(link, location, verbose)

# src/pip/_internal/vcs/versioncontrol.py
obtain(self, dest, url) → obtain(self, dest, url, verbose)
unpack(self, location, url) → unpack(self, location, url, verbose)

# src/pip/_internal/vcs/git.py
fetch_new(self, dest, url, rev_options) → fetch_new(self, dest, url, rev_options, verbose)
```
ziebam: FWIW I peaked around the codebase and didn't find a nicer way to access information like that either. I think @pradyunsg might have been fine with the changes so far and that's why they asked for the rebase, but I'm not exactly sure.
uranusjr: Is this still in WIP state? I'm waiting this to be considered ready before taking a look at the implementation.
SnoopJeDi: > Is this still in WIP state? I'm waiting this to be considered ready before taking a look at the implementation.

@uranusjr yes, this PR is still WIP, it's missing implementation for non-git VCS.
pfmoore: FWIW, I don't think this needs to block on deciding the perfect way of passing the "verbose" flag. The current proposal to change just the one signature `unpack_vcs_link` outside of the VCS support code seems clean enough for now, and we can always refactor later if needed.
SnoopJeDi: > FWIW, I don't think this needs to block on deciding the perfect way of passing the "verbose" flag. The current proposal to change just the one signature `unpack_vcs_link` outside of the VCS support code seems clean enough for now, and we can always refactor later if needed.

Agreed, when I come back for the other VCS implementations, I'll be using this approach.

Patch:
diff --git a/news/10617.bugfix.rst b/news/10617.bugfix.rst
new file mode 100755
index 000000000..8536d5e63
--- /dev/null
+++ b/news/10617.bugfix.rst
@@ -0,0 +1,2 @@
+Prevent pip from installing yanked releases unless
+explicitely pinned via the ``==`` or ``===`` operators.
diff --git a/news/8819.feature.rst b/news/8819.feature.rst
new file mode 100644
index 000000000..d539730e3
--- /dev/null
+++ b/news/8819.feature.rst
@@ -0,0 +1,1 @@
+Forward pip's verbosity configuration to VCS tools to control their output accordingly.
diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py
index 1607c78b1..177b3dbc2 100644
--- a/src/pip/_internal/cli/req_command.py
+++ b/src/pip/_internal/cli/req_command.py
@@ -237,6 +237,7 @@ class RequirementCommand(IndexGroupCommand):
         finder: PackageFinder,
         use_user_site: bool,
         download_dir: Optional[str] = None,
+        verbosity: int = 0,
     ) -> RequirementPreparer:
         """
         Create a RequirementPreparer instance for the given parameters.
@@ -295,6 +296,7 @@ class RequirementCommand(IndexGroupCommand):
             require_hashes=options.require_hashes,
             use_user_site=use_user_site,
             lazy_wheel=lazy_wheel,
+            verbosity=verbosity,
             in_tree_build=in_tree_build,
         )
 
diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py
index 7de207f13..233b7e983 100644
--- a/src/pip/_internal/commands/download.py
+++ b/src/pip/_internal/commands/download.py
@@ -113,6 +113,7 @@ class DownloadCommand(RequirementCommand):
             finder=finder,
             download_dir=options.download_dir,
             use_user_site=False,
+            verbosity=self.verbosity,
         )
 
         resolver = self.make_resolver(
diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py
index eedb1ff5d..34e4c2f84 100644
--- a/src/pip/_internal/commands/install.py
+++ b/src/pip/_internal/commands/install.py
@@ -319,6 +319,7 @@ class InstallCommand(RequirementCommand):
                 session=session,
                 finder=finder,
                 use_user_site=options.use_user_site,
+                verbosity=self.verbosity,
             )
             resolver = self.make_resolver(
                 preparer=preparer,
diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py
index cea81ee51..d5b20dc9f 100644
--- a/src/pip/_internal/commands/wheel.py
+++ b/src/pip/_internal/commands/wheel.py
@@ -128,6 +128,7 @@ class WheelCommand(RequirementCommand):
             finder=finder,
             download_dir=options.wheel_dir,
             use_user_site=False,
+            verbosity=self.verbosity,
         )
 
         resolver = self.make_resolver(
diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py
index 34cf9a51b..a726f031a 100644
--- a/src/pip/_internal/operations/prepare.py
+++ b/src/pip/_internal/operations/prepare.py
@@ -59,10 +59,10 @@ def _get_prepared_distribution(
     return abstract_dist.get_metadata_distribution()
 
 
-def unpack_vcs_link(link: Link, location: str) -> None:
+def unpack_vcs_link(link: Link, location: str, verbosity: int) -> None:
     vcs_backend = vcs.get_backend_for_scheme(link.scheme)
     assert vcs_backend is not None
-    vcs_backend.unpack(location, url=hide_url(link.url))
+    vcs_backend.unpack(location, url=hide_url(link.url), verbosity=verbosity)
 
 
 class File:
@@ -175,6 +175,7 @@ def unpack_url(
     link: Link,
     location: str,
     download: Downloader,
+    verbosity: int,
     download_dir: Optional[str] = None,
     hashes: Optional[Hashes] = None,
 ) -> Optional[File]:
@@ -187,7 +188,7 @@ def unpack_url(
     """
     # non-editable vcs urls
     if link.is_vcs:
-        unpack_vcs_link(link, location)
+        unpack_vcs_link(link, location, verbosity=verbosity)
         return None
 
     # Once out-of-tree-builds are no longer supported, could potentially
@@ -267,6 +268,7 @@ class RequirementPreparer:
         require_hashes: bool,
         use_user_site: bool,
         lazy_wheel: bool,
+        verbosity: int,
         in_tree_build: bool,
     ) -> None:
         super().__init__()
@@ -295,6 +297,9 @@ class RequirementPreparer:
         # Should wheels be downloaded lazily?
         self.use_lazy_wheel = lazy_wheel
 
+        # How verbose should underlying tooling be?
+        self.verbosity = verbosity
+
         # Should in-tree builds be used for local paths?
         self.in_tree_build = in_tree_build
 
@@ -525,7 +530,12 @@ class RequirementPreparer:
         elif link.url not in self._downloaded:
             try:
                 local_file = unpack_url(
-                    link, req.source_dir, self._download, self.download_dir, hashes
+                    link,
+                    req.source_dir,
+                    self._download,
+                    self.verbosity,
+                    self.download_dir,
+                    hashes,
                 )
             except NetworkConnectionError as exc:
                 raise InstallationError(
diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py
index 6fa6eb2a2..bd8da9ebd 100644
--- a/src/pip/_internal/req/req_install.py
+++ b/src/pip/_internal/req/req_install.py
@@ -607,7 +607,7 @@ class InstallRequirement:
         # So here, if it's neither a path nor a valid VCS URL, it's a bug.
         assert vcs_backend, f"Unsupported VCS URL {self.link.url}"
         hidden_url = hide_url(self.link.url)
-        vcs_backend.obtain(self.source_dir, url=hidden_url)
+        vcs_backend.obtain(self.source_dir, url=hidden_url, verbosity=0)
 
     # Top-level Actions
     def uninstall(
diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py
index c3aaa6957..a54dfd325 100644
--- a/src/pip/_internal/resolution/resolvelib/factory.py
+++ b/src/pip/_internal/resolution/resolvelib/factory.py
@@ -273,14 +273,27 @@ class Factory:
             )
             icans = list(result.iter_applicable())
 
-            # PEP 592: Yanked releases must be ignored unless only yanked
-            # releases can satisfy the version range. So if this is false,
-            # all yanked icans need to be skipped.
+            # PEP 592: Yanked releases are ignored unless the specifier
+            # explicitely pins a version (via '==' or '===') that can be
+            # solely satisfied by a yanked release.
             all_yanked = all(ican.link.is_yanked for ican in icans)
 
+            def is_pinned(specifier: SpecifierSet) -> bool:
+                for sp in specifier:
+                    if sp.operator == "===":
+                        return True
+                    if sp.operator != "==":
+                        continue
+                    if sp.version.endswith(".*"):
+                        continue
+                    return True
+                return False
+
+            pinned = is_pinned(specifier)
+
             # PackageFinder returns earlier versions first, so we reverse.
             for ican in reversed(icans):
-                if not all_yanked and ican.link.is_yanked:
+                if not (all_yanked and pinned) and ican.link.is_yanked:
                     continue
                 func = functools.partial(
                     self._make_candidate_from_link,
diff --git a/src/pip/_internal/vcs/bazaar.py b/src/pip/_internal/vcs/bazaar.py
index 82e75954a..a7b16e2e0 100644
--- a/src/pip/_internal/vcs/bazaar.py
+++ b/src/pip/_internal/vcs/bazaar.py
@@ -33,7 +33,9 @@ class Bazaar(VersionControl):
     def get_base_rev_args(rev: str) -> List[str]:
         return ["-r", rev]
 
-    def fetch_new(self, dest: str, url: HiddenText, rev_options: RevOptions) -> None:
+    def fetch_new(
+        self, dest: str, url: HiddenText, rev_options: RevOptions, verbosity: int
+    ) -> None:
         rev_display = rev_options.to_display()
         logger.info(
             "Checking out %s%s to %s",
@@ -41,7 +43,13 @@ class Bazaar(VersionControl):
             rev_display,
             display_path(dest),
         )
-        cmd_args = make_command("branch", "-q", rev_options.to_args(), url, dest)
+        if verbosity <= 0:
+            flag = "--quiet"
+        elif verbosity == 1:
+            flag = ""
+        else:
+            flag = f"-{'v'*verbosity}"
+        cmd_args = make_command("branch", flag, rev_options.to_args(), url, dest)
         self.run_command(cmd_args)
 
     def switch(self, dest: str, url: HiddenText, rev_options: RevOptions) -> None:
diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py
index 7a78ad12d..93be90472 100644
--- a/src/pip/_internal/vcs/git.py
+++ b/src/pip/_internal/vcs/git.py
@@ -253,9 +253,17 @@ class Git(VersionControl):
 
         return cls.get_revision(dest) == name
 
-    def fetch_new(self, dest: str, url: HiddenText, rev_options: RevOptions) -> None:
+    def fetch_new(
+        self, dest: str, url: HiddenText, rev_options: RevOptions, verbosity: int
+    ) -> None:
         rev_display = rev_options.to_display()
         logger.info("Cloning %s%s to %s", url, rev_display, display_path(dest))
+        if verbosity <= 0:
+            flags: Tuple[str, ...] = ("--quiet",)
+        elif verbosity == 1:
+            flags = ()
+        else:
+            flags = ("--verbose", "--progress")
         if self.get_git_version() >= (2, 17):
             # Git added support for partial clone in 2.17
             # https://git-scm.com/docs/partial-clone
@@ -264,13 +272,13 @@ class Git(VersionControl):
                 make_command(
                     "clone",
                     "--filter=blob:none",
-                    "-q",
+                    *flags,
                     url,
                     dest,
                 )
             )
         else:
-            self.run_command(make_command("clone", "-q", url, dest))
+            self.run_command(make_command("clone", *flags, url, dest))
 
         if rev_options.rev:
             # Then a specific revision was requested.
diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py
index 410c79d90..2a005e0af 100644
--- a/src/pip/_internal/vcs/mercurial.py
+++ b/src/pip/_internal/vcs/mercurial.py
@@ -1,7 +1,7 @@
 import configparser
 import logging
 import os
-from typing import List, Optional
+from typing import List, Optional, Tuple
 
 from pip._internal.exceptions import BadCommand, InstallationError
 from pip._internal.utils.misc import HiddenText, display_path
@@ -33,7 +33,9 @@ class Mercurial(VersionControl):
     def get_base_rev_args(rev: str) -> List[str]:
         return [rev]
 
-    def fetch_new(self, dest: str, url: HiddenText, rev_options: RevOptions) -> None:
+    def fetch_new(
+        self, dest: str, url: HiddenText, rev_options: RevOptions, verbosity: int
+    ) -> None:
         rev_display = rev_options.to_display()
         logger.info(
             "Cloning hg %s%s to %s",
@@ -41,9 +43,17 @@ class Mercurial(VersionControl):
             rev_display,
             display_path(dest),
         )
-        self.run_command(make_command("clone", "--noupdate", "-q", url, dest))
+        if verbosity <= 0:
+            flags: Tuple[str, ...] = ("--quiet",)
+        elif verbosity == 1:
+            flags = ()
+        elif verbosity == 2:
+            flags = ("--verbose",)
+        else:
+            flags = ("--verbose", "--debug")
+        self.run_command(make_command("clone", "--noupdate", *flags, url, dest))
         self.run_command(
-            make_command("update", "-q", rev_options.to_args()),
+            make_command("update", *flags, rev_options.to_args()),
             cwd=dest,
         )
 
diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py
index b5b6fd5c0..89c8754ce 100644
--- a/src/pip/_internal/vcs/subversion.py
+++ b/src/pip/_internal/vcs/subversion.py
@@ -277,7 +277,9 @@ class Subversion(VersionControl):
 
         return []
 
-    def fetch_new(self, dest: str, url: HiddenText, rev_options: RevOptions) -> None:
+    def fetch_new(
+        self, dest: str, url: HiddenText, rev_options: RevOptions, verbosity: int
+    ) -> None:
         rev_display = rev_options.to_display()
         logger.info(
             "Checking out %s%s to %s",
@@ -285,9 +287,13 @@ class Subversion(VersionControl):
             rev_display,
             display_path(dest),
         )
+        if verbosity <= 0:
+            flag = "--quiet"
+        else:
+            flag = ""
         cmd_args = make_command(
             "checkout",
-            "-q",
+            flag,
             self.get_remote_call_options(),
             rev_options.to_args(),
             url,
diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py
index 1139051f3..7b93657b9 100644
--- a/src/pip/_internal/vcs/versioncontrol.py
+++ b/src/pip/_internal/vcs/versioncontrol.py
@@ -458,7 +458,9 @@ class VersionControl:
         """
         return cls.normalize_url(url1) == cls.normalize_url(url2)
 
-    def fetch_new(self, dest: str, url: HiddenText, rev_options: RevOptions) -> None:
+    def fetch_new(
+        self, dest: str, url: HiddenText, rev_options: RevOptions, verbosity: int
+    ) -> None:
         """
         Fetch a revision from a repository, in the case that this is the
         first fetch from the repository.
@@ -466,6 +468,7 @@ class VersionControl:
         Args:
           dest: the directory to fetch the repository to.
           rev_options: a RevOptions object.
+          verbosity: verbosity level.
         """
         raise NotImplementedError
 
@@ -498,18 +501,19 @@ class VersionControl:
         """
         raise NotImplementedError
 
-    def obtain(self, dest: str, url: HiddenText) -> None:
+    def obtain(self, dest: str, url: HiddenText, verbosity: int) -> None:
         """
         Install or update in editable mode the package represented by this
         VersionControl object.
 
         :param dest: the repository directory in which to install or update.
         :param url: the repository URL starting with a vcs prefix.
+        :param verbosity: verbosity level.
         """
         url, rev_options = self.get_url_rev_options(url)
 
         if not os.path.exists(dest):
-            self.fetch_new(dest, url, rev_options)
+            self.fetch_new(dest, url, rev_options, verbosity=verbosity)
             return
 
         rev_display = rev_options.to_display()
@@ -565,14 +569,14 @@ class VersionControl:
         if response == "w":
             logger.warning("Deleting %s", display_path(dest))
             rmtree(dest)
-            self.fetch_new(dest, url, rev_options)
+            self.fetch_new(dest, url, rev_options, verbosity=verbosity)
             return
 
         if response == "b":
             dest_dir = backup_dir(dest)
             logger.warning("Backing up %s to %s", display_path(dest), dest_dir)
             shutil.move(dest, dest_dir)
-            self.fetch_new(dest, url, rev_options)
+            self.fetch_new(dest, url, rev_options, verbosity=verbosity)
             return
 
         # Do nothing if the response is "i".
@@ -586,16 +590,17 @@ class VersionControl:
             )
             self.switch(dest, url, rev_options)
 
-    def unpack(self, location: str, url: HiddenText) -> None:
+    def unpack(self, location: str, url: HiddenText, verbosity: int) -> None:
         """
         Clean up current location and download the url repository
         (and vcs infos) into location
 
         :param url: the repository URL starting with a vcs prefix.
+        :param verbosity: verbosity level.
         """
         if os.path.exists(location):
             rmtree(location)
-        self.obtain(location, url=url)
+        self.obtain(location, url=url, verbosity=verbosity)
 
     @classmethod
     def get_remote_url(cls, location: str) -> str:


Test patch:
diff --git a/tests/data/indexes/yanked_all/simple/index.html b/tests/data/indexes/yanked_all/simple/index.html
new file mode 100644
index 000000000..732340baa
--- /dev/null
+++ b/tests/data/indexes/yanked_all/simple/index.html
@@ -0,0 +1,7 @@
+<html>
+  <body>
+    <a data-yanked="test reason message" href="../../../packages/simple-1.0.tar.gz">simple-1.0.tar.gz</a>
+    <a data-yanked="test reason message" href="../../../packages/simple-2.0.tar.gz">simple-2.0.tar.gz</a>
+    <a data-yanked="test reason message" href="../../../packages/simple-3.0.tar.gz">simple-3.0.tar.gz</a>
+  </body>
+</html>
diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py
index a2f1ffb88..6c0ff4d61 100644
--- a/tests/functional/test_install.py
+++ b/tests/functional/test_install.py
@@ -2172,6 +2172,26 @@ def test_install_yanked_file_and_print_warning(
     assert "Successfully installed simple-3.0\n" in result.stdout, str(result)
 
 
+def test_error_all_yanked_files_and_no_pin(
+    script: PipTestEnvironment, data: TestData
+) -> None:
+    """
+    Test raising an error if there are only "yanked" files available and no pin
+    """
+    result = script.pip(
+        "install",
+        "simple",
+        "--index-url",
+        data.index_url("yanked_all"),
+        expect_error=True,
+    )
+    # Make sure an error is raised
+    assert (
+        result.returncode == 1
+        and "ERROR: No matching distribution found for simple\n" in result.stderr
+    ), str(result)
+
+
 @pytest.mark.parametrize(
     "install_args",
     [
diff --git a/tests/functional/test_vcs_git.py b/tests/functional/test_vcs_git.py
index 928aadd49..4f22386d5 100644
--- a/tests/functional/test_vcs_git.py
+++ b/tests/functional/test_vcs_git.py
@@ -292,7 +292,10 @@ def test_resolve_commit_not_on_branch(
     # check we can fetch our commit
     rev_options = Git.make_rev_options(commit)
     Git().fetch_new(
-        str(clone_path), HiddenText(repo_path.as_uri(), redacted="*"), rev_options
+        str(clone_path),
+        HiddenText(repo_path.as_uri(), redacted="*"),
+        rev_options,
+        verbosity=0,
     )
 
 
@@ -333,12 +336,14 @@ def test_partial_clone(script: PipTestEnvironment, tmp_path: pathlib.Path) -> No
         str(clone_path1),
         HiddenText(repo_path.as_uri(), redacted="*"),
         Git.make_rev_options(),
+        verbosity=0,
     )
     # Check that we can clone to commit
     Git().fetch_new(
         str(clone_path2),
         HiddenText(repo_path.as_uri(), redacted="*"),
         Git.make_rev_options(commit),
+        verbosity=0,
     )
 
     # Write some additional stuff to git pull
@@ -375,12 +380,14 @@ def test_partial_clone_without_server_support(
         str(clone_path1),
         HiddenText(repo_path.as_uri(), redacted="*"),
         Git.make_rev_options(),
+        verbosity=0,
     )
     # Check that we can clone to commit
     Git().fetch_new(
         str(clone_path2),
         HiddenText(repo_path.as_uri(), redacted="*"),
         Git.make_rev_options(commit),
+        verbosity=0,
     )
 
     # Write some additional stuff to git pull
@@ -414,6 +421,7 @@ def test_clone_without_partial_clone_support(
             str(clone_path),
             HiddenText(repo_path.as_uri(), redacted="*"),
             Git.make_rev_options(),
+            verbosity=0,
         )
 
     repo_file.write_text(u"...")
diff --git a/tests/lib/local_repos.py b/tests/lib/local_repos.py
index 8d11a3fb1..2ead17aca 100644
--- a/tests/lib/local_repos.py
+++ b/tests/lib/local_repos.py
@@ -55,7 +55,7 @@ def local_checkout(
     else:
         vcs_backend = vcs.get_backend(vcs_name)
         assert vcs_backend is not None
-        vcs_backend.obtain(repo_url_path, url=hide_url(remote_repo))
+        vcs_backend.obtain(repo_url_path, url=hide_url(remote_repo), verbosity=0)
 
     return "{}+{}".format(vcs_name, path_to_url(repo_url_path))
 
diff --git a/tests/unit/resolution_resolvelib/conftest.py b/tests/unit/resolution_resolvelib/conftest.py
index 109b337ff..8f3bba501 100644
--- a/tests/unit/resolution_resolvelib/conftest.py
+++ b/tests/unit/resolution_resolvelib/conftest.py
@@ -46,6 +46,7 @@ def preparer(finder: PackageFinder) -> Iterator[RequirementPreparer]:
                     session=session,
                     finder=finder,
                     use_user_site=False,
+                    verbosity=0,
                 )
 
                 yield preparer
diff --git a/tests/unit/test_operations_prepare.py b/tests/unit/test_operations_prepare.py
index 961b96ca7..a2ee87870 100644
--- a/tests/unit/test_operations_prepare.py
+++ b/tests/unit/test_operations_prepare.py
@@ -44,6 +44,7 @@ def test_unpack_url_with_urllib_response_without_content_type(data: TestData) ->
             temp_dir,
             download=download,
             download_dir=None,
+            verbosity=0,
         )
         assert set(os.listdir(temp_dir)) == {
             "PKG-INFO",
@@ -188,7 +189,7 @@ class Test_unpack_url:
 
     def test_unpack_url_no_download(self, tmpdir: Path, data: TestData) -> None:
         self.prep(tmpdir, data)
-        unpack_url(self.dist_url, self.build_dir, self.no_download)
+        unpack_url(self.dist_url, self.build_dir, self.no_download, verbosity=0)
         assert os.path.isdir(os.path.join(self.build_dir, "simple"))
         assert not os.path.isfile(os.path.join(self.download_dir, self.dist_file))
 
@@ -205,6 +206,7 @@ class Test_unpack_url:
                 self.build_dir,
                 download=self.no_download,
                 hashes=Hashes({"md5": ["bogus"]}),
+                verbosity=0,
             )
 
     def test_unpack_url_thats_a_dir(self, tmpdir: Path, data: TestData) -> None:
@@ -216,6 +218,7 @@ class Test_unpack_url:
             self.build_dir,
             download=self.no_download,
             download_dir=self.download_dir,
+            verbosity=0,
         )
         assert os.path.isdir(os.path.join(self.build_dir, "fspkg"))
 
@@ -241,7 +244,13 @@ def test_unpack_url_excludes_expected_dirs(tmpdir: Path, exclude_dir: str) -> No
     dst_included_dir = dst_dir.joinpath("subdir", exclude_dir)
 
     src_link = Link(path_to_url(src_dir))
-    unpack_url(src_link, dst_dir, Mock(side_effect=AssertionError), download_dir=None)
+    unpack_url(
+        src_link,
+        dst_dir,
+        Mock(side_effect=AssertionError),
+        download_dir=None,
+        verbosity=0,
+    )
     assert not os.path.isdir(dst_excluded_dir)
     assert not os.path.isfile(dst_excluded_file)
     assert os.path.isfile(dst_included_file)
diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py
index 53d9c9ef3..858695c1f 100644
--- a/tests/unit/test_req.py
+++ b/tests/unit/test_req.py
@@ -98,6 +98,7 @@ class TestRequirementSet:
                 require_hashes=require_hashes,
                 use_user_site=False,
                 lazy_wheel=False,
+                verbosity=0,
                 in_tree_build=False,
             )
             yield Resolver(
diff --git a/tests/unit/test_vcs.py b/tests/unit/test_vcs.py
index 67e3273c7..64f60bc40 100644
--- a/tests/unit/test_vcs.py
+++ b/tests/unit/test_vcs.py
@@ -764,12 +764,12 @@ class TestSubversionArgs(TestCase):
         assert self.call_subprocess_mock.call_args[0][0] == args
 
     def test_obtain(self) -> None:
-        self.svn.obtain(self.dest, hide_url(self.url))
+        self.svn.obtain(self.dest, hide_url(self.url), verbosity=0)
         self.assert_call_args(
             [
                 "svn",
                 "checkout",
-                "-q",
+                "--quiet",
                 "--non-interactive",
                 "--username",
                 "username",
@@ -781,12 +781,12 @@ class TestSubversionArgs(TestCase):
         )
 
     def test_fetch_new(self) -> None:
-        self.svn.fetch_new(self.dest, hide_url(self.url), self.rev_options)
+        self.svn.fetch_new(self.dest, hide_url(self.url), self.rev_options, verbosity=0)
         self.assert_call_args(
             [
                 "svn",
                 "checkout",
-                "-q",
+                "--quiet",
                 "--non-interactive",
                 hide_url("svn+http://username:password@svn.example.com/"),
                 "/tmp/test",
@@ -795,12 +795,12 @@ class TestSubversionArgs(TestCase):
 
     def test_fetch_new_revision(self) -> None:
         rev_options = RevOptions(Subversion, "123")
-        self.svn.fetch_new(self.dest, hide_url(self.url), rev_options)
+        self.svn.fetch_new(self.dest, hide_url(self.url), rev_options, verbosity=0)
         self.assert_call_args(
             [
                 "svn",
                 "checkout",
-                "-q",
+                "--quiet",
                 "--non-interactive",
                 "-r",
                 "123",