Skip to content

Commit a55595c

Browse files
PIG208freshpex
authored andcommitted
api: Add new typed_endpoint decorators.
The goal of typed_endpoint is to replicate most features supported by has_request_variables, and to improve on top of it. There are some unresolved issues that we don't plan to work on currently. For example, typed_endpoint does not support ignored_parameters_supported for 400 responses, and it does not run validators on path-only arguments. Unlike has_request_variables, typed_endpoint supports error handling by processing validation errors from Pydantic. Most features supported by has_request_variables are supported by typed_endpoint in various ways. To define a function, use a syntax like this with Annotated if there is any metadata you want to associate with a parameter, do note that parameters that are not keyword-only are ignored from the request: ``` @typed_endpoint def view( request: HttpRequest, user_profile: UserProfile, *, foo: Annotated[int, ApiParamConfig(path_only=True)], bar: Json[int], other: Annotated[ Json[int], ApiParamConfig( whence="lorem", documentation_status=NTENTIONALLY_UNDOCUMENTED ) ] = 10, ) -> HttpResponse: .... ``` There are also some shorthands for the commonly used annotated types, which are encouraged when applicable for better readability and less typing: ``` WebhookPayload = Annotated[Json[T], ApiParamConfig(argument_type_is_body=True)] PathOnly = Annotated[T, ApiParamConfig(path_only=True)] ``` Then the view function above can be rewritten as: ``` @typed_endpoint def view( request: HttpRequest, user_profile: UserProfile, *, foo: PathOnly[int], bar: Json[int], other: Annotated[ Json[int], ApiParamConfig( whence="lorem", documentation_status=INTENTIONALLY_UNDOCUMENTED ) ] = 10, ) -> HttpResponse: .... ``` There are some intentional restrictions: - A single parameter cannot have more than one ApiParamConfig - Path-only parameters cannot have default values - argument_type_is_body is incompatible with whence - Arguments of name "request", "user_profile", "args", and "kwargs" and etc. are ignored by typed_endpoint. - positional-only arguments are not supported by typed_endpoint. Only keyword-only parameters are expected to be parsed from the request. - Pydantic's strict mode is always enabled, because we don't want to coerce input parsed from JSON into other types unnecessarily. - Using strict mode all the time also means that we should always use Json[int] instead of int, because it is only possible for the request to have data of type str, and a type annotation of int will always reject such data. typed_endpoint's handling of ignored_parameters_unsupported is mostly identical to that of has_request_variables.
1 parent a58d58a commit a55595c

File tree

6 files changed

+1130
-0
lines changed

6 files changed

+1130
-0
lines changed

tools/linter_lib/custom_check.py

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"zerver/lib/email_mirror.py",
2424
"zerver/lib/email_notifications.py",
2525
"zerver/lib/send_email.py",
26+
"zerver/lib/typed_endpoint.py",
2627
"zerver/tests/test_new_users.py",
2728
"zerver/tests/test_email_mirror.py",
2829
"zerver/tests/test_message_notification_emails.py",

tools/semgrep.yml

+17
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,20 @@ rules:
169169
message: 'A batched migration should not be atomic. Add "atomic = False" to the Migration class'
170170
languages: [python]
171171
severity: ERROR
172+
173+
- id: typed_endpoint_without_keyword_only_param
174+
patterns:
175+
- pattern: |
176+
@typed_endpoint
177+
def $F(...)-> ...:
178+
...
179+
- pattern-not-inside: |
180+
@typed_endpoint
181+
def $F(..., *, ...)-> ...:
182+
...
183+
message: |
184+
@typed_endpoint should not be used without keyword-only parameters.
185+
Make parameters to be parsed from the request as keyword-only,
186+
or use @typed_endpoint_without_parameters instead.
187+
languages: [python]
188+
severity: ERROR

zerver/lib/exceptions.py

+6
Original file line numberDiff line numberDiff line change
@@ -527,3 +527,9 @@ def __init__(self) -> None:
527527
@staticmethod
528528
def msg_format() -> str:
529529
return _("Reaction doesn't exist.")
530+
531+
532+
class ApiParamValidationError(JsonableError):
533+
def __init__(self, msg: str, error_type: str) -> None:
534+
super().__init__(msg)
535+
self.error_type = error_type

0 commit comments

Comments
 (0)