Skip to content

Commit

Permalink
Add mostly-correct support for the option type in Python.
Browse files Browse the repository at this point in the history
The JSON representation is now compatible with ATD's convention.
The Python representation, however, is still a nullable since Python
doesn't have a standard option type.
See #332
  • Loading branch information
mjambon committed Mar 14, 2023
1 parent 6bd89e1 commit f810db8
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 8 deletions.
8 changes: 6 additions & 2 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

* atdgen: Annotate generated code with types to disambiguate OCaml
classic variants (#331)
* atdpy: Disable incorrect interpretation of option types and produce
a useful error message (#332)
* atdpy: Support the option type more correctly so that it follows
ATD's convention for JSON encoding. This allows compatibility with
JSON produced by other tools of the ATD suite. The Python type,
however, is still a nullable (`Optional`) to make things simpler for
Python programmers. This prevents distinguishing `["Some", "None"]`
from `"None"` which both translate to `None` in Python. (#332)

2.11.0 (2023-02-08)
-------------------
Expand Down
33 changes: 27 additions & 6 deletions atdpy/src/lib/Codegen.ml
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,19 @@ def _atd_read_nullable(read_elt: Callable[[Any], Any]) \
return read_nullable


def _atd_read_option(read_elt: Callable[[Any], Any]) \
-> Callable[[Optional[Any]], Optional[Any]]:
def read_option(x: Any) -> Any:
if x == 'None':
return None
elif isinstance(x, List) and len(x) == 2 and x[0] == 'Some':
return read_elt(x[1])
else:
_atd_bad_json('option', x)
raise AssertionError('impossible') # keep mypy happy
return read_option


def _atd_write_unit(x: Any) -> None:
if x is None:
return x
Expand Down Expand Up @@ -427,6 +440,16 @@ def _atd_write_nullable(write_elt: Callable[[Any], Any]) \
return write_nullable


def _atd_write_option(write_elt: Callable[[Any], Any]) \
-> Callable[[Optional[Any]], Optional[Any]]:
def write_option(x: Any) -> Any:
if x is None:
return 'None'
else:
return ['Some', write_elt(x)]
return write_option


############################################################################
# Public classes
############################################################################|}
Expand All @@ -436,10 +459,6 @@ def _atd_write_nullable(write_elt: Callable[[Any], Any]) \
let not_implemented loc msg =
A.error_at loc ("not implemented in atdpy: " ^ msg)

let not_implemented_option loc =
not_implemented loc
"option outside of an optional field; use nullable instead"

let todo hint =
failwith ("TODO: " ^ hint)

Expand Down Expand Up @@ -613,7 +632,8 @@ let rec json_writer env e =
sprintf "_atd_write_assoc_list_to_object(%s)"
(json_writer env value)
)
| Option (loc, e, an) -> not_implemented_option loc
| Option (loc, e, an) ->
sprintf "_atd_write_option(%s)" (json_writer env e)
| Nullable (loc, e, an) ->
sprintf "_atd_write_nullable(%s)" (json_writer env e)
| Shared (loc, e, an) -> not_implemented loc "shared"
Expand Down Expand Up @@ -694,7 +714,8 @@ let rec json_reader env (e : type_expr) =
sprintf "_atd_read_assoc_object_into_list(%s)"
(json_reader env value)
)
| Option (loc, e, an) -> not_implemented_option loc
| Option (loc, e, an) ->
sprintf "_atd_read_option(%s)" (json_reader env e)
| Nullable (loc, e, an) ->
sprintf "_atd_read_nullable(%s)" (json_reader env e)
| Shared (loc, e, an) -> not_implemented loc "shared"
Expand Down
1 change: 1 addition & 0 deletions atdpy/test/atd-input/everything.atd
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type root = {
assoc3: (float * int) list <python repr="dict">;
assoc4: (string * int) list <json repr="object"> <python repr="dict">;
nullables: int nullable list;
options: int option list;
untyped_things: abstract list;
parametrized_record: (int, float) parametrized_record;
parametrized_tuple: kind parametrized_tuple;
Expand Down
26 changes: 26 additions & 0 deletions atdpy/test/python-expected/everything.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,19 @@ def read_nullable(x: Any) -> Any:
return read_nullable


def _atd_read_option(read_elt: Callable[[Any], Any]) \
-> Callable[[Optional[Any]], Optional[Any]]:
def read_option(x: Any) -> Any:
if x == 'None':
return None
elif isinstance(x, List) and len(x) == 2 and x[0] == 'Some':
return read_elt(x[1])
else:
_atd_bad_json('option', x)
raise AssertionError('impossible') # keep mypy happy
return read_option


def _atd_write_unit(x: Any) -> None:
if x is None:
return x
Expand Down Expand Up @@ -232,6 +245,16 @@ def write_nullable(x: Any) -> Any:
return write_nullable


def _atd_write_option(write_elt: Callable[[Any], Any]) \
-> Callable[[Optional[Any]], Optional[Any]]:
def write_option(x: Any) -> Any:
if x is None:
return 'None'
else:
return ['Some', write_elt(x)]
return write_option


############################################################################
# Public classes
############################################################################
Expand Down Expand Up @@ -475,6 +498,7 @@ class Root:
assoc3: Dict[float, int]
assoc4: Dict[str, int]
nullables: List[Optional[int]]
options: List[Optional[int]]
untyped_things: List[Any]
parametrized_record: IntFloatParametrizedRecord
parametrized_tuple: KindParametrizedTuple
Expand All @@ -498,6 +522,7 @@ def from_json(cls, x: Any) -> 'Root':
assoc3=_atd_read_assoc_array_into_dict(_atd_read_float, _atd_read_int)(x['assoc3']) if 'assoc3' in x else _atd_missing_json_field('Root', 'assoc3'),
assoc4=_atd_read_assoc_object_into_dict(_atd_read_int)(x['assoc4']) if 'assoc4' in x else _atd_missing_json_field('Root', 'assoc4'),
nullables=_atd_read_list(_atd_read_nullable(_atd_read_int))(x['nullables']) if 'nullables' in x else _atd_missing_json_field('Root', 'nullables'),
options=_atd_read_list(_atd_read_option(_atd_read_int))(x['options']) if 'options' in x else _atd_missing_json_field('Root', 'options'),
untyped_things=_atd_read_list((lambda x: x))(x['untyped_things']) if 'untyped_things' in x else _atd_missing_json_field('Root', 'untyped_things'),
parametrized_record=IntFloatParametrizedRecord.from_json(x['parametrized_record']) if 'parametrized_record' in x else _atd_missing_json_field('Root', 'parametrized_record'),
parametrized_tuple=KindParametrizedTuple.from_json(x['parametrized_tuple']) if 'parametrized_tuple' in x else _atd_missing_json_field('Root', 'parametrized_tuple'),
Expand All @@ -522,6 +547,7 @@ def to_json(self) -> Any:
res['assoc3'] = _atd_write_assoc_dict_to_array(_atd_write_float, _atd_write_int)(self.assoc3)
res['assoc4'] = _atd_write_assoc_dict_to_object(_atd_write_int)(self.assoc4)
res['nullables'] = _atd_write_list(_atd_write_nullable(_atd_write_int))(self.nullables)
res['options'] = _atd_write_list(_atd_write_option(_atd_write_int))(self.options)
res['untyped_things'] = _atd_write_list((lambda x: x))(self.untyped_things)
res['parametrized_record'] = (lambda x: x.to_json())(self.parametrized_record)
res['parametrized_tuple'] = (lambda x: x.to_json())(self.parametrized_tuple)
Expand Down
12 changes: 12 additions & 0 deletions atdpy/test/python-tests/test_atdpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ def test_everything_to_json() -> None:
"h": 8,
},
nullables=[12, None, 34],
options=[56, None, 78],
untyped_things=[[["hello"]], {}, None, 123],
parametrized_record=e.IntFloatParametrizedRecord(
field_a=42,
Expand Down Expand Up @@ -166,6 +167,17 @@ def test_everything_to_json() -> None:
null,
34
],
"options": [
[
"Some",
56
],
"None",
[
"Some",
78
]
],
"untyped_things": [
[
[
Expand Down

0 comments on commit f810db8

Please sign in to comment.