Coverage for gws-app/gws/lib/text/__init__.py: 100%

42 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-23 20:06 +0200

1"""Text utilities. 

2 

3Base64 encoding, data URLs (RFC 2397), indentation and line handling, and 

4small string conversions. Decoding errors are raised as ``Error``. 

5 

6Example:: 

7 

8 s = gws.lib.text.to_base64('hello') # 'aGVsbG8=' 

9 gws.lib.text.from_base64(s) # b'hello' 

10 url = gws.lib.text.to_data_url(b'<svg/>', 'image/svg+xml') 

11 mime, content = gws.lib.text.parse_data_url(url) # ('image/svg+xml', b'<svg/>') 

12""" 

13 

14import base64 

15import binascii 

16import re 

17import textwrap 

18import urllib.parse 

19 

20import gws 

21 

22 

23class Error(gws.Error): 

24 """Text decoding error.""" 

25 

26 pass 

27 

28 

29def to_base64(x: str | bytes, url_safe: bool = False) -> str: 

30 """Encode a string or bytes as base64. 

31 

32 Args: 

33 x: A string, encoded as UTF-8, or bytes. 

34 url_safe: Use the URL-safe alphabet (``-`` and ``_`` instead of ``+`` and ``/``). 

35 

36 Returns: 

37 The base64 string, without line breaks. 

38 """ 

39 

40 b = gws.u.to_bytes(x) 

41 e = base64.urlsafe_b64encode(b) if url_safe else base64.standard_b64encode(b) 

42 return e.decode('ascii') 

43 

44 

45def from_base64(x: str | bytes, url_safe: bool = False) -> bytes: 

46 """Decode a base64 string. 

47 

48 Args: 

49 x: A base64 string or bytes. 

50 url_safe: Expect the URL-safe alphabet (``-`` and ``_`` instead of ``+`` and ``/``). 

51 

52 Returns: 

53 The decoded bytes. 

54 

55 Raises: 

56 ``Error``: If the input contains characters outside the alphabet or is incorrectly padded. 

57 """ 

58 

59 try: 

60 return base64.b64decode(gws.u.to_bytes(x), altchars=b'-_' if url_safe else None, validate=True) 

61 except (binascii.Error, ValueError) as exc: 

62 raise Error('invalid base64') from exc 

63 

64 

65def to_data_url(content: str | bytes, mime: gws.MimeType) -> str: 

66 """Create a base64 data URL. 

67 

68 Args: 

69 content: Content, a string is encoded as UTF-8. 

70 mime: MIME type of the content. 

71 

72 Returns: 

73 A ``data:<mime>;base64,...`` URL. 

74 """ 

75 

76 return f'data:{mime};base64,' + to_base64(content) 

77 

78 

79def parse_data_url(url: str) -> tuple[str, bytes]: 

80 """Parse a data URL. 

81 

82 Both base64 and percent-encoded payloads are accepted. Media type parameters 

83 (e.g. ``;charset=utf8``) are ignored. 

84 

85 Args: 

86 url: A ``data:`` URL. 

87 

88 Returns: 

89 A tuple of the lowercase MIME type (``text/plain`` if empty) and the content. 

90 

91 Raises: 

92 ``Error``: If the value is not a data URL or the payload cannot be decoded. 

93 """ 

94 

95 m = re.match(r'^data:([^,]*),', url) 

96 if not m: 

97 raise Error('invalid data url') 

98 

99 params = [p.strip().lower() for p in m.group(1).split(';')] 

100 mime = params[0] or 'text/plain' 

101 payload = url[m.end():] 

102 

103 if len(params) > 1 and params[-1] == 'base64': 

104 return mime, from_base64(payload) 

105 return mime, urllib.parse.unquote_to_bytes(payload) 

106 

107 

108def dedent(text: str) -> str: 

109 """Remove the common leading whitespace from all lines. 

110 

111 Args: 

112 text: A multiline string. 

113 

114 Returns: 

115 The dedented string. Lines that consist of whitespace only are emptied. 

116 """ 

117 

118 return textwrap.dedent(text) 

119 

120 

121def to_lines(text: str, comment: str = None) -> list[str]: 

122 """Convert a multiline string into a list of strings. 

123 

124 Args: 

125 text: A string. 

126 comment: Comment marker. If given, everything from the marker to the end of the line is removed. 

127 

128 Returns: 

129 A list of stripped, non-empty lines. 

130 """ 

131 

132 ls = [] 

133 

134 for s in text.splitlines(): 

135 if comment and comment in s: 

136 s = s.split(comment)[0] 

137 s = s.strip() 

138 if s: 

139 ls.append(s) 

140 

141 return ls 

142 

143 

144def to_int_str(x) -> str: 

145 """Convert a number to a string of its integer part. 

146 

147 Args: 

148 x: A number. 

149 

150 Returns: 

151 The integer as a string, e.g. ``'12'`` for ``12.7``. 

152 """ 

153 

154 return str(int(x))