Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/graphviz/quoting.py: 90%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""Quote strings to be valid DOT identifiers, assemble quoted attribute lists."""
3from collections.abc import Sequence, Set, Mapping
4import functools
5import re
6from typing import Final
7import warnings
9from . import _tools
10from . import exceptions
12__all__ = ['quote', 'quote_edge',
13 'a_list', 'attr_list',
14 'escape', 'nohtml']
16# https://www.graphviz.org/doc/info/lang.html
17# https://www.graphviz.org/doc/info/attrs.html#k:escString
19HTML_STRING: Final = re.compile(r'<.*>$', re.DOTALL)
21ID: Final = re.compile(r'([a-zA-Z_][a-zA-Z0-9_]*|-?(\.[0-9]+|[0-9]+(\.[0-9]*)?))$')
23KEYWORDS: Final[Set[str]] = {'node', 'edge', 'graph', 'digraph', 'subgraph', 'strict'}
25COMPASS: Final[Set[str]] = {'n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw', 'c', '_'} # TODO
27FINAL_ODD_BACKSLASHES: Final = re.compile(r'(?<!\\)(?:\\{2})*\\$')
29QUOTE_WITH_OPTIONAL_BACKSLASHES: Final = re.compile(r'''
30 (?P<escaped_backslashes>(?:\\{2})*)
31 \\? # treat \" same as "
32 (?P<literal_quote>")
33 ''', flags=re.VERBOSE)
35ESCAPE_UNESCAPED_QUOTES: Final = functools.partial(QUOTE_WITH_OPTIONAL_BACKSLASHES.sub,
36 r'\g<escaped_backslashes>'
37 r'\\'
38 r'\g<literal_quote>')
41@_tools.deprecate_positional_args(supported_number=1)
42def quote(identifier: str,
43 is_html_string=HTML_STRING.match,
44 is_valid_id=ID.match,
45 dot_keywords=KEYWORDS,
46 endswith_odd_number_of_backslashes=FINAL_ODD_BACKSLASHES.search,
47 escape_unescaped_quotes=ESCAPE_UNESCAPED_QUOTES) -> str:
48 r"""Return DOT identifier from string, quote if needed.
50 >>> quote('') # doctest: +NO_EXE
51 '""'
53 >>> quote('spam')
54 'spam'
56 >>> quote('spam spam')
57 '"spam spam"'
59 >>> quote('-4.2')
60 '-4.2'
62 >>> quote('.42')
63 '.42'
65 >>> quote('<<b>spam</b>>')
66 '<<b>spam</b>>'
68 >>> quote(nohtml('<>'))
69 '"<>"'
71 >>> print(quote('"'))
72 "\""
74 >>> print(quote('\\"'))
75 "\""
77 >>> print(quote('\\\\"'))
78 "\\\""
80 >>> print(quote('\\\\\\"'))
81 "\\\""
82 """
83 if is_html_string(identifier) and not isinstance(identifier, NoHtml):
84 pass
85 elif not is_valid_id(identifier) or identifier.lower() in dot_keywords:
86 if endswith_odd_number_of_backslashes(identifier):
87 warnings.warn('expect syntax error scanning invalid quoted string:'
88 f' {identifier!r}',
89 category=exceptions.DotSyntaxWarning)
90 return f'"{escape_unescaped_quotes(identifier)}"'
91 return identifier
94def quote_edge(identifier: str) -> str:
95 """Return DOT edge statement node_id from string, quote if needed.
97 >>> quote_edge('spam') # doctest: +NO_EXE
98 'spam'
100 >>> quote_edge('spam spam:eggs eggs')
101 '"spam spam":"eggs eggs"'
103 >>> quote_edge('spam:eggs:s')
104 'spam:eggs:s'
105 """
106 node, _, rest = identifier.partition(':')
107 parts = [quote(node)]
108 if rest:
109 port, _, compass = rest.partition(':')
110 parts.append(quote(port))
111 if compass:
112 parts.append(compass)
113 return ':'.join(parts)
116@_tools.deprecate_positional_args(supported_number=1)
117def a_list(label: str | None = None,
118 kwargs: Mapping[str, str] | None = None,
119 attributes: (Mapping[str, str]
120 | Sequence[tuple[str, str]]
121 | None) = None) -> str:
122 """Return assembled DOT a_list string.
124 >>> a_list('spam', kwargs={'spam': None, 'ham': 'ham ham', 'eggs': ''}) # doctest: +NO_EXE
125 'label=spam eggs="" ham="ham ham"'
126 """
127 result = [f'label={quote(label)}'] if label is not None else []
128 if kwargs:
129 result += [f'{quote(k)}={quote(v)}'
130 for k, v in _tools.mapping_items(kwargs) if v is not None]
131 if attributes:
132 items = (_tools.mapping_items(attributes)
133 if isinstance(attributes, Mapping) else attributes)
134 result += [f'{quote(k)}={quote(v)}'
135 for k, v in items if v is not None]
136 return ' '.join(result)
139@_tools.deprecate_positional_args(supported_number=1)
140def attr_list(label: str | None = None,
141 kwargs: Mapping[str, str] | None = None,
142 attributes: (Mapping[str, str]
143 | Sequence[tuple[str, str]]
144 | None) = None) -> str:
145 """Return assembled DOT attribute list string.
147 Sorts ``kwargs`` and ``attributes`` if they are plain dicts
148 (to avoid unpredictable order from hash randomization in Python < 3.7).
150 >>> attr_list() # doctest: +NO_EXE
151 ''
153 >>> attr_list('spam spam', kwargs={'eggs': 'eggs', 'ham': 'ham ham'})
154 ' [label="spam spam" eggs=eggs ham="ham ham"]'
156 >>> attr_list(kwargs={'spam': None, 'eggs': ''})
157 ' [eggs=""]'
158 """
159 content = a_list(label, kwargs=kwargs, attributes=attributes)
160 if not content:
161 return ''
162 return f' [{content}]'
165class Quote:
166 """Quote strings to be valid DOT identifiers, assemble quoted attribute lists."""
168 _quote = staticmethod(quote)
169 _quote_edge = staticmethod(quote_edge)
171 _a_list = staticmethod(a_list)
172 _attr_list = staticmethod(attr_list)
175def escape(s: str) -> str:
176 r"""Return string disabling special meaning of backslashes and ``'<...>'``.
178 Args:
179 s: String in which backslashes and ``'<...>'``
180 should be treated as literal.
182 Returns:
183 Escaped string subclass instance.
185 Raises:
186 TypeError: If ``s`` is not a ``str``.
188 Example:
189 >>> import graphviz # doctest: +NO_EXE
190 >>> print(graphviz.escape(r'\l'))
191 \\l
193 See also:
194 Upstream documentation:
195 https://www.graphviz.org/doc/info/attrs.html#k:escString
196 """
197 return nohtml(s.replace('\\', '\\\\'))
200class NoHtml(str):
201 """String subclass that does not treat ``'<...>'`` as DOT HTML string."""
203 __slots__ = ()
206def nohtml(s: str) -> str:
207 """Return string not treating ``'<...>'`` as DOT HTML string in quoting.
209 Args:
210 s: String in which leading ``'<'`` and trailing ``'>'``
211 should be treated as literal.
213 Returns:
214 String subclass instance.
216 Raises:
217 TypeError: If ``s`` is not a ``str``.
219 Example:
220 >>> import graphviz # doctest: +NO_EXE
221 >>> g = graphviz.Graph()
222 >>> g.node(graphviz.nohtml('<>-*-<>'))
223 >>> print(g.source) # doctest: +NORMALIZE_WHITESPACE
224 graph {
225 "<>-*-<>"
226 }
227 """
228 return NoHtml(s)