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
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-23 20:06 +0200
1"""Text utilities.
3Base64 encoding, data URLs (RFC 2397), indentation and line handling, and
4small string conversions. Decoding errors are raised as ``Error``.
6Example::
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"""
14import base64
15import binascii
16import re
17import textwrap
18import urllib.parse
20import gws
23class Error(gws.Error):
24 """Text decoding error."""
26 pass
29def to_base64(x: str | bytes, url_safe: bool = False) -> str:
30 """Encode a string or bytes as base64.
32 Args:
33 x: A string, encoded as UTF-8, or bytes.
34 url_safe: Use the URL-safe alphabet (``-`` and ``_`` instead of ``+`` and ``/``).
36 Returns:
37 The base64 string, without line breaks.
38 """
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')
45def from_base64(x: str | bytes, url_safe: bool = False) -> bytes:
46 """Decode a base64 string.
48 Args:
49 x: A base64 string or bytes.
50 url_safe: Expect the URL-safe alphabet (``-`` and ``_`` instead of ``+`` and ``/``).
52 Returns:
53 The decoded bytes.
55 Raises:
56 ``Error``: If the input contains characters outside the alphabet or is incorrectly padded.
57 """
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
65def to_data_url(content: str | bytes, mime: gws.MimeType) -> str:
66 """Create a base64 data URL.
68 Args:
69 content: Content, a string is encoded as UTF-8.
70 mime: MIME type of the content.
72 Returns:
73 A ``data:<mime>;base64,...`` URL.
74 """
76 return f'data:{mime};base64,' + to_base64(content)
79def parse_data_url(url: str) -> tuple[str, bytes]:
80 """Parse a data URL.
82 Both base64 and percent-encoded payloads are accepted. Media type parameters
83 (e.g. ``;charset=utf8``) are ignored.
85 Args:
86 url: A ``data:`` URL.
88 Returns:
89 A tuple of the lowercase MIME type (``text/plain`` if empty) and the content.
91 Raises:
92 ``Error``: If the value is not a data URL or the payload cannot be decoded.
93 """
95 m = re.match(r'^data:([^,]*),', url)
96 if not m:
97 raise Error('invalid data url')
99 params = [p.strip().lower() for p in m.group(1).split(';')]
100 mime = params[0] or 'text/plain'
101 payload = url[m.end():]
103 if len(params) > 1 and params[-1] == 'base64':
104 return mime, from_base64(payload)
105 return mime, urllib.parse.unquote_to_bytes(payload)
108def dedent(text: str) -> str:
109 """Remove the common leading whitespace from all lines.
111 Args:
112 text: A multiline string.
114 Returns:
115 The dedented string. Lines that consist of whitespace only are emptied.
116 """
118 return textwrap.dedent(text)
121def to_lines(text: str, comment: str = None) -> list[str]:
122 """Convert a multiline string into a list of strings.
124 Args:
125 text: A string.
126 comment: Comment marker. If given, everything from the marker to the end of the line is removed.
128 Returns:
129 A list of stripped, non-empty lines.
130 """
132 ls = []
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)
141 return ls
144def to_int_str(x) -> str:
145 """Convert a number to a string of its integer part.
147 Args:
148 x: A number.
150 Returns:
151 The integer as a string, e.g. ``'12'`` for ``12.7``.
152 """
154 return str(int(x))