elementpath/xpath_nodes.py
def match_attribute_node(obj: Any, name: Optional[str] = None) -> bool:
    """
    Returns `True` if the first argument is an attribute node matching the name, `False` otherwise.
    Raises a ValueError if the argument name has to be used, but it's in a wrong format.

    :param obj: the node to be tested.
    :param name: a fully qualified name, a local name or a wildcard. The accepted wildcard formats \
    are '*', '*:*', '*:local-name' and '{namespace}*'.
    """
    if name is None or name == '*' or name == '*:*':
        return isinstance(obj, (AttributeNode, TypedAttribute))
    elif not isinstance(obj, (AttributeNode, TypedAttribute)):
        return False
    elif isinstance(obj, TypedAttribute):
        obj = obj.attribute

    if not name:
        return not obj.name
    elif name[0] == '*':
        try:
            _, _name = name.split(':')
        except (ValueError, IndexError):
            raise ElementPathValueError("unexpected format %r for argument 'name'" % name)
        else:
            if obj.name.startswith('{'):
                return obj.name.split('}')[1] == _name
            else:
                return obj.name == _name

    elif name[-1] == '*':
        if name[0] != '{' or '}' not in name:
            raise ElementPathValueError("unexpected format %r for argument 'name'" % name)
        elif obj.name.startswith('{'):
            return obj.name.split('}')[0][1:] == name.split('}')[0][1:]
        else:
            return False
    else:
        return obj.name == name
elementpath/xpath_nodes.py
def node_document_uri(obj: Any) -> Optional[str]:
    if is_document_node(obj):
        try:
            uri = cast(str, obj.getroot().attrib[XML_BASE])
            parts = urlparse(uri)
        except (KeyError, ValueError):
            pass
        else:
            if parts.scheme and parts.netloc or parts.path.startswith('/'):
                return uri
    return None
elementpath/xpath_nodes.py
def etree_iter_strings(elem: Union[DocumentNode, ElementNode, TypedElement]) -> Iterator[str]:
    e: ElementNode

    if isinstance(elem, TypedElement):
        if elem.xsd_type.is_element_only():
            # Element-only text content is normalized
            elem = elem.elem
            for e in elem.iter():
                if callable(e.tag):
                    continue
                if e.text is not None:
                    yield e.text.strip() if e is elem else e.text
                if e.tail is not None and e is not elem:
                    yield e.tail.strip() if e in elem else e.tail
            return

        elem = elem.elem

    for e in elem.iter():
        if callable(e.tag):
            continue
        if e.text is not None:
            yield e.text
        if e.tail is not None and e is not elem:
            yield e.tail
elementpath/xpath_nodes.py
def is_etree_element(obj: Any) -> bool:
    return hasattr(obj, 'tag') and hasattr(obj, 'attrib') and hasattr(obj, 'text')
elementpath/xpath_nodes.py
def is_comment_node(obj: Any) -> bool:
    return hasattr(obj, 'tag') and callable(obj.tag) and obj.tag.__name__ == 'Comment'
elementpath/xpath_nodes.py
class NamespaceNode(XPathNode):
    """
    A class for processing XPath namespace nodes.

    :param prefix: the namespace prefix.
    :param uri: the namespace URI.
    :param parent: the parent element.
    """
    def __init__(self, prefix: str, uri: str, parent: Optional[ElementNode] = None) -> None:
        self.prefix = prefix
        self.uri = uri
        self.parent = parent

    @property
    def kind(self) -> str:
        return 'namespace'

    @property
    def name(self) -> str:
        return self.prefix

    @property
    def value(self) -> str:
        return self.uri

    def as_item(self) -> Tuple[str, str]:
        return self.prefix, self.uri

    def __repr__(self) -> str:
        if self.parent is not None:
            return '%s(prefix=%r, uri=%r, parent=%r)' % (
                self.__class__.__name__, self.prefix, self.uri, self.parent
            )
        return '%s(prefix=%r, uri=%r)' % (self.__class__.__name__, self.prefix, self.uri)

    def __eq__(self, other: Any) -> bool:
        return isinstance(other, self.__class__) and \
            self.prefix == other.prefix and \
            self.uri == other.uri and \
            self.parent is other.parent

    def __hash__(self) -> int:
        return hash((self.prefix, self.uri, self.parent))
