Coder Social home page Coder Social logo

kodemore / chili Goto Github PK

View Code? Open in Web Editor NEW
64.0 4.0 6.0 253 KB

Object serialization/deserialization tools for python.

License: MIT License

Python 99.22% Makefile 0.78%
python dataclasses hydration serialization deserialization mapping json

chili's People

Contributors

dawidkraczkowskikaizen avatar dkraczkowski avatar fairlight8 avatar gizioo avatar stortiz-lifeworks avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

chili's Issues

Nice! LICENSE file?

Hey, this is great!

I see there is an MIT license batch. Don't suppose... could you add also an MIT LICENSE file, so that it is really obviously mit, and so that it shows as MIT in the right hand side of the github repo front page?

typing.Optional seems to be required for optional fields (while README implies otherwise)

I actually managed to work around this issue, but just to notify you there seems to be some incorrect parts in the README docs. And to share some suggestions. I could make a pull request for both but I'm not sure what your thoughts are about it.

Edit: I just noticed it's possible to do what I want with dataclasses and chili.decoder import ClassDecoder (or just chili.decode() directly), as is shown in https://github.com/kodemore/chili/blob/main/tests/usecases/dataclasses_test.py#L140-L147
Which is perfect for my use case.

Below my original message, which could still be interesting:

When I try to decode a dict to a class while omitting a field that does have a default defined, I get a chili.error.DecoderError@missing_property

For example when I run the example straight from the README:

from typing import List
from chili import Decoder, decodable

@decodable
class Book:
    name: str
    author: str
    isbn: str = "1234567890"
    tags: List[str] = []

book_data = {"name": "The Hobbit", "author": "J.R.R. Tolkien"}
decoder = Decoder[Book]()

book = decoder.decode(book_data)

I get: chili.error.DecoderError@missing_property: key=isbn

I noticed in this unit test the optional fields are typed with Optional[SomeType], which indeed fixes the issue.
Although this seems to make sense at first given the name "Optional", it does make it possible to set the field to None, which might not always be as intended. A custom decoder could be used to parse 'None' to the default value, although it would be a bit more convenient if this would be done automatically by specifying a default while leaving out Optional.

Besides, it would be nice to be able to use the more recent SomeType | None syntax as an alternative to Optional[SomeType].

I also noticed that @encodable & @decodable seem to be redundant (or at least in the cases from the examples), making it possible to keep most parts of the code completely unaware of the chili lib, which I like a lot ๐Ÿ‘

Custom Type Encoders and 'composed types'

Hello again!

I am again finding myself in a dead end, and I don't know if this behavior is intended or not.

This test code comes from tests/usecases/custom_type_test.py:

from chili import encodable, Encoder, TypeEncoder

class ISBN(str):
    def __init__(self, value: str):
        self.value = value

@encodable
class Book:
    name: str
    isbn: ISBN

    def __init__(self, name: str, isbn: ISBN):
        self.name = name
        self.isbn = isbn


class ISBNEncoder(TypeEncoder):
    def encode(self, isbn: ISBN) -> str:
        return isbn.value

encoder = Encoder[Book]({ISBN: ISBNEncoder()})
book = Book("The Hobbit", ISBN("1234567890"))


result = encoder.encode(book)

print(result)

Thta code works flawlessly. However, if you change ISBN for list[ISBN] or any other thing, like a dictionary of ISBNS, it stops working:

from chili import encodable, Encoder, TypeEncoder

class ISBN(str):
    def __init__(self, value: str):
        self.value = value

@encodable
class Book:
    name: str
    isbn: list[ISBN]

    def __init__(self, name: str, isbn: ISBN):
        self.name = name
        self.isbn = isbn


class ISBNEncoder(TypeEncoder):
    def encode(self, isbn: ISBN) -> str:
        return isbn.value

encoder = Encoder[Book]({ISBN: ISBNEncoder()})
book = Book("The Hobbit", [ISBN("1234567890"), ISBN("1234567890456456")])

result = encoder.encode(book)

print(result)

I assume that the type encoder looks for List[ISBN] and does not find it. But if you have a ISBNEncoder, it should already work?

Default values just don't work

This example, taken literally from the README, fails on Python 3.11.8:

from typing import List
from chili import Decoder, decodable

@decodable
class Book:
    name: str
    author: str
    isbn: str = "1234567890"
    tags: List[str] = []

book_data = {"name": "The Hobbit", "author": "J.R.R. Tolkien"}
decoder = Decoder[Book]()

book = decoder.decode(book_data)

assert book.tags == []
assert book.isbn == "1234567890"

with

  File "<stdin>", line 1, in <module>
  File "[...]]/python3.11/site-packages/chili/decoder.py", line 599, in decode
    raise DecoderError.missing_property(key=key)
chili.error.DecoderError@missing_property: key=isbn

Datetime hydration loses millisecond precision

Hydrating a valid ISO-8601 datetime string loses precision in the milliseconds.

from dataclasses import dataclass
from datetime import datetime

from chili import init_dataclass


@dataclass
class MyDatetime:
    t: datetime


date = datetime.utcnow()
date_str = date.isoformat()  # produces valid ISO-8601 string

my_dataclass = MyDatetime(date)
hydrated_dataclass = init_dataclass({"t": date_str}, MyDatetime)

print(my_dataclass == hydrated_dataclass)
print(my_dataclass)
print(hydrated_dataclass)
print(date_str)

As can be seen in the example above, even if no error is produced for the milliseconds in the date string, these are ignored when hydrating.

[BUG / IMPROVEMENT] Child class does not receive new schema if parent is already decodable/encodable/serializable

Hello again!

I am using chili in an university project I am currently coding, and I just discovered a problem. We have two classes, parent and child. Both of them are serializable. The base class is a basic session, with basic functionality, and the other one inherits the base class, with extra tools.

