Skip to content
This repository was archived by the owner on Oct 8, 2021. It is now read-only.

Commit f8b8815

Browse files
committed
Merge branch 'expand-pythons'
2 parents b103c69 + e5d1616 commit f8b8815

File tree

14 files changed

+312
-298
lines changed

14 files changed

+312
-298
lines changed

README.md

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ classes using customizable rules.
77

88
If you're like the authors, you tried writing a encoding function that attempted to
99
encode and decode by interrogating the types at runtime, maybe calling some method like
10-
`asdict`. This works fine for generating JSON, but it gets sketchy when trying to decode
11-
the same JSON.
10+
`asdict`. This works fine for generating JSON, but it gets sketchy<sup
11+
id="a1">[1](#f1)</sup> when trying to decode the same JSON.
1212

1313
Further, we have annotations in Python 3! Even if you're not using a type checker, just
1414
labeling the types of fields makes complex data structures far more comprehensible.
@@ -123,6 +123,18 @@ them, and constructs an action to represent them.
123123
}
124124
```
125125

126+
#### Actual usage
127+
128+
The aim of all this is to enable reliable usage with your preferred JSON library:
129+
130+
```python
131+
with open('myfile.json', 'r') as fh:
132+
my_account = decode_account(json.load(fh))
133+
134+
with open('myfile.json', 'w') as fh:
135+
json.dump(encode_account(my_account))
136+
```
137+
126138
### Using generic types
127139

128140
Generally, the [typing][] module simple provides capital letter type names that obviously
@@ -156,7 +168,7 @@ The standard rules don't support:
156168
A union type lets you present alternate types that the converters will attempt in
157169
sequence, e.g. `typing.Union[MyType, int, MyEnum]`.
158170

159-
This is implemented in the `unions` rule as a so-called<sup id="a1">[1](#f1)</sup>
171+
This is implemented in the `unions` rule as a so-called<sup id="a2">[2](#f2)</sup>
160172
undiscriminated union. It means the module won't add any additional information to the
161173
value such as some kind of explicit tag.
162174

@@ -229,7 +241,8 @@ class AbstractAccount:
229241
class AccountA(AbstractAccount):
230242
...
231243

232-
encode_account = rules.lookup(typ=Union[AccountA, AccountB, AccountC], verb='python_to_json')
244+
encode_account = rules.lookup(typ=Union[AccountA, AccountB, AccountC],
245+
verb='python_to_json')
233246
```
234247

235248
### Adding custom rules
@@ -293,30 +306,36 @@ This package is maintained via the [poetry][] tool. Some useful commands:
293306

294307
1. Setup: `poetry install`
295308
2. Run tests: `poetry run pytest tests/`
296-
3. Reformat: `poetry run black json_syntax/ tests/`
309+
3. Reformat: `poetry run black -N json_syntax/ tests/`
297310

298311
### Setting up tox
299312

300313
You'll want pyenv, then install the pythons:
301314

302-
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
303-
pyenv install --list | egrep '^ *3\.[4567]|^ *pypy3.5'
304-
# figure out what versions you want
305-
for v in 3.4.9 3.5.10 ...; do
306-
pyenv install $v
307-
PYENV_VERSION=$v python get-pip.py
308-
done
315+
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
316+
pyenv install --list | egrep '^ *3\.[4567]|^ *pypy3.5'
317+
# figure out what versions you want
318+
for v in 3.4.9 3.5.10 ...; do
319+
pyenv install $v
320+
PYENV_VERSION=$v python get-pip.py
321+
done
309322

310-
Once you install `tox` in your preferred python, you should be good to go.
323+
Once you install `tox` in your preferred python, running it is just `tox`.
311324

312325
### Notes
313326

