# lang.py - dot language creation helpers """Quote strings to be valid DOT identifiers, assemble attribute lists.""" import re import collections from . import _compat, tools __all__ = ['quote', 'quote_edge', 'a_list', 'attr_list'] # http://www.graphviz.org/doc/info/lang.html HTML_STRING = re.compile(r'<.*>$', re.DOTALL) ID = re.compile(r'([a-zA-Z_][a-zA-Z0-9_]*|-?(\.\d+|\d+(\.\d*)?))$') KEYWORDS = {'node', 'edge', 'graph', 'digraph', 'subgraph', 'strict'} COMPASS = {'n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw', 'c', '_'} # TODO def quote(identifier, html=HTML_STRING.match, valid_id=ID.match, dot_keywords=KEYWORDS): """Return DOT identifier from string, quote if needed. >>> quote('') '""' >>> quote('spam') 'spam' >>> quote('spam spam') '"spam spam"' >>> quote('-4.2') '-4.2' >>> quote('.42') '.42' >>> quote('<spam>') '<spam>' >>> quote(nohtml('<>')) '"<>"' """ if html(identifier) and not isinstance(identifier, NoHtml): pass elif not valid_id(identifier) or identifier.lower() in dot_keywords: return '"%s"' % identifier.replace('"', '\\"') return identifier def quote_edge(identifier): """Return DOT edge statement node_id from string, quote if needed. >>> quote_edge('spam') 'spam' >>> quote_edge('spam spam:eggs eggs') '"spam spam":"eggs eggs"' >>> quote_edge('spam:eggs:s') 'spam:eggs:s' """ node, _, rest = identifier.partition(':') parts = [quote(node)] if rest: port, _, compass = rest.partition(':') parts.append(quote(port)) if compass: parts.append(compass) return ':'.join(parts) def a_list(label=None, kwargs=None, attributes=None): """Return assembled DOT a_list string. >>> a_list('spam', {'spam': None, 'ham': 'ham ham', 'eggs': ''}) 'label=spam eggs="" ham="ham ham"' """ result = ['label=%s' % quote(label)] if label is not None else [] if kwargs: items = ['%s=%s' % (quote(k), quote(v)) for k, v in tools.mapping_items(kwargs) if v is not None] result.extend(items) if attributes: if hasattr(attributes, 'items'): attributes = tools.mapping_items(attributes) items = ['%s=%s' % (quote(k), quote(v)) for k, v in attributes if v is not None] result.extend(items) return ' '.join(result) def attr_list(label=None, kwargs=None, attributes=None): """Return assembled DOT attribute list string. Sorts ``kwargs`` and ``attributes`` if they are plain dicts (to avoid unpredictable order from hash randomization in Python 3 versions). >>> attr_list() '' >>> attr_list('spam spam', kwargs={'eggs': 'eggs', 'ham': 'ham ham'}) ' [label="spam spam" eggs=eggs ham="ham ham"]' >>> attr_list(kwargs={'spam': None, 'eggs': ''}) ' [eggs=""]' """ content = a_list(label, kwargs, attributes) if not content: return '' return ' [%s]' % content class NoHtml(object): """Mixin for string subclasses disabling fall-through of ``'<...>'``.""" __slots__ = () _doc = "%s subclass that does not treat ``'<...>'`` as DOT HTML string." @classmethod def _subcls(cls, other): name = '%s_%s' % (cls.__name__, other.__name__) bases = (other, cls) ns = {'__doc__': cls._doc % other.__name__} return type(name, bases, ns) NOHTML = collections.OrderedDict((c, NoHtml._subcls(c)) for c in _compat.string_classes) def nohtml(s): """Return copy of ``s`` that will not treat ``'<...>'`` as DOT HTML string in quoting. Args: s: String in which leading ``'<'`` and trailing ``'>'`` should be treated as literal. Raises: TypeError: If ``s`` is not a ``str`` on Python 3, or a ``str``/``unicode`` on Python 2. >>> quote('<>-*-<>') '<>-*-<>' >>> quote(nohtml('<>-*-<>')) '"<>-*-<>"' """ try: subcls = NOHTML[type(s)] except KeyError: raise TypeError('%r does not have one of the required types: %r' % (s, list(NOHTML))) return subcls(s)