At some point I realized that chili was not encoding/decoding the objects of the child class properly. And, after some research, I realized that the schema was identical in both classes, parent and child.

Which is the source of the issue, then? When marking classes as encodable / decodable / serializable, the decorator checks this:

def _decorate(cls) -> Type[C]:
        if not hasattr(cls, _PROPERTIES):
            setattr(cls, _PROPERTIES, create_schema(cls))

What is happening when both classes are marked with the encodable / decodable / serializable decorator? The child class inherits the schema, and the decorator does not update the schema.

I have done some tests, and removing the line if not hasattr(cls, _PROPERTIES): solves the issue. I don't know if it breaks anything else. If you agree, I will create a pull request with this solution.

Thanks in advance.

dataclass vs decorated class instantiation differences

Hello,

I stumbled across the following unexpected behaviour today, wanted to raise it for visibility first.

Let's say I have the following dataclass:

@dataclasses.dataclass
class Contract:
    name: str
    start: int
    duration: int
    price: int

If I try instantiating this using an incomplete input, chili will do it just fine, it just won't have the duration field on it.

invalid_contract = {
    'name': 'Contract 1',
    'start': '0',
    'price': 10
}
contract = decode(invalid_contract, Contract)

If I change the class definition to provide a constructor, and I use the same initialisation as above, chili will raise an DecoderError.invalid_type exception, which is what I was expecting.

@chili.encodable
class Contract:
    name: str
    start: int
    duration: int
    price: int

    def __init__(self, name: str, start: int, duration: int, price:int):
        self.name = name
        self.start = start
        self.duration = duration
        self.price = price

I seem to recall chili handling this consistently with v1, but I could be wrong, so I wanted to ask for some clarification as to what the desired behaviour is.

doc starts with complicated stuff, before talking about the easy approach

chili is easy to use. Just do:

chili.encode(some_object)

or

chili.json_encode(some_object)

... but the doc currently talks about creating Encoder objects, annotating classes, etc, which is super complicated, and totally unnecessary for basic usage.

Strongly suggest starting with the easy way to use chili. Then afer tha, you can have a section "Advanced" to talk about more fancythings.

Can not init datalclass if @dataclass(frozen=True)

Given

@dataclass(frozen=True)
class Event:
    id: str
    occurred_on: datetime

@dataclass(frozen=True)
class SomethingImportantHappened(Event):
    message: str

When

event = init_dataclass({'id': 'dda5469de61b4c36a65dbdaa3850e8ab', 'occurred_on': '2022-11-24T15:09:12.348012', 'message': 'test message'}, SomethingImportantHappened)

Then

../../src/outbox_pattern/outbox_processor.py:42: in process_outbox_message
    event = init_dataclass(json.loads(message.data), event_cls)
/Users/szymonmiks/Library/Caches/pypoetry/virtualenvs/examples-hALDfU1m-py3.9/lib/python3.9/site-packages/chili/dataclasses.py:11: in init_dataclass
    return hydrate(data, dataclass, strict=False, mapping=mapping)
/Users/szymonmiks/Library/Caches/pypoetry/virtualenvs/examples-hALDfU1m-py3.9/lib/python3.9/site-packages/chili/hydration.py:653: in hydrate
    return strategy.hydrate(data)
/Users/szymonmiks/Library/Caches/pypoetry/virtualenvs/examples-hALDfU1m-py3.9/lib/python3.9/site-packages/chili/hydration.py:86: in hydrate
    setter(instance, value)
/Users/szymonmiks/Library/Caches/pypoetry/virtualenvs/examples-hALDfU1m-py3.9/lib/python3.9/site-packages/chili/hydration.py:464: in set_dataclass_property
    raise error
/Users/szymonmiks/Library/Caches/pypoetry/virtualenvs/examples-hALDfU1m-py3.9/lib/python3.9/site-packages/chili/hydration.py:460: in set_dataclass_property
    setattr(obj, property_name, setter(attributes[property_name]))
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <[AttributeError("'SomethingImportantHappened' object has no attribute 'id'") raised in repr()] SomethingImportantHappened object at 0x10e7e04c0>
name = 'id', value = 'dda5469de61b4c36a65dbdaa3850e8ab'

>   ???
E   dataclasses.FrozenInstanceError: cannot assign to field 'id'

<string>:4: FrozenInstanceError

Encoder.encode does not work like encode() when using custom types. Bug or intended?

Hello!

I found that the encoding behavior when using custom type encoders and decoders is different between Encoder[Class].encode(obj) and encode(obj, Class, ...). Here's the steps to reproduce it:

Steps to reproduce it

from chili import Encoder, TypeEncoder, encode, encodable
from typing import Any

class ExoticClass:
    name: str
    size: int

    def __init__(self, name, size):
        self.name = name
        self.size = size


class ExoticEncoder(TypeEncoder):
    def encode(self, value: ExoticClass):
        return {"name": f"DECODED:{value.name}", "size": -value.size}


type_encoders = {ExoticClass: ExoticEncoder()}

exotic_object = ExoticClass("foo", 5)

print(encode(exotic_object, ExoticClass, type_encoders))
print(Encoder[ExoticClass](encoders=type_encoders).encode(exotic_object))

The output of the first print is: {'name': 'DECODED:foo', 'size': -5}, as expected.
But the output of the second line is the exception chili.error.EncoderError@invalid_type: invalid_type

I think this is because I haven't decorated my class. If I add the decorator like:

@encodable
class ExoticClass:
    ...

Then, the outputs are {'name': 'DECODED:foo', 'size': -5} and {'name': 'foo', 'size': 5}.

The conclusion is that the second line always ignores the custom type encoders.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.