request: Map HttpRequest to ZulipRequestNotes for typing.

We create a class called ZulipRequestNotes as a new home to all the
additional attributes that we add to the Django HttpRequest object.
This allows mypy to do the typecheck and also enforces type safety.

Most of the attributes are added in the middleware, and thus it is
generally safe to assert that they are not None in a code path that
goes through the middleware. The caller is obligated to do manual
the type check otherwise.

This also resolves some cyclic dependencies that zerver.lib.request
have with zerver.lib.rate_limiter and zerver.tornado.handlers.
This commit is contained in:
PIG208 2021-07-08 22:10:54 +08:00 committed by Tim Abbott
parent 269f6fb577
commit 03693cd27e
3 changed files with 60 additions and 2 deletions

View File

@ -1,5 +1,7 @@
import threading
import weakref
from collections import defaultdict
from dataclasses import dataclass, field
from functools import wraps
from types import FunctionType
from typing import (
@ -8,6 +10,7 @@ from typing import (
Dict,
Generic,
List,
MutableMapping,
Optional,
Sequence,
TypeVar,
@ -22,8 +25,54 @@ from django.http import HttpRequest, HttpResponse
from django.utils.translation import gettext as _
from typing_extensions import Literal
import zerver.lib.rate_limiter as rate_limiter
import zerver.tornado.handlers as handlers
from zerver.lib.exceptions import ErrorCode, InvalidJSONError, JsonableError
from zerver.lib.types import Validator, ViewFuncT
from zerver.models import Client, Realm
@dataclass
class ZulipRequestNotes:
"""This class contains extra metadata that Zulip associated with a
Django HttpRequest object.
Note that most Optional fields will be definitely not None once
middlware has run. In the future, we may want to express that in
the types by having different types ZulipEarlyRequestNotes and
post-middleware ZulipRequestNotes types, but for now we have a lot
of `assert request_notes.foo is not None` when accessing them.
"""
client: Optional[Client] = None
client_name: Optional[str] = None
client_version: Optional[str] = None
log_data: Optional[MutableMapping[str, Any]] = None
rate_limit: Optional[str] = None
requestor_for_logs: Optional[str] = None
# We use realm_cached to indicate whether the realm is cached or not.
# Because the default value of realm is None, which can indicate "unset"
# and "nonexistence" at the same time.
realm: Optional[Realm] = None
has_fetched_realm: bool = False
set_language: Optional[str] = None
ratelimits_applied: List["rate_limiter.RateLimitResult"] = field(default_factory=lambda: [])
query: Optional[str] = None
error_format: Optional[str] = None
placeholder_open_graph_description: Optional[str] = None
saved_response: Optional[HttpResponse] = None
tornado_handler: Optional["handlers.AsyncDjangoHandler"] = None
request_notes_map: MutableMapping[HttpRequest, ZulipRequestNotes] = weakref.WeakKeyDictionary()
def get_request_notes(request: HttpRequest) -> ZulipRequestNotes:
try:
return request_notes_map[request]
except KeyError:
request_notes_map[request] = ZulipRequestNotes()
return request_notes_map[request]
class RequestConfusingParmsError(JsonableError):

View File

@ -49,9 +49,11 @@ from django.core.exceptions import ValidationError
from django.core.validators import URLValidator, validate_email
from django.utils.translation import gettext as _
from zerver.lib.request import JsonableError, ResultT
from zerver.lib.exceptions import JsonableError
from zerver.lib.types import ProfileFieldData, Validator
ResultT = TypeVar("ResultT")
def check_string(var_name: str, val: object) -> str:
if not isinstance(val, str):

View File

@ -13,7 +13,6 @@ from django.utils.cache import patch_vary_headers
from tornado.wsgi import WSGIContainer
from zerver.lib.response import json_response
from zerver.middleware import async_request_timer_restart, async_request_timer_stop
from zerver.tornado.descriptors import get_descriptor_by_handler_id
current_handler_id = 0
@ -45,6 +44,10 @@ def finish_handler(
) -> None:
err_msg = f"Got error finishing handler for queue {event_queue_id}"
try:
# We import async_request_timer_restart during runtime
# to avoid cyclic dependency with zerver.lib.request
from zerver.middleware import async_request_timer_restart
# We call async_request_timer_restart here in case we are
# being finished without any events (because another
# get_events request has supplanted this request)
@ -143,6 +146,10 @@ class AsyncDjangoHandler(tornado.web.RequestHandler, base.BaseHandler):
response = self.get_response(request)
if hasattr(response, "asynchronous"):
# We import async_request_timer_restart during runtime
# to avoid cyclic dependency with zerver.lib.request
from zerver.middleware import async_request_timer_stop
# For asynchronous requests, this is where we exit
# without returning the HttpResponse that Django
# generated back to the user in order to long-poll the