diff --git a/reviewboard/hostingsvcs/assembla.py b/reviewboard/hostingsvcs/assembla.py
index 41aaf68102af7eb36957fcc9f090d523b7054a54..06864e154d8b2f360820abb74ccb716cf92bf49d 100644
--- a/reviewboard/hostingsvcs/assembla.py
+++ b/reviewboard/hostingsvcs/assembla.py
@@ -1,12 +1,23 @@
+"""Hosting service for Assembla."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
 from django import forms
 from django.utils.translation import gettext_lazy as _
+from housekeeping import deprecate_non_keyword_only_args
 
 from reviewboard.admin.server import get_hostname
+from reviewboard.deprecation import RemovedInReviewBoard90Warning
 from reviewboard.hostingsvcs.base.forms import BaseHostingServiceRepositoryForm
 from reviewboard.hostingsvcs.base.hosting_service import BaseHostingService
 from reviewboard.scmtools.crypto_utils import (decrypt_password,
                                                encrypt_password)
 
+if TYPE_CHECKING:
+    from reviewboard.scmtools.core import SCMTool
+
 
 class AssemblaForm(BaseHostingServiceRepositoryForm):
     assembla_project_id = forms.CharField(
@@ -95,45 +106,62 @@
         """
         return '%s-%s' % (get_hostname(), project_id.replace('/', '-'))
 
-    def check_repository(self, path, username, password, scmtool_class,
-                         local_site_name, assembla_project_id=None,
-                         *args, **kwargs):
+    @deprecate_non_keyword_only_args(RemovedInReviewBoard90Warning)
+    def check_repository(
+        self,
+        *,
+        path: str | None,
+        username: str | None,
+        password: str | None,
+        scmtool_class: type[SCMTool],
+        local_site_name: str | None,
+        assembla_project_id: str,
+        **kwargs,
+    ) -> None:
         """Check the validity of a repository hosted on Assembla.
 
         Perforce repositories are handled specially. The Assembla project ID
         will be used as the Perforce host, which is needed to tell Assembla
         which repository on the server to use.
 
+        Version Changed:
+            7.1:
+            Made arguments keyword-only.
+
         Args:
-            path (unicode):
+            path (str):
                 The repository path.
 
-            username (unicode):
+            username (str):
                 The username used for authenticating.
 
-            password (unicode):
+            password (str):
                 The password used for authenticating.
 
             scmtool_class (type):
                 The SCMTool for the repository.
 
-            local_site_name (unicode):
+            local_site_name (str):
                 The name of the Local Site, if any.
 
-            assembla_project_id (unicode):
+            assembla_project_id (str):
                 The project ID for the Assembla team.
 
-            *args (tuple):
-                Additional arguments to pass to the superclass.
-
             **kwargs (dict):
-                Additional keyword arguments to pass to the superclass.
+                Additional keyword arguments passed by the repository form.
+
+        Raises:
+            ValueError:
+                The value of a field was incorrect.
         """
         # We want to use the configured username and other information from
         # the account.
         username = self.account.username
         password = self.get_password()
 
+        if path is None:
+            raise ValueError('path cannot be None')
+
         if scmtool_class.name == 'Perforce':
             scmtool_class.check_repository(
                 path=path,
@@ -143,7 +171,7 @@
                 p4_host=assembla_project_id,
                 p4_client=self.make_p4_client_name(assembla_project_id))
         else:
-            super(Assembla, self).check_repository(
+            super().check_repository(
                 path=path,
                 username=username,
                 password=password,
@@ -151,24 +179,32 @@
                 scmtool_class=scmtool_class,
                 **kwargs)
 
-    def authorize(self, username, password, *args, **kwargs):
+    @deprecate_non_keyword_only_args(RemovedInReviewBoard90Warning)
+    def authorize(
+        self,
+        *,
+        username: str | None,
+        password: str | None,
+        **kwargs,
+    ) -> None:
         """Authorize the Assembla account.
 
         For Assembla, we simply use the native SCMTool support, as there's
         no useful API available. We just store the password encrypted, which
         will be used by the SCMTool.
 
+        Version Changed:
+            7.1:
+            Made arguments keyword-only.
+
         Args:
-            username (unicode):
+            username (str):
                 The username for authentication.
 
-            password (unicode):
+            password (str):
                 The password for authentication.
 
-            *args (tuple):
-                Additional arguments.
-
-            **kwargs (dict):
+            **kwargs (dict, unused):
                 Additional keyword arguments.
         """
         self.account.data['password'] = encrypt_password(password)
diff --git a/reviewboard/hostingsvcs/base/client.py b/reviewboard/hostingsvcs/base/client.py
index 5297a317277830dd248ee2d16f671f673b3a0f46..d688ccb6fc5380fb534851d37dabe2a8eb5d0ffe 100644
--- a/reviewboard/hostingsvcs/base/client.py
+++ b/reviewboard/hostingsvcs/base/client.py
@@ -13,7 +13,7 @@
 import logging
 import ssl
 from email.generator import _make_boundary as generate_boundary
-from typing import Callable, Optional, TYPE_CHECKING, Tuple, Type, Union
+from typing import TYPE_CHECKING
 from urllib.error import URLError
 from urllib.parse import urlparse
 
@@ -30,6 +30,8 @@
 from reviewboard.scmtools.errors import UnverifiedCertificateError
 
 if TYPE_CHECKING:
+    from typing import Callable
+
     from typelets.json import JSONValue
 
     from reviewboard.hostingsvcs.base.hosting_service import (
@@ -70,7 +72,7 @@
     #:
     #: Version Added:
     #:     4.0
-    http_request_cls: Type[HostingServiceHTTPRequest] = \
+    http_request_cls: type[HostingServiceHTTPRequest] = \
         HostingServiceHTTPRequest
 
     #: The HTTP response class to construct HTTP responses.
@@ -80,7 +82,7 @@
     #:
     #: Version Added:
     #:     4.0
-    http_response_cls: Type[HostingServiceHTTPResponse] = \
+    http_response_cls: type[HostingServiceHTTPResponse] = \
         HostingServiceHTTPResponse
 
     #: Whether to add HTTP Basic Auth headers by default.
@@ -131,7 +133,7 @@
     def http_delete(
         self,
         url: str,
-        headers: Optional[HTTPHeaders] = None,
+        headers: (HTTPHeaders | None) = None,
         *args,
         **kwargs,
     ) -> HostingServiceHTTPResponse:
@@ -178,7 +180,7 @@
     def http_get(
         self,
         url: str,
-        headers: Optional[HTTPHeaders] = None,
+        headers: (HTTPHeaders | None) = None,
         *args,
         **kwargs,
     ) -> HostingServiceHTTPResponse:
@@ -225,7 +227,7 @@
     def http_head(
         self,
         url: str,
-        headers: Optional[HTTPHeaders] = None,
+        headers: (HTTPHeaders | None) = None,
         *args,
         **kwargs,
     ) -> HostingServiceHTTPResponse:
@@ -270,11 +272,11 @@
     def http_post(
         self,
         url: str,
-        body: Optional[bytes] = None,
-        fields: Optional[FormFields] = None,
-        files: Optional[UploadedFiles] = None,
-        content_type: Optional[str] = None,
-        headers: Optional[HTTPHeaders] = None,
+        body: (bytes | None) = None,
+        fields: (FormFields | None) = None,
+        files: (UploadedFiles | None) = None,
+        content_type: (str | None) = None,
+        headers: (HTTPHeaders | None) = None,
         *args,
         **kwargs,
     ) -> HostingServiceHTTPResponse:
@@ -351,11 +353,11 @@
     def http_put(
         self,
         url: str,
-        body: Optional[bytes] = None,
-        fields: Optional[FormFields] = None,
-        files: Optional[UploadedFiles] = None,
-        content_type: Optional[str] = None,
-        headers: Optional[HTTPHeaders] = None,
+        body: (bytes | None) = None,
+        fields: (FormFields | None) = None,
+        files: (UploadedFiles | None) = None,
+        content_type: (str | None) = None,
+        headers: (HTTPHeaders | None) = None,
         *args,
         **kwargs,
     ) -> HostingServiceHTTPResponse:
@@ -429,8 +431,8 @@
     def http_request(
         self,
         url: str,
-        body: Optional[bytes] = None,
-        headers: Optional[HTTPHeaders] = None,
+        body: (bytes | None) = None,
+        headers: (HTTPHeaders | None) = None,
         method: str = 'GET',
         **kwargs,
     ) -> HostingServiceHTTPResponse:
@@ -523,8 +525,8 @@
     def get_http_credentials(
         self,
         account: HostingServiceAccount,
-        username: Optional[str] = None,
-        password: Optional[str] = None,
+        username: (str | None) = None,
+        password: (str | None) = None,
         **kwargs,
     ) -> HostingServiceCredentials:
         """Return credentials used to authenticate with the service.
@@ -762,7 +764,7 @@
         self,
         *args,
         **kwargs,
-    ) -> Tuple[JSONValue, HTTPHeaders]:
+    ) -> tuple[JSONValue, HTTPHeaders]:
         """Perform an HTTP DELETE and interpret the results as JSON.
 
         Deprecated:
@@ -814,7 +816,7 @@
         self,
         *args,
         **kwargs,
-    ) -> Tuple[JSONValue, HTTPHeaders]:
+    ) -> tuple[JSONValue, HTTPHeaders]:
         """Perform an HTTP GET and interpret the results as JSON.
 
         Deprecated:
@@ -866,7 +868,7 @@
         self,
         *args,
         **kwargs,
-    ) -> Tuple[JSONValue, HTTPHeaders]:
+    ) -> tuple[JSONValue, HTTPHeaders]:
         """Perform an HTTP POST and interpret the results as JSON.
 
         Deprecated:
@@ -916,11 +918,13 @@
 
     def _do_json_method(
         self,
-        method: Callable[..., Union[Tuple[bytes, HTTPHeaders],
-                                    HostingServiceHTTPResponse]],
+        method: Callable[
+            ...,
+            tuple[bytes, HTTPHeaders] | HostingServiceHTTPResponse
+        ],
         *args,
         **kwargs,
-    ) -> Tuple[JSONValue, HTTPHeaders]:
+    ) -> tuple[JSONValue, HTTPHeaders]:
         """Parse the result of an HTTP operation as JSON.
 
         Deprecated:
@@ -971,16 +975,16 @@
 
             return data, headers
         else:
-            raise NotImplementedError('Unsupported response from %r' % method)
+            raise NotImplementedError(f'Unsupported response from {method!r}')
 
     def _build_put_post_request(
         self,
-        body: Optional[bytes] = None,
-        fields: Optional[FormFields] = None,
-        files: Optional[UploadedFiles] = None,
-        content_type: Optional[str] = None,
-        headers: Optional[HTTPHeaders] = None,
-    ) -> Tuple[bytes, HTTPHeaders]:
+        body: (bytes | None) = None,
+        fields: (FormFields | None) = None,
+        files: (UploadedFiles | None) = None,
+        content_type: (str | None) = None,
+        headers: (HTTPHeaders | None) = None,
+    ) -> tuple[bytes, HTTPHeaders]:
         """Build a request body and headers for a HTTP PUT or POST.
 
         Args:
@@ -1030,7 +1034,7 @@
         if content_type:
             headers['Content-Type'] = content_type
 
-        headers['Content-Length'] = '%d' % len(body)
+        headers['Content-Length'] = str(len(body))
 
         return body, headers
 
@@ -1040,9 +1044,9 @@
 
     @staticmethod
     def build_form_data(
-        fields: Optional[FormFields],
-        files: Optional[UploadedFiles] = None,
-    ) -> Tuple[bytes, str]:
+        fields: FormFields | None,
+        files: (UploadedFiles | None) = None,
+    ) -> tuple[bytes, str]:
         """Encode data for use in an HTTP POST.
 
         Args:
@@ -1119,7 +1123,7 @@
         content_parts.append(b'--%s--' % enc_boundary)
 
         content = b''.join(content_parts)
-        content_type = 'multipart/form-data; boundary=%s' % boundary
+        content_type = f'multipart/form-data; boundary={boundary}'
 
         return content, content_type
 
diff --git a/reviewboard/hostingsvcs/base/forms.py b/reviewboard/hostingsvcs/base/forms.py
index 02d08176b8629ea9dbf200f326a4555f970b1372..5647ea20c116d5161cc521d36305a19c1e3ee97f 100644
--- a/reviewboard/hostingsvcs/base/forms.py
+++ b/reviewboard/hostingsvcs/base/forms.py
@@ -8,10 +8,11 @@
 from __future__ import annotations
 
 import logging
-from typing import Optional, TYPE_CHECKING, Type
+from typing import TYPE_CHECKING
 
 from django import forms
 from django.utils.translation import gettext, gettext_lazy as _
+from typelets.runtime import raise_invalid_type
 
 from reviewboard.hostingsvcs.errors import (AuthorizationError,
                                             TwoFactorAuthCodeRequiredError)
@@ -57,12 +58,12 @@
     #:
     #: Type:
     #:     type
-    hosting_service_cls: Type[BaseHostingService]
+    hosting_service_cls: type[BaseHostingService]
 
     def __init__(
         self,
         *args,
-        hosting_service_cls: Type[BaseHostingService],
+        hosting_service_cls: type[BaseHostingService],
         **kwargs,
     ) -> None:
         """Initialize the authentication form.
@@ -84,7 +85,8 @@
                 message.
         """
         if not hosting_service_cls:
-            raise ValueError('hosting_service_cls cannot be None.')
+            raise_invalid_type(hosting_service_cls,
+                               'hosting_service_cls cannot be None.')
 
         self.hosting_service_cls = hosting_service_cls
 
@@ -188,12 +190,12 @@
     #:
     #: Type:
     #:     reviewboard.hostingsvcs.models.HostingServiceAccount
-    hosting_account: Optional[HostingServiceAccount]
+    hosting_account: HostingServiceAccount | None
 
     def __init__(
         self,
         *args,
-        hosting_account: Optional[HostingServiceAccount] = None,
+        hosting_account: (HostingServiceAccount | None) = None,
         **kwargs,
     ) -> None:
         """Initialize the authentication form.
@@ -314,7 +316,7 @@
         self,
         allow_authorize: bool = True,
         force_authorize: bool = False,
-        extra_authorize_kwargs: Optional[KwargsDict] = None,
+        extra_authorize_kwargs: (KwargsDict | None) = None,
         trust_host: bool = False,
         save: bool = True,
     ) -> HostingServiceAccount:
@@ -387,7 +389,7 @@
 
         hosting_account = self.hosting_account
         hosting_service_id = self.hosting_service_cls.hosting_service_id
-        hosting_url: Optional[str] = self.cleaned_data.get('hosting_url')
+        hosting_url: (str | None) = self.cleaned_data.get('hosting_url')
 
         assert hosting_service_id
 
@@ -423,7 +425,7 @@
             self.hosting_service_cls.needs_authorization and
             (not hosting_account.is_authorized or force_authorize)):
             # Attempt to authorize the account.
