14

I have a decorator which adds a user onto the flask global context g:

class User:
    def __init__(self, user_data) -> None:
        self.username: str = user_data["username"]
        self.email: str = user_data["email"]

def login_required(f):
    @wraps(f)
    def wrap(*args, **kwargs):
        user_data = get_user_data()
        user = User(user_data)
        g.user = User(user_data)

        return f(*args, **kwargs)

    return wrap

I want the type (User) of g.user to be known when I access g.user in the controllers. How can I achieve this? (I am using pyright)

Chris St Pierre
  • 141
  • 2
  • 4

4 Answers4

15

I had a similar issue described in Typechecking dynamically added attributes. One solution is to add the custom type hints using typing.TYPE_CHECKING:

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from flask.ctx import _AppCtxGlobals

    class MyGlobals(_AppCtxGlobals):
        user: 'User'

    g = MyGlobals()
else:
    from flask import g

Now e.g.

reveal_type(g.user)

will emit

note: Revealed type is 'myapp.User'

If the custom types should be reused in multiple modules, you can introduce a partial stub for flask. The location of the stubs is dependent on the type checker, e.g. mypy reads custom stubs from the MYPY_PATH environment variable, pyright looks for a typings directory in the project root dir etc. Example of a partial stub:

# _typeshed/flask/__init__.pyi

from typing import Any
from flask.ctx import _AppCtxGlobals
from models import User


def __getattr__(name: str) -> Any: ...  # incomplete


class MyGlobals(_AppCtxGlobals):
    user: User
    def __getattr__(self, name: str) -> Any: ...  # incomplete


g: MyGlobals
hoefling
  • 59,418
  • 12
  • 147
  • 194
4

This is a solution with an opinion: flask.g is magic and is tied really hard to the server implementation. IMO, its usage should be kept contained and minimal.

I have created a file to manage g, which allowed me to type it

    # request_context.py
    from flask import g
    from somewhere import User
    
    def set_user(user: User) -> None:
       g.user = user
    
    def get_user() -> User:
       # you could validate here that the user exists
       return g.user

and then in your code:

    # yourcode.py
    import request_context
    
    class User:
        ...
    
    def login_required(f):
        @wraps(f)
        def wrap(*args, **kwargs):
            user_data = get_user_data()
            user = User(user_data)
            request_context.set_user(User(user_data))
    
            return f(*args, **kwargs)
    
        return wrap

Mr. Nun.
  • 775
  • 11
  • 29
3

You could proxy the g object. Consider the following implementation:

import flask


class User:
    ...


class _g:

    user: User
    # Add type hints for other attributes
    # ...

    def __getattr__(self, key):
        return getattr(flask.g, key)


g = _g()

-2

You can annotate an attribute on a class, even if that class isn't yours, simply with a colon after it. For example:

g.user: User

That's it. Since it's presumably valid everywhere, I would put it at the top of your code:

from functools import wraps

from flask import Flask, g

app = Flask(__name__)


class User:
    def __init__(self, user_data) -> None:
        self.username: str = user_data["username"]
        self.email: str = user_data["email"]


# Annotate the g.user attribute
g.user: User


def login_required(f):
    @wraps(f)
    def wrap(*args, **kwargs):
        g.user = User({'username': 'wile-e-coyote',
                       'email': 'coyote@localhost'})

        return f(*args, **kwargs)

    return wrap


@app.route('/')
@login_required
def hello_world():
    return f'Hello, {g.user.email}'


if __name__ == '__main__':
    app.run()

That's it.

Ken Kinder
  • 12,654
  • 6
  • 50
  • 70
  • 2
    When I do this I get the following mypy error: Type cannot be declared in assignment to non-self attribute – axwell Jun 18 '20 at 12:28
  • Given that mypy is stated to be the reference implementation, I think it's pretty important that the answer works with mypy. If this isn't supposed to happen then I guess it's a bug in mypy which should be reported to mypy. – Teymour Aug 05 '20 at 09:57
  • It is really unfortunate that this does not work with mypy. PyCharm does not recognize this construct either. @Ken Kinder, how did you get this to work? What IDE are you using? – MEE Sep 03 '21 at 22:45