{"html_url": "https://github.com/simonw/datasette/issues/2078#issuecomment-1563282327", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2078", "id": 1563282327, "node_id": "IC_kwDOBm6k_c5dLcuX", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T17:46:05Z", "updated_at": "2023-05-25T17:46:05Z", "author_association": "OWNER", "body": "Here's what `wrap_view()` does:\r\n\r\nhttps://github.com/simonw/datasette/blob/49184c569cd70efbda4f3f062afef3a34401d8d5/datasette/app.py#L1676-L1700\r\n\r\nIt's used e.g. here:\r\n\r\nhttps://github.com/simonw/datasette/blob/49184c569cd70efbda4f3f062afef3a34401d8d5/datasette/app.py#L1371-L1375\r\n\r\nThe `BaseView` thing meanwhile works like this:\r\n\r\nhttps://github.com/simonw/datasette/blob/d97e82df3c8a3f2e97038d7080167be9bb74a68d/datasette/views/base.py#L56-L157", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726236847, "label": "Resolve the difference between `wrap_view()` and `BaseView`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2078#issuecomment-1563283939", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2078", "id": 1563283939, "node_id": "IC_kwDOBm6k_c5dLdHj", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T17:47:38Z", "updated_at": "2023-05-25T17:47:38Z", "author_association": "OWNER", "body": "The idea behind `wrap_view()` is dependency injection - it's mainly used by plugins:\r\n\r\nhttps://docs.datasette.io/en/0.64.3/plugin_hooks.html#register-routes-datasette\r\n\r\nBut I like the pattern so I started using it for some of Datasette's own features.\r\n\r\nI should use it for _all_ of Datasette's own features.\r\n\r\nBut I still like the way `BaseView` helps with running different code for GET/POST/etc verbs.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726236847, "label": "Resolve the difference between `wrap_view()` and `BaseView`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2078#issuecomment-1563292373", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2078", "id": 1563292373, "node_id": "IC_kwDOBm6k_c5dLfLV", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T17:55:12Z", "updated_at": "2023-05-25T17:55:30Z", "author_association": "OWNER", "body": "So I think subclasses of `BaseView` need to offer a callable which accepts all five of the DI arguments - `datasette`, `request`, `scope`, `send`, `receive` - and then makes a decision based on the HTTP verb as to which method of the class to call. Those methods themselves can accept a subset of those parameters and will only be sent on to them.\r\n\r\nHaving two layers of parameter detection feels a little bit untidy, but I think it will work.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726236847, "label": "Resolve the difference between `wrap_view()` and `BaseView`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2078#issuecomment-1563294669", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2078", "id": 1563294669, "node_id": "IC_kwDOBm6k_c5dLfvN", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T17:57:06Z", "updated_at": "2023-05-25T17:57:06Z", "author_association": "OWNER", "body": "I may need to be able to detect if a class instance has an `async def __call__` method - I think I can do that like so:\r\n\r\n```python\r\ndef iscoroutinefunction(obj):\r\n if inspect.iscoroutinefunction(obj):\r\n return True\r\n if hasattr(obj, '__call__') and inspect.iscoroutinefunction(obj.__call__):\r\n return True\r\n return False\r\n```\r\nFrom https://github.com/encode/starlette/issues/886#issuecomment-606585152", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726236847, "label": "Resolve the difference between `wrap_view()` and `BaseView`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2078#issuecomment-1563308919", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2078", "id": 1563308919, "node_id": "IC_kwDOBm6k_c5dLjN3", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T18:08:34Z", "updated_at": "2023-05-25T18:08:34Z", "author_association": "OWNER", "body": "After much fiddling this seems to work:\r\n```python\r\nimport asyncio, types\r\n\r\ndef is_async_callable(obj):\r\n if not callable(obj):\r\n raise ValueError(\"Object is not callable\")\r\n\r\n if isinstance(obj, types.FunctionType):\r\n return asyncio.iscoroutinefunction(obj)\r\n \r\n if hasattr(obj, '__call__'):\r\n return asyncio.iscoroutinefunction(obj.__call__)\r\n\r\n raise ValueError(\"Not a function and has no __call__ attribute\")\r\n```\r\nTested like so:\r\n```python\r\nclass AsyncClass:\r\n async def __call__(self):\r\n pass\r\n\r\nclass NotAsyncClass:\r\n def __call__(self):\r\n pass\r\n\r\nclass ClassNoCall:\r\n pass\r\n \r\nasync def async_func():\r\n pass\r\n\r\ndef non_async_func():\r\n pass\r\n\r\nfor thing in (AsyncClass(), NotAsyncClass(), ClassNoCall(), async_func, non_async_func):\r\n try:\r\n print(thing, is_async_callable(thing))\r\n except Exception as ex:\r\n print(thing, ex)\r\n```\r\n```\r\n<__main__.AsyncClass object at 0x106c32150> True\r\n<__main__.NotAsyncClass object at 0x106c32390> False\r\n<__main__.ClassNoCall object at 0x106c32750> Object is not callable\r\n True\r\n False\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726236847, "label": "Resolve the difference between `wrap_view()` and `BaseView`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2078#issuecomment-1563318598", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2078", "id": 1563318598, "node_id": "IC_kwDOBm6k_c5dLllG", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T18:17:03Z", "updated_at": "2023-05-25T18:21:25Z", "author_association": "OWNER", "body": "I think I want that to return `(is_callable, is_async)` - so I can both test if the thing can be called AND if it should be awaited in the same operation (without any exceptions).\r\n\r\nI tried this:\r\n```python\r\ndef is_callable(obj):\r\n \"Returns (is_callable, is_async_callable)\"\r\n if not callable(obj):\r\n return False, False\r\n\r\n if isinstance(obj, types.FunctionType):\r\n return True, asyncio.iscoroutinefunction(obj)\r\n \r\n if hasattr(obj, '__call__'):\r\n return True, asyncio.iscoroutinefunction(obj.__call__)\r\n\r\n return False, False\r\n```\r\n```python\r\nfor thing in (\r\n async_func, non_async_func, AsyncClass(), NotAsyncClass(), ClassNoCall(),\r\n AsyncClass, NotAsyncClass, ClassNoCall\r\n):\r\n print(thing, is_callable(thing))\r\n```\r\nAnd got:\r\n```\r\n (True, True)\r\n (True, False)\r\n<__main__.AsyncClass object at 0x106cce490> (True, True)\r\n<__main__.NotAsyncClass object at 0x106ccf710> (True, False)\r\n<__main__.ClassNoCall object at 0x106ccc810> (False, False)\r\n (True, True)\r\n (True, False)\r\n (True, False)\r\n```\r\nWhich is almost right, but I don't like that `AsyncClass` is shown as callable (which it is, since it's a class) and awaitable (which it is not - the `__call__` method may be async but calling the class constructor is not).\r\n\r\nSo I'm going to detect classes using `isinstance(obj, type)`.\r\n\r\n```python\r\ndef is_callable(obj):\r\n \"Returns (is_callable, is_async_callable)\"\r\n if not callable(obj):\r\n return False, False\r\n\r\n if isinstance(obj, type):\r\n # It's a class\r\n return True, False\r\n \r\n if isinstance(obj, types.FunctionType):\r\n return True, asyncio.iscoroutinefunction(obj)\r\n \r\n if hasattr(obj, '__call__'):\r\n return True, asyncio.iscoroutinefunction(obj.__call__)\r\n\r\n assert False, \"obj {} somehow is callable with no __call__ method\".format(obj)\r\n```\r\nI am reasonably confident the `AssertionError` can never be raised.\r\n\r\nAnd now:\r\n```\r\n (True, True)\r\n (True, False)\r\n<__main__.AsyncClass object at 0x106ccfa50> (True, True)\r\n<__main__.NotAsyncClass object at 0x106ccc8d0> (True, False)\r\n<__main__.ClassNoCall object at 0x106cd7690> (False, False)\r\n (True, False)\r\n (True, False)\r\n (True, False)\r\n```\r\nWhich is what I wanted.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726236847, "label": "Resolve the difference between `wrap_view()` and `BaseView`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2078#issuecomment-1563326000", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2078", "id": 1563326000, "node_id": "IC_kwDOBm6k_c5dLnYw", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T18:23:38Z", "updated_at": "2023-05-25T18:23:38Z", "author_association": "OWNER", "body": "I don't like that `is_callable()` implies a single boolean result but actually returns a pair. I'll call it `check_callable(obj)` instead.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726236847, "label": "Resolve the difference between `wrap_view()` and `BaseView`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2078#issuecomment-1563329245", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2078", "id": 1563329245, "node_id": "IC_kwDOBm6k_c5dLoLd", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T18:26:47Z", "updated_at": "2023-05-25T18:28:08Z", "author_association": "OWNER", "body": "With type hints and a namedtuple:\r\n```python\r\nimport asyncio\r\nimport types\r\nfrom typing import NamedTuple, Any\r\n\r\n\r\nclass CallableStatus(NamedTuple):\r\n is_callable: bool\r\n is_async_callable: bool\r\n\r\n\r\ndef check_callable(obj: Any) -> CallableStatus:\r\n if not callable(obj):\r\n return CallableStatus(False, False)\r\n\r\n if isinstance(obj, type):\r\n # It's a class\r\n return CallableStatus(True, False)\r\n\r\n if isinstance(obj, types.FunctionType):\r\n return CallableStatus(True, asyncio.iscoroutinefunction(obj))\r\n\r\n if hasattr(obj, \"__call__\"):\r\n return CallableStatus(True, asyncio.iscoroutinefunction(obj.__call__))\r\n\r\n assert False, \"obj {} is somehow callable with no __call__ method\".format(repr(obj))\r\n```\r\n```python\r\nfor thing in (\r\n async_func,\r\n non_async_func,\r\n AsyncClass(),\r\n NotAsyncClass(),\r\n ClassNoCall(),\r\n AsyncClass,\r\n NotAsyncClass,\r\n ClassNoCall,\r\n):\r\n print(thing, check_callable(thing))\r\n```\r\n```\r\n CallableStatus(is_callable=True, is_async_callable=True)\r\n CallableStatus(is_callable=True, is_async_callable=False)\r\n<__main__.AsyncClass object at 0x106ba7490> CallableStatus(is_callable=True, is_async_callable=True)\r\n<__main__.NotAsyncClass object at 0x106740150> CallableStatus(is_callable=True, is_async_callable=False)\r\n<__main__.ClassNoCall object at 0x10676d910> CallableStatus(is_callable=False, is_async_callable=False)\r\n CallableStatus(is_callable=True, is_async_callable=False)\r\n CallableStatus(is_callable=True, is_async_callable=False)\r\n CallableStatus(is_callable=True, is_async_callable=False)\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726236847, "label": "Resolve the difference between `wrap_view()` and `BaseView`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2078#issuecomment-1563359114", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2078", "id": 1563359114, "node_id": "IC_kwDOBm6k_c5dLveK", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T18:47:57Z", "updated_at": "2023-05-25T18:47:57Z", "author_association": "OWNER", "body": "Oops, that broke everything:\r\n```\r\n @documented\r\n async def await_me_maybe(value: typing.Any) -> typing.Any:\r\n \"If value is callable, call it. If awaitable, await it. Otherwise return it.\"\r\n> if callable(value):\r\nE TypeError: 'module' object is not callable\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726236847, "label": "Resolve the difference between `wrap_view()` and `BaseView`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2078#issuecomment-1563419066", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2078", "id": 1563419066, "node_id": "IC_kwDOBm6k_c5dL-G6", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T19:42:16Z", "updated_at": "2023-05-25T19:43:08Z", "author_association": "OWNER", "body": "Maybe what I want here is the ability to register classes with the router - and have the router know that if it's a class it should instantiate it via its constructor and then await `__call__` it.\r\n\r\nThe neat thing about it is that it can reduce the risk of having a class instance that accidentally shares state between requests.\r\n\r\nIt also encourages that each class only responds based on the `datasette, request, ...` objects that are passed to its methods.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726236847, "label": "Resolve the difference between `wrap_view()` and `BaseView`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2078#issuecomment-1563444296", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2078", "id": 1563444296, "node_id": "IC_kwDOBm6k_c5dMERI", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T20:06:08Z", "updated_at": "2023-05-25T20:06:08Z", "author_association": "OWNER", "body": "This prototype seems to work well:\r\n\r\n```diff\r\ndiff --git a/datasette/app.py b/datasette/app.py\r\nindex d7dace67..ed0edf28 100644\r\n--- a/datasette/app.py\r\n+++ b/datasette/app.py\r\n@@ -17,6 +17,7 @@ import secrets\r\n import sys\r\n import threading\r\n import time\r\n+import types\r\n import urllib.parse\r\n from concurrent import futures\r\n from pathlib import Path\r\n@@ -1266,6 +1267,8 @@ class Datasette:\r\n # TODO: /favicon.ico and /-/static/ deserve far-future cache expires\r\n add_route(favicon, \"/favicon.ico\")\r\n \r\n+ add_route(wrap_view(DemoView, self), '/demo')\r\n+\r\n add_route(\r\n asgi_static(app_root / \"datasette\" / \"static\"), r\"/-/static/(?P.*)$\"\r\n )\r\n@@ -1673,8 +1676,46 @@ def _cleaner_task_str(task):\r\n return _cleaner_task_str_re.sub(\"\", s)\r\n \r\n \r\n-def wrap_view(view_fn, datasette):\r\n- @functools.wraps(view_fn)\r\n+class DemoView:\r\n+ async def __call__(self, datasette, request):\r\n+ return Response.text(\"Hello there! {} - {}\".format(datasette, request))\r\n+\r\n+def wrap_view(view_fn_or_class, datasette):\r\n+ is_function = isinstance(view_fn_or_class, types.FunctionType)\r\n+ if is_function:\r\n+ return wrap_view_function(view_fn_or_class, datasette)\r\n+ else:\r\n+ if not isinstance(view_fn_or_class, type):\r\n+ raise ValueError(\"view_fn_or_class must be a function or a class\")\r\n+ return wrap_view_class(view_fn_or_class, datasette)\r\n+\r\n+\r\n+def wrap_view_class(view_class, datasette):\r\n+ async def async_view_for_class(request, send):\r\n+ instance = view_class()\r\n+ if inspect.iscoroutinefunction(instance.__call__):\r\n+ return await async_call_with_supported_arguments(\r\n+ instance.__call__,\r\n+ scope=request.scope,\r\n+ receive=request.receive,\r\n+ send=send,\r\n+ request=request,\r\n+ datasette=datasette,\r\n+ )\r\n+ else:\r\n+ return call_with_supported_arguments(\r\n+ instance.__call__,\r\n+ scope=request.scope,\r\n+ receive=request.receive,\r\n+ send=send,\r\n+ request=request,\r\n+ datasette=datasette,\r\n+ )\r\n+\r\n+ return async_view_for_class\r\n+\r\n+\r\n+def wrap_view_function(view_fn, datasette):\r\n async def async_view_fn(request, send):\r\n if inspect.iscoroutinefunction(view_fn):\r\n response = await async_call_with_supported_arguments(\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726236847, "label": "Resolve the difference between `wrap_view()` and `BaseView`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2078#issuecomment-1563488929", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2078", "id": 1563488929, "node_id": "IC_kwDOBm6k_c5dMPKh", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T20:48:12Z", "updated_at": "2023-05-25T20:48:39Z", "author_association": "OWNER", "body": "Actually no need for that extra level of parameter detection: `BaseView.__call__` should _always_ take `datasette, request` - `scope` and `receive` are both available on `request`, and `send` is only needed if you're not planning on returning a `Response` object.\r\n\r\nSo the `get` and `post` and suchlike methods should take `datasette` and `request` too.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726236847, "label": "Resolve the difference between `wrap_view()` and `BaseView`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2078#issuecomment-1563498048", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2078", "id": 1563498048, "node_id": "IC_kwDOBm6k_c5dMRZA", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T20:57:52Z", "updated_at": "2023-05-25T20:58:13Z", "author_association": "OWNER", "body": "Here's a new `BaseView` class that automatically populates `OPTIONS` based on available methods:\r\n```python\r\nclass BaseView:\r\n async def head(self, *args, **kwargs):\r\n try:\r\n response = await self.get(*args, **kwargs)\r\n response.body = b\"\"\r\n return response\r\n except AttributeError:\r\n raise\r\n\r\n async def method_not_allowed(self, request):\r\n if (\r\n request.path.endswith(\".json\")\r\n or request.headers.get(\"content-type\") == \"application/json\"\r\n ):\r\n response = Response.json(\r\n {\"ok\": False, \"error\": \"Method not allowed\"}, status=405\r\n )\r\n else:\r\n response = Response.text(\"Method not allowed\", status=405)\r\n return response\r\n\r\n async def options(self, request, *args, **kwargs):\r\n response = Response.text(\"ok\")\r\n response.headers[\"allow\"] = \", \".join(\r\n method.upper()\r\n for method in (\"head\", \"get\", \"post\", \"put\", \"patch\", \"delete\")\r\n if hasattr(self, method)\r\n )\r\n return response\r\n\r\n async def __call__(self, request, datasette):\r\n try:\r\n handler = getattr(self, request.method.lower())\r\n return await handler(request, datasette)\r\n except AttributeError:\r\n return await self.method_not_allowed(request)\r\n\r\n\r\nclass DemoView(BaseView):\r\n async def get(self, datasette, request):\r\n return Response.text(\"Hello there! {} - {}\".format(datasette, request))\r\n\r\n post = get\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726236847, "label": "Resolve the difference between `wrap_view()` and `BaseView`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2078#issuecomment-1563511171", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2078", "id": 1563511171, "node_id": "IC_kwDOBm6k_c5dMUmD", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T21:11:20Z", "updated_at": "2023-05-25T21:13:05Z", "author_association": "OWNER", "body": "I'm going to call this `VerbView` for the moment. Might even rename it to `View` later.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726236847, "label": "Resolve the difference between `wrap_view()` and `BaseView`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2078#issuecomment-1563522011", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2078", "id": 1563522011, "node_id": "IC_kwDOBm6k_c5dMXPb", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T21:22:30Z", "updated_at": "2023-05-25T21:22:30Z", "author_association": "OWNER", "body": "This is bad:\r\n```python\r\n async def __call__(self, request, datasette):\r\n try:\r\n handler = getattr(self, request.method.lower())\r\n return await handler(request, datasette)\r\n except AttributeError:\r\n return await self.method_not_allowed(request)\r\n```\r\nBecause it hides any `AttributeError` exceptions that might occur in the view code.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726236847, "label": "Resolve the difference between `wrap_view()` and `BaseView`"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/2078#issuecomment-1563625093", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2078", "id": 1563625093, "node_id": "IC_kwDOBm6k_c5dMwaF", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-05-25T23:23:15Z", "updated_at": "2023-05-25T23:23:15Z", "author_association": "OWNER", "body": "Rest of the work on this will happen in the PR:\r\n- #2080", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1726236847, "label": "Resolve the difference between `wrap_view()` and `BaseView`"}, "performed_via_github_app": null}