-            local_site_name: Optional[str] = None
+            local_site_name: (str | None) = None
 
             if self.local_site:
                 local_site_name = self.local_site.name
@@ -460,8 +462,8 @@
         self,
         hosting_account: HostingServiceAccount,
         hosting_service_id: str,
-        username: Optional[str] = None,
-        local_site_name: Optional[str] = None,
+        username: (str | None) = None,
+        local_site_name: (str | None) = None,
         **kwargs,
     ) -> None:
         """Authorize the service.
@@ -518,7 +520,7 @@
             # Re-raise the error.
             raise
 
-    def clean_hosting_url(self) -> Optional[str]:
+    def clean_hosting_url(self) -> str | None:
         """Clean the hosting URL field.
 
         Returns:
@@ -611,7 +613,7 @@
 
     def load(
         self,
-        repository: Optional[Repository] = None,
+        repository: (Repository | None) = None,
         **kwargs,
     ) -> None:
         """Load information for the form.
@@ -626,12 +628,15 @@
 
                 This is scheduled to be deprecated. Subclasses should use the
                 :py:attr:`repository` attribute instead.
+
+            **kwargs (dict, unused):
+                Unused keyword arguments.
         """
         super().load()
 
     def save(
         self,
-        repository: Optional[Repository] = None,
+        repository: (Repository | None) = None,
         **kwargs,
     ) -> None:
         """Save information from the form back to the repository.
@@ -645,6 +650,9 @@
 
                 This is scheduled to be deprecated. Subclasses should use the
                 :py:attr:`repository` attribute instead.
+
+            **kwargs (dict, unused):
+                Unused keyword arguments.
         """
         if repository is None:
             repository = self.repository
diff --git a/reviewboard/hostingsvcs/base/hosting_service.py b/reviewboard/hostingsvcs/base/hosting_service.py
index c1c27223e118415c18932a23f9c9c9512ae3a5a1..f201dec35dd33545a6ebf0e91deec44badaad87e 100644
--- a/reviewboard/hostingsvcs/base/hosting_service.py
+++ b/reviewboard/hostingsvcs/base/hosting_service.py
@@ -9,26 +9,34 @@
 from __future__ import annotations
 
 import logging
-from typing import (Any, Dict, List, Mapping, Optional, Sequence,
-                    TYPE_CHECKING, Tuple, Type)
+from collections.abc import Mapping
+from typing import Any, Dict, TYPE_CHECKING
 from urllib.parse import urlparse
 
 from django.utils.translation import gettext_lazy as _
-from typing_extensions import NotRequired, TypeAlias, TypedDict
+from housekeeping import deprecate_non_keyword_only_args
+from typing_extensions import TypedDict
 
+from reviewboard.deprecation import RemovedInReviewBoard90Warning
 from reviewboard.hostingsvcs.base.client import HostingServiceClient
 
 if TYPE_CHECKING:
+    from collections.abc import Sequence
+    from typing import ClassVar
+
+    from typing_extensions import TypeAlias
+
     from django.http import HttpRequest
     from django.urls import _AnyURL
     from django.utils.safestring import SafeString
     from paramiko import PKey
     from typelets.django.strings import StrOrPromise
+    from typing_extensions import NotRequired
 
     from reviewboard.hostingsvcs.base.forms import (
         BaseHostingServiceAuthForm,
         BaseHostingServiceRepositoryForm)
-    from reviewboard.hostingsvcs.base.paginator import APIPaginator
+    from reviewboard.hostingsvcs.base.paginator import BasePaginator
     from reviewboard.hostingsvcs.models import HostingServiceAccount
     from reviewboard.hostingsvcs.repository import RemoteRepository
     from reviewboard.scmtools.core import Branch, Commit, SCMTool
@@ -70,7 +78,7 @@
     #:
     #: Type:
     #:     type
-    form: NotRequired[Type[BaseHostingServiceRepositoryForm]]
+    form: NotRequired[type[BaseHostingServiceRepositoryForm]]
 
     #: Templated values to set for model repository fields.
     #:
@@ -142,13 +150,13 @@
     #:
     #: Type:
     #:     str
-    hosting_service_id: Optional[str] = None
+    hosting_service_id: ClassVar[str | None] = None
 
     #: The display name for the hosting service.
     #:
     #: Type:
     #:     str
-    name: Optional[StrOrPromise] = None
+    name: ClassVar[StrOrPromise | None] = None
 
     #: A list of available hosting service plans.
     #:
@@ -159,7 +167,7 @@
     #:
     #: Type:
     #:     list
-    plans: Optional[Sequence[Tuple[str, HostingServicePlan]]] = None
+    plans: ClassVar[Sequence[tuple[str, HostingServicePlan]] | None] = None
 
     #: Whether this service supports bug trackers.
     #:
@@ -171,7 +179,7 @@
     #:
     #: Type:
     #:     bool
-    supports_bug_trackers: bool = False
+    supports_bug_trackers: ClassVar[bool] = False
 
     #: Whether this service supports post-commit requests.
     #:
@@ -184,7 +192,7 @@
     #:
     #: Type:
     #:     bool
-    supports_post_commit: bool = False
+    supports_post_commit: ClassVar[bool] = False
 
     #: Whether this service supports source code repositories.
     #:
@@ -201,7 +209,7 @@
     #:
     #: Type:
     #:     bool
-    supports_repositories: bool = False
+    supports_repositories: ClassVar[bool] = False
 
     #: Whether this service supports automatic association of SSH keys.
     #:
@@ -214,7 +222,7 @@
     #:
     #: Type:
     #:     bool
-    supports_ssh_key_association: bool = False
+    supports_ssh_key_association: ClassVar[bool] = False
 
     #: Whether this service supports two-factor authentication.
     #:
@@ -224,7 +232,7 @@
     #:
     #: Type:
     #:     bool
-    supports_two_factor_auth: bool = False
+    supports_two_factor_auth: ClassVar[bool] = False
 
     #: Whether this service supports listing remote repositories.
     #:
@@ -237,7 +245,7 @@
     #:
     #: Type:
     #:     bool
-    supports_list_remote_repositories: bool = False
+    supports_list_remote_repositories: ClassVar[bool] = False
 
     #: Whether this service provides repository hook instructions.
     #:
@@ -248,7 +256,7 @@
     #:
     #: Type:
     #:     bool
-    has_repository_hook_instructions: bool = False
+    has_repository_hook_instructions: ClassVar[bool] = False
 
     #: Whether this service should be shown as an available option.
     #:
@@ -262,7 +270,7 @@
     #:
     #: Type:
     #:     bool
-    visible: bool = True
+    visible: ClassVar[bool] = True
 
     #: Whether this service can be self-hosted.
     #:
@@ -271,7 +279,7 @@
     #:
     #: Type:
     #:     bool
-    self_hosted: bool = False
+    self_hosted: ClassVar[bool] = False
 
     #: Custom URL patterns to include for the hosting service.
     #:
@@ -283,7 +291,7 @@
     #:
     #: Type:
     #:     list
-    repository_url_patterns: Optional[List[_AnyURL]] = None
+    repository_url_patterns: ClassVar[list[_AnyURL] | None] = None
 
     #: The utility class used to perform client functionality.
     #:
@@ -296,7 +304,7 @@
     #:
     #: Type:
     #:     type
-    client_class: Type[HostingServiceClient] = HostingServiceClient
+    client_class: ClassVar[type[HostingServiceClient]] = HostingServiceClient
 
     #: Optional form used to configure authentication settings for an account.
     #:
@@ -304,7 +312,7 @@
     #:
     #: Type:
     #:     type
-    auth_form: Optional[Type[BaseHostingServiceAuthForm]] = None
+    auth_form: ClassVar[type[BaseHostingServiceAuthForm] | None] = None
 
     #: Whether usage of this hosting service requires authorization.
     #:
@@ -312,7 +320,7 @@
     #:
     #: Type:
     #:     bool
-    needs_authorization: bool = False
+    needs_authorization: ClassVar[bool] = False
 
     #: Optional form used to configure repository settings.
     #:
@@ -322,7 +330,7 @@
     #:
     #: Type:
     #:     type
-    form: Optional[Type[BaseHostingServiceRepositoryForm]] = None
+    form: ClassVar[type[BaseHostingServiceRepositoryForm] | None] = None
 
     #: Templated values to set for model repository fields.
     #:
@@ -333,7 +341,7 @@
     #:
     #: Type:
     #:     dict
-    repository_fields: SCMToRepositoryFields = {}
+    repository_fields: ClassVar[SCMToRepositoryFields] = {}
 
     #: A templated value for the bug tracker field.
     #:
@@ -342,7 +350,7 @@
     #:
     #: Type:
     #:     str
-    bug_tracker_field: Optional[str] = None
+    bug_tracker_field: ClassVar[str | None] = None
 
     #: A list of SCMTools IDs or names that are supported by this service.
     #:
@@ -359,7 +367,7 @@
     #:
     #: Type:
     #:     list of str
-    supported_scmtools: List[str] = []
+    supported_scmtools: ClassVar[Sequence[str]] = []
 
     #: A list of SCMTool IDs that are visible when configuring the service.
     #:
@@ -379,7 +387,7 @@
     #:
     #: Type:
     #:     list of str
-    visible_scmtools: Optional[List[str]] = None
+    visible_scmtools: ClassVar[Sequence[str] | None] = None
 
     ######################
     # Instance variables #
@@ -420,7 +428,7 @@
         """
         return False
 
-    def get_password(self) -> Optional[str]:
+    def get_password(self) -> str | None:
         """Return the raw password for this hosting service.
 
         Not all hosting services provide this, and not all would need it.
@@ -487,19 +495,24 @@
         """
         raise NotImplementedError
 
+    @deprecate_non_keyword_only_args(RemovedInReviewBoard90Warning)
     def authorize(
         self,
-        username: Optional[str],
-        password: Optional[str],
-        hosting_url: Optional[str],
-        credentials: HostingServiceCredentials,
-        two_factor_auth_code: Optional[str] = None,
-        local_site_name: Optional[str] = None,
-        *args,
+        *,
+        username: str | None,
+        password: str | None,
+        hosting_url: (str | None) = None,
+        credentials: (HostingServiceCredentials | None) = None,
+        two_factor_auth_code: (str | None) = None,
+        local_site_name: (str | None) = None,
         **kwargs,
     ) -> None:
         """Authorize an account for the hosting service.
 
+        Version Changed:
+            7.1:
+            Made arguments keyword-only.
+
         Args:
             username (str):
                 The username for the account.
@@ -507,10 +520,10 @@
             password (str):
                 The password for the account.
 
-            hosting_url (str):
+            hosting_url (str, optional):
                 The hosting URL for the service, if self-hosted.
 
-            credentials (dict):
+            credentials (dict, optional):
                 All credentials provided by the authentication form.
 
                 This will contain the username, password, and anything else
@@ -523,9 +536,6 @@
                 The Local Site name, if any, that the account should be
                 bound to.
 
-            *args (tuple):
-                Extra unused positional arguments.
-
             **kwargs (dict):
                 Extra keyword arguments containing values from the
                 repository's configuration.
@@ -542,17 +552,18 @@
         """
         raise NotImplementedError
 
+    @deprecate_non_keyword_only_args(RemovedInReviewBoard90Warning)
     def check_repository(
         self,
-        path: str,
-        username: Optional[str],
-        password: Optional[str],
-        scmtool_class: Type[SCMTool],
-        local_site_name: Optional[str],
-        *args,
+        *,
+        path: str | None,
+        username: str | None,
+        password: str | None,
+        scmtool_class: type[SCMTool],
+        local_site_name: str | None,
         **kwargs,
     ) -> None:
-        """Checks the validity of a repository configuration.
+        """Check the validity of a repository configuration.
 
         This performs a check against the hosting service or repository
         to ensure that the information provided by the user represents
@@ -563,6 +574,10 @@
         LocalSite's name (if any), and all field data from the
         HostingServiceForm as keyword arguments.
 
+        Version Changed:
+            7.1:
+            Made arguments keyword-only.
+
         Args:
             path (str):
                 The repository URL.
@@ -577,14 +592,10 @@
                 The subclass of :py:class:`~reviewboard.scmtools.core.SCMTool`
                 that should be used.
 
-            local_site_name (unicode):
+            local_site_name (str):
                 The name of the local site associated with the repository, or
                 ``None``.
 
-            *args (tuple):
-                Additional positional arguments, unique to each hosting
-                service.
-
             **kwargs (dict):
                 Additional keyword arguments, unique to each hosting service.
 
@@ -592,6 +603,7 @@
             reviewboard.hostingsvcs.errors.RepositoryError:
                 The repository is not valid.
         """
+        assert path is not None
         scmtool_class.check_repository(path, username, password,
                                        local_site_name)
 
@@ -704,8 +716,8 @@
     def get_commits(
         self,
         repository: Repository,
-        branch: Optional[str] = None,
-        start: Optional[str] = None,
+        branch: (str | None) = None,
+        start: (str | None) = None,
     ) -> Sequence[Commit]:
         """Return a list of commits backward in history from a given point.
 
@@ -770,13 +782,13 @@
 
     def get_remote_repositories(
         self,
-        owner: Optional[str] = None,
-        owner_type: Optional[str] = None,
-        filter_type: Optional[str] = None,
-        start: Optional[int] = None,
-        per_page: Optional[int] = None,
+        owner: (str | None) = None,
+        owner_type: (str | None) = None,
+        filter_type: (str | None) = None,
+        start: (int | None) = None,
+        per_page: (int | None) = None,
         **kwargs,
-    ) -> APIPaginator:
+    ) -> BasePaginator[RemoteRepository, Any]:
         """Return a list of remote repositories for the owner.
 
         This method should be implemented by subclasses.
@@ -800,8 +812,11 @@
             per_page (int, optional):
                 The number of results per page.
 
+            **kwargs (dict):
+                Additional keyword arguments.
+
         Returns:
-            reviewboard.hostingsvcs.utils.APIPaginator:
+            reviewboard.hostingsvcs.utils.BasePaginator:
             A paginator for the returned repositories.
         """
         raise NotImplementedError
@@ -871,10 +886,10 @@
     def get_repository_fields(
         cls,
         username: str,
-        hosting_url: Optional[str],
-        plan: str,
+        hosting_url: str | None,
+        plan: str | None,
         tool_name: str,
-        field_vars: Dict[str, str],
+        field_vars: dict[str, str],
     ) -> RepositoryFields:
         """Return the repository fields based on the given plan and tool.
 
@@ -972,7 +987,7 @@
     @classmethod
     def get_bug_tracker_requires_username(
         cls,
-        plan: Optional[str] = None,
+        plan: (str | None) = None,
     ) -> bool:
         """Return whether or not the bug tracker requires usernames.
 
@@ -1000,8 +1015,8 @@
     @classmethod
     def get_bug_tracker_field(
         cls,
-        plan: str,
-        field_vars: Dict[str, str],
+        plan: str | None,
+        field_vars: dict[str, str],
     ) -> str:
         """Return the bug tracker field for the given plan.
 
@@ -1016,7 +1031,7 @@
             str:
             The value of the bug tracker field.
 
