Skip to content

Commit

Permalink
Fix to recognize TypedDict values with extra keys
Browse files Browse the repository at this point in the history
Fixes #19
  • Loading branch information
davidfstr committed Nov 11, 2023
1 parent 563775d commit 1d7ca63
Show file tree
Hide file tree
Showing 3 changed files with 39 additions and 6 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,12 @@ Raises:
* See the [Roadmap](https://github.com/davidfstr/trycast/wiki/Roadmap).
### v1.1.0
* Fix `trycast()` to recognize TypedDicts with extra keys. ([#19](https://github.com/davidfstr/trycast/issues/19))
* This new behavior helps recognize JSON structures with arbitrary additional keys
and is consistent with how static typecheckers treat additional keys.
### v1.0.0
* Extend `trycast()` to recognize more kinds of types:
Expand Down
34 changes: 29 additions & 5 deletions tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -940,20 +940,39 @@ class Point3D(RichTypedDict):
z: int

# Point2D
self.assertTryCastFailure(Point2D, {"x": 1})
self.assertTryCastSuccess(Point2D, {"x": 1, "y": 1})
self.assertTryCastFailure(Point2D, {"x": 1, "y": 1, "z": 1})
self.assertTryCastFailure(Point2D, {"x": 1, "y": "string"})
self.assertTryCastSuccess(Point2D, {"x": 1, "y": 1, "z": 1})
self.assertTryCastSuccess(Point2D, {"x": 1, "y": 1, "z": "string"})

# PartialPoint2D
self.assertTryCastSuccess(PartialPoint2D, {"x": 1, "y": 1})
self.assertTryCastFailure(PartialPoint2D, {"x": 1, "y": "string"})
self.assertTryCastSuccess(PartialPoint2D, {"y": 1})
self.assertTryCastSuccess(PartialPoint2D, {"x": 1})
self.assertTryCastSuccess(PartialPoint2D, {})
self.assertTryCastFailure(PartialPoint2D, {"x": 1, "y": 1, "z": 1})
self.assertTryCastSuccess(PartialPoint2D, {"x": 1, "y": 1, "z": 1})
self.assertTryCastSuccess(PartialPoint2D, {"x": 1, "y": 1, "z": "string"})

# Point3D
self.assertTryCastFailure(Point3D, {"x": 1, "y": 1})
self.assertTryCastSuccess(Point3D, {"x": 1, "y": 1, "z": 1})

def test_typeddict_with_extra_keys(self) -> None:
class Packet(NativeTypedDict):
type: str
payload: str

class PacketWithExtra(Packet):
extra: str

p = PacketWithExtra(type="hello", payload="v1", extra="english") # type: ignore[28] # pyre
p2: Packet = p

self.assertTryCastSuccess(PacketWithExtra, p)
self.assertTryCastSuccess(Packet, p2)

def test_typeddict_single_inheritance(self) -> None:
_1 = _BookBasedMovie(
name="Blade Runner",
Expand Down Expand Up @@ -1182,7 +1201,7 @@ def __getitem__(self, key: str) -> object:
if key == "name":
return self._name
else:
raise AttributeError # pragma: no cover
raise KeyError # pragma: no cover

def __len__(self) -> int:
return 1 # pragma: no cover
Expand Down Expand Up @@ -1828,16 +1847,21 @@ class TaggedMaybePoint1D(MaybePoint1D):
name: str

self.assertTryCastSuccess(Point2D, {"x": 1, "y": 2}, strict=False)
self.assertTryCastFailure(Point2D, {"x": 1, "y": "string"}, strict=False)
self.assertTryCastFailure(Point2D, {"x": 1}, strict=False)

self.assertTryCastSuccess(Point3D, {"x": 1, "y": 2, "z": 3}, strict=False)
self.assertTryCastFailure(
Point3D, {"x": 1, "y": 2, "z": "string"}, strict=False
)
self.assertTryCastSuccess(Point3D, {"x": 1, "y": 2}, strict=False)
self.assertTryCastSuccess(Point3D, {"x": 1}, strict=False) # surprise!
self.assertTryCastFailure(Point3D, {"q": 1}, strict=False)
self.assertTryCastSuccess(Point3D, {"q": 1}, strict=False) # surprise!

self.assertTryCastSuccess(MaybePoint1D, {"x": 1}, strict=False)
self.assertTryCastFailure(MaybePoint1D, {"x": "string"}, strict=False)
self.assertTryCastSuccess(MaybePoint1D, {}, strict=False)
self.assertTryCastFailure(MaybePoint1D, {"q": 1}, strict=False)
self.assertTryCastSuccess(MaybePoint1D, {"q": 1}, strict=False) # surprise!

self.assertTryCastSuccess(
TaggedMaybePoint1D, {"x": 1, "name": "one"}, strict=False
Expand Down
5 changes: 4 additions & 1 deletion trycast.py
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,10 @@ def _trycast_inner(tp, value, failure, options):

for (k, v) in value.items(): # type: ignore[attribute-error] # pytype
V = resolved_annotations.get(k, _MISSING)
if V is _MISSING or _trycast_inner(V, v, _FAILURE, options) is _FAILURE:
if (
V is not _MISSING
and _trycast_inner(V, v, _FAILURE, options) is _FAILURE
):
return failure

for k in required_keys:
Expand Down

0 comments on commit 1d7ca63

Please sign in to comment.