Skip to content

Commit fc35cdb

Browse files
committed
YamlCodec completed:
- `YamlCodec.decode_yamlable` renamed `decode` and `YamlCodec.encode_yamlable` renamed `encode` - added some checks in `YamlCodec.encode` to help users implement `to_yaml_dict` correctly - fixed bug in `register_with_pyyaml`: the wrong decoding method was registered - added tests and usage documentation This fixes #4
1 parent eb41d9c commit fc35cdb

3 files changed

Lines changed: 195 additions & 10 deletions

File tree

docs/usage.md

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,112 @@
22

33
You have seen in the [main page](./index) a small example to understand the concepts. Below we present other advanced usages.
44

5-
**TODO + explain YamlCodec**
5+
## `YamlCodec`
6+
7+
Sometimes you do not have the possibility to change the classes of the objects that you wish to encode/decode. In this case the solution is to write an independent codec, inheriting from `YamlCodec`. Once again this feature leverages the `multi_constructor` and `multi_representer` concepts available in the `PyYaml` internals, but with `YamlCodec` it becomes a bit easier to do.
8+
9+
Let's assume that the following two classes are given and can not be modified:
10+
11+
```python
12+
class Foo:
13+
def __init__(self, a, b):
14+
self.a = a
15+
self.b = b
16+
17+
def __eq__(self, other):
18+
return vars(self) == vars(other)
19+
20+
class Bar:
21+
def __init__(self, c):
22+
self.c = c
23+
24+
def __eq__(self, other):
25+
return vars(self) == vars(other)
26+
```
27+
28+
Writing a codec is quite simple:
29+
30+
- first inherit from `YamlCodec` and fill `get_yaml_prefix` so that it returns the common prefix that all yaml objects encoded/decoded by this codec will use
31+
- then fill the checkers:
32+
- `get_known_types` to return all object types that can be encoded by this codec
33+
- `is_yaml_tag_supported` to return `True` if a yaml tag (suffix) is supported by this codec for decoding.
34+
- finally fill the encoder/decoder:
35+
- `create_from_yaml_dict` to decode
36+
- `to_yaml_dict` to encode
37+
38+
The example below shows how it can be done:
39+
40+
41+
```python
42+
from yamlable import YamlCodec
43+
from typing import Type, Any, Iterable, Tuple
44+
45+
46+
# the yaml tag suffixes for the two classes
47+
foo_yaml = "yaml.tests.Foo"
48+
bar_yaml = "yaml.tests.Bar"
49+
50+
# 2-way mappings between the types and the yaml tags
51+
types_to_yaml_tags = {Foo: foo_yaml,
52+
Bar: bar_yaml}
53+
yaml_tags_to_types = {foo_yaml: Foo,
54+
bar_yaml: Bar}
55+
56+
class MyCodec(YamlCodec):
57+
@classmethod
58+
def get_yaml_prefix(cls):
59+
return "!mycodec/" # This is our root yaml tag
60+
61+
# ----
62+
63+
@classmethod
64+
def get_known_types(cls) -> Iterable[Type[Any]]:
65+
# return the list of types that we know how to encode
66+
return types_to_yaml_tags.keys()
67+
68+
@classmethod
69+
def is_yaml_tag_supported(cls, yaml_tag_suffix: str) -> bool:
70+
# return True if the given yaml tag suffix is supported
71+
return yaml_tag_suffix in yaml_tags_to_types.keys()
72+
73+
# ----
74+
75+
@classmethod
76+
def create_from_yaml_dict(cls, yaml_tag_suffix: str, dct, **kwargs):
77+
# Create an object corresponding to the given tag, from the decoded dict
78+
typ = yaml_tags_to_types[yaml_tag_suffix]
79+
return typ(**dct)
80+
81+
@classmethod
82+
def to_yaml_dict(cls, obj) -> Tuple[str, Any]:
83+
# Encode the given object and also return the tag that it should have
84+
return types_to_yaml_tags[type(obj)], vars(obj)
85+
```
86+
87+
When you codec has been defined, it needs to be registerd before being usable. You can specify with which `PyYaml` Loaders/Dumpers it should be registered, or use the default (all):
88+
89+
```python
90+
# register the codec
91+
MyCodec.register_with_pyyaml()
92+
```
93+
94+
Finally let's test that the codec works:
95+
96+
```python
97+
from yaml import dump, load
98+
99+
# instantiate
100+
f = Foo(1, 'hello')
101+
fy = "!mycodec/yaml.tests.Foo {a: 1, b: hello}\n"
102+
103+
b = Bar('what?')
104+
by = "!mycodec/yaml.tests.Bar {c: 'what?'}\n"
105+
106+
# dump
107+
assert dump(f) == fy
108+
assert dump(b) == by
109+
110+
# load
111+
assert f == load(fy)
112+
assert b == load(by)
113+
```

yamlable/main.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from abc import abstractmethod, ABC
2-
from typing import TypeVar, Callable, Optional, Iterable, Any, Tuple
2+
from typing import TypeVar, Callable, Optional, Iterable, Any, Tuple, Mapping
3+
34
try:
45
from typing import Type
56
except ImportError:
@@ -339,7 +340,7 @@ def get_yaml_prefix(cls):
339340
"""
340341

341342
@classmethod
342-
def decode_yamlable(cls, loader, yaml_tag_suffix, node, **kwargs):
343+
def decode(cls, loader, yaml_tag_suffix, node, **kwargs):
343344
"""
344345
The method used to decode object instances
345346
@@ -365,12 +366,12 @@ def is_yaml_tag_supported(cls, yaml_tag_suffix: str) -> bool:
365366

