Coverage for yamlable/base.py: 75%
88 statements
« prev ^ index » next coverage.py v6.4.1, created at 2022-07-06 08:57 +0000
« prev ^ index » next coverage.py v6.4.1, created at 2022-07-06 08:57 +0000
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>
6from abc import ABCMeta
7from collections import OrderedDict
9from yaml import ScalarNode, SequenceNode, MappingNode
11try:
12 # Python 2 only:
13 from StringIO import StringIO as _StringIO # type: ignore # noqa
15 # create a variant that can serve as a context manager
16 class StringIO(_StringIO):
17 def __enter__(self):
18 return self
20 def __exit__(self, exception_type, exception_value, traceback):
21 self.close()
23except ImportError:
24 # (IOBase is only used in type hints)
25 from io import IOBase, StringIO # type: ignore
27from warnings import warn
29import six
31try: # python 3.5+
32 from typing import Union, TypeVar, Dict, Any, Sequence
34 Y = TypeVar('Y', bound='AbstractYamlObject')
36except ImportError:
37 pass
39try: # python 3.5.4+
40 from typing import Type
41except ImportError:
42 pass # normal for old versions of typing
45class AbstractYamlObject(six.with_metaclass(ABCMeta, object)):
46 """
47 Adds convenient methods load(s)_yaml/dump(s)_yaml to any object, to call pyyaml features directly on the object or
48 on the object class.
50 Also adds the two methods __to_yaml_dict__ / __from_yaml_dict__, that are common to YamlObject2 and YamlAble.
51 Default implementation uses vars(self) and cls(**dct), but subclasses can override.
52 """
54 # def __to_yaml_scalar__(self):
55 # # type: (...) -> Any
56 # """
57 # Implementors should transform the object into a scalar containing all information necessary to decode the
58 # object as a YAML scalar in the future.
59 #
60 # Default implementation raises an error.
61 # :return:
62 # """
63 # raise NotImplementedError("Please override `__to_yaml_scalar__` if you wish to dump instances of `%s`"
64 # " as yaml scalars." % type(self).__name__)
65 #
66 # def __to_yaml_list__(self):
67 # # type: (...) -> Sequence[Any]
68 # """
69 # Implementors should transform the object into a Sequence containing all information necessary to decode the
70 # object as a YAML sequence in the future.
71 #
72 # Default implementation raises an error.
73 # :return:
74 # """
75 # raise NotImplementedError("Please override `__to_yaml_list__` if you wish to dump instances of `%s`"
76 # " as yaml sequences." % type(self).__name__)
78 def __to_yaml_dict__(self):
79 # type: (...) -> Dict[str, Any]
80 """
81 Implementors should transform the object into a dictionary containing all information necessary to decode the
82 object in the future. That dictionary will be serialized as a YAML mapping.
84 Default implementation returns vars(self).
85 :return:
86 """
87 # Legacy compliance with old 'not dunder' method name TODO remove in future version
88 if 'to_yaml_dict' in dir(self):
89 warn(type(self).__name__ + " still uses the legacy method name 'to_yaml_dict'. This name will not be "
90 "supported in future version, please use '__to_yaml_dict__' instead")
91 return self.to_yaml_dict() # type: ignore
93 # Default: return vars(self) (Note: no need to make a copy, pyyaml does not modify it)
94 return vars(self)
96 @classmethod
97 def __from_yaml_scalar__(cls, # type: Type[Y]
98 scalar, # type: Any
99 yaml_tag # type: str
100 ):
101 # type: (...) -> Y
102 """
103 Implementors should transform the given scalar (read from yaml by the pyYaml stack) into an object instance.
104 The yaml tag associated to this object, read in the yaml document, is provided in parameter.
106 Note that for YamlAble and YamlObject2 subclasses, if this method is called the yaml tag will already have
107 been checked so implementors do not have to validate it.
109 Default implementation returns cls(scalar)
111 :param scalar: the yaml scalar
112 :param yaml_tag: the yaml schema id that was used for encoding the object (it has already been checked
113 against is_json_schema_id_supported)
114 :return:
115 """
116 # Default: call constructor with positional arguments
117 return cls(scalar) # type: ignore
119 @classmethod
120 def __from_yaml_list__(cls, # type: Type[Y]
121 seq, # type: Sequence[Any]
122 yaml_tag # type: str
123 ):
124 # type: (...) -> Y
125 """
126 Implementors should transform the given Sequence (read from yaml by the pyYaml stack) into an object instance.
127 The yaml tag associated to this object, read in the yaml document, is provided in parameter.
129 Note that for YamlAble and YamlObject2 subclasses, if this method is called the yaml tag will already have
130 been checked so implementors do not have to validate it.
132 Default implementation returns cls(*seq)
134 :param seq: the yaml sequence
135 :param yaml_tag: the yaml schema id that was used for encoding the object (it has already been checked
136 against is_json_schema_id_supported)
137 :return:
138 """
139 # Default: call constructor with positional arguments
140 return cls(*seq) # type: ignore
142 @classmethod
143 def __from_yaml_dict__(cls, # type: Type[Y]
144 dct, # type: Dict[str, Any]
145 yaml_tag # type: str
146 ):
147 # type: (...) -> Y
148 """
149 Implementors should transform the given dictionary (read from yaml by the pyYaml stack) into an object instance.
150 The yaml tag associated to this object, read in the yaml document, is provided in parameter.
152 Note that for YamlAble and YamlObject2 subclasses, if this method is called the yaml tag will already have
153 been checked so implementors do not have to validate it.
155 Default implementation returns cls(**dct)
157 :param dct:
158 :param yaml_tag: the yaml schema id that was used for encoding the object (it has already been checked
159 against is_json_schema_id_supported)
160 :return:
161 """
162 # Legacy compliance with old 'not dunder' method name TODO remove in future version
163 if 'from_yaml_dict' in dir(cls):
164 warn(cls.__name__ + " still uses the legacy method name 'from_yaml_dict'. This name will not be "
165 "supported in future version, please use '__from_yaml_dict__' instead")
166 return cls.from_yaml_dict(dct, yaml_tag) # type: ignore
168 # Default: call constructor with all keyword arguments
169 return cls(**dct) # type: ignore
171 def dump_yaml(self,
172 file_path_or_stream, # type: Union[str, IOBase, StringIO]
173 safe=True, # type: bool
174 **pyyaml_kwargs # type: Any
175 ):
176 # type: (...) -> None
177 """
178 Dumps this object to a yaml file or stream using pyYaml.
180 :param file_path_or_stream: either a string representing the file path, or a stream where to write
181 :param safe: True (default) uses `yaml.safe_dump`. False uses `yaml.dump`
182 :param pyyaml_kwargs: keyword arguments for the pyYaml dump method
183 :return:
184 """
185 from yaml import safe_dump, dump
186 if isinstance(file_path_or_stream, str): 186 ↛ 187line 186 didn't jump to line 187, because the condition on line 186 was never true
187 with open(file_path_or_stream, mode='w+t') as f:
188 if safe:
189 safe_dump(self, f, **pyyaml_kwargs)
190 else:
191 dump(self, f, **pyyaml_kwargs)
192 else:
193 with file_path_or_stream as f: # type: ignore
194 if safe: 194 ↛ 197line 194 didn't jump to line 197, because the condition on line 194 was never false
195 safe_dump(self, f, **pyyaml_kwargs)
196 else:
197 dump(self, f, **pyyaml_kwargs)
199 def dumps_yaml(self,
200 safe=True, # type: bool
201 **pyyaml_kwargs # type: Any
202 ):
203 # type: (...) -> str
204 """
205 Dumps this object to a yaml string and returns it.
207 :param pyyaml_kwargs: keyword arguments for the pyYaml dump method
208 :param safe: True (default) uses `yaml.safe_dump`. False uses `yaml.dump`
209 :return:
210 """
211 from yaml import safe_dump, dump
212 if safe:
213 return safe_dump(self, **pyyaml_kwargs)
214 else:
215 return dump(self, **pyyaml_kwargs)
217 @classmethod
218 def loads_yaml(cls, # type: Type[Y]
219 yaml_str, # type: str
220 safe=True # type: bool
221 ):
222 # type: (...) -> Y
223 """
224 Utility method to load an instance of this class from the provided yaml string. This methods only returns
225 successfully if the result is an instance of `cls`.
227 :param yaml_str:
228 :param safe: True (default) uses `yaml.safe_load`. False uses `yaml.load`
229 :return:
230 """
231 return cls.load_yaml(StringIO(yaml_str), safe=safe)
233 @classmethod
234 def load_yaml(cls, # type: Type[Y]
235 file_path_or_stream, # type: Union[str, IOBase, StringIO]
236 safe=True # type: bool
237 ):
238 # type: (...) -> Y
239 """
240 Parses the given file path or stream as a yaml document. This methods only returns successfully if the result
241 is an instance of `cls`.
243 :param file_path_or_stream:
244 :param safe: True (default) uses `yaml.safe_load`. False uses `yaml.load`
245 :return:
246 """
247 from yaml import safe_load, load
248 if isinstance(file_path_or_stream, str): 248 ↛ 249line 248 didn't jump to line 249, because the condition on line 248 was never true
249 with open(file_path_or_stream, mode='rt') as f:
250 if safe:
251 res = safe_load(f.read())
252 else:
253 res = load(f.read())
254 else:
255 with file_path_or_stream as f: # type: ignore
256 if safe: 256 ↛ 259line 256 didn't jump to line 259, because the condition on line 256 was never false
257 res = safe_load(f.read())
258 else:
259 res = load(f.read())
261 if isinstance(res, cls): 261 ↛ 264line 261 didn't jump to line 264, because the condition on line 261 was never false
262 return res
263 else:
264 raise TypeError("Decoded object is not an instance of {}, but a {}. Please make sure that the YAML document"
265 " starts with the tag defined in you class' `yaml_tag` field, for example `!my_type`"
266 "".format(cls.__name__, type(res).__name__))
269NONE_IGNORE_CHECKS = None
270# """Tag to be used as yaml tag for abstract classes, to indicate that
271# they are abstract (checks disabled). Not used anymore, kept for legacy reasons"""
274def read_yaml_node_as_dict(loader, node):
275 # type: (...) -> OrderedDict
276 """
277 Utility method to read a yaml node into a dictionary
279 :param loader:
280 :param node:
281 :return:
282 """
283 # loader.flatten_mapping(node)
284 # pairs = loader.construct_pairs(node, deep=True) # 'deep' allows the construction to be complete (inner seq...)
285 pairs = loader.construct_mapping(node, deep=True) # 'deep' allows the construction to be complete (inner seq...)
286 constructor_args = OrderedDict(pairs)
287 return constructor_args
290def read_yaml_node_as_sequence(loader, node):
291 # type: (...) -> Sequence
292 """
293 Utility method to read a yaml node into a sequence
295 :param loader:
296 :param node:
297 :return:
298 """
299 seq = loader.construct_sequence(node, deep=True) # 'deep' allows the construction to be complete (inner seq...)
300 return seq
303def read_yaml_node_as_scalar(loader, node):
304 # type: (...) -> Any
305 """
306 Utility method to read a yaml node into a sequence
308 :param loader:
309 :param node:
310 :return:
311 """
312 value = loader.construct_scalar(node)
313 return value
316def read_yaml_node_as_yamlobject(
317 cls, # type: Type[AbstractYamlObject]
318 loader,
319 node, # type: MappingNode
320 yaml_tag # type: str
321):
322 # type: (...) -> AbstractYamlObject
323 """
324 Default implementation: loads the node as a dictionary and calls __from_yaml_dict__ with this dictionary
326 :param loader:
327 :param node:
328 :return:
329 """
330 if isinstance(node, ScalarNode):
331 constructor_args = read_yaml_node_as_scalar(loader, node)
332 return cls.__from_yaml_scalar__(constructor_args, yaml_tag=yaml_tag) # type: ignore
334 elif isinstance(node, SequenceNode):
335 constructor_args = read_yaml_node_as_sequence(loader, node)
336 return cls.__from_yaml_list__(constructor_args, yaml_tag=yaml_tag) # type: ignore
338 elif isinstance(node, MappingNode): 338 ↛ 343line 338 didn't jump to line 343, because the condition on line 338 was never false
339 constructor_args = read_yaml_node_as_dict(loader, node)
340 return cls.__from_yaml_dict__(constructor_args, yaml_tag=yaml_tag) # type: ignore
342 else:
343 raise TypeError("Unknown type of yaml node: %r. Please report this to `yamlable` project." % type(node))