def to_nodename(string, invalid=None, raise_exc=False):
    """Makes a Quilt Node name (perhaps an ugly one) out of any string.

    This should match whatever the current definition of a node name is, as
    defined in is_nodename().

    This isn't an isomorphic change, the original name can't be recovered
    from the change in all cases, so it must be stored separately (`FileNode`
    metadata)

    If `invalid` is given, it should be an iterable of names that the returned
    string cannot match -- for example, other node names.

    If `raise_exc` is True, an exception is raised when the converted string
    is present in `invalid`.  Otherwise, the converted string will have a
    number appended to its name.

    Example:
    # replace special chars -> remove prefix underscores -> rename keywords
    # '!if' -> '_if' -> 'if' -> 'if_'
    >>> to_nodename('!if') -> 'if_'
    >>> to_nodename('if', ['if_']) -> 'if__2'
    >>> to_nodename('9#blah') -> 'n9_blah'
    >>> to_nodename('9:blah', ['n9_blah', 'n9_blah_2']) -> 'n9_blah_3'

    :param string: string to convert to a nodename
    :param invalid: Container of names to avoid.  Efficiency: Use set or dict
    :param raise_exc: Raise an exception on name conflicts if truthy.
    :returns: valid node name
    """
    string = to_identifier(string)

    #TODO: Remove this stanza once keywords are permissible in nodenames
    if keyword.iskeyword(string):
        string += '_'   # there are no keywords ending in "_"

    # Done if no deduplication needed
    if invalid is None:
        return string

    # Deduplicate
    if string in invalid and raise_exc:
        raise QuiltException("Conflicting node name after string conversion: {!r}".format(string))

    result = string
    counter = 1
    while result in invalid:
        # first conflicted name will be "somenode_2"
        # The result is "somenode", "somenode_2", "somenode_3"..
        counter += 1
        result = "{}_{}".format(string, counter)

    return result
Makes a Quilt Node name (perhaps an ugly one) out of any string.

    This should match whatever the current definition of a node name is, as
    defined in is_nodename().

    This isn't an isomorphic change, the original name can't be recovered
    from the change in all cases, so it must be stored separately (`FileNode`
    metadata)

    If `invalid` is given, it should be an iterable of names that the returned
    string cannot match -- for example, other node names.

    If `raise_exc` is True, an exception is raised when the converted string
    is present in `invalid`.  Otherwise, the converted string will have a
    number appended to its name.

    Example:
    # replace special chars -> remove prefix underscores -> rename keywords
    # '!if' -> '_if' -> 'if' -> 'if_'
    >>> to_nodename('!if') -> 'if_'
    >>> to_nodename('if', ['if_']) -> 'if__2'
    >>> to_nodename('9#blah') -> 'n9_blah'
    >>> to_nodename('9:blah', ['n9_blah', 'n9_blah_2']) -> 'n9_blah_3'

    :param string: string to convert to a nodename
    :param invalid: Container of names to avoid.  Efficiency: Use set or dict
    :param raise_exc: Raise an exception on name conflicts if truthy.
    :returns: valid node name