Coverage for configzen/formats/yaml.py: 57%

45 statements  

« prev     ^ index     » next       coverage.py v7.5.1, created at 2024-05-15 02:42 +0000

1"""`configzen.formats.yaml`: The YAML data format.""" 

2 

3from __future__ import annotations 1abcdefgh

4 

5from typing import TYPE_CHECKING, ClassVar, Literal 1abcdefgh

6 

7from ruamel.yaml import YAML 1abcdefgh

8from ruamel.yaml.comments import CommentedMap 1abcdefgh

9from runtime_generics import runtime_generic 1abcdefgh

10 

11from configzen.data import DataFormatOptions, TextDataFormat 1abcdefgh

12 

13if TYPE_CHECKING: 1abcdefgh

14 from typing import IO 

15 

16 from typing_extensions import TypeAlias, Unpack 

17 

18 from configzen.data import Data 

19 

20__all__ = ( 1abcdefgh

21 "YAMLDataFormat", 

22 "YAMLOptions", 

23) 

24 

25 

26YAMLVersion: TypeAlias = "list[int] | str | tuple[int, int]" 1abcdefgh

27YAMLTyp: TypeAlias = Literal["rt", "rtsc", "safe", "unsafe", "base"] 1abcdefgh

28 

29 

30class YAMLOptions(DataFormatOptions, total=False): 1abcdefgh

31 """ 

32 Prototype of the allowed options for the YAML data format. 

33 

34 For more information, see the documentation of the `ruamel.yaml.YAML` class. 

35 """ 

36 

37 typ: YAMLTyp | list[YAMLTyp] | None 1abcdefgh

38 pure: bool 1abcdefgh

39 plug_ins: list[str] 1abcdefgh

40 

41 classes: list[type] 1abcdefgh

42 """List of classes to automatically call YAML.register_class() on.""" 

43 

44 version: str | tuple[int | str, ...] 1abcdefgh

45 """The YAML version to use.""" 

46 

47 indent: int 1abcdefgh

48 """Indentation width.""" 

49 

50 block_seq_indent: int 1abcdefgh

51 """Indentation for nested block sequences.""" 1abcdefgh

52 

53 

54@runtime_generic 1abcdefgh

55class YAMLDataFormat(TextDataFormat[YAMLOptions]): 1abcdefgh

56 """The YAML data format.""" 

57 

58 option_name: ClassVar[str] = "yaml" 1abcdefgh

59 

60 # Subclass and override for global effect. 

61 yaml: YAML = YAML() 1abcdefgh

62 

63 default_extension: ClassVar[str] = "yml" 1abcdefgh

64 file_extensions: ClassVar[set[str]] = {"yaml"} 1abcdefgh

65 

66 def configure(self, **options: Unpack[YAMLOptions]) -> None: 1abcdefgh

67 """For the documentation of the options, see the YAMLOptions class.""" 

68 yaml_classes = options.pop("classes", None) or [] 

69 

70 old_yaml = self.yaml 

71 yaml_version = options.pop("version", None) or old_yaml.version 

72 yaml_indent = options.pop("indent", None) or old_yaml.old_indent 

73 yaml_block_seq_indent = ( 

74 options.pop("block_seq_indent", None) or old_yaml.block_seq_indent 

75 ) 

76 

77 yaml = YAML(**options) # type: ignore[arg-type,misc] 

78 yaml.version = yaml_version # type: ignore[assignment] 

79 yaml.indent = yaml_indent 

80 yaml.block_seq_indent = yaml_block_seq_indent 

81 

82 for cls in yaml_classes: 

83 yaml.register_class(cls) 

84 

85 self.yaml = yaml 

86 

87 def load(self, stream: IO[str]) -> Data: 1abcdefgh

88 """ 

89 Load the data from a stream. 

90 

91 Return a mutable mapping representing the loaded data 

92 which is mutation-sensitive (for round-trip processing). 

93 

94 Every configuration source transforms the input data into a stream 

95 to be processed by the data format, because most data format libraries 

96 operate on streams. 

97 

98 This method is called by the configuration model. 

99 """ 

100 data = self.yaml.load(stream) or CommentedMap() 

101 if not isinstance(data, dict): 

102 msg = f"Expected a dict, but got {type(data).__name__}." 

103 raise TypeError(msg) 

104 return data 

105 

106 def dump(self, data: Data, stream: IO[str]) -> None: 1abcdefgh

107 """ 

108 Load the data from a stream. 

109 

110 Every configuration source transforms the input data into a stream 

111 to be processed by the data format, because most data format libraries 

112 operate on streams. 

113 

114 This method is called by the configuration model. 

115 """ 

116 self.yaml.dump(data, stream)