366367
@classmethod
367368
@abstractmethod
368-
def create_from_yaml_dict(cls, yaml_tag_suffix: str, constructor_args, **kwargs):
369+
def create_from_yaml_dict(cls, yaml_tag_suffix: str, dct, **kwargs):
369370
"""
370371
Implementing classes should create an object corresponding to the given yaml tag, using the given constructor
371372
arguments.
372373
373-
:param constructor_args:
374+
:param dct:
374375
:param yaml_tag_suffix:
375376
:param kwargs: keyword arguments coming from pyyaml, not sure what you will find here.
376377
:return:
@@ -387,7 +388,7 @@ def get_known_types(cls) -> Iterable['Type[Any]']:
387388
"""
388389

389390
@classmethod
390-
def encode_yamlable(cls, dumper, obj, without_custom_tag: bool = False, **kwargs):
391+
def encode(cls, dumper, obj, without_custom_tag: bool = False, **kwargs):
391392
"""
392393
The method used to encode YamlAble object instances
393394
@@ -400,14 +401,19 @@ def encode_yamlable(cls, dumper, obj, without_custom_tag: bool = False, **kwargs
400401
"""
401402
# Convert objects to a dictionary of their representation
402403
yaml_tag_suffix, obj_as_dict = cls.to_yaml_dict(obj)
404+
if not isinstance(obj_as_dict, Mapping) or not isinstance(yaml_tag_suffix, str):
405+
raise TypeError("`to_yaml_dict` did not return correct results. It shoudl return a tuple of "
406+
"`yaml_tag_suffix, obj_as_dict`")
403407

404408
if without_custom_tag:
405409
# TODO check that it works
406410
return dumper.represent_mapping(None, obj_as_dict, flow_style=None)
407411
else:
408412
# Add the tag information
409-
# TODO make sure that there is a '/'
410-
yaml_tag = cls.get_yaml_prefix() + yaml_tag_suffix
413+
prefix = cls.get_yaml_prefix()
414+
if len(prefix) == 0 or prefix[-1] != '/':
415+
prefix = prefix + '/'
416+
yaml_tag = prefix + yaml_tag_suffix
411417
return dumper.represent_mapping(yaml_tag, obj_as_dict, flow_style=None)
412418

413419
@classmethod
@@ -436,8 +442,8 @@ def register_with_pyyaml(cls, loaders={Loader, SafeLoader}, dumpers={Dumper, Saf
436442
:return:
437443
"""
438444
for loader in loaders:
439-
loader.add_multi_constructor(cls.get_yaml_prefix(), decode_yamlable)
445+
loader.add_multi_constructor(cls.get_yaml_prefix(), cls.decode)
440446

441447
for dumper in dumpers:
442448
for t in cls.get_known_types():
443-
dumper.add_multi_representer(t, cls.encode_yamlable)
449+
dumper.add_multi_representer(t, cls.encode)

yamlable/tests/test_yamlcodec.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from typing import Tuple, Any, Iterable
2+
3+
from yaml import dump, load
4+
5+
from yamlable import YamlCodec
6+
7+
8+
def test_yamlcodec():
9+
""" Tests that custom yaml codec work """
10+
11+
class Foo:
12+
def __init__(self, a, b):
13+
self.a = a
14+
self.b = b
15+
16+
def __eq__(self, other):
17+
return vars(self) == vars(other)
18+
19+
class Bar:
20+
def __init__(self, c):
21+
self.c = c
22+
23+
def __eq__(self, other):
24+
return vars(self) == vars(other)
25+
26+
foo_yaml = "yaml.tests.Foo"
27+
bar_yaml = "yaml.tests.Bar"
28+
types_to_yaml_tags = {Foo: foo_yaml,
29+
Bar: bar_yaml}
30+
yaml_tags_to_types = {foo_yaml: Foo,
31+
bar_yaml: Bar}
32+
33+
class MyCodec(YamlCodec):
34+
@classmethod
35+
def get_yaml_prefix(cls):
36+
return "!mycodec/"
37+
38+
@classmethod
39+
def get_known_types(cls) -> Iterable['Type[Any]']:
40+
return types_to_yaml_tags.keys()
41+
42+
@classmethod
43+
def is_yaml_tag_supported(cls, yaml_tag_suffix: str) -> bool:
44+
return yaml_tag_suffix in yaml_tags_to_types.keys()
45+
46+
@classmethod
47+
def create_from_yaml_dict(cls, yaml_tag_suffix: str, dct, **kwargs):
48+
typ = yaml_tags_to_types[yaml_tag_suffix]
49+
return typ(**dct)
50+
51+
@classmethod
52+
def to_yaml_dict(cls, obj) -> Tuple[str, Any]:
53+
return types_to_yaml_tags[type(obj)], vars(obj)
54+
55+
# register the codec
56+
MyCodec.register_with_pyyaml()
57+
58+
# instantiate
59+
f = Foo(1, 'hello')
60+
fy = "!mycodec/yaml.tests.Foo {a: 1, b: hello}\n"
61+
62+
b = Bar('what?')
63+
by = "!mycodec/yaml.tests.Bar {c: 'what?'}\n"
64+
65+
# dump pyyaml
66+
assert dump(f) == fy
67+
assert dump(b) == by
68+
69+
# load pyyaml
70+
assert f == load(fy)
71+
assert b == load(by)

0 commit comments

Comments
 (0)