Coverage for yamlable/yaml_objects.py: 95%

30 statements  

« 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> 

5 

6from abc import ABCMeta 

7 

8import six 

9 

10try: # python 3.5+ 

11 from typing import TypeVar 

12 

13 YO2 = TypeVar('YO2', bound='YamlObject2') 

14except ImportError: 

15 pass 

16try: # python 3.5.4+ 

17 from typing import Type 

18except ImportError: 

19 pass # normal for old versions of typing 

20 

21from yaml import YAMLObjectMetaclass, YAMLObject, SafeLoader, MappingNode 

22 

23from yamlable.base import AbstractYamlObject, read_yaml_node_as_yamlobject 

24 

25 

26class YAMLObjectMetaclassStrict(YAMLObjectMetaclass): 

27 """ 

28 Improved metaclass for YAMLObject, that raises an error if yaml_tag is not defined 

29 """ 

30 def __init__(cls, # type: Type[YO2] # type: ignore 

31 name, bases, kwds): 

32 

33 # construct as usual 

34 super(YAMLObjectMetaclass, cls).__init__(name, bases, kwds) 

35 

36 # if yaml_tag is provided 

37 if 'yaml_tag' in kwds: 

38 # if cls.yaml_tag != NONE_IGNORE_CHECKS: 

39 if kwds['yaml_tag'] is not None: 

40 cls.yaml_loader.add_constructor(cls.yaml_tag, cls.from_yaml) 

41 cls.yaml_dumper.add_representer(cls, cls.to_yaml) 

42 else: 

43 if 'yaml_tag' in cls.__dict__: 43 ↛ 48line 43 didn't jump to line 48, because the condition on line 43 was never false

44 # this is an explicitly disabled class (yaml_tag=None is set on it), ok 

45 pass 

46 else: 

47 # this class inherits from the yaml_tag=None and does not redefine it, not ok 

48 raise TypeError("`yaml_tag` field is not redefined by class {}, cannot inherit from YAMLObject " 

49 "properly. Note that abstract classes can set the tag explicitly to `None` to skip " 

50 "this check. It won't disable the check for their subclasses".format(cls)) 

51 

52 else: 

53 raise TypeError("`yaml_tag` field is not redefined by class {}, cannot inherit from YAMLObject properly" 

54 "".format(cls)) 

55 

56 

57class ABCYAMLMeta(YAMLObjectMetaclassStrict, ABCMeta): 

58 """The subclass of both YAMLObjectMetaclass and ABCMeta, to be used in YamlObject2""" 

59 pass 

60 

61 

62class YamlObject2(six.with_metaclass(ABCYAMLMeta, AbstractYamlObject, YAMLObject)): 

63 """ 

64 A helper class to register a class as able to dump instances to yaml and to load them back from yaml. 

65 

66 This class relies on the `YAMLObject` class provided in pyyaml, and implements the to_yaml/from_yaml methods by 

67 leveraging the AbstractYamlObject class (__to_yaml_dict__ / __from_yaml_dict__). 

68 

69 It is basically an extension of YAMLObject 

70 - mapping the methods to methods in AbstractYamlObject (for consistency with YamlAble) so that you only have to 

71 provide a mapping to dictionary and do not have to care about pyyaml internals 

72 - and raising an error if the `yaml_tag` class field is not properly set. 

73 

74 You still have to 

75 - define `yaml_tag` either directly or using the @yaml_info() decorator 

76 - optionally override methods from AbstractYamlObject: __to_yaml_dict__ and __from_yaml_dict__ 

77 

78 Note: since this class extends YAMLObject, it relies on metaclass. You might therefore prefer to extend YamlAble 

79 instead. 

80 """ 

81 yaml_loader = SafeLoader # explicitly use SafeLoader by default 

82 # yaml_dumper = Dumper 

83 yaml_tag = None 

84 # yaml_flow_style = ... 

85 

86 @classmethod 

87 def to_yaml(cls, # type: Type[YamlObject2] 

88 dumper, 

89 data # type: AbstractYamlObject 

90 ): 

91 # type: (...) -> MappingNode 

92 """ 

93 Default implementation: relies on AbstractYamlObject API to serialize all public variables 

94 

95 :param dumper: 

96 :param data: 

97 :return: 

98 """ 

99 new_data = data.__to_yaml_dict__() 

100 return dumper.represent_mapping(cls.yaml_tag, new_data, flow_style=cls.yaml_flow_style) 

101 

102 @classmethod 

103 def from_yaml(cls, # type: Type[YamlObject2] 

104 loader, 

105 node # type: MappingNode 

106 ): 

107 # type: (...) -> YamlObject2 

108 """ 

109 Default implementation: relies on AbstractYamlObject API to load the node as a dictionary/sequence/scalar 

110 

111 :param loader: 

112 :param node: 

113 :return: 

114 """ 

115 return read_yaml_node_as_yamlobject(cls=cls, loader=loader, node=node, yaml_tag=cls.yaml_tag)