diff --git a/reviewboard/scmtools/bzr/__init__.py b/reviewboard/scmtools/bzr/__init__.py
index 53af61a3fb487761337f84a48f00fa2dc3ff5653..1e37b93d885cdae309db255b7bdccf9467495b92 100644
--- a/reviewboard/scmtools/bzr/__init__.py
+++ b/reviewboard/scmtools/bzr/__init__.py
@@ -1,8 +1,11 @@
 """Repository support for Bazaar."""
 
+from __future__ import annotations
+
 import os
 import urllib.parse
 from datetime import timezone
+from typing import TYPE_CHECKING
 
 import dateutil.parser
 from django.utils.encoding import force_str
@@ -14,6 +17,10 @@ from reviewboard.scmtools.errors import (FileNotFoundError,
                                          RepositoryNotFoundError, SCMError)
 from reviewboard.ssh import utils as sshutils
 
+if TYPE_CHECKING:
+    from reviewboard.scmtools.models import Repository
+    from reviewboard.site.models import LocalSite
+
 
 # Register these URI schemes so we can handle them properly.
 sshutils.ssh_uri_schemes.append('bzr+ssh')
@@ -112,22 +119,28 @@ class BZRTool(SCMTool):
         'tag:',
     )
 
-    def __init__(self, repository):
+    def __init__(
+        self,
+        repository: Repository,
+    ) -> None:
         """Initialize the Bazaar tool.
 
         Args:
             repository (reviewboard.scmtools.models.Repository):
                 The repository to communicate with.
         """
-        super(BZRTool, self).__init__(repository)
+        super().__init__(repository)
 
         if repository.local_site:
             local_site_name = repository.local_site.name
         else:
             local_site_name = None
 
-        self.client = BZRClient(path=repository.path,
-                                local_site_name=local_site_name)
+        self.client = BZRClient(
+            path=repository.path,
+            local_site_name=local_site_name,
+            local_site=repository.local_site,
+        )
 
     def get_file(self, path, revision, **kwargs):
         """Return the contents from a file with the given path and revision.
@@ -268,26 +281,36 @@ class BZRTool(SCMTool):
         return revspec
 
     @classmethod
-    def check_repository(cls, path, username=None, password=None,
-                         local_site_name=None, **kwargs):
+    def check_repository(
+        cls,
+        path: str,
+        username: (str | None) = None,
+        password: (str | None) = None,
+        local_site_name: (str | None) = None,
+        local_site: (LocalSite | None) = None,
+        **kwargs,
+    ) -> None:
         """Check a repository to test its validity.
 
         This checks if a Bazaar repository exists and can be connected to. If
         the repository could not be found, an exception will be raised.
 
         Args:
-            path (unicode):
+            path (str):
                 The repository path.
 
-            username (unicode):
+            username (str):
                 The optional username used to connect to the repository.
 
-            password (unicode):
+            password (str):
                 The optional password used to connect to the repository.
 
-            local_site_name (unicode):
+            local_site_name (str):
                 The name of the Local Site that will own the repository.
 
+            local_site (reviewboard.site.models.LocalSite, optional):
+                The Local Site that will own the repository.
+
             **kwargs (dict, unused):
                 Additional settings for the repository.
 
@@ -296,11 +319,19 @@ class BZRTool(SCMTool):
                 The repository could not be found, or there was an error
                 communicating with it.
         """
-        super(BZRTool, cls).check_repository(path, username, password,
-                                             local_site_name)
-
-        client = BZRClient(path=path,
-                           local_site_name=local_site_name)
+        super().check_repository(
+            path=path,
+            username=username,
+            password=password,
+            local_site_name=local_site_name,
+            local_site=local_site,
+        )
+
+        client = BZRClient(
+            path=path,
+            local_site_name=local_site_name,
+            local_site=local_site,
+        )
 
         if not client.is_valid_repository():
             raise RepositoryNotFoundError()
@@ -315,20 +346,29 @@ class BZRClient(SCMClient):
 
     _plugin_path = None
 
-    def __init__(self, path, local_site_name):
+    def __init__(
+        self,
+        path: str,
+        local_site_name: str | None,
+        **kwargs,
+    ) -> None:
         """Initialize the client.
 
         Args:
-            path (unicode):
+            path (str):
                 The repository path provided by the user.
 