-        Raises
+        Raises:
             KeyError:
                The provided plan is not valid for the hosting service.
         """
@@ -1044,9 +1059,9 @@
     @classmethod
     def get_field(
         cls,
-        plan: str,
+        plan: str | None,
         name: str,
-        default: Optional[str] = None,
+        default: (str | None) = None,
     ) -> Any:
         """Return the value of the field for the given plan.
 
@@ -1073,10 +1088,6 @@
 
             for plan_name, info in cls.plans:
                 if plan_name == plan and name in info:
-                    # NOTE: The type checker can complain here due to a
-                    #       non-Literal name being used to index into a
-                    #       TypedDict. However, we've already checked for
-                    #       presence above.
-                    return info[name]  # type: ignore
+                    return info[name]
 
         return getattr(cls, name, default)
diff --git a/reviewboard/hostingsvcs/base/http.py b/reviewboard/hostingsvcs/base/http.py
index 97db069c54b73ad1feca4411cf6de0bfc677a826..8a7fcb3326d47105eb8cc636b3471d8a80cf463f 100644
--- a/reviewboard/hostingsvcs/base/http.py
+++ b/reviewboard/hostingsvcs/base/http.py
@@ -13,7 +13,7 @@
 import logging
 import ssl
 from collections import OrderedDict
-from typing import Any, Dict, List, NoReturn, Optional, TYPE_CHECKING, Union
+from typing import Any, Dict, TypedDict, TYPE_CHECKING, Union
 from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
 from urllib.request import (
     Request as BaseURLRequest,
@@ -26,7 +26,6 @@
 from django.utils.encoding import force_str
 from djblets.log import log_timed
 from djblets.util.decorators import cached_property
-from typing_extensions import TypeAlias, TypedDict
 
 from reviewboard.deprecation import RemovedInReviewBoard80Warning
 
@@ -34,6 +33,7 @@
     from urllib.request import BaseHandler
 
     from typelets.json import JSONValue
+    from typing_extensions import Never, TypeAlias
 
     from reviewboard.hostingsvcs.base.hosting_service import BaseHostingService
 
@@ -52,21 +52,20 @@
     #:
     #: Type:
     #:      bytes
-    content: Union[bytes, str]
+    content: bytes | str
 
     #: The base name of the file.
     #:
     #: Type:
     #:      bytes
-    filename: Union[bytes, str]
+    filename: bytes | str
 
 
 #: Form field data to set when performing a POST or PUT.
 #:
 #: Version Added:
 #:     6.0
-FormFields: TypeAlias = Dict[Union[bytes, str],
-                             Union[bytes, str]]
+FormFields: TypeAlias = Dict[Union[bytes, str], Union[bytes, str]]
 
 
 #: Type for a mapping of HTTP headers for a request or response.
@@ -91,19 +90,27 @@
 
 
 def _log_and_raise(
+    value: Never,
     request: HostingServiceHTTPRequest,
     msg: str,
     **fmt_dict,
-) -> NoReturn:
+) -> Never:
     """Log and raise an exception with the given message.
 
     This is used when validating data going into the request, and is
     intended to help with debugging bad calls to the HTTP code.
 
+    Version Changed:
+        7.1:
+        Modified to take in the offending value.
+
     Version Added:
         4.0
 
     Args:
+        value (object):
+            The value which was not valid.
+
         request (HostingServiceHTTPRequest):
             The HTTP request.
 
@@ -117,7 +124,7 @@
         TypeError:
             The exception containing the provided message.
     """
-    msg = msg % dict({
+    msg %= dict({
         'method': request.method,
         'service': type(request.hosting_service),
     }, **fmt_dict)
@@ -150,7 +157,7 @@
     #:
     #: Type:
     #:     bytes
-    body: Optional[bytes]
+    body: bytes | None
 
     #: The headers to send in the request.
     #:
@@ -162,7 +169,7 @@
     #:
     #: Type:
     #:     HostingService
-    hosting_service: Optional[BaseHostingService]
+    hosting_service: BaseHostingService | None
 
     #: The HTTP method to perform.
     #:
@@ -174,7 +181,7 @@
     #:
     #: Type:
     #:     dict
-    query: Optional[QueryArgs]
+    query: QueryArgs | None
 
     #: The URL the request is being made to.
     #:
@@ -186,16 +193,16 @@
     #:
     #: Type:
     #:     list
-    _urlopen_handlers: List[BaseHandler]
+    _urlopen_handlers: list[BaseHandler]
 
     def __init__(
         self,
         url: str,
-        query: Optional[QueryArgs] = None,
-        body: Optional[bytes] = None,
-        headers: Optional[HTTPHeaders] = None,
+        query: (QueryArgs | None) = None,
+        body: (bytes | None) = None,
+        headers: (HTTPHeaders | None) = None,
         method: str = 'GET',
-        hosting_service: Optional[BaseHostingService] = None,
+        hosting_service: (BaseHostingService | None) = None,
         **kwargs,
     ) -> None:
         """Initialize the request.
@@ -236,6 +243,7 @@
 
         if body is not None and not isinstance(body, bytes):
             _log_and_raise(
+                body,
                 self,
                 'Received non-bytes body for the HTTP request for '
                 '%(service)r. This is likely an implementation problem. '
@@ -270,19 +278,19 @@
         self._urlopen_handlers = []
 
     @property
-    def data(self) -> Optional[bytes]:
+    def data(self) -> bytes | None:
         """The payload data for the request.
 
         Deprecated:
             4.0:
             This is deprecated in favor of the :py:attr:`body` attribute.
         """
+        class_name = type(self).__name__
+
         RemovedInReviewBoard80Warning.warn(
-            '%(class_name)s.data is deprecated in favor of '
-            '%(class_name)s.body. This will be removed in Review Board 8.'
-            % {
-                'class_name': type(self).__name__,
-            })
+            f'{class_name}.data is deprecated in favor of '
+            f'{class_name}.body. This will be removed in Review Board 8.'
+        )
 
         return self.body
 
@@ -300,24 +308,35 @@
             value (str):
                 The header value.
         """
-        if (not isinstance(name, str) or
-            not isinstance(value, str)):
-            _log_and_raise(
-                self,
-                'Received non-Unicode header %(header)r (value=%(value)r) '
-                'for the HTTP request for %(service)r. This is likely an '
-                'implementation problem. Please make sure only Unicode '
-                'strings are sent in request headers.',
-                header=name,
-                value=value)
+        if not isinstance(name, str):
+            _log_and_raise(
+                name,
+                self,
+                'Received non-Unicode header name %(header)r '
+                '(value=%(data)r) for the HTTP request for %(service)r. '
+                'This is likely an implementation problem. Please make sure '
+                'only Unicode strings are sent in request headers.',
+                header=name,
+                data=value)
+
+        if not isinstance(value, str):
+            _log_and_raise(
+                value,
+                self,
+                'Received non-Unicode header value for %(header)r '
+                '(value=%(data)r) for the HTTP request for %(service)r. This '
+                'is likely an implementation problem. Please make sure only '
+                'Unicode strings are sent in request headers.',
+                header=name,
+                data=value)
 
         self.headers[name.capitalize()] = value
 
     def get_header(
         self,
         name: str,
-        default: Optional[str] = None,
-    ) -> Optional[str]:
+        default: (str | None) = None,
+    ) -> str | None:
         """Return a header from the request.
 
         Args:
@@ -332,15 +351,15 @@
             The header value.
         """
         assert isinstance(name, str), (
-            '%s.get_header() requires a Unicode header name'
-            % type(self).__name__)
+            f'{type(self).__name__}.get_header() requires a Unicode header '
+            f'name')
 
         return self.headers.get(name.capitalize(), default)
 
     def add_basic_auth(
         self,
-        username: Union[bytes, str],
-        password: Union[bytes, str],
+        username: bytes | str,
+        password: bytes | str,
     ) -> None:
         """Add HTTP Basic Authentication headers to the request.
 
@@ -358,8 +377,10 @@
             password = password.encode('utf-8')
 
         auth = b'%s:%s' % (username, password)
+        encoded = force_str(base64.b64encode(auth))
+
         self.add_header(HTTPBasicAuthHandler.auth_header,
-                        'Basic %s' % force_str(base64.b64encode(auth)))
+                        f'Basic {encoded}')
 
     def add_digest_auth(
         self,
@@ -376,7 +397,7 @@
                 The password.
         """
         result = urlparse(self.url)
-        top_level_url = '%s://%s' % (result.scheme, result.netloc)
+        top_level_url = f'{result.scheme}://{result.netloc}'
 
         password_mgr = HTTPPasswordMgrWithDefaultRealm()
         password_mgr.add_password(None, top_level_url, username, password)
@@ -517,7 +538,7 @@
         self,
         request: HostingServiceHTTPRequest,
         url: str,
-        data: bytes,
+        data: bytes | None,
         headers: HTTPHeaders,
         status_code: int,
     ) -> None:
@@ -543,38 +564,53 @@
         """
         self.request = request
 
-        if data is not None and not isinstance(data, bytes):
+        if data is None:
+            data = b''
+        elif not isinstance(data, bytes):
             # HTTP response data will be in byte strings, unless something is
             # overridden. Users should never see this in production, but
             # it'll be confusing for development. Make sure developers see
             # this through both a log message and an exception.
             _log_and_raise(
+                data,
                 request,
                 'Received non-byte data from the HTTP %(method)s request '
                 'for %(service)r. This is likely an implementation '
                 'problem in a unit test or subclass. Please make sure '
                 'only byte strings are sent.')
 
-        if headers is None:
+        if not isinstance(headers, dict):
             _log_and_raise(
+                headers,
                 request,
-                'Headers response for HTTP %(method)s request for '
-                '%(service)r is None. This is likely an implementation '
-                'problem in a unit test. Please make sure a dictionary '
-                'is returned.')
+                'Headers response for HTTP %(method)s request for %(service)r '
+                'is not a dict. This is likely an implementation problem in a '
+                'unit test. Please make sure a dictionary is returned.')
 
         new_headers: HTTPHeaders = {}
 
         for key, value in headers.items():
-            if not isinstance(key, str) or not isinstance(value, str):
+            if not isinstance(key, str):
                 _log_and_raise(
+                    key,
                     request,
-                    'Received non-native string header %(header)r from the '
+                    'Received non string header %(header)r from the '
                     'HTTP %(method)s request for %(service)r. This is likely '
                     'an implementation problem in a unit test. Please '
-                    'make sure only native strings are sent.',
+                    'make sure only strings are sent.',
                     header=key)
 
+            if not isinstance(value, str):
+                _log_and_raise(
+                    value,
+                    request,
+                    'Received non string header value %(header)r '
+                    '(key=%(key)r) from the HTTP %(method)s request for '
+                    '%(service)r. This is likely an implementation problem in '
+                    'a unit test. Please make sure only strings are  sent.',
+                    key=key,
+                    header=value)
+
             new_headers[key.capitalize()] = value
 
         self.url = url
@@ -602,8 +638,8 @@
     def get_header(
         self,
         name: str,
-        default: Optional[str] = None,
-    ) -> Optional[str]:
+        default: (str | None) = None,
+    ) -> str | None:
         """Return the value of a header as a Unicode string.
 
         This accepts a header name with any form of capitalization. The header
@@ -652,13 +688,13 @@
             IndexError:
                 An index other than 0 or 1 was requested.
         """
+        class_name = type(self).__name__
+
         RemovedInReviewBoard80Warning.warn(
-            'Accessing %(class_name)s by index is deprecated. Please use '
-            '%(class_name)s.data or %(class_name)s.headers instead. This '
+            f'Accessing {class_name} by index is deprecated. Please use '
+            f'{class_name}.data or {class_name}.headers instead. This '
             'will be removed in Review Board 8.'
-            % {
-                'class_name': type(self).__name__,
-            })
+        )
 
         if i == 0:
             return self.data
diff --git a/reviewboard/hostingsvcs/base/paginator.py b/reviewboard/hostingsvcs/base/paginator.py
index ff8219106a59a228be95f3607447b58f338ec018..924e97dad7c93d256155b22c8bd2d0c7a1da8619 100644
--- a/reviewboard/hostingsvcs/base/paginator.py
+++ b/reviewboard/hostingsvcs/base/paginator.py
@@ -8,12 +8,14 @@
 
 from __future__ import annotations
 
-from typing import (Any, Callable, Generic, Iterator, Optional, Sequence,
-                    TYPE_CHECKING, TypeVar)
+from collections.abc import Callable, Sequence
+from typing import Any, Generic, TYPE_CHECKING, TypeVar, Union
 
 from typing_extensions import NotRequired, TypeAlias, TypedDict
 
 if TYPE_CHECKING:
+    from collections.abc import Iterator
+
     from typelets.funcs import KwargsDict
 
     from reviewboard.hostingsvcs.base import HostingServiceClient
@@ -28,8 +30,8 @@
 #:
 #: Version Added:
 #:     6.0
-_ProxyNormalizePageDataFunc: TypeAlias = Callable[[Optional[Any]],
-                                                  Optional[_PageDataT]]
+_ProxyNormalizePageDataFunc: TypeAlias = Callable[[Any],
+                                                  Union[_PageDataT, None]]
 
 
 class APIPaginatorPageData(TypedDict):
@@ -55,19 +57,19 @@
     #:
     #: Type:
     #:     str
-    next_url: NotRequired[Optional[str]]
+    next_url: NotRequired[str | None]
 
     #: The optional limit on the number of items fetched on each page.
     #:
     #: Type:
     #:     int
-    per_page: NotRequired[Optional[int]]
+    per_page: NotRequired[int | None]
 
     #: The optional URL to the previous page.
     #:
     #: Type:
     #:     str
-    prev_url: NotRequired[Optional[str]]
+    prev_url: NotRequired[str | None]
 
     #: The API response data.
     #:
@@ -79,7 +81,7 @@
     #:
     #: Type:
     #:     int
-    total_count: NotRequired[Optional[int]]
+    total_count: NotRequired[int | None]
 
 
 class InvalidPageError(Exception):
@@ -118,13 +120,13 @@
     #:
     #: Type:
     #:     object
-    page_data: Optional[_PageDataT]
+    page_data: _PageDataT | None
 
     #: The number of items to fetch per page.
     #:
     #: Type:
     #:     int
-    per_page: Optional[int]
+    per_page: int | None
 
     #: Keyword arguments to pass when making HTTP requests.
     #:
@@ -138,7 +140,7 @@
     #:
     #: Type:
     #:     int
-    start: Optional[int]
+    start: int | None
 
     #: The total number of results across all pages.
     #:
@@ -146,13 +148,13 @@
     #:
     #: Type:
     #:     int
-    total_count: Optional[int]
+    total_count: int | None
 
     def __init__(
         self,
-        start: Optional[int] = None,
-        per_page: Optional[int] = None,
-        request_kwargs: Optional[KwargsDict] = None,
+        start: (int | None) = None,
+        per_page: (int | None) = None,
+        request_kwargs: (KwargsDict | None) = None,
     ) -> None:
         """Initialize the paginator.
 
