Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions mesop/features/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .page import page as page
from .serve import server as server
17 changes: 17 additions & 0 deletions mesop/features/serve.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from typing import Callable

from mesop.runtime import runtime


def serve(
*,
rule: str,
) -> Callable[[Callable[[], None]], Callable[[], None]]:
def decorator(func: Callable[[], None]) -> Callable[[], None]:
runtime().register_handler(
rule=rule,
handler=func,
)
return func

return decorator
23 changes: 22 additions & 1 deletion mesop/runtime/runtime.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from copy import deepcopy
from dataclasses import dataclass
from typing import Any, Callable, Generator, Type, TypeVar, cast
from typing import Any, Callable, Dict, Generator, Tuple, Type, TypeVar, cast

from flask import g, request

Expand Down Expand Up @@ -49,6 +49,9 @@ def __init__(self):
# clients polling whether to request a hot reload.
self.hot_reload_counter = 0
self._path_to_page_config: dict[str, PageConfig] = {}
self._rule_to_handler: dict[
str, Callable[[], Tuple[str, int, Dict[str, str]]]
] = {}
self.component_fns: set[Callable[..., Any]] = set()
self.event_mappers: dict[Type[Any], Callable[[pb.UserEvent, Key], Any]] = {}
self._state_classes: list[type[Any]] = []
Expand Down Expand Up @@ -131,9 +134,27 @@ def register_page(self, *, path: str, page_config: PageConfig) -> None:
)
self._path_to_page_config[path] = page_config

def register_handler(
self, *, rule: str, handler: Callable[[], Tuple[str, int, Dict[str, str]]]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[question] Where did you get this callable signature from - Callable[[], Tuple[str, int, Dict[str, str]]? Seems like it's a subset of what Flask expects in RouteCallable (tuple[ResponseValue, int, HeadersValue]?)

(https://github.com/pallets/flask/blob/a5f9742398c9429ef84ac8a57b0f3eb418394d9e/src/flask/typing.py#L84)

This does bring up the question of exposing Flask internals. Ideally we'd try to hide the Flask internals, though I think this will be somewhat hard to avoid. Maybe we can use TypeAliases to avoid explicitly in exposing the Flask types in the public serve function.

Also the script that does the type checking is not part of the local presubmits. You'll need to run it manually to check the types.

Usually something like this from the root of the directory (but assumes you've set up your dev env -- though you can also try to spin up a GitHub Codespaces)

./scripts/run_py_typecheck.sh

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I managed to run the typechecker. For the types, we could either copy Flasks's types and use the if TYPE_CHECKING trick, or... use "Any"? It looks like the type checker is ok with it for some reason?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think ANY could be ok here so long as it is internal. For Mesop, we like to have annotations for the public API. Makes it more user friendly, so we need to be consistent with that.

We don't have to expose the Flask full type for Mesop. We could try just a subset of what Flask allows.

):
if self._has_served_traffic:
raise MesopDeveloperException(
"Cannot register a handler after traffic has been served. You must register all handlers upon server startup before any traffic has been served. This prevents security issues."
)
if rule in self._rule_to_handler:
raise MesopDeveloperException(
f"Handler for rule '{rule}' already registered."
)
self._rule_to_handler[rule] = handler

def get_page_config(self, *, path: str) -> PageConfig | None:
return self._path_to_page_config.get(path)

def get_rule_to_handlers(
self,
) -> dict[str, Callable[[], Tuple[str, int, Dict[str, str]]]]:
return deepcopy(self._rule_to_handler)

def get_path_to_page_configs(self) -> dict[str, PageConfig]:
return deepcopy(self._path_to_page_config)

Expand Down
4 changes: 4 additions & 0 deletions mesop/server/static_file_serving.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ def configure_static_file_serving(
disable_gzip_cache: bool = False,
default_allowed_iframe_parents: str = "'self'",
):
# Register @me.serve handlers.
for rule, handler_func in runtime().get_rule_to_handlers().items():
app.add_url_rule(rule, view_func=handler_func)

def get_path(path: str):
safe_path = safe_join(static_file_runfiles_base, path)
assert safe_path
Expand Down
Loading