-            local_site_name (unicode):
+            local_site_name (str):
                 The name of the Local Site owning the repository.
+
+            **kwargs (dict):
+                Additional keyword arguments to pass to the parent method.
         """
         if path.startswith('/'):
-            self.path = 'file://%s' % path
-        else:
-            self.path = path
+            path = f'file://{path}'
+
+        super().__init__(path=path,
+                         **kwargs)
 
         self.local_site_name = local_site_name
 
diff --git a/reviewboard/scmtools/core.py b/reviewboard/scmtools/core.py
index d14be63ddf89ad505cd10618310f8fb634e27653..12b696eb543704478b0b8bbb9bfab6c3e28f3638 100644
--- a/reviewboard/scmtools/core.py
+++ b/reviewboard/scmtools/core.py
@@ -1716,6 +1716,12 @@ class SCMClient:
     # Instance variables #
     ######################
 
+    #: The Local Site that owns the repository.
+    #:
+    #: Version Added:
+    #:     7.1
+    local_site: LocalSite | None
+
     #: The password used for communicating with the repository.
     password: Optional[str]
 
@@ -1728,11 +1734,18 @@ class SCMClient:
     def __init__(
         self,
         path: str,
-        username: Optional[str] = None,
-        password: Optional[str] = None,
+        username: (str | None) = None,
+        password: (str | None) = None,
+        *,
+        local_site: (LocalSite | None) = None,
     ) -> None:
         """Initialize the client.
 
+        Version Changed:
+            7.1:
+            Added the ``local_site`` argument. All SCMTools should pass this
+            in.
+
         Args:
             path (str):
                 The repository path.
@@ -1742,10 +1755,17 @@ class SCMClient:
 
             password (str, optional):
                 The password used for the repository.
+
+            local_site (reviewboard.site.models.LocalSite, optional):
+                The Local Site that owns the repository.
+
+                Version Added:
+                    7.1
         """
         self.path = path
         self.username = username
         self.password = password
+        self.local_site = local_site
 
     def get_file_http(
         self,
diff --git a/reviewboard/scmtools/cvs.py b/reviewboard/scmtools/cvs.py
index 4e7104edececc76e27bde6f86499bb64e8bf5a60..9224dfe1bb0e27110e0c605c4881cfa883636315 100644
--- a/reviewboard/scmtools/cvs.py
+++ b/reviewboard/scmtools/cvs.py
@@ -1,8 +1,13 @@
+"""SCMTool implementation for CVS."""
+
+from __future__ import annotations
+
 import logging
 import os
 import re
 import shutil
 import tempfile
+from typing import TYPE_CHECKING
 from urllib.parse import urlparse
 
 from django.core.exceptions import ValidationError
@@ -10,7 +15,7 @@ from django.utils.encoding import force_str
 from django.utils.translation import gettext as _
 from djblets.util.filesystem import is_exe_in_path
 
-from reviewboard.scmtools.core import SCMTool, HEAD, PRE_CREATION
+from reviewboard.scmtools.core import SCMClient, SCMTool, HEAD, PRE_CREATION
 from reviewboard.scmtools.errors import (AuthenticationError,
                                          SCMError,
                                          FileNotFoundError,
@@ -19,6 +24,9 @@ from reviewboard.diffviewer.parser import DiffParser, DiffParserError
 from reviewboard.ssh import utils as sshutils
 from reviewboard.ssh.errors import SSHAuthenticationError, SSHError
 
+if TYPE_CHECKING:
+    from reviewboard.scmtools.models import Repository
+
 
 logger = logging.getLogger(__name__)
 
@@ -45,8 +53,17 @@ class CVSTool(SCMTool):
         r'(?P<hostname>[^:]+):(?P<port>\d+)?(?P<path>.*)')
     local_cvsroot_re = re.compile(r'^:(?P<protocol>local|fork):(?P<path>.+)')
 
-    def __init__(self, repository):
-        super(CVSTool, self).__init__(repository)
+    def __init__(
+        self,
+        repository: Repository,
+    ) -> None:
+        """Initialize the CVS tool.
+
+        Args:
+            repository (reviewboard.scmtools.models.Repository):
+                The repository owning the instance of this tool.
+        """
+        super().__init__(repository)
 
         credentials = repository.get_credentials()
 
@@ -65,7 +82,12 @@ class CVSTool(SCMTool):
         if repository.local_site:
             local_site_name = repository.local_site.name
 
-        self.client = CVSClient(self.cvsroot, self.repopath, local_site_name)
+        self.client = CVSClient(
+            cvsroot=self.cvsroot,
+            path=self.repopath,
+            local_site_name=local_site_name,
+            local_site=repository.local_site,
+        )
 
     def get_file(self, path, revision=HEAD, **kwargs):
         if not path:
@@ -580,7 +602,9 @@ class CVSDiffParser(DiffParser):
         return filename
 
 
-class CVSClient(object):
+class CVSClient(SCMClient):
+    """Client class for CVS repositories."""
+
     keywords = [
         b'Author',
         b'Date',
@@ -594,18 +618,45 @@ class CVSClient(object):
         b'State',
     ]
 
-    def __init__(self, cvsroot, path, local_site_name):
-        self.tempdir = None
-        self.currentdir = os.getcwd()
-        self.cvsroot = cvsroot
-        self.path = path
-        self.local_site_name = local_site_name
+    def __init__(
+        self,
+        cvsroot: str,
+        path: str,
+        local_site_name: str | None,
+        **kwargs,
+    ) -> None:
+        """Initialize the client.
+
+        Args:
+            cvsroot (str):
+                The CVSROOT for the repository.
+
+            path (str):
+                The configured repository path.
+
+            local_site_name (str):
+                The name of the Local Site, if any.
 
