-
-
Notifications
You must be signed in to change notification settings - Fork 16.3k
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
Changes from 5 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
8a8a608
Move object_hook outside loads method so class can be extend and reused
s-block d911c89
Merge branch 'master' into master
kennethreitz ea2e960
Merge branch 'master' into json-object-hook
davidism 5e1ced3
make session serializer extensible
davidism ca176cb
pass serializer at tag init instead of to each method
davidism bbd15d5
docs style cleanup
davidism 9bee250
finish documentation
davidism fd8b959
add tests for flask.json.tag
davidism File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
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.
Sorry, something went wrong. |
||
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) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
This comment was marked as off-topic.
Sorry, something went wrong.
This comment was marked as off-topic.
Sorry, something went wrong.
This comment was marked as off-topic.
Sorry, something went wrong.