elementpath/xpath_nodes.py
def node_kind(obj: Any) -> Optional[str]:
    if isinstance(obj, XPathNode):
        return obj.kind
    elif is_element_node(obj):
        return 'element'
    elif is_document_node(obj):
        return 'document-node'
    elif is_comment_node(obj):
        return 'comment'
    elif is_processing_instruction_node(obj):
        return 'processing-instruction'
    else:
        return None
elementpath/xpath_nodes.py
def etree_deep_equal(e1: ElementNode, e2: ElementNode) -> bool:
    if e1.tag != e2.tag:
        return False
    elif (e1.text or '').strip() != (e2.text or '').strip():
        return False
    elif (e1.tail or '').strip() != (e2.tail or '').strip():
        return False
    elif e1.attrib != e2.attrib:
        return False
    elif len(e1) != len(e2):
        return False
    return all(etree_deep_equal(c1, c2) for c1, c2 in zip(e1, e2))
elementpath/xpath_nodes.py
class TextNode(XPathNode):
    """
    A class for processing XPath text nodes. An Element's property
    (elem.text or elem.tail) with a `None` value is not a text node.

    :param value: a string value.
    :param parent: the parent element.
    :param tail: provide `True` if the text node is the parent Element's tail.
    """
    text: None
    _tail = False

    def __init__(self, value: str, parent: Optional[ElementNode] = None,
                 tail: bool = False) -> None:
        self.value = value
        self.parent = parent
        if tail and parent is not None:
            self._tail = True

    @property
    def kind(self) -> str:
        return 'text'

    def is_tail(self) -> bool:
        """Returns `True` if the node has a parent and represents the tail text."""
        return self._tail

    def __repr__(self) -> str:
        if self.parent is not None:
            return '%s(%r, parent=%r, tail=%r)' % (
                self.__class__.__name__, self.value, self.parent, self._tail
            )
        return '%s(%r)' % (self.__class__.__name__, self.value)

    def __eq__(self, other: Any) -> bool:
        return isinstance(other, self.__class__) and \
            self.value == other.value and \
            self.parent is other.parent and \
            self._tail is other._tail

    def __hash__(self) -> int:
        return hash((self.value, self.parent, self._tail))
elementpath/xpath_nodes.py
def node_nilled(obj: Any) -> Optional[bool]:
    if is_element_node(obj):
        if isinstance(obj, TypedElement):
            return obj.elem.get(XSI_NIL) in ('true', '1')
        return obj.get(XSI_NIL) in ('true', '1')
    return None
elementpath/xpath_nodes.py
def node_attributes(obj: Any) -> Optional[Dict[str, Any]]:
    return obj.attrib if is_element_node(obj) else None
elementpath/xpath_nodes.py
def is_document_node(obj: Any) -> bool:
    return hasattr(obj, 'getroot') and hasattr(obj, 'parse') and hasattr(obj, 'iter')
