Skip to content

Commit 1d0cae0

Browse files
author
Sylvain MARIE
committed
Fixed documentation build. Now using mkdocs-gallery for documentation examples. Fixed #14
1 parent 1787dc4 commit 1d0cae0

7 files changed

Lines changed: 449 additions & 263 deletions

File tree

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
# Authors: Sylvain MARIE <sylvain.marie@se.com>
2+
# + All contributors to <https://github.com/smarie/python-yamlable>
3+
#
4+
# License: 3-clause BSD, <https://github.com/smarie/python-yamlable/blob/master/LICENSE>
5+
"""
6+
Yaml-able classes
7+
=================
8+
9+
Let's make a class yaml-aware so that instances can be loaded from YAML and dumped to
10+
YAML.
11+
12+
1. Basics
13+
---------
14+
15+
To make a class yaml-able, we have to
16+
17+
- inherit from `YamlAble`
18+
- decorate it with the `@yaml_info` annotation to declare the associated yaml tag
19+
"""
20+
21+
from yamlable import yaml_info, YamlAble
22+
23+
@yaml_info(yaml_tag_ns='com.yamlable.example')
24+
class Foo(YamlAble):
25+
26+
def __init__(self, a, b="hey"):
27+
""" Constructor """
28+
self.a = a
29+
self.b = b
30+
31+
def __repr__(self):
32+
""" String representation for prints """
33+
return f"{type(self).__name__} - {dict(a=self.a, b=self.b)}"
34+
35+
# %%
36+
# That's it! Let's check that our class is correct and allows us to create instances:
37+
38+
f = Foo(1, 'hello')
39+
f
40+
41+
42+
# %%
43+
# Now let's dump it to a YAML document using `pyyaml`:
44+
45+
import yaml
46+
47+
print(yaml.dump(f))
48+
49+
# %%
50+
# we can also load an instance from a document:
51+
52+
f2 = yaml.safe_load("""
53+
!yamlable/com.yamlable.example.Foo
54+
a: 0
55+
b: hey
56+
""")
57+
print(f2)
58+
59+
# %%
60+
# For more general cases where your object is embedded in a more complex structure for example, it will work as expected:
61+
62+
d = {'foo': f, 'foo2': 12}
63+
print(yaml.dump(d))
64+
65+
# %%
66+
# In addition, the object directly offers the `dump_yaml` (dumping to file) / `dumps_yaml` (dumping to string)
67+
# convenience methods, and the class directly offers the `load_yaml` (load from file) / `loads_yaml` (load from string)
68+
# convenience methods.
69+
#
70+
# See [PyYaml documentation](http://pyyaml.org/wiki/PyYAMLDocumentation) for the various formatting arguments that you
71+
# can use, they are the same than in the `yaml.dump` method. For example:
72+
73+
74+
print(f.dumps_yaml(default_flow_style=False))
75+
76+
# %%
77+
# 2. Customization
78+
# ----------------
79+
#
80+
# ### a. dumper/loader
81+
#
82+
# As could be seen above, `YamlAble` comes with a default implementation of the yaml formatting and parsing
83+
# associated with the classes. This is controlled by two methods that you may wish to override:
84+
#
85+
# - `__to_yaml_dict__` is an instance method that controls what to dump. By default it returns `vars(self)`
86+
# - `__from_yaml_dict__` is a class method that controls the loading process. By default it returns `cls(**dct)`.
87+
#
88+
# You may wish to override one, or both of these methods.
89+
# For example if you do not wish to dump all of the object attributes:
90+
91+
@yaml_info(yaml_tag_ns='com.yamlable.example')
92+
class CustomFoo(YamlAble):
93+
94+
def __init__(self, a, b):
95+
""" Constructor """
96+
self.a = a
97+
self.b = b
98+
self.irrelevant = 37
99+
100+
def __repr__(self):
101+
""" String representation for prints """
102+
return f"{type(self).__name__} - {dict(a=self.a, b=self.b, irrelevant=self.irrelevant)}"
103+
104+
def __to_yaml_dict__(self):
105+
# Do not dump 'irrelevant'
106+
return {'a': self.a, 'b': self.b}
107+
108+
@classmethod
109+
def __from_yaml_dict__(cls, dct, yaml_tag):
110+
# Accept a default value for b
111+
return cls(dct['a'], dct.get('b', "default"))
112+
113+
# %%
114+
# Let's test it: loading...
115+
116+
f3 = yaml.safe_load("""
117+
!yamlable/com.yamlable.example.CustomFoo
118+
a: 0
119+
""")
120+
print(f3)
121+
122+
# %%
123+
# ...and dumping again
124+
125+
print(f3.dumps_yaml())
126+
127+
# %%
128+
# ### b. YAML tag
129+
#
130+
# You probably noticed in the above examples that the dumped YAML document contains a tag such as
131+
# `!yamlable/com.yamlable.example.CustomFoo`.
132+
#
133+
# When you dump a `YamlAble` object `o` to yaml, the corresponding tag is `f!yamlable/{o.__yaml_tag_suffix__}`.
134+
# Note that you can override the class attribute, or even the instance attribute:
135+
136+
f3.__yaml_tag_suffix__ = "wow_you_changed_me"
137+
print(f3.dumps_yaml())
138+
139+
# %%
140+
# The `@yaml_info` decorator is just a convenient way to fill the `__yaml_tag_suffix__` attribute on a class, nothing
141+
# more.
142+
# You can either provide a full yaml tag suffix:
143+
144+
@yaml_info("com.example.WowObject")
145+
class MyFoo(YamlAble):
146+
pass
147+
148+
print(MyFoo.__yaml_tag_suffix__)
149+
print(MyFoo().dumps_yaml())
150+
151+
# %%
152+
# Notice that this is great for retrocompatiblity: you can change your class name or module without changing the
153+
# YAML serialization.
154+
#
155+
# Otherwise you can simply provide a namespace, that will be appended with `.{cls.__name__}`:
156+
157+
@yaml_info(yaml_tag_ns="com.example")
158+
class MyFoo(YamlAble):
159+
pass
160+
161+
print(MyFoo.__yaml_tag_suffix__)
162+
print(MyFoo().dumps_yaml())
163+
164+
# %%
165+
# In that case, you'll have to be sure that your class name does not change over time.
166+
# If you do not like the `!yamlable` prefix, you should use the
167+
# [alternate `YamlObject2` class](#3-alternate-way-yamlobject2) - in that case the decorator should not be used.
168+
#
169+
# 3. Alternate way: `YamlObject2`
170+
# -------------------------------
171+
# If you absolutely wish to use PyYaml's `YamlObject` for some reason, you can use `YamlObject2` as an alternative to
172+
# `YamlAble`. But it comes with the metaclass, like `YamlObject`.
173+
#
174+
# Nevertheless, the way to work is very similar: simply override the optional methods. However you must specify the
175+
# **entire** yaml tag directly using the `yaml_tag` class variable. The `@yaml_info` decorator
176+
# can not be used with classes subclassing `YamlObject2`.
177+
178+
from yamlable import YamlObject2
179+
180+
class CustomFoo2(YamlObject2):
181+
yaml_tag = '!foo'
182+
183+
def __init__(self, a, b):
184+
""" Constructor """
185+
self.a = a
186+
self.b = b
187+
self.irrelevant = 37
188+
189+
def __repr__(self):
190+
""" String representation for prints """
191+
return f"{type(self).__name__} - {dict(a=self.a, b=self.b, irrelevant=self.irrelevant)}"
192+
193+
def __to_yaml_dict__(self):
194+
# Do not dump 'irrelevant'
195+
return {'a': self.a, 'b': self.b}
196+
197+
@classmethod
198+
def __from_yaml_dict__(cls, dct, yaml_tag):
199+
# Accept a default value for b
200+
return cls(dct['a'], dct.get('b', "default"))
201+
202+
# instantiate
203+
f = CustomFoo2(1, 'hello')
204+
205+
# dump to yaml
206+
o = yaml.dump(f)
207+
print(o)
208+
209+
print(yaml.safe_load(o))
210+
211+
212+
# %%
213+
# 4. Support for sequences and scalars
214+
# ------------------------------------
215+
#
216+
# Objects can also be loaded from YAML sequences:
217+
218+
yaml.safe_load("""
219+
!yamlable/com.yamlable.example.Foo
220+
- 0
221+
- hey
222+
""")
223+
224+
# %%
225+
# The default implementation of `__from_yaml_list__` (that you may wish to override in your subclass), is to call
226+
# the constructor with the sequence contents as positional arguments.
227+
#
228+
# The same also works for scalars:
229+
230+
yaml.safe_load("""
231+
!yamlable/com.yamlable.example.Foo 0
232+
""")
233+
234+
# %%
235+
# The default implementation of `__from_yaml_scalar__` (that you may wish to override in your subclass), is to call
236+
# the constructor with the scalar as first positional argument.
237+
#
238+
# !!! warning "Scalars are not resolved"
239+
# As can be seen in the above example, scalars are not auto-resolved when constructing an object from a scalar. So an
240+
# integer `0` is actually received as a string `"0"` by `from_yaml_scalar`.

0 commit comments

Comments
 (0)