@@ -197,7 +199,7 @@
         """
         raise NotImplementedError
 
-    def prev(self) -> Optional[_PageDataT]:
+    def prev(self) -> _PageDataT | None:
         """Fetch the previous page, returning the page data.
 
         Subclasses must override this to provide the logic for fetching pages.
@@ -215,7 +217,7 @@
         """
         raise NotImplementedError
 
-    def next(self) -> Optional[_PageDataT]:
+    def next(self) -> _PageDataT | None:
         """Fetch the next page, returning the page data.
 
         Subclasses must override this to provide the logic for fetching pages.
@@ -235,7 +237,7 @@
 
     def iter_items(
         self,
-        max_pages: Optional[int] = None,
+        max_pages: (int | None) = None,
     ) -> Iterator[_PageDataItemT]:
         """Iterate through all items across pages.
 
@@ -256,18 +258,18 @@
         for page in self.iter_pages(max_pages=max_pages):
             if page:
                 assert isinstance(page, Sequence), (
-                    "page_data is not a sequence (it's a %s). This is "
-                    "either an unexpected result, or %s.iter_items() needs "
-                    "to be overridden."
-                    % (type(page), type(self).__name__)
+                    f"page_data is not a sequence (it's a {type(page)}). This "
+                    f"is either an unexpected result, or "
+                    f"{type(self).__name__}.iter_items() needs to be "
+                    "overridden."
                 )
 
                 yield from page
 
     def iter_pages(
         self,
-        max_pages: Optional[int] = None,
-    ) -> Iterator[Optional[_PageDataT]]:
+        max_pages: (int | None) = None,
+    ) -> Iterator[_PageDataT | None]:
         """Iterate through pages of results.
 
         This will repeatedly fetch pages, providing each parsed page payload
@@ -298,7 +300,7 @@
         except InvalidPageError:
             pass
 
-    def __iter__(self) -> Iterator[Optional[_PageDataT]]:
+    def __iter__(self) -> Iterator[_PageDataT | None]:
         """Iterate through pages of results.
 
         This is a simple wrapper for :py:meth:`iter_pages`.
@@ -342,7 +344,7 @@
     #:
     #: Type:
     #:     str
-    start_query_param: Optional[str] = None
+    start_query_param: (str | None) = None
 
     #: Query parameter name for the requested number of results per page.
     #:
@@ -351,7 +353,7 @@
     #:
     #: Type:
     #:     str
-    per_page_query_param: Optional[str] = None
+    per_page_query_param: (str | None) = None
 
     ######################
     # Instance variables #
@@ -367,25 +369,25 @@
     #:
     #: Type:
     #:     str
-    next_url: Optional[str]
+    next_url: str | None
 
     #: HTTP headers returned for the current page.
     #:
     #: Type:
     #:     dict
-    page_headers: Optional[HTTPHeaders]
+    page_headers: HTTPHeaders | None
 
     #: The URL for the previous set of results in the page.
     #:
     #: Type:
     #:     str
-    prev_url: Optional[str]
+    prev_url: str | None
 
     #: The URL used to fetch the current page of data.
     #:
     #: Type:
     #:     str
-    url: Optional[str]
+    url: str | None
 
     def __init__(
         self,
@@ -457,7 +459,7 @@
         """
         return self.next_url is not None
 
-    def prev(self) -> Optional[_PageDataT]:
+    def prev(self) -> _PageDataT | None:
         """Fetch the previous page, returning the page data.
 
         Returns:
@@ -477,7 +479,7 @@
         self.url = self.prev_url
         return self._fetch_page()
 
-    def next(self) -> Optional[_PageDataT]:
+    def next(self) -> _PageDataT | None:
         """Fetch the next page, returning the page data.
 
         Returns:
@@ -517,7 +519,7 @@
         """
         raise NotImplementedError
 
-    def _fetch_page(self) -> Optional[_PageDataT]:
+    def _fetch_page(self) -> _PageDataT | None:
         """Fetch a page and extracts the information from it.
 
         Returns:
@@ -538,36 +540,34 @@
         self.total_count = page_info.get('total_count')
 
         # Make sure the implementation sent the correct data to us.
-        assert self.prev_url is None or isinstance(self.prev_url, str), \
-            ('"prev_url" result from fetch_url() must be None or Unicode '
-             'string, not %r'
-             % type(self.prev_url))
+        assert self.prev_url is None or isinstance(self.prev_url, str), (
+            f'"prev_url" result from fetch_url() must be None or Unicode '
+            f'string, not {type(self.prev_url)!r}'
+        )
 
-        assert self.next_url is None or isinstance(self.next_url, str), \
-            ('"next_url" result from fetch_url() must be None or Unicode '
-             'string, not %r'
-             % type(self.next_url))
+        assert self.next_url is None or isinstance(self.next_url, str), (
+            f'"next_url" result from fetch_url() must be None or Unicode '
+            f'string, not {type(self.next_url)!r}'
+        )
 
-        assert self.total_count is None or isinstance(self.total_count, int), \
-            ('"total_count" result from fetch_url() must be None or int, not '
-             '%r'
-             % type(self.total_count))
+        assert self.total_count is None or isinstance(self.total_count, int), (
+            f'"total_count" result from fetch_url() must be None or int, not '
+            f'{type(self.total_count)!r}'
+        )
 
-        assert self.per_page is None or isinstance(self.per_page, int), \
-            ('"per_page" result from fetch_url() must be an int, not %r'
-             % type(self.per_page))
+        assert self.per_page is None or isinstance(self.per_page, int), (
+            f'"per_page" result from fetch_url() must be an int, not '
+            f'{type(self.per_page)!r}'
+        )
 
-        assert isinstance(self.page_headers, dict), \
-            ('"page_headers" result from fetch_url() must be a dictionary, '
-             'not %r'
-             % type(self.page_headers))
+        assert isinstance(self.page_headers, dict), (
+            f'"page_headers" result from fetch_url() must be a dictionary, '
+            f'not {type(self.page_headers)!r}'
+        )
 
         return self.page_data
 
 
-_ProxiedPaginatorT = TypeVar('_ProxiedPaginatorT', bound=BasePaginator)
-
-
 class ProxyPaginator(BasePaginator[_PageDataItemT, _PageDataT]):
     """A paginator that proxies to another paginator, transforming data.
 
@@ -605,12 +605,16 @@
     #:
     #: Type:
     #:     callable
-    normalize_page_data_func: Optional[_ProxyNormalizePageDataFunc]
+    normalize_page_data_func: (
+        _ProxyNormalizePageDataFunc[_PageDataItemT] | None
+    )
 
     def __init__(
         self,
         paginator: BasePaginator[Any, Any],
-        normalize_page_data_func: Optional[_ProxyNormalizePageDataFunc] = None,
+        normalize_page_data_func: (
+            _ProxyNormalizePageDataFunc[_PageDataItemT] | None
+        ) = None,
     ) -> None:
         """Initialize the paginator.
 
@@ -647,7 +651,7 @@
         return self.paginator.has_next
 
     @property
-    def per_page(self) -> Optional[int]:  # type: ignore
+    def per_page(self) -> int | None:
         """The number of items requested per page.
 
         Type:
@@ -656,7 +660,7 @@
         return self.paginator.per_page
 
     @property
-    def total_count(self) -> Optional[int]:  # type: ignore
+    def total_count(self) -> int | None:
         """The number of items across all pages, if known.
 
         Type:
@@ -664,7 +668,7 @@
         """
         return self.paginator.total_count
 
-    def prev(self) -> Optional[_PageDataT]:
+    def prev(self) -> _PageDataT | None:
         """Fetch the previous page, returning the page data.
 
         Returns:
@@ -680,7 +684,7 @@
         """
         return self._process_page(self.paginator.prev())
 
-    def next(self) -> Optional[_PageDataT]:
+    def next(self) -> _PageDataT | None:
         """Fetch the next page, returning the page data.
 
         Returns:
@@ -698,8 +702,8 @@
 
     def normalize_page_data(
         self,
-        data: Optional[Any],
-    ) -> Optional[_PageDataT]:
+        data: Any,
+    ) -> _PageDataT | None:
         """Normalize a page of data.
 
         If :py:attr:`normalize_page_data_func` was passed on construction, this
@@ -724,8 +728,8 @@
 
     def _process_page(
         self,
-        page_data: Optional[Any],
-    ) -> Optional[_PageDataT]:
+        page_data: Any,
+    ) -> _PageDataT | None:
         """Process a page of data.
 
         This will normalize the page data, store it, and return it.
diff --git a/reviewboard/hostingsvcs/base/registry.py b/reviewboard/hostingsvcs/base/registry.py
index 227237f222c7d69bb3033df52b8c34cebdc78717..f4e38b55123607549db30cac30b429cedcac14e8 100644
--- a/reviewboard/hostingsvcs/base/registry.py
+++ b/reviewboard/hostingsvcs/base/registry.py
@@ -12,7 +12,7 @@
 import logging
 import re
 from importlib import import_module
-from typing import Dict, Iterator, Optional, Sequence, TYPE_CHECKING, Type
+from typing import TYPE_CHECKING
 
 from django.urls import include, re_path
 from django.utils.translation import gettext_lazy as _
@@ -25,13 +25,16 @@
 from reviewboard.registries.registry import EntryPointRegistry
 
 if TYPE_CHECKING:
+    from collections.abc import Iterator, Sequence
+    from importlib.metadata import EntryPoint
+
     from django.urls import _AnyURL
 
 
 logger = logging.getLogger(__name__)
 
 
-class HostingServiceRegistry(EntryPointRegistry[Type[BaseHostingService]]):
+class HostingServiceRegistry(EntryPointRegistry[type[BaseHostingService]]):
     """A registry for managing hosting services.
 
     Version Changed:
@@ -64,15 +67,15 @@
     #:
     #: Type:
     #:     dict
-    _url_patterns: Dict[str, Sequence[_AnyURL]]
+    _url_patterns: dict[str, Sequence[_AnyURL]]
 
-    def __init__(self):
+    def __init__(self) -> None:
         """Initialize the registry."""
         super().__init__()
 
         self._url_patterns = {}
 
-    def get_defaults(self) -> Iterator[Type[BaseHostingService]]:
+    def get_defaults(self) -> Iterator[type[BaseHostingService]]:
         """Yield the built-in hosting services.
 
         This will make sure the standard hosting services are always present in
@@ -106,7 +109,8 @@
                 ('unfuddle', 'Unfuddle'),
                 ('versionone', 'VersionOne'),
             ):
-            mod = import_module('reviewboard.hostingsvcs.%s' % _module)
+            mod = import_module(f'reviewboard.hostingsvcs.{_module}')
+
             yield getattr(mod, _service_cls_name)
 
         yield from super().get_defaults()
@@ -114,7 +118,7 @@
     def get_hosting_service(
         self,
         hosting_service_id: str,
-    ) -> Optional[Type[BaseHostingService]]:
+    ) -> type[BaseHostingService] | None:
         """Return a hosting service with the given ID.
 
         Args:
@@ -132,7 +136,7 @@
 
     def unregister(
         self,
-        service: Type[BaseHostingService],
+        service: type[BaseHostingService],
     ) -> None:
         """Unregister a hosting service.
 
@@ -164,10 +168,8 @@
         defined.
 
         Args:
-            service (type):
-                The
-                :py:class:`~reviewboard.hostingsvcs.base.hosting_service.
-                BaseHostingService` subclass.
+            hosting_service_id (str):
+                The ID of the hosting service to unregister.
         """
         try:
             self.unregister_by_attr('hosting_service_id', hosting_service_id)
@@ -179,8 +181,8 @@
 
     def process_value_from_entry_point(
         self,
-        entry_point,
-    ) -> Type[BaseHostingService]:
+        entry_point: EntryPoint,
+    ) -> type[BaseHostingService]:
         """Load the class from the entry point.
 
         The ``hosting_service_id`` attribute will be set on the class from the
@@ -203,7 +205,7 @@
 
     def register(
         self,
-        service: Type[BaseHostingService],
+        service: type[BaseHostingService],
     ) -> None:
         """Register a hosting service.
 
@@ -220,9 +222,10 @@
         if service.repository_url_patterns:
             assert service.hosting_service_id
 
+            escaped_id = re.escape(service.hosting_service_id)
+
             cls_urlpatterns = [
-                re_path(r'^(?P<hosting_service_id>%s)/'
-                        % re.escape(service.hosting_service_id),
+                re_path(rf'^(?P<hosting_service_id>{escaped_id})/',
                         include(service.repository_url_patterns)),
             ]
 
diff --git a/reviewboard/hostingsvcs/base/repository.py b/reviewboard/hostingsvcs/base/repository.py
index 79b2516805243bee048b2c15ec0515ce511a8b53..917462c74b226df9ec8793834931b3ed20bd2baa 100644
--- a/reviewboard/hostingsvcs/base/repository.py
+++ b/reviewboard/hostingsvcs/base/repository.py
@@ -7,7 +7,7 @@
 
 from __future__ import annotations
 
-from typing import Optional, TYPE_CHECKING
+from typing import TYPE_CHECKING
 
 if TYPE_CHECKING:
     from typelets.json import JSONDict
@@ -61,7 +61,7 @@
     #:
     #: Type:
     #:     str
-    mirror_path: Optional[str]
+    mirror_path: str | None
 
     #: The displayed name of the repository.
     #:
@@ -95,8 +95,8 @@
         owner: str,
         scm_type: str,
         path: str,
-        mirror_path: Optional[str] = None,
-        extra_data: JSONDict = {},
+        mirror_path: (str | None) = None,
+        extra_data: (JSONDict | None) = None,
     ) -> None:
         """Initialize the remote repository representation.
 
@@ -134,7 +134,7 @@
         self.scm_type = scm_type
         self.path = path
         self.mirror_path = mirror_path
-        self.extra_data = extra_data
+        self.extra_data = extra_data or {}
 
     def __repr__(self) -> str:
         """Return a representation of the remote repository information.
@@ -143,5 +143,7 @@
             str:
             The string representation.
         """
-        return ('<RemoteRepository: "%s" (owner=%s, scm_type=%s)>'
-                % (self.name, self.owner, self.scm_type))
+        return (
+            f'<RemoteRepository: "{self.name}" (owner={self.owner}, '
+            f'scm_type={self.scm_type})>'
+        )
diff --git a/reviewboard/hostingsvcs/beanstalk.py b/reviewboard/hostingsvcs/beanstalk.py
index fe813396c5def66d7b88ca2327f4c75b6d17b616..ffe8b09706070e1f60e7fed42644c234907127f9 100644
--- a/reviewboard/hostingsvcs/beanstalk.py
+++ b/reviewboard/hostingsvcs/beanstalk.py
@@ -1,3 +1,7 @@
+"""Hosting service for Beanstalk."""
+
+from __future__ import annotations
+
 import json
 import logging
 import os
@@ -10,8 +14,10 @@
 from django.urls import path
 from django.utils.translation import gettext_lazy as _
 from django.views.decorators.http import require_POST
+from housekeeping import deprecate_non_keyword_only_args
 
 from reviewboard.admin.server import get_server_url