+            **kwargs (dict):
+                Additional keyword arguments to pass to the parent method.
+
+        Raises:
+            ImportError:
+                The :command:`cvs` command was not found.
+        """
         if not is_exe_in_path('cvs'):
             # This is technically not the right kind of error, but it's the
             # pattern we use with all the other tools.
             raise ImportError
 
+        super().__init__(path=path,
+                         **kwargs)
+
+        self.tempdir = None
+        self.currentdir = os.getcwd()
+        self.cvsroot = cvsroot
+        self.local_site_name = local_site_name
+
     def cleanup(self):
         if self.currentdir != os.getcwd():
             # Restore current working directory
diff --git a/reviewboard/scmtools/git.py b/reviewboard/scmtools/git.py
index f1d3e5bd0bef2334dd9af0da7d73510f209abdc9..f449cef82fb80eb9b44ccff9114e1f44118503cd 100644
--- a/reviewboard/scmtools/git.py
+++ b/reviewboard/scmtools/git.py
@@ -1,12 +1,14 @@
 """SCMTool implementation for Git."""
 
+from __future__ import annotations
+
 import io
 import logging
 import os
 import platform
 import re
 import stat
-from typing import Any, Dict
+from typing import Any, Dict, TYPE_CHECKING
 from urllib.parse import (quote as urlquote,
                           urlparse,
                           urlsplit as urlsplit,
@@ -29,6 +31,10 @@ from reviewboard.scmtools.errors import (FileNotFoundError,
 from reviewboard.scmtools.forms import StandardSCMToolRepositoryForm
 from reviewboard.ssh import utils as sshutils
 
+if TYPE_CHECKING:
+    from reviewboard.scmtools.models import Repository
+    from reviewboard.site.models import LocalSite
+
 
 logger = logging.getLogger(__name__)
 
@@ -114,8 +120,17 @@ class GitTool(SCMTool):
         'executables': ['git']
     }
 
-    def __init__(self, repository):
-        super(GitTool, self).__init__(repository)
+    def __init__(
+        self,
+        repository: Repository,
+    ) -> None:
+        """Initialize the Git tool.
+
+        Args:
+            repository (reviewboard.scmtools.models.Repository):
+                The repository owning the instance of this tool.
+        """
+        super().__init__(repository)
 
         local_site_name = None
 
@@ -124,10 +139,15 @@ class GitTool(SCMTool):
 
         credentials = repository.get_credentials()
 
-        self.client = GitClient(repository.path, repository.raw_file_url,
-                                credentials['username'],
-                                credentials['password'],
-                                repository.encoding, local_site_name)
+        self.client = GitClient(
+            path=repository.path,
+            raw_file_url=repository.raw_file_url,
+            username=credentials['username'],
+            password=credentials['password'],
+            encoding=repository.encoding,
+            local_site_name=local_site_name,
+            local_site=repository.local_site,
+        )
 
     def get_file(self, path, revision=HEAD, **kwargs):
         if revision == PRE_CREATION:
@@ -222,8 +242,15 @@ class GitTool(SCMTool):
         return GitDiffParser(data)
 
     @classmethod
-    def check_repository(cls, path, username=None, password=None,
-                         local_site_name=None, **kwargs):
+    def check_repository(
+        cls,
+        path: str,
+        username: (str | None) = None,
+        password: (str | None) = None,
+        local_site_name: (str | None) = None,
+        local_site: (LocalSite | None) = None,
+        **kwargs,
+    ) -> None:
         """Check a repository configuration for validity.
 
         This should check if a repository exists and can be connected to.
