Externally accessing the `request` object

Hi,

I’m coming from Flask and have the reflex to load the current request (or “g”) via the import of the module, which is not how Sanic behave.
Reading https://github.com/sanic-org/sanic/issues/69 kind of explains the reasons and motivations about it, but I have an implementation question coming from this.

Imagine the current scenario:

You have an endpoint: “POST /tags/” to create a new tag for your users
You have a middleware before that, that loads the currently connected user to request.ctx.user

On your endpoint, you pass the request.json to your a Form validation system to ensure that the tag name matches the correct rules (not too long, slug, etc).

Ideally too, on the Form validation, you would test if that name is not present on the database where you’ll save it, for that connected user.

That’s where I’m stuck. Back in the old day with Flask, I would do something like this (simplified):

from .models import Tag

class FormTag(BaseForm):
    name = Field(validators=[Length(max=25), Slug()])

    def validate_name(self):
        if Tag.find(self.name, request.ctx.user.id):
            raise ValidationError('This tag already exists')

@app.post('/tags/')
def add_tag(request):
    form = FormTag(request.json)
    tag = Tag.add(form.name)
    return json(tag))

Of course, I can’t access the request.ctx.user from the form since it’s not available that easily.
I could do the validation on the endpoint, but that’s not semantically correct : the validation should be on the form’s end.

My question is kind of a dual purpose: to show a common example to answer the question asked on the Github ticket (https://github.com/sanic-org/sanic/issues/69#issuecomment-761893451) and to hopefully find a proper and clean alternative way that works with Sanic?

Thanks in advance!

Hi @cx42net

This seems to be like an issue with the implementation in the FormTag class, particularly in the validate_name method.

The FormTag class is constructed from the contents of the Form, which doesn’t include the user’s id. But it has a validate_name() class which does need the user’s id in order to to a lookup.
Relying on magic global variables to exist at runtime is not very pythonic, and wouldn’t work in a async server like Sanic (for the reasons given in the thread you listed). I don’t know how your BaseForm class works, or how/when validate_name() is called, but the obvious solution to me would be to pass in the user’s id to perform the lookup:

class FormTag(BaseForm):
    name = Field(validators=[Length(max=25), Slug()])

    def validate_name(self, user_id):
        if Tag.find(self.name, user_id):
            raise ValidationError('This tag already exists')

@app.post('/tags/')
def add_tag(request):
    form = FormTag(request.json)
    v = form.validate_name(request.ctx.user.id)
    tag = Tag.add(form.name)
    return json(tag))

Or, if the Form class is going to need access to the user’s id for many different things, then add it to the constructor and associate the form with that user at the object level.

class FormTag(BaseForm):
    name = Field(validators=[Length(max=25), Slug()])
    _id: str
    
    def __init__(json_obj, user_id):
        super(FormTag, self).__init__(json_obj)
        self._id = user_id

    def validate_name(self):
        if Tag.find(self.name, self._id):
            raise ValidationError('This tag already exists')

@app.post('/tags/')
def add_tag(request):
    form = FormTag(request.json, request.ctx.user.id)
    v = form.validate_name()
    tag = Tag.add(form.name)
    return json(tag))

Let me know if I’ve missed the mark or completely misunderstood what you’re asking.

2 Likes

Thank you for your help.

I think the second example would make more sense.
All in all, I think the request must be passed to the form one way or another, so it makes sense to send it in the constructor to have access to it to all the methods in that class after that.

Thanks!