Skip to content

Commit 16a9335

Browse files
author
David Robertson
committed
Type annotations for canonicaljson
1 parent b9d6381 commit 16a9335

File tree

6 files changed

+73
-22
lines changed

6 files changed

+73
-22
lines changed

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ include *.py
33
include *.md
44
include LICENSE
55
include tox.ini
6+
include pyproject.toml
67
prune .travis
78
prune debian

canonicaljson.py

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,14 @@
1616
# limitations under the License.
1717

1818
import platform
19-
from typing import Optional, Type
19+
from typing import Any, Generator, Optional, Type
2020

21-
frozendict_type: Optional[Type]
21+
try:
22+
from typing import Protocol
23+
except ImportError: # pragma: no cover
24+
from typing_extensions import Protocol # type: ignore[misc]
25+
26+
frozendict_type: Optional[Type[Any]]
2227
try:
2328
from frozendict import frozendict as frozendict_type
2429
except ImportError:
@@ -27,22 +32,37 @@
2732
__version__ = "1.6.0"
2833

2934

30-
def _default(obj): # pragma: no cover
35+
def _default(obj: object) -> object: # pragma: no cover
3136
if type(obj) is frozendict_type:
3237
# If frozendict is available and used, cast `obj` into a dict
33-
return dict(obj)
38+
return dict(obj) # type: ignore[call-overload]
3439
raise TypeError(
3540
"Object of type %s is not JSON serializable" % obj.__class__.__name__
3641
)
3742

3843

44+
class Encoder(Protocol): # pragma: no cover
45+
def encode(self, data: object) -> str:
46+
pass
47+
48+
def iterencode(self, data: object) -> Generator[str, None, None]:
49+
pass
50+
51+
def __call__(self, *args: Any, **kwargs: Any) -> "Encoder":
52+
pass
53+
54+
55+
class JsonLibrary(Protocol):
56+
JSONEncoder: Encoder
57+
58+
3959
# Declare these in the module scope, but they get configured in
4060
# set_json_library.
41-
_canonical_encoder = None
42-
_pretty_encoder = None
61+
_canonical_encoder: Encoder = None # type: ignore[assignment]
62+
_pretty_encoder: Encoder = None # type: ignore[assignment]
4363

4464

45-
def set_json_library(json_lib):
65+
def set_json_library(json_lib: JsonLibrary) -> None:
4666
"""
4767
Set the underlying JSON library that canonicaljson uses to json_lib.
4868
@@ -69,7 +89,7 @@ def set_json_library(json_lib):
6989
)
7090

7191

72-
def encode_canonical_json(json_object):
92+
def encode_canonical_json(json_object: object) -> bytes:
7393
"""Encodes the shortest UTF-8 JSON encoding with dictionary keys
7494
lexicographically sorted by unicode code point.
7595
@@ -82,7 +102,7 @@ def encode_canonical_json(json_object):
82102
return s.encode("utf-8")
83103

84104

85-
def iterencode_canonical_json(json_object):
105+
def iterencode_canonical_json(json_object: object) -> Generator[bytes, None, None]:
86106
"""Encodes the shortest UTF-8 JSON encoding with dictionary keys
87107
lexicographically sorted by unicode code point.
88108
@@ -95,7 +115,7 @@ def iterencode_canonical_json(json_object):
95115
yield chunk.encode("utf-8")
96116

97117

98-
def encode_pretty_printed_json(json_object):
118+
def encode_pretty_printed_json(json_object: object) -> bytes:
99119
"""
100120
Encodes the JSON object dict as human readable UTF-8 bytes.
101121
@@ -108,7 +128,7 @@ def encode_pretty_printed_json(json_object):
108128
return _pretty_encoder.encode(json_object).encode("utf-8")
109129

110130