@@ -235,19 +262,22 @@ class GitTool(SCMTool):
         be thrown.
 
         Args:
-            path (unicode):
+            path (str):
                 The repository path.
 
-            username (unicode, optional):
+            username (str, optional):
                 The optional username for the repository.
 
-            password (unicode, optional):
+            password (str, optional):
                 The optional password for the repository.
 
-            local_site_name (unicode, optional):
+            local_site_name (str, optional):
                 The name of the :term:`Local Site` that owns this repository.
                 This is optional.
 
+            local_site (reviewboard.site.models.LocalSite, optional):
+                The :term:`Local Site` that owns this repository.
+
             **kwargs (dict, unused):
                 Additional settings for the repository.
 
@@ -279,17 +309,22 @@ class GitTool(SCMTool):
                 An unexpected exception has occurred. Callers should check
                 for this and handle it.
         """
-        client = GitClient(path,
-                           local_site_name=local_site_name,
-                           username=username,
-                           password=password)
+        client = GitClient(
+            path=path,
+            username=username,
+            password=password,
+            local_site_name=local_site_name,
+            local_site=local_site,
+        )
 
-        super(GitTool, cls).check_repository(
+        super().check_repository(
             path=client.path,
             username=username,
             password=password,
             local_site_name=local_site_name,
-            **kwargs)
+            local_site=local_site,
+            **kwargs,
+        )
 
         if not client.is_valid_repository():
             raise RepositoryNotFoundError()
@@ -751,11 +786,56 @@ class GitClient(SCMClient):
         r'^(?P<username>[A-Za-z0-9_\.-]+@)?(?P<hostname>[A-Za-z0-9_\.-]+):'
         r'(?P<path>.*)')
 
-    def __init__(self, path, raw_file_url=None, username=None, password=None,
-                 encoding='', local_site_name=None):
-        super(GitClient, self).__init__(self._normalize_git_url(path),
-                                        username=username,
-                                        password=password)
+    def __init__(
+        self,
+        path: str,
+        raw_file_url: (str | None) = None,
+        username: (str | None) = None,
+        password: (str | None) = None,
+        encoding: str = '',
+        local_site_name: (str | None) = None,
+        **kwargs,
+    ) -> None:
+        """Initialize the Git client.
+
+        Args:
+            path (str):
+                The configured repository path.
+
+            raw_file_url (str, optional):
+                The optional URL template for accessing raw files via HTTP(S).
+
+            username (str, optional):
+                The optional username for communicating with the repository.
+
+            password (str, optional):
+                The optional password for communicating with the repository.
+
+            encoding (str, optional):
+                An optional encoding.
+
+                This is unused and scheduled for deprecation.
+
+            local_site_name (str, optional):
+                The name of the associated Local Site, if any.
+
+            **kwargs (dict):
+                Keyword arguments to pass to the parent method.
+
+        Raises:
+            ImportError:
+                There was an error locating the :command:`git` command line
+                tool.
+
+            reviewboard.scmtools.errors.SCMError:
+                There was an error accessing the repository.
+        """
+        super().__init__(
+            self._normalize_git_url(path),
+            username=username,
+            password=password,
+            **kwargs,
+        )
 
         if not is_exe_in_path('git'):
             # This is technically not the right kind of error, but it's the
diff --git a/reviewboard/scmtools/hg.py b/reviewboard/scmtools/hg.py
index 724886fa5dfb215ca90d482d4eba092ed8f9834d..22850cc1d48b4e1f15c483fda4c90a8704b1d318 100644
--- a/reviewboard/scmtools/hg.py
+++ b/reviewboard/scmtools/hg.py
@@ -34,6 +34,7 @@ if TYPE_CHECKING:
         RevisionID,
     )
     from reviewboard.scmtools.models import Repository
+    from reviewboard.site.models import LocalSite
 
 
 logger = logging.getLogger(__name__)
@@ -58,15 +59,23 @@ class HgTool(SCMTool):
         self,
         repository: Repository,
     ) -> None:
-        """Initialize the SCMTool."""
+        """Initialize the SCMTool.
+
+        Args:
+            repository (reviewboard.scmtools.models.Repository):
+                The repository owning the instance of this tool.
+        """
         super().__init__(repository)
 
         if repository.path.startswith('http'):
             credentials = repository.get_credentials()
 
-            self.client = HgWebClient(repository.path,
-                                      credentials['username'],
-                                      credentials['password'])
+            self.client = HgWebClient(
+                path=repository.path,
+                username=credentials['username'],
+                password=credentials['password'],
+                local_site=repository.local_site,
+            )
         else:
             if not is_exe_in_path('hg'):
                 # This is technically not the right kind of error, but it's the
@@ -78,7 +87,11 @@ class HgTool(SCMTool):
             else:
                 local_site_name = None
 
-            self.client = HgClient(repository.path, local_site_name)
+            self.client = HgClient(
+                path=repository.path,
+                local_site_name=local_site_name,
+                local_site=repository.local_site,
+            )
 
     def get_file(
         self,
@@ -258,9 +271,10 @@ class HgTool(SCMTool):
     def check_repository(
         cls,
         path: str,
-        username: Optional[str] = None,
-        password: Optional[str] = None,
-        local_site_name: Optional[str] = None,
+        username: (str | None) = None,
+        password: (str | None) = None,
+        local_site_name: (str | None) = None,
+        local_site: (LocalSite | None) = None,
         **kwargs,
     ) -> None:
         """Check a repository configuration for validity.
