Skip to content

Commit

Permalink
Attrs compatibility example + doc improvement
Browse files Browse the repository at this point in the history
  • Loading branch information
wyfo committed Nov 6, 2020
1 parent 6dbd785 commit 681c1df
Show file tree
Hide file tree
Showing 8 changed files with 83 additions and 6 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ This library fulfill the following goals:

No known alternative achieves that.

(Actually, *Apischema* is even adaptable enough to enable support of "rival" libraries in a few dozens of line of code)
(Actually, *Apischema* is even adaptable enough to enable support of competitor libraries in a few dozens of line of code)

## Example

Expand Down
2 changes: 1 addition & 1 deletion docs/benchmark.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ cerberus | `1.3.2` | 40.3x slower | 2043.3μs

Package | Version | Relative Performance | Mean serialization time
--- | --- | --- | ---
apischema | `0.1` | | 24.8μs
apischema | `0.11` | | 24.8μs
pydantic | `1.7.2` | 2.3x slower | 57.5μs

Benchmarks were run with Python 3.8 (*CPython*) and the package versions listed above installed via *pypi* on *macOs* 10.15.7
Expand Down
4 changes: 2 additions & 2 deletions docs/conversions.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Actually, *Apischema* uses internally its own feature to support standard librar

ORM support can easily be achieved with this feature (see [SQLAlchemy example](examples/sqlalchemy.md)).

In fact, you can even add support of other "rival" libraries like *Pydantic* (see [*Pydantic* compatibility example](examples/pydantic_compatibility.md))
In fact, you can even add support of competitor libraries like *Pydantic* (see [*Pydantic* compatibility example](examples/pydantic_compatibility.md))

## Principle - *Apischema* conversions

Expand Down Expand Up @@ -49,7 +49,7 @@ This is not possible to overwrite this way deserializers (because they stack), b

All serializers are naturally inherited. In fact, with a conversion function `(Source) -> Target`, you can always pass a subtype of `Source` and get a `Target` in return.

Moreover, when serializer is a method (and no `param` is passed to `serializer`, overriding this method in a subclass will override the inherited serializer.
Moreover, when serializer is a method, overriding this method in a subclass will override the inherited serializer.

```python
{!serializer_inheritance.py!}
Expand Down
2 changes: 1 addition & 1 deletion docs/data_model.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ For `Enum`, this is the value and not the attribute name that is serialized

: Kind of discount dataclass without field customization

- `Any`
- `typing.Any`

: Untouched by deserialization

Expand Down
5 changes: 5 additions & 0 deletions docs/examples/attrs_compatibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Attrs compatibility

```python
{!examples/attrs_compatibility.py!}
```
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ This library fulfill the following goals:
No known alternative achieves that.

!!! note
Actually, *Apischema* is even adaptable enough to enable support of "rival" libraries in a few dozens of line of code (see [conversions section](conversions.md))
Actually, *Apischema* is even adaptable enough to enable support of competitor libraries in a few dozens of line of code (see [conversions section](conversions.md))

## Example

Expand Down
71 changes: 71 additions & 0 deletions examples/examples/attrs_compatibility.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from dataclasses import field, fields, make_dataclass
from functools import lru_cache
from typing import Optional

import attr

from apischema import deserialize, serialize, settings
from apischema.conversions import Conversions, Deserialization, Serialization


@lru_cache()
def attrs_to_dataclass(cls: type) -> type:
assert hasattr(cls, "__attrs_attrs__")
fields_without_default = [
(a.name, a.type) for a in cls.__attrs_attrs__ if a.default == attr.NOTHING
]
fields_with_default = [
(a.name, a.type, field(default=a.default))
for a in cls.__attrs_attrs__
if a.default != attr.NOTHING
]
return make_dataclass(cls.__name__, fields_without_default + fields_with_default)


prev_deserialization = settings.deserialization()
prev_serialization = settings.serialization()


@settings.deserialization
def deserialization(
cls: type, conversions: Optional[Conversions]
) -> Optional[Deserialization]:
result = prev_deserialization(cls, conversions)
if result is not None:
return result
elif hasattr(cls, "__attrs_attrs__"):
source = attrs_to_dataclass(cls)

def converter(source_obj):
return cls(**{f.name: getattr(source_obj, f.name) for f in fields(source)})

return {source: (converter, None)}
else:
return None


@settings.serialization
def serialization(
cls: type, conversions: Optional[Conversions]
) -> Optional[Serialization]:
result = prev_serialization(cls, conversions)
if result is not None:
return result
elif hasattr(cls, "__attrs_attrs__"):
target = attrs_to_dataclass(cls)

def converter(obj):
return target(**{a.name: getattr(obj, a.name) for a in cls.__attrs_attrs__})

return target, (converter, None)
else:
return None


@attr.s
class Foo:
bar: int = attr.ib()


assert deserialize(Foo, {"bar": 0}) == Foo(0)
assert serialize(Foo(0)) == {"bar": 0}
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ nav:
- OpenRPC: examples/open_rpc.md
- SQLAlchemy support: examples/sqlalchemy.md
- examples/pydantic_compatibility.md
- examples/attrs_compatibility.md
- examples/recoverable_fields.md
- benchmark.md
- Releases: https://github.com/wyfo/apischema/releases
Expand Down

0 comments on commit 681c1df

Please sign in to comment.