elementpath/xpath_nodes.py
def match_element_node(obj: Any, tag: Optional[str] = None) -> Any:
    """
    Returns `True` if the first argument is an element node matching the tag, `False` otherwise.
    Raises a ValueError if the argument tag has to be used but it's in a wrong format.

    :param obj: the node to be tested.
    :param tag: a fully qualified name, a local name or a wildcard. The accepted
    wildcard formats are '*', '*:*', '*:local-name' and '{namespace}*'.
    """
    if isinstance(obj, TypedElement):
        obj = obj.elem
    elif not is_etree_element(obj) or callable(obj.tag):
        return False

    if not tag:
        return True
    elif not obj.tag:
        return obj.tag == tag
    elif tag == '*' or tag == '*:*':
        return obj.tag != ''
    elif tag[0] == '*':
        try:
            _, name = tag.split(':')
        except (ValueError, IndexError):
            raise ElementPathValueError("unexpected format %r for argument 'tag'" % tag)
        else:
            if obj.tag[0] == '{':
                return obj.tag.split('}')[1] == name
            else:
                return obj.tag == name

    elif tag[-1] == '*':
        if tag[0] != '{' or '}' not in tag:
            raise ElementPathValueError("unexpected format %r for argument 'tag'" % tag)
        elif obj.tag[0] == '{':
            return obj.tag.split('}')[0][1:] == tag.split('}')[0][1:]
        else:
            return False
    else:
        return obj.tag == tag
elementpath/xpath_nodes.py
def etree_iter_nodes(root: Union[DocumentNode, ElementNode], with_root: bool = True) \
        -> Iterator[Union[DocumentNode, ElementNode, TextNode]]:

    _root: Union[ElementProtocol, LxmlElementProtocol]
    _lxml_root: Optional[LxmlElementProtocol] = None

    if hasattr(root, 'getroot'):
        document = cast(DocumentNode, root)
        if with_root:
            yield document
        else:
            with_root = True
        _root = document.getroot()
    elif isinstance(root, TypedElement):
        _root = cast(ElementProtocol, root.elem)
    else:
        _root = cast(ElementProtocol, root)

    if with_root:
        yield _root
        if hasattr(_root, 'getparent'):  # TODO: type checking
            _lxml_root = cast(LxmlElementProtocol, _root)
            if _lxml_root.getparent() is None:
                previous_siblings = []
                sibling = _lxml_root.getprevious()
                while sibling is not None:
                    previous_siblings.append(cast(ElementProtocol, sibling))
                    sibling = sibling.getprevious()
                yield from reversed(previous_siblings)
            else:
                _lxml_root = None

    if _root.text is not None:
        yield TextNode(_root.text, _root)

    descendants = _root.iter()
    next(descendants)  # discard root
    for e in descendants:
        if callable(e.tag):
            # a comment or a process instruction
            yield e
            if e.tail is not None:
                yield TextNode(e.tail, e, True)
            continue

        yield e
        if e.text is not None:
            yield TextNode(e.text, e)
        if e.tail is not None:
            yield TextNode(e.tail, e, True)

    if _lxml_root is not None:
        sibling = _lxml_root.getnext()
        while sibling is not None:
            yield cast(ElementProtocol, sibling)
            sibling = sibling.getnext()
elementpath/xpath_nodes.py
def node_base_uri(obj: Any) -> Optional[str]:
    try:
        if is_element_node(obj):
            return cast(str, obj.attrib[XML_BASE])
        elif is_document_node(obj):
            return cast(str, obj.getroot().attrib[XML_BASE])
        return None
    except KeyError:
        return None
elementpath/xpath_nodes.py
def is_processing_instruction_node(obj: Any) -> bool:
    return hasattr(obj, 'tag') and callable(obj.tag) and obj.tag.__name__ == 'ProcessingInstruction'
elementpath/xpath_nodes.py
class TypedElement(XPathNode):
    """
    A class for processing typed element nodes.

    :param elem: the linked element. Can be an Element, or an XSD element \
    when XPath is applied on a schema.
    :param xsd_type: the reference XSD type.
    :param value: the decoded value. Can be `None` for empty or element-only elements."
    """
    def __init__(self, elem: ElementProtocol, xsd_type: Any, value: Any) -> None:
        self.elem = elem
        self.xsd_type = xsd_type
        self.value = value

    @property
    def kind(self) -> str:
        return 'element'

    @property
    def name(self) -> str:
        return self.elem.tag

    @property
    def tag(self) -> str:
        return self.elem.tag

    def __repr__(self) -> str:
        return '%s(tag=%r)' % (self.__class__.__name__, self.elem.tag)

    def __eq__(self, other: Any) -> bool:
        return isinstance(other, self.__class__) and \
            self.elem is other.elem and \
            self.value == other.value

    def __hash__(self) -> int:
        return hash((self.elem, self.value))