@@ -287,6 +301,10 @@ class HgTool(SCMTool):
                 The name of the :term:`Local Site` that owns this repository.
                 This is optional.
 
+            local_site (reviewboard.site.models.LocalSite, optional):
+                The :term:`Local Site` instance that owns this repository.
+                This is optional.
+
             **kwargs (dict, unused):
                 Additional settings for the repository.
 
@@ -328,13 +346,20 @@ class HgTool(SCMTool):
             username=username,
             password=password,
             local_site_name=local_site_name,
-            **kwargs)
+            local_site=local_site,
+            **kwargs,
+        )
 
         # Create a client. This will fail if the repository doesn't exist.
         if result.scheme in ('http', 'https'):
-            HgWebClient(path, username, password)
+            HgWebClient(path=path,
+                        username=username,
+                        password=password,
+                        local_site=local_site)
         else:
-            HgClient(path, local_site_name)
+            HgClient(path=path,
+                     local_site_name=local_site_name,
+                     local_site=local_site)
 
     def normalize_patch(
         self,
@@ -644,27 +669,25 @@ class HgWebClient(SCMClient):
 
     def __init__(
         self,
-        path: str,
-        username: Optional[str],
-        password: Optional[str],
+        *args,
+        **kwargs,
     ) -> None:
         """Initialize the client.
 
         Args:
-            path (str):
-                The repository path.
+            *args (tuple):
+                Positional arguments to pass to the parent method.
 
-            username (str, optional):
-                The username used for the repository.
-
-            password (str, optional):
-                The password used for the repository.
+            **kwargs (dict):
+                Keyword arguments to pass to the parent method.
         """
-        super().__init__(path, username=username, password=password)
+        super().__init__(*args, **kwargs)
+
+        path = self.path
+        self.path_stripped = path.rstrip('/')
 
-        self.path_stripped = self.path.rstrip('/')
         logger.debug('Initialized HgWebClient with url=%r, username=%r',
-                     self.path, self.username)
+                     path, self.username)
 
     def cat_file(
         self,
@@ -920,18 +943,30 @@ class HgClient(SCMClient):
     def __init__(
         self,
         path: str,
-        local_site_name: Optional[str],
+        local_site_name: str | None,
+        **kwargs,
     ) -> None:
         """Initialize the client.
 
+        Version Changed:
+            7.1:
+            Added the ``**kwargs`` argument.
+
         Args:
             path (str):
                 The path to the repository.
 
             local_site_name (str, optional):
                 The name of the Local Site that the repository is part of.
+
+            **kwargs (dict):
+                Additional keyword arguments for the parent method.
+
+                Version Added:
+                    7.1
         """
-        super().__init__(path)
+        super().__init__(path, **kwargs)
+
         self.default_args = None
         self.local_site_name = local_site_name
 
diff --git a/reviewboard/scmtools/perforce.py b/reviewboard/scmtools/perforce.py
index 6ef69eace7f34e05cdc37a9f3f82f1a132307818..99b9e897eea51eff78e46e152838a22c34b058a1 100644
--- a/reviewboard/scmtools/perforce.py
+++ b/reviewboard/scmtools/perforce.py
@@ -24,8 +24,11 @@ from djblets.util.filesystem import is_exe_in_path
 
 from reviewboard.diffviewer.parser import DiffParser
 from reviewboard.scmtools.certs import Certificate
-from reviewboard.scmtools.core import (SCMTool, ChangeSet,
-                                       HEAD, PRE_CREATION)
+from reviewboard.scmtools.core import (ChangeSet,
+                                       HEAD,
+                                       PRE_CREATION,
+                                       SCMClient,
+                                       SCMTool)
 from reviewboard.scmtools.errors import (SCMError, EmptyChangeSetError,
                                          AuthenticationError,
                                          InvalidRevisionFormatError,
@@ -34,6 +37,8 @@ from reviewboard.scmtools.errors import (SCMError, EmptyChangeSetError,
 
 if TYPE_CHECKING:
     from reviewboard.scmtools.core import Revision
+    from reviewboard.scmtools.models import Repository
+    from reviewboard.site.models import LocalSite
 
 
 logger = logging.getLogger(__name__)
@@ -222,7 +227,7 @@ class STunnelProxy(object):
                     pass
 
 
-class PerforceClient(object):
+class PerforceClient(SCMClient):
     """Client for talking to a Perforce server.
 
     This manages Perforce connections to the server, and provides a set of
@@ -237,38 +242,55 @@ class PerforceClient(object):
     #: We default this to 1 hour.
     TICKET_RENEWAL_SECS = 1 * 60 * 60
 
-    def __init__(self, path, username, password, encoding='', host=None,
-                 client_name=None, local_site_name=None,
-                 use_ticket_auth=False):
+    def __init__(
+        self,
+        path: str,
+        username: str | None,
+        password: str | None,
+        encoding: str = '',
+        host: (str | None) = None,
+        client_name: (str | None) = None,
+        local_site_name: (str | None) = None,
+        use_ticket_auth: bool = False,
+        **kwargs,
+    ) -> None:
         """Initialize the client.
 
         Args:
-            path (unicode):
+            path (str):
                 The path to the repository (equivalent to :envvar:`P4PORT`).
 
-            username (unicode):
+            username (str):
                 The username for the connection.
 
-            password (unicode):
+            password (str):
                 The password for the connection.
 
-            encoding (unicode, optional):
+            encoding (str, optional):
                 The encoding to use for the connection.
 
-            host (unicode, optional):
+            host (str, optional):
                 The client's host name to use for the connection (equivalent
                 to :envvar:`P4HOST`).
 
-            client_name (unicode, optional):
+            client_name (str, optional):
                 The name of the Perforce client (equivalent to
                 :envvar:`P4CLIENT`).
 
-            local_site_name (unicode, optional):
+            local_site_name (str, optional):
                 The name of the local site used for the repository.
 
             use_ticket_auth (bool, optional):
                 Whether to use ticket-based authentication. By default, this
                 is not used.
+
+            **kwargs (dict):
+                Keyword arguments to pass to the parent method.
+
+        Raises:
+            AttributeError:
+                Stunnel was specified, but :command:`stunnel` was not in
+                the path.
         """
         if path.startswith('stunnel:'):
             path = path[8:]
@@ -276,22 +298,27 @@ class PerforceClient(object):
         else:
             self.use_stunnel = False
 
+        if self.use_stunnel and not is_exe_in_path('stunnel'):
+            raise AttributeError('stunnel proxy was requested, but stunnel '
+                                 'binary is not in the exec path.')
+
+        import P4
+        self.p4 = P4.P4()
+
+        super().__init__(
+            path=path,
+            username=username,
+            password=password or '',
+            **kwargs,
+        )
+
         self.p4port = path
-        self.username = username
-        self.password = password or ''
         self.encoding = encoding
         self.p4host = host
         self.client_name = client_name
         self.local_site_name = local_site_name
         self.use_ticket_auth = use_ticket_auth
 
-        import P4
-        self.p4 = P4.P4()
-
-        if self.use_stunnel and not is_exe_in_path('stunnel'):
-            raise AttributeError('stunnel proxy was requested, but stunnel '
-                                 'binary is not in the exec path.')
-
     def get_ticket_status(self):
         """Return the status of the current login ticket.
 
@@ -674,14 +701,17 @@ class PerforceTool(SCMTool):
         'modules': ['P4'],
     }
 
-    def __init__(self, repository):
+    def __init__(
+        self,
+        repository: Repository,
+    ) -> None:
         """Initialize the Perforce tool.
 
         Args:
             repository (reviewboard.scmtools.models.Repository):
                 The repository owning the instance of this tool.
         """
-        super(PerforceTool, self).__init__(repository)
+        super().__init__(repository)
 
         credentials = repository.get_credentials()
 
@@ -699,12 +729,23 @@ class PerforceTool(SCMTool):
             client_name=repository.extra_data.get('p4_client'),
             local_site_name=local_site_name,
             use_ticket_auth=repository.extra_data.get('use_ticket_auth',
-                                                      False))
+                                                      False),
+            local_site=repository.local_site,
+        )
 
     @classmethod
