Skip to content

Commit

Permalink
marshall: allow implicit cast of int to float and None to any other type
Browse files Browse the repository at this point in the history
should resolve #54
  • Loading branch information
karlicoss committed Nov 8, 2023
1 parent 2abd77b commit a294b26
Show file tree
Hide file tree
Showing 2 changed files with 41 additions and 10 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ Cachew gives the best of two worlds and makes it both **easy and efficient**. Th

# How it works

- first your objects get [converted](src/cachew/marshall/cachew.py#L34) into a simpler JSON-like representation
- first your objects get [converted](src/cachew/marshall/cachew.py#L35) into a simpler JSON-like representation
- after that, they are mapped into byte blobs via [`orjson`](https://github.com/ijl/orjson).

When the function is called, cachew [computes the hash of your function's arguments ](src/cachew/__init__.py:#L589)
Expand Down
49 changes: 40 additions & 9 deletions src/cachew/marshall/cachew.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from collections import abc
from dataclasses import dataclass, is_dataclass
from datetime import date, datetime, timezone
from numbers import Real
import sys
import types
from typing import (
Expand Down Expand Up @@ -119,6 +120,10 @@ class SUnion(Schema):
args: tuple[tuple[int, Schema], ...]

def dump(self, obj):
if obj is None:
# if it's a None, then doesn't really matter how to serialize and deserialize it
return (0, None)

# TODO could do a bit of magic here and remember the last index that worked?
# that way if some objects dominate the Union, the first isinstance would always work
for tidx, a in self.args:
Expand All @@ -134,13 +139,17 @@ def dump(self, obj):
# '__value__': jj,
# }
else:
assert False, "shouldn't happen!"
assert False, f"shouldn't happen: {self.args} {obj}"

def load(self, dct):
# tidx = d['__union_index__']
# s = self.args[tidx]
# return s.load(d['__value__'])
tidx, val = dct
if val is None:
# counterpart for None handling in .dump method
return None

_, s = self.args[tidx]
return s.load(val)

Expand Down Expand Up @@ -262,20 +271,25 @@ def load(self, dct: str):


PRIMITIVES = {
int,
str,
type(None),
float,
bool,
# int and float are handled a bit differently to allow implicit casts
# isinstance(.., Real) works both for int and for float
# Real can't be serialized back, but if you look in SPrimitive, it leaves the values intact anyway
# since the actual serialization of primitives is handled by orjson
int: Real,
float: Real,
str: str,
type(None): type(None),
bool: bool,
# if type is Any, there isn't much we can do to dump it -- just dump into json and rely on the best
# so in this sense it works exacly like primitives
Any,
Any: Any,
}


def build_schema(Type) -> Schema:
if Type in PRIMITIVES:
return SPrimitive(type=Type)
ptype = PRIMITIVES.get(Type)
if ptype is not None:
return SPrimitive(type=ptype)

origin = get_origin(Type)

Expand Down Expand Up @@ -403,11 +417,28 @@ def test_serialize_and_deserialize() -> None:
helper(None, type(None))
# TODO emit other value as none type? not sure what should happen

# implicit casts, simple version
helper(None, int)
helper(None, str)
helper(1, float)

# unions
helper(1, Union[str, int])
if sys.version_info[:2] >= (3, 10):
helper('aaa', str | int)

# implicit casts, inside other types
# technically not type safe, but might happen in practice
# doesn't matter how to deserialize None anyway so let's allow this
helper(None, Union[str, int])

# even though 1 is not isinstance(float), often it ends up as float in data
# see https://github.com/karlicoss/cachew/issues/54
helper(1, Union[float, str])
helper(2, Union[float, int])
helper(2.0, Union[float, int])
helper((1, 2), Tuple[int, float])

# optionals
helper('aaa', Optional[str])
helper('aaa', Union[str, None])
Expand Down

0 comments on commit a294b26

Please sign in to comment.