Coverage for gws-app/gws/lib/xmlx/tag.py: 98%

61 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-05 13:35 +0200

1"""XML builder.""" 

2 

3import re 

4 

5import gws 

6 

7from . import element, error, namespace, util 

8 

9 

10def tag(name: str, *args, **kwargs) -> gws.XmlElement: 

11 """Build an XML element from arguments. 

12 

13 Tag and attribute names are local names, Clark names (``{uri}name``) or ``ID:name``, where ``ID`` is the name 

14 of a well-known namespace (``GML``, ``OWS_11``), which is resolved to a Clark name. 

15 

16 Example:: 

17 

18 tag('geometry/GML:Point', {'GML:id': 'xy'}, tag('GML:coordinates', '12.345,56.789'), srsName=3857) 

19 

20 Args: 

21 name: Tag name, or a slash-separated list of tag names, in which case nested elements are created. 

22 Slashes inside the ``{uri}`` part of a Clark name do not separate. 

23 *args: Content of the innermost element. A string, number, bool, date or datetime is appended 

24 to the text, an ``XmlElement`` is appended as a child, a dict or a ``gws.Data`` object updates 

25 the attributes (``None`` values are skipped), ``None`` is ignored, and any other iterable 

26 is processed item by item with the same rules. 

27 **kwargs: Additional attributes of the innermost element. 

28 

29 Returns: 

30 The outermost element. 

31 

32 Raises: 

33 BuildError: If a tag name is empty, a namespace ``ID`` is unknown, or an argument cannot be used. 

34 """ 

35 

36 elements = [] 

37 

38 for part in _split_path(name): 

39 part = part.strip() 

40 if not part: 

41 raise error.BuildError(f'invalid tag name: {name!r}') 

42 el = element.XmlElement(_resolve_name(part)) 

43 if elements: 

44 elements[-1].append(el) 

45 elements.append(el) 

46 

47 if not elements: 

48 raise error.BuildError(f'invalid tag name: {name!r}') 

49 

50 for arg in args: 

51 _add(elements[-1], arg) 

52 

53 if kwargs: 

54 _add(elements[-1], kwargs) 

55 

56 return elements[0] 

57 

58 

59## 

60 

61 

62def _resolve_name(name: str) -> str: 

63 # local and Clark names are taken as they are, ``ID:name`` is resolved via the well-known table 

64 

65 uri, prefix, pname = namespace.parse_name(name) 

66 if uri: 

67 return name 

68 if prefix: 

69 ns = namespace.find_well_known(prefix) 

70 if not ns: 

71 raise error.BuildError(f'unknown namespace {prefix!r} in {name!r}') 

72 return namespace.full_name(pname, ns) 

73 return name 

74 

75 

76def _split_path(name: str) -> list[str]: 

77 # split on '/', but not inside the {uri} part of a Clark name 

78 

79 return re.split(r'/(?![^{]*})', name) 

80 

81 

82def _add(el: gws.XmlElement, arg): 

83 """Add an argument of ``tag()`` to an element.""" 

84 

85 if arg is None: 

86 return 

87 

88 if isinstance(arg, gws.XmlElement): 

89 el.append(arg) 

90 return 

91 

92 s, ok = util.atom_to_string(arg) 

93 if ok: 

94 _add_text(el, s) 

95 return 

96 

97 if gws.is_data_object(arg): 

98 arg = gws.u.to_dict(arg) 

99 

100 if isinstance(arg, dict): 

101 for k, v in arg.items(): 

102 if v is not None: 

103 el.set(_resolve_name(k), v) 

104 return 

105 

106 try: 

107 arg = list(arg) 

108 except TypeError: 

109 raise error.BuildError(f'invalid argument: in {el.tag!r}, {arg=}') 

110 

111 for a in arg: 

112 _add(el, a) 

113 

114 

115def _add_text(el, s): 

116 """Append text to the element text or to the tail of its last child.""" 

117 

118 if not s: 

119 return 

120 if len(el) == 0: 

121 el.text = (el.text or '') + s 

122 else: 

123 el[-1].tail = (el[-1].tail or '') + s