+from reviewboard.deprecation import RemovedInReviewBoard90Warning
 from reviewboard.hostingsvcs.base.forms import (
     BaseHostingServiceAuthForm,
     BaseHostingServiceRepositoryForm,
@@ -252,22 +258,63 @@
              BeanstalkHookViews.process_post_receive_hook),
     ]
 
-    def check_repository(self, beanstalk_account_domain=None,
-                         beanstalk_repo_name=None, *args, **kwargs):
-        """Checks the validity of a repository.
+    @deprecate_non_keyword_only_args(RemovedInReviewBoard90Warning)
+    def check_repository(
+        self,
+        *,
+        beanstalk_account_domain: str,
+        beanstalk_repo_name: str,
+        **kwargs,
+    ) -> None:
+        """Check the validity of a repository.
 
         This will perform an API request against Beanstalk to get
         information on the repository. This will throw an exception if
         the repository was not found, and return cleanly if it was found.
+
+        Version Changed:
+            7.1:
+            Made arguments keyword-only.
+
+        Args:
+            beanstalk_account_domain (str):
+                The domain for the Beanstalk account.
+
+            beanstalk_repo_name (str):
+                The name of the repository.
+
+            **kwargs (dict, unused):
+                Additional information passed by the repository form.
         """
         self._api_get_repository(beanstalk_account_domain, beanstalk_repo_name)
 
-    def authorize(self, username, password, hosting_url,
-                  local_site_name=None, *args, **kwargs):
-        """Authorizes the Beanstalk repository.
+    @deprecate_non_keyword_only_args(RemovedInReviewBoard90Warning)
+    def authorize(
+        self,
+        *,
+        username: str | None,
+        password: str | None,
+        **kwargs,
+    ) -> None:
+        """Authorize the Beanstalk repository.
 
         Beanstalk uses HTTP Basic Auth for the API, so this will store the
         provided password, encrypted, for use in later API requests.
+
+        Version Changed:
+            7.1:
+            Made arguments keyword-only.
+
+        Args:
+            username (str):
+                The username for the account.
+
+            password (str):
+                The password for the account.
+
+            **kwargs (dict, unused):
+                Extra keyword arguments containing values from the
+                repository's configuration.
         """
         self.account.data['password'] = encrypt_password(password)
         self.account.save()
diff --git a/reviewboard/hostingsvcs/bitbucket.py b/reviewboard/hostingsvcs/bitbucket.py
index cb0a984b5000bad74b5d8d220f3a8d702eb2976a..f0ed0a602df5e60a24ed9dd24d813880db124993 100644
--- a/reviewboard/hostingsvcs/bitbucket.py
+++ b/reviewboard/hostingsvcs/bitbucket.py
@@ -1,3 +1,7 @@
+"""Hosting service for Bitbucket."""
+
+from __future__ import annotations
+
 import json
 import logging
 from collections import defaultdict
@@ -13,8 +17,10 @@
 from django.urls import path
 from django.utils.translation import gettext_lazy as _, gettext
 from django.views.decorators.http import require_POST
+from housekeeping import deprecate_non_keyword_only_args
 
 from reviewboard.admin.server import build_server_url, get_server_url
+from reviewboard.deprecation import RemovedInReviewBoard90Warning
 from reviewboard.hostingsvcs.base.forms import (
     BaseHostingServiceAuthForm,
     BaseHostingServiceRepositoryForm,
@@ -936,8 +942,14 @@
 
     DEFAULT_PLAN = 'personal'
 
-    def check_repository(self, plan=DEFAULT_PLAN, tool_name=None,
-                         *args, **kwargs):
+    @deprecate_non_keyword_only_args(RemovedInReviewBoard90Warning)
+    def check_repository(
+        self,
+        *,
+        plan: str = DEFAULT_PLAN,
+        tool_name: (str | None) = None,
+        **kwargs,
+    ) -> None:
         """Check the validity of a repository configuration.
 
         This will ensure that the configuration data being provided by the
@@ -949,17 +961,18 @@
         the repository was not found, or does not match the expected
         repository type, and return cleanly if it was found.
 
+        Version Changed:
+            7.1:
+            Made arguments keyword-only.
+
         Args:
-            plan (unicode, optional):
+            plan (str, optional):
                 The configured repository plan.
 
-            tool_name (unicode, optional):
+            tool_name (str, optional):
                 The name of the tool selected to communicate with the
                 repository.
 
-            *args (tuple, unused):
-                Unused positional arguments.
-
             **kwargs (dict, unused):
                 Additional information passed by the repository form.
 
@@ -1001,7 +1014,14 @@
                 gettext('The Bitbucket repository being configured does not '
                         'match the type of repository you have selected.'))
 
-    def authorize(self, username, password, *args, **kwargs):
+    @deprecate_non_keyword_only_args(RemovedInReviewBoard90Warning)
+    def authorize(
+        self,
+        *,
+        username: str | None,
+        password: str | None,
+        **kwargs,
+    ) -> None:
         """Authorize an account on Bitbucket.
 
         This will attempt to access the user session resource using the
@@ -1014,16 +1034,17 @@
 
         If successful, the password is stored in an encrypted form.
 
+        Version Changed:
+            7.1:
+            Made arguments keyword-only.
+
         Args:
-            username (unicode):
+            username (str):
                 The username for the account.
 
-            password (unicode):
+            password (str):
                 The user's password or app password.
 
-            *args (tuple, unused):
-                Unused positional arguments.
-
             **kwargs (dict, unused):
                 Unused keyword arguments.
 
diff --git a/reviewboard/hostingsvcs/codebasehq.py b/reviewboard/hostingsvcs/codebasehq.py
index 4e38085a3e0f632b839aa2d7b154b9dbbe578003..7b3e5fe3b062822ab340961f9e5f088b053e5c72 100644
--- a/reviewboard/hostingsvcs/codebasehq.py
+++ b/reviewboard/hostingsvcs/codebasehq.py
@@ -1,10 +1,17 @@
+"""Hosting service for CodebaseHQ."""
+
+from __future__ import annotations
+
 import logging
+from typing import TYPE_CHECKING
 from urllib.error import HTTPError, URLError
 from xml.dom.minidom import parseString
 
 from django import forms
 from django.utils.translation import gettext_lazy as _, gettext
+from housekeeping import deprecate_non_keyword_only_args
 
+from reviewboard.deprecation import RemovedInReviewBoard90Warning
 from reviewboard.hostingsvcs.base.client import HostingServiceClient
 from reviewboard.hostingsvcs.base.forms import (
     BaseHostingServiceAuthForm,
@@ -18,6 +25,10 @@
                                                encrypt_password)
 from reviewboard.scmtools.errors import FileNotFoundError
 
+if TYPE_CHECKING:
+    from reviewboard.hostingsvcs.base.hosting_service import \
+        HostingServiceCredentials
+
 
 logger = logging.getLogger(__name__)
 
@@ -388,7 +399,15 @@
 
         self.client = CodebaseHQClient(self)
 
-    def authorize(self, username, password, credentials, *args, **kwargs):
+    @deprecate_non_keyword_only_args(RemovedInReviewBoard90Warning)
+    def authorize(
+        self,
+        *,
+        username: str | None,
+        password: str | None,
+        credentials: HostingServiceCredentials,
+        **kwargs,
+    ) -> None:
         """Authorize an account for Codebase.
 
         Codebase uses HTTP Basic Auth with an API username (consisting of the
@@ -396,20 +415,21 @@
         the password) for API calls, and a standard username/password for
         Subversion repository access. We need to store all of this.
 
+        Version Changed:
+            7.1:
+            Made arguments keyword-only.
+
         Args:
-            username (unicode):
+            username (str):
                 The username to authorize.
 
-            password (unicode):
+            password (str):
                 The API token used as a password.
 
             credentials (dict):
                 Additional credentials from the authentication form.
 
-            *args (tuple):
-                Extra unused positional arguments.
-
-            **kwargs (dict):
+            **kwargs (dict, unused):
                 Extra unused keyword arguments.
 
         Raises:
@@ -466,30 +486,37 @@
         """
         return decrypt_password(self.account.data['password'])
 
-    def check_repository(self, codebasehq_project_name=None,
-                         codebasehq_repo_name=None, tool_name=None,
-                         *args, **kwargs):
+    @deprecate_non_keyword_only_args(RemovedInReviewBoard90Warning)
+    def check_repository(
+        self,
+        *,
+        codebasehq_project_name: (str | None) = None,
+        codebasehq_repo_name: (str | None) = None,
+        tool_name: (str | None) = None,
+        **kwargs,
+    ) -> None:
         """Check the validity of a repository.
 
         This will perform an API request against Codebase to get information on
         the repository. This will throw an exception if the repository was not
         found, and return cleanly if it was found.
 
+        Version Changed:
+            7.1:
+            Made arguments keyword-only.
+
         Args:
-            codebase_project_name (unicode):
+            codebasehq_project_name (str):
                 The name of the project on Codebase.
 
-            codebasehq_repo_name (unicode):
+            codebasehq_repo_name (str):
                 The name of the repository on Codebase.
 
-            tool_name (unicode):
+            tool_name (str):
                 The name of the SCMTool for the repository.
 
-            *args (tuple):
-                Extra unused positional arguments passed to this function.
-
-            **kwargs (dict):
-                Extra unused keyword arguments passed to this function.
+            **kwargs (dict, unused):
+                Additional keyword arguments passed by the repository form.
 
         Raises:
             reviewboard.hostingsvcs.errors.RepositoryError:
diff --git a/reviewboard/hostingsvcs/errors.py b/reviewboard/hostingsvcs/errors.py
index 12dcc0204e857a0e75e39c404cd0825c0ffb083a..943a7b5714225db3287d41ae9e8af2198ad9a6e9 100644
--- a/reviewboard/hostingsvcs/errors.py
+++ b/reviewboard/hostingsvcs/errors.py
@@ -79,16 +79,16 @@
     ######################
 
     #: The HTTP status code for the error, if available.
-    http_code: Optional[int]
+    http_code: int | None
 
     #: The parsed payload for the error, if available.
-    rsp: Optional[Dict]
+    rsp: object | None
 
     def __init__(
         self,
-        msg: Optional[str] = None,
-        http_code: Optional[int] = None,
-        rsp: Optional[Dict] = None,
+        msg: (str | None) = None,
+        http_code: (int | None) = None,
+        rsp: (object | None) = None,
     ) -> None:
         """Initialize the error.
 
diff --git a/reviewboard/hostingsvcs/gerrit.py b/reviewboard/hostingsvcs/gerrit.py
index cd10a524b0a8fa48f8234aee2b641d1474be9b60..da64296f853676e49bcaeb215576ce734f9d3c0f 100644
--- a/reviewboard/hostingsvcs/gerrit.py
+++ b/reviewboard/hostingsvcs/gerrit.py
@@ -1,15 +1,20 @@
 """Gerrit source code hosting support."""
 
+from __future__ import annotations
+
 import base64
 import json
 import logging
+from typing import TYPE_CHECKING
 from urllib.error import HTTPError, URLError
 from urllib.parse import quote_plus, urljoin, urlparse
 
 from django import forms
 from django.utils.translation import gettext, gettext_lazy as _
 from djblets.util.decorators import cached_property
+from housekeeping import deprecate_non_keyword_only_args
 
+from reviewboard.deprecation import RemovedInReviewBoard90Warning
 from reviewboard.hostingsvcs.base.client import HostingServiceClient
 from reviewboard.hostingsvcs.base.forms import (
     BaseHostingServiceAuthForm,
@@ -26,6 +31,10 @@
                                                encrypt_password)
 from reviewboard.scmtools.errors import FileNotFoundError
 
+if TYPE_CHECKING:
+    from reviewboard.hostingsvcs.base.hosting_service import \
+        HostingServiceCredentials
+
 
 logger = logging.getLogger(__name__)
 
@@ -288,26 +297,33 @@
         },
     }
 
-    def check_repository(self, gerrit_url=None, gerrit_project_name=None,
-                         *args, **kwargs):
+    @deprecate_non_keyword_only_args(RemovedInReviewBoard90Warning)
+    def check_repository(
+        self,
+        *,
+        gerrit_url: str,
+        gerrit_project_name: str,
+        **kwargs,
+    ) -> None:
         """Check that the repository is configured correctly.
 
         This method ensures that the user has access to an existing repository
         on the Gerrit server and that the ``gerrit-reviewboard`` plugin is
         installed and of a compatible version.
 
+        Version Changed:
+            7.1:
+            Made arguments keyword-only.
+
         Args:
-            gerrit_url (unicode):
+            gerrit_url (str):
                 The URL to the Gerrit server.
 
-            gerrit_project_name (unicode):
+            gerrit_project_name (str):
                 The repository's configured project name on Gerrit.
 
-            *args (tuple):
-                Ignored positional arguments.
-
-            **kwargs (dict):
-                Ignored keyword arguments.
+            **kwargs (dict, unused):
+                Additional keyword arguments passed by the repository form.
 
         Raises:
             reviewboard.hostingsvcs.errors.RepositoryError:
@@ -386,31 +402,41 @@
                     }
                 )
 
-    def authorize(self, username, password, credentials,
-                  local_site_name=None, gerrit_url=None, *args, **kwargs):
+    @deprecate_non_keyword_only_args(RemovedInReviewBoard90Warning)
+    def authorize(
+        self,
+        *,
+        username: str | None,
+        password: str | None,
+        credentials: HostingServiceCredentials,
+        local_site_name: (str | None) = None,
+        gerrit_url: str,
+        **kwargs,
+    ) -> None:
         """Authorize against the Gerrit server.
 
+        Version Changed:
+            7.1:
+            Made arguments keyword-only.
+
         Args:
-            username (unicode):
+            username (str):
                 The username to use for authentication.
 
-            password  unicode):
+            password (str):
                 The password to use for authentication.
 
             credentials (dict):
                 The credentials from the authentication form.
 
-            local_site_name (unicode, optional):
+            local_site_name (str, optional):
                 The name of the :py:class:`~reviewboard.site.models.LocalSite`
                 the repository is associated with.
 
-            gerrit_url (unicode):
+            gerrit_url (str):
                 The URL of the Gerrit server.
 
-            *args (tuple):
-                Ignored positional arguments.
-
-            **kwargs (dict):
+            **kwargs (dict, unused):
                 Ignored keyword arguments.
 
         Raises:
diff --git a/reviewboard/hostingsvcs/github.py b/reviewboard/hostingsvcs/github.py
index 11071143b2f34ea4e01f40e59aede350f1d651d4..204763e207f609780a5474dfd3266d65103531fc 100644
--- a/reviewboard/hostingsvcs/github.py
+++ b/reviewboard/hostingsvcs/github.py
@@ -19,8 +19,10 @@
 from django.urls import path
 from django.utils.translation import gettext, gettext_lazy as _
 from django.views.decorators.http import require_POST
+from housekeeping import deprecate_non_keyword_only_args
 
 from reviewboard.admin.server import build_server_url, get_server_url
