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

Commit f5b250e

Browse files
authored
Add string types (#14)
* Add string verbs to generate JSON object keys. - Drop read_all as we shouldn't try to save `init=False` keys in attrs. - Make sure helpers._Context is stringifying context items. * Don't stringify bool as keys; that should be a custom rule.
1 parent 2ceba6b commit f5b250e

File tree

9 files changed

+269
-185
lines changed

9 files changed

+269
-185
lines changed

json_syntax/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
)
2323
from .attrs import attrs_classes, named_tuples, tuples
2424
from .unions import unions
25+
from .string import stringify_keys
2526
from .helpers import JSON2PY, PY2JSON, INSP_PY, INSP_JSON, PATTERN # noqa
2627

2728

@@ -55,6 +56,7 @@ def std_ruleset(
5556
named_tuples,
5657
tuples,
5758
dicts,
59+
stringify_keys,
5860
unions,
5961
*extras,
6062
cache=cache,

json_syntax/attrs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def attrs_classes(
4747
"""
4848
if verb not in _SUPPORTED_VERBS:
4949
return
50-
inner_map = build_attribute_map(verb, typ, ctx, read_all=verb == PY2JSON)
50+
inner_map = build_attribute_map(verb, typ, ctx)
5151
if inner_map is None:
5252
return
5353

json_syntax/extras/dynamodb.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,16 @@
2525
Ref: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_AttributeValue.html#DDB-Type-AttributeValue-NS
2626
"""
2727

28-
from json_syntax.helpers import issub_safe, NoneType, has_origin, get_origin
28+
from json_syntax.helpers import (
29+
issub_safe,
30+
NoneType,
31+
has_origin,
32+
get_origin,
33+
STR2PY,
34+
PY2STR,
35+
)
2936
from json_syntax.product import build_attribute_map
37+
from json_syntax.string import stringify_keys
3038
from json_syntax.ruleset import SimpleRuleSet
3139

3240
import base64 as b64
@@ -39,6 +47,7 @@
3947

4048
DDB2PY = "dynamodb_to_python"
4149
PY2DDB = "python_to_dynamodb"
50+
_STRING_ACTIONS = {DDB2PY: STR2PY, PY2DDB: PY2STR}
4251

4352

4453
def booleans(verb, typ, ctx):
@@ -122,17 +131,17 @@ def dicts(verb, typ, ctx):
122131
"""
123132
A rule to represent lists as Dynamo list values.
124133
"""
125-
if not has_origin(typ, dict, num_args=2):
134+
if verb not in _STRING_ACTIONS or not has_origin(typ, dict, num_args=2):
126135
return
127136
(key_typ, val_typ) = typ.__args__
128-
if key_typ != str:
129-
return
130-
131-
inner = ctx.lookup(verb=verb, typ=val_typ)
137+
inner_key = ctx.lookup(verb=_STRING_ACTIONS[verb], typ=key_typ)
138+
inner_val = ctx.lookup(verb=verb, typ=val_typ)
132139
if verb == DDB2PY:
133-
return partial(decode_dict, inner_key=str, inner_val=inner, con=get_origin(typ))
140+
return partial(
141+
decode_dict, inner_key=inner_key, inner_val=inner_val, con=get_origin(typ)
142+
)
134143
elif verb == PY2DDB:
135-
return partial(encode_dict, inner=inner)
144+
return partial(encode_dict, inner_key=inner_key, inner_val=inner_val)
136145

137146

138147
def sets(verb, typ, ctx):
@@ -169,7 +178,7 @@ def attrs(verb, typ, ctx):
169178
"""
170179
A rule to represent attrs classes. This isn't trying to support hooks or any of that.
171180
"""
172-
inner_map = build_attribute_map(verb, typ, ctx, read_all=verb == PY2DDB)
181+
inner_map = build_attribute_map(verb, typ, ctx)
173182
if inner_map is None:
174183
return
175184

@@ -322,6 +331,7 @@ def dynamodb_ruleset(
322331
enums,
323332
sets,
324333
dicts,
334+
stringify_keys,
325335
optionals,
326336
nulls,
327337
*extras,
@@ -461,8 +471,8 @@ def decode_dict(value, inner_key, inner_val, con):
461471
return con(((inner_key(key), inner_val(val)) for key, val in value.items()))
462472

463473

464-
def encode_dict(value, inner):
465-
return {"M": {str(key): inner(val) for key, val in value.items()}}
474+
def encode_dict(value, inner_key, inner_val):
475+
return {"M": {inner_key(key): inner_val(val) for key, val in value.items()}}
466476

467477

468478
def decode_map(value, inner_map, con):

json_syntax/helpers.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
PY2JSON = "python_to_json"
1010
INSP_JSON = "inspect_json"
1111
INSP_PY = "inspect_python"
12+
INSP_STR = "inspect_string"
13+
STR2PY = "string_to_python"
14+
PY2STR = "python_to_string"
1215
PATTERN = "show_pattern"
1316
NoneType = type(None)
1417
SENTINEL = object()
@@ -173,7 +176,7 @@ def __init__(self, original, lead, context):
173176

174177
def __str__(self):
175178
return "{}{}{}".format(
176-
self.original, self.lead, "".join(reversed(self.context))
179+
self.original, self.lead, "".join(map(str, reversed(self.context)))
177180
)
178181

179182
def __repr__(self):

json_syntax/product.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def attr_map(verb, outer, ctx, gen):
101101
return tuple(result)
102102

103103

104-
def build_attribute_map(verb, typ, ctx, read_all):
104+
def build_attribute_map(verb, typ, ctx):
105105
"""
106106
Examine an attrs or dataclass type and construct a list of attributes.
107107
@@ -129,7 +129,7 @@ def build_attribute_map(verb, typ, ctx, read_all):
129129
default=field.default,
130130
)
131131
for field in fields
132-
if read_all or field.init
132+
if field.init
133133
),
134134
)
135135

json_syntax/std.py

Lines changed: 18 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
INSP_JSON,
99
INSP_PY,
1010
PATTERN,
11+
STR2PY,
12+
PY2STR,
13+
INSP_STR,
1114
)
1215
from .action_v1 import (
1316
check_collection,
@@ -288,45 +291,29 @@ def sets(verb, typ, ctx):
288291
return pat.Array.homog(inner)
289292

290293

291-
def _stringly(verb, typ, ctx):
292-
"""
293-
Rule to handle types that reliably convert directly to strings.
294-
295-
This is used internally by dicts.
296-
"""
297-
for base in str, int:
298-
if typ == base:
299-
if verb == PATTERN and base == str:
300-
return pat.String.any
301-
if verb in (JSON2PY, PY2JSON):
302-
return base
303-
elif verb == INSP_PY:
304-
return partial(check_isinst, typ=base)
305-
elif verb in (INSP_JSON, PATTERN):
306-
inspect = partial(check_parse_error, parser=base, error=ValueError)
307-
return pat.String(typ.__name__, inspect) if verb == PATTERN else inspect
308-
if typ in (datetime, time):
309-
return
310-
for rule in enums, iso_dates:
311-
action = rule(verb=verb, typ=typ, ctx=ctx)
312-
if action is not None:
313-
return action
294+
_STRING = {
295+
JSON2PY: STR2PY,
296+
PY2JSON: PY2STR,
297+
INSP_JSON: INSP_STR,
298+
INSP_PY: INSP_PY,
299+
PATTERN: PATTERN,
300+
}
314301

315302

316303
def dicts(verb, typ, ctx):
317304
"""
318305
Handle a ``Dict[key, value]`` where key is a string, integer, date or enum type.
319306
"""
320-
if not has_origin(typ, (dict, OrderedDict), num_args=2):
307+
if verb not in _STRING or not has_origin(typ, (dict, OrderedDict), num_args=2):
321308
return
322309
(key_type, val_type) = typ.__args__
323-
key_type = _stringly(verb=verb, typ=key_type, ctx=ctx)
324-
if key_type is None:
325-
return
326-
val_type = ctx.lookup(verb=verb, typ=val_type)
310+
inner_key = ctx.lookup(verb=_STRING[verb], typ=key_type)
311+
inner_val = ctx.lookup(verb=verb, typ=val_type)
327312
if verb in (JSON2PY, PY2JSON):
328-
return partial(convert_mapping, key=key_type, val=val_type, con=get_origin(typ))
313+
return partial(
314+
convert_mapping, key=inner_key, val=inner_val, con=get_origin(typ)
315+
)
329316
elif verb in (INSP_JSON, INSP_PY):
330-
return partial(check_mapping, key=key_type, val=val_type, con=get_origin(typ))
317+
return partial(check_mapping, key=inner_key, val=inner_val, con=get_origin(typ))
331318
elif verb == PATTERN:
332-
return pat.Object.homog(key_type, val_type)
319+
return pat.Object.homog(inner_key, inner_val)

json_syntax/string.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from .action_v1 import (
2+
check_parse_error,
3+
check_str_enum,
4+
convert_date,
5+
convert_enum_str,
6+
convert_str_enum,
7+
)
8+
from .helpers import STR2PY, PY2STR, INSP_STR, issub_safe
9+
10+
from datetime import date
11+
from enum import Enum
12+
from functools import partial
13+
14+
15+
"""
16+
As JSON requires string keys, unless dicts are only allowed to be Dict[str, T], we need to be able to encode
17+
values as strings.
18+
19+
Recommendations:
20+
21+
* The string verbs are not intended for direct use.
22+
* Use these verbs for any type that must be represented as a key in a JSON object.
23+
* The standard rules will only handle types that are reliable keys and have obvious string encodings.
24+
25+
See std.dicts for an example.
26+
"""
27+
28+
29+
def stringify_keys(verb, typ, ctx):
30+
if verb not in (STR2PY, PY2STR, INSP_STR):
31+
return
32+
if typ in (str, int):
33+
if verb == STR2PY:
34+
return typ
35+
elif verb == PY2STR:
36+
return str
37+
elif verb == INSP_STR:
38+
return partial(check_parse_error, parser=typ, error=ValueError)
39+
elif typ == date:
40+
if verb == PY2STR:
41+
return typ.isoformat
42+
elif verb in (STR2PY, INSP_STR):
43+
parse = convert_date
44+
if verb == STR2PY:
45+
return parse
46+
else:
47+
return partial(
48+
check_parse_error, parser=parse, error=(TypeError, ValueError)
49+
)
50+
elif issub_safe(typ, Enum):
51+
if verb == PY2STR:
52+
return partial(convert_enum_str, typ=typ)
53+
elif verb == STR2PY:
54+
return partial(convert_str_enum, typ=typ)
55+
elif verb == INSP_STR:
56+
return partial(check_str_enum, typ=typ)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "json-syntax"
3-
version = "2.0.2"
3+
version = "2.1.0"
44
description = "Generates functions to convert Python classes to JSON dumpable objects."
55
authors = ["Ben Samuel <bsamuel@unitedincome.com>"]
66
license = "MIT"

0 commit comments

Comments
 (0)