Add ‘Context’ property to the request.Request() class

Hey all,

I know that we have ctx property in the Request class in order to store arbitrary data.

But for that purpose, we should create a middleware and attach a key to the context, it doesn’t look as redundant steps?

I have been thinking about, what will be if we will attach context object to the Request class itself.

It will avoid us creating middlewares etc.and will make the flexibility of the context object in general better, IMO.

and I already playing with it, and show you the code snippet above.

class Request:
    __slots__ = (
        ...... 
        "context",
    )

    def __init__(self, url_bytes, headers, version, method, transport, app):
        ...... 
        self.context = Context()

The Context class will look like

import typing


class Singleton:
    _instances = {}

    def __new__(klass, *args, **kwargs):
        if klass not in klass._instances:
            klass._instances[klass] = super(Singleton, klass).__new__(klass, *args, **kwargs)
        return klass._instances[klass]


class Context(Singleton):
    def __setattr__(self, key, value):
        self.__dict__.__setitem__(key, value)

    def __setitem__(self, key: str, value: typing.Any) -> typing.NoReturn:
        self.__dict__[key] = value

    def __getitem__(self, key: str):
        return self.__dict__[key]

    def pop(self, key: str, default: typing.Any = None) -> typing.Any:
        return self.__dict__.pop(key, default)

    def get(self, key: str) -> typing.Any:
        return self.__dict__.get(key)

    def __contains__(self, key: str) -> bool:
        return key in self.__dict__

and the usage of the Context

from sanic import Sanic
from sanic.response import json

app = Sanic("App Name")


@app.route("/set_context_variable")
async def set_context(request):
    request.context.check_data =  'hello world'
    return json({"context": "world"})


@app.route("/check_context_variable")
async def check_context(request):
    return json({"context": f'{request.context.get("check_data")}'})


@app.route('/delete_context_variable')
async def delete_context_variable(request):
    request.context.pop('check_data')
    return json({"context": f'{request.context.get("check_data")}'})

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8000, auto_reload=True)

if it sounds reasonable, I can make draft PR.

This is a very different concept. What you are talking about is something that is going to live in the individual process memory. To me, the biggest red flag that I see is that it will be majorly confusing to users.

  1. The name being close to ctx, it will not be obvious what context is and what ctx is.
  2. Undoubtedly, it will lead to unexpected behaviors in its implementation when someone adds some user related information, which then leaks to other requests.
  3. There are also gotchas being process specific. Spinning up multiple instances with either an orchestrator, workers, etc will not behave out of the box the same. So requests being routed to different instances would produce different results.

That is not to say that some use case of this does not exist. But, if it is, that is precisely the kind of thing that request.ctx and middleware are meant to achieve.

@app.middleware("request")
def inject_some_singleton(request):
    request.ctx.singleton = MyCustomSingleton()

Hi @5onic

A lot of what you’re suggesting here is already implemented (imho in a cleaner way) in the Contextualize plugin.
Contextualize is a light-weight bonus plugin that comes bundled with the SanicPluginsFramework library.
See https://pypi.org/project/Sanic-Plugins-Framework/

From the SPF readme:

The Context Object Manager

One feature that many find missing from Sanic is a context object. SPF provides multiple context objects that can be used for different purposes.

  • A shared context: All plugins registered in the SPF have access to a shared, persistent context object, which anyone can read and write to.
  • A per-request context: All plugins get access to a shared temporary context object anyone can read and write to that is created at the start of a request, and deleted when a request is completed.
  • A per-plugin context: All plugins get their own private persistent context object that only that plugin can read and write to.
  • A per-plugin per-request context: All plugins get a temporary private context object that is created at the start of a request, and deleted when a request is completed.

The Contextualize plugins serves to allow users access to the enhanced context manager of SPF, without having to write their own Sanic plugin in order to get access to it.

And a code example of how to use the Contextualize plugin here:

@ahopkins @ashleysommer Thank you, in that case, I don’t see any reasons to continue this topic :slightly_smiling_face: and going to close it.