elementpath/xpath_nodes.py
def node_name(obj: Any) -> Optional[str]:
    if isinstance(obj, XPathNode):
        return cast(Optional[str], obj.name)
    elif not hasattr(obj, 'tag') or not hasattr(obj, 'text'):
        return None
    elif not callable(obj.tag):
        return cast(str, obj.tag)
    elif obj.tag.__name__ != 'ProcessingInstruction':
        return None
    else:
        # Return pi target. ElementTree doesn't have a specific attribute
        # for target but put it before the text, separated by a space.
        try:
            return cast(str, obj.target)
        except AttributeError:
            return cast(str, obj.text.split(' ', maxsplit=1)[0])
elementpath/xpath_nodes.py
def etree_iter_paths(elem: ElementNode, path: str = '.') -> Iterator[Tuple[ElementNode, str]]:
    yield elem, path
    comment_nodes = 0
    pi_nodes = Counter[Optional[str]]()
    positions = Counter[Optional[str]]()

    for child in elem:
        if callable(child.tag):
            if child.tag.__name__ != 'ProcessingInstruction':  # type: ignore[attr-defined]
                comment_nodes += 1
                yield child, f'{path}/comment()[{comment_nodes}]'
                continue

            name = node_name(child)
            pi_nodes[name] += 1
            yield child, f'{path}/processing-instruction({name})[{pi_nodes[name]}]'
            continue

        if child.tag.startswith('{'):
            tag = f'Q{child.tag}'
        else:
            tag = f'Q{{}}{child.tag}'

        if path == '/':
            child_path = f'/{tag}'
        elif path:
            child_path = '/'.join((path, tag))
        else:
            child_path = tag

        positions[child.tag] += 1
        child_path += f'[{positions[child.tag]}]'

        yield from etree_iter_paths(child, child_path)
elementpath/xpath_nodes.py
class TypedAttribute(XPathNode):
    """
    A class for processing typed attribute nodes.

    :param attribute: the origin AttributeNode instance.
    :param xsd_type: the reference XSD type.
    :param value: the types value.
    """
    def __init__(self, attribute: AttributeNode, xsd_type: Any, value: Any) -> None:
        self.attribute = attribute
        self.xsd_type = xsd_type
        self.value = value
        self.parent = attribute.parent

    @property
    def kind(self) -> str:
        return 'attribute'

    @property
    def name(self) -> str:
        return self.attribute.name

    def as_item(self) -> Tuple[str, Any]:
        return self.attribute.name, self.value

    def __repr__(self) -> str:
        return '%s(name=%r)' % (self.__class__.__name__, self.attribute.name)

    def __eq__(self, other: Any) -> bool:
        return isinstance(other, self.__class__) and \
            self.attribute == other.attribute and \
            self.value == other.value

    def __hash__(self) -> int:
        return hash((self.attribute, self.value))
elementpath/xpath_nodes.py
def node_children(obj: Any) -> Optional[Iterator[ElementNode]]:
    if is_element_node(obj):
        return (child for child in obj)
    elif is_document_node(obj):
        return (child for child in [obj.getroot()])
    else:
        return None
