Skip to content

Commit

Permalink
Fix incomplete json parsing of records due to nested ForwardRefs in f…
Browse files Browse the repository at this point in the history
…ield annotations
  • Loading branch information
Tinitto committed Feb 27, 2023
1 parent 3bfc61e commit 7fa1d1e
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 213 deletions.
31 changes: 25 additions & 6 deletions funml/data/records.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class Color:
Set,
List,
Union,
ForwardRef,
)

from typing_extensions import dataclass_transform
Expand Down Expand Up @@ -234,12 +235,14 @@ def _normalize(cls, _globals: Dict[str, Any], _locals: Dict[str, Any]):
module = importlib.import_module(cls.__module_path__)
_globals.update(_default_globals)
_globals.update(getattr(module, "__dict__", {}))
_annotations = {
key: value
if not isinstance(value, str)
else _parse_lazy_type(_to_generic(value), _globals, _locals)
for key, value in cls.__annotations__.items()
}
_annotations = {}

for key, value in cls.__annotations__.items():
if isinstance(value, str):
value = _parse_lazy_type(_to_generic(value), _globals, _locals)

value = _evaluate_forward_refs(value, _globals, _locals)
_annotations[key] = value

cls.__annotations__ = _annotations
cls._validate_class_defaults()
Expand Down Expand Up @@ -322,6 +325,22 @@ def _parse_lazy_type(
raise exp


def _evaluate_forward_refs(
type_: Type,
__globals: Optional[Dict[str, Any]] = ...,
__locals: Optional[Mapping[str, Any]] = ...,
) -> Type:
"""Evaluates any forward refs in the given type"""
try:
type_.__args__ = tuple(
_evaluate_forward_refs(arg, __globals, __locals) for arg in type_.__args__
)
except AttributeError:
if isinstance(type_, ForwardRef):
return eval(type_.__forward_arg__, __globals, __locals)
return type_


def _get_cls_defaults(cls: type, _annotations: Dict[str, type]) -> Dict[str, Any]:
"""Retrieves all default values of a class attributes.
Expand Down
1 change: 1 addition & 0 deletions funml/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"""
import inspect
import json
import typing
from typing import Any, TypeVar, Type, Mapping, Tuple, Union, Dict

from funml import Enum, Record
Expand Down
32 changes: 32 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from __future__ import annotations
from typing import List, Any

from funml import record, Enum


@record
class Student:
name: str
favorite_color: "Color"


@record
class Color:
r: int
g: int
b: int
a: List["Alpha"]


class Alpha(Enum):
OPAQUE = None
TRANSLUCENT = float


@record
class Department:
seniors: list[str]
juniors: List[str]
locations: tuple[str, ...]
misc: dict[str, Any]
head: str
10 changes: 3 additions & 7 deletions tests/test_enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import pytest

from funml import Option, Result, Enum, record, to_json, from_json
from tests import conftest


def test_enum_creation():
Expand Down Expand Up @@ -180,11 +181,6 @@ class Alpha(Enum):

def test_from_json_strict():
"""from_json with strict transforms a JSON string representation into an Enum or errors"""

class Alpha(Enum):
OPAQUE = None
TRANSLUCENT = float

test_data = [
'"Alph.OPAQUE: "OPAQUE""',
'"OPAQUE: "OPAQUE""',
Expand All @@ -193,6 +189,6 @@ class Alpha(Enum):

for item in test_data:
with pytest.raises(ValueError, match=r"unable to deserialize JSON.*"):
from_json(Alpha, item)
from_json(conftest.Alpha, item)

assert from_json(Alpha, item, strict=False) == item
assert from_json(conftest.Alpha, item, strict=False) == item
132 changes: 48 additions & 84 deletions tests/test_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

import pytest

from funml import l, imap, ifilter, ireduce, Enum, record, to_json, from_json
from funml import l, imap, ifilter, ireduce, to_json, from_json
from funml.data.lists import IList
from tests import conftest


def test_list_creation():
Expand Down Expand Up @@ -96,58 +97,45 @@ def test_tail():

def test_to_json():
"""to_json method transforms list into a JSON string representation of list"""

@record
class Student:
name: str
favorite_color: "Color"

@record
class Color:
r: int
g: int
b: int
a: "Alpha"

class Alpha(Enum):
OPAQUE = None
TRANSLUCENT = float

test_data = [
(l(2, 3, 5), "[2, 3, 5]"),
(l("foo", 6.0), '["foo", 6.0]'),
(l(True, -6.0, 7), "[true, -6.0, 7]"),
(
l(
True,
Color(r=8, g=4, b=78, a=Alpha.OPAQUE),
Color(r=55, g=40, b=9, a=Alpha.TRANSLUCENT(0.4)),
conftest.Color(r=8, g=4, b=78, a=[conftest.Alpha.OPAQUE]),
conftest.Color(r=55, g=40, b=9, a=[conftest.Alpha.TRANSLUCENT(0.4)]),
),
(
"["
"true, "
'{"r": 8, "g": 4, "b": 78, "a": "Alpha.OPAQUE: \\"OPAQUE\\""}, '
'{"r": 55, "g": 40, "b": 9, "a": "Alpha.TRANSLUCENT: 0.4"}'
'{"r": 8, "g": 4, "b": 78, "a": ["Alpha.OPAQUE: \\"OPAQUE\\""]}, '
'{"r": 55, "g": 40, "b": 9, "a": ["Alpha.TRANSLUCENT: 0.4"]}'
"]"
),
),
(
l(
True,
Student(
conftest.Student(
name="John Doe",
favorite_color=Color(r=8, g=4, b=78, a=Alpha.OPAQUE),
favorite_color=conftest.Color(
r=8, g=4, b=78, a=[conftest.Alpha.OPAQUE]
),
),
Student(
conftest.Student(
name="Jane Doe",
favorite_color=Color(r=55, g=40, b=9, a=Alpha.TRANSLUCENT(0.4)),
favorite_color=conftest.Color(
r=55, g=40, b=9, a=[conftest.Alpha.TRANSLUCENT(0.4)]
),
),
),
(
"["
"true, "
'{"name": "John Doe", "favorite_color": {"r": 8, "g": 4, "b": 78, "a": "Alpha.OPAQUE: \\"OPAQUE\\""}}, '
'{"name": "Jane Doe", "favorite_color": {"r": 55, "g": 40, "b": 9, "a": "Alpha.TRANSLUCENT: 0.4"}}'
'{"name": "John Doe", "favorite_color": {"r": 8, "g": 4, "b": 78, "a": ["Alpha.OPAQUE: \\"OPAQUE\\""]}}, '
'{"name": "Jane Doe", "favorite_color": {"r": 55, "g": 40, "b": 9, "a": ["Alpha.TRANSLUCENT: 0.4"]}}'
"]"
),
),
Expand All @@ -160,53 +148,41 @@ class Alpha(Enum):
def test_from_json_strict():
"""from_json with strict transforms a JSON string representation into an IList of given annotation"""

@record
class Student:
name: str
favorite_color: "Color"

@record
class Color:
r: int
g: int
b: int
a: "Alpha"

class Alpha(Enum):
OPAQUE = None
TRANSLUCENT = float

test_data = [
("[2, 3, 5]", IList[Any], l(2, 3, 5)),
(
(
"["
'{"r": 8, "g": 4, "b": 78, "a": "Alpha.OPAQUE: \\"OPAQUE\\""}, '
'{"r": 55, "g": 40, "b": 9, "a": "Alpha.TRANSLUCENT: 0.4"}'
'{"r": 8, "g": 4, "b": 78, "a": ["Alpha.OPAQUE: \\"OPAQUE\\""]}, '
'{"r": 55, "g": 40, "b": 9, "a": ["Alpha.TRANSLUCENT: 0.4"]}'
"]"
),
IList[Color],
IList[conftest.Color],
l(
Color(r=8, g=4, b=78, a=Alpha.OPAQUE),
Color(r=55, g=40, b=9, a=Alpha.TRANSLUCENT(0.4)),
conftest.Color(r=8, g=4, b=78, a=[conftest.Alpha.OPAQUE]),
conftest.Color(r=55, g=40, b=9, a=[conftest.Alpha.TRANSLUCENT(0.4)]),
),
),
(
(
"["
'{"name": "John Doe", "favorite_color": {"r": 8, "g": 4, "b": 78, "a": "Alpha.OPAQUE: \\"OPAQUE\\""}}, '
'{"name": "Jane Doe", "favorite_color": {"r": 55, "g": 40, "b": 9, "a": "Alpha.TRANSLUCENT: 0.4"}}'
'{"name": "John Doe", "favorite_color": {"r": 8, "g": 4, "b": 78, "a": ["Alpha.OPAQUE: \\"OPAQUE\\""]}}, '
'{"name": "Jane Doe", "favorite_color": {"r": 55, "g": 40, "b": 9, "a": ["Alpha.TRANSLUCENT: 0.4"]}}'
"]"
),
IList[Student],
IList[conftest.Student],
l(
Student(
conftest.Student(
name="John Doe",
favorite_color=Color(r=8, g=4, b=78, a=Alpha.OPAQUE),
favorite_color=conftest.Color(
r=8, g=4, b=78, a=[conftest.Alpha.OPAQUE]
),
),
Student(
conftest.Student(
name="Jane Doe",
favorite_color=Color(r=55, g=40, b=9, a=Alpha.TRANSLUCENT(0.4)),
favorite_color=conftest.Color(
r=55, g=40, b=9, a=[conftest.Alpha.TRANSLUCENT(0.4)]
),
),
),
),
Expand All @@ -221,58 +197,46 @@ def test_from_json_not_strict():
"""from_json with not strict attempts to transform each item in a JSON string IList representation to the given annotation,
defaulting to the expected JSON.loads output on error"""

@record
class Student:
name: str
favorite_color: "Color"

@record
class Color:
r: int
g: int
b: int
a: "Alpha"

class Alpha(Enum):
OPAQUE = None
TRANSLUCENT = float

test_data = [
('["foo", 6.0]', IList[int], l("foo", 6)),
("[true, -6.0, 7]", IList[int], l(1, -6, 7)),
(
(
"["
"true, "
'{"r": 8, "g": 4, "b": 78, "a": "Alpha.OPAQUE: \\"OPAQUE\\""}, '
'{"r": 55, "g": 40, "b": 9, "a": "Alpha.TRANSLUCENT: 0.4"}'
'{"r": 8, "g": 4, "b": 78, "a": ["Alpha.OPAQUE: \\"OPAQUE\\""]}, '
'{"r": 55, "g": 40, "b": 9, "a": ["Alpha.TRANSLUCENT: 0.4"]}'
"]"
),
IList[Color],
IList[conftest.Color],
l(
True,
Color(r=8, g=4, b=78, a=Alpha.OPAQUE),
Color(r=55, g=40, b=9, a=Alpha.TRANSLUCENT(0.4)),
conftest.Color(r=8, g=4, b=78, a=[conftest.Alpha.OPAQUE]),
conftest.Color(r=55, g=40, b=9, a=[conftest.Alpha.TRANSLUCENT(0.4)]),
),
),
(
(
"["
'{"name": "John Doe", "favorite_color": {"r": 8, "g": 4, "b": 78, "a": "Alpha.OPAQUE: \\"OPAQUE\\""}}, '
'{"name": "John Doe", "favorite_color": {"r": 8, "g": 4, "b": 78, "a": ["Alpha.OPAQUE: \\"OPAQUE\\""]}}, '
'"foo", '
'{"name": "Jane Doe", "favorite_color": {"r": 55, "g": 40, "b": 9, "a": "Alpha.TRANSLUCENT: 0.4"}}'
'{"name": "Jane Doe", "favorite_color": {"r": 55, "g": 40, "b": 9, "a": ["Alpha.TRANSLUCENT: 0.4"]}}'
"]"
),
IList[Student],
IList[conftest.Student],
l(
Student(
conftest.Student(
name="John Doe",
favorite_color=Color(r=8, g=4, b=78, a=Alpha.OPAQUE),
favorite_color=conftest.Color(
r=8, g=4, b=78, a=[conftest.Alpha.OPAQUE]
),
),
"foo",
Student(
conftest.Student(
name="Jane Doe",
favorite_color=Color(r=55, g=40, b=9, a=Alpha.TRANSLUCENT(0.4)),
favorite_color=conftest.Color(
r=55, g=40, b=9, a=[conftest.Alpha.TRANSLUCENT(0.4)]
),
),
),
),
Expand Down
Loading

0 comments on commit 7fa1d1e

Please sign in to comment.