+from reviewboard.deprecation import RemovedInReviewBoard90Warning
 from reviewboard.hostingsvcs.base.bug_tracker import BaseBugTracker
 from reviewboard.hostingsvcs.base.client import HostingServiceClient
 from reviewboard.hostingsvcs.base.hosting_service import BaseHostingService
@@ -724,12 +726,33 @@
                             name)
         return plan_data[key]
 
-    def check_repository(self, plan=None, *args, **kwargs):
-        """Checks the validity of a repository.
+    @deprecate_non_keyword_only_args(RemovedInReviewBoard90Warning)
+    def check_repository(
+        self,
+        *,
+        plan: str,
+        **kwargs,
+    ) -> None:
+        """Check the validity of a repository.
 
         This will perform an API request against GitHub to get
         information on the repository. This will throw an exception if
         the repository was not found, and return cleanly if it was found.
+
+        Version Changed:
+            7.1:
+            Made arguments keyword-only.
+
+        Args:
+            plan (str):
+                The ID of the plan that the repository is on.
+
+            **kwargs (dict, unused):
+                Additional keyword arguments passed by the repository form.
+
+        Raises:
+            reviewboard.hostingsvcs.errors.RepositoryError:
+                The repository is not valid.
         """
         try:
             rsp = self.client.http_get(
@@ -739,7 +762,7 @@
             repo_info = rsp.json
         except HostingServiceError as e:
             if e.http_code == 404:
-                if plan in ('public', 'private'):
+                if plan in {'public', 'private'}:
                     raise RepositoryError(
                         gettext('A repository with this name was not found, '
                                 'or your user may not own it.'))
@@ -761,37 +784,46 @@
         if 'private' in repo_info:
             is_private = repo_info['private']
 
-            if is_private and plan in ('public', 'public-org'):
+            if is_private and plan in {'public', 'public-org'}:
                 raise RepositoryError(
                     gettext('This is a private repository, but you have '
                             'selected a public plan.'))
-            elif not is_private and plan in ('private', 'private-org'):
+            elif not is_private and plan in {'private', 'private-org'}:
                 raise RepositoryError(
                     gettext('This is a public repository, but you have '
                             'selected a private plan.'))
 
-    def authorize(self, username, password, hosting_url=None,
-                  local_site_name=None, *args, **kwargs):
+    @deprecate_non_keyword_only_args(RemovedInReviewBoard90Warning)
+    def authorize(
+        self,
+        *,
+        username: str | None,
+        password: str | None,
+        hosting_url: (str | None) = None,
+        local_site_name: (str | None) = None,
+        **kwargs,
+    ) -> None:
         """Authorize an account for the hosting service.
 
+        Version Changed:
+            7.1:
+            Made arguments keyword-only.
+
         Args:
-            username (unicode):
+            username (str):
                 The username for the account.
 
-            password (unicode):
+            password (str):
                 The Personal Access Token for the account.
 
-            hosting_url (unicode):
+            hosting_url (str, optional):
                 The hosting URL for the service, if self-hosted.
 
-            local_site_name (unicode, optional):
+            local_site_name (str, optional):
                 The Local Site name, if any, that the account should be
                 bound to.
 
-            *args (tuple):
-                Extra unused positional arguments.
-
-            **kwargs (dict):
+            **kwargs (dict, unused):
                 Extra keyword arguments containing values from the
                 repository's configuration.
 
diff --git a/reviewboard/hostingsvcs/gitlab.py b/reviewboard/hostingsvcs/gitlab.py
index 31dffbfbc0055cad9db8cb8c85cce4ffe27135e6..642909a4f5067a7ab00d21ae423300a90f295d76 100644
--- a/reviewboard/hostingsvcs/gitlab.py
+++ b/reviewboard/hostingsvcs/gitlab.py
@@ -1,5 +1,10 @@
+"""Hosting service for GitLab."""
+
+from __future__ import annotations
+
 import logging
 import re
+from typing import TYPE_CHECKING
 from urllib.error import HTTPError, URLError
 from urllib.parse import quote, quote_plus, urlparse
 
@@ -10,8 +15,10 @@
 from django.utils.encoding import force_str
 from django.utils.translation import gettext, gettext_lazy as _
 from djblets.cache.backend import cache_memoize
+from housekeeping import deprecate_non_keyword_only_args
 
 from reviewboard.admin.support import get_kb_url
+from reviewboard.deprecation import RemovedInReviewBoard90Warning
 from reviewboard.hostingsvcs.base.forms import (
     BaseHostingServiceAuthForm,
     BaseHostingServiceRepositoryForm,
@@ -26,6 +33,10 @@
 from reviewboard.scmtools.errors import FileNotFoundError
 from reviewboard.scmtools.core import Branch, Commit
 
+if TYPE_CHECKING:
+    from reviewboard.hostingsvcs.base.hosting_service import \
+        HostingServiceCredentials
+
 
 logger = logging.getLogger(__name__)
 
@@ -296,37 +307,67 @@
         }),
     ]
 
-    def check_repository(self, plan=None, *args, **kwargs):
-        """Checks the validity of a repository.
+    @deprecate_non_keyword_only_args(RemovedInReviewBoard90Warning)
+    def check_repository(
+        self,
+        *,
+        plan: str,
+        **kwargs,
+    ) -> None:
+        """Check the validity of a repository.
 
         This will perform an API request against GitLab to get
         information on the repository. This will throw an exception if
         the repository was not found, and return cleanly if it was found.
+
+        Version Changed:
+            7.1:
+            Made arguments keyword-only.
+
+        Args:
+            plan (str):
+                The ID of the plan that the repository is on.
+
+            **kwargs (dict, unused):
+                Additional keyword arguments passed by the repository form.
+
+        Raises:
+            reviewboard.hostingsvcs.errors.RepositoryError:
+                The repository is not valid.
         """
         self._find_repository_id(
             plan,
             self._get_repository_owner(plan, kwargs),
             self._get_repository_name(plan, kwargs))
 
-    def authorize(self, username, credentials, hosting_url, *args, **kwargs):
+    @deprecate_non_keyword_only_args(RemovedInReviewBoard90Warning)
+    def authorize(
+        self,
+        *,
+        username: str | None,
+        credentials: HostingServiceCredentials,
+        hosting_url: str | None,
+        **kwargs,
+    ) -> None:
         """Authorize the GitLab repository.
 
         GitLab uses HTTP Basic Auth for the API, so this will store the
         provided password, encrypted, for use in later API requests.
 
+        Version Changed:
+            7.1:
+            Made arguments keyword-only.
+
         Args:
-            username (unicode):
+            username (str):
                 The username of the account being linked.
 
             credentials (dict):
                 Authentication credentials.
 
-            hosting_url (unicode):
+            hosting_url (str):
                 The URL of the GitLab server.
 
-            *args (tuple, unused):
-                Ignored positional arguments.
-
             **kwargs (dict, unused):
                 Ignored keyword arguments.
 
diff --git a/reviewboard/hostingsvcs/kiln.py b/reviewboard/hostingsvcs/kiln.py
index 0e09f4116e6dca7c8b08d9349488dc3e3a3f9cb8..019147a1fd5d1a7c2fca81b8686bb2d19d4c956c 100644
--- a/reviewboard/hostingsvcs/kiln.py
+++ b/reviewboard/hostingsvcs/kiln.py
@@ -1,10 +1,16 @@
+"""Hosting service for Kiln."""
+
+from __future__ import annotations
+
 import binascii
 import json
 from urllib.error import HTTPError, URLError
 
 from django import forms
 from django.utils.translation import gettext, gettext_lazy as _
+from housekeeping import deprecate_non_keyword_only_args
 
+from reviewboard.deprecation import RemovedInReviewBoard90Warning
 from reviewboard.hostingsvcs.base.client import HostingServiceClient
 from reviewboard.hostingsvcs.base.forms import BaseHostingServiceRepositoryForm
 from reviewboard.hostingsvcs.base.hosting_service import BaseHostingService
@@ -219,14 +225,45 @@
         },
     }
 
-    def check_repository(self, kiln_account_domain=None,
-                         kiln_project_name=None, kiln_group_name=None,
-                         kiln_repo_name=None, *args, **kwargs):
-        """Checks the validity of a repository.
+    @deprecate_non_keyword_only_args(RemovedInReviewBoard90Warning)
+    def check_repository(
+        self,
+        *,
+        kiln_account_domain: str,
+        kiln_project_name: str,
+        kiln_group_name: str,
+        kiln_repo_name: str,
+        **kwargs,
+    ) -> None:
+        """Check the validity of a repository.
 
         This will check to see if there's a repository accessible to the
         user matching the provided information. This will throw an exception
         if the repository was not found, and return cleanly if it was found.
+
+        Version Changed:
+            7.1:
+            Made arguments keyword-only.
+
+        Args:
+            kiln_account_domain (str):
+                The domain for the user's Kiln account.
+
+            kiln_project_name (str):
+                The name of the project.
+
+            kiln_group_name (str):
+                The name of the group within the project.
+
+            kiln_repo_name (str):
+                The name of the repository.
+
+            **kwargs (dict, unused):
+                Additional keyword arguments provided by the repository form.
+
+        Raises:
+            reviewboard.hostingsvcs.errors.RepositoryError:
+                The repository is not valid.
         """
         repo_info = self._find_repository_info(kiln_project_name,
                                                kiln_group_name,
@@ -238,14 +275,38 @@
                 'found. Please verify that the information exactly matches '
                 'the configuration on Kiln.'))
 
-    def authorize(self, username, password, kiln_account_domain,
-                  *args, **kwargs):
-        """Authorizes the Kiln repository.
+    @deprecate_non_keyword_only_args(RemovedInReviewBoard90Warning)
+    def authorize(
+        self,
+        *,
+        username: str | None,
+        password: str | None,
+        kiln_account_domain: str,
+        **kwargs,
+    ) -> None:
+        """Authorize the Kiln repository.
 
         Kiln requires an authentication request against a login URL,
         and will return an API token on success. This token is stored
         along with the account data. The username and password are not
         stored.
+
+        Version Changed:
+            7.1:
+            Made arguments keyword-only.
+
+        Args:
+            username (str):
+                The username for the account.
+
+            password (str):
+                The password for the account.
+
+            kiln_account_domain (str):
+                The domain for the user's Kiln account.
+
+            **kwargs (dict, unused):
+                Unused keyword arguments.
         """
         self.account.data['kiln_account_domain'] = kiln_account_domain
 
diff --git a/reviewboard/hostingsvcs/rbgateway.py b/reviewboard/hostingsvcs/rbgateway.py
index ce5fa630cf6bd79e503da784bcd4820ab63fa11a..7a8208ce921f2fb5dc8e2bfc1c72e98353c006de 100644
--- a/reviewboard/hostingsvcs/rbgateway.py
+++ b/reviewboard/hostingsvcs/rbgateway.py
@@ -1,3 +1,7 @@
+"""Hosting service for Review Board Gateway."""
+
+from __future__ import annotations
+
 import hashlib
 import hmac
 import json
@@ -11,8 +15,10 @@
 from django.template.loader import render_to_string
 from django.urls import path
 from django.utils.translation import gettext_lazy as _, gettext
+from housekeeping import deprecate_non_keyword_only_args
 
 from reviewboard.admin.server import build_server_url, get_server_url
+from reviewboard.deprecation import RemovedInReviewBoard90Warning
 from reviewboard.hostingsvcs.base.client import HostingServiceClient
 from reviewboard.hostingsvcs.base.forms import BaseHostingServiceRepositoryForm
 from reviewboard.hostingsvcs.base.hosting_service import BaseHostingService
@@ -638,18 +644,21 @@
              name='rbgateway-hooks-close-submitted'),
     ]
 
-    def check_repository(self, rbgateway_repo_name, *args, **kwargs):
+    @deprecate_non_keyword_only_args(RemovedInReviewBoard90Warning)
+    def check_repository(
+        self,
+        *,
+        rbgateway_repo_name: str,
+        **kwargs,
+    ) -> None:
         """Checks the validity of a repository configuration.
 
         Args:
-            rbgateway_repo_name (unicode):
+            rbgateway_repo_name (str):
                 The name of the repository to check.
 
-            *args (tuple, unused):
-                Unused positional arguments.
-
             **kwargs (dict, unused):
-                Unused dictionary arguments.
+                Additional keyword arguments provided by the repository form.
 
         Raises:
             reviewboard.hostingsvcs.errors.RepositoryError:
@@ -666,23 +675,31 @@
 
             raise
 
-    def authorize(self, username, password, *args, **kwargs):
+    @deprecate_non_keyword_only_args(RemovedInReviewBoard90Warning)
+    def authorize(
+        self,
+        *,
+        username: str | None,
+        password: str | None,
+        **kwargs,
+    ) -> None:
         """Authorize an account on the RB Gateway service.
 
         This will perform an authentication request against the API. If
         successful, the generated API token will be stored, encrypted, for
         future requests to the API.
 
+        Version Changed:
+            7.1:
+            Made arguments keyword-only.
+
         Args:
-            username (unicode):
+            username (str):
                 The username for the account.
 
-            password (unicode):
+            password (str):
                 The password for the account.
 
-            *args (tuple, unused):
-                Unused positional arguments.
-
             **kwargs (dict, unused):
                 Unused keyword arguments.
 
diff --git a/reviewboard/hostingsvcs/tests/test_assembla.py b/reviewboard/hostingsvcs/tests/test_assembla.py
index 652262c6c9c6d2f873e056d7856fe9bd4df2ef25..2c6989da3baaf072846bbf7dd18e21e87c678946 100644
--- a/reviewboard/hostingsvcs/tests/test_assembla.py
+++ b/reviewboard/hostingsvcs/tests/test_assembla.py
@@ -65,7 +65,7 @@
 
         self.assertFalse(service.is_authorized())
 
-        service.authorize('myuser', 'abc123', None)
+        service.authorize(username='myuser', password='abc123')
 
         self.assertIn('password', account.data)
         self.assertNotEqual(account.data['password'], 'abc123')
@@ -77,7 +77,7 @@
             account = self.create_hosting_account()
             service = account.service
 
-            service.authorize('myuser', 'abc123', None)
+            service.authorize(username='myuser', password='abc123')
 
             repository = self.create_repository(hosting_account=account,
                                                 tool_name='Perforce')
@@ -105,7 +105,7 @@
             account = self.create_hosting_account()
             service = account.service
 