111-
def iterencode_pretty_printed_json(json_object):
131+
def iterencode_pretty_printed_json(json_object: object) -> Generator[bytes, None, None]:
112132
"""Encodes the JSON object dict as human readable UTF-8 bytes.
113133
114134
Args:
@@ -132,7 +152,7 @@ def iterencode_pretty_printed_json(json_object):
132152
#
133153
# Note that it seems performance is on par or better using json from the
134154
# standard library as of Python 3.7.
135-
import simplejson as json
155+
import simplejson as json # type: ignore[no-redef]
136156

137157
# Set the JSON library to the backwards compatible version.
138158
set_json_library(json)

pyproject.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[tool.mypy]
2+
show_error_codes = true
3+
strict = true
4+
5+
files = ["."]
6+
exclude = "setup.py"
7+
#mypy_path = "stubs"
8+
9+
#[[tool.mypy.overrides]]
10+
#module = [
11+
# "idna",
12+
# "netaddr",
13+
# "prometheus_client",
14+
# "signedjson.*",
15+
# "sortedcontainers",
16+
#]
17+
#ignore_missing_imports = true
18+

setup.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ def exec_file(path_segments, name):
4949
# simplerjson versions before 3.14.0 had a bug with some characters
5050
# (e.g. \u2028) if ensure_ascii was set to false.
5151
"simplejson>=3.14.0",
52+
# typing.Protocol was only added to the stdlib in Python 3.8
53+
"typing_extensions>=4.0.0; python_version < '3.8'",
5254
],
5355
extras_require={
5456
# frozendict support can be enabled using the `canonicaljson[frozendict]` syntax

test_canonicaljson.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131

3232

3333
class TestCanonicalJson(unittest.TestCase):
34-
def test_encode_canonical(self):
34+
def test_encode_canonical(self) -> None:
3535
self.assertEqual(encode_canonical_json({}), b"{}")
3636

3737
# ctrl-chars should be encoded.
@@ -68,7 +68,7 @@ def test_encode_canonical(self):
6868
# Iteratively encoding should work.
6969
self.assertEqual(list(iterencode_canonical_json({})), [b"{}"])
7070

71-
def test_ascii(self):
71+
def test_ascii(self) -> None:
7272
"""
7373
Ensure the proper ASCII characters are escaped.
7474
@@ -95,10 +95,10 @@ def test_ascii(self):
9595
# And other characters are passed unescaped.
9696
unescaped = [0x20, 0x21] + list(range(0x23, 0x5C)) + list(range(0x5D, 0x7E))
9797
for c in unescaped:
98-
c = chr(c)
99-
self.assertEqual(encode_canonical_json(c), b'"' + c.encode("ascii") + b'"')
98+
s = chr(c)
99+
self.assertEqual(encode_canonical_json(s), b'"' + s.encode("ascii") + b'"')
100100

101-
def test_encode_pretty_printed(self):
101+
def test_encode_pretty_printed(self) -> None:
102102
self.assertEqual(encode_pretty_printed_json({}), b"{}")
103103
self.assertEqual(list(iterencode_pretty_printed_json({})), [b"{}"])
104104

@@ -112,7 +112,9 @@ def test_encode_pretty_printed(self):
112112
frozendict_type is None,
113113
"If `frozendict` is not available, skip test",
114114
)
115-
def test_frozen_dict(self):
115+
def test_frozen_dict(self) -> None:
116+
# For mypy's benefit:
117+
assert frozendict_type is not None
116118
self.assertEqual(
117119
encode_canonical_json(frozendict_type({"a": 1})),
118120
b'{"a":1}',
@@ -122,7 +124,7 @@ def test_frozen_dict(self):
122124
b'{\n "a": 1\n}',
123125
)
124126

125-
def test_unknown_type(self):
127+
def test_unknown_type(self) -> None:
126128
class Unknown(object):
127129
pass
128130

@@ -133,7 +135,7 @@ class Unknown(object):
133135
with self.assertRaises(Exception):
134136
encode_pretty_printed_json(unknown_object)
135137

136-
def test_invalid_float_values(self):
138+
def test_invalid_float_values(self) -> None:
137139
"""Infinity/-Infinity/NaN are not allowed in canonicaljson."""
138140

139141
with self.assertRaises(ValueError):
@@ -154,7 +156,7 @@ def test_invalid_float_values(self):
154156
with self.assertRaises(ValueError):
155157
encode_pretty_printed_json(nan)
156158

157-
def test_set_json(self):
159+
def test_set_json(self) -> None:
158160
"""Ensure that changing the underlying JSON implementation works."""
159161
mock_json = mock.Mock(spec=["JSONEncoder"])
160162
mock_json.JSONEncoder.return_value.encode.return_value = "sentinel"
@@ -163,6 +165,6 @@ def test_set_json(self):
163165
self.assertEqual(encode_canonical_json({}), b"sentinel")
164166
finally:
165167
# Reset the JSON library to whatever was originally set.
166-
from canonicaljson import json
168+
from canonicaljson import json # type: ignore[attr-defined]
167169

168170
set_json_library(json)

tox.ini

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,11 @@ basepython = python3.7
2626
deps =
2727
black==21.9b0
2828
commands = python -m black --check --diff .
29+
30+
[testenv:mypy]
31+
deps =
32+
mypy==0.942
33+
types-frozendict==2.0.8
34+
types-simplejson==3.17.5
35+
types-setuptools==57.4.14
36+
commands = mypy

0 commit comments

Comments
 (0)