-
Notifications
You must be signed in to change notification settings - Fork 15
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
Type annotations for canonicaljson #49
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,5 +3,6 @@ include *.py | |
include *.md | ||
include LICENSE | ||
include tox.ini | ||
include pyproject.toml | ||
prune .travis | ||
prune debian |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,9 +16,14 @@ | |
# limitations under the License. | ||
|
||
import platform | ||
from typing import Optional, Type | ||
from typing import Any, Generator, Optional, Type | ||
|
||
frozendict_type: Optional[Type] | ||
try: | ||
from typing import Protocol | ||
except ImportError: # pragma: no cover | ||
from typing_extensions import Protocol # type: ignore[misc] | ||
|
||
frozendict_type: Optional[Type[Any]] | ||
try: | ||
from frozendict import frozendict as frozendict_type | ||
except ImportError: | ||
|
@@ -27,22 +32,37 @@ | |
__version__ = "1.6.0" | ||
|
||
|
||
def _default(obj): # pragma: no cover | ||
def _default(obj: object) -> object: # pragma: no cover | ||
if type(obj) is frozendict_type: | ||
# If frozendict is available and used, cast `obj` into a dict | ||
return dict(obj) | ||
return dict(obj) # type: ignore[call-overload] | ||
raise TypeError( | ||
"Object of type %s is not JSON serializable" % obj.__class__.__name__ | ||
) | ||
|
||
|
||
class Encoder(Protocol): # pragma: no cover | ||
def encode(self, data: object) -> str: | ||
pass | ||
|
||
def iterencode(self, data: object) -> Generator[str, None, None]: | ||
pass | ||
|
||
def __call__(self, *args: Any, **kwargs: Any) -> "Encoder": | ||
pass | ||
|
||
|
||
class JsonLibrary(Protocol): | ||
JSONEncoder: Encoder | ||
|
||
|
||
# Declare these in the module scope, but they get configured in | ||
# set_json_library. | ||
_canonical_encoder = None | ||
_pretty_encoder = None | ||
_canonical_encoder: Encoder = None # type: ignore[assignment] | ||
_pretty_encoder: Encoder = None # type: ignore[assignment] | ||
|
||
|
||
def set_json_library(json_lib): | ||
def set_json_library(json_lib: JsonLibrary) -> None: | ||
""" | ||
Set the underlying JSON library that canonicaljson uses to json_lib. | ||
|
||
|
@@ -69,55 +89,44 @@ def set_json_library(json_lib): | |
) | ||
|
||
|
||
def encode_canonical_json(json_object): | ||
"""Encodes the shortest UTF-8 JSON encoding with dictionary keys | ||
lexicographically sorted by unicode code point. | ||
def encode_canonical_json(data: object) -> bytes: | ||
"""Encodes the given `data` as a UTF-8 canonical JSON bytestring. | ||
|
||
Args: | ||
json_object (dict): The JSON object to encode. | ||
|
||
Returns: | ||
bytes encoding the JSON object""" | ||
s = _canonical_encoder.encode(json_object) | ||
return s.encode("utf-8") | ||
|
||
|
||
def iterencode_canonical_json(json_object): | ||
"""Encodes the shortest UTF-8 JSON encoding with dictionary keys | ||
This encoding is the shortest possible. Dictionary keys are | ||
lexicographically sorted by unicode code point. | ||
""" | ||
s = _canonical_encoder.encode(data) | ||
return s.encode("utf-8") | ||
|
||
Args: | ||
json_object (dict): The JSON object to encode. | ||
|
||
Returns: | ||
generator which yields bytes encoding the JSON object""" | ||
for chunk in _canonical_encoder.iterencode(json_object): | ||
yield chunk.encode("utf-8") | ||
def iterencode_canonical_json(data: object) -> Generator[bytes, None, None]: | ||
"""Iteratively encodes the given `data` as a UTF-8 canonical JSON bytestring. | ||
|
||
This yields one or more bytestrings; concatenating them all together yields the | ||
full encoding of `data`. Building up the encoding gradually in this way allows us to | ||
encode large pieces of `data` without blocking other tasks. | ||
|
||
def encode_pretty_printed_json(json_object): | ||
This encoding is the shortest possible. Dictionary keys are | ||
lexicographically sorted by unicode code point. | ||
""" | ||
Encodes the JSON object dict as human readable UTF-8 bytes. | ||
|
||
Args: | ||
json_object (dict): The JSON object to encode. | ||
|
||
Returns: | ||
bytes encoding the JSON object""" | ||
for chunk in _canonical_encoder.iterencode(data): | ||
yield chunk.encode("utf-8") | ||
|
||
return _pretty_encoder.encode(json_object).encode("utf-8") | ||
|
||
def encode_pretty_printed_json(data: object) -> bytes: | ||
"""Encodes the given `data` as a UTF-8 human-readable JSON bytestring.""" | ||
|
||
def iterencode_pretty_printed_json(json_object): | ||
"""Encodes the JSON object dict as human readable UTF-8 bytes. | ||
return _pretty_encoder.encode(data).encode("utf-8") | ||
|
||
Args: | ||
json_object (dict): The JSON object to encode. | ||
|
||
Returns: | ||
generator which yields bytes encoding the JSON object""" | ||
def iterencode_pretty_printed_json(data: object) -> Generator[bytes, None, None]: | ||
"""Iteratively encodes the given `data` as a UTF-8 human-readable JSON bytestring. | ||
|
||
for chunk in _pretty_encoder.iterencode(json_object): | ||
This yields one or more bytestrings; concatenating them all together yields the | ||
full encoding of `data`. Building up the encoding gradually in this way allows us to | ||
encode large pieces of `data` without blocking other tasks. | ||
""" | ||
for chunk in _pretty_encoder.iterencode(data): | ||
yield chunk.encode("utf-8") | ||
|
||
|
||
|
@@ -132,7 +141,7 @@ def iterencode_pretty_printed_json(json_object): | |
# | ||
# Note that it seems performance is on par or better using json from the | ||
# standard library as of Python 3.7. | ||
import simplejson as json | ||
import simplejson as json # type: ignore[no-redef] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Arg, I wish we could figure out a way to kill this, but I don't think we can due to old Synapse versions which unconditionally install it... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ahh, I hadn't realised that Synapse imports and uses IMO the dependencies that we control (canonicaljson, signedjson, matrix-common?) should be specified in Synapse's metadata with semver bounds to avoid this sort of thing. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Absolutely, but we didn't set an upper bound previously and there isn't really a way to go back and fix that (unless maybe once the Python's from those versions are unsupported?) 🤷 |
||
|
||
# Set the JSON library to the backwards compatible version. | ||
set_json_library(json) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
[tool.mypy] | ||
show_error_codes = true | ||
strict = true | ||
|
||
files = ["."] | ||
exclude = "setup.py" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you do something silly like
import json as _json
and then set these types to_json.JSONEncoder
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe---but that might cause further pain having to wrangle with
simplejson
.(I was trying to avoid touching any of the import stuff---feels a bit icky.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Being completely honest a lot of this is overkill: the main benefit to Synapse is the existence of
-> bytes
and-> Generator[bytes, None, None]
return types.