314-
<b id="f1">1</b>: A discriminated union has a tag that identifies the variant, such as
327+
<b id="f1">1</b>: Writing the encoder is deceptively easy because the instances in
328+
Python have complete information. The standard `json` module provides a hook to let
329+
you encode an object, and another hook to recognize `dict`s that have some special
330+
attribute. This can work quite well, but you'll have to encode *all* non-JSON types
331+
with dict-wrappers for the process to work in reverse. [](#a1)
332+
333+
<b id="f2">2</b>: A discriminated union has a tag that identifies the variant, such as
315334
status codes that indicate success and a payload, or some error. Strictly, all unions
316-
must be discriminated in some way if different code paths are executed. In the
317-
`unions` rule, the discriminant is the class information in Python, and the structure of
318-
the JSON data. A less flattering description would be that this is a "poorly"
319-
discriminated union. [](#a1)
335+
must be discriminated in some way if different code paths are executed. In the `unions`
336+
rule, the discriminant is the class information in Python, and the structure of the JSON
337+
data. A less flattering description would be that this is a "poorly" discriminated
338+
union. [](#a2)
320339

321340
[poetry]: https://poetry.eustace.io/docs/#installation
322341
[gradual typing]: https://www.python.org/dev/peps/pep-0483/#summary-of-gradual-typing

json_syntax/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
)
2323
from .attrs import attrs_classes, named_tuples, tuples
2424
from .unions import unions
25-
from .helpers import J2P, P2J, IP, IJ # noqa
25+
from .helpers import JSON2PY, PY2JSON, INSP_PY, INSP_JSON # noqa
2626

2727

2828
def std_ruleset(

json_syntax/attrs.py

Lines changed: 24 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
from .helpers import (
2-
IJ,
3-
IP,
4-
J2P,
5-
JPI,
6-
P2J,
2+
JSON2PY,
3+
PY2JSON,
4+
INSP_JSON,
5+
INSP_PY,
76
SENTINEL,
87
has_origin,
98
identity,
@@ -34,7 +33,7 @@ def attrs_classes(
3433
"""
3534
Handle an ``@attr.s`` or ``@dataclass`` decorated class.
3635
"""
37-
if verb not in JPI:
36+
if verb not in (JSON2PY, PY2JSON, INSP_PY, INSP_JSON):
3837
return
3938
try:
4039
fields = typ.__attrs_attrs__
@@ -46,38 +45,38 @@ def attrs_classes(
4645
else:
4746
fields = fields.values()
4847

49-
if verb == IP:
48+
if verb == INSP_PY:
5049
return partial(check_isinst, typ=typ)
5150

5251
inner_map = []
5352
for field in fields:
54-
if field.init or verb == P2J:
53+
if field.init or verb == PY2JSON:
5554
tup = (
5655
field.name,
5756
ctx.lookup(
5857
verb=verb, typ=resolve_fwd_ref(field.type, typ), accept_missing=True
5958
),
6059
)
61-
if verb == P2J:
60+
if verb == PY2JSON:
6261
tup += (field.default,)
63-
elif verb == IJ:
62+
elif verb == INSP_JSON:
6463
tup += (is_attrs_field_required(field),)
6564
inner_map.append(tup)
6665

67-
if verb == J2P:
66+
if verb == JSON2PY:
6867
pre_hook_method = getattr(typ, pre_hook, identity)
6968
return partial(
7069
convert_dict_to_attrs,
7170
pre_hook=pre_hook_method,
7271
inner_map=tuple(inner_map),
7372
con=typ,
7473
)
75-
elif verb == P2J:
74+
elif verb == PY2JSON:
7675
post_hook = post_hook if hasattr(typ, post_hook) else None
7776
return partial(
7877
convert_attrs_to_dict, post_hook=post_hook, inner_map=tuple(inner_map)
7978
)
80-
elif verb == IJ:
79+
elif verb == INSP_JSON:
8180
check = getattr(typ, check, None)
8281
if check:
8382
return check
@@ -91,7 +90,7 @@ def named_tuples(verb, typ, ctx):
9190
9291
Also handles a ``collections.namedtuple`` if you have a fallback handler.
9392
"""
94-
if verb not in JPI or not issub_safe(typ, tuple):
93+
if verb not in (JSON2PY, PY2JSON, INSP_PY, INSP_JSON) or not issub_safe(typ, tuple):
9594
return
9695
try:
9796
fields = typ._field_types
@@ -103,7 +102,7 @@ def named_tuples(verb, typ, ctx):
103102
fields = [(name, None) for name in fields]
104103
else:
105104
fields = fields.items()
106-
if verb == IP:
105+
if verb == INSP_PY:
107106
return partial(check_isinst, typ=typ)
108107

109108
defaults = {}
@@ -115,24 +114,24 @@ def named_tuples(verb, typ, ctx):
115114
name,
116115
ctx.lookup(verb=verb, typ=resolve_fwd_ref(inner, typ), accept_missing=True),
117116
)
118-
if verb == P2J:
117+
if verb == PY2JSON:
119118
tup += (defaults.get(name, SENTINEL),)
120-
elif verb == IJ:
119+
elif verb == INSP_JSON:
121120
tup += (name not in defaults,)
122121
inner_map.append(tup)
123122

124-
if verb == J2P:
123+
if verb == JSON2PY:
125124
return partial(
126125
convert_dict_to_attrs,
127126
pre_hook=identity,
128127
inner_map=tuple(inner_map),
129128
con=typ,
130129
)
131-
elif verb == P2J:
130+
elif verb == PY2JSON:
132131
return partial(
133132
convert_attrs_to_dict, post_hook=None, inner_map=tuple(inner_map)
134133
)
135-
elif verb == IJ:
134+
elif verb == INSP_JSON:
136135
return partial(check_dict, pre_hook=identity, inner_map=tuple(inner_map))
137136

138137

@@ -141,18 +140,18 @@ def tuples(verb, typ, ctx):
141140
Handle a ``Tuple[type, type, type]`` product type. Use a ``NamedTuple`` if you don't
142141
want a list.
143142
"""
144-
if verb not in JPI or not has_origin(typ, tuple):
143+
if verb not in (JSON2PY, PY2JSON, INSP_PY, INSP_JSON) or not has_origin(typ, tuple):
145144
return
146145
args = typ.__args__
147146
if Ellipsis in args:
148147
# This is a homogeneous tuple, use the lists rule.
149148
return
150149
inner = [ctx.lookup(verb=verb, typ=arg) for arg in args]
151-
if verb == J2P:
150+
if verb == JSON2PY:
152151
return partial(convert_tuple_as_list, inner=inner, con=tuple)
153-
elif verb == P2J:
152+
elif verb == PY2JSON:
154153
return partial(convert_tuple_as_list, inner=inner, con=list)
155-
elif verb == IP:
154+
elif verb == INSP_PY:
156155
return partial(check_tuple_as_list, inner=inner, con=tuple)
157-
elif verb == IJ:
156+
elif verb == INSP_JSON:
158157
return partial(check_tuple_as_list, inner=inner, con=list)

json_syntax/examples/flags.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
This lets you construct a quick set of enums that are represented as strings.
55
"""
66

7-
from ..helpers import JP, II
7+
from ..helpers import JSON2PY, PY2JSON, INSP_JSON, INSP_PY
88
from functools import partial
99

1010

@@ -70,7 +70,7 @@ def flags(*, verb, typ, ctx):
7070
"""
7171
if not isinstance(typ, Flag):
7272
return
73-
if verb in JP:
73+
if verb in (JSON2PY, PY2JSON):
7474
return partial(_convert_flag, typ.elems)
75-
elif verb in II:
75+
elif verb in (INSP_JSON, INSP_PY):
7676
return partial(_check_flag, typ.elems)

json_syntax/examples/loose_dates.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from json_syntax.helpers import J2P, P2J, IJ, IP
1+
from json_syntax.helpers import JSON2PY, PY2JSON, INSP_JSON, INSP_PY
22
from json_syntax.action_v1 import check_parse_error, check_has_type
33

44
from datetime import date, datetime
@@ -21,13 +21,13 @@ def convert_date_loosely(value):
2121

2222
def iso_dates_loose(verb, typ, ctx):
2323
if typ == date:
24-
if verb == P2J:
24+
if verb == PY2JSON:
2525
return date.isoformat
26-
elif verb == J2P:
26+
elif verb == JSON2PY:
2727
return convert_date_loosely
28-
elif verb == IP:
28+
elif verb == INSP_PY:
2929
return partial(check_has_type, typ=date)
30-
elif verb == IJ:
30+
elif verb == INSP_JSON:
3131
return partial(
3232
check_parse_error,
3333
parser=convert_date_loosely,

json_syntax/helpers.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,10 @@
55

66
_eval_type = getattr(t, "_eval_type", None)
77
logger = logging.getLogger(__name__)
8-
J2P = "json_to_python"
9-
P2J = "python_to_json"
10-
IJ = "inspect_json"
11-
IP = "inspect_python"
12-
II = (IJ, IP)
13-
JP = (J2P, P2J)
14-
JPI = (J2P, P2J, IP, IJ)
8+
JSON2PY = "json_to_python"
9+
PY2JSON = "python_to_json"
10+
INSP_JSON = "inspect_json"
11+
INSP_PY = "inspect_python"
1512
NoneType = type(None)
1613
SENTINEL = object()
1714
python_minor = sys.version_info[:2]
@@ -180,7 +177,7 @@ def is_attrs_field_required(field):
180177
return factory in _missing_values
181178

182179

183-
def _add_context(exc, context):
180+
def _add_context(context, exc):
184181
try:
185182
if exc is None:
186183
return

0 commit comments

Comments
 (0)