-            service.authorize('myuser', 'abc123', None)
+            service.authorize(username='myuser', password='abc123')
 
             repository = self.create_repository(
                 path='https://svn.example.com/',
diff --git a/reviewboard/hostingsvcs/tests/test_beanstalk.py b/reviewboard/hostingsvcs/tests/test_beanstalk.py
index 32df5296261449216fed7d9636843484dfb1df44..84581292277d29c9c184226ef833a967a942235e 100644
--- a/reviewboard/hostingsvcs/tests/test_beanstalk.py
+++ b/reviewboard/hostingsvcs/tests/test_beanstalk.py
@@ -65,7 +65,7 @@
 
         self.assertFalse(service.is_authorized())
 
-        service.authorize('myuser', 'abc123', None)
+        service.authorize(username='myuser', password='abc123')
 
         self.assertIn('password', account.data)
         self.assertNotEqual(account.data['password'], 'abc123')
diff --git a/reviewboard/hostingsvcs/tests/test_client.py b/reviewboard/hostingsvcs/tests/test_client.py
index 0ddc12234444074db4b70726a47d004b9b5c714a..491bfe2bea15c6291e45339fe620991773d7998e 100644
--- a/reviewboard/hostingsvcs/tests/test_client.py
+++ b/reviewboard/hostingsvcs/tests/test_client.py
@@ -2,9 +2,8 @@
 
 from __future__ import annotations
 
-from typing import List
+from typing import TYPE_CHECKING
 
-from djblets.testing.testcases import ExpectedWarning
 from kgb import SpyAgency
 
 from reviewboard.deprecation import RemovedInReviewBoard80Warning
@@ -15,12 +14,15 @@
 from reviewboard.testing.hosting_services import TestService
 from reviewboard.testing.testcase import TestCase
 
-
-class DummyHTTPRequest(HostingServiceHTTPRequest):
-    def open(self):
+if TYPE_CHECKING:
+    from djblets.testing.testcases import ExpectedWarning
+
+
+class _DummyHTTPRequest(HostingServiceHTTPRequest):
+    def open(self) -> HostingServiceHTTPResponse:
         method = self.method
 
-        if method in ('DELETE', 'HEAD'):
+        if method in {'DELETE', 'HEAD'}:
             data = None
         else:
             data = b'{"key": "test response"}'
@@ -37,7 +39,7 @@
             url=self.url,
             data=data,
             headers={
-                str('Test-header'): str('Value'),
+                'Test-header': 'Value',
             },
             status_code=status_code)
 
@@ -45,7 +47,7 @@
 class HostingServiceHTTPRequestTests(TestCase):
     """Unit tests for HostingServiceHTTPRequest."""
 
-    def test_init_with_query(self):
+    def test_init_with_query(self) -> None:
         """Testing HostingServiceHTTPRequest construction with query="""
         request = HostingServiceHTTPRequest(
             url='http://example.com?z=1&z=2&baz=true',
@@ -60,27 +62,27 @@
             'http://example.com?a=10&baz=true&foo=bar&list=a&list=b&list=c'
             '&z=1&z=2')
 
-    def test_init_with_body_not_bytes(self):
+    def test_init_with_body_not_bytes(self) -> None:
         """Testing HostingServiceHTTPRequest construction with non-bytes body
         """
         account = HostingServiceAccount()
         service = TestService(account)
 
         expected_message = (
-            'Received non-bytes body for the HTTP request for %r. This is '
-            'likely an implementation problem. Please make sure only byte '
-            'strings are sent for the request body.'
-            % TestService
+            f'Received non-bytes body for the HTTP request for '
+            f'{TestService!r}. This is likely an implementation problem. '
+            f'Please make sure only byte strings are sent for the request '
+            f'body.'
         )
 
         with self.assertRaisesMessage(TypeError, expected_message):
             HostingServiceHTTPRequest(
                 url='http://example.com?z=1&z=2&baz=true',
                 method='POST',
-                body=123,
+                body=123,  # type:ignore
                 hosting_service=service)
 
-    def test_init_with_header_key_not_unicode(self):
+    def test_init_with_header_key_not_unicode(self) -> None:
         """Testing HostingServiceHTTPRequest construction with non-Unicode
         header key
         """
@@ -88,22 +90,22 @@
         service = TestService(account)
 
         expected_message = (
-            'Received non-Unicode header %r (value=%r) for the HTTP request '
-            'for %r. This is likely an implementation problem. Please make '
-            'sure only Unicode strings are sent in request headers.'
-            % (b'My-Header', 'abc', TestService)
+            f"Received non-Unicode header name b'My-Header' (value='abc') for "
+            f"the HTTP request for {TestService!r}. This is likely an "
+            f"implementation problem. Please make sure only Unicode strings "
+            f"are sent in request headers."
         )
 
         with self.assertRaisesMessage(TypeError, expected_message):
             HostingServiceHTTPRequest(
                 url='http://example.com?z=1&z=2&baz=true',
                 method='POST',
-                headers={
+                headers={  # type:ignore
                     b'My-Header': 'abc',
                 },
                 hosting_service=service)
 
-    def test_init_with_header_value_not_unicode(self):
+    def test_init_with_header_value_not_unicode(self) -> None:
         """Testing HostingServiceHTTPRequest construction with non-Unicode
         header value
         """
@@ -111,22 +113,22 @@
         service = TestService(account)
 
         expected_message = (
-            'Received non-Unicode header %r (value=%r) for the HTTP request '
-            'for %r. This is likely an implementation problem. Please make '
-            'sure only Unicode strings are sent in request headers.'
-            % ('My-Header', b'abc', TestService)
+            f"Received non-Unicode header value for 'My-Header' "
+            f"(value=b'abc') for the HTTP request for {TestService!r}. This "
+            f"is likely an implementation problem. Please make sure only "
+            f"Unicode strings are sent in request headers."
         )
 
         with self.assertRaisesMessage(TypeError, expected_message):
             HostingServiceHTTPRequest(
                 url='http://example.com?z=1&z=2&baz=true',
                 method='POST',
-                headers={
+                headers={  # type:ignore
                     'My-Header': b'abc',
                 },
                 hosting_service=service)
 
-    def test_add_basic_auth(self):
+    def test_add_basic_auth(self) -> None:
         """Testing HostingServiceHTTPRequest.add_basic_auth"""
         request = HostingServiceHTTPRequest('http://example.com')
         request.add_basic_auth(b'username', b'password')
@@ -137,7 +139,7 @@
                 'Authorization': 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=',
             })
 
-    def test_get_header(self):
+    def test_get_header(self) -> None:
         """Testing HostingServiceHTTPRequest.get_header"""
         request = HostingServiceHTTPRequest(
             'http://example.com',
@@ -158,7 +160,7 @@
 class HostingServiceHTTPResponseTests(TestCase):
     """Unit tests for HostingServiceHTTPResponse."""
 
-    def test_json(self):
+    def test_json(self) -> None:
         """Testing HostingServiceHTTPResponse.json"""
         request = HostingServiceHTTPRequest('http://example.com')
         response = HostingServiceHTTPResponse(request=request,
@@ -173,7 +175,7 @@
                 'b': 2,
             })
 
-    def test_json_with_non_json_response(self):
+    def test_json_with_non_json_response(self) -> None:
         """Testing HostingServiceHTTPResponse.json with non-JSON response"""
         request = HostingServiceHTTPRequest('http://example.com')
         response = HostingServiceHTTPResponse(request=request,
@@ -185,7 +187,7 @@
         with self.assertRaises(ValueError):
             response.json
 
-    def test_get_header(self):
+    def test_get_header(self) -> None:
         """Testing HostingServiceHTTPRequest.get_header"""
         request = HostingServiceHTTPRequest('http://example.com')
         response = HostingServiceHTTPResponse(
@@ -194,8 +196,8 @@
             status_code=200,
             data=b'',
             headers={
-                str('Authorization'): str('Basic abc123'),
-                str('Content-Length'): str('123'),
+                'Authorization': 'Basic abc123',
+                'Content-Length': '123',
             })
 
         self.assertEqual(response.get_header('Authorization'), 'Basic abc123')
@@ -216,21 +218,23 @@
     #:     reviewboard.hostingsvcs.base.client.HostingServiceClient
     client: HostingServiceClient
 
-    def setUp(self):
+    def setUp(self) -> None:
+        """Set up the test case."""
         super().setUp()
 
         account = HostingServiceAccount()
         service = TestService(account)
 
         self.client = HostingServiceClient(service)
-        self.client.http_request_cls = DummyHTTPRequest
+        self.client.http_request_cls = _DummyHTTPRequest
 
     def tearDown(self) -> None:
+        """Tear down the test case."""
         super().tearDown()
 
         self.client = None  # type: ignore
 
-    def test_http_delete(self):
+    def test_http_delete(self) -> None:
         """Testing HostingServiceClient.http_delete"""
         self.spy_on(self.client.build_http_request)
 
@@ -243,18 +247,18 @@
             password='password')
 
         self.assertIsInstance(response, HostingServiceHTTPResponse)
-        self.assertIsNone(response.data)
+        self.assertEqual(response.data, b'')
         self.assertEqual(response.url, 'http://example.com')
         self.assertEqual(response.status_code, 204)
         self.assertIsInstance(response.headers, dict)
         self.assertEqual(
             response.headers,
             {
-                str('Test-header'): str('Value'),
+                'Test-header': 'Value',
             })
 
         # One for each item in the tuple, + 1 to detect the bounds.
-        expected_warnings: List[ExpectedWarning] = [
+        expected_warnings: list[ExpectedWarning] = [
             {
                 'cls': RemovedInReviewBoard80Warning,
                 'message': (
@@ -264,7 +268,7 @@
                     'be removed in Review Board 8.'
                 ),
             }
-            for i in range(3)
+            for _i in range(3)
         ]
 
         with self.assertWarnings(expected_warnings):
@@ -297,7 +301,7 @@
                 'Foo': 'bar',
             })
 
-    def test_http_get(self):
+    def test_http_get(self) -> None:
         """Testing HostingServiceClient.http_get"""
         self.spy_on(self.client.build_http_request)
 
@@ -317,11 +321,11 @@
         self.assertEqual(
             response.headers,
             {
-                str('Test-header'): str('Value'),
+                'Test-header': 'Value',
             })
 
         # One for each item in the tuple, + 1 to detect the bounds.
-        expected_warnings: List[ExpectedWarning] = [
+        expected_warnings: list[ExpectedWarning] = [
             {
                 'cls': RemovedInReviewBoard80Warning,
                 'message': (
@@ -331,7 +335,7 @@
                     'be removed in Review Board 8.'
                 ),
             }
-            for i in range(3)
+            for _i in range(3)
         ]
 
         with self.assertWarnings(expected_warnings):
@@ -363,7 +367,7 @@
                 'Foo': 'bar',
             })
 
-    def test_http_head(self):
+    def test_http_head(self) -> None:
         """Testing HostingServiceClient.http_head"""
         self.spy_on(self.client.build_http_request)
 
@@ -376,18 +380,18 @@
             password='password')
 
         self.assertIsInstance(response, HostingServiceHTTPResponse)
-        self.assertIsNone(response.data)
+        self.assertEqual(response.data, b'')
         self.assertEqual(response.url, 'http://example.com')
         self.assertEqual(response.status_code, 200)
         self.assertIsInstance(response.headers, dict)
         self.assertEqual(
             response.headers,
             {
-                str('Test-header'): str('Value'),
+                'Test-header': 'Value',
             })
 
         # One for each item in the tuple, + 1 to detect the bounds.
-        expected_warnings: List[ExpectedWarning] = [
+        expected_warnings: list[ExpectedWarning] = [
             {
                 'cls': RemovedInReviewBoard80Warning,
                 'message': (
@@ -397,7 +401,7 @@
                     'be removed in Review Board 8.'
                 ),
             }
-            for i in range(3)
+            for _i in range(3)
         ]
 
         with self.assertWarnings(expected_warnings):
@@ -429,13 +433,13 @@
                 'Foo': 'bar',
             })
 
-    def test_http_post_with_body_unicode(self):
+    def test_http_post_with_body_unicode(self) -> None:
         """Testing HostingServiceClient.http_post with body as Unicode"""
         self.spy_on(self.client.build_http_request)
 
         response = self.client.http_post(
             url='http://example.com',
-            body='test body\U0001f60b',
+            body='test body\U0001f60b'.encode('utf-8'),
             headers={
                 'Foo': 'bar',
             },
@@ -450,11 +454,11 @@
         self.assertEqual(
             response.headers,
             {
-                str('Test-header'): str('Value'),
+                'Test-header': 'Value',
             })
 
         # One for each item in the tuple, + 1 to detect the bounds.
-        expected_warnings: List[ExpectedWarning] = [
+        expected_warnings: list[ExpectedWarning] = [
             {
                 'cls': RemovedInReviewBoard80Warning,
                 'message': (
@@ -464,7 +468,7 @@
                     'be removed in Review Board 8.'
                 ),
             }
-            for i in range(3)
+            for _i in range(3)
         ]
 
         with self.assertWarnings(expected_warnings):
@@ -498,7 +502,7 @@
                 'Foo': 'bar',
             })
 
-    def test_http_post_with_body_bytes(self):
+    def test_http_post_with_body_bytes(self) -> None:
         """Testing HostingServiceClient.http_post with body as bytes"""
         self.spy_on(self.client.build_http_request)
 
@@ -519,11 +523,11 @@
         self.assertEqual(
             response.headers,
             {
-                str('Test-header'): str('Value'),
+                'Test-header': 'Value',
             })
 
         # One for each item in the tuple, + 1 to detect the bounds.
-        expected_warnings: List[ExpectedWarning] = [
+        expected_warnings: list[ExpectedWarning] = [
             {
                 'cls': RemovedInReviewBoard80Warning,
                 'message': (
@@ -533,7 +537,7 @@
                     'be removed in Review Board 8.'
                 ),
             }
-            for i in range(3)
+            for _i in range(3)
         ]
 
         with self.assertWarnings(expected_warnings):
@@ -567,7 +571,7 @@
                 'Foo': 'bar',
             })
 
-    def test_http_put_with_body_unicode(self):
+    def test_http_put_with_body_unicode(self) -> None:
         """Testing HostingServiceClient.http_put with body as Unicode"""
         self.spy_on(self.client.build_http_request)
 
@@ -588,11 +592,11 @@
         self.assertEqual(
             response.headers,
             {
-                str('Test-header'): str('Value'),
+                'Test-header': 'Value',
             })
 
         # One for each item in the tuple, + 1 to detect the bounds.
-        expected_warnings: List[ExpectedWarning] = [
+        expected_warnings: list[ExpectedWarning] = [
             {
                 'cls': RemovedInReviewBoard80Warning,
                 'message': (
@@ -602,7 +606,7 @@
                     'be removed in Review Board 8.'
                 ),
             }
-            for i in range(3)
+            for _i in range(3)
         ]
 
         with self.assertWarnings(expected_warnings):
@@ -636,7 +640,7 @@
                 'Foo': 'bar',
             })
 
-    def test_http_put_with_body_bytes(self):
+    def test_http_put_with_body_bytes(self) -> None:
         """Testing HostingServiceClient.http_put with body as bytes"""
         self.spy_on(self.client.build_http_request)
 
@@ -657,11 +661,11 @@
         self.assertEqual(
             response.headers,
             {
-                str('Test-header'): str('Value'),
+                'Test-header': 'Value',
             })
 
         # One for each item in the tuple, + 1 to detect the bounds.
-        expected_warnings: List[ExpectedWarning] = [
+        expected_warnings: list[ExpectedWarning] = [
             {
                 'cls': RemovedInReviewBoard80Warning,
                 'message': (
@@ -671,7 +675,7 @@
                     'be removed in Review Board 8.'
                 ),
             }
-            for i in range(3)
+            for _i in range(3)
         ]
 
         with self.assertWarnings(expected_warnings):
@@ -705,7 +709,7 @@
                 'Foo': 'bar',
             })
 
