From d5bcff591a8e896fa1659ceb61c7476bccad1102 Mon Sep 17 00:00:00 2001 From: Kiran Jonnalagadda Date: Mon, 11 Dec 2023 23:50:57 +0530 Subject: [PATCH] Provide GetAttr as a type-hinting compatible alt to route_model_map --- src/coaster/views/classview.py | 55 ++++++++++++++++++--- tests/coaster_tests/views_classview_test.py | 7 ++- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/src/coaster/views/classview.py b/src/coaster/views/classview.py index 252b2154..59534fe4 100644 --- a/src/coaster/views/classview.py +++ b/src/coaster/views/classview.py @@ -881,6 +881,39 @@ def view(self): :class:`~ModelView.GetAttr` class approach. """ + class GetAttr: + """ + An alternative to :attr:`~ModelView.route_model_map` with type hinting support. + + All methods in this class must be static or class methods. The methods must have + the same name as the view variable, and must accept an instance of the object as + the first and only positional parameter. Example:: + + @route('//') + class MyModelView(ModelView[MyModel]): + class GetAttr: + @staticmethod + def parent(obj: MyModel) -> str: + return obj.parent.name + + @staticmethod + def document(obj: MyModel) -> str: + return obj.name + + The :attr:`~ModelView.route_model_map` dict and :class:`~ModelView.GetAttr` + class can be used together in the same view, with the class taking priority. + ``GetAttr`` in a subclass will override the base class unless explicitly + subclassed:: + + class Mixin: + class GetAttr: + ... + + class MyModelView(Mixin, ModelView[MyModel]): + class GetAttr(Mixin.GetAttr): + ... + """ + def __init__(self, obj: t.Optional[ModelType] = None) -> None: """ Instantiate ModelView with an optional object. @@ -1104,12 +1137,22 @@ def register_paths_from_app( rulevars = _get_arguments_from_rule( reg_rule, reg_endpoint, reg_options, reg_app.url_map ) - # Make a subset of cls.route_model_map with the required variables - params = { - v: cast(t.Type[ModelView], cls).route_model_map[v] - for v in rulevars - if v in cast(t.Type[ModelView], cls).route_model_map - } + # Make a subset of cls.GetAttr and cls.route_model_map with the required + # variables + try: + params = { + v: getattr(cls.GetAttr, v) + if hasattr(cls.GetAttr, v) + else cls.route_model_map[v] + for v in rulevars + } + except KeyError as exc: + raise TypeError( + f"View variable {exc.args[0]} missing in both" + f" {cls.__qualname__}.GetAttr and" + f" {cls.__qualname__}.route_model_map" + ) from None + # Register endpoint with the view function's name, endpoint name and # parameters model.register_endpoint( diff --git a/tests/coaster_tests/views_classview_test.py b/tests/coaster_tests/views_classview_test.py index e7526a6e..9d4207e1 100644 --- a/tests/coaster_tests/views_classview_test.py +++ b/tests/coaster_tests/views_classview_test.py @@ -309,7 +309,12 @@ def view(self): class MultiDocumentView(UrlForView, ModelView[ViewDocument]): """Test ModelView that has multiple documents.""" - route_model_map = {'doc1': 'name', 'doc2': '**doc2.url_name'} + route_model_map = {'doc2': '**doc2.url_name'} + + class GetAttr: + @staticmethod + def doc1(obj: ViewDocument) -> str: + return obj.name def loader( # type: ignore[override] # pylint: disable=arguments-differ self, doc1: str, doc2: str