Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make session serializer extensible #2352

Merged
merged 8 commits into from
Jun 2, 2017
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ Major release, unreleased
- ``TRAP_BAD_REQUEST_ERRORS`` is enabled by default in debug mode.
``BadRequestKeyError`` has a message with the bad key in debug mode instead
of the generic bad request message. (`#2348`_)
- Allow registering new tags with ``TaggedJSONSerializer`` to support
storing other types in the session cookie. (`#2352`_)

.. _#1489: https://github.com/pallets/flask/pull/1489
.. _#1621: https://github.com/pallets/flask/pull/1621
Expand All @@ -84,6 +86,7 @@ Major release, unreleased
.. _#2319: https://github.com/pallets/flask/pull/2319
.. _#2326: https://github.com/pallets/flask/pull/2326
.. _#2348: https://github.com/pallets/flask/pull/2348
.. _#2352: https://github.com/pallets/flask/pull/2352

Version 0.12.2
--------------
Expand Down
13 changes: 2 additions & 11 deletions flask/json.py → flask/json/__init__.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
# -*- coding: utf-8 -*-
"""
flask.json
~~~~~~~~~~

Implementation helpers for the JSON support in Flask.

:copyright: (c) 2015 by Armin Ronacher.
:license: BSD, see LICENSE for more details.
"""
import io
import uuid
from datetime import date
from .globals import current_app, request
from ._compat import text_type, PY2
from flask.globals import current_app, request
from flask._compat import text_type, PY2

from werkzeug.http import http_date
from jinja2 import Markup
Expand Down
253 changes: 253 additions & 0 deletions flask/json/tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
from base64 import b64decode, b64encode
from datetime import datetime
from uuid import UUID

from jinja2 import Markup
from werkzeug.http import http_date, parse_date

from flask._compat import iteritems, text_type
from flask.json import dumps, loads


class JSONTag(object):
"""Base class for defining type tags for :class:`TaggedJSONSerializer`."""

__slots__ = ('serializer',)
key = None
"""The tag to mark the serialized object with. If ``None``, this tag is

This comment was marked as off-topic.

This comment was marked as off-topic.

This comment was marked as off-topic.

only used as an intermediate step during tagging."""

def __init__(self, serializer):
"""Create a tagger for the given serializer."""

This comment was marked as off-topic.

self.serializer = serializer

def check(self, value):
"""Check if the given value should be tagged by this tag."""

raise NotImplementedError

def to_json(self, value):
"""Convert the Python object to an object that is a valid JSON type.
The tag will be added later."""

raise NotImplementedError

def to_python(self, value):
"""Convert the JSON representation back to the correct type. The tag
will already be removed."""

raise NotImplementedError

def tag(self, value):
"""Convert the value to a valid JSON type and add the tag structure
around it."""

return {self.key: self.to_json(value)}


class TagDict(JSONTag):
"""Tag for 1-item dicts whose only key matches a registered tag.

Internally, the dict key is suffixed with `__`, and the suffix is removed
when deserializing.
"""

__slots__ = ('serializer',)
key = ' di'

def check(self, value):
return (
isinstance(value, dict)
and len(value) == 1
and next(iter(value)) in self.serializer.tags
)

def to_json(self, value):
key = next(iter(value))
return {key + '__': self.serializer.tag(value[key])}

def to_python(self, value):
key = next(iter(value))
return {key[:-2]: value[key]}


class PassDict(JSONTag):
__slots__ = ('serializer',)

def check(self, value):
return isinstance(value, dict)

def to_json(self, value):
# JSON objects may only have string keys, so don't bother tagging the
# key here.
return dict((k, self.serializer.tag(v)) for k, v in iteritems(value))

tag = to_json


class TagTuple(JSONTag):
__slots__ = ('serializer',)
key = ' t'

def check(self, value):
return isinstance(value, tuple)

def to_json(self, value):
return [self.serializer.tag(item) for item in value]

def to_python(self, value):
return tuple(value)


class PassList(JSONTag):
__slots__ = ('serializer',)

def check(self, value):
return isinstance(value, list)

def to_json(self, value):
return [self.serializer.tag(item) for item in value]

tag = to_json


class TagBytes(JSONTag):
__slots__ = ('serializer',)
key = ' b'

def check(self, value):
return isinstance(value, bytes)

def to_json(self, value):
return b64encode(value).decode('ascii')

def to_python(self, value):
return b64decode(value)


class TagMarkup(JSONTag):
"""Serialize anything matching the :class:`~markupsafe.Markup` API by
having a ``__html__`` method to the result of that method. Always
deserializes to an instance of :class:`~markupsafe.Markup`."""

__slots__ = ('serializer',)
key = ' m'

def check(self, value):
return callable(getattr(value, '__html__', None))

def to_json(self, value):
return text_type(value.__html__())

def to_python(self, value):
return Markup(value)


class TagUUID(JSONTag):
__slots__ = ('serializer',)
key = ' u'

def check(self, value):
return isinstance(value, UUID)

def to_json(self, value):
return value.hex

def to_python(self, value):
return UUID(value)


class TagDateTime(JSONTag):
__slots__ = ('serializer',)
key = ' d'

def check(self, value):
return isinstance(value, datetime)

def to_json(self, value):
return http_date(value)

def to_python(self, value):
return parse_date(value)


class TaggedJSONSerializer(object):
"""Serializer that uses a tag system to compactly represent objects that
are not JSON types. Passed as the intermediate serializer to
:class:`itsdangerous.Serializer`."""

__slots__ = ('tags', 'order')
default_tags = [
TagDict, PassDict, TagTuple, PassList, TagBytes, TagMarkup, TagUUID,
TagDateTime,
]
"""Tag classes to bind when creating the serializer. Other tags can be
added later using :meth:`~register`."""

def __init__(self):
self.tags = {}
self.order = []

for cls in self.default_tags:
self.register(cls)

def register(self, tag_class, force=False, index=-1):
"""Register a new tag with this serializer.

:param tag_class: tag class to register. Will be instantiated with this
serializer instance.
:param force: overwrite an existing tag. If false (default), a
:exc:`KeyError` is raised.
:param index: index to insert the new tag in the tag order. Useful when
the new tag is a special case of an existing tag. If -1 (default),
the tag is appended to the end of the order.

:raise KeyError: if the tag key is already registered and ``force`` is
not true.
"""

tag = tag_class(self)
key = tag.key

if key is not None:
if not force and key in self.tags:
raise KeyError("Tag '{0}' is already registered.".format(key))

self.tags[key] = tag

if index == -1:
self.order.append(tag)
else:
self.order.insert(index, tag)

def tag(self, value):
"""Convert a value to a tagged representation if necessary."""

for tag in self.order:
if tag.check(value):
return tag.tag(value)

return value

def untag(self, value):
"""Convert a tagged representation back to the original type."""

if len(value) != 1:
return value

key = next(iter(value))

if key not in self.tags:
return value

return self.tags[key].to_python(value[key])

def dumps(self, value):
"""Tag the value and dump it to a compact JSON string."""

return dumps(self.tag(value), separators=(',', ':'))

def loads(self, value):
"""Load data from a JSON string and deserialized any tagged objects."""
return loads(value, object_hook=self.untag)
Loading