-    def check_repository(cls, path, username=None, password=None,
-                         p4_host=None, p4_client=None, local_site_name=None,
-                         **kwargs):
+    def check_repository(
+        cls,
+        path: str,
+        username: (str | None) = None,
+        password: (str | None) = None,
+        local_site_name: (str | None) = None,
+        local_site: (LocalSite | None) = None,
+        *,
+        p4_host: (str | None) = None,
+        p4_client: (str | None) = None,
+        **kwargs,
+    ) -> None:
         """Perform checks on a repository to test its validity.
 
         This checks if a repository exists and can be connected to.
@@ -715,27 +756,30 @@ class PerforceTool(SCMTool):
         be thrown.
 
         Args:
-            path (unicode):
+            path (str):
                 The Perforce repository path (equivalent to :envvar:`P4PORT`).
 
-            username (unicode, optional):
+            username (str, optional):
                 The username used to authenticate.
 
-            password (unicode, optional):
+            password (str, optional):
                 The password used to authenticate.
 
-            p4_host (unicode, optional):
+            p4_host (str, optional):
                 The optional Perforce host name (equivalent to
                 :envvar:`P4HOST`).
 
-            p4_client (unicode, optional):
+            p4_client (str, optional):
                 The optional Perforce client name (equivalent to
                 :envvar:`P4CLIENT`).
 
-            local_site_name (unicode, optional):
+            local_site_name (str, optional):
                 The name of the :term:`Local Site` that owns this repository.
                 This is optional.
 
+            local_site (reviewboard.site.models.LocalSite, optional):
+                The :term:`Local Site` instance that owns this repository.
+
             **kwargs (dict, unused):
                 Additional settings for the repository.
 
@@ -749,25 +793,30 @@ class PerforceTool(SCMTool):
             reviewboard.scmtools.errors.SCMError:
                 There was a general error communicating with Perforce.
 
-            reviewboard.scmtools.errors.UnverifiedCertificateError:
+            reviewboard.certs.errors.CertificateVerificationError:
                 The Perforce SSL certificate could not be verified.
         """
-        super(PerforceTool, cls).check_repository(
+        super().check_repository(
             path=path,
             username=username,
             password=password,
             local_site_name=local_site_name,
-            **kwargs)
+            local_site=local_site,
+            **kwargs,
+        )
 
         # 'p4 info' will succeed even if the server requires ticket auth and we
         # don't run 'p4 login' first. We therefore don't go through all the
         # trouble of handling tickets here.
-        client = PerforceClient(path=path,
-                                username=username,
-                                password=password,
-                                host=p4_host,
-                                client_name=p4_client,
-                                local_site_name=local_site_name)
+        client = PerforceClient(
+            path=path,
+            username=username,
+            password=password,
+            host=p4_host,
+            client_name=p4_client,
+            local_site_name=local_site_name,
+            local_site=local_site,
+        )
         client.get_info()
 
     def get_changeset(self, changeset_id, allow_empty=False):
