user890167
user890167

Reputation:

Flask session attribute loses typing after initial load

Flask's session object appears to be changing an OrderedDict into a dict. This matters because I had hoped to store information regarding the current user's accessible pages in an OrderedDict to assist with automating the creation of the navigation bar.

I could be sleepy-eyed but I don't think I've written anything here that would overwrite existing data, so AFAICT it just converts to dict for no apparent reason.

Here's the code that sets the OrderedDict in the session object.

def safe_route(page_request):
    """Returns the correct page for the user's request.

    If a user requests a non-existent page, 404.
    If user wants a page they shouldn't see, but somehow
    they know about it, redirects to an access-denied page.

    If everything is in order, returns the page the user requests.
    """
    if page_request not in ordered_urls:
        return abort(404)      # early return for 404s

    # get current user and build their navi-dict.
    # this dict will be used to build the navibar in the webpage.
    if not 'registered' in session:
        CurrentUser = get_user(request)
        session['name'] = CurrentUser.name
        session['navi'] = build_user_navi(CurrentUser)
        session['registered'] = True
    if page_request in session['navi']:
        tpl = "/{}.html".format(page_request)
    else:
        # if here, the user requested a page that DOES exist
        # but they do NOT have access to. therefore, they are being
        # goons and require a trip to boxxytown
        tpl = "accessdenied.html"
    return render_template(tpl, on_page=page_request)

It checks to ensure that the session has the 'registered' flag; if not it gets the user information and then calls build_user_navi which returns the OrderedDict. This works the first time; the initial page load shows the links in the navibar as I originally intended. Upon subsequent clicking around, the order is lost and apparently so is the type on the OrderedDict.

Before the render_template call, I added this line in the code to see if it was just changing types on me:

print type(session['navi'])

Sure enough, it switches between <class 'collections.OrderedDict'> and <type 'dict'>.

Is there a method for correctly storing an OrderedDict as session data, or is there some Perfectly Good Reason why this would only work the first time which should dissuade me from this approach?

Upvotes: 3

Views: 770

Answers (2)

davidism
davidism

Reputation: 127340

All data in the session needs to be JSON serializable. An OrderedDict is serializable because it is a subclass of dict, but there is no ordered dict in JSON, so you lose order during serialization.

You can override how Flask serializes the session in order to support ordered dicts. It's not the most convenient process, since you have to copy the code for the class.

from base64 import b64encode, b64decode
from collections import OrderedDict
from datetime import datetime
import uuid
from flask import json
from flask._compat import text_type, iteritems
from flask.debughelpers import UnexpectedUnicodeError
from markupsafe import Markup
from werkzeug.http import http_date, parse_date


class TaggedJSONSerializer(object):
    def dumps(self, value):
        def _tag(value):
            if isinstance(value, tuple):
                return {' t': [_tag(x) for x in value]}
            elif isinstance(value, uuid.UUID):
                return {' u': value.hex}
            elif isinstance(value, bytes):
                return {' b': b64encode(value).decode('ascii')}
            elif callable(getattr(value, '__html__', None)):
                return {' m': text_type(value.__html__())}
            elif isinstance(value, list):
                return [_tag(x) for x in value]
            elif isinstance(value, datetime):
                return {' d': http_date(value)}
            elif isinstance(value, OrderedDict):
                return {'OrderedDict': [[k, _tag(v)] for k, v in iteritems(value)]}
            elif isinstance(value, dict):
                return dict((k, _tag(v)) for k, v in iteritems(value))
            elif isinstance(value, str):
                try:
                    return text_type(value)
                except UnicodeError:
                    raise UnexpectedUnicodeError(u'A byte string with '
                        u'non-ASCII data was passed to the session system '
                        u'which can only store unicode strings.  Consider '
                        u'base64 encoding your string (String was %r)' % value)
            return value
        return json.dumps(_tag(value), separators=(',', ':'))

    def loads(self, value):
        def object_hook(obj):
            if len(obj) != 1:
                return obj
            the_key, the_value = next(iteritems(obj))
            if the_key == ' t':
                return tuple(the_value)
            elif the_key == ' u':
                return uuid.UUID(the_value)
            elif the_key == ' b':
                return b64decode(the_value)
            elif the_key == ' m':
                return Markup(the_value)
            elif the_key == ' d':
                return parse_date(the_value)
            elif the_key == 'OrderedDict':
                return OrderedDict(the_value)
            return obj
        return json.loads(value, object_hook=object_hook)

Here's a demonstration of the new tag in action. The OrderedDict is deserialized correctly.

s = TaggedJSONSerializer()
data = OrderedDict((c, i) for i, c in enumerate('abcd'))
print(data)  # OrderedDict([('a', 0), ('b', 1), ('c', 2), ('d', 3)])
data = s.dumps(data)
print(data)  # {"OrderedDict":[["a",0],["b",1],["c",2],["d",3]]}
data = s.loads(data)
print(data)  # OrderedDict([('a', 0), ('b', 1), ('c', 2), ('d', 3)])

Then tell your app to use this serializer.

app.session_interface.serializer = TaggedJSONSerializer()

Upvotes: 2

9000
9000

Reputation: 40894

My suspicion is that session object is deep-copied somewhere, and the OrderedDict gets copied as a plain dict. This can be found out by e.g. consulting the source.

The simplest fix I'd come up with is to store a plain list of key-value pairs instead of an OrderedDict, which is about as easy to iterate over and is easy to convert to any king of dict should you need repeated fast lookup.

Upvotes: 1

Related Questions