Coverage for gws-app/gws/lib/xmlx/serializer.py: 92%

119 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-17 01:37 +0200

1from typing import Optional 

2import gws 

3from . import namespace, error 

4 

5 

6class _SerializerState: 

7 extra_namespaces: list[gws.XmlNamespace] 

8 

9 compact_whitespace = False 

10 remove_namespaces = False 

11 fold_tags = False 

12 with_namespace_declarations = False 

13 with_schema_locations = False 

14 with_xml_declaration = False 

15 

16 buf: list[str] 

17 defaultNamespace: Optional[gws.XmlNamespace] 

18 

19 

20def _state(kwargs): 

21 ser = _SerializerState() 

22 

23 ser.extra_namespaces = kwargs.get('extra_namespaces', []) 

24 

25 ser.compact_whitespace = kwargs.get('compact_whitespace', False) 

26 ser.fold_tags = kwargs.get('fold_tags', False) 

27 ser.remove_namespaces = kwargs.get('remove_namespaces', False) 

28 ser.with_namespace_declarations = kwargs.get('with_namespace_declarations', False) 

29 ser.with_schema_locations = kwargs.get('with_schema_locations', False) 

30 ser.with_xml_declaration = kwargs.get('with_xml_declaration', False) 

31 

32 ser.buf = [] 

33 ser.defaultNamespace = [] 

34 

35 return ser 

36 

37 

38## 

39 

40 

41def to_string(el: gws.XmlElement, **kwargs): 

42 ser = _state(kwargs) 

43 

44 _set_default_namespace(ser, el) 

45 

46 extra_atts = None 

47 

48 if ser.with_namespace_declarations: 

49 extra_atts = namespace.declarations( 

50 for_element=el, 

51 default_ns=ser.defaultNamespace, 

52 with_schema_locations=ser.with_schema_locations, 

53 extra_ns=ser.extra_namespaces, 

54 ) 

55 

56 if ser.with_xml_declaration: 

57 ser.buf.append(_XML_DECL) 

58 

59 _to_string(ser, el, extra_atts) 

60 

61 return ''.join(ser.buf) 

62 

63 

64def _to_string(ser: _SerializerState, el: gws.XmlElement, extra_atts=None): 

65 atts = {} 

66 

67 for key, val in el.attrib.items(): 

68 if val is None: 

69 continue 

70 atts[_make_name(ser, key)] = _value_to_string(ser, val) 

71 

72 if extra_atts: 

73 atts.update(extra_atts) 

74 

75 open_pos = len(ser.buf) 

76 ser.buf.append('') 

77 

78 s = _text_to_string(ser, el.text) 

79 if s: 

80 ser.buf.append(s) 

81 

82 for c in el: 

83 _to_string(ser, c) 

84 

85 open_tag = _make_name(ser, el.tag) 

86 close_tag = open_tag 

87 if atts: 

88 open_tag += ' ' + ' '.join(f'{k}="{v}"' for k, v in atts.items()) 

89 

90 if len(ser.buf) > open_pos + 1: 

91 ser.buf[open_pos] = f'<{open_tag}>' 

92 ser.buf.append(f'</{close_tag}>') 

93 else: 

94 ser.buf[open_pos] += f'<{open_tag}/>' 

95 

96 s = _text_to_string(ser, el.tail) 

97 if s: 

98 ser.buf.append(s) 

99 

100 

101## 

102 

103def to_list(el: gws.XmlElement, **kwargs): 

104 ser = _state(kwargs) 

105 

106 _set_default_namespace(ser, el) 

107 

108 return _to_list(ser, el) 

109 

110 

111def _to_list(ser, el): 

112 name = _make_name(ser, el.tag) 

113 attr = {_make_name(ser, k): v for k, v in el.attrib.items()} 

114 text = (el.text or '').strip() 

115 tail = (el.tail or '').strip() 

116 

117 sub = [_to_list(ser, c) for c in el] 

118 

119 if ser.fold_tags and len(sub) == 1 and (not attr and not text and not tail): 

120 # single wrapper tag, create 'tag/subtag 

121 inner = sub[0] 

122 inner[0] = name + '/' + inner[0] 

123 return inner 

124 

125 res = [name, attr, text, sub, tail] 

126 return [x for x in res if x] 

127 

128 

129## 

130 

131def _set_default_namespace(ser, el): 

132 xmlns = el.get('xmlns') 

133 if xmlns: 

134 ns = namespace.get(xmlns) 

135 if not ns: 

136 raise error.NamespaceError(f'unknown namespace {xmlns!r}') 

137 ser.defaultNamespace = ns 

138 

139 

140def _text_to_string(ser, s): 

141 if s is None: 

142 return '' 

143 if isinstance(s, (int, float, bool)): 

144 return str(s).lower() 

145 if not isinstance(s, str): 

146 s = str(s) 

147 if ser.compact_whitespace: 

148 s = ' '.join(s.strip().split()) 

149 s = s.replace("&", "&amp;") 

150 s = s.replace(">", "&gt;") 

151 s = s.replace("<", "&lt;") 

152 return s 

153 

154 

155def _value_to_string(ser, s): 

156 if s is None: 

157 return '' 

158 if isinstance(s, (int, float, bool)): 

159 return str(s).lower() 

160 if not isinstance(s, str): 

161 s = str(s) 

162 s = s.replace("&", "&amp;") 

163 s = s.replace('"', "&quot;") 

164 s = s.replace(">", "&gt;") 

165 s = s.replace("<", "&lt;") 

166 return s 

167 

168 

169def _make_name(ser, name): 

170 if ser.remove_namespaces: 

171 return namespace.unqualify_name(name) 

172 ns, pname = namespace.parse_name(name) 

173 if not ns: 

174 return name 

175 if ns and ser.defaultNamespace and ns.uri == ser.defaultNamespace.uri: 

176 return pname 

177 return ns.xmlns + ':' + pname 

178 

179 

180_XML_DECL = '<?xml version="1.0" encoding="UTF-8"?>'