{"html_url": "https://github.com/simonw/datasette/issues/878#issuecomment-970712713", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/878", "id": 970712713, "node_id": "IC_kwDOBm6k_c452-aJ", "user": {"value": 9599, "label": "simonw"}, "created_at": "2021-11-16T21:54:33Z", "updated_at": "2021-11-16T21:54:33Z", "author_association": "OWNER", "body": "I'm going to continue working on this in a PR.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 648435885, "label": "New pattern for views that return either JSON or HTML, available for plugins"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/878#issuecomment-970705738", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/878", "id": 970705738, "node_id": "IC_kwDOBm6k_c4528tK", "user": {"value": 9599, "label": "simonw"}, "created_at": "2021-11-16T21:44:31Z", "updated_at": "2021-11-16T21:44:31Z", "author_association": "OWNER", "body": "Wrote a TIL about what I learned using `TopologicalSorter`: https://til.simonwillison.net/python/graphlib-topologicalsorter", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 648435885, "label": "New pattern for views that return either JSON or HTML, available for plugins"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/878#issuecomment-970673085", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/878", "id": 970673085, "node_id": "IC_kwDOBm6k_c4520u9", "user": {"value": 9599, "label": "simonw"}, "created_at": "2021-11-16T20:58:24Z", "updated_at": "2021-11-16T20:58:24Z", "author_association": "OWNER", "body": "New test:\r\n```python\r\n\r\nclass Complex(AsyncBase):\r\n def __init__(self):\r\n self.log = []\r\n\r\n async def d(self):\r\n await asyncio.sleep(random() * 0.1)\r\n print(\"LOG: d\")\r\n self.log.append(\"d\")\r\n\r\n async def c(self):\r\n await asyncio.sleep(random() * 0.1)\r\n print(\"LOG: c\")\r\n self.log.append(\"c\")\r\n\r\n async def b(self, c, d):\r\n print(\"LOG: b\")\r\n self.log.append(\"b\")\r\n\r\n async def a(self, b, c):\r\n print(\"LOG: a\")\r\n self.log.append(\"a\")\r\n\r\n async def go(self, a):\r\n print(\"LOG: go\")\r\n self.log.append(\"go\")\r\n return self.log\r\n\r\n\r\n@pytest.mark.asyncio\r\nasync def test_complex():\r\n result = await Complex().go()\r\n # 'c' should only be called once\r\n assert tuple(result) in (\r\n # c and d could happen in either order\r\n (\"c\", \"d\", \"b\", \"a\", \"go\"),\r\n (\"d\", \"c\", \"b\", \"a\", \"go\"),\r\n )\r\n```\r\nAnd this code passes it:\r\n```python\r\nimport asyncio\r\nfrom functools import wraps\r\nimport inspect\r\n\r\ntry:\r\n import graphlib\r\nexcept ImportError:\r\n from . import vendored_graphlib as graphlib\r\n\r\n\r\nclass AsyncMeta(type):\r\n def __new__(cls, name, bases, attrs):\r\n # Decorate any items that are 'async def' methods\r\n _registry = {}\r\n new_attrs = {\"_registry\": _registry}\r\n for key, value in attrs.items():\r\n if inspect.iscoroutinefunction(value) and not value.__name__ == \"resolve\":\r\n new_attrs[key] = make_method(value)\r\n _registry[key] = new_attrs[key]\r\n else:\r\n new_attrs[key] = value\r\n # Gather graph for later dependency resolution\r\n graph = {\r\n key: {\r\n p\r\n for p in inspect.signature(method).parameters.keys()\r\n if p != \"self\" and not p.startswith(\"_\")\r\n }\r\n for key, method in _registry.items()\r\n }\r\n new_attrs[\"_graph\"] = graph\r\n return super().__new__(cls, name, bases, new_attrs)\r\n\r\n\r\ndef make_method(method):\r\n parameters = inspect.signature(method).parameters.keys()\r\n\r\n @wraps(method)\r\n async def inner(self, _results=None, **kwargs):\r\n print(\"\\n{}.{}({}) _results={}\".format(self, method.__name__, kwargs, _results))\r\n\r\n # Any parameters not provided by kwargs are resolved from registry\r\n to_resolve = [p for p in parameters if p not in kwargs and p != \"self\"]\r\n missing = [p for p in to_resolve if p not in self._registry]\r\n assert (\r\n not missing\r\n ), \"The following DI parameters could not be found in the registry: {}\".format(\r\n missing\r\n )\r\n\r\n results = {}\r\n results.update(kwargs)\r\n if to_resolve:\r\n resolved_parameters = await self.resolve(to_resolve, _results)\r\n results.update(resolved_parameters)\r\n return_value = await method(self, **results)\r\n if _results is not None:\r\n _results[method.__name__] = return_value\r\n return return_value\r\n\r\n return inner\r\n\r\n\r\nclass AsyncBase(metaclass=AsyncMeta):\r\n async def resolve(self, names, results=None):\r\n print(\"\\n resolve: \", names)\r\n if results is None:\r\n results = {}\r\n\r\n # Come up with an execution plan, just for these nodes\r\n ts = graphlib.TopologicalSorter()\r\n to_do = set(names)\r\n done = set()\r\n while to_do:\r\n item = to_do.pop()\r\n dependencies = self._graph[item]\r\n ts.add(item, *dependencies)\r\n done.add(item)\r\n # Add any not-done dependencies to the queue\r\n to_do.update({k for k in dependencies if k not in done})\r\n\r\n ts.prepare()\r\n plan = []\r\n while ts.is_active():\r\n node_group = ts.get_ready()\r\n plan.append(node_group)\r\n ts.done(*node_group)\r\n\r\n print(\"plan:\", plan)\r\n\r\n results = {}\r\n for node_group in plan:\r\n awaitables = [\r\n self._registry[name](\r\n self,\r\n _results=results,\r\n **{k: v for k, v in results.items() if k in self._graph[name]},\r\n )\r\n for name in node_group\r\n ]\r\n print(\" results = \", results)\r\n print(\" awaitables: \", awaitables)\r\n awaitable_results = await asyncio.gather(*awaitables)\r\n results.update(\r\n {p[0].__name__: p[1] for p in zip(awaitables, awaitable_results)}\r\n )\r\n\r\n print(\" End of resolve(), returning\", results)\r\n return {key: value for key, value in results.items() if key in names}\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 648435885, "label": "New pattern for views that return either JSON or HTML, available for plugins"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/878#issuecomment-970660299", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/878", "id": 970660299, "node_id": "IC_kwDOBm6k_c452xnL", "user": {"value": 9599, "label": "simonw"}, "created_at": "2021-11-16T20:39:43Z", "updated_at": "2021-11-16T20:42:27Z", "author_association": "OWNER", "body": "But that does seem to be the plan that `TopographicalSorter` provides:\r\n```python\r\ngraph = {\"go\": {\"a\"}, \"a\": {\"b\", \"c\"}, \"b\": {\"c\", \"d\"}}\r\n\r\nts = TopologicalSorter(graph)\r\nts.prepare()\r\nwhile ts.is_active():\r\n nodes = ts.get_ready()\r\n print(nodes)\r\n ts.done(*nodes)\r\n```\r\nOutputs:\r\n```\r\n('c', 'd')\r\n('b',)\r\n('a',)\r\n('go',)\r\n```\r\nAlso:\r\n```python\r\ngraph = {\"go\": {\"d\", \"e\", \"f\"}, \"d\": {\"b\", \"c\"}, \"b\": {\"c\"}}\r\n\r\nts = TopologicalSorter(graph)\r\nts.prepare()\r\nwhile ts.is_active():\r\n nodes = ts.get_ready()\r\n print(nodes)\r\n ts.done(*nodes)\r\n```\r\nGives:\r\n```\r\n('e', 'f', 'c')\r\n('b',)\r\n('d',)\r\n('go',)\r\n```\r\nI'm confident that `TopologicalSorter` is the way to do this. I think I need to rewrite my code to call it once to get that plan, then `await asyncio.gather(*nodes)` in turn to execute it.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 648435885, "label": "New pattern for views that return either JSON or HTML, available for plugins"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/878#issuecomment-970657874", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/878", "id": 970657874, "node_id": "IC_kwDOBm6k_c452xBS", "user": {"value": 9599, "label": "simonw"}, "created_at": "2021-11-16T20:36:01Z", "updated_at": "2021-11-16T20:36:01Z", "author_association": "OWNER", "body": "My goal here is to calculate the most efficient way to resolve the different nodes, running them in parallel where possible.\r\n\r\nSo for this class:\r\n\r\n```python\r\nclass Complex(AsyncBase):\r\n async def d(self):\r\n pass\r\n\r\n async def c(self):\r\n pass\r\n\r\n async def b(self, c, d):\r\n pass\r\n\r\n async def a(self, b, c):\r\n pass\r\n\r\n async def go(self, a):\r\n pass\r\n```\r\nA call to `go()` should do this:\r\n\r\n- `c` and `d` in parallel\r\n- `b`\r\n- `a`\r\n- `go`", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 648435885, "label": "New pattern for views that return either JSON or HTML, available for plugins"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/878#issuecomment-970655927", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/878", "id": 970655927, "node_id": "IC_kwDOBm6k_c452wi3", "user": {"value": 9599, "label": "simonw"}, "created_at": "2021-11-16T20:33:11Z", "updated_at": "2021-11-16T20:33:11Z", "author_association": "OWNER", "body": "What should be happening here instead is it should resolve the full graph and notice that `c` is depended on by both `b` and `a` - so it should run `c` first, then run the next ones in parallel.\r\n\r\nSo maybe the algorithm I'm inheriting from https://docs.python.org/3/library/graphlib.html isn't the correct algorithm?", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 648435885, "label": "New pattern for views that return either JSON or HTML, available for plugins"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/878#issuecomment-970655304", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/878", "id": 970655304, "node_id": "IC_kwDOBm6k_c452wZI", "user": {"value": 9599, "label": "simonw"}, "created_at": "2021-11-16T20:32:16Z", "updated_at": "2021-11-16T20:32:16Z", "author_association": "OWNER", "body": "This code is really fiddly. I just got to this version:\r\n```python\r\nimport asyncio\r\nfrom functools import wraps\r\nimport inspect\r\n\r\ntry:\r\n import graphlib\r\nexcept ImportError:\r\n from . import vendored_graphlib as graphlib\r\n\r\n\r\nclass AsyncMeta(type):\r\n def __new__(cls, name, bases, attrs):\r\n # Decorate any items that are 'async def' methods\r\n _registry = {}\r\n new_attrs = {\"_registry\": _registry}\r\n for key, value in attrs.items():\r\n if inspect.iscoroutinefunction(value) and not value.__name__ == \"resolve\":\r\n new_attrs[key] = make_method(value)\r\n _registry[key] = new_attrs[key]\r\n else:\r\n new_attrs[key] = value\r\n # Gather graph for later dependency resolution\r\n graph = {\r\n key: {\r\n p\r\n for p in inspect.signature(method).parameters.keys()\r\n if p != \"self\" and not p.startswith(\"_\")\r\n }\r\n for key, method in _registry.items()\r\n }\r\n new_attrs[\"_graph\"] = graph\r\n return super().__new__(cls, name, bases, new_attrs)\r\n\r\n\r\ndef make_method(method):\r\n @wraps(method)\r\n async def inner(self, _results=None, **kwargs):\r\n print(\"inner - _results=\", _results)\r\n parameters = inspect.signature(method).parameters.keys()\r\n # Any parameters not provided by kwargs are resolved from registry\r\n to_resolve = [p for p in parameters if p not in kwargs and p != \"self\"]\r\n missing = [p for p in to_resolve if p not in self._registry]\r\n assert (\r\n not missing\r\n ), \"The following DI parameters could not be found in the registry: {}\".format(\r\n missing\r\n )\r\n results = {}\r\n results.update(kwargs)\r\n if to_resolve:\r\n resolved_parameters = await self.resolve(to_resolve, _results)\r\n results.update(resolved_parameters)\r\n return_value = await method(self, **results)\r\n if _results is not None:\r\n _results[method.__name__] = return_value\r\n return return_value\r\n\r\n return inner\r\n\r\n\r\nclass AsyncBase(metaclass=AsyncMeta):\r\n async def resolve(self, names, results=None):\r\n print(\"\\n resolve: \", names)\r\n if results is None:\r\n results = {}\r\n\r\n # Resolve them in the correct order\r\n ts = graphlib.TopologicalSorter()\r\n for name in names:\r\n ts.add(name, *self._graph[name])\r\n ts.prepare()\r\n\r\n async def resolve_nodes(nodes):\r\n print(\" resolve_nodes\", nodes)\r\n print(\" (current results = {})\".format(repr(results)))\r\n awaitables = [\r\n self._registry[name](\r\n self,\r\n _results=results,\r\n **{k: v for k, v in results.items() if k in self._graph[name]},\r\n )\r\n for name in nodes\r\n if name not in results\r\n ]\r\n print(\" awaitables: \", awaitables)\r\n awaitable_results = await asyncio.gather(*awaitables)\r\n results.update(\r\n {p[0].__name__: p[1] for p in zip(awaitables, awaitable_results)}\r\n )\r\n\r\n if not ts.is_active():\r\n # Nothing has dependencies - just resolve directly\r\n print(\" no dependencies, resolve directly\")\r\n await resolve_nodes(names)\r\n else:\r\n # Resolve in topological order\r\n while ts.is_active():\r\n nodes = ts.get_ready()\r\n print(\" ts.get_ready() returned nodes:\", nodes)\r\n await resolve_nodes(nodes)\r\n for node in nodes:\r\n ts.done(node)\r\n\r\n print(\" End of resolve(), returning\", results)\r\n return {key: value for key, value in results.items() if key in names}\r\n```\r\nWith this test:\r\n```python\r\nclass Complex(AsyncBase):\r\n def __init__(self):\r\n self.log = []\r\n\r\n async def c(self):\r\n print(\"LOG: c\")\r\n self.log.append(\"c\")\r\n\r\n async def b(self, c):\r\n print(\"LOG: b\")\r\n self.log.append(\"b\")\r\n\r\n async def a(self, b, c):\r\n print(\"LOG: a\")\r\n self.log.append(\"a\")\r\n\r\n async def go(self, a):\r\n print(\"LOG: go\")\r\n self.log.append(\"go\")\r\n return self.log\r\n\r\n\r\n@pytest.mark.asyncio\r\nasync def test_complex():\r\n result = await Complex().go()\r\n # 'c' should only be called once\r\n assert result == [\"c\", \"b\", \"a\", \"go\"]\r\n```\r\nThis test sometimes passes, and sometimes fails!\r\n\r\nOutput for a pass:\r\n```\r\ntests/test_asyncdi.py inner - _results= None\r\n\r\n resolve: ['a']\r\n ts.get_ready() returned nodes: ('c', 'b')\r\n resolve_nodes ('c', 'b')\r\n (current results = {})\r\n awaitables: [, ]\r\ninner - _results= {}\r\nLOG: c\r\ninner - _results= {'c': None}\r\n\r\n resolve: ['c']\r\n ts.get_ready() returned nodes: ('c',)\r\n resolve_nodes ('c',)\r\n (current results = {'c': None})\r\n awaitables: []\r\n End of resolve(), returning {'c': None}\r\nLOG: b\r\n ts.get_ready() returned nodes: ('a',)\r\n resolve_nodes ('a',)\r\n (current results = {'c': None, 'b': None})\r\n awaitables: []\r\ninner - _results= {'c': None, 'b': None}\r\nLOG: a\r\n End of resolve(), returning {'c': None, 'b': None, 'a': None}\r\nLOG: go\r\n```\r\nOutput for a fail:\r\n```\r\ntests/test_asyncdi.py inner - _results= None\r\n\r\n resolve: ['a']\r\n ts.get_ready() returned nodes: ('b', 'c')\r\n resolve_nodes ('b', 'c')\r\n (current results = {})\r\n awaitables: [, ]\r\ninner - _results= {}\r\n\r\n resolve: ['c']\r\n ts.get_ready() returned nodes: ('c',)\r\n resolve_nodes ('c',)\r\n (current results = {})\r\n awaitables: []\r\ninner - _results= {}\r\nLOG: c\r\ninner - _results= {'c': None}\r\nLOG: c\r\n End of resolve(), returning {'c': None}\r\nLOG: b\r\n ts.get_ready() returned nodes: ('a',)\r\n resolve_nodes ('a',)\r\n (current results = {'c': None, 'b': None})\r\n awaitables: []\r\ninner - _results= {'c': None, 'b': None}\r\nLOG: a\r\n End of resolve(), returning {'c': None, 'b': None, 'a': None}\r\nLOG: go\r\nF\r\n\r\n=================================================================================================== FAILURES ===================================================================================================\r\n_________________________________________________________________________________________________ test_complex _________________________________________________________________________________________________\r\n\r\n @pytest.mark.asyncio\r\n async def test_complex():\r\n result = await Complex().go()\r\n # 'c' should only be called once\r\n> assert result == [\"c\", \"b\", \"a\", \"go\"]\r\nE AssertionError: assert ['c', 'c', 'b', 'a', 'go'] == ['c', 'b', 'a', 'go']\r\nE At index 1 diff: 'c' != 'b'\r\nE Left contains one more item: 'go'\r\nE Use -v to get the full diff\r\n\r\ntests/test_asyncdi.py:48: AssertionError\r\n================== short test summary info ================================\r\nFAILED tests/test_asyncdi.py::test_complex - AssertionError: assert ['c', 'c', 'b', 'a', 'go'] == ['c', 'b', 'a', 'go']\r\n```\r\nI figured out why this is happening.\r\n\r\n`a` requires `b` and `c`\r\n\r\n`b` also requires `c`\r\n\r\nThe code decides to run `b` and `c` in parallel.\r\n\r\nIf `c` completes first, then when `b` runs it gets to use the already-calculated result for `c` - so it doesn't need to call `c` again.\r\n\r\nIf `b` gets to that point before `c` does it also needs to call `c`.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 648435885, "label": "New pattern for views that return either JSON or HTML, available for plugins"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/878#issuecomment-970624197", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/878", "id": 970624197, "node_id": "IC_kwDOBm6k_c452ozF", "user": {"value": 9599, "label": "simonw"}, "created_at": "2021-11-16T19:49:05Z", "updated_at": "2021-11-16T19:49:05Z", "author_association": "OWNER", "body": "Here's the latest version of my weird dependency injection async class:\r\n```python\r\nimport inspect\r\n\r\nclass AsyncMeta(type):\r\n def __new__(cls, name, bases, attrs):\r\n # Decorate any items that are 'async def' methods\r\n _registry = {}\r\n new_attrs = {\"_registry\": _registry}\r\n for key, value in attrs.items():\r\n if inspect.iscoroutinefunction(value) and not value.__name__ == \"resolve\":\r\n new_attrs[key] = make_method(value)\r\n _registry[key] = new_attrs[key]\r\n else:\r\n new_attrs[key] = value\r\n\r\n # Topological sort of _registry by parameter dependencies\r\n graph = {\r\n key: {\r\n p for p in inspect.signature(method).parameters.keys()\r\n if p != \"self\" and not p.startswith(\"_\")\r\n }\r\n for key, method in _registry.items()\r\n }\r\n new_attrs[\"_graph\"] = graph\r\n return super().__new__(cls, name, bases, new_attrs)\r\n\r\n\r\ndef make_method(method):\r\n @wraps(method)\r\n async def inner(self, **kwargs):\r\n parameters = inspect.signature(method).parameters.keys()\r\n # Any parameters not provided by kwargs are resolved from registry\r\n to_resolve = [p for p in parameters if p not in kwargs and p != \"self\"]\r\n missing = [p for p in to_resolve if p not in self._registry]\r\n assert (\r\n not missing\r\n ), \"The following DI parameters could not be found in the registry: {}\".format(\r\n missing\r\n )\r\n results = {}\r\n results.update(kwargs)\r\n results.update(await self.resolve(to_resolve))\r\n return await method(self, **results)\r\n\r\n return inner\r\n\r\n\r\nbad = [0]\r\n\r\nclass AsyncBase(metaclass=AsyncMeta):\r\n async def resolve(self, names):\r\n print(\" resolve({})\".format(names))\r\n results = {}\r\n # Resolve them in the correct order\r\n ts = TopologicalSorter()\r\n ts2 = TopologicalSorter()\r\n print(\" names = \", names)\r\n print(\" self._graph = \", self._graph)\r\n for name in names:\r\n if self._graph[name]:\r\n ts.add(name, *self._graph[name])\r\n ts2.add(name, *self._graph[name])\r\n print(\" static_order =\", tuple(ts2.static_order()))\r\n ts.prepare()\r\n while ts.is_active():\r\n print(\" is_active, i = \", bad[0])\r\n bad[0] += 1\r\n if bad[0] > 20:\r\n print(\" Infinite loop?\")\r\n break\r\n nodes = ts.get_ready()\r\n print(\" Do nodes:\", nodes)\r\n awaitables = [self._registry[name](self, **{\r\n k: v for k, v in results.items() if k in self._graph[name]\r\n }) for name in nodes]\r\n print(\" awaitables: \", awaitables)\r\n awaitable_results = await asyncio.gather(*awaitables)\r\n results.update({\r\n p[0].__name__: p[1] for p in zip(awaitables, awaitable_results)\r\n })\r\n print(results)\r\n for node in nodes:\r\n ts.done(node)\r\n\r\n return results\r\n```\r\nExample usage:\r\n```python\r\nclass Foo(AsyncBase):\r\n async def graa(self, boff):\r\n print(\"graa\")\r\n return 5\r\n async def boff(self):\r\n print(\"boff\")\r\n return 8\r\n async def other(self, boff, graa):\r\n print(\"other\")\r\n return 5 + boff + graa\r\n\r\nfoo = Foo()\r\nawait foo.other()\r\n```\r\nOutput:\r\n```\r\n resolve(['boff', 'graa'])\r\n names = ['boff', 'graa']\r\n self._graph = {'graa': {'boff'}, 'boff': set(), 'other': {'graa', 'boff'}}\r\n static_order = ('boff', 'graa')\r\n is_active, i = 0\r\n Do nodes: ('boff',)\r\n awaitables: []\r\n resolve([])\r\n names = []\r\n self._graph = {'graa': {'boff'}, 'boff': set(), 'other': {'graa', 'boff'}}\r\n static_order = ()\r\nboff\r\n{'boff': 8}\r\n is_active, i = 1\r\n Do nodes: ('graa',)\r\n awaitables: []\r\n resolve([])\r\n names = []\r\n self._graph = {'graa': {'boff'}, 'boff': set(), 'other': {'graa', 'boff'}}\r\n static_order = ()\r\ngraa\r\n{'boff': 8, 'graa': 5}\r\nother\r\n18\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 648435885, "label": "New pattern for views that return either JSON or HTML, available for plugins"}, "performed_via_github_app": null}