elementpath/xpath_nodes.py
class AttributeNode(XPathNode):
    """
    A class for processing XPath attribute nodes.

    :param name: the attribute name.
    :param value: a string value or an XSD attribute when XPath is applied on a schema.
    :param parent: the parent element.
    """
    name: str

    def __init__(self, name: str, value: Union[str, XsdAttributeProtocol],
                 parent: Optional[ElementNode] = None) -> None:
        self.name = name
        self.value: Union[str, XsdAttributeProtocol] = value
        self.parent = parent

    @property
    def kind(self) -> str:
        return 'attribute'

    def as_item(self) -> Tuple[str, Union[str, XsdAttributeProtocol]]:
        return self.name, self.value

    def __repr__(self) -> str:
        if self.parent is not None:
            return '%s(name=%r, value=%r, parent=%r)' % (
                self.__class__.__name__, self.name, self.value, self.parent
            )
        return '%s(name=%r, value=%r)' % (self.__class__.__name__, self.name, self.value)

    def __eq__(self, other: Any) -> bool:
        return isinstance(other, self.__class__) and \
            self.name == other.name and \
            self.value == other.value and \
            self.parent is other.parent

    def __hash__(self) -> int:
        return hash((self.name, self.value, self.parent))


Question: import unittest
import io
import xml.etree.ElementTree as ElementTree
from unittest.mock import patch
from elementpath.xpath_nodes import AttributeNode, TextNode, TypedAttribute, \
    TypedElement, NamespaceNode, is_etree_element, etree_iter_strings, \
    etree_deep_equal, match_element_node, match_attribute_node, is_comment_node, \
    is_document_node, is_processing_instruction_node, node_attributes, node_base_uri, \
    node_document_uri, node_children, node_nilled, node_kind, node_name, \
    etree_iter_nodes, etree_iter_paths
            xsd_type = DummyXsdType()

            typed_element = TypedElement(element, xsd_type, None)
            self.assertEqual(repr(typed_element), "TypedElement(tag='schema')")

    def test_text_nodes(self):
        parent = ElementTree.Element('element')
        self.assertEqual(TextNode('alpha'), TextNode('alpha'))
        self.assertEqual(TextNode('alpha', parent), TextNode('alpha', parent))
        self.assertEqual(TextNode('alpha', parent, tail=True),
                         TextNode('alpha', parent, tail=True))
        self.assertEqual(TextNode('alpha', tail=True), TextNode('alpha'))
        self.assertNotEqual(TextNode('alpha', parent), TextNode('alpha'))
        self.assertNotEqual(TextNode('alpha', parent, tail=True),
                            TextNode('alpha', parent))
        self.assertNotEqual(TextNode('alpha', parent),
                            TextNode('alpha', parent=ElementTree.Element('element')))  # != id()

        self.assertFalse(TextNode('alpha', parent).is_tail())
        self.assertTrue(TextNode('alpha', parent, tail=True).is_tail())
        self.assertFalse(TextNode('alpha', tail=True).is_tail())

        self.assertEqual(repr(TextNode('alpha')), "TextNode('alpha')")
        text = TextNode('alpha', parent)
        self.assertTrue(repr(text).startswith("TextNode('alpha', parent=<Element "))
        self.assertTrue(repr(text).endswith(", tail=False)"))
        text = TextNode('alpha', parent, tail=True)
        self.assertTrue(repr(text).endswith(", tail=True)"))

    def test_namespace_nodes(self):
        parent = ElementTree.Element('element')
        namespace = NamespaceNode('tns', 'http://xpath.test/ns')

        self.assertEqual(repr(namespace),
                         "NamespaceNode(prefix='tns', uri='http://xpath.test/ns')")
        self.assertEqual(namespace.value, 'http://xpath.test/ns')
        self.assertEqual(namespace, NamespaceNode('tns', 'http://xpath.test/ns'))
        self.assertEqual(namespace.as_item(), ('tns', 'http://xpath.test/ns'))
        self.assertNotEqual(namespace,
                            NamespaceNode('tns', 'http://xpath.test/ns', parent))

        namespace = NamespaceNode('tns', 'http://xpath.test/ns', parent)
        self.assertEqual(repr(namespace)[:81],
                         "NamespaceNode(prefix='tns', uri='http://xpath.test/ns', "
                         "parent=<Element 'element'")

        self.assertEqual(namespace, NamespaceNode('tns', 'http://xpath.test/ns', parent))
        self.assertEqual(namespace.as_item(), ('tns', 'http://xpath.test/ns'))
        self.assertNotEqual(namespace, NamespaceNode('tns', 'http://xpath.test/ns'))
        self.assertNotEqual(namespace, NamespaceNode('tns', 'http://xpath.test/ns',
                                                     parent=ElementTree.Element('element')))

    def test_node_children_function(self):
        self.assertListEqual(list(node_children(self.elem)), [])
        elem = ElementTree.XML("<A><B1/><B2/></A>")
        self.assertListEqual(list(node_children(elem)), [x for x in elem])
        document = ElementTree.parse(io.StringIO("<A><B1/><B2/></A>"))
        self.assertListEqual(list(node_children(document)), [document.getroot()])
        self.assertIsNone(node_children('a text node'))

    def test_node_nilled_function(self):
        xml_test = '<A xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true" />'
        self.assertTrue(node_nilled(ElementTree.XML(xml_test)))
        xml_test = '<A xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="false" />'
        self.assertFalse(node_nilled(ElementTree.XML(xml_test)))
        self.assertFalse(node_nilled(ElementTree.XML('<A />')))
        self.assertFalse(node_nilled(TextNode('foo')))

    def test_node_kind_function(self):
        document = ElementTree.parse(io.StringIO(u'<A/>'))
        element = ElementTree.Element('schema')
        attribute = AttributeNode('id', '0212349350')
        namespace = NamespaceNode('xs', 'http://www.w3.org/2001/XMLSchema')
        comment = ElementTree.Comment('nothing important')
        pi = ElementTree.ProcessingInstruction('action', 'nothing to do')
        text = TextNode('betelgeuse')
        self.assertEqual(node_kind(document), 'document-node')
        self.assertEqual(node_kind(element), 'element')
        self.assertEqual(node_kind(attribute), 'attribute')
        self.assertEqual(node_kind(namespace), 'namespace')
        self.assertEqual(node_kind(comment), 'comment')
        self.assertEqual(node_kind(pi), 'processing-instruction')
        self.assertEqual(node_kind(text), 'text')
        self.assertIsNone(node_kind(()))
        self.assertIsNone(node_kind(None))
        self.assertIsNone(node_kind(10))

        with patch.multiple(DummyXsdType, is_simple=lambda x: True):
            xsd_type = DummyXsdType()

            typed_attribute = TypedAttribute(attribute, xsd_type, '0212349350')
            self.assertEqual(node_kind(typed_attribute), 'attribute')

            typed_element = TypedElement(element, xsd_type, None)
            self.assertEqual(node_kind(typed_element), 'element')

    def test_node_name_function(self):
        elem = ElementTree.Element('root')
        attr = AttributeNode('a1', '20')
        namespace = NamespaceNode('xs', 'http://www.w3.org/2001/XMLSchema')
        self.assertEqual(node_name(elem), 'root')
        self.assertEqual(node_name(attr), 'a1')
        self.assertEqual(node_name(namespace), 'xs')
        self.assertIsNone(node_name(()))
        self.assertIsNone(node_name(None))

        with patch.multiple(DummyXsdType, is_simple=lambda x: True):
            xsd_type = DummyXsdType()

            typed_elem = TypedElement(elem=elem, xsd_type=xsd_type, value=10)
            self.assertEqual(node_name(typed_elem), 'root')

            typed_attr = TypedAttribute(attribute=attr, xsd_type=xsd_type, value=20)
            self.assertEqual(node_name(typed_attr), 'a1')

    def test_etree_iter_paths(self):
        root = ElementTree.XML('<a><b1><c1/><c2/></b1><b2/><b3><c3/></b3></a>')
        root[2].append(ElementTree.Comment('a comment'))
        root[2].append(ElementTree.Element('c3'))  # duplicated tag


Answer:         items = list(etree_iter_paths(root))