-    def test_http_request(self):
+    def test_http_request(self) -> None:
         """Testing HostingServiceClient.http_request"""
         self.spy_on(self.client.build_http_request)
 
@@ -727,11 +731,11 @@
         self.assertEqual(
             response.headers,
             {
-                str('Test-header'): str('Value'),
+                'Test-header': 'Value',
             })
 
         # One for each item in the tuple, + 1 to detect the bounds.
-        expected_warnings: List[ExpectedWarning] = [
+        expected_warnings: list[ExpectedWarning] = [
             {
                 'cls': RemovedInReviewBoard80Warning,
                 'message': (
@@ -741,7 +745,7 @@
                     'be removed in Review Board 8.'
                 ),
             }
-            for i in range(3)
+            for _i in range(3)
         ]
 
         with self.assertWarnings(expected_warnings):
@@ -773,7 +777,7 @@
                 'Foo': 'bar',
             })
 
-    def test_build_http_request(self):
+    def test_build_http_request(self) -> None:
         """Testing HostingServiceClient.build_http_request"""
         request = self.client.build_http_request(
             url='http://example.com',
@@ -793,7 +797,7 @@
                 'Foo': 'bar',
             })
 
-    def test_build_http_request_with_basic_auth(self):
+    def test_build_http_request_with_basic_auth(self) -> None:
         """Testing HostingServiceClient.build_http_request with username and
         password
         """
@@ -819,7 +823,7 @@
                 'Foo': 'bar',
             })
 
-    def test_json_delete(self):
+    def test_json_delete(self) -> None:
         """Testing HostingServiceClient.json_delete"""
         self.spy_on(self.client.build_http_request)
 
@@ -838,12 +842,12 @@
                 username='username',
                 password='password')
 
-        self.assertIsNone(rsp)
+        self.assertEqual(rsp, b'')
         self.assertIsInstance(headers, dict)
         self.assertEqual(
             headers,
             {
-                str('Test-header'): str('Value'),
+                'Test-header': 'Value',
             })
 
         self.assertSpyCalledWith(
@@ -870,7 +874,7 @@
                 'Foo': 'bar',
             })
 
-    def test_json_get(self):
+    def test_json_get(self) -> None:
         """Testing HostingServiceClient.json_get"""
         self.spy_on(self.client.build_http_request)
 
@@ -898,7 +902,7 @@
         self.assertEqual(
             headers,
             {
-                str('Test-header'): str('Value'),
+                'Test-header': 'Value',
             })
 
         self.assertSpyCalledWith(
@@ -924,7 +928,7 @@
                 'Foo': 'bar',
             })
 
-    def test_json_post_with_body_unicode(self):
+    def test_json_post_with_body_unicode(self) -> None:
         """Testing HostingServiceClient.json_post with body as Unicode"""
         self.spy_on(self.client.build_http_request)
 
@@ -953,7 +957,7 @@
         self.assertEqual(
             headers,
             {
-                str('Test-header'): str('Value'),
+                'Test-header': 'Value',
             })
 
         self.assertSpyCalledWith(
@@ -981,7 +985,7 @@
                 'Foo': 'bar',
             })
 
-    def test_json_post_with_body_bytes(self):
+    def test_json_post_with_body_bytes(self) -> None:
         """Testing HostingServiceClient.json_post with body as bytes"""
         self.spy_on(self.client.build_http_request)
 
@@ -1010,7 +1014,7 @@
         self.assertEqual(
             headers,
             {
-                str('Test-header'): str('Value'),
+                'Test-header': 'Value',
             })
 
         self.assertSpyCalledWith(
diff --git a/reviewboard/hostingsvcs/tests/test_gitlab.py b/reviewboard/hostingsvcs/tests/test_gitlab.py
index 0f44e38eda9d281aaffdb88cc0674d8c7b08555f..2e1f7c6c28b7871c7e41b5cbfda5842d4548af94 100644
--- a/reviewboard/hostingsvcs/tests/test_gitlab.py
+++ b/reviewboard/hostingsvcs/tests/test_gitlab.py
@@ -362,7 +362,7 @@
                                   hosting_account=hosting_account) as ctx:
             with self.assertRaisesMessage(GitLabAPIVersionError, message):
                 ctx.service.authorize(
-                    'myuser',
+                    username='myuser',
                     credentials={
                         'username': 'myuser',
                         'private_token': 'foobarbaz',
@@ -1303,7 +1303,7 @@
         with self.setup_http_test(hosting_account=hosting_account,
                                   *args, **kwargs) as ctx:
             ctx.service.authorize(
-                'myuser',
+                username='myuser',
                 credentials={
                     'username': 'myuser',
                     'private_token': 'foobarbaz',
diff --git a/reviewboard/hostingsvcs/tests/test_registration.py b/reviewboard/hostingsvcs/tests/test_registration.py
index 77e6d6fbde7481e2dbec7363eed7621648ffdbd4..5afa214edb3d5fc7117ef3c19c4c8334061e1167 100644
--- a/reviewboard/hostingsvcs/tests/test_registration.py
+++ b/reviewboard/hostingsvcs/tests/test_registration.py
@@ -1,27 +1,55 @@
+"""Unit tests for Hosting Service registration."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
 from django.http import HttpResponse
 from django.urls import NoReverseMatch, path
 from djblets.registries.errors import AlreadyRegisteredError, ItemLookupError
 
-from reviewboard.hostingsvcs.base import BaseHostingService
+from reviewboard.deprecation import RemovedInReviewBoard90Warning
+from reviewboard.hostingsvcs.base.hosting_service import BaseHostingService
+from reviewboard.hostingsvcs.base.registry import hosting_service_registry
 from reviewboard.hostingsvcs.service import (register_hosting_service,
                                              unregister_hosting_service)
 from reviewboard.site.urlresolvers import local_site_reverse
 from reviewboard.testing import TestCase
 
-
-def hosting_service_url_test_view(request, repo_id):
-    """View to test URL pattern addition when registering a hosting service"""
+if TYPE_CHECKING:
+    from django.http import HttpRequest
+
+
+def hosting_service_url_test_view(
+    request: HttpRequest,
+    repo_id: int,
+) -> HttpResponse:
+    """View to test URL pattern addition when registering a hosting service.
+
+    Args:
+        request (django.http.HttpRequest):
+            The HTTP request.
+
+        repo_id (int):
+            The ID of the repository.
+
+    Returns:
+        django.http.HttpResponse:
+        A fake response.
+    """
     return HttpResponse(str(repo_id))
 
 
 class HostingServiceRegistrationTests(TestCase):
     """Unit tests for Hosting Service registration."""
 
-    class DummyService(BaseHostingService):
+    class _DummyService(BaseHostingService):
         name = 'DummyService'
+        hosting_service_id = 'dummy-service'
 
-    class DummyServiceWithURLs(BaseHostingService):
+    class _DummyServiceWithURLs(BaseHostingService):
         name = 'DummyServiceWithURLs'
+        hosting_service_id = 'dummy-service'
 
         repository_url_patterns = [
             path('hooks/pre-commit/',
@@ -29,33 +57,34 @@
                  name='dummy-service-post-commit-hook'),
         ]
 
-    def tearDown(self):
-        super(HostingServiceRegistrationTests, self).tearDown()
+    def tearDown(self) -> None:
+        """Tear down the test case."""
+        super().tearDown()
 
         # Unregister the service, going back to a default state. It's okay
         # if it fails.
         #
         # This will match whichever service we added for testing.
         try:
-            unregister_hosting_service('dummy-service')
+            hosting_service_registry.unregister_by_id('dummy-service')
         except ItemLookupError:
             pass
 
-    def test_register_without_urls(self):
+    def test_register_without_urls(self) -> None:
         """Testing HostingService registration"""
-        register_hosting_service('dummy-service', self.DummyService)
+        hosting_service_registry.register(self._DummyService)
 
         with self.assertRaises(AlreadyRegisteredError):
-            register_hosting_service('dummy-service', self.DummyService)
+            hosting_service_registry.register(self._DummyService)
 
-    def test_unregister(self):
+    def test_unregister(self) -> None:
         """Testing HostingService unregistration"""
-        register_hosting_service('dummy-service', self.DummyService)
-        unregister_hosting_service('dummy-service')
+        hosting_service_registry.register(self._DummyService)
+        hosting_service_registry.unregister(self._DummyService)
 
-    def test_registration_with_urls(self):
+    def test_registration_with_urls(self) -> None:
         """Testing HostingService registration with URLs"""
-        register_hosting_service('dummy-service', self.DummyServiceWithURLs)
+        hosting_service_registry.register(self._DummyServiceWithURLs)
 
         self.assertEqual(
             local_site_reverse(
@@ -76,15 +105,14 @@
                 }),
             '/s/test-site/repos/1/dummy-service/hooks/pre-commit/')
 
-        # Once registered, should not be able to register again
+        # Once registered, should not be able to register again.
         with self.assertRaises(AlreadyRegisteredError):
-            register_hosting_service('dummy-service',
-                                     self.DummyServiceWithURLs)
+            hosting_service_registry.register(self._DummyServiceWithURLs)
 
-    def test_unregistration_with_urls(self):
+    def test_unregistration_with_urls(self) -> None:
         """Testing HostingService unregistration with URLs"""
-        register_hosting_service('dummy-service', self.DummyServiceWithURLs)
-        unregister_hosting_service('dummy-service')
+        hosting_service_registry.register(self._DummyServiceWithURLs)
+        hosting_service_registry.unregister(self._DummyServiceWithURLs)
 
         with self.assertRaises(NoReverseMatch):
             local_site_reverse(
@@ -92,8 +120,30 @@
                 kwargs={
                     'repository_id': 1,
                     'hosting_service_id': 'dummy-service',
-                }),
+                })
 
-        # Once unregistered, should not be able to unregister again
+        # Once unregistered, should not be able to unregister again.
         with self.assertRaises(ItemLookupError):
+            hosting_service_registry.unregister(self._DummyServiceWithURLs)
+
+    def test_legacy_register_methods(self) -> None:
+        """Testing legacy register and unregister methods"""
+        message = (
+            '`reviewboard.hostingsvcs.service.register_hosting_service()` '
+            'has moved to `reviewboard.hostingsvcs.base.registry.'
+            'HostingServiceRegistry.register`. The old function is '
+            'deprecated and will be removed in Review Board 9.0.'
+        )
+
+        with self.assertWarns(RemovedInReviewBoard90Warning, message):
+            register_hosting_service('dummy-service', self._DummyService)
+
+        message = (
+            '`reviewboard.hostingsvcs.service.unregister_hosting_service()` '
+            'has moved to `reviewboard.hostingsvcs.base.registry.'
+            'HostingServiceRegistry.unregister_by_id`. The old function is '
+            'deprecated and will be removed in Review Board 9.0.'
+        )
+
+        with self.assertWarns(RemovedInReviewBoard90Warning, message):
             unregister_hosting_service('dummy-service')
diff --git a/reviewboard/hostingsvcs/unfuddle.py b/reviewboard/hostingsvcs/unfuddle.py
index 3308a263f15beb5e08be7151804ec2721e5a8de9..b40e38f05501e9477495e8c9d2e4d38a6be77eba 100644
--- a/reviewboard/hostingsvcs/unfuddle.py
+++ b/reviewboard/hostingsvcs/unfuddle.py
@@ -1,9 +1,15 @@
+"""Hosting service for Unfuddle."""
+
+from __future__ import annotations
+
 from urllib.error import HTTPError, URLError
 from urllib.parse import quote
 
 from django import forms
 from django.utils.translation import gettext, gettext_lazy as _
+from housekeeping import deprecate_non_keyword_only_args
 
+from reviewboard.deprecation import RemovedInReviewBoard90Warning
 from reviewboard.hostingsvcs.base.forms import BaseHostingServiceRepositoryForm
 from reviewboard.hostingsvcs.base.hosting_service import BaseHostingService
 from reviewboard.hostingsvcs.errors import (AuthorizationError,
@@ -83,24 +89,75 @@
         'svn': 'Subversion',
     }
 
-    def check_repository(self, unfuddle_account_domain=None,
-                         unfuddle_repo_name=None, tool_name=None,
-                         *args, **kwargs):
-        """Checks the validity of a repository.
+    @deprecate_non_keyword_only_args(RemovedInReviewBoard90Warning)
+    def check_repository(
+        self,
+        *,
+        unfuddle_account_domain: str,
+        unfuddle_repo_name: str,
+        tool_name: (str | None) = None,
+        **kwargs,
+    ) -> None:
+        """Check the validity of a repository.
 
         This will perform an API request against Unfuddle to get
         information on the repository. This will throw an exception if
         the repository was not found, and return cleanly if it was found.
+
+        Version Changed:
+            7.1:
+            Made arguments keyword-only.
+
+        Args:
+            unfuddle_account_domain (str):
+                The domain for the user's Unfuddle account.
+
+            unfuddle_repo_name (str):
+                The name of the repository.
+
+            tool_name (str, optional):
+                The name of the tool to use.
+
+            **kwargs (dict, unused):
+                Additional keyword arguments provided by the repository form.
+
+        Raises:
+            reviewboard.hostingsvcs.errors.RepositoryError:
+                The repository is not valid.
         """
         self._api_get_repository(unfuddle_account_domain, unfuddle_repo_name,
                                  tool_name)
 
-    def authorize(self, username, password, unfuddle_account_domain=None,
-                  *args, **kwargs):
-        """Authorizes the Unfuddle repository.
+    @deprecate_non_keyword_only_args(RemovedInReviewBoard90Warning)
+    def authorize(
+        self,
+        *,
+        username: str | None,
+        password: str | None,
+        unfuddle_account_domain: str,
+        **kwargs,
+    ) -> None:
+        """Authorize the Unfuddle repository.
 
         Unfuddle uses HTTP Basic Auth for the API, so this will store the
         provided password, encrypted, for use in later API requests.
+
+        Version Changed:
+            7.1:
+            Made arguments keyword-only.
+
+        Args:
+            username (str):
+                The username for the account.
+
+            password (str):
+                The password for the account.
+
+            unfuddle_account_domain (str):
+                The domain for the user's Unfuddle account.
+
+            **kwargs (dict, unused):
+                Additional keyword arguments.
         """
         # This will raise an exception if it fails, which the form will
         # catch.
diff --git a/reviewboard/scmtools/forms.py b/reviewboard/scmtools/forms.py
index 5e4d53d0314d795170fda2f20622f2360ebe7f84..f2323dd4a932ee3f9949172bcb8a02157df2d277 100644
--- a/reviewboard/scmtools/forms.py
+++ b/reviewboard/scmtools/forms.py
@@ -2286,6 +2286,7 @@
             return
 
         subforms_cleaned_data = self.subforms_cleaned_data
+        assert subforms_cleaned_data is not None
         path = subforms_cleaned_data.get('path')
         username = subforms_cleaned_data.get('username')
         password = subforms_cleaned_data.get('password')
