diff --git a/analytics/lib/counts.py b/analytics/lib/counts.py index b0b3e06722..6e616535eb 100644 --- a/analytics/lib/counts.py +++ b/analytics/lib/counts.py @@ -8,7 +8,7 @@ from django.conf import settings from django.db import connection, models from django.db.models import F from psycopg2.sql import SQL, Composable, Identifier, Literal -from typing_extensions import TypeAlias +from typing_extensions import TypeAlias, override from analytics.models import ( BaseCount, @@ -63,6 +63,7 @@ class CountStat: else: self.interval = self.time_increment + @override def __repr__(self) -> str: return f"" diff --git a/analytics/management/commands/check_analytics_state.py b/analytics/management/commands/check_analytics_state.py index 66bb7694e8..606892ae82 100644 --- a/analytics/management/commands/check_analytics_state.py +++ b/analytics/management/commands/check_analytics_state.py @@ -5,6 +5,7 @@ from typing import Any, Dict from django.core.management.base import BaseCommand from django.utils.timezone import now as timezone_now +from typing_extensions import override from analytics.lib.counts import COUNT_STATS, CountStat from analytics.models import installation_epoch @@ -24,6 +25,7 @@ class Command(BaseCommand): Run as a cron job that runs every hour.""" + @override def handle(self, *args: Any, **options: Any) -> None: fill_state = self.get_fill_state() status = fill_state["status"] diff --git a/analytics/management/commands/clear_analytics_tables.py b/analytics/management/commands/clear_analytics_tables.py index 84e0af846c..9292d94fc6 100644 --- a/analytics/management/commands/clear_analytics_tables.py +++ b/analytics/management/commands/clear_analytics_tables.py @@ -2,6 +2,7 @@ from argparse import ArgumentParser from typing import Any from django.core.management.base import BaseCommand, CommandError +from typing_extensions import override from analytics.lib.counts import do_drop_all_analytics_tables @@ -9,9 +10,11 @@ from analytics.lib.counts import do_drop_all_analytics_tables class Command(BaseCommand): help = """Clear analytics tables.""" + @override def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument("--force", action="store_true", help="Clear analytics tables.") + @override def handle(self, *args: Any, **options: Any) -> None: if options["force"]: do_drop_all_analytics_tables() diff --git a/analytics/management/commands/clear_single_stat.py b/analytics/management/commands/clear_single_stat.py index f0b8f0ff4e..c70a8ae66f 100644 --- a/analytics/management/commands/clear_single_stat.py +++ b/analytics/management/commands/clear_single_stat.py @@ -2,6 +2,7 @@ from argparse import ArgumentParser from typing import Any from django.core.management.base import BaseCommand, CommandError +from typing_extensions import override from analytics.lib.counts import COUNT_STATS, do_drop_single_stat @@ -9,10 +10,12 @@ from analytics.lib.counts import COUNT_STATS, do_drop_single_stat class Command(BaseCommand): help = """Clear analytics tables.""" + @override def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument("--force", action="store_true", help="Actually do it.") parser.add_argument("--property", help="The property of the stat to be cleared.") + @override def handle(self, *args: Any, **options: Any) -> None: property = options["property"] if property not in COUNT_STATS: diff --git a/analytics/management/commands/populate_analytics_db.py b/analytics/management/commands/populate_analytics_db.py index 8766408212..6d3f96331a 100644 --- a/analytics/management/commands/populate_analytics_db.py +++ b/analytics/management/commands/populate_analytics_db.py @@ -5,7 +5,7 @@ from typing import Any, Dict, List, Mapping, Type, Union from django.core.files.uploadedfile import UploadedFile from django.core.management.base import BaseCommand from django.utils.timezone import now as timezone_now -from typing_extensions import TypeAlias +from typing_extensions import TypeAlias, override from analytics.lib.counts import COUNT_STATS, CountStat, do_drop_all_analytics_tables from analytics.lib.fixtures import generate_time_series_data @@ -68,6 +68,7 @@ class Command(BaseCommand): random_seed=self.random_seed, ) + @override def handle(self, *args: Any, **options: Any) -> None: # TODO: This should arguably only delete the objects # associated with the "analytics" realm. diff --git a/analytics/management/commands/update_analytics_counts.py b/analytics/management/commands/update_analytics_counts.py index 28693fe3e6..1a68cbf0e5 100644 --- a/analytics/management/commands/update_analytics_counts.py +++ b/analytics/management/commands/update_analytics_counts.py @@ -8,6 +8,7 @@ from django.conf import settings from django.core.management.base import BaseCommand from django.utils.dateparse import parse_datetime from django.utils.timezone import now as timezone_now +from typing_extensions import override from analytics.lib.counts import COUNT_STATS, logger, process_count_stat from scripts.lib.zulip_tools import ENDC, WARNING @@ -21,6 +22,7 @@ class Command(BaseCommand): Run as a cron job that runs every hour.""" + @override def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument( "--time", @@ -37,6 +39,7 @@ class Command(BaseCommand): "--verbose", action="store_true", help="Print timing information to stdout." ) + @override def handle(self, *args: Any, **options: Any) -> None: try: os.mkdir(settings.ANALYTICS_LOCK_DIR) diff --git a/analytics/models.py b/analytics/models.py index 861c8c51f4..4ec03edf64 100644 --- a/analytics/models.py +++ b/analytics/models.py @@ -1,7 +1,11 @@ +# https://github.com/typeddjango/django-stubs/issues/1698 +# mypy: disable-error-code="explicit-override" + import datetime from django.db import models from django.db.models import Q, UniqueConstraint +from typing_extensions import override from zerver.lib.timestamp import floor_to_day from zerver.models import Realm, Stream, UserProfile @@ -16,6 +20,7 @@ class FillState(models.Model): STARTED = 2 state = models.PositiveSmallIntegerField() + @override def __str__(self) -> str: return f"{self.property} {self.end_time} {self.state}" @@ -58,6 +63,7 @@ class InstallationCount(BaseCount): ), ] + @override def __str__(self) -> str: return f"{self.property} {self.subgroup} {self.value}" @@ -86,6 +92,7 @@ class RealmCount(BaseCount): ) ] + @override def __str__(self) -> str: return f"{self.realm!r} {self.property} {self.subgroup} {self.value}" @@ -117,6 +124,7 @@ class UserCount(BaseCount): ) ] + @override def __str__(self) -> str: return f"{self.user!r} {self.property} {self.subgroup} {self.value}" @@ -148,5 +156,6 @@ class StreamCount(BaseCount): ) ] + @override def __str__(self) -> str: return f"{self.stream!r} {self.property} {self.subgroup} {self.value} {self.id}" diff --git a/analytics/tests/test_counts.py b/analytics/tests/test_counts.py index d182b71444..72b0a1531e 100644 --- a/analytics/tests/test_counts.py +++ b/analytics/tests/test_counts.py @@ -8,6 +8,7 @@ from django.db import models from django.db.models import Sum from django.utils.timezone import now as timezone_now from psycopg2.sql import SQL, Literal +from typing_extensions import override from analytics.lib.counts import ( COUNT_STATS, @@ -81,6 +82,7 @@ class AnalyticsTestCase(ZulipTestCase): TIME_ZERO = datetime(1988, 3, 14, tzinfo=timezone.utc) TIME_LAST_HOUR = TIME_ZERO - HOUR + @override def setUp(self) -> None: super().setUp() self.default_realm = do_create_realm( @@ -455,6 +457,7 @@ class TestProcessCountStat(AnalyticsTestCase): class TestCountStats(AnalyticsTestCase): + @override def setUp(self) -> None: super().setUp() # This tests two things for each of the queries/CountStats: Handling @@ -1543,6 +1546,7 @@ class TestDeleteStats(AnalyticsTestCase): class TestActiveUsersAudit(AnalyticsTestCase): + @override def setUp(self) -> None: super().setUp() self.user = self.create_user() @@ -1725,6 +1729,7 @@ class TestActiveUsersAudit(AnalyticsTestCase): class TestRealmActiveHumans(AnalyticsTestCase): + @override def setUp(self) -> None: super().setUp() self.stat = COUNT_STATS["realm_active_humans::day"] diff --git a/analytics/tests/test_stats_views.py b/analytics/tests/test_stats_views.py index e96a9a7705..1b8a1d6486 100644 --- a/analytics/tests/test_stats_views.py +++ b/analytics/tests/test_stats_views.py @@ -2,6 +2,7 @@ from datetime import datetime, timedelta, timezone from typing import List, Optional from django.utils.timezone import now as timezone_now +from typing_extensions import override from analytics.lib.counts import COUNT_STATS, CountStat from analytics.lib.time_utils import time_range @@ -68,6 +69,7 @@ class TestStatsEndpoint(ZulipTestCase): class TestGetChartData(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() self.realm = get_realm("zulip") diff --git a/analytics/tests/test_support_views.py b/analytics/tests/test_support_views.py index 4e5538deea..6ffa337965 100644 --- a/analytics/tests/test_support_views.py +++ b/analytics/tests/test_support_views.py @@ -4,6 +4,7 @@ from unittest import mock import orjson from django.utils.timezone import now as timezone_now +from typing_extensions import override from corporate.lib.stripe import add_months, update_sponsorship_status from corporate.models import Customer, CustomerPlan, LicenseLedger, get_customer_by_realm @@ -31,6 +32,7 @@ from zilencer.models import RemoteZulipServer class TestRemoteServerSupportEndpoint(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() diff --git a/confirmation/models.py b/confirmation/models.py index 57347086f7..33da8f69fa 100644 --- a/confirmation/models.py +++ b/confirmation/models.py @@ -16,7 +16,7 @@ from django.http import HttpRequest, HttpResponse from django.template.response import TemplateResponse from django.urls import reverse from django.utils.timezone import now as timezone_now -from typing_extensions import TypeAlias +from typing_extensions import TypeAlias, override from confirmation import settings as confirmation_settings from zerver.lib.types import UnspecifiedValue @@ -190,6 +190,7 @@ class Confirmation(models.Model): class Meta: unique_together = ("type", "confirmation_key") + @override def __str__(self) -> str: return f"{self.content_object!r}" diff --git a/corporate/models.py b/corporate/models.py index 14bba604b6..25e8bc2e7e 100644 --- a/corporate/models.py +++ b/corporate/models.py @@ -4,6 +4,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.db import models from django.db.models import CASCADE, Q +from typing_extensions import override from zerver.models import Realm, UserProfile from zilencer.models import RemoteZulipServer @@ -36,6 +37,7 @@ class Customer(models.Model): ) ] + @override def __str__(self) -> str: return f"{self.realm!r} {self.stripe_customer_id}" diff --git a/corporate/tests/test_stripe.py b/corporate/tests/test_stripe.py index 1834d05a3b..9371ed40d1 100644 --- a/corporate/tests/test_stripe.py +++ b/corporate/tests/test_stripe.py @@ -33,7 +33,7 @@ from django.core import signing from django.urls.resolvers import get_resolver from django.utils.crypto import get_random_string from django.utils.timezone import now as timezone_now -from typing_extensions import ParamSpec +from typing_extensions import ParamSpec, override from corporate.lib.stripe import ( DEFAULT_INVOICE_DAYS_UNTIL_DUE, @@ -383,6 +383,7 @@ def mock_stripe( class StripeTestCase(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() realm = get_realm("zulip") @@ -4171,6 +4172,7 @@ class EventStatusTest(StripeTestCase): class RequiresBillingAccessTest(StripeTestCase): + @override def setUp(self, *mocks: Mock) -> None: super().setUp() hamlet = self.example_user("hamlet") diff --git a/manage.py b/manage.py index 0e4234c13d..271db6d23b 100755 --- a/manage.py +++ b/manage.py @@ -13,6 +13,7 @@ setup_path() from django.core.management import ManagementUtility, get_commands from django.core.management.color import color_style +from typing_extensions import override from scripts.lib.zulip_tools import assert_not_running_as_root @@ -73,6 +74,7 @@ class FilteredManagementUtility(ManagementUtility): All other change are just code style differences to pass the Zulip linter. """ + @override def main_help_text(self, commands_only: bool = False) -> str: """Return the script's main help text, as a string.""" if commands_only: diff --git a/pyproject.toml b/pyproject.toml index 4c73c438fa..2f1adfeee2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ enable_error_code = [ "truthy-iterable", "ignore-without-code", "unused-awaitable", + "explicit-override", ] # Display the codes needed for # type: ignore[code] annotations. diff --git a/scripts/lib/supervisor.py b/scripts/lib/supervisor.py index fccf522977..aee690c0e1 100644 --- a/scripts/lib/supervisor.py +++ b/scripts/lib/supervisor.py @@ -4,8 +4,11 @@ from http.client import HTTPConnection from typing import Dict, List, Optional, Tuple, Union from xmlrpc import client +from typing_extensions import override + class UnixStreamHTTPConnection(HTTPConnection): + @override def connect(self) -> None: self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) connected = False @@ -28,6 +31,7 @@ class UnixStreamTransport(client.Transport): self.socket_path = socket_path super().__init__() + @override def make_connection( self, host: Union[Tuple[str, Dict[str, str]], str] ) -> UnixStreamHTTPConnection: diff --git a/tools/documentation_crawler/documentation_crawler/spiders/check_help_documentation.py b/tools/documentation_crawler/documentation_crawler/spiders/check_help_documentation.py index ace4809417..64eede178e 100644 --- a/tools/documentation_crawler/documentation_crawler/spiders/check_help_documentation.py +++ b/tools/documentation_crawler/documentation_crawler/spiders/check_help_documentation.py @@ -3,6 +3,8 @@ from posixpath import basename from typing import Any, List, Set from urllib.parse import urlparse +from typing_extensions import override + from .common.spiders import BaseDocumentationSpider @@ -21,6 +23,7 @@ class UnusedImagesLinterSpider(BaseDocumentationSpider): self.static_images: Set[str] = set() self.images_static_dir: str = get_images_dir(self.images_path) + @override def _is_external_url(self, url: str) -> bool: is_external = url.startswith("http") and self.start_urls[0] not in url if self._has_extension(url) and f"localhost:9981/{self.images_path}" in url: @@ -55,6 +58,7 @@ class APIDocumentationSpider(UnusedImagesLinterSpider): class PorticoDocumentationSpider(BaseDocumentationSpider): + @override def _is_external_url(self, url: str) -> bool: return ( not url.startswith("http://localhost:9981") diff --git a/tools/lib/template_parser.py b/tools/lib/template_parser.py index fb10c2f9a4..76b8f69816 100644 --- a/tools/lib/template_parser.py +++ b/tools/lib/template_parser.py @@ -1,5 +1,7 @@ from typing import Callable, List, Optional +from typing_extensions import override + class FormattedError(Exception): pass @@ -9,6 +11,7 @@ class TemplateParserError(Exception): def __init__(self, message: str) -> None: self.message = message + @override def __str__(self) -> str: return self.message diff --git a/tools/run-dev b/tools/run-dev index 1e19fdd632..95fc2ea22e 100755 --- a/tools/run-dev +++ b/tools/run-dev @@ -20,6 +20,7 @@ sanity_check.check_venv(__file__) from tornado import httpclient, httputil, web from tornado.platform.asyncio import AsyncIOMainLoop +from typing_extensions import override from tools.lib.test_script import add_provision_check_override_param, assert_provisioning_status_ok @@ -218,27 +219,35 @@ class BaseHandler(web.RequestHandler): headers.add(header, v) return headers + @override def get(self) -> None: pass + @override def head(self) -> None: pass + @override def post(self) -> None: pass + @override def put(self) -> None: pass + @override def patch(self) -> None: pass + @override def options(self) -> None: pass + @override def delete(self) -> None: pass + @override async def prepare(self) -> None: assert self.request.method is not None assert self.request.remote_ip is not None @@ -307,6 +316,7 @@ class Application(web.Application): enable_logging=enable_logging, ) + @override def log_request(self, handler: web.RequestHandler) -> None: if self.settings["enable_logging"]: super().log_request(handler) diff --git a/tools/tests/test_zulint_custom_rules.py b/tools/tests/test_zulint_custom_rules.py index 8deb85dbbd..c56b3d031d 100644 --- a/tools/tests/test_zulint_custom_rules.py +++ b/tools/tests/test_zulint_custom_rules.py @@ -3,6 +3,7 @@ from io import StringIO from unittest import TestCase from unittest.mock import patch +from typing_extensions import override from zulint.custom_rules import RuleList from tools.linter_lib.custom_check import non_py_rules, python_rules @@ -12,6 +13,7 @@ CHECK_MESSAGE = "Fix the corresponding rule in `tools/linter_lib/custom_check.py class TestRuleList(TestCase): + @override def setUp(self) -> None: all_rules = list(python_rules.rules) for rule in non_py_rules: diff --git a/zerver/apps.py b/zerver/apps.py index 5622586d1b..5ff2f301e4 100644 --- a/zerver/apps.py +++ b/zerver/apps.py @@ -5,6 +5,7 @@ from django.apps import AppConfig from django.conf import settings from django.core.cache import cache from django.db.models.signals import post_migrate +from typing_extensions import override def flush_cache(sender: Optional[AppConfig], **kwargs: Any) -> None: @@ -15,6 +16,7 @@ def flush_cache(sender: Optional[AppConfig], **kwargs: Any) -> None: class ZerverConfig(AppConfig): name: str = "zerver" + @override def ready(self) -> None: if settings.SENTRY_DSN: # nocoverage from zproject.config import get_config diff --git a/zerver/filters.py b/zerver/filters.py index fa04acccd6..85e60ff333 100644 --- a/zerver/filters.py +++ b/zerver/filters.py @@ -2,9 +2,11 @@ from typing import Any, Dict, Optional from django.http import HttpRequest from django.views.debug import SafeExceptionReporterFilter +from typing_extensions import override class ZulipExceptionReporterFilter(SafeExceptionReporterFilter): + @override def get_post_parameters(self, request: Optional[HttpRequest]) -> Dict[str, Any]: post_data = SafeExceptionReporterFilter.get_post_parameters(self, request) assert isinstance(post_data, dict) diff --git a/zerver/forms.py b/zerver/forms.py index a9f1c61de1..41eb032800 100644 --- a/zerver/forms.py +++ b/zerver/forms.py @@ -20,6 +20,7 @@ from django.utils.translation import gettext as _ from markupsafe import Markup from two_factor.forms import AuthenticationTokenForm as TwoFactorAuthenticationTokenForm from two_factor.utils import totp_digits +from typing_extensions import override from zerver.actions.user_settings import do_change_password from zerver.lib.email_validation import ( @@ -324,6 +325,7 @@ class LoggingSetPasswordForm(SetPasswordForm): return new_password + @override def save(self, commit: bool = True) -> UserProfile: assert isinstance(self.user, UserProfile) do_change_password(self.user, self.cleaned_data["new_password1"], commit=commit) @@ -340,6 +342,7 @@ def generate_password_reset_url( class ZulipPasswordResetForm(PasswordResetForm): + @override def save( self, domain_override: Optional[str] = None, @@ -452,9 +455,11 @@ class RateLimitedPasswordResetByEmail(RateLimitedObject): self.email = email super().__init__() + @override def key(self) -> str: return f"{type(self).__name__}:{self.email}" + @override def rules(self) -> List[Tuple[int, int]]: return settings.RATE_LIMITING_RULES["password_reset_form_by_email"] @@ -473,6 +478,7 @@ class CreateUserForm(forms.Form): class OurAuthenticationForm(AuthenticationForm): logger = logging.getLogger("zulip.auth.OurAuthenticationForm") + @override def clean(self) -> Dict[str, Any]: username = self.cleaned_data.get("username") password = self.cleaned_data.get("password") @@ -540,6 +546,7 @@ class OurAuthenticationForm(AuthenticationForm): return self.cleaned_data + @override def add_prefix(self, field_name: str) -> str: """Disable prefix, since Zulip doesn't use this Django forms feature (and django-two-factor does use it), and we'd like both to be @@ -561,6 +568,7 @@ class AuthenticationTokenForm(TwoFactorAuthenticationTokenForm): class MultiEmailField(forms.Field): + @override def to_python(self, emails: Optional[str]) -> List[str]: """Normalize data to a list of strings.""" if not emails: @@ -568,6 +576,7 @@ class MultiEmailField(forms.Field): return [email.strip() for email in emails.split(",")] + @override def validate(self, emails: List[str]) -> None: """Check if value consists only of valid emails.""" super().validate(emails) diff --git a/zerver/lib/async_utils.py b/zerver/lib/async_utils.py index a68985a8b1..e5a82a1126 100644 --- a/zerver/lib/async_utils.py +++ b/zerver/lib/async_utils.py @@ -1,5 +1,7 @@ import asyncio +from typing_extensions import override + class NoAutoCreateEventLoopPolicy(asyncio.DefaultEventLoopPolicy): """ @@ -12,5 +14,6 @@ class NoAutoCreateEventLoopPolicy(asyncio.DefaultEventLoopPolicy): accident. """ + @override def get_event_loop(self) -> asyncio.AbstractEventLoop: # nocoverage return asyncio.get_running_loop() diff --git a/zerver/lib/db.py b/zerver/lib/db.py index b325bb8c37..f8b008e04e 100644 --- a/zerver/lib/db.py +++ b/zerver/lib/db.py @@ -3,7 +3,7 @@ from typing import Any, Callable, Dict, Iterable, List, Mapping, Sequence, TypeV from psycopg2.extensions import connection, cursor from psycopg2.sql import Composable -from typing_extensions import TypeAlias +from typing_extensions import TypeAlias, override CursorObj = TypeVar("CursorObj", bound=cursor) Query: TypeAlias = Union[str, bytes, Composable] @@ -32,9 +32,11 @@ def wrapper_execute( class TimeTrackingCursor(cursor): """A psycopg2 cursor class that tracks the time spent executing queries.""" + @override def execute(self, query: Query, vars: Params = None) -> None: wrapper_execute(self, super().execute, query, vars) + @override def executemany(self, query: Query, vars: Iterable[Params]) -> None: # nocoverage wrapper_execute(self, super().executemany, query, vars) diff --git a/zerver/lib/email_mirror.py b/zerver/lib/email_mirror.py index 71d26b8a1c..4042bd328d 100644 --- a/zerver/lib/email_mirror.py +++ b/zerver/lib/email_mirror.py @@ -6,6 +6,7 @@ from email.message import EmailMessage from typing import Dict, List, Match, Optional, Tuple from django.conf import settings +from typing_extensions import override from zerver.actions.message_send import ( check_send_message, @@ -527,9 +528,11 @@ class RateLimitedRealmMirror(RateLimitedObject): self.realm = realm super().__init__() + @override def key(self) -> str: return f"{type(self).__name__}:{self.realm.string_id}" + @override def rules(self) -> List[Tuple[int, int]]: return settings.RATE_LIMITING_MIRROR_REALM_RULES diff --git a/zerver/lib/exceptions.py b/zerver/lib/exceptions.py index bbfa71dfbd..d1cc4c237d 100644 --- a/zerver/lib/exceptions.py +++ b/zerver/lib/exceptions.py @@ -4,6 +4,7 @@ from typing import Any, Dict, List, Optional, Tuple, Union from django.core.exceptions import ValidationError from django.utils.translation import gettext as _ from django_stubs_ext import StrPromise +from typing_extensions import override class ErrorCode(Enum): @@ -127,6 +128,7 @@ class JsonableError(Exception): def data(self) -> Dict[str, Any]: return dict(((f, getattr(self, f)) for f in self.data_fields), code=self.code.name) + @override def __str__(self) -> str: return self.msg @@ -147,6 +149,7 @@ class UnauthorizedError(JsonableError): raise AssertionError("Invalid www_authenticate value!") @property + @override def extra_headers(self) -> Dict[str, Any]: extra_headers_dict = super().extra_headers extra_headers_dict["WWW-Authenticate"] = self.www_authenticate @@ -161,6 +164,7 @@ class StreamDoesNotExistError(JsonableError): self.stream = stream @staticmethod + @override def msg_format() -> str: return _("Stream '{stream}' does not exist") @@ -173,6 +177,7 @@ class StreamWithIDDoesNotExistError(JsonableError): self.stream_id = stream_id @staticmethod + @override def msg_format() -> str: return _("Stream with ID '{stream_id}' does not exist") @@ -186,6 +191,7 @@ class CannotDeactivateLastUserError(JsonableError): self.entity = _("organization owner") if is_last_owner else _("user") @staticmethod + @override def msg_format() -> str: return _("Cannot deactivate the only {entity}.") @@ -198,6 +204,7 @@ class InvalidMarkdownIncludeStatementError(JsonableError): self.include_statement = include_statement @staticmethod + @override def msg_format() -> str: return _("Invalid Markdown include statement: {include_statement}") @@ -210,10 +217,12 @@ class RateLimitedError(JsonableError): self.secs_to_freedom = secs_to_freedom @staticmethod + @override def msg_format() -> str: return _("API usage exceeded rate limit") @property + @override def extra_headers(self) -> Dict[str, Any]: extra_headers_dict = super().extra_headers if self.secs_to_freedom is not None: @@ -222,6 +231,7 @@ class RateLimitedError(JsonableError): return extra_headers_dict @property + @override def data(self) -> Dict[str, Any]: data_dict = super().data data_dict["retry-after"] = self.secs_to_freedom @@ -233,6 +243,7 @@ class InvalidJSONError(JsonableError): code = ErrorCode.INVALID_JSON @staticmethod + @override def msg_format() -> str: return _("Malformed JSON") @@ -244,6 +255,7 @@ class OrganizationMemberRequiredError(JsonableError): pass @staticmethod + @override def msg_format() -> str: return _("Must be an organization member") @@ -255,6 +267,7 @@ class OrganizationAdministratorRequiredError(JsonableError): pass @staticmethod + @override def msg_format() -> str: return _("Must be an organization administrator") @@ -266,6 +279,7 @@ class OrganizationOwnerRequiredError(JsonableError): pass @staticmethod + @override def msg_format() -> str: return _("Must be an organization owner") @@ -279,6 +293,7 @@ class AuthenticationFailedError(JsonableError): pass @staticmethod + @override def msg_format() -> str: return _("Your username or password is incorrect") @@ -287,6 +302,7 @@ class UserDeactivatedError(AuthenticationFailedError): code: ErrorCode = ErrorCode.USER_DEACTIVATED @staticmethod + @override def msg_format() -> str: return _("Account is deactivated") @@ -295,6 +311,7 @@ class RealmDeactivatedError(AuthenticationFailedError): code: ErrorCode = ErrorCode.REALM_DEACTIVATED @staticmethod + @override def msg_format() -> str: return _("This organization has been deactivated") @@ -303,6 +320,7 @@ class RemoteServerDeactivatedError(AuthenticationFailedError): code: ErrorCode = ErrorCode.REALM_DEACTIVATED @staticmethod + @override def msg_format() -> str: return _( "The mobile push notification service registration for your server has been deactivated" @@ -313,6 +331,7 @@ class PasswordAuthDisabledError(AuthenticationFailedError): code: ErrorCode = ErrorCode.PASSWORD_AUTH_DISABLED @staticmethod + @override def msg_format() -> str: return _("Password authentication is disabled in this organization") @@ -321,6 +340,7 @@ class PasswordResetRequiredError(AuthenticationFailedError): code: ErrorCode = ErrorCode.PASSWORD_RESET_REQUIRED @staticmethod + @override def msg_format() -> str: return _("Your password has been disabled and needs to be reset") @@ -337,12 +357,14 @@ class InvalidAPIKeyError(JsonableError): pass @staticmethod + @override def msg_format() -> str: return _("Invalid API key") class InvalidAPIKeyFormatError(InvalidAPIKeyError): @staticmethod + @override def msg_format() -> str: return _("Malformed API key") @@ -381,6 +403,7 @@ class UnsupportedWebhookEventTypeError(WebhookError): self.event_type = event_type @staticmethod + @override def msg_format() -> str: return _( "The '{event_type}' event isn't currently supported by the {webhook_name} webhook; ignoring" @@ -401,6 +424,7 @@ class AnomalousWebhookPayloadError(WebhookError): code = ErrorCode.ANOMALOUS_WEBHOOK_PAYLOAD @staticmethod + @override def msg_format() -> str: return _("Unable to parse request: Did {webhook_name} generate this event?") @@ -424,6 +448,7 @@ class InvalidSubdomainError(JsonableError): pass @staticmethod + @override def msg_format() -> str: return _("Invalid subdomain") @@ -464,6 +489,7 @@ class AccessDeniedError(JsonableError): pass @staticmethod + @override def msg_format() -> str: return _("Access denied") @@ -502,6 +528,7 @@ class MessageMoveError(JsonableError): self.total_messages_allowed_to_move = total_messages_allowed_to_move @staticmethod + @override def msg_format() -> str: return _( "You only have permission to move the {total_messages_allowed_to_move}/{total_messages_in_topic} most recent messages in this topic." @@ -515,6 +542,7 @@ class ReactionExistsError(JsonableError): pass @staticmethod + @override def msg_format() -> str: return _("Reaction already exists.") @@ -526,6 +554,7 @@ class ReactionDoesNotExistError(JsonableError): pass @staticmethod + @override def msg_format() -> str: return _("Reaction doesn't exist.") diff --git a/zerver/lib/logging_util.py b/zerver/lib/logging_util.py index 7935639722..d52eea5c26 100644 --- a/zerver/lib/logging_util.py +++ b/zerver/lib/logging_util.py @@ -13,6 +13,7 @@ from django.conf import settings from django.core.cache import cache from django.http import HttpRequest from django.utils.timezone import now as timezone_now +from typing_extensions import override class _RateLimitFilter: @@ -108,11 +109,13 @@ class EmailLimiter(_RateLimitFilter): class ReturnTrue(logging.Filter): + @override def filter(self, record: logging.LogRecord) -> bool: return True class RequireReallyDeployed(logging.Filter): + @override def filter(self, record: logging.LogRecord) -> bool: return settings.PRODUCTION @@ -193,6 +196,7 @@ class ZulipFormatter(logging.Formatter): pieces.extend(["[%(zulip_origin)s]", "%(message)s"]) return " ".join(pieces) + @override def format(self, record: logging.LogRecord) -> str: if not hasattr(record, "zulip_decorated"): record.zulip_level_abbrev = abbrev_log_levelname(record.levelname) @@ -202,6 +206,7 @@ class ZulipFormatter(logging.Formatter): class ZulipWebhookFormatter(ZulipFormatter): + @override def _compute_fmt(self) -> str: basic = super()._compute_fmt() multiline = [ @@ -217,6 +222,7 @@ class ZulipWebhookFormatter(ZulipFormatter): ] return "\n".join(multiline) + @override def format(self, record: logging.LogRecord) -> str: request: Optional[HttpRequest] = getattr(record, "request", None) if request is None: diff --git a/zerver/lib/management.py b/zerver/lib/management.py index 8f1b7c91f0..e9de2e826e 100644 --- a/zerver/lib/management.py +++ b/zerver/lib/management.py @@ -10,6 +10,7 @@ from django.core import validators from django.core.exceptions import MultipleObjectsReturned, ValidationError from django.core.management.base import BaseCommand, CommandError, CommandParser from django.db.models import Q, QuerySet +from typing_extensions import override from zerver.lib.initial_password import initial_password from zerver.models import Client, Realm, UserProfile, get_client @@ -45,6 +46,7 @@ class CreateUserParameters: class ZulipBaseCommand(BaseCommand): # Fix support for multi-line usage + @override def create_parser(self, prog_name: str, subcommand: str, **kwargs: Any) -> CommandParser: parser = super().create_parser(prog_name, subcommand, **kwargs) parser.formatter_class = RawTextHelpFormatter diff --git a/zerver/lib/markdown/__init__.py b/zerver/lib/markdown/__init__.py index 6c8eccf71b..cefca166f5 100644 --- a/zerver/lib/markdown/__init__.py +++ b/zerver/lib/markdown/__init__.py @@ -50,7 +50,7 @@ from markdown.blockparser import BlockParser from markdown.extensions import codehilite, nl2br, sane_lists, tables from soupsieve import escape as css_escape from tlds import tld_set -from typing_extensions import TypeAlias +from typing_extensions import TypeAlias, override from zerver.lib import mention from zerver.lib.cache import cache_with_key @@ -524,6 +524,7 @@ class InlineImageProcessor(markdown.treeprocessors.Treeprocessor): super().__init__(zmd) self.zmd = zmd + @override def run(self, root: Element) -> None: # Get all URLs from the blob found_imgs = walk_tree(root, lambda e: e if e.tag == "img" else None) @@ -553,6 +554,7 @@ class InlineVideoProcessor(markdown.treeprocessors.Treeprocessor): super().__init__(zmd) self.zmd = zmd + @override def run(self, root: Element) -> None: # Get all URLs from the blob found_videos = walk_tree(root, lambda e: e if e.tag == "video" else None) @@ -573,6 +575,7 @@ class InlineVideoProcessor(markdown.treeprocessors.Treeprocessor): class BacktickInlineProcessor(markdown.inlinepatterns.BacktickInlineProcessor): """Return a `` element containing the matching text.""" + @override def handleMatch( # type: ignore[override] # https://github.com/python/mypy/issues/10197 self, m: Match[str], data: str ) -> Union[Tuple[None, None, None], Tuple[Element, int, int]]: @@ -1234,6 +1237,7 @@ class InlineInterestingLinkProcessor(markdown.treeprocessors.Treeprocessor): if info["remove"] is not None: info["parent"].remove(info["remove"]) + @override def run(self, root: Element) -> None: # Get all URLs from the blob found_urls = walk_tree_with_family(root, self.get_url_data) @@ -1391,6 +1395,7 @@ class CompiledInlineProcessor(markdown.inlinepatterns.InlineProcessor): class Timestamp(markdown.inlinepatterns.Pattern): + @override def handleMatch(self, match: Match[str]) -> Optional[Element]: time_input_string = match.group("time") try: @@ -1475,6 +1480,7 @@ class EmoticonTranslation(markdown.inlinepatterns.Pattern): super().__init__(pattern, zmd) self.zmd = zmd + @override def handleMatch(self, match: Match[str]) -> Optional[Element]: db_data: Optional[DbData] = self.zmd.zulip_db_data if db_data is None or not db_data.translate_emoticons: @@ -1490,6 +1496,7 @@ TEXT_PRESENTATION_RE = regex.compile(r"\P{Emoji_Presentation}\u20E3?") class UnicodeEmoji(CompiledInlineProcessor): + @override def handleMatch( # type: ignore[override] # https://github.com/python/mypy/issues/10197 self, match: Match[str], data: str ) -> Union[Tuple[None, None, None], Tuple[Element, int, int]]: @@ -1515,6 +1522,7 @@ class Emoji(markdown.inlinepatterns.Pattern): super().__init__(pattern, zmd) self.zmd = zmd + @override def handleMatch(self, match: Match[str]) -> Optional[Union[str, Element]]: orig_syntax = match.group("syntax") name = orig_syntax[1:-1] @@ -1543,6 +1551,7 @@ def content_has_emoji_syntax(content: str) -> bool: class Tex(markdown.inlinepatterns.Pattern): + @override def handleMatch(self, match: Match[str]) -> Union[str, Element]: rendered = render_tex(match.group("body"), is_inline=True) if rendered is not None: @@ -1633,6 +1642,7 @@ class CompiledPattern(markdown.inlinepatterns.Pattern): class AutoLink(CompiledPattern): + @override def handleMatch(self, match: Match[str]) -> ElementStringNone: url = match.group("url") db_data: Optional[DbData] = self.zmd.zulip_db_data @@ -1691,6 +1701,7 @@ class BlockQuoteProcessor(markdown.blockprocessors.BlockQuoteProcessor): RE = re.compile(r"(^|\n)(?!(?:[ ]{0,3}>\s*(?:$|\n))*(?:$|\n))[ ]{0,3}>[ ]?(.*)") # run() is very slightly forked from the base class; see notes below. + @override def run(self, parent: Element, blocks: List[str]) -> None: block = blocks.pop(0) m = self.RE.search(block) @@ -1716,6 +1727,7 @@ class BlockQuoteProcessor(markdown.blockprocessors.BlockQuoteProcessor): self.parser.parseChunk(quote, block) self.parser.state.reset() + @override def clean(self, line: str) -> str: # Silence all the mentions inside blockquotes line = mention.MENTIONS_RE.sub(lambda m: "@_**{}**".format(m.group("match")), line) @@ -1742,6 +1754,7 @@ class MarkdownListPreprocessor(markdown.preprocessors.Preprocessor): LI_RE = re.compile(r"^[ ]*([*+-]|\d\.)[ ]+(.*)", re.MULTILINE) + @override def run(self, lines: List[str]) -> List[str]: """Insert a newline between a paragraph and ulist if missing""" inserts = 0 @@ -1823,6 +1836,7 @@ class LinkifierPattern(CompiledInlineProcessor): super().__init__(compiled_re2, zmd) + @override def handleMatch( # type: ignore[override] # https://github.com/python/mypy/issues/10197 self, m: Match[str], data: str ) -> Union[Tuple[Element, int, int], Tuple[None, None, None]]: @@ -1843,6 +1857,7 @@ class LinkifierPattern(CompiledInlineProcessor): class UserMentionPattern(CompiledInlineProcessor): + @override def handleMatch( # type: ignore[override] # https://github.com/python/mypy/issues/10197 self, m: Match[str], data: str ) -> Union[Tuple[None, None, None], Tuple[Element, int, int]]: @@ -1906,6 +1921,7 @@ class UserMentionPattern(CompiledInlineProcessor): class UserGroupMentionPattern(CompiledInlineProcessor): + @override def handleMatch( # type: ignore[override] # https://github.com/python/mypy/issues/10197 self, m: Match[str], data: str ) -> Union[Tuple[None, None, None], Tuple[Element, int, int]]: @@ -1946,6 +1962,7 @@ class StreamPattern(CompiledInlineProcessor): stream_id = db_data.stream_names.get(name) return stream_id + @override def handleMatch( # type: ignore[override] # https://github.com/python/mypy/issues/10197 self, m: Match[str], data: str ) -> Union[Tuple[None, None, None], Tuple[Element, int, int]]: @@ -1977,6 +1994,7 @@ class StreamTopicPattern(CompiledInlineProcessor): stream_id = db_data.stream_names.get(name) return stream_id + @override def handleMatch( # type: ignore[override] # https://github.com/python/mypy/issues/10197 self, m: Match[str], data: str ) -> Union[Tuple[None, None, None], Tuple[Element, int, int]]: @@ -2041,6 +2059,7 @@ class AlertWordNotificationProcessor(markdown.preprocessors.Preprocessor): return True return False + @override def run(self, lines: List[str]) -> List[str]: db_data: Optional[DbData] = self.zmd.zulip_db_data if db_data is not None: @@ -2096,6 +2115,7 @@ class LinkInlineProcessor(markdown.inlinepatterns.LinkInlineProcessor): return el + @override def handleMatch( # type: ignore[override] # https://github.com/python/mypy/issues/10197 self, m: Match[str], data: str ) -> Union[Tuple[None, None, None], Tuple[Element, int, int]]: @@ -2156,6 +2176,7 @@ class ZulipMarkdown(markdown.Markdown): ) self.set_output_format("html") + @override def build_parser(self) -> markdown.Markdown: # Build the parser using selected default features from Python-Markdown. # The complete list of all available processors can be found in the diff --git a/zerver/lib/markdown/api_arguments_table_generator.py b/zerver/lib/markdown/api_arguments_table_generator.py index 4a681898c0..d933716629 100644 --- a/zerver/lib/markdown/api_arguments_table_generator.py +++ b/zerver/lib/markdown/api_arguments_table_generator.py @@ -6,6 +6,7 @@ import markdown from django.utils.html import escape as escape_html from markdown.extensions import Extension from markdown.preprocessors import Preprocessor +from typing_extensions import override from zerver.lib.markdown.priorities import PREPROCESSOR_PRIORITES from zerver.openapi.openapi import ( @@ -49,6 +50,7 @@ OBJECT_CODE_TEMPLATE = "{value}".strip() class MarkdownArgumentsTableGenerator(Extension): + @override def extendMarkdown(self, md: markdown.Markdown) -> None: md.preprocessors.register( APIArgumentsTablePreprocessor(md, self.getConfigs()), @@ -61,6 +63,7 @@ class APIArgumentsTablePreprocessor(Preprocessor): def __init__(self, md: markdown.Markdown, config: Mapping[str, Any]) -> None: super().__init__(md) + @override def run(self, lines: List[str]) -> List[str]: done = False while not done: diff --git a/zerver/lib/markdown/api_return_values_table_generator.py b/zerver/lib/markdown/api_return_values_table_generator.py index 71117afc2a..6280fb21e3 100644 --- a/zerver/lib/markdown/api_return_values_table_generator.py +++ b/zerver/lib/markdown/api_return_values_table_generator.py @@ -6,6 +6,7 @@ from typing import Any, Dict, List, Mapping, Optional import markdown from markdown.extensions import Extension from markdown.preprocessors import Preprocessor +from typing_extensions import override from zerver.lib.markdown.priorities import PREPROCESSOR_PRIORITES from zerver.openapi.openapi import check_deprecated_consistency, get_openapi_return_values @@ -16,6 +17,7 @@ REGEXP = re.compile(r"\{generate_return_values_table\|\s*(.+?)\s*\|\s*(.+)\s*\}" class MarkdownReturnValuesTableGenerator(Extension): + @override def extendMarkdown(self, md: markdown.Markdown) -> None: md.preprocessors.register( APIReturnValuesTablePreprocessor(md, self.getConfigs()), @@ -28,6 +30,7 @@ class APIReturnValuesTablePreprocessor(Preprocessor): def __init__(self, md: markdown.Markdown, config: Mapping[str, Any]) -> None: super().__init__(md) + @override def run(self, lines: List[str]) -> List[str]: done = False while not done: diff --git a/zerver/lib/markdown/fenced_code.py b/zerver/lib/markdown/fenced_code.py index e0d69836d7..607216ab38 100644 --- a/zerver/lib/markdown/fenced_code.py +++ b/zerver/lib/markdown/fenced_code.py @@ -86,6 +86,7 @@ from markdown.extensions.codehilite import CodeHiliteExtension, parse_hl_lines from markdown.preprocessors import Preprocessor from pygments.lexers import find_lexer_class_by_name from pygments.util import ClassNotFound +from typing_extensions import override from zerver.lib.exceptions import MarkdownRenderingError from zerver.lib.markdown.priorities import PREPROCESSOR_PRIORITES @@ -156,6 +157,7 @@ class FencedCodeExtension(Extension): for key, value in config.items(): self.setConfig(key, value) + @override def extendMarkdown(self, md: Markdown) -> None: """Add FencedBlockPreprocessor to the Markdown instance.""" md.registerExtension(self) @@ -268,6 +270,7 @@ class OuterHandler(ZulipBaseHandler): self.default_language = default_language super().__init__(processor, output) + @override def handle_line(self, line: str) -> None: check_for_new_fence( self.processor, self.output, line, self.run_content_validators, self.default_language @@ -287,6 +290,7 @@ class CodeHandler(ZulipBaseHandler): self.run_content_validators = run_content_validators super().__init__(processor, output, fence) + @override def done(self) -> None: # run content validators (if any) if self.run_content_validators: @@ -294,6 +298,7 @@ class CodeHandler(ZulipBaseHandler): validator(self.lines) super().done() + @override def format_text(self, text: str) -> str: return self.processor.format_code(self.lang, text) @@ -309,6 +314,7 @@ class QuoteHandler(ZulipBaseHandler): self.default_language = default_language super().__init__(processor, output, fence, process_contents=True) + @override def handle_line(self, line: str) -> None: if line.rstrip() == self.fence: self.done() @@ -317,6 +323,7 @@ class QuoteHandler(ZulipBaseHandler): self.processor, self.lines, line, default_language=self.default_language ) + @override def format_text(self, text: str) -> str: return self.processor.format_quote(text) @@ -332,17 +339,20 @@ class SpoilerHandler(ZulipBaseHandler): self.spoiler_header = spoiler_header super().__init__(processor, output, fence, process_contents=True) + @override def handle_line(self, line: str) -> None: if line.rstrip() == self.fence: self.done() else: check_for_new_fence(self.processor, self.lines, line) + @override def format_text(self, text: str) -> str: return self.processor.format_spoiler(self.spoiler_header, text) class TexHandler(ZulipBaseHandler): + @override def format_text(self, text: str) -> str: return self.processor.format_tex(text) @@ -411,6 +421,7 @@ class FencedBlockPreprocessor(Preprocessor): def pop(self) -> None: self.handlers.pop() + @override def run(self, lines: Iterable[str]) -> List[str]: """Match and store Fenced Code Blocks in the HtmlStash.""" diff --git a/zerver/lib/markdown/help_emoticon_translations_table.py b/zerver/lib/markdown/help_emoticon_translations_table.py index a443ad8eac..f054d6adb5 100644 --- a/zerver/lib/markdown/help_emoticon_translations_table.py +++ b/zerver/lib/markdown/help_emoticon_translations_table.py @@ -4,6 +4,7 @@ from typing import Any, List, Match from markdown import Markdown from markdown.extensions import Extension from markdown.preprocessors import Preprocessor +from typing_extensions import override from zerver.lib.emoji import EMOTICON_CONVERSIONS, name_to_codepoint from zerver.lib.markdown.priorities import PREPROCESSOR_PRIORITES @@ -38,6 +39,7 @@ ROW_HTML = """\ class EmoticonTranslationsHelpExtension(Extension): + @override def extendMarkdown(self, md: Markdown) -> None: """Add SettingHelpExtension to the Markdown instance.""" md.registerExtension(self) @@ -49,6 +51,7 @@ class EmoticonTranslationsHelpExtension(Extension): class EmoticonTranslation(Preprocessor): + @override def run(self, lines: List[str]) -> List[str]: for loc, line in enumerate(lines): match = REGEXP.search(line) diff --git a/zerver/lib/markdown/help_relative_links.py b/zerver/lib/markdown/help_relative_links.py index 796c06a192..2c1c45d55a 100644 --- a/zerver/lib/markdown/help_relative_links.py +++ b/zerver/lib/markdown/help_relative_links.py @@ -4,6 +4,7 @@ from typing import Any, List, Match from markdown import Markdown from markdown.extensions import Extension from markdown.preprocessors import Preprocessor +from typing_extensions import override from zerver.lib.markdown.priorities import PREPROCESSOR_PRIORITES @@ -133,6 +134,7 @@ LINK_TYPE_HANDLERS = { class RelativeLinksHelpExtension(Extension): + @override def extendMarkdown(self, md: Markdown) -> None: """Add RelativeLinksHelpExtension to the Markdown instance.""" md.registerExtension(self) @@ -150,6 +152,7 @@ def set_relative_help_links(value: bool) -> None: class RelativeLinks(Preprocessor): + @override def run(self, lines: List[str]) -> List[str]: done = False while not done: diff --git a/zerver/lib/markdown/help_settings_links.py b/zerver/lib/markdown/help_settings_links.py index 01f6811d5d..4cf41d2580 100644 --- a/zerver/lib/markdown/help_settings_links.py +++ b/zerver/lib/markdown/help_settings_links.py @@ -4,6 +4,7 @@ from typing import Any, List, Match from markdown import Markdown from markdown.extensions import Extension from markdown.preprocessors import Preprocessor +from typing_extensions import override from zerver.lib.markdown.priorities import PREPROCESSOR_PRIORITES @@ -131,6 +132,7 @@ def getMarkdown(setting_type_name: str, setting_name: str, setting_link: str) -> class SettingHelpExtension(Extension): + @override def extendMarkdown(self, md: Markdown) -> None: """Add SettingHelpExtension to the Markdown instance.""" md.registerExtension(self) @@ -146,6 +148,7 @@ def set_relative_settings_links(value: bool) -> None: class Setting(Preprocessor): + @override def run(self, lines: List[str]) -> List[str]: done = False while not done: diff --git a/zerver/lib/markdown/include.py b/zerver/lib/markdown/include.py index f694e751ef..394181064e 100644 --- a/zerver/lib/markdown/include.py +++ b/zerver/lib/markdown/include.py @@ -6,6 +6,7 @@ from xml.etree.ElementTree import Element from markdown import Extension, Markdown from markdown.blockparser import BlockParser from markdown.blockprocessors import BlockProcessor +from typing_extensions import override from zerver.lib.exceptions import InvalidMarkdownIncludeStatementError from zerver.lib.markdown.priorities import BLOCK_PROCESSOR_PRIORITIES @@ -16,6 +17,7 @@ class IncludeExtension(Extension): super().__init__() self.base_path = base_path + @override def extendMarkdown(self, md: Markdown) -> None: md.parser.blockprocessors.register( IncludeBlockProcessor(md.parser, self.base_path), @@ -31,6 +33,7 @@ class IncludeBlockProcessor(BlockProcessor): super().__init__(parser) self.base_path = base_path + @override def test(self, parent: Element, block: str) -> bool: return bool(self.RE.search(block)) @@ -46,6 +49,7 @@ class IncludeBlockProcessor(BlockProcessor): return "\n".join(lines) + @override def run(self, parent: Element, blocks: List[str]) -> None: self.parser.state.set("include") self.parser.parseChunk(parent, self.RE.sub(self.expand_include, blocks.pop(0))) diff --git a/zerver/lib/markdown/nested_code_blocks.py b/zerver/lib/markdown/nested_code_blocks.py index 56f81e7d71..5a177d46ab 100644 --- a/zerver/lib/markdown/nested_code_blocks.py +++ b/zerver/lib/markdown/nested_code_blocks.py @@ -3,12 +3,14 @@ from xml.etree.ElementTree import Element, SubElement import markdown from markdown.extensions import Extension +from typing_extensions import override from zerver.lib.markdown import ResultWithFamily, walk_tree_with_family from zerver.lib.markdown.priorities import PREPROCESSOR_PRIORITES class NestedCodeBlocksRenderer(Extension): + @override def extendMarkdown(self, md: markdown.Markdown) -> None: md.treeprocessors.register( NestedCodeBlocksRendererTreeProcessor(md, self.getConfigs()), @@ -21,6 +23,7 @@ class NestedCodeBlocksRendererTreeProcessor(markdown.treeprocessors.Treeprocesso def __init__(self, md: markdown.Markdown, config: Mapping[str, Any]) -> None: super().__init__(md) + @override def run(self, root: Element) -> None: code_tags = walk_tree_with_family(root, self.get_code_tags) nested_code_blocks = self.get_nested_code_blocks(code_tags) diff --git a/zerver/lib/markdown/static.py b/zerver/lib/markdown/static.py index f2d2a98eb2..685090e1b4 100644 --- a/zerver/lib/markdown/static.py +++ b/zerver/lib/markdown/static.py @@ -4,11 +4,13 @@ from xml.etree.ElementTree import Element import markdown from django.contrib.staticfiles.storage import staticfiles_storage from markdown.extensions import Extension +from typing_extensions import override from zerver.lib.markdown.priorities import PREPROCESSOR_PRIORITES class MarkdownStaticImagesGenerator(Extension): + @override def extendMarkdown(self, md: markdown.Markdown) -> None: md.treeprocessors.register( StaticImageProcessor(md), @@ -22,6 +24,7 @@ class StaticImageProcessor(markdown.treeprocessors.Treeprocessor): Rewrite img tags which refer to /static/ to use staticfiles """ + @override def run(self, root: Element) -> None: for img in root.iter("img"): url = img.get("src") diff --git a/zerver/lib/markdown/tabbed_sections.py b/zerver/lib/markdown/tabbed_sections.py index 2706c1fe1e..d598225c38 100644 --- a/zerver/lib/markdown/tabbed_sections.py +++ b/zerver/lib/markdown/tabbed_sections.py @@ -4,6 +4,7 @@ from typing import Any, Dict, List, Mapping, Optional import markdown from markdown.extensions import Extension from markdown.preprocessors import Preprocessor +from typing_extensions import override from zerver.lib.markdown.priorities import PREPROCESSOR_PRIORITES @@ -105,6 +106,7 @@ TAB_SECTION_LABELS = { class TabbedSectionsGenerator(Extension): + @override def extendMarkdown(self, md: markdown.Markdown) -> None: md.preprocessors.register( TabbedSectionsPreprocessor(md, self.getConfigs()), @@ -117,6 +119,7 @@ class TabbedSectionsPreprocessor(Preprocessor): def __init__(self, md: markdown.Markdown, config: Mapping[str, Any]) -> None: super().__init__(md) + @override def run(self, lines: List[str]) -> List[str]: tab_section = self.parse_tabs(lines) while tab_section: diff --git a/zerver/lib/narrow.py b/zerver/lib/narrow.py index 84c527ed4c..4f261bc0e3 100644 --- a/zerver/lib/narrow.py +++ b/zerver/lib/narrow.py @@ -43,7 +43,7 @@ from sqlalchemy.sql import ( ) from sqlalchemy.sql.selectable import SelectBase from sqlalchemy.types import ARRAY, Boolean, Integer, Text -from typing_extensions import TypeAlias +from typing_extensions import TypeAlias, override from zerver.lib.addressee import get_user_profiles, get_user_profiles_by_ids from zerver.lib.exceptions import ErrorCode, JsonableError @@ -204,6 +204,7 @@ class BadNarrowOperatorError(JsonableError): self.desc: str = desc @staticmethod + @override def msg_format() -> str: return _("Invalid narrow operator: {desc}") diff --git a/zerver/lib/notes.py b/zerver/lib/notes.py index b46bbde7a0..e65240dd8c 100644 --- a/zerver/lib/notes.py +++ b/zerver/lib/notes.py @@ -2,6 +2,8 @@ import weakref from abc import ABCMeta, abstractmethod from typing import Any, ClassVar, Generic, MutableMapping, TypeVar +from typing_extensions import override + _KeyT = TypeVar("_KeyT") _DataT = TypeVar("_DataT") @@ -28,6 +30,7 @@ class BaseNotes(Generic[_KeyT, _DataT], metaclass=ABCMeta): __notes_map: ClassVar[MutableMapping[Any, Any]] + @override def __init_subclass__(cls, **kwargs: object) -> None: super().__init_subclass__(**kwargs) if not hasattr(cls, "__notes_map"): diff --git a/zerver/lib/outgoing_http.py b/zerver/lib/outgoing_http.py index b6b71b9034..cd5bff0e5f 100644 --- a/zerver/lib/outgoing_http.py +++ b/zerver/lib/outgoing_http.py @@ -1,6 +1,7 @@ from typing import Any, Dict, Optional, Union import requests +from typing_extensions import override from urllib3.util import Retry @@ -35,10 +36,12 @@ class OutgoingHTTPAdapter(requests.adapters.HTTPAdapter): self.timeout = timeout super().__init__(max_retries=max_retries) + @override def send(self, *args: Any, **kwargs: Any) -> requests.Response: if kwargs.get("timeout") is None: kwargs["timeout"] = self.timeout return super().send(*args, **kwargs) + @override def proxy_headers(self, proxy: str) -> Dict[str, str]: return {"X-Smokescreen-Role": self.role} diff --git a/zerver/lib/outgoing_webhook.py b/zerver/lib/outgoing_webhook.py index 595cca43c0..cf8f604d7e 100644 --- a/zerver/lib/outgoing_webhook.py +++ b/zerver/lib/outgoing_webhook.py @@ -9,6 +9,7 @@ import requests from django.conf import settings from django.utils.translation import gettext as _ from requests import Response +from typing_extensions import override from version import ZULIP_VERSION from zerver.actions.message_send import check_send_message @@ -52,6 +53,7 @@ class OutgoingWebhookServiceInterface(metaclass=abc.ABCMeta): class GenericOutgoingWebhookService(OutgoingWebhookServiceInterface): + @override def make_request( self, base_url: str, event: Dict[str, Any], realm: Realm ) -> Optional[Response]: @@ -82,6 +84,7 @@ class GenericOutgoingWebhookService(OutgoingWebhookServiceInterface): return self.session.post(base_url, json=request_data) + @override def process_success(self, response_json: Dict[str, Any]) -> Optional[Dict[str, Any]]: if "response_not_required" in response_json and response_json["response_not_required"]: return None @@ -103,6 +106,7 @@ class GenericOutgoingWebhookService(OutgoingWebhookServiceInterface): class SlackOutgoingWebhookService(OutgoingWebhookServiceInterface): + @override def make_request( self, base_url: str, event: Dict[str, Any], realm: Realm ) -> Optional[Response]: @@ -142,6 +146,7 @@ class SlackOutgoingWebhookService(OutgoingWebhookServiceInterface): ] return self.session.post(base_url, data=request_data) + @override def process_success(self, response_json: Dict[str, Any]) -> Optional[Dict[str, Any]]: if "text" in response_json: content = response_json["text"] diff --git a/zerver/lib/push_notifications.py b/zerver/lib/push_notifications.py index e6d9764ba9..db11d47be7 100644 --- a/zerver/lib/push_notifications.py +++ b/zerver/lib/push_notifications.py @@ -29,7 +29,7 @@ from django.db.models import F, Q from django.utils.timezone import now as timezone_now from django.utils.translation import gettext as _ from django.utils.translation import override as override_language -from typing_extensions import TypeAlias +from typing_extensions import TypeAlias, override from zerver.lib.avatar import absolute_avatar_url from zerver.lib.emoji_utils import hex_codepoint_to_emoji @@ -115,6 +115,7 @@ class UserPushIdentityCompat: assert self.user_id is not None and self.user_uuid is not None return Q(user_uuid=self.user_uuid) | Q(user_id=self.user_id) + @override def __str__(self) -> str: result = "" if self.user_id is not None: @@ -124,6 +125,7 @@ class UserPushIdentityCompat: return result + @override def __eq__(self, other: object) -> bool: if isinstance(other, UserPushIdentityCompat): return self.user_id == other.user_id and self.user_uuid == other.user_uuid diff --git a/zerver/lib/queue.py b/zerver/lib/queue.py index dd54e51e79..a00a1b18c4 100644 --- a/zerver/lib/queue.py +++ b/zerver/lib/queue.py @@ -18,7 +18,7 @@ from pika.adapters.blocking_connection import BlockingChannel from pika.channel import Channel from pika.spec import Basic from tornado import ioloop -from typing_extensions import TypeAlias +from typing_extensions import TypeAlias, override from zerver.lib.utils import assert_is_not_none @@ -147,6 +147,7 @@ class QueueClient(Generic[ChannelT], metaclass=ABCMeta): class SimpleQueueClient(QueueClient[BlockingChannel]): connection: Optional[pika.BlockingConnection] + @override def _connect(self) -> None: start = time.time() self.connection = pika.BlockingConnection(self._get_parameters()) @@ -154,6 +155,7 @@ class SimpleQueueClient(QueueClient[BlockingChannel]): self.channel.basic_qos(prefetch_count=self.prefetch) self.log.info("SimpleQueueClient connected (connecting took %.3fs)", time.time() - start) + @override def _reconnect(self) -> None: self.connection = None self.channel = None @@ -164,6 +166,7 @@ class SimpleQueueClient(QueueClient[BlockingChannel]): if self.connection is not None: self.connection.close() + @override def ensure_queue(self, queue_name: str, callback: Callable[[BlockingChannel], object]) -> None: """Ensure that a given queue has been declared, and then call the callback with no arguments.""" @@ -271,6 +274,7 @@ class TornadoQueueClient(QueueClient[Channel]): self._on_open_cbs: List[Callable[[Channel], None]] = [] self._connection_failure_count = 0 + @override def _connect(self) -> None: self.log.info("Beginning TornadoQueueClient connection") self.connection = ExceptionFreeTornadoConnection( @@ -280,6 +284,7 @@ class TornadoQueueClient(QueueClient[Channel]): on_close_callback=self._on_connection_closed, ) + @override def _reconnect(self) -> None: self.connection = None self.channel = None @@ -350,6 +355,7 @@ class TornadoQueueClient(QueueClient[Channel]): self.connection.close() self.connection = None + @override def ensure_queue(self, queue_name: str, callback: Callable[[Channel], object]) -> None: def set_qos(frame: Any) -> None: assert self.channel is not None diff --git a/zerver/lib/rate_limiter.py b/zerver/lib/rate_limiter.py index d7977cd3a1..c086b1fe3e 100644 --- a/zerver/lib/rate_limiter.py +++ b/zerver/lib/rate_limiter.py @@ -9,6 +9,7 @@ import redis from circuitbreaker import CircuitBreakerError, circuit from django.conf import settings from django.http import HttpRequest +from typing_extensions import override from zerver.lib.cache import cache_with_key from zerver.lib.exceptions import RateLimitedError @@ -122,9 +123,11 @@ class RateLimitedUser(RateLimitedObject): backend = None super().__init__(backend=backend) + @override def key(self) -> str: return f"{type(self).__name__}:{self.user_id}:{self.domain}" + @override def rules(self) -> List[Tuple[int, int]]: # user.rate_limits are general limits, applicable to the domain 'api_by_user' if self.rate_limits != "" and self.domain == "api_by_user": @@ -146,10 +149,12 @@ class RateLimitedIPAddr(RateLimitedObject): backend = None super().__init__(backend=backend) + @override def key(self) -> str: # The angle brackets are important since IPv6 addresses contain :. return f"{type(self).__name__}:<{self.ip_addr}>:{self.domain}" + @override def rules(self) -> List[Tuple[int, int]]: return rules[self.domain] @@ -257,6 +262,7 @@ class TornadoInMemoryRateLimiterBackend(RateLimiterBackend): return False, 0.0 @classmethod + @override def get_api_calls_left( cls, entity_key: str, range_seconds: int, max_calls: int ) -> Tuple[int, float]: @@ -272,21 +278,25 @@ class TornadoInMemoryRateLimiterBackend(RateLimiterBackend): return int(calls_remaining), reset_time - now @classmethod + @override def block_access(cls, entity_key: str, seconds: int) -> None: now = time.time() cls.timestamps_blocked_until[entity_key] = now + seconds @classmethod + @override def unblock_access(cls, entity_key: str) -> None: del cls.timestamps_blocked_until[entity_key] @classmethod + @override def clear_history(cls, entity_key: str) -> None: for reset_times_for_rule in cls.reset_times.values(): reset_times_for_rule.pop(entity_key, None) cls.timestamps_blocked_until.pop(entity_key, None) @classmethod + @override def rate_limit_entity( cls, entity_key: str, rules: List[Tuple[int, int]], max_api_calls: int, max_api_window: int ) -> Tuple[bool, float]: @@ -317,6 +327,7 @@ class RedisRateLimiterBackend(RateLimiterBackend): ] @classmethod + @override def block_access(cls, entity_key: str, seconds: int) -> None: """Manually blocks an entity for the desired number of seconds""" _, _, blocking_key = cls.get_keys(entity_key) @@ -326,16 +337,19 @@ class RedisRateLimiterBackend(RateLimiterBackend): pipe.execute() @classmethod + @override def unblock_access(cls, entity_key: str) -> None: _, _, blocking_key = cls.get_keys(entity_key) client.delete(blocking_key) @classmethod + @override def clear_history(cls, entity_key: str) -> None: for key in cls.get_keys(entity_key): client.delete(key) @classmethod + @override def get_api_calls_left( cls, entity_key: str, range_seconds: int, max_calls: int ) -> Tuple[int, float]: @@ -467,6 +481,7 @@ class RedisRateLimiterBackend(RateLimiterBackend): continue @classmethod + @override def rate_limit_entity( cls, entity_key: str, rules: List[Tuple[int, int]], max_api_calls: int, max_api_window: int ) -> Tuple[bool, float]: @@ -501,9 +516,11 @@ class RateLimitedSpectatorAttachmentAccessByFile(RateLimitedObject): self.path_id = path_id super().__init__() + @override def key(self) -> str: return f"{type(self).__name__}:{self.path_id}" + @override def rules(self) -> List[Tuple[int, int]]: return settings.RATE_LIMITING_RULES["spectator_attachment_access_by_file"] diff --git a/zerver/lib/request.py b/zerver/lib/request.py index 932597e049..12a8083e85 100644 --- a/zerver/lib/request.py +++ b/zerver/lib/request.py @@ -24,7 +24,7 @@ from django.conf import settings from django.core.exceptions import ValidationError from django.http import HttpRequest, HttpResponse from django.utils.translation import gettext as _ -from typing_extensions import Concatenate, ParamSpec +from typing_extensions import Concatenate, ParamSpec, override from zerver.lib import rate_limiter from zerver.lib.exceptions import ErrorCode, InvalidJSONError, JsonableError @@ -74,6 +74,7 @@ class RequestNotes(BaseNotes[HttpRequest, "RequestNotes"]): is_webhook_view: bool = False @classmethod + @override def init_notes(cls) -> "RequestNotes": return RequestNotes() @@ -87,6 +88,7 @@ class RequestConfusingParamsError(JsonableError): self.var_name2: str = var_name2 @staticmethod + @override def msg_format() -> str: return _("Can't decide between '{var_name1}' and '{var_name2}' arguments") @@ -99,6 +101,7 @@ class RequestVariableMissingError(JsonableError): self.var_name: str = var_name @staticmethod + @override def msg_format() -> str: return _("Missing '{var_name}' argument") @@ -112,6 +115,7 @@ class RequestVariableConversionError(JsonableError): self.bad_value = bad_value @staticmethod + @override def msg_format() -> str: return _("Bad value for '{var_name}': {bad_value}") diff --git a/zerver/lib/response.py b/zerver/lib/response.py index 83b313249f..98807fc707 100644 --- a/zerver/lib/response.py +++ b/zerver/lib/response.py @@ -2,6 +2,7 @@ from typing import Any, Dict, Iterator, List, Mapping, Optional import orjson from django.http import HttpRequest, HttpResponse, HttpResponseNotAllowed +from typing_extensions import override from zerver.lib.exceptions import JsonableError, UnauthorizedError @@ -36,6 +37,7 @@ class MutableJsonResponse(HttpResponse): # is used here to encompass all of those return values. # See https://github.com/typeddjango/django-stubs/commit/799b41fe47cfe2e56be33eee8cfbaf89a9853a8e # and https://github.com/python/mypy/issues/3004. + @override # type: ignore[explicit-override] # https://github.com/python/mypy/issues/15900 @property def content(self) -> Any: """Get content for the response. If the content hasn't been @@ -68,6 +70,7 @@ class MutableJsonResponse(HttpResponse): # property, so in order to not break the implementation of the superclass with # our lazy content generation, we override the iterator to access `self.content` # through our getter. + @override def __iter__(self) -> Iterator[bytes]: return iter([self.content]) diff --git a/zerver/lib/safe_session_cached_db.py b/zerver/lib/safe_session_cached_db.py index 1d4bd5a667..9da827133d 100644 --- a/zerver/lib/safe_session_cached_db.py +++ b/zerver/lib/safe_session_cached_db.py @@ -2,6 +2,7 @@ from typing import Optional from django.contrib.sessions.backends.cached_db import SessionStore as CachedDbSessionStore from django.db.transaction import get_connection +from typing_extensions import override class SessionStore(CachedDbSessionStore): @@ -16,10 +17,12 @@ class SessionStore(CachedDbSessionStore): """ + @override def save(self, must_create: bool = False) -> None: assert not get_connection().in_atomic_block super().save(must_create) + @override def delete(self, session_key: Optional[str] = None) -> None: assert not get_connection().in_atomic_block super().delete(session_key) diff --git a/zerver/lib/sqlalchemy_utils.py b/zerver/lib/sqlalchemy_utils.py index 9bc78b8fe0..d4b7d3d186 100644 --- a/zerver/lib/sqlalchemy_utils.py +++ b/zerver/lib/sqlalchemy_utils.py @@ -4,6 +4,7 @@ from typing import Iterator, Optional import sqlalchemy from django.db import connection from sqlalchemy.engine import Connection, Engine +from typing_extensions import override from zerver.lib.db import TimeTrackingConnection @@ -11,6 +12,7 @@ from zerver.lib.db import TimeTrackingConnection # This is a Pool that doesn't close connections. Therefore it can be used with # existing Django database connections. class NonClosingPool(sqlalchemy.pool.NullPool): + @override def status(self) -> str: return "NonClosingPool" diff --git a/zerver/lib/storage.py b/zerver/lib/storage.py index be35562094..04245b89ca 100644 --- a/zerver/lib/storage.py +++ b/zerver/lib/storage.py @@ -8,6 +8,7 @@ from django.conf import settings from django.contrib.staticfiles.storage import ManifestStaticFilesStorage from django.core.files.base import File from django.core.files.storage import FileSystemStorage +from typing_extensions import override if settings.DEBUG: from django.contrib.staticfiles.finders import find @@ -22,6 +23,7 @@ else: class IgnoreBundlesManifestStaticFilesStorage(ManifestStaticFilesStorage): + @override def hashed_name( self, name: str, content: Optional["File[bytes]"] = None, filename: Optional[str] = None ) -> str: diff --git a/zerver/lib/test_classes.py b/zerver/lib/test_classes.py index 9bbaa20ba8..3e1c4fc252 100644 --- a/zerver/lib/test_classes.py +++ b/zerver/lib/test_classes.py @@ -45,6 +45,7 @@ from django.utils.module_loading import import_string from django.utils.timezone import now as timezone_now from fakeldap import MockLDAP from two_factor.plugins.phonenumber.models import PhoneDevice +from typing_extensions import override from corporate.models import Customer, CustomerPlan, LicenseLedger from zerver.actions.message_send import check_send_message, check_send_stream_message @@ -133,6 +134,7 @@ class UploadSerializeMixin(SerializeMixin): lockfile = "var/upload_lock" @classmethod + @override def setUpClass(cls: Any) -> None: if not os.path.exists(cls.lockfile): with open(cls.lockfile, "w"): # nocoverage - rare locking case @@ -149,6 +151,7 @@ class ZulipTestCaseMixin(SimpleTestCase): # expectation. expected_console_output: Optional[str] = None + @override def setUp(self) -> None: super().setUp() self.API_KEYS: Dict[str, str] = {} @@ -157,6 +160,7 @@ class ZulipTestCaseMixin(SimpleTestCase): bounce_key_prefix_for_testing(test_name) bounce_redis_key_prefix_for_testing(test_name) + @override def tearDown(self) -> None: super().tearDown() # Important: we need to clear event queues to avoid leaking data to future tests. @@ -179,6 +183,7 @@ class ZulipTestCaseMixin(SimpleTestCase): def get_user_from_email(self, email: str, realm: Realm) -> UserProfile: return get_user(email, realm) + @override def run(self, result: Optional[TestResult] = None) -> Optional[TestResult]: # nocoverage if not settings.BAN_CONSOLE_OUTPUT and self.expected_console_output is None: return super().run(result) @@ -1975,10 +1980,12 @@ class ZulipTransactionTestCase(ZulipTestCaseMixin, TransactionTestCase): ZulipTransactionTestCase tests if they leak state. """ + @override def setUp(self) -> None: super().setUp() self.models_ids_set = dict(get_row_ids_in_all_tables()) + @override def tearDown(self) -> None: """Verifies that the test did not adjust the set of rows in the test database. This is a sanity check to help ensure that tests @@ -2029,6 +2036,7 @@ class WebhookTestCase(ZulipTestCase): def test_user(self) -> UserProfile: return self.get_user_from_email(self.TEST_USER_EMAIL, get_realm("zulip")) + @override def setUp(self) -> None: super().setUp() self.url = self.build_webhook_url() @@ -2274,6 +2282,7 @@ class MigrationsTestCase(ZulipTestCase): # nocoverage migrate_from: Optional[str] = None migrate_to: Optional[str] = None + @override def setUp(self) -> None: assert ( self.migrate_from and self.migrate_to diff --git a/zerver/lib/test_console_output.py b/zerver/lib/test_console_output.py index 66466a8ce4..7b92a4c03c 100644 --- a/zerver/lib/test_console_output.py +++ b/zerver/lib/test_console_output.py @@ -7,6 +7,8 @@ from io import SEEK_SET, TextIOWrapper from types import TracebackType from typing import IO, TYPE_CHECKING, Iterable, Iterator, List, Optional, Type +from typing_extensions import override + if TYPE_CHECKING: from _typeshed import ReadableBuffer @@ -50,77 +52,99 @@ class WrappedIO(IO[bytes]): self.extra_output_finder = extra_output_finder @property + @override def mode(self) -> str: return self.stream.mode @property + @override def name(self) -> str: return self.stream.name + @override def close(self) -> None: pass @property + @override def closed(self) -> bool: return self.stream.closed + @override def fileno(self) -> int: return self.stream.fileno() + @override def flush(self) -> None: self.stream.flush() + @override def isatty(self) -> bool: return self.stream.isatty() + @override def read(self, n: int = -1) -> bytes: return self.stream.read(n) + @override def readable(self) -> bool: return self.stream.readable() + @override def readline(self, limit: int = -1) -> bytes: return self.stream.readline(limit) + @override def readlines(self, hint: int = -1) -> List[bytes]: return self.stream.readlines(hint) + @override def seek(self, offset: int, whence: int = SEEK_SET) -> int: return self.stream.seek(offset, whence) + @override def seekable(self) -> bool: return self.stream.seekable() + @override def tell(self) -> int: return self.stream.tell() + @override def truncate(self, size: Optional[int] = None) -> int: return self.truncate(size) + @override def writable(self) -> bool: return self.stream.writable() + @override def write(self, data: "ReadableBuffer") -> int: num_chars = self.stream.write(data) self.extra_output_finder.find_extra_output(bytes(data)) return num_chars + @override def writelines(self, data: "Iterable[ReadableBuffer]") -> None: data, data_copy = itertools.tee(data) self.stream.writelines(data) lines = b"".join(data_copy) self.extra_output_finder.find_extra_output(lines) + @override def __next__(self) -> bytes: return next(self.stream) + @override def __iter__(self) -> Iterator[bytes]: return self + @override def __enter__(self) -> IO[bytes]: self.stream.__enter__() return self + @override def __exit__( self, exc_type: Optional[Type[BaseException]], diff --git a/zerver/lib/test_helpers.py b/zerver/lib/test_helpers.py index 29a876041d..e3a8910d8f 100644 --- a/zerver/lib/test_helpers.py +++ b/zerver/lib/test_helpers.py @@ -38,6 +38,7 @@ from django.test import override_settings from django.urls import URLResolver from moto.s3 import mock_s3 from mypy_boto3_s3.service_resource import Bucket +from typing_extensions import override from zerver.actions.realm_settings import do_set_realm_user_default_setting from zerver.actions.user_settings import do_change_user_setting @@ -372,6 +373,7 @@ class HostRequestMock(HttpRequest): ), ) + @override def get_host(self) -> str: return self.host diff --git a/zerver/lib/test_runner.py b/zerver/lib/test_runner.py index 85f5cc2771..3bd84da3f3 100644 --- a/zerver/lib/test_runner.py +++ b/zerver/lib/test_runner.py @@ -14,7 +14,7 @@ from django.test import runner as django_runner from django.test.runner import DiscoverRunner from django.test.signals import template_rendered from returns.curry import partial -from typing_extensions import TypeAlias +from typing_extensions import TypeAlias, override from scripts.lib.zulip_tools import ( TEMPLATE_DATABASE_DIR, @@ -61,24 +61,29 @@ class TextTestResult(runner.TextTestResult): def addInstrumentation(self, test: unittest.TestCase, data: Dict[str, Any]) -> None: append_instrumentation_data(data) + @override def startTest(self, test: unittest.TestCase) -> None: TestResult.startTest(self, test) self.stream.write(f"Running {test.id()}\n") self.stream.flush() + @override def addSuccess(self, *args: Any, **kwargs: Any) -> None: TestResult.addSuccess(self, *args, **kwargs) + @override def addError(self, *args: Any, **kwargs: Any) -> None: TestResult.addError(self, *args, **kwargs) test_name = args[0].id() self.failed_tests.append(test_name) + @override def addFailure(self, *args: Any, **kwargs: Any) -> None: TestResult.addFailure(self, *args, **kwargs) test_name = args[0].id() self.failed_tests.append(test_name) + @override def addSkip(self, test: unittest.TestCase, reason: str) -> None: TestResult.addSkip(self, test, reason) self.stream.write(f"** Skipping {test.id()}: {reason}\n") @@ -259,6 +264,7 @@ class Runner(DiscoverRunner): self.shallow_tested_templates: Set[str] = set() template_rendered.connect(self.on_template_rendered) + @override def get_resultclass(self) -> Optional[Type[TextTestResult]]: return TextTestResult @@ -275,6 +281,7 @@ class Runner(DiscoverRunner): def get_shallow_tested_templates(self) -> Set[str]: return self.shallow_tested_templates + @override def setup_test_environment(self, *args: Any, **kwargs: Any) -> Any: settings.DATABASES["default"]["NAME"] = BACKEND_DATABASE_TEMPLATE # We create/destroy the test databases in run_tests to avoid @@ -298,6 +305,7 @@ class Runner(DiscoverRunner): return super().setup_test_environment(*args, **kwargs) + @override def teardown_test_environment(self, *args: Any, **kwargs: Any) -> Any: # The test environment setup clones the zulip_test_template # database, creating databases with names: @@ -347,6 +355,7 @@ class Runner(DiscoverRunner): break check_import_error(test_name) + @override def run_tests( self, test_labels: List[str], diff --git a/zerver/lib/timeout.py b/zerver/lib/timeout.py index 6495719ab4..dba89cc74f 100644 --- a/zerver/lib/timeout.py +++ b/zerver/lib/timeout.py @@ -6,12 +6,15 @@ import time from types import TracebackType from typing import Callable, Optional, Tuple, Type, TypeVar +from typing_extensions import override + # Based on https://code.activestate.com/recipes/483752/ class TimeoutExpiredError(Exception): """Exception raised when a function times out.""" + @override def __str__(self) -> str: return "Function call timed out." @@ -49,6 +52,7 @@ def timeout(timeout: float, func: Callable[[], ResultT]) -> ResultT: # if this is the only thread left. self.daemon = True + @override def run(self) -> None: try: self.result = func() diff --git a/zerver/lib/upload/local.py b/zerver/lib/upload/local.py index 84e8c04f40..4cd819deb0 100644 --- a/zerver/lib/upload/local.py +++ b/zerver/lib/upload/local.py @@ -7,6 +7,7 @@ from datetime import datetime from typing import IO, Any, BinaryIO, Callable, Iterator, Literal, Optional, Tuple from django.conf import settings +from typing_extensions import override from zerver.lib.avatar_hash import user_avatar_path from zerver.lib.timestamp import timestamp_to_datetime @@ -66,9 +67,11 @@ def delete_local_file(type: Literal["avatars", "files"], path: str) -> bool: class LocalUploadBackend(ZulipUploadBackend): + @override def get_public_upload_root_url(self) -> str: return "/user_avatars/" + @override def generate_message_upload_path(self, realm_id: str, uploaded_file_name: str) -> str: # Split into 256 subdirectories to prevent directories from getting too big return "/".join( @@ -80,6 +83,7 @@ class LocalUploadBackend(ZulipUploadBackend): ] ) + @override def upload_message_attachment( self, uploaded_file_name: str, @@ -98,12 +102,15 @@ class LocalUploadBackend(ZulipUploadBackend): create_attachment(uploaded_file_name, path, user_profile, target_realm, uploaded_file_size) return "/user_uploads/" + path + @override def save_attachment_contents(self, path_id: str, filehandle: BinaryIO) -> None: filehandle.write(read_local_file("files", path_id)) + @override def delete_message_attachment(self, path_id: str) -> bool: return delete_local_file("files", path_id) + @override def all_message_attachments(self) -> Iterator[Tuple[str, datetime]]: assert settings.LOCAL_UPLOADS_DIR is not None for dirname, _, files in os.walk(settings.LOCAL_UPLOADS_DIR + "/files"): @@ -114,6 +121,7 @@ class LocalUploadBackend(ZulipUploadBackend): timestamp_to_datetime(os.path.getmtime(fullpath)), ) + @override def get_avatar_url(self, hash_key: str, medium: bool = False) -> str: medium_suffix = "-medium" if medium else "" return f"/user_avatars/{hash_key}{medium_suffix}.png" @@ -127,6 +135,7 @@ class LocalUploadBackend(ZulipUploadBackend): resized_medium = resize_avatar(image_data, MEDIUM_AVATAR_SIZE) write_local_file("avatars", file_path + "-medium.png", resized_medium) + @override def upload_avatar_image( self, user_file: IO[bytes], @@ -139,6 +148,7 @@ class LocalUploadBackend(ZulipUploadBackend): image_data = user_file.read() self.write_avatar_images(file_path, image_data) + @override def copy_avatar(self, source_profile: UserProfile, target_profile: UserProfile) -> None: source_file_path = user_avatar_path(source_profile) target_file_path = user_avatar_path(target_profile) @@ -146,6 +156,7 @@ class LocalUploadBackend(ZulipUploadBackend): image_data = read_local_file("avatars", source_file_path + ".original") self.write_avatar_images(target_file_path, image_data) + @override def ensure_avatar_image(self, user_profile: UserProfile, is_medium: bool = False) -> None: file_extension = "-medium.png" if is_medium else ".png" file_path = user_avatar_path(user_profile) @@ -169,6 +180,7 @@ class LocalUploadBackend(ZulipUploadBackend): resized_avatar = resize_avatar(image_data) write_local_file("avatars", file_path + file_extension, resized_avatar) + @override def delete_avatar_image(self, user: UserProfile) -> None: path_id = user_avatar_path(user) @@ -176,9 +188,11 @@ class LocalUploadBackend(ZulipUploadBackend): delete_local_file("avatars", path_id + ".png") delete_local_file("avatars", path_id + "-medium.png") + @override def get_realm_icon_url(self, realm_id: int, version: int) -> str: return f"/user_avatars/{realm_id}/realm/icon.png?version={version}" + @override def upload_realm_icon_image(self, icon_file: IO[bytes], user_profile: UserProfile) -> None: upload_path = self.realm_avatar_and_logo_path(user_profile.realm) image_data = icon_file.read() @@ -187,6 +201,7 @@ class LocalUploadBackend(ZulipUploadBackend): resized_data = resize_avatar(image_data) write_local_file("avatars", os.path.join(upload_path, "icon.png"), resized_data) + @override def get_realm_logo_url(self, realm_id: int, version: int, night: bool) -> str: if night: file_name = "night_logo.png" @@ -194,6 +209,7 @@ class LocalUploadBackend(ZulipUploadBackend): file_name = "logo.png" return f"/user_avatars/{realm_id}/realm/{file_name}?version={version}" + @override def upload_realm_logo_image( self, logo_file: IO[bytes], user_profile: UserProfile, night: bool ) -> None: @@ -210,6 +226,7 @@ class LocalUploadBackend(ZulipUploadBackend): resized_data = resize_logo(image_data) write_local_file("avatars", os.path.join(upload_path, resized_file), resized_data) + @override def get_emoji_url(self, emoji_file_name: str, realm_id: int, still: bool = False) -> str: if still: return os.path.join( @@ -227,6 +244,7 @@ class LocalUploadBackend(ZulipUploadBackend): ), ) + @override def upload_emoji_image( self, emoji_file: IO[bytes], emoji_file_name: str, user_profile: UserProfile ) -> bool: @@ -248,10 +266,12 @@ class LocalUploadBackend(ZulipUploadBackend): write_local_file("avatars", still_path, still_image_data) return is_animated + @override def get_export_tarball_url(self, realm: Realm, export_path: str) -> str: # export_path has a leading `/` return realm.uri + export_path + @override def upload_export_tarball( self, realm: Realm, @@ -270,6 +290,7 @@ class LocalUploadBackend(ZulipUploadBackend): public_url = realm.uri + "/user_avatars/" + path return public_url + @override def delete_export_tarball(self, export_path: str) -> Optional[str]: # Get the last element of a list in the form ['user_avatars', ''] assert export_path.startswith("/") diff --git a/zerver/lib/upload/s3.py b/zerver/lib/upload/s3.py index ca547447f5..9d333d54f8 100644 --- a/zerver/lib/upload/s3.py +++ b/zerver/lib/upload/s3.py @@ -13,6 +13,7 @@ from botocore.client import Config from django.conf import settings from mypy_boto3_s3.client import S3Client from mypy_boto3_s3.service_resource import Bucket, Object +from typing_extensions import override from zerver.lib.avatar_hash import user_avatar_path from zerver.lib.upload.base import ( @@ -194,6 +195,7 @@ class S3UploadBackend(ZulipUploadBackend): (split_url.scheme, split_url.netloc, split_url.path[: -len(DUMMY_KEY)], "", "") ) + @override def get_public_upload_root_url(self) -> str: return self.public_upload_url_base @@ -204,6 +206,7 @@ class S3UploadBackend(ZulipUploadBackend): assert not key.startswith("/") return urllib.parse.urljoin(self.public_upload_url_base, key) + @override def generate_message_upload_path(self, realm_id: str, uploaded_file_name: str) -> str: return "/".join( [ @@ -213,6 +216,7 @@ class S3UploadBackend(ZulipUploadBackend): ] ) + @override def upload_message_attachment( self, uploaded_file_name: str, @@ -241,18 +245,22 @@ class S3UploadBackend(ZulipUploadBackend): ) return url + @override def save_attachment_contents(self, path_id: str, filehandle: BinaryIO) -> None: for chunk in self.uploads_bucket.Object(path_id).get()["Body"]: filehandle.write(chunk) + @override def delete_message_attachment(self, path_id: str) -> bool: return self.delete_file_from_s3(path_id, self.uploads_bucket) + @override def delete_message_attachments(self, path_ids: List[str]) -> None: self.uploads_bucket.delete_objects( Delete={"Objects": [{"Key": path_id} for path_id in path_ids]} ) + @override def all_message_attachments(self) -> Iterator[Tuple[str, datetime]]: client = self.session.client( "s3", region_name=settings.S3_REGION, endpoint_url=settings.S3_ENDPOINT_URL @@ -308,10 +316,12 @@ class S3UploadBackend(ZulipUploadBackend): key = self.avatar_bucket.Object(file_name) return key + @override def get_avatar_url(self, hash_key: str, medium: bool = False) -> str: medium_suffix = "-medium.png" if medium else "" return self.get_public_upload_url(f"{hash_key}{medium_suffix}") + @override def upload_avatar_image( self, user_file: IO[bytes], @@ -326,6 +336,7 @@ class S3UploadBackend(ZulipUploadBackend): image_data = user_file.read() self.write_avatar_images(s3_file_name, target_user_profile, image_data, content_type) + @override def copy_avatar(self, source_profile: UserProfile, target_profile: UserProfile) -> None: s3_source_file_name = user_avatar_path(source_profile) s3_target_file_name = user_avatar_path(target_profile) @@ -336,6 +347,7 @@ class S3UploadBackend(ZulipUploadBackend): self.write_avatar_images(s3_target_file_name, target_profile, image_data, content_type) + @override def ensure_avatar_image(self, user_profile: UserProfile, is_medium: bool = False) -> None: # BUG: The else case should be user_avatar_path(user_profile) + ".png". # See #12852 for details on this bug and how to migrate it. @@ -358,6 +370,7 @@ class S3UploadBackend(ZulipUploadBackend): resized_avatar, ) + @override def delete_avatar_image(self, user: UserProfile) -> None: path_id = user_avatar_path(user) @@ -365,10 +378,12 @@ class S3UploadBackend(ZulipUploadBackend): self.delete_file_from_s3(path_id + "-medium.png", self.avatar_bucket) self.delete_file_from_s3(path_id, self.avatar_bucket) + @override def get_realm_icon_url(self, realm_id: int, version: int) -> str: public_url = self.get_public_upload_url(f"{realm_id}/realm/icon.png") return public_url + f"?version={version}" + @override def upload_realm_icon_image(self, icon_file: IO[bytes], user_profile: UserProfile) -> None: content_type = guess_type(icon_file.name)[0] s3_file_name = os.path.join(self.realm_avatar_and_logo_path(user_profile.realm), "icon") @@ -393,6 +408,7 @@ class S3UploadBackend(ZulipUploadBackend): # See avatar_url in avatar.py for URL. (That code also handles the case # that users use gravatar.) + @override def get_realm_logo_url(self, realm_id: int, version: int, night: bool) -> str: if not night: file_name = "logo.png" @@ -401,6 +417,7 @@ class S3UploadBackend(ZulipUploadBackend): public_url = self.get_public_upload_url(f"{realm_id}/realm/{file_name}") return public_url + f"?version={version}" + @override def upload_realm_logo_image( self, logo_file: IO[bytes], user_profile: UserProfile, night: bool ) -> None: @@ -431,6 +448,7 @@ class S3UploadBackend(ZulipUploadBackend): # See avatar_url in avatar.py for URL. (That code also handles the case # that users use gravatar.) + @override def get_emoji_url(self, emoji_file_name: str, realm_id: int, still: bool = False) -> str: if still: emoji_path = RealmEmoji.STILL_PATH_ID_TEMPLATE.format( @@ -444,6 +462,7 @@ class S3UploadBackend(ZulipUploadBackend): ) return self.get_public_upload_url(emoji_path) + @override def upload_emoji_image( self, emoji_file: IO[bytes], emoji_file_name: str, user_profile: UserProfile ) -> bool: @@ -486,10 +505,12 @@ class S3UploadBackend(ZulipUploadBackend): return is_animated + @override def get_export_tarball_url(self, realm: Realm, export_path: str) -> str: # export_path has a leading / return self.get_public_upload_url(export_path[1:]) + @override def upload_export_tarball( self, realm: Optional[Realm], @@ -509,6 +530,7 @@ class S3UploadBackend(ZulipUploadBackend): public_url = self.get_public_upload_url(key.key) return public_url + @override def delete_export_tarball(self, export_path: str) -> Optional[str]: assert export_path.startswith("/") path_id = export_path[1:] diff --git a/zerver/lib/url_preview/parsers/generic.py b/zerver/lib/url_preview/parsers/generic.py index 24c273779d..2eab71e110 100644 --- a/zerver/lib/url_preview/parsers/generic.py +++ b/zerver/lib/url_preview/parsers/generic.py @@ -2,12 +2,14 @@ from typing import Optional from urllib.parse import urlparse from bs4.element import Tag +from typing_extensions import override from zerver.lib.url_preview.parsers.base import BaseParser from zerver.lib.url_preview.types import UrlEmbedData class GenericParser(BaseParser): + @override def extract_data(self) -> UrlEmbedData: return UrlEmbedData( title=self._get_title(), diff --git a/zerver/lib/url_preview/parsers/open_graph.py b/zerver/lib/url_preview/parsers/open_graph.py index d3d7f01e8d..70b5b7eeff 100644 --- a/zerver/lib/url_preview/parsers/open_graph.py +++ b/zerver/lib/url_preview/parsers/open_graph.py @@ -1,11 +1,14 @@ from urllib.parse import urlparse +from typing_extensions import override + from zerver.lib.url_preview.types import UrlEmbedData from .base import BaseParser class OpenGraphParser(BaseParser): + @override def extract_data(self) -> UrlEmbedData: meta = self._soup.findAll("meta") diff --git a/zerver/lib/validator.py b/zerver/lib/validator.py index d46bfc2d1b..a6b82eaa5c 100644 --- a/zerver/lib/validator.py +++ b/zerver/lib/validator.py @@ -56,6 +56,7 @@ from django.core.validators import URLValidator, validate_email from django.utils.translation import gettext as _ from pydantic import ValidationInfo, model_validator from pydantic.functional_validators import ModelWrapValidatorHandler +from typing_extensions import override from zerver.lib.exceptions import InvalidJSONError, JsonableError from zerver.lib.timezone import canonicalize_timezone @@ -648,6 +649,7 @@ class WildValue: def __bool__(self) -> bool: return bool(self.value) + @override def __eq__(self, other: object) -> bool: return self.value == other @@ -658,6 +660,7 @@ class WildValue: ) return len(self.value) + @override def __str__(self) -> NoReturn: raise TypeError("cannot convert WildValue to string; try .tame(check_string)") @@ -698,10 +701,12 @@ class WildValue: class WildValueList(WildValue): value: List[object] + @override def __iter__(self) -> Iterator[WildValue]: for i, item in enumerate(self.value): yield wrap_wild_value(f"{self.var_name}[{i}]", item) + @override def __getitem__(self, key: Union[int, str]) -> WildValue: if not isinstance(key, int): return super().__getitem__(key) @@ -719,9 +724,11 @@ class WildValueList(WildValue): class WildValueDict(WildValue): value: Dict[str, object] + @override def __contains__(self, key: str) -> bool: return key in self.value + @override def __getitem__(self, key: Union[int, str]) -> WildValue: if not isinstance(key, str): return super().__getitem__(key) @@ -735,19 +742,23 @@ class WildValueDict(WildValue): return wrap_wild_value(var_name, item) + @override def get(self, key: str, default: object = None) -> WildValue: item = self.value.get(key, default) if isinstance(item, WildValue): return item return wrap_wild_value(f"{self.var_name}[{key!r}]", item) + @override def keys(self) -> Iterator[str]: yield from self.value.keys() + @override def values(self) -> Iterator[WildValue]: for key, value in self.value.items(): yield wrap_wild_value(f"{self.var_name}[{key!r}]", value) + @override def items(self) -> Iterator[Tuple[str, WildValue]]: for key, value in self.value.items(): yield key, wrap_wild_value(f"{self.var_name}[{key!r}]", value) diff --git a/zerver/lib/webhooks/common.py b/zerver/lib/webhooks/common.py index a1c22a848e..3b5e56899a 100644 --- a/zerver/lib/webhooks/common.py +++ b/zerver/lib/webhooks/common.py @@ -7,7 +7,7 @@ from urllib.parse import unquote from django.http import HttpRequest from django.utils.translation import gettext as _ from pydantic import Json -from typing_extensions import Annotated, TypeAlias +from typing_extensions import Annotated, TypeAlias, override from zerver.actions.message_send import ( check_send_private_message, @@ -74,6 +74,7 @@ class MissingHTTPEventHeaderError(AnomalousWebhookPayloadError): self.header = header @staticmethod + @override def msg_format() -> str: return _("Missing the HTTP event header '{header}'") diff --git a/zerver/management/commands/add_users_to_streams.py b/zerver/management/commands/add_users_to_streams.py index 73e181bda1..4463ef8087 100644 --- a/zerver/management/commands/add_users_to_streams.py +++ b/zerver/management/commands/add_users_to_streams.py @@ -1,6 +1,7 @@ from typing import Any from django.core.management.base import CommandParser +from typing_extensions import override from zerver.actions.streams import bulk_add_subscriptions from zerver.lib.management import ZulipBaseCommand @@ -10,6 +11,7 @@ from zerver.lib.streams import ensure_stream class Command(ZulipBaseCommand): help = """Add some or all users in a realm to a set of streams.""" + @override def add_arguments(self, parser: CommandParser) -> None: self.add_realm_args(parser, required=True) self.add_user_list_args(parser, all_users_help="Add all users in realm to these streams.") @@ -18,6 +20,7 @@ class Command(ZulipBaseCommand): "-s", "--streams", required=True, help="A comma-separated list of stream names." ) + @override def handle(self, *args: Any, **options: Any) -> None: realm = self.get_realm(options) assert realm is not None # Should be ensured by parser diff --git a/zerver/management/commands/archive_messages.py b/zerver/management/commands/archive_messages.py index 8d383fdd05..3d493b75ae 100644 --- a/zerver/management/commands/archive_messages.py +++ b/zerver/management/commands/archive_messages.py @@ -1,11 +1,13 @@ from typing import Any from django.core.management.base import BaseCommand +from typing_extensions import override from zerver.lib.retention import archive_messages, clean_archived_data class Command(BaseCommand): + @override def handle(self, *args: Any, **options: str) -> None: clean_archived_data() archive_messages() diff --git a/zerver/management/commands/audit_fts_indexes.py b/zerver/management/commands/audit_fts_indexes.py index 39ff37ce33..9fc19b141d 100644 --- a/zerver/management/commands/audit_fts_indexes.py +++ b/zerver/management/commands/audit_fts_indexes.py @@ -1,11 +1,13 @@ from typing import Any from django.db import connection +from typing_extensions import override from zerver.lib.management import ZulipBaseCommand class Command(ZulipBaseCommand): + @override def handle(self, *args: Any, **kwargs: str) -> None: with connection.cursor() as cursor: cursor.execute( diff --git a/zerver/management/commands/backup.py b/zerver/management/commands/backup.py index 2cea57e9bb..79c1d7c816 100644 --- a/zerver/management/commands/backup.py +++ b/zerver/management/commands/backup.py @@ -8,6 +8,7 @@ from django.conf import settings from django.core.management.base import CommandParser from django.db import connection from django.utils.timezone import now as timezone_now +from typing_extensions import override from scripts.lib.zulip_tools import TIMESTAMP_FORMAT, parse_os_release, run from version import ZULIP_VERSION @@ -17,16 +18,19 @@ from zerver.logging_handlers import try_git_describe class Command(ZulipBaseCommand): # Fix support for multi-line usage strings + @override def create_parser(self, prog_name: str, subcommand: str, **kwargs: Any) -> CommandParser: parser = super().create_parser(prog_name, subcommand, **kwargs) parser.formatter_class = RawTextHelpFormatter return parser + @override def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument("--output", help="Filename of output tarball") parser.add_argument("--skip-db", action="store_true", help="Skip database backup") parser.add_argument("--skip-uploads", action="store_true", help="Skip uploads backup") + @override def handle(self, *args: Any, **options: Any) -> None: timestamp = timezone_now().strftime(TIMESTAMP_FORMAT) with tempfile.TemporaryDirectory( diff --git a/zerver/management/commands/bulk_change_user_name.py b/zerver/management/commands/bulk_change_user_name.py index 34bcf8d584..b0113a8401 100644 --- a/zerver/management/commands/bulk_change_user_name.py +++ b/zerver/management/commands/bulk_change_user_name.py @@ -2,6 +2,7 @@ from argparse import ArgumentParser from typing import Any from django.core.management.base import CommandError +from typing_extensions import override from zerver.actions.user_settings import do_change_full_name from zerver.lib.management import ZulipBaseCommand @@ -10,6 +11,7 @@ from zerver.lib.management import ZulipBaseCommand class Command(ZulipBaseCommand): help = """Change the names for many users.""" + @override def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument( "data_file", @@ -18,6 +20,7 @@ class Command(ZulipBaseCommand): ) self.add_realm_args(parser, required=True) + @override def handle(self, *args: Any, **options: str) -> None: data_file = options["data_file"] realm = self.get_realm(options) diff --git a/zerver/management/commands/change_password.py b/zerver/management/commands/change_password.py index 843dc98f44..e1244f78d4 100644 --- a/zerver/management/commands/change_password.py +++ b/zerver/management/commands/change_password.py @@ -5,6 +5,7 @@ from typing import Any, List from django.contrib.auth.password_validation import validate_password from django.core.exceptions import ValidationError from django.core.management.base import CommandError +from typing_extensions import override from zerver.lib.management import ZulipBaseCommand @@ -27,10 +28,12 @@ class Command(ZulipBaseCommand): raise CommandError("aborted") return p + @override def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument("email", metavar="", help="email of user to change role") self.add_realm_args(parser, required=True) + @override def handle(self, *args: Any, **options: Any) -> str: email = options["email"] realm = self.get_realm(options) diff --git a/zerver/management/commands/change_realm_subdomain.py b/zerver/management/commands/change_realm_subdomain.py index c9b98a39af..481070eb74 100644 --- a/zerver/management/commands/change_realm_subdomain.py +++ b/zerver/management/commands/change_realm_subdomain.py @@ -3,6 +3,7 @@ from typing import Any from django.core.exceptions import ValidationError from django.core.management.base import CommandError +from typing_extensions import override from zerver.actions.create_realm import do_change_realm_subdomain from zerver.forms import check_subdomain_available @@ -12,6 +13,7 @@ from zerver.lib.management import ZulipBaseCommand class Command(ZulipBaseCommand): help = """Change realm's subdomain.""" + @override def add_arguments(self, parser: ArgumentParser) -> None: self.add_realm_args(parser, required=True) parser.add_argument("new_subdomain", metavar="", help="realm new subdomain") @@ -26,6 +28,7 @@ class Command(ZulipBaseCommand): help="Allow use of reserved subdomains", ) + @override def handle(self, *args: Any, **options: Any) -> None: realm = self.get_realm(options) assert realm is not None # Should be ensured by parser diff --git a/zerver/management/commands/change_user_email.py b/zerver/management/commands/change_user_email.py index 59b1b3195b..c97df0e76f 100644 --- a/zerver/management/commands/change_user_email.py +++ b/zerver/management/commands/change_user_email.py @@ -1,6 +1,8 @@ from argparse import ArgumentParser from typing import Any +from typing_extensions import override + from zerver.actions.user_settings import do_change_user_delivery_email from zerver.lib.management import ZulipBaseCommand @@ -8,11 +10,13 @@ from zerver.lib.management import ZulipBaseCommand class Command(ZulipBaseCommand): help = """Change the email address for a user.""" + @override def add_arguments(self, parser: ArgumentParser) -> None: self.add_realm_args(parser) parser.add_argument("old_email", metavar="", help="email address to change") parser.add_argument("new_email", metavar="", help="new email address") + @override def handle(self, *args: Any, **options: str) -> None: old_email = options["old_email"] new_email = options["new_email"] diff --git a/zerver/management/commands/change_user_role.py b/zerver/management/commands/change_user_role.py index 6db2acd79f..e6ff080079 100644 --- a/zerver/management/commands/change_user_role.py +++ b/zerver/management/commands/change_user_role.py @@ -2,6 +2,7 @@ from argparse import ArgumentParser from typing import Any from django.core.management.base import CommandError +from typing_extensions import override from zerver.actions.users import ( do_change_can_create_users, @@ -18,6 +19,7 @@ class Command(ZulipBaseCommand): ONLY perform this on customer request from an authorized person. """ + @override def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument("email", metavar="", help="email of user to change role") parser.add_argument( @@ -42,6 +44,7 @@ ONLY perform this on customer request from an authorized person. ) self.add_realm_args(parser, required=True) + @override def handle(self, *args: Any, **options: Any) -> None: email = options["email"] realm = self.get_realm(options) diff --git a/zerver/management/commands/check_redis.py b/zerver/management/commands/check_redis.py index 6a5e62eb1e..179bbf16b4 100644 --- a/zerver/management/commands/check_redis.py +++ b/zerver/management/commands/check_redis.py @@ -5,6 +5,7 @@ from typing import Any, Callable, Optional from django.conf import settings from django.core.management.base import BaseCommand, CommandError, CommandParser from returns.curry import partial +from typing_extensions import override from zerver.lib.rate_limiter import RateLimitedUser, client from zerver.models import get_user_profile_by_id @@ -16,6 +17,7 @@ class Command(BaseCommand): Usage: ./manage.py [--trim] check_redis""" + @override def add_arguments(self, parser: CommandParser) -> None: parser.add_argument("-t", "--trim", action="store_true", help="Actually trim excess") @@ -46,6 +48,7 @@ than max_api_calls! (trying to trim) %s %s", client.expire(key, entity.max_api_window()) trim_func(key, max_calls) + @override def handle(self, *args: Any, **options: Any) -> None: if not settings.RATE_LIMITING: raise CommandError("This machine is not using Redis or rate limiting, aborting") diff --git a/zerver/management/commands/checkconfig.py b/zerver/management/commands/checkconfig.py index 1ead3a386d..9e2f864334 100644 --- a/zerver/management/commands/checkconfig.py +++ b/zerver/management/commands/checkconfig.py @@ -1,6 +1,7 @@ from typing import Any from django.core.management.base import BaseCommand +from typing_extensions import override from zerver.lib.management import check_config @@ -8,5 +9,6 @@ from zerver.lib.management import check_config class Command(BaseCommand): help = """Checks /etc/zulip/settings.py for common configuration issues.""" + @override def handle(self, *args: Any, **options: Any) -> None: check_config() diff --git a/zerver/management/commands/compilemessages.py b/zerver/management/commands/compilemessages.py index 86f15ffac1..d9b09a3cc1 100644 --- a/zerver/management/commands/compilemessages.py +++ b/zerver/management/commands/compilemessages.py @@ -14,9 +14,11 @@ from django.utils.translation import gettext as _ from django.utils.translation import override as override_language from django.utils.translation import to_language from pyuca import Collator +from typing_extensions import override class Command(compilemessages.Command): + @override def add_arguments(self, parser: CommandParser) -> None: super().add_arguments(parser) @@ -24,6 +26,7 @@ class Command(compilemessages.Command): "--strict", "-s", action="store_true", help="Stop execution in case of errors." ) + @override def handle(self, *args: Any, **options: Any) -> None: super().handle(*args, **options) self.strict = options["strict"] diff --git a/zerver/management/commands/convert_gitter_data.py b/zerver/management/commands/convert_gitter_data.py index b9ac2e1cf2..158d3eba35 100644 --- a/zerver/management/commands/convert_gitter_data.py +++ b/zerver/management/commands/convert_gitter_data.py @@ -5,6 +5,7 @@ from typing import Any from django.conf import settings from django.core.management.base import BaseCommand, CommandError, CommandParser +from typing_extensions import override from zerver.data_import.gitter import do_convert_data @@ -12,6 +13,7 @@ from zerver.data_import.gitter import do_convert_data class Command(BaseCommand): help = """Convert the Gitter data into Zulip data format.""" + @override def add_arguments(self, parser: CommandParser) -> None: parser.add_argument( "gitter_data", nargs="+", metavar="", help="Gitter data in json format" @@ -29,6 +31,7 @@ class Command(BaseCommand): parser.formatter_class = argparse.RawTextHelpFormatter + @override def handle(self, *args: Any, **options: Any) -> None: output_dir = options["output_dir"] if output_dir is None: diff --git a/zerver/management/commands/convert_mattermost_data.py b/zerver/management/commands/convert_mattermost_data.py index d9fd0f71a7..17d29318fb 100644 --- a/zerver/management/commands/convert_mattermost_data.py +++ b/zerver/management/commands/convert_mattermost_data.py @@ -2,6 +2,8 @@ import argparse import os from typing import Any +from typing_extensions import override + """ Example usage for testing purposes. For testing data see the mattermost_fixtures in zerver/tests/. @@ -22,6 +24,7 @@ from zerver.data_import.mattermost import do_convert_data class Command(BaseCommand): help = """Convert the mattermost data into Zulip data format.""" + @override def add_arguments(self, parser: CommandParser) -> None: dir_help = ( "Directory containing exported JSON file and exported_emoji (optional) directory." @@ -43,6 +46,7 @@ class Command(BaseCommand): parser.formatter_class = argparse.RawTextHelpFormatter + @override def handle(self, *args: Any, **options: Any) -> None: output_dir = options["output_dir"] if output_dir is None: diff --git a/zerver/management/commands/convert_rocketchat_data.py b/zerver/management/commands/convert_rocketchat_data.py index 3e1396a0c5..2082434943 100644 --- a/zerver/management/commands/convert_rocketchat_data.py +++ b/zerver/management/commands/convert_rocketchat_data.py @@ -3,6 +3,7 @@ import os from typing import Any from django.core.management.base import BaseCommand, CommandError, CommandParser +from typing_extensions import override from zerver.data_import.rocketchat import do_convert_data @@ -10,6 +11,7 @@ from zerver.data_import.rocketchat import do_convert_data class Command(BaseCommand): help = """Convert the Rocketchat data into Zulip data format.""" + @override def add_arguments(self, parser: CommandParser) -> None: dir_help = "Directory containing all the `bson` files from mongodb dump of rocketchat." parser.add_argument( @@ -22,6 +24,7 @@ class Command(BaseCommand): parser.formatter_class = argparse.RawTextHelpFormatter + @override def handle(self, *args: Any, **options: Any) -> None: output_dir = options["output_dir"] if output_dir is None: diff --git a/zerver/management/commands/convert_slack_data.py b/zerver/management/commands/convert_slack_data.py index 2eafedf848..c0a16d6a9d 100644 --- a/zerver/management/commands/convert_slack_data.py +++ b/zerver/management/commands/convert_slack_data.py @@ -5,6 +5,7 @@ from typing import Any from django.conf import settings from django.core.management.base import BaseCommand, CommandError, CommandParser +from typing_extensions import override from zerver.data_import.slack import do_convert_data @@ -12,6 +13,7 @@ from zerver.data_import.slack import do_convert_data class Command(BaseCommand): help = """Convert the Slack data into Zulip data format.""" + @override def add_arguments(self, parser: CommandParser) -> None: parser.add_argument( "slack_data_path", @@ -42,6 +44,7 @@ class Command(BaseCommand): parser.formatter_class = argparse.RawTextHelpFormatter + @override def handle(self, *args: Any, **options: Any) -> None: output_dir = options["output_dir"] if output_dir is None: diff --git a/zerver/management/commands/create_default_stream_groups.py b/zerver/management/commands/create_default_stream_groups.py index d6d7423b3a..8565d5cb3f 100644 --- a/zerver/management/commands/create_default_stream_groups.py +++ b/zerver/management/commands/create_default_stream_groups.py @@ -1,6 +1,8 @@ from argparse import ArgumentParser from typing import Any +from typing_extensions import override + from zerver.lib.management import ZulipBaseCommand from zerver.lib.streams import ensure_stream from zerver.models import DefaultStreamGroup @@ -13,6 +15,7 @@ Create default stream groups which the users can choose during sign up. ./manage.py create_default_stream_groups -s gsoc-1,gsoc-2,gsoc-3 -d "Google summer of code" -r zulip """ + @override def add_arguments(self, parser: ArgumentParser) -> None: self.add_realm_args(parser, required=True) @@ -34,6 +37,7 @@ Create default stream groups which the users can choose during sign up. "-s", "--streams", required=True, help="A comma-separated list of stream names." ) + @override def handle(self, *args: Any, **options: Any) -> None: realm = self.get_realm(options) assert realm is not None # Should be ensured by parser diff --git a/zerver/management/commands/create_realm.py b/zerver/management/commands/create_realm.py index 28b3f927e3..2eaa14d02e 100644 --- a/zerver/management/commands/create_realm.py +++ b/zerver/management/commands/create_realm.py @@ -3,6 +3,7 @@ from typing import Any from django.core.exceptions import ValidationError from django.core.management.base import CommandError +from typing_extensions import override from zerver.actions.create_realm import do_create_realm from zerver.actions.create_user import do_create_user @@ -30,6 +31,7 @@ initial organization owner user for the new realm, using the same workflow as `./manage.py create_user`. """ + @override def add_arguments(self, parser: argparse.ArgumentParser) -> None: parser.add_argument("realm_name", help="Name for the new organization") parser.add_argument( @@ -44,6 +46,7 @@ workflow as `./manage.py create_user`. ) self.add_create_user_args(parser) + @override def handle(self, *args: Any, **options: Any) -> None: realm_name = options["realm_name"] string_id = options["string_id"] diff --git a/zerver/management/commands/create_realm_internal_bots.py b/zerver/management/commands/create_realm_internal_bots.py index c2cdd23ff0..a658c963a5 100644 --- a/zerver/management/commands/create_realm_internal_bots.py +++ b/zerver/management/commands/create_realm_internal_bots.py @@ -1,6 +1,7 @@ from typing import Any from django.core.management.base import BaseCommand +from typing_extensions import override from zerver.lib.onboarding import create_if_missing_realm_internal_bots @@ -13,6 +14,7 @@ These are normally created when the realm is, so this should be a no-op except when upgrading to a version that adds a new realm internal bot. """ + @override def handle(self, *args: Any, **options: Any) -> None: create_if_missing_realm_internal_bots() # create_users is idempotent -- it's a no-op when a given email diff --git a/zerver/management/commands/create_stream.py b/zerver/management/commands/create_stream.py index f1a3db75a7..7b405e0b12 100644 --- a/zerver/management/commands/create_stream.py +++ b/zerver/management/commands/create_stream.py @@ -1,6 +1,8 @@ from argparse import ArgumentParser from typing import Any +from typing_extensions import override + from zerver.lib.management import ZulipBaseCommand from zerver.lib.streams import create_stream_if_needed @@ -11,10 +13,12 @@ class Command(ZulipBaseCommand): This should be used for TESTING only, unless you understand the limitations of the command.""" + @override def add_arguments(self, parser: ArgumentParser) -> None: self.add_realm_args(parser, required=True, help="realm in which to create the stream") parser.add_argument("stream_name", metavar="", help="name of stream to create") + @override def handle(self, *args: Any, **options: str) -> None: realm = self.get_realm(options) assert realm is not None # Should be ensured by parser diff --git a/zerver/management/commands/create_user.py b/zerver/management/commands/create_user.py index b97fb2b6ef..91835d41ed 100644 --- a/zerver/management/commands/create_user.py +++ b/zerver/management/commands/create_user.py @@ -3,6 +3,7 @@ from typing import Any from django.core.management.base import CommandError from django.db.utils import IntegrityError +from typing_extensions import override from zerver.actions.create_user import do_create_user from zerver.lib.management import ZulipBaseCommand @@ -21,12 +22,14 @@ If the server has Terms of Service configured, the user will be prompted to accept the Terms of Service the first time they login. """ + @override def add_arguments(self, parser: argparse.ArgumentParser) -> None: self.add_create_user_args(parser) self.add_realm_args( parser, required=True, help="The name of the existing realm to which to add the user." ) + @override def handle(self, *args: Any, **options: Any) -> None: realm = self.get_realm(options) assert realm is not None # Should be ensured by parser diff --git a/zerver/management/commands/deactivate_realm.py b/zerver/management/commands/deactivate_realm.py index da4cc7014b..91b7dbb167 100644 --- a/zerver/management/commands/deactivate_realm.py +++ b/zerver/management/commands/deactivate_realm.py @@ -1,6 +1,8 @@ from argparse import ArgumentParser from typing import Any +from typing_extensions import override + from zerver.actions.realm_settings import do_add_deactivated_redirect, do_deactivate_realm from zerver.lib.management import ZulipBaseCommand @@ -8,12 +10,14 @@ from zerver.lib.management import ZulipBaseCommand class Command(ZulipBaseCommand): help = """Script to deactivate a realm.""" + @override def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument( "--redirect_url", metavar="", help="URL to which the realm has moved" ) self.add_realm_args(parser, required=True) + @override def handle(self, *args: Any, **options: str) -> None: realm = self.get_realm(options) diff --git a/zerver/management/commands/deactivate_user.py b/zerver/management/commands/deactivate_user.py index a398e00b07..6234daa54c 100644 --- a/zerver/management/commands/deactivate_user.py +++ b/zerver/management/commands/deactivate_user.py @@ -2,6 +2,7 @@ from argparse import ArgumentParser from typing import Any from django.core.management.base import CommandError +from typing_extensions import override from zerver.actions.users import do_deactivate_user from zerver.lib.management import ZulipBaseCommand @@ -12,6 +13,7 @@ from zerver.lib.users import get_active_bots_owned_by_user class Command(ZulipBaseCommand): help = "Deactivate a user, including forcibly logging them out." + @override def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument( "-f", @@ -22,6 +24,7 @@ class Command(ZulipBaseCommand): parser.add_argument("email", metavar="", help="email of user to deactivate") self.add_realm_args(parser) + @override def handle(self, *args: Any, **options: Any) -> None: realm = self.get_realm(options) user_profile = self.get_user(options["email"], realm) diff --git a/zerver/management/commands/delete_old_unclaimed_attachments.py b/zerver/management/commands/delete_old_unclaimed_attachments.py index dd4d39fd57..3271d53b71 100644 --- a/zerver/management/commands/delete_old_unclaimed_attachments.py +++ b/zerver/management/commands/delete_old_unclaimed_attachments.py @@ -4,6 +4,7 @@ from typing import Any from django.core.management.base import BaseCommand, CommandError from django.utils.timezone import now as timezone_now +from typing_extensions import override from zerver.actions.uploads import do_delete_old_unclaimed_attachments from zerver.lib.upload import all_message_attachments, delete_message_attachments @@ -15,6 +16,7 @@ class Command(BaseCommand): numerical value indicating the limit of how old the attachment can be. The default is five weeks.""" + @override def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument( "-w", @@ -40,6 +42,7 @@ class Command(BaseCommand): "any files which are not in the database. This may take a very long time!", ) + @override def handle(self, *args: Any, **options: Any) -> None: delta_weeks = options["delta_weeks"] print(f"Deleting unclaimed attached files older than {delta_weeks} weeks") diff --git a/zerver/management/commands/delete_realm.py b/zerver/management/commands/delete_realm.py index f7038e8c0d..8d59c80af8 100644 --- a/zerver/management/commands/delete_realm.py +++ b/zerver/management/commands/delete_realm.py @@ -3,6 +3,7 @@ from typing import Any from django.conf import settings from django.core.management.base import CommandError +from typing_extensions import override from zerver.actions.realm_settings import do_delete_all_realm_attachments from zerver.lib.management import ZulipBaseCommand @@ -13,9 +14,11 @@ class Command(ZulipBaseCommand): help = """Script to permanently delete a realm. Recommended only for removing realms used for testing; consider using deactivate_realm instead.""" + @override def add_arguments(self, parser: ArgumentParser) -> None: self.add_realm_args(parser, required=True) + @override def handle(self, *args: Any, **options: str) -> None: realm = self.get_realm(options) assert realm is not None # Should be ensured by parser diff --git a/zerver/management/commands/delete_user.py b/zerver/management/commands/delete_user.py index feb73923b0..9e32ed31df 100644 --- a/zerver/management/commands/delete_user.py +++ b/zerver/management/commands/delete_user.py @@ -2,6 +2,7 @@ from argparse import ArgumentParser from typing import Any from django.core.management.base import CommandError +from typing_extensions import override from zerver.actions.users import do_delete_user from zerver.lib.management import ZulipBaseCommand @@ -38,6 +39,7 @@ This will: sent/received by them, you can use the command on them individually. """ + @override def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument( "-f", @@ -48,6 +50,7 @@ This will: self.add_realm_args(parser) self.add_user_list_args(parser) + @override def handle(self, *args: Any, **options: Any) -> None: realm = self.get_realm(options) user_profiles = self.get_users(options, realm) diff --git a/zerver/management/commands/deliver_scheduled_emails.py b/zerver/management/commands/deliver_scheduled_emails.py index c7ffbfd631..a0c7dc8b0f 100644 --- a/zerver/management/commands/deliver_scheduled_emails.py +++ b/zerver/management/commands/deliver_scheduled_emails.py @@ -12,6 +12,7 @@ from django.conf import settings from django.core.management.base import BaseCommand from django.db import transaction from django.utils.timezone import now as timezone_now +from typing_extensions import override from zerver.lib.logging_util import log_to_file from zerver.lib.send_email import EmailNotDeliveredError, deliver_scheduled_emails @@ -31,6 +32,7 @@ Run this command under supervisor. Usage: ./manage.py deliver_scheduled_emails """ + @override def handle(self, *args: Any, **options: Any) -> None: try: while True: diff --git a/zerver/management/commands/deliver_scheduled_messages.py b/zerver/management/commands/deliver_scheduled_messages.py index ceddaaa594..1277deb19d 100644 --- a/zerver/management/commands/deliver_scheduled_messages.py +++ b/zerver/management/commands/deliver_scheduled_messages.py @@ -6,6 +6,7 @@ from typing import Any from django.conf import settings from django.core.management.base import BaseCommand from django.utils.timezone import now as timezone_now +from typing_extensions import override from zerver.actions.scheduled_messages import try_deliver_one_scheduled_message from zerver.lib.logging_util import log_to_file @@ -24,6 +25,7 @@ This management command is run via supervisor. Usage: ./manage.py deliver_scheduled_messages """ + @override def handle(self, *args: Any, **options: Any) -> None: try: while True: diff --git a/zerver/management/commands/edit_linkifiers.py b/zerver/management/commands/edit_linkifiers.py index a68d8fb2a0..233a63ba03 100644 --- a/zerver/management/commands/edit_linkifiers.py +++ b/zerver/management/commands/edit_linkifiers.py @@ -3,6 +3,7 @@ from argparse import ArgumentParser from typing import Any from django.core.management.base import CommandError +from typing_extensions import override from zerver.actions.realm_linkifiers import do_add_linkifier, do_remove_linkifier from zerver.lib.management import ZulipBaseCommand @@ -24,6 +25,7 @@ Example: ./manage.py edit_linkifiers --realm=zulip --op=remove '#(?P[0-9]{2, Example: ./manage.py edit_linkifiers --realm=zulip --op=show """ + @override def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument( "--op", default="show", help="What operation to do (add, show, remove)." @@ -39,6 +41,7 @@ Example: ./manage.py edit_linkifiers --realm=zulip --op=show ) self.add_realm_args(parser, required=True) + @override def handle(self, *args: Any, **options: str) -> None: realm = self.get_realm(options) assert realm is not None # Should be ensured by parser diff --git a/zerver/management/commands/email_mirror.py b/zerver/management/commands/email_mirror.py index 0e47a52993..c1b88e3874 100644 --- a/zerver/management/commands/email_mirror.py +++ b/zerver/management/commands/email_mirror.py @@ -26,6 +26,7 @@ from typing import Any, Generator from django.conf import settings from django.core.management.base import BaseCommand, CommandError +from typing_extensions import override from zerver.lib.email_mirror import logger, process_message @@ -80,6 +81,7 @@ def get_imap_messages() -> Generator[EmailMessage, None, None]: class Command(BaseCommand): help = __doc__ + @override def handle(self, *args: Any, **options: str) -> None: for message in get_imap_messages(): process_message(message) diff --git a/zerver/management/commands/enqueue_digest_emails.py b/zerver/management/commands/enqueue_digest_emails.py index 6507335c51..3373bdfcd9 100644 --- a/zerver/management/commands/enqueue_digest_emails.py +++ b/zerver/management/commands/enqueue_digest_emails.py @@ -5,6 +5,7 @@ from typing import Any from django.conf import settings from django.core.management.base import BaseCommand from django.utils.timezone import now as timezone_now +from typing_extensions import override from zerver.lib.digest import DIGEST_CUTOFF, enqueue_emails from zerver.lib.logging_util import log_to_file @@ -19,6 +20,7 @@ class Command(BaseCommand): in a while. """ + @override def handle(self, *args: Any, **options: Any) -> None: cutoff = timezone_now() - datetime.timedelta(days=DIGEST_CUTOFF) enqueue_emails(cutoff) diff --git a/zerver/management/commands/enqueue_file.py b/zerver/management/commands/enqueue_file.py index 674a97ea3d..3c1efc0a8f 100644 --- a/zerver/management/commands/enqueue_file.py +++ b/zerver/management/commands/enqueue_file.py @@ -4,6 +4,7 @@ from typing import IO, Any import orjson from django.core.management.base import BaseCommand +from typing_extensions import override from zerver.lib.queue import queue_json_publish @@ -41,6 +42,7 @@ first field is a timestamp that we ignore.) You can use "-" to represent stdin. """ + @override def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument( "queue_name", metavar="", help="name of worker queue to enqueue to" @@ -49,6 +51,7 @@ You can use "-" to represent stdin. "file_name", metavar="", help="name of file containing JSON lines" ) + @override def handle(self, *args: Any, **options: str) -> None: queue_name = options["queue_name"] file_name = options["file_name"] diff --git a/zerver/management/commands/export.py b/zerver/management/commands/export.py index b508b7ac97..ac2196d78b 100644 --- a/zerver/management/commands/export.py +++ b/zerver/management/commands/export.py @@ -5,6 +5,7 @@ from typing import Any from django.conf import settings from django.core.management.base import CommandError +from typing_extensions import override from zerver.actions.realm_settings import do_deactivate_realm from zerver.lib.export import export_realm_wrapper @@ -72,6 +73,7 @@ class Command(ZulipBaseCommand): minutes. But this will vary a lot depending on the average number of recipients of messages in the realm, hardware, etc.""" + @override def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument( "--output", dest="output_dir", help="Directory to write exported data to." @@ -106,6 +108,7 @@ class Command(ZulipBaseCommand): ) self.add_realm_args(parser, required=True) + @override def handle(self, *args: Any, **options: Any) -> None: realm = self.get_realm(options) assert realm is not None # Should be ensured by parser diff --git a/zerver/management/commands/export_search.py b/zerver/management/commands/export_search.py index 0e79216762..c583f6afb0 100644 --- a/zerver/management/commands/export_search.py +++ b/zerver/management/commands/export_search.py @@ -11,6 +11,7 @@ from typing import Any, Dict, Set, Tuple import orjson from django.core.management.base import CommandError from django.db.models import Q +from typing_extensions import override from zerver.lib.management import ZulipBaseCommand from zerver.lib.soft_deactivation import reactivate_user_if_soft_deactivated @@ -40,6 +41,7 @@ senders/recipients. This is most often used for legal compliance. """ + @override def add_arguments(self, parser: ArgumentParser) -> None: self.add_realm_args(parser, required=True) parser.add_argument( @@ -94,6 +96,7 @@ This is most often used for legal compliance. help="Limit to messages received by users with any of these emails (may be specified more than once). This is a superset of --sender, since senders receive every message they send.", ) + @override def handle(self, *args: Any, **options: Any) -> None: terms = set() if options["file"]: diff --git a/zerver/management/commands/export_single_user.py b/zerver/management/commands/export_single_user.py index 61b5203d40..ab751fd197 100644 --- a/zerver/management/commands/export_single_user.py +++ b/zerver/management/commands/export_single_user.py @@ -5,6 +5,7 @@ from argparse import ArgumentParser from typing import Any from django.core.management.base import CommandError +from typing_extensions import override from zerver.lib.export import do_export_user from zerver.lib.management import ZulipBaseCommand @@ -19,6 +20,7 @@ class Command(ZulipBaseCommand): realm-public metadata needed to understand it; it does nothing with (for example) any bots owned by the user.""" + @override def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument("email", metavar="", help="email of user to export") parser.add_argument( @@ -26,6 +28,7 @@ class Command(ZulipBaseCommand): ) self.add_realm_args(parser) + @override def handle(self, *args: Any, **options: Any) -> None: realm = self.get_realm(options) user_profile = self.get_user(options["email"], realm) diff --git a/zerver/management/commands/export_usermessage_batch.py b/zerver/management/commands/export_usermessage_batch.py index 5a19b97b76..a9c5907b83 100644 --- a/zerver/management/commands/export_usermessage_batch.py +++ b/zerver/management/commands/export_usermessage_batch.py @@ -5,6 +5,7 @@ from argparse import ArgumentParser from typing import Any from django.core.management.base import BaseCommand +from typing_extensions import override from zerver.lib.export import export_usermessages_batch @@ -12,6 +13,7 @@ from zerver.lib.export import export_usermessages_batch class Command(BaseCommand): help = """UserMessage fetching helper for export.py""" + @override def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument("--path", help="Path to find messages.json archives") parser.add_argument("--thread", help="Thread ID") @@ -21,6 +23,7 @@ class Command(BaseCommand): help="ID of the message advertising users to react with thumbs up", ) + @override def handle(self, *args: Any, **options: Any) -> None: logging.info("Starting UserMessage batch thread %s", options["thread"]) files = set(glob.glob(os.path.join(options["path"], "messages-*.json.partial"))) diff --git a/zerver/management/commands/fetch_tor_exit_nodes.py b/zerver/management/commands/fetch_tor_exit_nodes.py index 3c23233a92..58f4de0dde 100644 --- a/zerver/management/commands/fetch_tor_exit_nodes.py +++ b/zerver/management/commands/fetch_tor_exit_nodes.py @@ -4,6 +4,7 @@ from typing import Any, Set import orjson from django.conf import settings +from typing_extensions import override from urllib3.util import Retry from zerver.lib.management import ZulipBaseCommand @@ -33,6 +34,7 @@ to a file for access from Django for rate-limiting purposes. Does nothing unless RATE_LIMIT_TOR_TOGETHER is enabled. """ + @override def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument( "--max-retries", @@ -41,6 +43,7 @@ Does nothing unless RATE_LIMIT_TOR_TOGETHER is enabled. help="Number of times to retry fetching data from TOR", ) + @override def handle(self, *args: Any, **options: Any) -> None: if not settings.RATE_LIMIT_TOR_TOGETHER: return diff --git a/zerver/management/commands/fill_memcached_caches.py b/zerver/management/commands/fill_memcached_caches.py index 32fd29f65c..b9bb14f018 100644 --- a/zerver/management/commands/fill_memcached_caches.py +++ b/zerver/management/commands/fill_memcached_caches.py @@ -2,16 +2,19 @@ from argparse import ArgumentParser from typing import Any, Optional from django.core.management.base import BaseCommand +from typing_extensions import override from zerver.lib.cache_helpers import cache_fillers, fill_remote_cache class Command(BaseCommand): + @override def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument( "--cache", help="Populate one specific cache", choices=cache_fillers.keys() ) + @override def handle(self, *args: Any, **options: Optional[str]) -> None: if options["cache"] is not None: fill_remote_cache(options["cache"]) diff --git a/zerver/management/commands/generate_realm_creation_link.py b/zerver/management/commands/generate_realm_creation_link.py index cdd7aaf2d1..a572419c6f 100644 --- a/zerver/management/commands/generate_realm_creation_link.py +++ b/zerver/management/commands/generate_realm_creation_link.py @@ -2,6 +2,7 @@ from typing import Any from django.core.management.base import CommandError from django.db import ProgrammingError +from typing_extensions import override from confirmation.models import generate_realm_creation_url from zerver.lib.management import ZulipBaseCommand @@ -17,6 +18,7 @@ class Command(ZulipBaseCommand): Usage: ./manage.py generate_realm_creation_link """ + @override def handle(self, *args: Any, **options: Any) -> None: try: # first check if the db has been initialized diff --git a/zerver/management/commands/get_migration_status.py b/zerver/management/commands/get_migration_status.py index d8ee7fb7fd..95b153b693 100644 --- a/zerver/management/commands/get_migration_status.py +++ b/zerver/management/commands/get_migration_status.py @@ -4,6 +4,7 @@ from typing import Any from django.core.management.base import BaseCommand from django.db import DEFAULT_DB_ALIAS +from typing_extensions import override from scripts.lib.zulip_tools import get_dev_uuid_var_path from zerver.lib.test_fixtures import get_migration_status @@ -12,6 +13,7 @@ from zerver.lib.test_fixtures import get_migration_status class Command(BaseCommand): help = "Get status of migrations." + @override def add_arguments(self, parser: argparse.ArgumentParser) -> None: parser.add_argument( "app_label", nargs="?", help="App label of an application to synchronize the state." @@ -25,6 +27,7 @@ class Command(BaseCommand): parser.add_argument("--output", help="Path to store the status to (default to stdout).") + @override def handle(self, *args: Any, **options: Any) -> None: result = get_migration_status(**options) if options["output"] is not None: diff --git a/zerver/management/commands/import.py b/zerver/management/commands/import.py index f4ba01dec3..0cf399da59 100644 --- a/zerver/management/commands/import.py +++ b/zerver/management/commands/import.py @@ -9,6 +9,7 @@ from django.conf import settings from django.core.exceptions import ValidationError from django.core.management import call_command from django.core.management.base import BaseCommand, CommandError, CommandParser +from typing_extensions import override from zerver.forms import OverridableValidationError, check_subdomain_available from zerver.lib.import_realm import do_import_realm @@ -20,6 +21,7 @@ class Command(BaseCommand): This command should be used only on a newly created, empty Zulip instance to import a database dump from one or more JSON files.""" + @override def add_arguments(self, parser: CommandParser) -> None: parser.add_argument( "--destroy-rebuild-database", @@ -58,6 +60,7 @@ import a database dump from one or more JSON files.""" call_command("flush", verbosity=0, interactive=False) subprocess.check_call([os.path.join(settings.DEPLOY_ROOT, "scripts/setup/flush-memcached")]) + @override def handle(self, *args: Any, **options: Any) -> None: num_processes = int(options["processes"]) if num_processes < 1: diff --git a/zerver/management/commands/list_realms.py b/zerver/management/commands/list_realms.py index 641359daf8..51b2b993e1 100644 --- a/zerver/management/commands/list_realms.py +++ b/zerver/management/commands/list_realms.py @@ -2,6 +2,8 @@ import sys from argparse import ArgumentParser from typing import Any +from typing_extensions import override + from zerver.lib.management import ZulipBaseCommand from zerver.models import Realm @@ -14,11 +16,13 @@ Usage examples: ./manage.py list_realms ./manage.py list_realms --all""" + @override def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument( "--all", action="store_true", help="Print all the configuration settings of the realms." ) + @override def handle(self, *args: Any, **options: Any) -> None: realms = Realm.objects.all() diff --git a/zerver/management/commands/logout_all_users.py b/zerver/management/commands/logout_all_users.py index 5580cb5fa1..8be17b25f8 100644 --- a/zerver/management/commands/logout_all_users.py +++ b/zerver/management/commands/logout_all_users.py @@ -2,6 +2,7 @@ from argparse import ArgumentParser from typing import Any from django.db.models import Q +from typing_extensions import override from zerver.actions.user_settings import bulk_regenerate_api_keys from zerver.lib.management import ZulipBaseCommand @@ -21,6 +22,7 @@ Does not disable API keys, and thus will not log users out of the mobile apps. """ + @override def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument( "--deactivated-only", @@ -34,6 +36,7 @@ mobile apps. ) self.add_realm_args(parser, help="Only log out all users in a particular realm") + @override def handle(self, *args: Any, **options: Any) -> None: realm = self.get_realm(options) rotate_api_keys = options["rotate_api_keys"] diff --git a/zerver/management/commands/makemessages.py b/zerver/management/commands/makemessages.py index 2c2ace6f3d..c3bb328609 100644 --- a/zerver/management/commands/makemessages.py +++ b/zerver/management/commands/makemessages.py @@ -43,6 +43,7 @@ from django.core.management.base import CommandParser from django.core.management.commands import makemessages from django.template.base import BLOCK_TAG_END, BLOCK_TAG_START from django.utils.translation import template +from typing_extensions import override strip_whitespace_right = re.compile( f"({BLOCK_TAG_START}-?\\s*(trans|pluralize).*?-{BLOCK_TAG_END})\\s+" @@ -79,6 +80,7 @@ class Command(makemessages.Command): for func, tag in tags: xgettext_options += [f'--keyword={func}:1,"{tag}"'] + @override def add_arguments(self, parser: CommandParser) -> None: super().add_arguments(parser) parser.add_argument( @@ -97,6 +99,7 @@ class Command(makemessages.Command): help="Namespace of the frontend locale file", ) + @override def handle(self, *args: Any, **options: Any) -> None: self.handle_django_locales(*args, **options) self.handle_frontend_locales(**options) diff --git a/zerver/management/commands/merge_streams.py b/zerver/management/commands/merge_streams.py index 0005a7801e..1f72b551cb 100644 --- a/zerver/management/commands/merge_streams.py +++ b/zerver/management/commands/merge_streams.py @@ -1,6 +1,8 @@ from argparse import ArgumentParser from typing import Any +from typing_extensions import override + from zerver.actions.streams import merge_streams from zerver.lib.management import ZulipBaseCommand from zerver.models import get_stream @@ -9,6 +11,7 @@ from zerver.models import get_stream class Command(ZulipBaseCommand): help = """Merge two streams.""" + @override def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument("stream_to_keep", help="name of stream to keep") parser.add_argument( @@ -16,6 +19,7 @@ class Command(ZulipBaseCommand): ) self.add_realm_args(parser, required=True) + @override def handle(self, *args: Any, **options: str) -> None: realm = self.get_realm(options) assert realm is not None # Should be ensured by parser diff --git a/zerver/management/commands/process_queue.py b/zerver/management/commands/process_queue.py index 8844354d3d..c10642180a 100644 --- a/zerver/management/commands/process_queue.py +++ b/zerver/management/commands/process_queue.py @@ -12,6 +12,7 @@ from django.conf import settings from django.core.management.base import BaseCommand, CommandError from django.utils import autoreload from sentry_sdk import configure_scope +from typing_extensions import override from zerver.worker.queue_processors import get_active_worker_queues, get_worker @@ -33,6 +34,7 @@ def log_and_exit_if_exception( class Command(BaseCommand): + @override def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument("--queue_name", metavar="", help="queue to process") parser.add_argument( @@ -49,6 +51,7 @@ class Command(BaseCommand): help = "Runs a queue processing worker" + @override def handle(self, *args: Any, **options: Any) -> None: logging.basicConfig() logger = logging.getLogger("process_queue") @@ -119,6 +122,7 @@ class ThreadedWorker(threading.Thread): with log_and_exit_if_exception(logger, queue_name, threaded=True): self.worker = get_worker(queue_name, threaded=True) + @override def run(self) -> None: with configure_scope() as scope, log_and_exit_if_exception( self.logger, self.queue_name, threaded=True diff --git a/zerver/management/commands/promote_new_full_members.py b/zerver/management/commands/promote_new_full_members.py index 38dbde18a1..1fe116cec1 100644 --- a/zerver/management/commands/promote_new_full_members.py +++ b/zerver/management/commands/promote_new_full_members.py @@ -1,5 +1,7 @@ from typing import Any +from typing_extensions import override + from zerver.actions.user_groups import promote_new_full_members from zerver.lib.management import ZulipBaseCommand @@ -7,5 +9,6 @@ from zerver.lib.management import ZulipBaseCommand class Command(ZulipBaseCommand): help = """Add users to full members system group.""" + @override def handle(self, *args: Any, **options: Any) -> None: promote_new_full_members() diff --git a/zerver/management/commands/purge_queue.py b/zerver/management/commands/purge_queue.py index 4a9a676fc3..8b5c02d2da 100644 --- a/zerver/management/commands/purge_queue.py +++ b/zerver/management/commands/purge_queue.py @@ -3,18 +3,21 @@ from typing import Any from django.core.management import CommandError from django.core.management.base import BaseCommand +from typing_extensions import override from zerver.lib.queue import SimpleQueueClient from zerver.worker.queue_processors import get_active_worker_queues class Command(BaseCommand): + @override def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument(dest="queue_name", nargs="?", help="queue to purge") parser.add_argument("--all", action="store_true", help="purge all queues") help = "Discards all messages from the given queue" + @override def handle(self, *args: Any, **options: str) -> None: def purge_queue(queue_name: str) -> None: queue = SimpleQueueClient() diff --git a/zerver/management/commands/query_ldap.py b/zerver/management/commands/query_ldap.py index c07e983e9f..e251e1c1ca 100644 --- a/zerver/management/commands/query_ldap.py +++ b/zerver/management/commands/query_ldap.py @@ -2,14 +2,17 @@ from argparse import ArgumentParser from typing import Any from django.core.management.base import BaseCommand +from typing_extensions import override from zproject.backends import query_ldap class Command(BaseCommand): + @override def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument("email", metavar="", help="email of user to query") + @override def handle(self, *args: Any, **options: str) -> None: email = options["email"] values = query_ldap(email) diff --git a/zerver/management/commands/rate_limit.py b/zerver/management/commands/rate_limit.py index b2c3c60e49..98caf8228e 100644 --- a/zerver/management/commands/rate_limit.py +++ b/zerver/management/commands/rate_limit.py @@ -2,6 +2,7 @@ from argparse import ArgumentParser from typing import Any from django.core.management.base import CommandError +from typing_extensions import override from zerver.lib.management import ZulipBaseCommand from zerver.lib.rate_limiter import RateLimitedUser @@ -11,6 +12,7 @@ from zerver.models import UserProfile, get_user_profile_by_api_key class Command(ZulipBaseCommand): help = """Manually block or unblock a user from accessing the API""" + @override def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument("-e", "--email", help="Email account of user.") parser.add_argument("-a", "--api-key", help="API key of user.") @@ -36,6 +38,7 @@ class Command(ZulipBaseCommand): ) self.add_realm_args(parser) + @override def handle(self, *args: Any, **options: Any) -> None: if (not options["api_key"] and not options["email"]) or ( options["api_key"] and options["email"] diff --git a/zerver/management/commands/reactivate_realm.py b/zerver/management/commands/reactivate_realm.py index a122f919e3..aac267ebd8 100644 --- a/zerver/management/commands/reactivate_realm.py +++ b/zerver/management/commands/reactivate_realm.py @@ -1,6 +1,8 @@ from argparse import ArgumentParser from typing import Any +from typing_extensions import override + from zerver.actions.realm_settings import do_reactivate_realm from zerver.lib.management import ZulipBaseCommand @@ -8,9 +10,11 @@ from zerver.lib.management import ZulipBaseCommand class Command(ZulipBaseCommand): help = """Script to reactivate a deactivated realm.""" + @override def add_arguments(self, parser: ArgumentParser) -> None: self.add_realm_args(parser, required=True) + @override def handle(self, *args: Any, **options: str) -> None: realm = self.get_realm(options) assert realm is not None # Should be ensured by parser diff --git a/zerver/management/commands/realm_domain.py b/zerver/management/commands/realm_domain.py index f77c6ea03f..f8c70956e8 100644 --- a/zerver/management/commands/realm_domain.py +++ b/zerver/management/commands/realm_domain.py @@ -5,6 +5,7 @@ from typing import Any, Union from django.core.exceptions import ValidationError from django.core.management.base import CommandError from django.db.utils import IntegrityError +from typing_extensions import override from zerver.lib.domains import validate_domain from zerver.lib.management import ZulipBaseCommand @@ -14,6 +15,7 @@ from zerver.models import RealmDomain, get_realm_domains class Command(ZulipBaseCommand): help = """Manage domains for the specified realm""" + @override def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument( "--op", default="show", help="What operation to do (add, show, remove)." @@ -24,6 +26,7 @@ class Command(ZulipBaseCommand): parser.add_argument("domain", metavar="", nargs="?", help="domain to add or remove") self.add_realm_args(parser, required=True) + @override def handle(self, *args: Any, **options: Union[str, bool]) -> None: realm = self.get_realm(options) assert realm is not None # Should be ensured by parser diff --git a/zerver/management/commands/register_server.py b/zerver/management/commands/register_server.py index cf03f2c586..352981b9a5 100644 --- a/zerver/management/commands/register_server.py +++ b/zerver/management/commands/register_server.py @@ -7,6 +7,7 @@ from django.conf import settings from django.core.management.base import CommandError from django.utils.crypto import get_random_string from requests.models import Response +from typing_extensions import override from zerver.lib.management import ZulipBaseCommand, check_config from zerver.lib.remote_server import PushBouncerSession @@ -20,6 +21,7 @@ else: class Command(ZulipBaseCommand): help = """Register a remote Zulip server for push notifications.""" + @override def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument( "--agree_to_terms_of_service", @@ -32,6 +34,7 @@ class Command(ZulipBaseCommand): help="Automatically rotate your server's zulip_org_key", ) + @override def handle(self, *args: Any, **options: Any) -> None: if not settings.DEVELOPMENT: check_config() diff --git a/zerver/management/commands/remove_users_from_stream.py b/zerver/management/commands/remove_users_from_stream.py index 7c6910d221..0849f0b331 100644 --- a/zerver/management/commands/remove_users_from_stream.py +++ b/zerver/management/commands/remove_users_from_stream.py @@ -1,6 +1,7 @@ from typing import Any from django.core.management.base import CommandParser +from typing_extensions import override from zerver.actions.streams import bulk_remove_subscriptions from zerver.lib.management import ZulipBaseCommand @@ -10,6 +11,7 @@ from zerver.models import get_stream class Command(ZulipBaseCommand): help = """Remove some or all users in a realm from a stream.""" + @override def add_arguments(self, parser: CommandParser) -> None: parser.add_argument("-s", "--stream", required=True, help="A stream name.") @@ -18,6 +20,7 @@ class Command(ZulipBaseCommand): parser, all_users_help="Remove all users in realm from this stream." ) + @override def handle(self, *args: Any, **options: Any) -> None: realm = self.get_realm(options) assert realm is not None # Should be ensured by parser diff --git a/zerver/management/commands/reset_authentication_attempt_count.py b/zerver/management/commands/reset_authentication_attempt_count.py index 1fbd21dae2..fd96f445bb 100644 --- a/zerver/management/commands/reset_authentication_attempt_count.py +++ b/zerver/management/commands/reset_authentication_attempt_count.py @@ -2,6 +2,7 @@ from argparse import ArgumentParser from typing import Any from django.core.management.base import CommandError +from typing_extensions import override from zerver.lib.management import ZulipBaseCommand from zproject.backends import RateLimitedAuthenticationByUsername @@ -10,9 +11,11 @@ from zproject.backends import RateLimitedAuthenticationByUsername class Command(ZulipBaseCommand): help = """Reset the rate limit for authentication attempts for username.""" + @override def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument("-u", "--username", help="Username to reset the rate limit for.") + @override def handle(self, *args: Any, **options: Any) -> None: if not options["username"]: self.print_help("./manage.py", "reset_authentication_attempt_count") diff --git a/zerver/management/commands/restore_messages.py b/zerver/management/commands/restore_messages.py index 80ca9b5d34..60afe7aa50 100644 --- a/zerver/management/commands/restore_messages.py +++ b/zerver/management/commands/restore_messages.py @@ -1,6 +1,7 @@ from typing import Any from django.core.management.base import CommandParser +from typing_extensions import override from zerver.lib.management import ZulipBaseCommand from zerver.lib.retention import ( @@ -27,6 +28,7 @@ To restore a specific ArchiveTransaction: ./manage.py restore_messages --transaction-id=1 """ + @override def add_arguments(self, parser: CommandParser) -> None: parser.add_argument( "--all", @@ -49,6 +51,7 @@ To restore a specific ArchiveTransaction: "(Does not restore manually deleted messages.)", ) + @override def handle(self, *args: Any, **options: Any) -> None: realm = self.get_realm(options) if realm: diff --git a/zerver/management/commands/runtornado.py b/zerver/management/commands/runtornado.py index 51457414e8..0f1dafee4e 100644 --- a/zerver/management/commands/runtornado.py +++ b/zerver/management/commands/runtornado.py @@ -11,6 +11,7 @@ from django.conf import settings from django.core.management.base import BaseCommand, CommandError, CommandParser from tornado import autoreload from tornado.platform.asyncio import AsyncIOMainLoop +from typing_extensions import override settings.RUNNING_INSIDE_TORNADO = True if settings.PRODUCTION: @@ -38,12 +39,14 @@ asyncio.set_event_loop_policy(NoAutoCreateEventLoopPolicy()) class Command(BaseCommand): help = "Starts a Tornado Web server wrapping Django." + @override def add_arguments(self, parser: CommandParser) -> None: parser.add_argument( "addrport", help="[port number or ipaddr:port]", ) + @override def handle(self, *args: Any, **options: Any) -> None: interactive_debug_listen() addrport = options["addrport"] diff --git a/zerver/management/commands/scrub_realm.py b/zerver/management/commands/scrub_realm.py index 2e4f9ee118..42642fcda1 100644 --- a/zerver/management/commands/scrub_realm.py +++ b/zerver/management/commands/scrub_realm.py @@ -2,6 +2,7 @@ from argparse import ArgumentParser from typing import Any from django.core.management.base import CommandError +from typing_extensions import override from zerver.actions.realm_settings import do_scrub_realm from zerver.lib.management import ZulipBaseCommand @@ -10,9 +11,11 @@ from zerver.lib.management import ZulipBaseCommand class Command(ZulipBaseCommand): help = """Script to scrub a deactivated realm.""" + @override def add_arguments(self, parser: ArgumentParser) -> None: self.add_realm_args(parser, required=True) + @override def handle(self, *args: Any, **options: str) -> None: realm = self.get_realm(options) assert realm is not None # Should be ensured by parser diff --git a/zerver/management/commands/send_custom_email.py b/zerver/management/commands/send_custom_email.py index 863088f011..0303f4569c 100644 --- a/zerver/management/commands/send_custom_email.py +++ b/zerver/management/commands/send_custom_email.py @@ -4,6 +4,7 @@ from typing import Any, Callable, Dict, List, Optional import orjson from django.conf import settings from django.db.models import Q, QuerySet +from typing_extensions import override from confirmation.models import one_click_unsubscribe_link from zerver.lib.management import ZulipBaseCommand @@ -20,6 +21,7 @@ class Command(ZulipBaseCommand): The From and Subject headers can be provided in the body of the Markdown document used to generate the email, or on the command line.""" + @override def add_arguments(self, parser: ArgumentParser) -> None: targets = parser.add_mutually_exclusive_group(required=True) targets.add_argument( @@ -83,6 +85,7 @@ class Command(ZulipBaseCommand): help="Prints emails of the recipients and text of the email.", ) + @override def handle(self, *args: Any, **options: str) -> None: target_emails: List[str] = [] users: QuerySet[UserProfile] = UserProfile.objects.none() diff --git a/zerver/management/commands/send_password_reset_email.py b/zerver/management/commands/send_password_reset_email.py index eae9d972bc..9baa9e8a75 100644 --- a/zerver/management/commands/send_password_reset_email.py +++ b/zerver/management/commands/send_password_reset_email.py @@ -3,6 +3,7 @@ from typing import Any, Iterable from django.contrib.auth.tokens import default_token_generator from django.core.management.base import CommandError +from typing_extensions import override from zerver.forms import generate_password_reset_url from zerver.lib.management import ZulipBaseCommand @@ -13,6 +14,7 @@ from zerver.models import UserProfile class Command(ZulipBaseCommand): help = """Send email to specified email address.""" + @override def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument( "--entire-server", action="store_true", help="Send to every user on the server. " @@ -24,6 +26,7 @@ class Command(ZulipBaseCommand): ) self.add_realm_args(parser) + @override def handle(self, *args: Any, **options: str) -> None: if options["entire_server"]: users: Iterable[UserProfile] = UserProfile.objects.filter( diff --git a/zerver/management/commands/send_realm_reactivation_email.py b/zerver/management/commands/send_realm_reactivation_email.py index 6ab31e67f1..e1cfcc551a 100644 --- a/zerver/management/commands/send_realm_reactivation_email.py +++ b/zerver/management/commands/send_realm_reactivation_email.py @@ -2,6 +2,7 @@ from argparse import ArgumentParser from typing import Any from django.core.management.base import CommandError +from typing_extensions import override from zerver.actions.realm_settings import do_send_realm_reactivation_email from zerver.lib.management import ZulipBaseCommand @@ -10,9 +11,11 @@ from zerver.lib.management import ZulipBaseCommand class Command(ZulipBaseCommand): help = """Sends realm reactivation email to admins""" + @override def add_arguments(self, parser: ArgumentParser) -> None: self.add_realm_args(parser, required=True) + @override def handle(self, *args: Any, **options: str) -> None: realm = self.get_realm(options) assert realm is not None diff --git a/zerver/management/commands/send_test_email.py b/zerver/management/commands/send_test_email.py index 1af737855e..92d819fb15 100644 --- a/zerver/management/commands/send_test_email.py +++ b/zerver/management/commands/send_test_email.py @@ -7,11 +7,13 @@ from django.conf import settings from django.core.mail import mail_admins, mail_managers, send_mail from django.core.management import CommandError from django.core.management.commands import sendtestemail +from typing_extensions import override from zerver.lib.send_email import FromAddress, log_email_config_errors class Command(sendtestemail.Command): + @override def handle(self, *args: Any, **kwargs: str) -> None: if settings.WARN_NO_EMAIL: raise CommandError( diff --git a/zerver/management/commands/send_to_email_mirror.py b/zerver/management/commands/send_to_email_mirror.py index 7ac9b91435..8674f07ce6 100644 --- a/zerver/management/commands/send_to_email_mirror.py +++ b/zerver/management/commands/send_to_email_mirror.py @@ -8,6 +8,7 @@ from typing import Any, Optional import orjson from django.conf import settings from django.core.management.base import CommandError, CommandParser +from typing_extensions import override from zerver.lib.email_mirror import mirror_email_message from zerver.lib.email_mirror_helpers import encode_email_address @@ -36,6 +37,7 @@ Example: """ + @override def add_arguments(self, parser: CommandParser) -> None: parser.add_argument( "-f", @@ -54,6 +56,7 @@ Example: self.add_realm_args(parser, help="Specify which realm to connect to; default is zulip") + @override def handle(self, *args: Any, **options: Optional[str]) -> None: if options["fixture"] is None: self.print_help("./manage.py", "send_to_email_mirror") diff --git a/zerver/management/commands/send_webhook_fixture_message.py b/zerver/management/commands/send_webhook_fixture_message.py index 5f8524dce7..d88f04055f 100644 --- a/zerver/management/commands/send_webhook_fixture_message.py +++ b/zerver/management/commands/send_webhook_fixture_message.py @@ -5,6 +5,7 @@ import orjson from django.conf import settings from django.core.management.base import CommandError, CommandParser from django.test import Client +from typing_extensions import override from zerver.lib.management import ZulipBaseCommand from zerver.lib.webhooks.common import standardize_headers @@ -30,6 +31,7 @@ not contain any spaces in them and that you use the precise quoting approach shown above. """ + @override def add_arguments(self, parser: CommandParser) -> None: parser.add_argument( "-f", "--fixture", help="The path to the fixture you'd like to send into Zulip" @@ -61,6 +63,7 @@ approach shown above. ) return standardize_headers(custom_headers_dict) + @override def handle(self, *args: Any, **options: Optional[str]) -> None: if options["fixture"] is None or options["url"] is None: self.print_help("./manage.py", "send_webhook_fixture_message") diff --git a/zerver/management/commands/send_welcome_bot_message.py b/zerver/management/commands/send_welcome_bot_message.py index 738e63133b..db084b6df0 100644 --- a/zerver/management/commands/send_welcome_bot_message.py +++ b/zerver/management/commands/send_welcome_bot_message.py @@ -1,6 +1,8 @@ from argparse import ArgumentParser from typing import Any +from typing_extensions import override + from zerver.lib.management import ZulipBaseCommand from zerver.lib.onboarding import send_initial_direct_message @@ -8,6 +10,7 @@ from zerver.lib.onboarding import send_initial_direct_message class Command(ZulipBaseCommand): help = """Sends the initial welcome bot message.""" + @override def add_arguments(self, parser: ArgumentParser) -> None: self.add_user_list_args( parser, @@ -16,6 +19,7 @@ class Command(ZulipBaseCommand): ) self.add_realm_args(parser) + @override def handle(self, *args: Any, **options: str) -> None: for user_profile in self.get_users(options, self.get_realm(options), is_bot=False): send_initial_direct_message(user_profile) diff --git a/zerver/management/commands/show_admins.py b/zerver/management/commands/show_admins.py index d4492bdd12..9694c87a95 100644 --- a/zerver/management/commands/show_admins.py +++ b/zerver/management/commands/show_admins.py @@ -2,6 +2,7 @@ from argparse import ArgumentParser from typing import Any from django.core.management.base import CommandError +from typing_extensions import override from zerver.lib.management import ZulipBaseCommand @@ -9,9 +10,11 @@ from zerver.lib.management import ZulipBaseCommand class Command(ZulipBaseCommand): help = """Show the owners and administrators in an organization.""" + @override def add_arguments(self, parser: ArgumentParser) -> None: self.add_realm_args(parser, required=True) + @override def handle(self, *args: Any, **options: Any) -> None: realm = self.get_realm(options) assert realm is not None # True because of required=True above diff --git a/zerver/management/commands/soft_deactivate_users.py b/zerver/management/commands/soft_deactivate_users.py index 3897582b4d..2d4017a23d 100644 --- a/zerver/management/commands/soft_deactivate_users.py +++ b/zerver/management/commands/soft_deactivate_users.py @@ -4,6 +4,7 @@ from typing import Any, Dict, List from django.conf import settings from django.core.management.base import CommandError +from typing_extensions import override from zerver.lib.management import ZulipBaseCommand from zerver.lib.soft_deactivation import ( @@ -33,6 +34,7 @@ def get_users_from_emails(emails: List[str], filter_kwargs: Dict[str, Realm]) -> class Command(ZulipBaseCommand): help = """Soft activate/deactivate users. Users are recognised by their emails here.""" + @override def add_arguments(self, parser: ArgumentParser) -> None: self.add_realm_args(parser) parser.add_argument( @@ -54,6 +56,7 @@ class Command(ZulipBaseCommand): help="A list of user emails to soft activate/deactivate.", ) + @override def handle(self, *args: Any, **options: Any) -> None: if settings.STAGING: print("This is a Staging server. Suppressing management command.") diff --git a/zerver/management/commands/sync_ldap_user_data.py b/zerver/management/commands/sync_ldap_user_data.py index 8b9d7a7770..59e1e6a236 100644 --- a/zerver/management/commands/sync_ldap_user_data.py +++ b/zerver/management/commands/sync_ldap_user_data.py @@ -5,6 +5,7 @@ from typing import Any, Collection from django.conf import settings from django.core.management.base import CommandError from django.db import transaction +from typing_extensions import override from zerver.lib.logging_util import log_to_file from zerver.lib.management import ZulipBaseCommand @@ -62,6 +63,7 @@ def sync_ldap_user_data( class Command(ZulipBaseCommand): + @override def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument( "-f", @@ -73,6 +75,7 @@ class Command(ZulipBaseCommand): self.add_realm_args(parser) self.add_user_list_args(parser) + @override def handle(self, *args: Any, **options: Any) -> None: if options.get("realm_id") is not None: realm = self.get_realm(options) diff --git a/zerver/management/commands/transfer_uploads_to_s3.py b/zerver/management/commands/transfer_uploads_to_s3.py index 7f3d09dcd2..6709964595 100644 --- a/zerver/management/commands/transfer_uploads_to_s3.py +++ b/zerver/management/commands/transfer_uploads_to_s3.py @@ -2,6 +2,7 @@ from typing import Any from django.conf import settings from django.core.management.base import BaseCommand, CommandError, CommandParser +from typing_extensions import override from zerver.lib.transfer import transfer_uploads_to_s3 @@ -9,6 +10,7 @@ from zerver.lib.transfer import transfer_uploads_to_s3 class Command(BaseCommand): help = """Transfer uploads to S3 """ + @override def add_arguments(self, parser: CommandParser) -> None: parser.add_argument( "--processes", @@ -16,6 +18,7 @@ class Command(BaseCommand): help="Processes to use for exporting uploads in parallel", ) + @override def handle(self, *args: Any, **options: Any) -> None: num_processes = int(options["processes"]) if num_processes < 1: diff --git a/zerver/management/commands/unarchive_stream.py b/zerver/management/commands/unarchive_stream.py index a9ce682fc2..e198799666 100644 --- a/zerver/management/commands/unarchive_stream.py +++ b/zerver/management/commands/unarchive_stream.py @@ -2,6 +2,7 @@ from argparse import ArgumentParser from typing import Any, Optional from django.core.management.base import CommandError +from typing_extensions import override from zerver.actions.streams import deactivated_streams_by_old_name, do_unarchive_stream from zerver.lib.management import ZulipBaseCommand @@ -11,6 +12,7 @@ from zerver.models import RealmAuditLog, Stream class Command(ZulipBaseCommand): help = """Reactivate a stream that was deactivated.""" + @override def add_arguments(self, parser: ArgumentParser) -> None: specify_stream = parser.add_mutually_exclusive_group(required=True) specify_stream.add_argument( @@ -30,6 +32,7 @@ class Command(ZulipBaseCommand): ) self.add_realm_args(parser, required=True) + @override def handle(self, *args: Any, **options: Optional[str]) -> None: realm = self.get_realm(options) assert realm is not None # Should be ensured by parser diff --git a/zerver/middleware.py b/zerver/middleware.py index d6c0951786..f44e6b9f40 100644 --- a/zerver/middleware.py +++ b/zerver/middleware.py @@ -23,7 +23,7 @@ from django.views.csrf import csrf_failure as html_csrf_failure from django_scim.middleware import SCIMAuthCheckMiddleware from django_scim.settings import scim_settings from sentry_sdk import set_tag -from typing_extensions import Concatenate, ParamSpec +from typing_extensions import Concatenate, ParamSpec, override from zerver.lib.cache import get_remote_cache_requests, get_remote_cache_time from zerver.lib.db import reset_queries @@ -444,6 +444,7 @@ class CsrfFailureError(JsonableError): self.reason: str = reason @staticmethod + @override def msg_format() -> str: return _("CSRF error: {reason}") @@ -456,6 +457,7 @@ def csrf_failure(request: HttpRequest, reason: str = "") -> HttpResponse: class LocaleMiddleware(DjangoLocaleMiddleware): + @override def process_response( self, request: HttpRequest, response: HttpResponseBase ) -> HttpResponseBase: @@ -610,6 +612,7 @@ class ProxyMisconfigurationError(JsonableError): self.proxy_reason = proxy_reason @staticmethod + @override def msg_format() -> str: return _("Reverse proxy misconfiguration: {proxy_reason}") diff --git a/zerver/migrations/0149_realm_emoji_drop_unique_constraint.py b/zerver/migrations/0149_realm_emoji_drop_unique_constraint.py index bbd6236ed6..fdd1a593a3 100644 --- a/zerver/migrations/0149_realm_emoji_drop_unique_constraint.py +++ b/zerver/migrations/0149_realm_emoji_drop_unique_constraint.py @@ -7,6 +7,7 @@ from django.db import migrations, models from django.db.backends.base.schema import BaseDatabaseSchemaEditor from django.db.migrations.state import StateApps from mypy_boto3_s3.type_defs import CopySourceTypeDef +from typing_extensions import override class Uploader: @@ -49,6 +50,7 @@ class LocalUploader(Uploader): if not os.path.isdir(dirname): os.makedirs(dirname) + @override def copy_files(self, src_path: str, dst_path: str) -> None: assert settings.LOCAL_UPLOADS_DIR is not None assert settings.LOCAL_AVATARS_DIR is not None @@ -69,6 +71,7 @@ class S3Uploader(Uploader): "s3", region_name=settings.S3_REGION, endpoint_url=settings.S3_ENDPOINT_URL ).Bucket(self.bucket_name) + @override def copy_files(self, src_key: str, dst_key: str) -> None: source = CopySourceTypeDef(Bucket=self.bucket_name, Key=src_key) self.bucket.copy(CopySource=source, Key=dst_key) diff --git a/zerver/models.py b/zerver/models.py index 461405bd1a..2e3231fb02 100644 --- a/zerver/models.py +++ b/zerver/models.py @@ -1,3 +1,6 @@ +# https://github.com/typeddjango/django-stubs/issues/1698 +# mypy: disable-error-code="explicit-override" + import datetime import hashlib import secrets @@ -51,6 +54,7 @@ from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy from django_cte import CTEManager from django_stubs_ext import StrPromise, ValuesQuerySet +from typing_extensions import override from confirmation import settings as confirmation_settings from zerver.lib import cache @@ -137,6 +141,7 @@ class EmojiInfo(TypedDict): class AndZero(models.Lookup[int]): lookup_name = "andz" + @override def as_sql( self, compiler: SQLCompiler, connection: BaseDatabaseWrapper ) -> Tuple[str, List[Union[str, int]]]: # nocoverage # currently only used in migrations @@ -149,6 +154,7 @@ class AndZero(models.Lookup[int]): class AndNonZero(models.Lookup[int]): lookup_name = "andnz" + @override def as_sql( self, compiler: SQLCompiler, connection: BaseDatabaseWrapper ) -> Tuple[str, List[Union[str, int]]]: # nocoverage # currently only used in migrations @@ -816,6 +822,7 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub ) night_logo_version = models.PositiveSmallIntegerField(default=1) + @override def __str__(self) -> str: return f"{self.string_id} {self.id}" @@ -1161,6 +1168,7 @@ class RealmEmoji(models.Model): ), ] + @override def __str__(self) -> str: return f"{self.realm.string_id}: {self.id} {self.name} {self.deactivated} {self.file_name}" @@ -1279,9 +1287,11 @@ class RealmFilter(models.Model): class Meta: unique_together = ("realm", "pattern") + @override def __str__(self) -> str: return f"{self.realm.string_id}: {self.pattern} {self.url_template}" + @override def clean(self) -> None: """Validate whether the set of parameters in the URL template match the set of parameters in the regular expression. @@ -1382,9 +1392,11 @@ class RealmPlayground(models.Model): class Meta: unique_together = (("realm", "pygments_language", "name"),) + @override def __str__(self) -> str: return f"{self.realm.string_id}: {self.pygments_language} {self.name}" + @override def clean(self) -> None: """Validate whether the URL template is valid for the playground, ensuring that "code" is the sole variable present in it. @@ -1468,6 +1480,7 @@ class Recipient(models.Model): # N.B. If we used Django's choice=... we would get this for free (kinda) _type_names = {PERSONAL: "personal", STREAM: "stream", HUDDLE: "huddle"} + @override def __str__(self) -> str: return f"{self.label()} ({self.type_id}, {self.type})" @@ -2062,6 +2075,7 @@ class UserProfile(AbstractBaseUser, PermissionsMixin, UserBaseSettings): # type else: return False + @override def __str__(self) -> str: return f"{self.email} {self.realm!r}" @@ -2259,6 +2273,7 @@ class UserProfile(AbstractBaseUser, PermissionsMixin, UserBaseSettings): # type def format_requester_for_logs(self) -> str: return "{}@{}".format(self.id, self.realm.string_id or "root") + @override def set_password(self, password: Optional[str]) -> None: if password is None: self.set_unusable_password() @@ -2718,6 +2733,7 @@ class Stream(models.Model): models.Index(Upper("name"), name="upper_stream_name_idx"), ] + @override def __str__(self) -> str: return self.name @@ -2837,6 +2853,7 @@ class UserTopic(models.Model): ), ] + @override def __str__(self) -> str: return f"({self.user_profile.email}, {self.stream.name}, {self.topic_name}, {self.last_updated})" @@ -2849,6 +2866,7 @@ class MutedUser(models.Model): class Meta: unique_together = ("user_profile", "muted_user") + @override def __str__(self) -> str: return f"{self.user_profile.email} -> {self.muted_user.email}" @@ -2861,6 +2879,7 @@ class Client(models.Model): MAX_NAME_LENGTH = 30 name = models.CharField(max_length=MAX_NAME_LENGTH, db_index=True, unique=True) + @override def __str__(self) -> str: return self.name @@ -3040,6 +3059,7 @@ class AbstractMessage(models.Model): class Meta: abstract = True + @override def __str__(self) -> str: return f"{self.recipient.label()} / {self.subject} / {self.sender!r}" @@ -3058,6 +3078,7 @@ class ArchiveTransaction(models.Model): # If type is set to MANUAL, this should be null. realm = models.ForeignKey(Realm, null=True, on_delete=CASCADE) + @override def __str__(self) -> str: return "id: {id}, type: {type}, realm: {realm}, timestamp: {timestamp}".format( id=self.id, @@ -3304,6 +3325,7 @@ class Draft(models.Model): content = models.TextField() # Length should not exceed MAX_MESSAGE_LENGTH last_edit_time = models.DateTimeField(db_index=True) + @override def __str__(self) -> str: return f"{self.user_profile.email} / {self.id} / {self.last_edit_time}" @@ -3395,6 +3417,7 @@ class Reaction(AbstractReaction): # client-side sorting code. return Reaction.objects.filter(message_id__in=needed_ids).values(*fields).order_by("id") + @override def __str__(self) -> str: return f"{self.user_profile.email} / {self.message.id} / {self.emoji_name}" @@ -3585,6 +3608,7 @@ class UserMessage(AbstractUserMessage): ), ] + @override def __str__(self) -> str: recipient_string = self.message.recipient.label() return f"{recipient_string} / {self.user_profile.email} ({self.flags_list()})" @@ -3623,6 +3647,7 @@ class ArchivedUserMessage(AbstractUserMessage): message = models.ForeignKey(ArchivedMessage, on_delete=CASCADE) + @override def __str__(self) -> str: recipient_string = self.message.recipient.label() return f"{recipient_string} / {self.user_profile.email} ({self.flags_list()})" @@ -3666,6 +3691,7 @@ class AbstractAttachment(models.Model): class Meta: abstract = True + @override def __str__(self) -> str: return self.file_name @@ -3926,6 +3952,7 @@ class Subscription(models.Model): ), ] + @override def __str__(self) -> str: return f"{self.user_profile!r} -> {self.recipient!r}" @@ -4394,6 +4421,7 @@ class ScheduledEmail(AbstractScheduledJob): INVITATION_REMINDER = 3 type = models.PositiveSmallIntegerField() + @override def __str__(self) -> str: return f"{self.type} {self.address or list(self.users.all())} {self.scheduled_timestamp}" @@ -4408,6 +4436,7 @@ class MissedMessageEmailAddress(models.Model): # Number of times the missed message address has been used. times_used = models.PositiveIntegerField(default=0, db_index=True) + @override def __str__(self) -> str: return settings.EMAIL_GATEWAY_PATTERN % (self.email_token,) @@ -4544,6 +4573,7 @@ class ScheduledMessage(models.Model): ), ] + @override def __str__(self) -> str: return f"{self.recipient.label()} {self.subject} {self.sender!r} {self.scheduled_timestamp}" @@ -4780,6 +4810,7 @@ class RealmAuditLog(AbstractRealmAuditLog): ) event_last_message_id = models.IntegerField(null=True) + @override def __str__(self) -> str: if self.modified_user is not None: return f"{self.modified_user!r} {self.event_type} {self.event_time} {self.id}" @@ -4933,6 +4964,7 @@ class CustomProfileField(models.Model): class Meta: unique_together = ("realm", "name") + @override def __str__(self) -> str: return f"{self.realm!r} {self.name} {self.field_type} {self.order}" @@ -4969,6 +5001,7 @@ class CustomProfileFieldValue(models.Model): class Meta: unique_together = ("user_profile", "field") + @override def __str__(self) -> str: return f"{self.user_profile!r} {self.field!r} {self.value}" diff --git a/zerver/openapi/markdown_extension.py b/zerver/openapi/markdown_extension.py index c77e620a5c..48885b425b 100644 --- a/zerver/openapi/markdown_extension.py +++ b/zerver/openapi/markdown_extension.py @@ -16,6 +16,7 @@ import markdown from django.conf import settings from markdown.extensions import Extension from markdown.preprocessors import Preprocessor +from typing_extensions import override import zerver.openapi.python_examples from zerver.lib.markdown.priorities import PREPROCESSOR_PRIORITES @@ -404,6 +405,7 @@ class APIMarkdownExtension(Extension): ], } + @override def extendMarkdown(self, md: markdown.Markdown) -> None: md.preprocessors.register( APICodeExamplesPreprocessor(md, self.getConfigs()), @@ -435,6 +437,7 @@ class BasePreprocessor(Preprocessor): self.api_url = config["api_url"] self.REGEXP = regexp + @override def run(self, lines: List[str]) -> List[str]: done = False while not done: @@ -472,6 +475,7 @@ class APICodeExamplesPreprocessor(BasePreprocessor): def __init__(self, md: markdown.Markdown, config: Mapping[str, Any]) -> None: super().__init__(MACRO_REGEXP, md, config) + @override def generate_text(self, match: Match[str]) -> List[str]: language = match.group(1) or "" function = match.group(2) @@ -491,6 +495,7 @@ class APICodeExamplesPreprocessor(BasePreprocessor): ) return text + @override def render(self, function: str) -> List[str]: path, method = function.rsplit(":", 1) return generate_openapi_fixture(path, method) @@ -500,6 +505,7 @@ class APIHeaderPreprocessor(BasePreprocessor): def __init__(self, md: markdown.Markdown, config: Mapping[str, Any]) -> None: super().__init__(MACRO_REGEXP_HEADER, md, config) + @override def render(self, function: str) -> List[str]: path, method = function.rsplit(":", 1) raw_title = get_openapi_summary(path, method) @@ -518,6 +524,7 @@ class ResponseDescriptionPreprocessor(BasePreprocessor): def __init__(self, md: markdown.Markdown, config: Mapping[str, Any]) -> None: super().__init__(MACRO_REGEXP_RESPONSE_DESC, md, config) + @override def render(self, function: str) -> List[str]: path, method = function.rsplit(":", 1) raw_description = get_responses_description(path, method) @@ -528,6 +535,7 @@ class ParameterDescriptionPreprocessor(BasePreprocessor): def __init__(self, md: markdown.Markdown, config: Mapping[str, Any]) -> None: super().__init__(MACRO_REGEXP_PARAMETER_DESC, md, config) + @override def render(self, function: str) -> List[str]: path, method = function.rsplit(":", 1) raw_description = get_parameters_description(path, method) diff --git a/zerver/tests/test_attachments.py b/zerver/tests/test_attachments.py index 86c0630b73..cbc594444d 100644 --- a/zerver/tests/test_attachments.py +++ b/zerver/tests/test_attachments.py @@ -1,12 +1,15 @@ from typing import Any from unittest import mock +from typing_extensions import override + from zerver.lib.attachments import user_attachments from zerver.lib.test_classes import ZulipTestCase from zerver.models import Attachment class AttachmentsTests(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() user_profile = self.example_user("cordelia") diff --git a/zerver/tests/test_auth_backends.py b/zerver/tests/test_auth_backends.py index b55f6354a2..2ea1bb943c 100644 --- a/zerver/tests/test_auth_backends.py +++ b/zerver/tests/test_auth_backends.py @@ -52,6 +52,7 @@ from onelogin.saml2.utils import OneLogin_Saml2_Utils from social_core.exceptions import AuthFailed, AuthStateForbidden from social_django.storage import BaseDjangoStorage from social_django.strategy import DjangoStrategy +from typing_extensions import override from confirmation.models import Confirmation, create_confirmation_link from zerver.actions.create_realm import do_create_realm @@ -898,6 +899,7 @@ class SocialAuthBase(DesktopFlowTestingLib, ZulipTestCase, ABC): def get_account_data_dict(self, email: str, name: str) -> Dict[str, Any]: raise NotImplementedError + @override def setUp(self) -> None: super().setUp() self.user_profile = self.example_user("hamlet") @@ -1948,6 +1950,7 @@ class SAMLAuthBackendTest(SocialAuthBase): # We have to define our own social_auth_test as the flow of SAML authentication # is different from the other social backends. + @override def social_auth_test( self, account_data_dict: Dict[str, str], @@ -2108,6 +2111,7 @@ class SAMLAuthBackendTest(SocialAuthBase): result = self.client_get("http://zulip.testserver/complete/saml/", parameters) return result + @override def get_account_data_dict(self, email: str, name: str) -> Dict[str, Any]: return dict(email=email, name=name) @@ -2461,6 +2465,7 @@ class SAMLAuthBackendTest(SocialAuthBase): expect_full_name_prepopulated=False, ) + @override def test_social_auth_no_key(self) -> None: """ Since in the case of SAML there isn't a direct equivalent of CLIENT_KEY_SETTING, @@ -2549,6 +2554,7 @@ class SAMLAuthBackendTest(SocialAuthBase): expect_confirm_registration_page=False, ) + @override def test_social_auth_complete(self) -> None: with mock.patch.object(OneLogin_Saml2_Response, "is_valid", return_value=True): with mock.patch.object( @@ -2574,6 +2580,7 @@ class SAMLAuthBackendTest(SocialAuthBase): ], ) + @override def test_social_auth_complete_when_base_exc_is_raised(self) -> None: with mock.patch.object(OneLogin_Saml2_Response, "is_valid", return_value=True): with mock.patch( @@ -2843,6 +2850,7 @@ class SAMLAuthBackendTest(SocialAuthBase): ], ) + @override def test_social_auth_invalid_email(self) -> None: """ This test needs an override from the original class. For security reasons, @@ -3327,6 +3335,7 @@ class AppleIdAuthBackendTest(AppleAuthMixin, SocialAuthBase): # dummy value to keep SocialAuthBase common code happy. USER_INFO_URL = "/invalid-unused-url" + @override def social_auth_test_finish( self, result: "TestHttpResponse", @@ -3344,6 +3353,7 @@ class AppleIdAuthBackendTest(AppleAuthMixin, SocialAuthBase): ) return result + @override def register_extra_endpoints( self, requests_mock: responses.RequestsMock, @@ -3360,6 +3370,7 @@ class AppleIdAuthBackendTest(AppleAuthMixin, SocialAuthBase): json=json.loads(EXAMPLE_JWK), ) + @override def generate_access_token_url_payload(self, account_data_dict: Dict[str, str]) -> str: # The ACCESS_TOKEN_URL endpoint works a bit different than in standard Oauth2, # and here, similarly to OIDC, id_token is also returned in the response. @@ -3451,6 +3462,7 @@ class AppleAuthBackendNativeFlowTest(AppleAuthMixin, SocialAuthBase): SIGNUP_URL = "/complete/apple/" LOGIN_URL = "/complete/apple/" + @override def prepare_login_url_and_headers( self, subdomain: str, @@ -3492,6 +3504,7 @@ class AppleAuthBackendNativeFlowTest(AppleAuthMixin, SocialAuthBase): url += f"&{urlencode(params)}" return url, headers + @override def social_auth_test( self, account_data_dict: Dict[str, str], @@ -3569,6 +3582,7 @@ class AppleAuthBackendNativeFlowTest(AppleAuthMixin, SocialAuthBase): ) self.assert_json_error(result, "Missing id_token parameter") + @override def test_social_auth_session_fields_cleared_correctly(self) -> None: mobile_flow_otp = "1234abcd" * 8 account_data_dict = self.get_account_data_dict(email=self.email, name=self.name) @@ -3637,12 +3651,14 @@ class AppleAuthBackendNativeFlowTest(AppleAuthMixin, SocialAuthBase): ], ) + @override def test_social_auth_desktop_success(self) -> None: """ The desktop app doesn't use the native flow currently and the desktop app flow in its current form happens in the browser, thus only the web flow is viable there. """ + @override def test_social_auth_no_key(self) -> None: """ The basic validation of server configuration is handled on the @@ -3664,6 +3680,7 @@ class GenericOpenIdConnectTest(SocialAuthBase): USER_INFO_URL = f"{BASE_OIDC_URL}/userinfo" AUTH_FINISH_URL = "/complete/oidc/" + @override def social_auth_test( self, *args: Any, @@ -3707,6 +3724,7 @@ class GenericOpenIdConnectTest(SocialAuthBase): return result + @override def social_auth_test_finish(self, *args: Any, **kwargs: Any) -> "TestHttpResponse": # Trying to generate a (access_token, id_token) pair here in tests that would # successfully pass validation by validate_and_return_id_token is impractical @@ -3719,6 +3737,7 @@ class GenericOpenIdConnectTest(SocialAuthBase): ): return super().social_auth_test_finish(*args, **kwargs) + @override def register_extra_endpoints( self, requests_mock: responses.RequestsMock, @@ -3732,6 +3751,7 @@ class GenericOpenIdConnectTest(SocialAuthBase): json=json.loads(EXAMPLE_JWK), ) + @override def generate_access_token_url_payload(self, account_data_dict: Dict[str, str]) -> str: return json.dumps( { @@ -3742,6 +3762,7 @@ class GenericOpenIdConnectTest(SocialAuthBase): } ) + @override def get_account_data_dict(self, email: str, name: Optional[str]) -> Dict[str, Any]: if name is not None: name_parts = name.split(" ") @@ -3813,6 +3834,7 @@ class GenericOpenIdConnectTest(SocialAuthBase): expect_full_name_prepopulated=False, ) + @override def test_social_auth_no_key(self) -> None: """ Requires overriding because client key/secret are configured @@ -3860,6 +3882,7 @@ class GenericOpenIdConnectTest(SocialAuthBase): [f"ERROR:django.request:Internal Server Error: {self.LOGIN_URL}"], ) + @override def test_config_error_development(self) -> None: """ This test is redundant for now, as test_social_auth_no_key already @@ -3868,6 +3891,7 @@ class GenericOpenIdConnectTest(SocialAuthBase): """ return + @override def test_config_error_production(self) -> None: """ This test is redundant for now, as test_social_auth_no_key already @@ -3889,6 +3913,7 @@ class GitHubAuthBackendTest(SocialAuthBase): AUTH_FINISH_URL = "/complete/github/" email_data: List[Dict[str, Any]] = [] + @override def social_auth_test_finish( self, result: "TestHttpResponse", @@ -3939,6 +3964,7 @@ class GitHubAuthBackendTest(SocialAuthBase): return result + @override def register_extra_endpoints( self, requests_mock: responses.RequestsMock, @@ -3965,6 +3991,7 @@ class GitHubAuthBackendTest(SocialAuthBase): self.email_data = email_data + @override def get_account_data_dict( self, email: str, name: str, user_avatar_url: str = "" ) -> Dict[str, Any]: @@ -4404,6 +4431,7 @@ class GitLabAuthBackendTest(SocialAuthBase): with self.settings(AUTHENTICATION_BACKENDS=("zproject.backends.GitLabAuthBackend",)): self.assertTrue(gitlab_auth_enabled()) + @override def get_account_data_dict(self, email: str, name: str) -> Dict[str, Any]: return dict(email=email, name=name, email_verified=True) @@ -4419,6 +4447,7 @@ class GoogleAuthBackendTest(SocialAuthBase): USER_INFO_URL = "https://www.googleapis.com/oauth2/v3/userinfo" AUTH_FINISH_URL = "/complete/google/" + @override def get_account_data_dict(self, email: str, name: str) -> Dict[str, Any]: return dict(email=email, name=name, email_verified=True) @@ -4751,6 +4780,7 @@ class GoogleAuthBackendTest(SocialAuthBase): m.output, [f"WARNING:root:log_into_subdomain: Invalid token given: {token}"] ) + @override def test_user_cannot_log_into_wrong_subdomain(self) -> None: data: ExternalAuthDataDict = { "full_name": "Full Name", @@ -4798,6 +4828,7 @@ class JSONFetchAPIKeyTest(ZulipTestCase): class FetchAPIKeyTest(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() self.user_profile = self.example_user("hamlet") @@ -5005,6 +5036,7 @@ class FetchAPIKeyTest(ZulipTestCase): class DevFetchAPIKeyTest(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() self.user_profile = self.example_user("hamlet") @@ -5882,6 +5914,7 @@ class TestJWTLogin(ZulipTestCase): class DjangoToLDAPUsernameTests(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() self.init_default_ldap_database() @@ -6022,6 +6055,7 @@ class DjangoToLDAPUsernameTests(ZulipTestCase): class ZulipLDAPTestCase(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() @@ -7178,6 +7212,7 @@ class LDAPBackendTest(ZulipTestCase): class JWTFetchAPIKeyTest(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() self.email = self.example_email("hamlet") diff --git a/zerver/tests/test_create_video_call.py b/zerver/tests/test_create_video_call.py index 323b9b0881..be4471fd95 100644 --- a/zerver/tests/test_create_video_call.py +++ b/zerver/tests/test_create_video_call.py @@ -4,12 +4,14 @@ import orjson import responses from django.core.signing import Signer from django.http import HttpResponseRedirect +from typing_extensions import override from zerver.lib.test_classes import ZulipTestCase from zerver.lib.url_encoding import append_url_query_string class TestVideoCall(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() self.user = self.example_user("hamlet") diff --git a/zerver/tests/test_custom_profile_data.py b/zerver/tests/test_custom_profile_data.py index 65694190e6..39a8f88eea 100644 --- a/zerver/tests/test_custom_profile_data.py +++ b/zerver/tests/test_custom_profile_data.py @@ -2,6 +2,7 @@ from typing import Any, Dict, Iterable, List, Optional, Tuple, Union, cast from unittest import mock import orjson +from typing_extensions import override from zerver.actions.custom_profile_fields import ( do_remove_realm_custom_profile_field, @@ -24,6 +25,7 @@ from zerver.models import ( class CustomProfileFieldTestCase(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() self.realm = get_realm("zulip") diff --git a/zerver/tests/test_decorators.py b/zerver/tests/test_decorators.py index be470db21c..a9ecd9c1ff 100644 --- a/zerver/tests/test_decorators.py +++ b/zerver/tests/test_decorators.py @@ -11,6 +11,7 @@ from django.conf import settings from django.contrib.auth.models import AnonymousUser from django.http import HttpRequest, HttpResponse from django.utils.timezone import now as timezone_now +from typing_extensions import override from zerver.actions.create_realm import do_create_realm from zerver.actions.create_user import do_reactivate_user @@ -877,6 +878,7 @@ class TestIncomingWebhookBot(ZulipTestCase): class TestValidateApiKey(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() zulip_realm = get_realm("zulip") diff --git a/zerver/tests/test_embedded_bot_system.py b/zerver/tests/test_embedded_bot_system.py index 7f3d3eeb2b..1495692ba6 100644 --- a/zerver/tests/test_embedded_bot_system.py +++ b/zerver/tests/test_embedded_bot_system.py @@ -1,6 +1,7 @@ from unittest.mock import patch import orjson +from typing_extensions import override from zerver.lib.bot_lib import EmbeddedBotQuitError from zerver.lib.test_classes import ZulipTestCase @@ -14,6 +15,7 @@ from zerver.models import ( class TestEmbeddedBotMessaging(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() self.user_profile = self.example_user("othello") diff --git a/zerver/tests/test_event_queue.py b/zerver/tests/test_event_queue.py index 4150ebfac6..331feb9f90 100644 --- a/zerver/tests/test_event_queue.py +++ b/zerver/tests/test_event_queue.py @@ -4,6 +4,7 @@ from unittest import mock import orjson from django.http import HttpRequest, HttpResponse +from typing_extensions import override from zerver.actions.message_send import internal_send_private_message from zerver.actions.muted_users import do_mute_user @@ -182,6 +183,7 @@ class MissedMessageHookTest(ZulipTestCase): self.user_profile, sub, stream, property_name, value, acting_user=None ) + @override def setUp(self) -> None: super().setUp() self.user_profile = self.example_user("hamlet") @@ -195,6 +197,7 @@ class MissedMessageHookTest(ZulipTestCase): self.login_user(self.user_profile) + @override def tearDown(self) -> None: self.destroy_event_queue(self.user_profile, self.client_descriptor.event_queue.id) super().tearDown() diff --git a/zerver/tests/test_event_system.py b/zerver/tests/test_event_system.py index 6363ffd1d9..5dae746eb0 100644 --- a/zerver/tests/test_event_system.py +++ b/zerver/tests/test_event_system.py @@ -8,6 +8,7 @@ from django.conf import settings from django.http import HttpRequest, HttpResponse from django.test import override_settings from django.utils.timezone import now as timezone_now +from typing_extensions import override from version import API_FEATURE_LEVEL, ZULIP_MERGE_BASE, ZULIP_VERSION from zerver.actions.custom_profile_fields import try_update_realm_custom_profile_field @@ -1307,6 +1308,7 @@ class FetchQueriesTest(ZulipTestCase): class TestEventsRegisterAllPublicStreamsDefaults(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() self.user_profile = self.example_user("hamlet") @@ -1350,6 +1352,7 @@ class TestEventsRegisterAllPublicStreamsDefaults(ZulipTestCase): class TestEventsRegisterNarrowDefaults(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() self.user_profile = self.example_user("hamlet") diff --git a/zerver/tests/test_events.py b/zerver/tests/test_events.py index 3f76c20016..ad7ad4e007 100644 --- a/zerver/tests/test_events.py +++ b/zerver/tests/test_events.py @@ -15,6 +15,7 @@ import orjson from dateutil.parser import parse as dateparser from django.utils.timezone import now as timezone_now from returns.curry import partial +from typing_extensions import override from zerver.actions.alert_words import do_add_alert_words, do_remove_alert_words from zerver.actions.bots import ( @@ -261,6 +262,7 @@ class BaseAction(ZulipTestCase): for extensive design details for this testing system. """ + @override def setUp(self) -> None: super().setUp() self.user_profile = self.example_user("hamlet") diff --git a/zerver/tests/test_external.py b/zerver/tests/test_external.py index ecd90118b8..44b150eea7 100644 --- a/zerver/tests/test_external.py +++ b/zerver/tests/test_external.py @@ -11,6 +11,7 @@ from django.conf import settings from django.core.exceptions import ValidationError from django.test import override_settings from django.utils.timezone import now as timezone_now +from typing_extensions import override from zerver.forms import email_is_not_mit_mailing_list from zerver.lib.cache import cache_delete @@ -76,6 +77,7 @@ class MITNameTest(ZulipTestCase): class RateLimitTests(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() @@ -93,6 +95,7 @@ class RateLimitTests(ZulipTestCase): settings.RATE_LIMITING = True + @override def tearDown(self) -> None: settings.RATE_LIMITING = False diff --git a/zerver/tests/test_hotspots.py b/zerver/tests/test_hotspots.py index 27d142f43b..bdc9effe45 100644 --- a/zerver/tests/test_hotspots.py +++ b/zerver/tests/test_hotspots.py @@ -1,3 +1,5 @@ +from typing_extensions import override + from zerver.actions.create_user import do_create_user from zerver.actions.hotspots import do_mark_hotspot_as_read from zerver.lib.hotspots import ALL_HOTSPOTS, INTRO_HOTSPOTS, get_next_hotspots @@ -8,6 +10,7 @@ from zerver.models import UserHotspot, UserProfile, get_realm # Splitting this out, since I imagine this will eventually have most of the # complicated hotspots logic. class TestGetNextHotspots(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() self.user = do_create_user( diff --git a/zerver/tests/test_i18n.py b/zerver/tests/test_i18n.py index c15b7e08de..d0f25da540 100644 --- a/zerver/tests/test_i18n.py +++ b/zerver/tests/test_i18n.py @@ -6,6 +6,7 @@ import orjson from django.conf import settings from django.core import mail from django.utils import translation +from typing_extensions import override from zerver.lib.email_notifications import enqueue_welcome_emails from zerver.lib.i18n import get_browser_language_code @@ -80,6 +81,7 @@ class TranslationTestCase(ZulipTestCase): aware. """ + @override def tearDown(self) -> None: translation.activate(settings.LANGUAGE_CODE) super().tearDown() @@ -164,6 +166,7 @@ class TranslationTestCase(ZulipTestCase): class JsonTranslationTestCase(ZulipTestCase): + @override def tearDown(self) -> None: translation.activate(settings.LANGUAGE_CODE) super().tearDown() diff --git a/zerver/tests/test_import_export.py b/zerver/tests/test_import_export.py index cfc5682778..af6f24697b 100644 --- a/zerver/tests/test_import_export.py +++ b/zerver/tests/test_import_export.py @@ -10,6 +10,7 @@ from django.conf import settings from django.core.exceptions import ValidationError from django.db.models import Q, QuerySet from django.utils.timezone import now as timezone_now +from typing_extensions import override from analytics.models import UserCount from zerver.actions.alert_words import do_add_alert_words @@ -140,6 +141,7 @@ class ExportFile(ZulipTestCase): """This class is a container for shared helper functions used for both the realm-level and user-level export tests.""" + @override def setUp(self) -> None: super().setUp() assert settings.LOCAL_UPLOADS_DIR is not None diff --git a/zerver/tests/test_invite.py b/zerver/tests/test_invite.py index d97497327b..b36647015d 100644 --- a/zerver/tests/test_invite.py +++ b/zerver/tests/test_invite.py @@ -15,6 +15,7 @@ from django.test import override_settings from django.urls import reverse from django.utils.timezone import now as timezone_now from returns.curry import partial +from typing_extensions import override from confirmation import settings as confirmation_settings from confirmation.models import ( @@ -2239,6 +2240,7 @@ class InvitationsTestCase(InviteUserBase): class InviteeEmailsParserTests(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() self.email1 = "email1@zulip.com" @@ -2269,6 +2271,7 @@ class InviteeEmailsParserTests(ZulipTestCase): class MultiuseInviteTest(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() self.realm = get_realm("zulip") diff --git a/zerver/tests/test_link_embed.py b/zerver/tests/test_link_embed.py index e24e27af50..835d786b59 100644 --- a/zerver/tests/test_link_embed.py +++ b/zerver/tests/test_link_embed.py @@ -9,6 +9,7 @@ from django.test import override_settings from django.utils.html import escape from pyoembed.providers import get_provider from requests.exceptions import ConnectionError +from typing_extensions import override from zerver.actions.message_delete import do_delete_messages from zerver.lib.cache import cache_delete, cache_get, preview_url_cache_key @@ -321,6 +322,7 @@ class PreviewTestCase(ZulipTestCase): """ + @override def setUp(self) -> None: super().setUp() Realm.objects.all().update(inline_url_embed_preview=True) diff --git a/zerver/tests/test_management_commands.py b/zerver/tests/test_management_commands.py index 8000d409ce..e9645d0ab5 100644 --- a/zerver/tests/test_management_commands.py +++ b/zerver/tests/test_management_commands.py @@ -12,6 +12,7 @@ from django.core.management import call_command, find_commands from django.core.management.base import CommandError from django.test import override_settings from django.utils.timezone import now as timezone_now +from typing_extensions import override from confirmation.models import RealmCreationKey, generate_realm_creation_url from zerver.actions.create_user import do_create_user @@ -47,6 +48,7 @@ class TestCheckConfig(ZulipTestCase): class TestZulipBaseCommand(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() self.zulip_realm = get_realm("zulip") @@ -200,6 +202,7 @@ class TestZulipBaseCommand(ZulipTestCase): class TestCommandsCanStart(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() self.commands = [ @@ -224,6 +227,7 @@ class TestCommandsCanStart(ZulipTestCase): class TestSendWebhookFixtureMessage(ZulipTestCase): COMMAND_NAME = "send_webhook_fixture_message" + @override def setUp(self) -> None: super().setUp() self.fixture_path = os.path.join("some", "fake", "path.json") diff --git a/zerver/tests/test_markdown.py b/zerver/tests/test_markdown.py index 24a2990e30..3e887d51f1 100644 --- a/zerver/tests/test_markdown.py +++ b/zerver/tests/test_markdown.py @@ -11,6 +11,7 @@ from bs4 import BeautifulSoup from django.conf import settings from django.test import override_settings from markdown import Markdown +from typing_extensions import override from zerver.actions.alert_words import do_add_alert_words from zerver.actions.create_realm import do_create_realm @@ -76,9 +77,11 @@ from zerver.models import ( class SimulatedFencedBlockPreprocessor(FencedBlockPreprocessor): # Simulate code formatting. + @override def format_code(self, lang: Optional[str], code: str) -> str: return (lang or "") + ":" + code + @override def placeholder(self, s: str) -> str: return "**" + s.strip("\n") + "**" @@ -387,10 +390,12 @@ Outside. Should convert:<> class MarkdownTest(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() clear_state_for_testing() + @override def assertEqual(self, first: Any, second: Any, msg: str = "") -> None: if isinstance(first, str) and isinstance(second, str): if first != second: diff --git a/zerver/tests/test_message_fetch.py b/zerver/tests/test_message_fetch.py index abd6ed92b7..f954b8f49f 100644 --- a/zerver/tests/test_message_fetch.py +++ b/zerver/tests/test_message_fetch.py @@ -8,6 +8,7 @@ from django.test import override_settings from django.utils.timezone import now as timezone_now from sqlalchemy.sql import ClauseElement, Select, and_, column, select, table from sqlalchemy.types import Integer +from typing_extensions import override from analytics.lib.counts import COUNT_STATS from analytics.models import RealmCount @@ -101,6 +102,7 @@ def first_visible_id_as(message_id: int) -> Any: class NarrowBuilderTest(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() self.realm = get_realm("zulip") diff --git a/zerver/tests/test_message_flags.py b/zerver/tests/test_message_flags.py index 3662266f64..2e7371f22a 100644 --- a/zerver/tests/test_message_flags.py +++ b/zerver/tests/test_message_flags.py @@ -3,6 +3,7 @@ from unittest import mock import orjson from django.db import connection, transaction +from typing_extensions import override from zerver.actions.message_flags import do_update_message_flags from zerver.actions.streams import do_change_stream_permission @@ -166,6 +167,7 @@ class FirstUnreadAnchorTests(ZulipTestCase): class UnreadCountTests(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() with mock.patch( diff --git a/zerver/tests/test_migrations.py b/zerver/tests/test_migrations.py index e6057637a5..a71b263414 100644 --- a/zerver/tests/test_migrations.py +++ b/zerver/tests/test_migrations.py @@ -9,6 +9,7 @@ from unittest import skip import orjson from django.db.migrations.state import StateApps +from typing_extensions import override from zerver.lib.test_classes import MigrationsTestCase from zerver.lib.test_helpers import use_db_models @@ -35,6 +36,7 @@ class ScheduledEmailData(MigrationsTestCase): migrate_to = "0468_rename_followup_day_email_templates" @use_db_models + @override def setUpBeforeMigration(self, apps: StateApps) -> None: iago = self.example_user("iago") ScheduledEmail = apps.get_model("zerver", "ScheduledEmail") diff --git a/zerver/tests/test_new_users.py b/zerver/tests/test_new_users.py index 8897edb0f5..40c0677408 100644 --- a/zerver/tests/test_new_users.py +++ b/zerver/tests/test_new_users.py @@ -6,6 +6,7 @@ from unittest import mock from django.conf import settings from django.core import mail from django.test import override_settings +from typing_extensions import override from corporate.lib.stripe import get_latest_seat_count from zerver.actions.create_user import notify_new_user @@ -127,6 +128,7 @@ class SendLoginEmailTest(ZulipTestCase): class TestBrowserAndOsUserAgentStrings(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() self.user_agents = [ diff --git a/zerver/tests/test_outgoing_http.py b/zerver/tests/test_outgoing_http.py index 7f0673e452..deddb52b39 100644 --- a/zerver/tests/test_outgoing_http.py +++ b/zerver/tests/test_outgoing_http.py @@ -4,6 +4,7 @@ from unittest import mock import requests import responses +from typing_extensions import override from urllib3.util import Retry from zerver.lib.outgoing_http import OutgoingSession @@ -11,6 +12,7 @@ from zerver.lib.test_classes import ZulipTestCase class RequestMockWithProxySupport(responses.RequestsMock): + @override def _on_request( self, adapter: requests.adapters.HTTPAdapter, @@ -29,6 +31,7 @@ class RequestMockWithProxySupport(responses.RequestsMock): class RequestMockWithTimeoutAsHeader(responses.RequestsMock): + @override def _on_request( self, adapter: requests.adapters.HTTPAdapter, diff --git a/zerver/tests/test_outgoing_webhook_interfaces.py b/zerver/tests/test_outgoing_webhook_interfaces.py index 2f5d441786..e56e3d2e45 100644 --- a/zerver/tests/test_outgoing_webhook_interfaces.py +++ b/zerver/tests/test_outgoing_webhook_interfaces.py @@ -3,6 +3,7 @@ from typing import Any, Dict from unittest import mock import requests +from typing_extensions import override from zerver.lib.avatar import get_gravatar_url from zerver.lib.exceptions import JsonableError @@ -23,6 +24,7 @@ from zerver.openapi.openapi import validate_against_openapi_schema class TestGenericOutgoingWebhookService(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() @@ -156,6 +158,7 @@ class TestGenericOutgoingWebhookService(ZulipTestCase): class TestSlackOutgoingWebhookService(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() self.bot_user = get_user("outgoing-webhook@zulip.com", get_realm("zulip")) diff --git a/zerver/tests/test_presence.py b/zerver/tests/test_presence.py index 38d4dd7e83..ff838cbbf8 100644 --- a/zerver/tests/test_presence.py +++ b/zerver/tests/test_presence.py @@ -5,6 +5,7 @@ from unittest import mock from django.conf import settings from django.utils.timezone import now as timezone_now +from typing_extensions import override from zerver.actions.users import do_deactivate_user from zerver.lib.presence import format_legacy_presence_dict, get_presence_dict_by_realm @@ -110,6 +111,7 @@ class UserPresenceModelTests(ZulipTestCase): class UserPresenceTests(ZulipTestCase): + @override def setUp(self) -> None: """ Create some initial, old presence data to make the intended set up diff --git a/zerver/tests/test_push_notifications.py b/zerver/tests/test_push_notifications.py index 262ddaa860..3bb93a49d3 100644 --- a/zerver/tests/test_push_notifications.py +++ b/zerver/tests/test_push_notifications.py @@ -21,6 +21,7 @@ from django.utils.crypto import get_random_string from django.utils.timezone import now from requests.exceptions import ConnectionError from requests.models import PreparedRequest +from typing_extensions import override from analytics.lib.counts import CountStat, LoggingCountStat from analytics.models import InstallationCount, RealmCount @@ -92,6 +93,7 @@ if settings.ZILENCER_ENABLED: @skipUnless(settings.ZILENCER_ENABLED, "requires zilencer") class BouncerTestCase(ZulipTestCase): + @override def setUp(self) -> None: self.server_uuid = "6cde5f7a-1f7e-4978-9716-49f69ebfc9fe" self.server = RemoteZulipServer( @@ -103,6 +105,7 @@ class BouncerTestCase(ZulipTestCase): self.server.save() super().setUp() + @override def tearDown(self) -> None: RemoteZulipServer.objects.filter(uuid=self.server_uuid).delete() super().tearDown() @@ -1032,6 +1035,7 @@ class AnalyticsBouncerTest(BouncerTestCase): class PushNotificationTest(BouncerTestCase): + @override def setUp(self) -> None: super().setUp() self.user_profile = self.example_user("hamlet") @@ -1136,6 +1140,7 @@ class HandlePushNotificationTest(PushNotificationTest): self.user_profile = self.example_user("hamlet") self.soft_deactivate_user(self.user_profile) + @override def request_callback(self, request: PreparedRequest) -> Tuple[int, ResponseHeaders, bytes]: assert request.url is not None # allow mypy to infer url is present. assert settings.PUSH_NOTIFICATION_BOUNCER_URL is not None @@ -2678,6 +2683,7 @@ class GCMParseOptionsTest(ZulipTestCase): @mock.patch("zerver.lib.push_notifications.gcm_client") class GCMSendTest(PushNotificationTest): + @override def setUp(self) -> None: super().setUp() self.setup_gcm_tokens() diff --git a/zerver/tests/test_queue.py b/zerver/tests/test_queue.py index 9c058685f3..fb2f246c10 100644 --- a/zerver/tests/test_queue.py +++ b/zerver/tests/test_queue.py @@ -4,6 +4,7 @@ from unittest import mock import orjson from django.test import override_settings from pika.exceptions import AMQPConnectionError, ConnectionClosed +from typing_extensions import override from zerver.lib.queue import ( SimpleQueueClient, @@ -114,6 +115,7 @@ class TestQueueImplementation(ZulipTestCase): assert message is None @override_settings(USING_RABBITMQ=True) + @override def setUp(self) -> None: queue_client = get_queue_client() assert queue_client.channel diff --git a/zerver/tests/test_queue_worker.py b/zerver/tests/test_queue_worker.py index 8fd98c878c..2887071580 100644 --- a/zerver/tests/test_queue_worker.py +++ b/zerver/tests/test_queue_worker.py @@ -15,7 +15,7 @@ import time_machine from django.conf import settings from django.db.utils import IntegrityError from django.test import override_settings -from typing_extensions import TypeAlias +from typing_extensions import TypeAlias, override from zerver.lib.email_mirror import RateLimitedRealmMirror from zerver.lib.email_mirror_helpers import encode_email_address @@ -626,6 +626,7 @@ class WorkerTest(ZulipTestCase): @queue_processors.assign_queue("unreliable_worker", is_test_queue=True) class UnreliableWorker(queue_processors.QueueProcessingWorker): + @override def consume(self, data: Mapping[str, Any]) -> None: if data["type"] == "unexpected behaviour": raise Exception("Worker task not performing as expected!") @@ -661,6 +662,7 @@ class WorkerTest(ZulipTestCase): @queue_processors.assign_queue("unreliable_loopworker", is_test_queue=True) class UnreliableLoopWorker(queue_processors.LoopQueueProcessingWorker): + @override def consume_batch(self, events: List[Dict[str, Any]]) -> None: for event in events: if event["type"] == "unexpected behaviour": @@ -702,6 +704,7 @@ class WorkerTest(ZulipTestCase): class TimeoutWorker(queue_processors.QueueProcessingWorker): MAX_CONSUME_SECONDS = 1 + @override def consume(self, data: Mapping[str, Any]) -> None: if data["type"] == "timeout": time.sleep(1.5) @@ -755,6 +758,7 @@ class WorkerTest(ZulipTestCase): class TimeoutWorker(FetchLinksEmbedData): MAX_CONSUME_SECONDS = 1 + @override def consume(self, data: Mapping[str, Any]) -> None: # Send SIGALRM to ourselves to simulate a timeout. pid = os.getpid() @@ -785,6 +789,7 @@ class WorkerTest(ZulipTestCase): def __init__(self) -> None: super().__init__() + @override def consume(self, data: Mapping[str, Any]) -> None: pass # nocoverage # this is intentionally not called diff --git a/zerver/tests/test_rate_limiter.py b/zerver/tests/test_rate_limiter.py index d972175d36..b56c8c9995 100644 --- a/zerver/tests/test_rate_limiter.py +++ b/zerver/tests/test_rate_limiter.py @@ -4,6 +4,8 @@ from abc import ABC, abstractmethod from typing import Dict, List, Tuple, Type from unittest import mock +from typing_extensions import override + from zerver.lib.rate_limiter import ( RateLimitedIPAddr, RateLimitedObject, @@ -27,9 +29,11 @@ class RateLimitedTestObject(RateLimitedObject): self._rules.sort(key=lambda x: x[0]) super().__init__(backend) + @override def key(self) -> str: return RANDOM_KEY_PREFIX + self.name + @override def rules(self) -> List[Tuple[int, int]]: return self._rules @@ -37,6 +41,7 @@ class RateLimitedTestObject(RateLimitedObject): class RateLimiterBackendBase(ZulipTestCase, ABC): backend: Type[RateLimiterBackend] + @override def setUp(self) -> None: super().setUp() self.requests_record: Dict[str, List[float]] = {} @@ -155,6 +160,7 @@ class RateLimiterBackendBase(ZulipTestCase, ABC): class RedisRateLimiterBackendTest(RateLimiterBackendBase): backend = RedisRateLimiterBackend + @override def api_calls_left_from_history( self, history: List[float], max_window: int, max_calls: int, now: float ) -> Tuple[int, float]: @@ -180,6 +186,7 @@ class RedisRateLimiterBackendTest(RateLimiterBackendBase): class TornadoInMemoryRateLimiterBackendTest(RateLimiterBackendBase): backend = TornadoInMemoryRateLimiterBackend + @override def api_calls_left_from_history( self, history: List[float], max_window: int, max_calls: int, now: float ) -> Tuple[int, float]: diff --git a/zerver/tests/test_reactions.py b/zerver/tests/test_reactions.py index 3c3c635540..b015896e04 100644 --- a/zerver/tests/test_reactions.py +++ b/zerver/tests/test_reactions.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING, Any, Dict, List from unittest import mock import orjson +from typing_extensions import override from zerver.actions.reactions import notify_reaction_update from zerver.actions.streams import do_change_stream_permission @@ -672,6 +673,7 @@ class EmojiReactionBase(ZulipTestCase): class DefaultEmojiReactionTests(EmojiReactionBase): + @override def setUp(self) -> None: super().setUp() reaction_info = { @@ -937,6 +939,7 @@ class ZulipExtraEmojiReactionTest(EmojiReactionBase): class RealmEmojiReactionTests(EmojiReactionBase): + @override def setUp(self) -> None: super().setUp() green_tick_emoji = RealmEmoji.objects.get(name="green_tick") diff --git a/zerver/tests/test_realm.py b/zerver/tests/test_realm.py index 3bcb811baf..23174d0080 100644 --- a/zerver/tests/test_realm.py +++ b/zerver/tests/test_realm.py @@ -8,6 +8,7 @@ from unittest import mock import orjson from django.conf import settings from django.utils.timezone import now as timezone_now +from typing_extensions import override from confirmation.models import Confirmation, create_confirmation_link from zerver.actions.create_realm import do_change_realm_subdomain, do_create_realm @@ -1156,6 +1157,7 @@ class RealmTest(ZulipTestCase): class RealmAPITest(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() self.login("desdemona") diff --git a/zerver/tests/test_realm_domains.py b/zerver/tests/test_realm_domains.py index d038eaa233..9faacd31ec 100644 --- a/zerver/tests/test_realm_domains.py +++ b/zerver/tests/test_realm_domains.py @@ -1,6 +1,7 @@ import orjson from django.core.exceptions import ValidationError from django.db.utils import IntegrityError +from typing_extensions import override from zerver.actions.create_realm import do_create_realm from zerver.actions.realm_domains import do_change_realm_domain, do_remove_realm_domain @@ -13,6 +14,7 @@ from zerver.models import DomainNotAllowedForRealmError, RealmDomain, UserProfil class RealmDomainTest(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() realm = get_realm("zulip") diff --git a/zerver/tests/test_realm_linkifiers.py b/zerver/tests/test_realm_linkifiers.py index ac737cc083..c4a7d3f406 100644 --- a/zerver/tests/test_realm_linkifiers.py +++ b/zerver/tests/test_realm_linkifiers.py @@ -3,12 +3,14 @@ from typing import List import orjson from django.core.exceptions import ValidationError +from typing_extensions import override from zerver.lib.test_classes import ZulipTestCase from zerver.models import RealmAuditLog, RealmFilter, url_template_validator class RealmFilterTest(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() iago = self.example_user("iago") diff --git a/zerver/tests/test_redis_utils.py b/zerver/tests/test_redis_utils.py index 0daa9f5afa..8369396cf3 100644 --- a/zerver/tests/test_redis_utils.py +++ b/zerver/tests/test_redis_utils.py @@ -1,6 +1,7 @@ from unittest import mock from redis import StrictRedis +from typing_extensions import override from zerver.lib.redis_utils import ( MAX_KEY_LENGTH, @@ -19,6 +20,7 @@ class RedisUtilsTest(ZulipTestCase): redis_client: "StrictRedis[bytes]" @classmethod + @override def setUpClass(cls) -> None: cls.redis_client = get_redis_client() return super().setUpClass() diff --git a/zerver/tests/test_retention.py b/zerver/tests/test_retention.py index 8f6d7b61e8..424e19dd52 100644 --- a/zerver/tests/test_retention.py +++ b/zerver/tests/test_retention.py @@ -4,6 +4,7 @@ from unittest import mock from django.conf import settings from django.utils.timezone import now as timezone_now +from typing_extensions import override from zerver.actions.create_realm import do_create_realm from zerver.actions.message_delete import do_delete_messages @@ -103,6 +104,7 @@ class RetentionTestingBase(ZulipTestCase): class ArchiveMessagesTestingBase(RetentionTestingBase): + @override def setUp(self) -> None: super().setUp() self.zulip_realm = get_realm("zulip") @@ -576,6 +578,7 @@ class TestArchivingReactions(ArchiveMessagesTestingBase): class MoveMessageToArchiveBase(RetentionTestingBase): + @override def setUp(self) -> None: super().setUp() self.sender = self.example_user("hamlet") diff --git a/zerver/tests/test_scim.py b/zerver/tests/test_scim.py index 0e73f5a74d..1bab9984d4 100644 --- a/zerver/tests/test_scim.py +++ b/zerver/tests/test_scim.py @@ -5,6 +5,7 @@ from unittest import mock import orjson from django.conf import settings +from typing_extensions import override from zerver.actions.user_settings import do_change_full_name from zerver.lib.scim import ZulipSCIMUser @@ -20,6 +21,7 @@ class SCIMHeadersDict(TypedDict): class SCIMTestCase(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() self.realm = get_realm("zulip") diff --git a/zerver/tests/test_service_bot_system.py b/zerver/tests/test_service_bot_system.py index c73065887c..5eda173fbc 100644 --- a/zerver/tests/test_service_bot_system.py +++ b/zerver/tests/test_service_bot_system.py @@ -5,7 +5,7 @@ from unittest import mock import orjson from django.conf import settings from django.test import override_settings -from typing_extensions import Concatenate, ParamSpec +from typing_extensions import Concatenate, ParamSpec, override from zerver.actions.create_user import do_create_user from zerver.actions.message_send import get_service_bot_events @@ -171,6 +171,7 @@ class TestServiceBotBasics(ZulipTestCase): class TestServiceBotStateHandler(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() self.user_profile = self.example_user("othello") @@ -348,6 +349,7 @@ class TestServiceBotStateHandler(ZulipTestCase): class TestServiceBotConfigHandler(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() self.user_profile = self.example_user("othello") @@ -452,6 +454,7 @@ def patch_queue_publish( class TestServiceBotEventTriggers(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() self.user_profile = self.example_user("othello") diff --git a/zerver/tests/test_sessions.py b/zerver/tests/test_sessions.py index 305a02578b..dd63347fd1 100644 --- a/zerver/tests/test_sessions.py +++ b/zerver/tests/test_sessions.py @@ -3,6 +3,7 @@ from typing import Any, Callable from unittest import mock from django.utils.timezone import now as timezone_now +from typing_extensions import override from zerver.actions.realm_settings import do_set_realm_property from zerver.actions.users import change_user_is_active @@ -126,6 +127,7 @@ class TestSessions(ZulipTestCase): class TestExpirableSessionVars(ZulipTestCase): + @override def setUp(self) -> None: self.session = self.client.session super().setUp() diff --git a/zerver/tests/test_slack_message_conversion.py b/zerver/tests/test_slack_message_conversion.py index 081809dd7f..6de5ba07f8 100644 --- a/zerver/tests/test_slack_message_conversion.py +++ b/zerver/tests/test_slack_message_conversion.py @@ -2,6 +2,7 @@ import os from typing import Any, Dict, List, Tuple import orjson +from typing_extensions import override from zerver.data_import.slack_message_conversion import ( convert_to_zulip_markdown, @@ -12,6 +13,7 @@ from zerver.lib.test_classes import ZulipTestCase class SlackMessageConversion(ZulipTestCase): + @override def assertEqual(self, first: Any, second: Any, msg: str = "") -> None: if isinstance(first, str) and isinstance(second, str): if first != second: diff --git a/zerver/tests/test_subs.py b/zerver/tests/test_subs.py index dba68ebe7a..fa9ef0f97e 100644 --- a/zerver/tests/test_subs.py +++ b/zerver/tests/test_subs.py @@ -10,6 +10,7 @@ from django.conf import settings from django.core.exceptions import ValidationError from django.http import HttpResponse from django.utils.timezone import now as timezone_now +from typing_extensions import override from zerver.actions.bots import do_change_bot_owner from zerver.actions.create_realm import do_create_realm @@ -3932,6 +3933,7 @@ class SubscriptionRestApiTest(ZulipTestCase): class SubscriptionAPITest(ZulipTestCase): + @override def setUp(self) -> None: """ All tests will be logged in as hamlet. Also save various useful values @@ -5821,6 +5823,7 @@ class InviteOnlyStreamTest(ZulipTestCase): class GetSubscribersTest(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() self.user_profile = self.example_user("hamlet") diff --git a/zerver/tests/test_tornado.py b/zerver/tests/test_tornado.py index 1475bd1046..d2b920582f 100644 --- a/zerver/tests/test_tornado.py +++ b/zerver/tests/test_tornado.py @@ -14,7 +14,7 @@ from django.test import override_settings from tornado import netutil from tornado.httpclient import AsyncHTTPClient, HTTPResponse from tornado.httpserver import HTTPServer -from typing_extensions import ParamSpec +from typing_extensions import ParamSpec, override from zerver.lib.test_classes import ZulipTestCase from zerver.tornado import event_queue @@ -39,6 +39,7 @@ async def in_django_thread(f: Callable[[], T]) -> T: class TornadoWebTestCase(ZulipTestCase): @async_to_sync_decorator + @override async def setUp(self) -> None: super().setUp() @@ -53,11 +54,13 @@ class TornadoWebTestCase(ZulipTestCase): self.session_cookie: Optional[Dict[str, str]] = None @async_to_sync_decorator + @override async def tearDown(self) -> None: self.http_client.close() self.http_server.stop() super().tearDown() + @override def run(self, result: Optional[TestResult] = None) -> Optional[TestResult]: return async_to_sync( sync_to_async(super().run, thread_sensitive=False), force_new_loop=True @@ -73,6 +76,7 @@ class TornadoWebTestCase(ZulipTestCase): f"http://127.0.0.1:{self.port}{path}", method=method, **kwargs ) + @override def login_user(self, *args: Any, **kwargs: Any) -> None: super().login_user(*args, **kwargs) session_cookie = settings.SESSION_COOKIE_NAME diff --git a/zerver/tests/test_tutorial.py b/zerver/tests/test_tutorial.py index 84d5740728..caa710e5eb 100644 --- a/zerver/tests/test_tutorial.py +++ b/zerver/tests/test_tutorial.py @@ -1,4 +1,5 @@ from django.conf import settings +from typing_extensions import override from zerver.actions.message_send import internal_send_private_message from zerver.lib.test_classes import ZulipTestCase @@ -7,6 +8,7 @@ from zerver.models import UserProfile, get_system_bot class TutorialTests(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() # This emulates the welcome message sent by the welcome bot to hamlet@zulip.com diff --git a/zerver/tests/test_typed_endpoint.py b/zerver/tests/test_typed_endpoint.py index d902482f3e..818934e5db 100644 --- a/zerver/tests/test_typed_endpoint.py +++ b/zerver/tests/test_typed_endpoint.py @@ -6,7 +6,7 @@ from django.http import HttpRequest, HttpResponse from pydantic import BaseModel, ConfigDict, Json, StringConstraints, ValidationInfo, WrapValidator from pydantic.dataclasses import dataclass from pydantic.functional_validators import ModelWrapValidatorHandler -from typing_extensions import Annotated +from typing_extensions import Annotated, override from zerver.lib.exceptions import ApiParamValidationError, JsonableError from zerver.lib.request import RequestConfusingParamsError, RequestVariableMissingError @@ -599,6 +599,7 @@ class ValidationErrorHandlingTest(ZulipTestCase): # data. error_message: str + @override def __repr__(self) -> str: return f"Pydantic error type: {self.error_type}; Parameter type: {self.param_type}; Expected error message: {self.error_message}" diff --git a/zerver/tests/test_upload.py b/zerver/tests/test_upload.py index 9bc1d9c26c..9e57fd42d2 100644 --- a/zerver/tests/test_upload.py +++ b/zerver/tests/test_upload.py @@ -10,6 +10,7 @@ from unittest.mock import patch import orjson from django.conf import settings from PIL import Image +from typing_extensions import override from urllib3 import encode_multipart_formdata from urllib3.fields import RequestField @@ -1750,6 +1751,7 @@ class SanitizeNameTests(ZulipTestCase): class UploadSpaceTests(UploadSerializeMixin, ZulipTestCase): + @override def setUp(self) -> None: super().setUp() self.realm = get_realm("zulip") @@ -1790,6 +1792,7 @@ class UploadSpaceTests(UploadSerializeMixin, ZulipTestCase): class DecompressionBombTests(ZulipTestCase): + @override def setUp(self) -> None: super().setUp() self.test_urls = [ diff --git a/zerver/tests/test_webhooks_common.py b/zerver/tests/test_webhooks_common.py index b4966788a7..358d67f52a 100644 --- a/zerver/tests/test_webhooks_common.py +++ b/zerver/tests/test_webhooks_common.py @@ -4,6 +4,7 @@ from unittest.mock import MagicMock, patch from django.http import HttpRequest from django.http.response import HttpResponse +from typing_extensions import override from zerver.actions.streams import do_rename_stream from zerver.decorator import webhook_view @@ -145,6 +146,7 @@ class WebhookURLConfigurationTestCase(WebhookTestCase): WEBHOOK_DIR_NAME = "helloworld" URL_TEMPLATE = "/api/v1/external/helloworld?stream={stream}&api_key={api_key}" + @override def setUp(self) -> None: super().setUp() stream = self.subscribe(self.test_user, self.STREAM_NAME) @@ -213,5 +215,6 @@ class MissingEventHeaderTestCase(WebhookTestCase): self.assertEqual(msg.sender.id, notification_bot.id) self.assertEqual(msg.content, expected_message) + @override def get_body(self, fixture_name: str) -> str: return self.webhook_fixture_data("groove", fixture_name, file_type="json") diff --git a/zerver/tornado/django_api.py b/zerver/tornado/django_api.py index f092877042..89ad1b1c14 100644 --- a/zerver/tornado/django_api.py +++ b/zerver/tornado/django_api.py @@ -10,6 +10,7 @@ from django.db import transaction from requests.adapters import ConnectionError, HTTPAdapter from requests.models import PreparedRequest, Response from returns.curry import partial +from typing_extensions import override from urllib3.util import Retry from zerver.lib.queue import queue_json_publish @@ -31,6 +32,7 @@ class TornadoAdapter(HTTPAdapter): retry = Retry(total=3, backoff_factor=1, allowed_methods=retry_methods) super().__init__(max_retries=retry) + @override def send( self, request: PreparedRequest, diff --git a/zerver/tornado/event_queue.py b/zerver/tornado/event_queue.py index 6369189af2..3bc9761bd3 100644 --- a/zerver/tornado/event_queue.py +++ b/zerver/tornado/event_queue.py @@ -35,6 +35,7 @@ import tornado.ioloop from django.conf import settings from django.utils.translation import gettext as _ from tornado import autoreload +from typing_extensions import override from version import API_FEATURE_LEVEL, ZULIP_MERGE_BASE, ZULIP_VERSION from zerver.lib.exceptions import JsonableError @@ -157,6 +158,7 @@ class ClientDescriptor: linkifier_url_template=self.linkifier_url_template, ) + @override def __repr__(self) -> str: return f"ClientDescriptor<{self.event_queue.id}>" diff --git a/zerver/tornado/exceptions.py b/zerver/tornado/exceptions.py index 252cba6417..392d3c755d 100644 --- a/zerver/tornado/exceptions.py +++ b/zerver/tornado/exceptions.py @@ -1,4 +1,5 @@ from django.utils.translation import gettext as _ +from typing_extensions import override from zerver.lib.exceptions import ErrorCode, JsonableError @@ -11,5 +12,6 @@ class BadEventQueueIdError(JsonableError): self.queue_id: str = queue_id @staticmethod + @override def msg_format() -> str: return _("Bad event queue ID: {queue_id}") diff --git a/zerver/tornado/handlers.py b/zerver/tornado/handlers.py index 3082db3df0..a2effd58b2 100644 --- a/zerver/tornado/handlers.py +++ b/zerver/tornado/handlers.py @@ -14,6 +14,7 @@ from django.urls import set_script_prefix from django.utils.cache import patch_vary_headers from tornado.iostream import StreamClosedError from tornado.wsgi import WSGIContainer +from typing_extensions import override from zerver.lib.response import AsynchronousResponse, json_response from zerver.tornado.descriptors import get_descriptor_by_handler_id @@ -82,6 +83,7 @@ def finish_handler(handler_id: int, event_queue_id: str, contents: List[Dict[str class AsyncDjangoHandler(tornado.web.RequestHandler): handler_id: int + @override def initialize(self, django_handler: base.BaseHandler) -> None: self.django_handler = django_handler @@ -94,6 +96,7 @@ class AsyncDjangoHandler(tornado.web.RequestHandler): self._request: Optional[HttpRequest] = None + @override def __repr__(self) -> str: descriptor = get_descriptor_by_handler_id(self.handler_id) return f"AsyncDjangoHandler<{self.handler_id}, {descriptor}>" @@ -154,6 +157,7 @@ class AsyncDjangoHandler(tornado.web.RequestHandler): with suppress(StreamClosedError): await self.finish() + @override async def get(self, *args: Any, **kwargs: Any) -> None: request = await self.convert_tornado_request_to_django_request() response = await sync_to_async( @@ -191,15 +195,19 @@ class AsyncDjangoHandler(tornado.web.RequestHandler): # connections. await sync_to_async(response.close, thread_sensitive=True)() + @override async def head(self, *args: Any, **kwargs: Any) -> None: await self.get(*args, **kwargs) + @override async def post(self, *args: Any, **kwargs: Any) -> None: await self.get(*args, **kwargs) + @override async def delete(self, *args: Any, **kwargs: Any) -> None: await self.get(*args, **kwargs) + @override def on_connection_close(self) -> None: # Register a Tornado handler that runs when client-side # connections are closed to notify the events system. diff --git a/zerver/transaction_tests/test_user_groups.py b/zerver/transaction_tests/test_user_groups.py index d2ed29d9f4..42d407e3ba 100644 --- a/zerver/transaction_tests/test_user_groups.py +++ b/zerver/transaction_tests/test_user_groups.py @@ -5,6 +5,7 @@ from unittest import mock import orjson from django.db import OperationalError, connections, transaction from django.http import HttpRequest +from typing_extensions import override from zerver.actions.user_groups import add_subgroups_to_user_group, check_add_user_group from zerver.lib.exceptions import JsonableError @@ -65,6 +66,7 @@ class UserGroupRaceConditionTestCase(ZulipTransactionTestCase): counter = 0 CHAIN_LENGTH = 3 + @override def tearDown(self) -> None: # Clean up the user groups created to minimize leakage with transaction.atomic(): @@ -106,6 +108,7 @@ class UserGroupRaceConditionTestCase(ZulipTransactionTestCase): self.subgroup_ids = subgroup_ids self.supergroup_id = supergroup_id + @override def run(self) -> None: try: self.response = dev_update_subgroups( diff --git a/zerver/views/documentation.py b/zerver/views/documentation.py index 95f7628f52..682e5fe2ea 100644 --- a/zerver/views/documentation.py +++ b/zerver/views/documentation.py @@ -12,6 +12,7 @@ from django.views.generic import TemplateView from lxml import html from lxml.etree import Element, SubElement, XPath, _Element from markupsafe import Markup +from typing_extensions import override from zerver.context_processors import zulip_default_context from zerver.decorator import add_google_analytics_context @@ -63,6 +64,7 @@ def add_api_url_context(context: Dict[str, Any], request: HttpRequest) -> None: class ApiURLView(TemplateView): + @override def get_context_data(self, **kwargs: Any) -> Dict[str, str]: context = super().get_context_data(**kwargs) add_api_url_context(context, self.request) @@ -148,6 +150,7 @@ class MarkdownDirectoryView(ApiURLView): endpoint_method=endpoint_method, ) + @override def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: article = kwargs["article"] context: Dict[str, Any] = super().get_context_data() @@ -261,6 +264,7 @@ class MarkdownDirectoryView(ApiURLView): add_google_analytics_context(context) return context + @override def get( self, request: HttpRequest, *args: object, article: str = "", **kwargs: object ) -> HttpResponse: @@ -319,6 +323,7 @@ def add_integrations_open_graph_context(context: Dict[str, Any], request: HttpRe class IntegrationView(ApiURLView): template_name = "zerver/integrations/index.html" + @override def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: context: Dict[str, Any] = super().get_context_data(**kwargs) add_integrations_context(context) diff --git a/zerver/webhooks/beanstalk/tests.py b/zerver/webhooks/beanstalk/tests.py index 839ff52f25..3667a7aa78 100644 --- a/zerver/webhooks/beanstalk/tests.py +++ b/zerver/webhooks/beanstalk/tests.py @@ -1,6 +1,8 @@ from typing import Dict from unittest.mock import MagicMock, patch +from typing_extensions import override + from zerver.lib.test_classes import WebhookTestCase from zerver.lib.webhooks.git import COMMITS_LIMIT @@ -169,5 +171,6 @@ class BeanstalkHookTests(WebhookTestCase): self.test_user, "svn_changefile", expected_topic, expected_message, content_type=None ) + @override def get_payload(self, fixture_name: str) -> Dict[str, str]: return {"payload": self.webhook_fixture_data("beanstalk", fixture_name)} diff --git a/zerver/webhooks/deskdotcom/tests.py b/zerver/webhooks/deskdotcom/tests.py index 63fcff9e8a..3766865b4a 100644 --- a/zerver/webhooks/deskdotcom/tests.py +++ b/zerver/webhooks/deskdotcom/tests.py @@ -1,3 +1,5 @@ +from typing_extensions import override + from zerver.lib.test_classes import WebhookTestCase # Tests for the Desk.com webhook integration. @@ -76,5 +78,6 @@ class DeskDotComHookTests(WebhookTestCase): content_type="application/x-www-form-urlencoded", ) + @override def get_body(self, fixture_name: str) -> str: return self.webhook_fixture_data("deskdotcom", fixture_name, file_type="txt") diff --git a/zerver/webhooks/hellosign/tests.py b/zerver/webhooks/hellosign/tests.py index 78a6a74c06..9d4338fce1 100644 --- a/zerver/webhooks/hellosign/tests.py +++ b/zerver/webhooks/hellosign/tests.py @@ -1,5 +1,7 @@ from typing import Dict +from typing_extensions import override + from zerver.lib.test_classes import WebhookTestCase @@ -51,5 +53,6 @@ class HelloSignHookTests(WebhookTestCase): topic=expected_topic, ) + @override def get_payload(self, fixture_name: str) -> Dict[str, str]: return {"json": self.webhook_fixture_data("hellosign", fixture_name, file_type="json")} diff --git a/zerver/webhooks/heroku/tests.py b/zerver/webhooks/heroku/tests.py index 2c392ccb52..c927c22897 100644 --- a/zerver/webhooks/heroku/tests.py +++ b/zerver/webhooks/heroku/tests.py @@ -1,3 +1,5 @@ +from typing_extensions import override + from zerver.lib.test_classes import WebhookTestCase @@ -45,5 +47,6 @@ user@example.com deployed version 3eb5f44 of [sample-project](http://sample-proj content_type="application/x-www-form-urlencoded", ) + @override def get_body(self, fixture_name: str) -> str: return self.webhook_fixture_data("heroku", fixture_name, file_type="txt") diff --git a/zerver/webhooks/intercom/view.py b/zerver/webhooks/intercom/view.py index 17b2ca5ed5..2e2eefb92d 100644 --- a/zerver/webhooks/intercom/view.py +++ b/zerver/webhooks/intercom/view.py @@ -3,6 +3,7 @@ from typing import Callable, Dict, List, Tuple from django.http import HttpRequest, HttpResponse from returns.curry import partial +from typing_extensions import override from zerver.decorator import return_success_on_head_request, webhook_view from zerver.lib.exceptions import UnsupportedWebhookEventTypeError @@ -73,6 +74,7 @@ class MLStripper(HTMLParser): self.convert_charrefs = True self.fed: List[str] = [] + @override def handle_data(self, d: str) -> None: self.fed.append(d) diff --git a/zerver/webhooks/librato/tests.py b/zerver/webhooks/librato/tests.py index a3782dd0e9..e9ac587d3c 100644 --- a/zerver/webhooks/librato/tests.py +++ b/zerver/webhooks/librato/tests.py @@ -1,5 +1,7 @@ import urllib +from typing_extensions import override + from zerver.lib.test_classes import WebhookTestCase @@ -9,6 +11,7 @@ class LibratoHookTests(WebhookTestCase): WEBHOOK_DIR_NAME = "librato" IS_ATTACHMENT = False + @override def get_body(self, fixture_name: str) -> str: if self.IS_ATTACHMENT: return self.webhook_fixture_data("librato", fixture_name, file_type="json") diff --git a/zerver/webhooks/papertrail/tests.py b/zerver/webhooks/papertrail/tests.py index 6074a8c197..1862d14257 100644 --- a/zerver/webhooks/papertrail/tests.py +++ b/zerver/webhooks/papertrail/tests.py @@ -1,5 +1,7 @@ from urllib.parse import urlencode +from typing_extensions import override + from zerver.lib.test_classes import WebhookTestCase @@ -69,6 +71,7 @@ message body 4 self.assertIn("Events key is missing from payload", e.exception.args[0]) + @override def get_body(self, fixture_name: str) -> str: # Papertrail webhook sends a POST request with payload parameter # containing the JSON body. Documented here: diff --git a/zerver/webhooks/pivotal/tests.py b/zerver/webhooks/pivotal/tests.py index f738194cef..22622c9235 100644 --- a/zerver/webhooks/pivotal/tests.py +++ b/zerver/webhooks/pivotal/tests.py @@ -1,6 +1,7 @@ from unittest import mock import orjson +from typing_extensions import override from zerver.lib.exceptions import UnsupportedWebhookEventTypeError from zerver.lib.test_classes import WebhookTestCase @@ -102,6 +103,7 @@ class PivotalV3HookTests(WebhookTestCase): "type_changed", expected_topic, expected_message, content_type="application/xml" ) + @override def get_body(self, fixture_name: str) -> str: return self.webhook_fixture_data("pivotal", fixture_name, file_type="xml") @@ -222,5 +224,6 @@ Try again next time with mock.patch("zerver.webhooks.pivotal.view.orjson.loads", return_value=bad): api_pivotal_webhook_v5(request, hamlet) + @override def get_body(self, fixture_name: str) -> str: return self.webhook_fixture_data("pivotal", f"v5_{fixture_name}", file_type="json") diff --git a/zerver/webhooks/slack/tests.py b/zerver/webhooks/slack/tests.py index 4948d477db..396f5db592 100644 --- a/zerver/webhooks/slack/tests.py +++ b/zerver/webhooks/slack/tests.py @@ -1,3 +1,5 @@ +from typing_extensions import override + from zerver.lib.test_classes import WebhookTestCase @@ -52,5 +54,6 @@ class SlackWebhookTests(WebhookTestCase): result = self.client_post(url, payload, content_type="application/x-www-form-urlencoded") self.assert_json_error(result, "Error: channels_map_to_topics parameter other than 0 or 1") + @override def get_body(self, fixture_name: str) -> str: return self.webhook_fixture_data("slack", fixture_name, file_type="txt") diff --git a/zerver/webhooks/slack_incoming/tests.py b/zerver/webhooks/slack_incoming/tests.py index f6d368f059..c6f7696060 100644 --- a/zerver/webhooks/slack_incoming/tests.py +++ b/zerver/webhooks/slack_incoming/tests.py @@ -1,3 +1,5 @@ +from typing_extensions import override + from zerver.lib.test_classes import WebhookTestCase @@ -231,6 +233,7 @@ Value without title expected_message, ) + @override def get_body(self, fixture_name: str) -> str: if "urlencoded" in fixture_name: file_type = "txt" diff --git a/zerver/webhooks/taiga/tests.py b/zerver/webhooks/taiga/tests.py index 265cef910e..936e4f5baf 100644 --- a/zerver/webhooks/taiga/tests.py +++ b/zerver/webhooks/taiga/tests.py @@ -1,3 +1,5 @@ +from typing_extensions import override + from zerver.lib.test_classes import WebhookTestCase @@ -7,6 +9,7 @@ class TaigaHookTests(WebhookTestCase): URL_TEMPLATE = "/api/v1/external/taiga?stream={stream}&api_key={api_key}" WEBHOOK_DIR_NAME = "taiga" + @override def setUp(self) -> None: super().setUp() self.url = self.build_webhook_url(topic=self.TOPIC) diff --git a/zerver/webhooks/transifex/tests.py b/zerver/webhooks/transifex/tests.py index 7c554f5566..5c43fad235 100644 --- a/zerver/webhooks/transifex/tests.py +++ b/zerver/webhooks/transifex/tests.py @@ -1,5 +1,7 @@ from typing import Dict +from typing_extensions import override + from zerver.lib.test_classes import WebhookTestCase @@ -36,5 +38,6 @@ class TransifexHookTests(WebhookTestCase): ) self.check_webhook("", expected_topic, expected_message) + @override def get_payload(self, fixture_name: str) -> Dict[str, str]: return {} diff --git a/zerver/webhooks/travis/tests.py b/zerver/webhooks/travis/tests.py index d21417de33..bf2f4d4897 100644 --- a/zerver/webhooks/travis/tests.py +++ b/zerver/webhooks/travis/tests.py @@ -1,5 +1,7 @@ import urllib +from typing_extensions import override + from zerver.lib.test_classes import WebhookTestCase @@ -127,6 +129,7 @@ one or more new messages. ) self.assertEqual(str(exc.exception), expected_error_message) + @override def get_body(self, fixture_name: str) -> str: return urllib.parse.urlencode( {"payload": self.webhook_fixture_data("travis", fixture_name, file_type="json")} diff --git a/zerver/webhooks/wekan/tests.py b/zerver/webhooks/wekan/tests.py index 981c4d239d..f8707409bc 100644 --- a/zerver/webhooks/wekan/tests.py +++ b/zerver/webhooks/wekan/tests.py @@ -1,3 +1,5 @@ +from typing_extensions import override + from zerver.lib.test_classes import WebhookTestCase @@ -213,5 +215,6 @@ class WekanHookTests(WebhookTestCase): content_type="application/x-www-form-urlencoded", ) + @override def get_body(self, fixture_name: str) -> str: return self.webhook_fixture_data("wekan", fixture_name, file_type="json") diff --git a/zerver/webhooks/wordpress/tests.py b/zerver/webhooks/wordpress/tests.py index ad84ed2e92..95230a9c01 100644 --- a/zerver/webhooks/wordpress/tests.py +++ b/zerver/webhooks/wordpress/tests.py @@ -1,3 +1,5 @@ +from typing_extensions import override + from zerver.lib.test_classes import WebhookTestCase @@ -107,5 +109,6 @@ class WordPressHookTests(WebhookTestCase): self.assert_json_error(result, "Unknown WordPress webhook action: WordPress action") + @override def get_body(self, fixture_name: str) -> str: return self.webhook_fixture_data("wordpress", fixture_name, file_type="txt") diff --git a/zerver/webhooks/zendesk/tests.py b/zerver/webhooks/zendesk/tests.py index a4f5fc3fd0..4671f8e16b 100644 --- a/zerver/webhooks/zendesk/tests.py +++ b/zerver/webhooks/zendesk/tests.py @@ -1,5 +1,7 @@ from typing import Dict +from typing_extensions import override + from zerver.lib.test_classes import WebhookTestCase @@ -7,6 +9,7 @@ class ZenDeskHookTests(WebhookTestCase): STREAM_NAME = "zendesk" URL_TEMPLATE = "/api/v1/external/zendesk?stream={stream}" + @override def get_payload(self, fixture_name: str) -> Dict[str, str]: return { "ticket_title": self.TICKET_TITLE, diff --git a/zerver/worker/queue_processors.py b/zerver/worker/queue_processors.py index dd7193b6a6..63ef4d9a65 100644 --- a/zerver/worker/queue_processors.py +++ b/zerver/worker/queue_processors.py @@ -44,6 +44,7 @@ from django.utils.translation import gettext as _ from django.utils.translation import override as override_language from returns.curry import partial from sentry_sdk import add_breadcrumb, configure_scope +from typing_extensions import override from zulip_bots.lib import extract_query_without_mention from zerver.actions.invites import do_send_confirmation_email @@ -116,6 +117,7 @@ class WorkerTimeoutError(Exception): self.limit = limit self.event_count = event_count + @override def __str__(self) -> str: return f"Timed out in {self.queue_name} after {self.limit * self.event_count} seconds processing {self.event_count} events" @@ -416,9 +418,11 @@ class LoopQueueProcessingWorker(QueueProcessingWorker): sleep_delay = 1 batch_size = 100 + @override def setup(self) -> None: self.q = SimpleQueueClient(prefetch=max(self.PREFETCH, self.batch_size)) + @override def start(self) -> None: # nocoverage assert self.q is not None self.initialize_statistics() @@ -433,6 +437,7 @@ class LoopQueueProcessingWorker(QueueProcessingWorker): def consume_batch(self, events: List[Dict[str, Any]]) -> None: pass + @override def consume(self, event: Dict[str, Any]) -> None: """In LoopQueueProcessingWorker, consume is used just for automated tests""" self.consume_batch([event]) @@ -440,6 +445,7 @@ class LoopQueueProcessingWorker(QueueProcessingWorker): @assign_queue("invites") class ConfirmationEmailWorker(QueueProcessingWorker): + @override def consume(self, data: Mapping[str, Any]) -> None: if "invite_expires_in_days" in data: invite_expires_in_minutes = data["invite_expires_in_days"] * 24 * 60 @@ -513,11 +519,13 @@ class UserActivityWorker(LoopQueueProcessingWorker): client_id_map: Dict[str, int] = {} + @override def start(self) -> None: # For our unit tests to make sense, we need to clear this on startup. self.client_id_map = {} super().start() + @override def consume_batch(self, user_activity_events: List[Dict[str, Any]]) -> None: uncommitted_events: Dict[Tuple[int, int, str], Tuple[int, float]] = {} @@ -548,6 +556,7 @@ class UserActivityWorker(LoopQueueProcessingWorker): @assign_queue("user_activity_interval") class UserActivityIntervalWorker(QueueProcessingWorker): + @override def consume(self, event: Mapping[str, Any]) -> None: user_profile = get_user_profile_by_id(event["user_profile_id"]) log_time = timestamp_to_datetime(event["time"]) @@ -556,6 +565,7 @@ class UserActivityIntervalWorker(QueueProcessingWorker): @assign_queue("user_presence") class UserPresenceWorker(QueueProcessingWorker): + @override def consume(self, event: Mapping[str, Any]) -> None: logging.debug("Received presence event: %s", event) user_profile = get_user_profile_by_id(event["user_profile_id"]) @@ -586,6 +596,7 @@ class MissedMessageWorker(QueueProcessingWorker): # The main thread, which handles the RabbitMQ connection and creates # database rows from them. + @override def consume(self, event: Dict[str, Any]) -> None: logging.debug("Processing missedmessage_emails event: %s", event) # When we consume an event, check if there are existing pending emails @@ -649,6 +660,7 @@ class MissedMessageWorker(QueueProcessingWorker): "ScheduledMessageNotificationEmail row could not be created. The message may have been deleted. Skipping event." ) + @override def start(self) -> None: with self.cv: self.stopping = False @@ -762,6 +774,7 @@ class MissedMessageWorker(QueueProcessingWorker): events_to_process.delete() + @override def stop(self) -> None: with self.cv: self.stopping = True @@ -789,10 +802,12 @@ class EmailSendingWorker(LoopQueueProcessingWorker): self.connection = initialize_connection(self.connection) send_email(**copied_event, connection=self.connection) + @override def consume_batch(self, events: List[Dict[str, Any]]) -> None: for event in events: self.send_email(event) + @override def stop(self) -> None: try: self.connection.close() @@ -807,6 +822,7 @@ class PushNotificationsWorker(QueueProcessingWorker): # play well with asyncio. MAX_CONSUME_SECONDS = None + @override def start(self) -> None: # initialize_push_notifications doesn't strictly do anything # beyond printing some logging warnings if push notifications @@ -814,6 +830,7 @@ class PushNotificationsWorker(QueueProcessingWorker): initialize_push_notifications() super().start() + @override def consume(self, event: Dict[str, Any]) -> None: try: if event.get("type", "add") == "remove": @@ -836,6 +853,7 @@ class PushNotificationsWorker(QueueProcessingWorker): class DigestWorker(QueueProcessingWorker): # nocoverage # Who gets a digest is entirely determined by the enqueue_digest_emails # management command, not here. + @override def consume(self, event: Mapping[str, Any]) -> None: if "user_ids" in event: user_ids = event["user_ids"] @@ -847,6 +865,7 @@ class DigestWorker(QueueProcessingWorker): # nocoverage @assign_queue("email_mirror") class MirrorWorker(QueueProcessingWorker): + @override def consume(self, event: Mapping[str, Any]) -> None: rcpt_to = event["rcpt_to"] msg = email.message_from_bytes( @@ -877,6 +896,7 @@ class FetchLinksEmbedData(QueueProcessingWorker): # Update stats file after every consume call. CONSUME_ITERATIONS_BEFORE_UPDATE_STATS_NUM = 1 + @override def consume(self, event: Mapping[str, Any]) -> None: url_embed_data: Dict[str, Optional[UrlEmbedData]] = {} for url in event["urls"]: @@ -910,6 +930,7 @@ class FetchLinksEmbedData(QueueProcessingWorker): ) do_update_embedded_data(message.sender, message, message.content, rendering_result) + @override def timer_expired( self, limit: int, events: List[Dict[str, Any]], signal: int, frame: Optional[FrameType] ) -> None: @@ -928,6 +949,7 @@ class FetchLinksEmbedData(QueueProcessingWorker): @assign_queue("outgoing_webhooks") class OutgoingWebhookWorker(QueueProcessingWorker): + @override def consume(self, event: Dict[str, Any]) -> None: message = event["message"] event["command"] = message["content"] @@ -944,6 +966,7 @@ class EmbeddedBotWorker(QueueProcessingWorker): def get_bot_api_client(self, user_profile: UserProfile) -> EmbeddedBotHandler: return EmbeddedBotHandler(user_profile) + @override def consume(self, event: Mapping[str, Any]) -> None: user_profile_id = event["user_profile_id"] user_profile = get_user_profile_by_id(user_profile_id) @@ -991,6 +1014,7 @@ class DeferredWorker(QueueProcessingWorker): # remove any processing timeouts MAX_CONSUME_SECONDS = None + @override def consume(self, event: Dict[str, Any]) -> None: start = time.time() if event["type"] == "mark_stream_messages_as_read": @@ -1158,6 +1182,7 @@ class TestWorker(QueueProcessingWorker): # creating significant side effects. It can be useful in development or # for troubleshooting prod/staging. It pulls a message off the test queue # and appends it to a file in /var/log/zulip. + @override def consume(self, event: Mapping[str, Any]) -> None: # nocoverage fn = settings.ZULIP_WORKER_TEST_FILE message = orjson.dumps(event) @@ -1182,6 +1207,7 @@ class NoopWorker(QueueProcessingWorker): self.max_consume = max_consume self.slow_queries: Set[int] = set(slow_queries) + @override def consume(self, event: Mapping[str, Any]) -> None: self.consumed += 1 if self.consumed in self.slow_queries: @@ -1210,6 +1236,7 @@ class BatchNoopWorker(LoopQueueProcessingWorker): self.max_consume = max_consume self.slow_queries: Set[int] = set(slow_queries) + @override def consume_batch(self, events: List[Dict[str, Any]]) -> None: event_numbers = set(range(self.consumed + 1, self.consumed + 1 + len(events))) found_slow = self.slow_queries & event_numbers diff --git a/zilencer/auth.py b/zilencer/auth.py index 34786987f7..44141a0eb4 100644 --- a/zilencer/auth.py +++ b/zilencer/auth.py @@ -8,7 +8,7 @@ from django.urls.resolvers import URLPattern from django.utils.crypto import constant_time_compare from django.utils.translation import gettext as _ from django.views.decorators.csrf import csrf_exempt -from typing_extensions import Concatenate, ParamSpec +from typing_extensions import Concatenate, ParamSpec, override from zerver.decorator import get_basic_credentials, process_client from zerver.lib.exceptions import ( @@ -42,12 +42,14 @@ class InvalidZulipServerError(JsonableError): self.role: str = role @staticmethod + @override def msg_format() -> str: return "Zulip server auth failure: {role} is not registered -- did you run `manage.py register_server`?" class InvalidZulipServerKeyError(InvalidZulipServerError): @staticmethod + @override def msg_format() -> str: return "Zulip server auth failure: key does not match role {role}" diff --git a/zilencer/management/commands/add_mock_conversation.py b/zilencer/management/commands/add_mock_conversation.py index 5f681912b3..4cd919434e 100644 --- a/zilencer/management/commands/add_mock_conversation.py +++ b/zilencer/management/commands/add_mock_conversation.py @@ -1,6 +1,7 @@ from typing import Any, Dict, List from django.core.management.base import BaseCommand +from typing_extensions import override from zerver.actions.create_user import do_create_user from zerver.actions.message_send import do_send_messages, internal_prep_stream_message @@ -147,5 +148,6 @@ From image editing program: starr, preview_message, "thumbs_up", thumbs_up.emoji_code, thumbs_up.reaction_type ) + @override def handle(self, *args: Any, **options: str) -> None: self.add_message_formatting_conversation() diff --git a/zilencer/management/commands/calculate_first_visible_message_id.py b/zilencer/management/commands/calculate_first_visible_message_id.py index 4b87f0e399..cea5454813 100644 --- a/zilencer/management/commands/calculate_first_visible_message_id.py +++ b/zilencer/management/commands/calculate_first_visible_message_id.py @@ -1,6 +1,7 @@ from typing import Any, Iterable from django.core.management.base import CommandParser +from typing_extensions import override from zerver.lib.management import ZulipBaseCommand from zerver.lib.message import maybe_update_first_visible_message_id @@ -10,6 +11,7 @@ from zerver.models import Realm class Command(ZulipBaseCommand): help = """Calculate the value of first visible message ID and store it in cache""" + @override def add_arguments(self, parser: CommandParser) -> None: self.add_realm_args(parser) parser.add_argument( @@ -20,6 +22,7 @@ class Command(ZulipBaseCommand): required=True, ) + @override def handle(self, *args: Any, **options: Any) -> None: target_realm = self.get_realm(options) diff --git a/zilencer/management/commands/compare_messages.py b/zilencer/management/commands/compare_messages.py index 93218b0978..3c15aaf95b 100644 --- a/zilencer/management/commands/compare_messages.py +++ b/zilencer/management/commands/compare_messages.py @@ -2,6 +2,7 @@ from typing import Any import orjson from django.core.management.base import BaseCommand, CommandParser +from typing_extensions import override class Command(BaseCommand): @@ -10,10 +11,12 @@ class Command(BaseCommand): Usage: ./manage.py compare_messages """ + @override def add_arguments(self, parser: CommandParser) -> None: parser.add_argument("dump1", help="First file to compare") parser.add_argument("dump2", help="Second file to compare") + @override def handle(self, *args: Any, **options: Any) -> None: total_count = 0 changed_count = 0 diff --git a/zilencer/management/commands/downgrade_small_realms_behind_on_payments.py b/zilencer/management/commands/downgrade_small_realms_behind_on_payments.py index 5a9abf85d4..560aaca025 100644 --- a/zilencer/management/commands/downgrade_small_realms_behind_on_payments.py +++ b/zilencer/management/commands/downgrade_small_realms_behind_on_payments.py @@ -1,5 +1,7 @@ from typing import Any +from typing_extensions import override + from corporate.lib.stripe import downgrade_small_realms_behind_on_payments_as_needed from zerver.lib.management import ZulipBaseCommand @@ -7,5 +9,6 @@ from zerver.lib.management import ZulipBaseCommand class Command(ZulipBaseCommand): help = "Downgrade small realms that are running behind on payments" + @override def handle(self, *args: Any, **options: Any) -> None: downgrade_small_realms_behind_on_payments_as_needed() diff --git a/zilencer/management/commands/invoice_plans.py b/zilencer/management/commands/invoice_plans.py index 52757e7bf3..9cc0cf8b5b 100644 --- a/zilencer/management/commands/invoice_plans.py +++ b/zilencer/management/commands/invoice_plans.py @@ -1,6 +1,7 @@ from typing import Any from django.conf import settings +from typing_extensions import override from zerver.lib.management import ZulipBaseCommand @@ -11,6 +12,7 @@ if settings.BILLING_ENABLED: class Command(ZulipBaseCommand): help = """Generates invoices for customers if needed.""" + @override def handle(self, *args: Any, **options: Any) -> None: if settings.BILLING_ENABLED: invoice_plans_as_needed() diff --git a/zilencer/management/commands/mark_all_messages_unread.py b/zilencer/management/commands/mark_all_messages_unread.py index 1aa0d5f1c8..5008a77c25 100644 --- a/zilencer/management/commands/mark_all_messages_unread.py +++ b/zilencer/management/commands/mark_all_messages_unread.py @@ -5,6 +5,7 @@ from django.conf import settings from django.core.cache import cache from django.core.management.base import BaseCommand from django.db.models import F +from typing_extensions import override from zerver.models import UserMessage @@ -12,6 +13,7 @@ from zerver.models import UserMessage class Command(BaseCommand): help = """Script to mark all messages as unread.""" + @override def handle(self, *args: Any, **options: Any) -> None: assert settings.DEVELOPMENT UserMessage.objects.all().update(flags=F("flags").bitand(~UserMessage.flags.read)) diff --git a/zilencer/management/commands/populate_db.py b/zilencer/management/commands/populate_db.py index 270a02b99e..bda61c3cfc 100644 --- a/zilencer/management/commands/populate_db.py +++ b/zilencer/management/commands/populate_db.py @@ -16,6 +16,7 @@ from django.db import connection from django.db.models import F from django.db.models.signals import post_delete from django.utils.timezone import now as timezone_now +from typing_extensions import override from scripts.lib.zulip_tools import get_or_create_dev_uuid_var_path from zerver.actions.create_realm import do_create_realm @@ -196,6 +197,7 @@ def create_alert_words(realm_id: int) -> None: class Command(BaseCommand): help = "Populate a test database" + @override def add_arguments(self, parser: CommandParser) -> None: parser.add_argument( "-n", "--num-messages", type=int, default=1000, help="The number of messages to create." @@ -284,6 +286,7 @@ class Command(BaseCommand): "data set for the backend tests.", ) + @override def handle(self, *args: Any, **options: Any) -> None: # Suppress spammy output from the push notifications logger push_notifications_logger.disabled = True diff --git a/zilencer/management/commands/print_initial_password.py b/zilencer/management/commands/print_initial_password.py index 6835a7a9e5..9e94c870ef 100644 --- a/zilencer/management/commands/print_initial_password.py +++ b/zilencer/management/commands/print_initial_password.py @@ -1,6 +1,8 @@ from argparse import ArgumentParser from typing import Any +from typing_extensions import override + from zerver.lib.initial_password import initial_password from zerver.lib.management import ZulipBaseCommand from zerver.lib.users import get_api_key @@ -11,6 +13,7 @@ class Command(ZulipBaseCommand): fmt = "%-30s %-16s %-32s" + @override def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument( "emails", @@ -20,6 +23,7 @@ class Command(ZulipBaseCommand): ) self.add_realm_args(parser) + @override def handle(self, *args: Any, **options: str) -> None: realm = self.get_realm(options) print(self.fmt % ("email", "password", "API key")) diff --git a/zilencer/management/commands/profile_request.py b/zilencer/management/commands/profile_request.py index 2a2df0ac1e..40f6071cd1 100644 --- a/zilencer/management/commands/profile_request.py +++ b/zilencer/management/commands/profile_request.py @@ -6,6 +6,7 @@ from typing import Any from django.contrib.sessions.backends.base import SessionBase from django.core.management.base import CommandParser from django.http import HttpRequest, HttpResponseBase +from typing_extensions import override from zerver.lib.management import ZulipBaseCommand from zerver.lib.request import RequestNotes @@ -34,10 +35,12 @@ def profile_request(request: HttpRequest) -> HttpResponseBase: class Command(ZulipBaseCommand): + @override def add_arguments(self, parser: CommandParser) -> None: parser.add_argument("email", metavar="", help="Email address of the user") self.add_realm_args(parser) + @override def handle(self, *args: Any, **options: Any) -> None: realm = self.get_realm(options) user = self.get_user(options["email"], realm) diff --git a/zilencer/management/commands/queue_rate.py b/zilencer/management/commands/queue_rate.py index 6377c4ce2b..bcc04694f0 100644 --- a/zilencer/management/commands/queue_rate.py +++ b/zilencer/management/commands/queue_rate.py @@ -3,6 +3,7 @@ from timeit import timeit from typing import Any, Union from django.core.management.base import BaseCommand, CommandParser +from typing_extensions import override from zerver.lib.queue import SimpleQueueClient, queue_json_publish from zerver.worker.queue_processors import BatchNoopWorker, NoopWorker @@ -11,6 +12,7 @@ from zerver.worker.queue_processors import BatchNoopWorker, NoopWorker class Command(BaseCommand): help = """Times the overhead of enqueuing and dequeuing messages from RabbitMQ.""" + @override def add_arguments(self, parser: CommandParser) -> None: parser.add_argument( "--count", help="Number of messages to enqueue", default=10000, type=int @@ -33,6 +35,7 @@ class Command(BaseCommand): default=[], ) + @override def handle(self, *args: Any, **options: Any) -> None: print("Purging queue...") queue = SimpleQueueClient() diff --git a/zilencer/management/commands/render_messages.py b/zilencer/management/commands/render_messages.py index 4a7b5fee32..fc0ef03701 100644 --- a/zilencer/management/commands/render_messages.py +++ b/zilencer/management/commands/render_messages.py @@ -4,6 +4,7 @@ from typing import Any, Iterator import orjson from django.core.management.base import BaseCommand, CommandParser from django.db.models import QuerySet +from typing_extensions import override from zerver.lib.message import render_markdown from zerver.models import Message @@ -24,11 +25,13 @@ class Command(BaseCommand): Usage: ./manage.py render_messages [--amount=10000] """ + @override def add_arguments(self, parser: CommandParser) -> None: parser.add_argument("destination", help="Destination file path") parser.add_argument("--amount", default=100000, help="Number of messages to render") parser.add_argument("--latest_id", default=0, help="Last message id to render") + @override def handle(self, *args: Any, **options: Any) -> None: dest_dir = os.path.realpath(os.path.dirname(options["destination"])) amount = int(options["amount"]) diff --git a/zilencer/management/commands/rundjangoserver.py b/zilencer/management/commands/rundjangoserver.py index e3fb2d12f5..6426534e1e 100644 --- a/zilencer/management/commands/rundjangoserver.py +++ b/zilencer/management/commands/rundjangoserver.py @@ -4,6 +4,7 @@ from typing import Callable from dateutil.tz import tzlocal from django.core.management.commands.runserver import Command as DjangoCommand +from typing_extensions import override def output_styler(style_func: Callable[[str], str]) -> Callable[[str], str]: @@ -26,6 +27,7 @@ def output_styler(style_func: Callable[[str], str]) -> Callable[[str], str]: class Command(DjangoCommand): + @override def inner_run(self, *args: object, **options: object) -> None: self.stdout.style_func = output_styler(self.stdout.style_func) super().inner_run(*args, **options) diff --git a/zilencer/management/commands/switch_realm_from_standard_to_plus_plan.py b/zilencer/management/commands/switch_realm_from_standard_to_plus_plan.py index 8f1da39552..c11f553a10 100644 --- a/zilencer/management/commands/switch_realm_from_standard_to_plus_plan.py +++ b/zilencer/management/commands/switch_realm_from_standard_to_plus_plan.py @@ -2,6 +2,7 @@ from typing import Any from django.conf import settings from django.core.management.base import CommandError, CommandParser +from typing_extensions import override from zerver.lib.management import ZulipBaseCommand @@ -10,9 +11,11 @@ if settings.BILLING_ENABLED: class Command(ZulipBaseCommand): + @override def add_arguments(self, parser: CommandParser) -> None: self.add_realm_args(parser) + @override def handle(self, *args: Any, **options: Any) -> None: realm = self.get_realm(options) diff --git a/zilencer/management/commands/sync_api_key.py b/zilencer/management/commands/sync_api_key.py index 619781aaa3..9a03255faf 100644 --- a/zilencer/management/commands/sync_api_key.py +++ b/zilencer/management/commands/sync_api_key.py @@ -3,6 +3,7 @@ from configparser import ConfigParser from typing import Any from django.core.management.base import BaseCommand +from typing_extensions import override from zerver.models import UserProfile, get_realm, get_user_by_delivery_email @@ -10,6 +11,7 @@ from zerver.models import UserProfile, get_realm, get_user_by_delivery_email class Command(BaseCommand): help = """Sync your API key from ~/.zuliprc into your development instance""" + @override def handle(self, *args: Any, **options: Any) -> None: config_file = os.path.join(os.environ["HOME"], ".zuliprc") if not os.path.exists(config_file): diff --git a/zilencer/models.py b/zilencer/models.py index af6682eb76..e7cb3a94ce 100644 --- a/zilencer/models.py +++ b/zilencer/models.py @@ -1,8 +1,12 @@ +# https://github.com/typeddjango/django-stubs/issues/1698 +# mypy: disable-error-code="explicit-override" + from typing import List, Tuple from django.conf import settings from django.core.exceptions import ValidationError from django.db import models +from typing_extensions import override from analytics.models import BaseCount from zerver.lib.rate_limiter import RateLimitedObject @@ -49,6 +53,7 @@ class RemoteZulipServer(models.Model): # The current billing plan for the remote server, similar to Realm.plan_type. plan_type = models.PositiveSmallIntegerField(default=PLAN_TYPE_SELF_HOSTED) + @override def __str__(self) -> str: return f"{self.hostname} {str(self.uuid)[0:12]}" @@ -74,6 +79,7 @@ class RemotePushDeviceToken(AbstractPushDeviceToken): ("server", "user_uuid", "kind", "token"), ] + @override def __str__(self) -> str: return f"{self.server!r} {self.user_id}" @@ -90,6 +96,7 @@ class RemoteZulipServerAuditLog(AbstractRealmAuditLog): server = models.ForeignKey(RemoteZulipServer, on_delete=models.CASCADE) + @override def __str__(self) -> str: return f"{self.server!r} {self.event_type} {self.event_time} {self.id}" @@ -104,6 +111,7 @@ class RemoteRealmAuditLog(AbstractRealmAuditLog): # The remote_id field lets us deduplicate data from the remote server remote_id = models.IntegerField() + @override def __str__(self) -> str: return f"{self.server!r} {self.event_type} {self.event_time} {self.id}" @@ -136,6 +144,7 @@ class RemoteInstallationCount(BaseCount): ), ] + @override def __str__(self) -> str: return f"{self.property} {self.subgroup} {self.value}" @@ -160,6 +169,7 @@ class RemoteRealmCount(BaseCount): ), ] + @override def __str__(self) -> str: return f"{self.server!r} {self.realm_id} {self.property} {self.subgroup} {self.value}" @@ -178,8 +188,10 @@ class RateLimitedRemoteZulipServer(RateLimitedObject): self.domain = domain super().__init__() + @override def key(self) -> str: return f"{type(self).__name__}:<{self.uuid}>:{self.domain}" + @override def rules(self) -> List[Tuple[int, int]]: return rate_limiter_rules[self.domain] diff --git a/zproject/backends.py b/zproject/backends.py index fc1d598ae2..67653be9c0 100644 --- a/zproject/backends.py +++ b/zproject/backends.py @@ -76,6 +76,7 @@ from social_core.exceptions import ( ) from social_core.pipeline.partial import partial from social_django.utils import load_backend, load_strategy +from typing_extensions import override from zxcvbn import zxcvbn from zerver.actions.create_user import do_create_user, do_reactivate_user @@ -306,9 +307,11 @@ class RateLimitedAuthenticationByUsername(RateLimitedObject): self.username = username super().__init__() + @override def key(self) -> str: return f"{type(self).__name__}:{self.username}" + @override def rules(self) -> List[Tuple[int, int]]: return settings.RATE_LIMITING_RULES["authenticate_by_username"] @@ -1536,6 +1539,7 @@ class ZulipRemoteUserBackend(RemoteUserBackend, ExternalAuthMethod): create_unknown_user = False + @override def authenticate( # type: ignore[override] # authenticate has an incompatible signature with ModelBackend and BaseBackend self, request: Optional[HttpRequest] = None, @@ -1551,6 +1555,7 @@ class ZulipRemoteUserBackend(RemoteUserBackend, ExternalAuthMethod): return common_get_active_user(email, realm, return_data=return_data) @classmethod + @override def dict_representation(cls, realm: Optional[Realm] = None) -> List[ExternalAuthMethodDictT]: return [ dict( @@ -1976,6 +1981,7 @@ class SocialAuthMixin(ZulipAuthMixin, ExternalAuthMethod, BaseAuth): return {"social_auth_backend": self.name} @classmethod + @override def dict_representation(cls, realm: Optional[Realm] = None) -> List[ExternalAuthMethodDictT]: return [ dict( @@ -2257,6 +2263,7 @@ class AppleAuthBackend(SocialAuthMixin, AppleIdAuth): return user_details + @override def auth_complete(self, *args: Any, **kwargs: Any) -> Optional[HttpResponse]: if not self.is_native_flow(): # The default implementation in python-social-auth is the browser flow. @@ -2405,6 +2412,7 @@ class SAMLDocument: class SAMLRequest(SAMLDocument): + @override def get_issuers(self) -> List[str]: config = self.backend.generate_saml_config() saml_settings = OneLogin_Saml2_Settings(config, sp_validation_only=True) @@ -2422,6 +2430,7 @@ class SAMLRequest(SAMLDocument): class SAMLResponse(SAMLDocument): + @override def get_issuers(self) -> List[str]: config = self.backend.generate_saml_config() saml_settings = OneLogin_Saml2_Settings(config, sp_validation_only=True) @@ -2712,6 +2721,7 @@ class SAMLAuthBackend(SocialAuthMixin, SAMLAuth): return HttpResponseRedirect(url) + @override def auth_complete(self, *args: Any, **kwargs: Any) -> Optional[HttpResponse]: """ Additional ugly wrapping on top of auth_complete in SocialAuthMixin. @@ -2862,6 +2872,7 @@ class SAMLAuthBackend(SocialAuthMixin, SAMLAuth): return True @classmethod + @override def dict_representation(cls, realm: Optional[Realm] = None) -> List[ExternalAuthMethodDictT]: result: List[ExternalAuthMethodDictT] = [] for idp_name, idp_dict in settings.SOCIAL_AUTH_SAML_ENABLED_IDPS.items(): @@ -2882,6 +2893,7 @@ class SAMLAuthBackend(SocialAuthMixin, SAMLAuth): return result + @override def should_auto_signup(self) -> bool: """ This function is meant to be called in the social pipeline or later, @@ -2894,6 +2906,7 @@ class SAMLAuthBackend(SocialAuthMixin, SAMLAuth): assert isinstance(auto_signup, bool) return auto_signup + @override def get_params_to_store_in_authenticated_session(self) -> Dict[str, str]: idp_name = self.strategy.session_get("saml_idp_name") saml_session_index = self.strategy.session_get("saml_session_index") @@ -2961,6 +2974,7 @@ class GenericOpenIdConnectBackend(SocialAuthMixin, OpenIdConnectAuth): return True @classmethod + @override def dict_representation(cls, realm: Optional[Realm] = None) -> List[ExternalAuthMethodDictT]: return [ dict( @@ -2972,6 +2986,7 @@ class GenericOpenIdConnectBackend(SocialAuthMixin, OpenIdConnectAuth): ) ] + @override def should_auto_signup(self) -> bool: result = self.settings_dict.get("auto_signup", False) assert isinstance(result, bool) diff --git a/zproject/email_backends.py b/zproject/email_backends.py index 33878ab94c..2856cb908a 100644 --- a/zproject/email_backends.py +++ b/zproject/email_backends.py @@ -9,6 +9,7 @@ from django.core.mail import EmailMultiAlternatives from django.core.mail.backends.smtp import EmailBackend from django.core.mail.message import EmailMessage from django.template import loader +from typing_extensions import override def get_forward_address() -> str: @@ -94,6 +95,7 @@ class EmailLogBackEnd(EmailBackend): def _do_send_messages(self, email_messages: Sequence[EmailMessage]) -> int: return super().send_messages(email_messages) # nocoverage + @override def send_messages(self, email_messages: Sequence[EmailMessage]) -> int: num_sent = len(email_messages) if get_forward_address(): diff --git a/zproject/template_loaders.py b/zproject/template_loaders.py index f67ae0a05f..8ab119e66f 100644 --- a/zproject/template_loaders.py +++ b/zproject/template_loaders.py @@ -2,9 +2,11 @@ from pathlib import Path from typing import List, Union from django.template.loaders import app_directories +from typing_extensions import override class TwoFactorLoader(app_directories.Loader): + @override def get_dirs(self) -> List[Union[str, Path]]: dirs = super().get_dirs() # app_directories.Loader returns only a list of