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 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 is_processing_instruction_node(obj: Any) -> bool:
    return hasattr(obj, 'tag') and callable(obj.tag) and obj.tag.__name__ == 'ProcessingInstruction'
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
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 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
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
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_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_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 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
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))
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_attributes(obj: Any) -> Optional[Dict[str, Any]]:
    return obj.attrib if is_element_node(obj) else None
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 is_etree_element(obj: Any) -> bool:
    return hasattr(obj, 'tag') and hasattr(obj, 'attrib') and hasattr(obj, 'text')
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 is_document_node(obj: Any) -> bool:
    return hasattr(obj, 'getroot') and hasattr(obj, 'parse') and hasattr(obj, 'iter')
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 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
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()


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
        result = [root, TextNode('text1\n', root),
                  root[0], TextNode('text2', root[0]), root[1],
                  root[2], root[2][0], TextNode('text3', root[2][0])]

        self.assertListEqual(list(etree_iter_nodes(root)), result)
        self.assertListEqual(list(etree_iter_nodes(root, with_root=False)), result[1:])

        with patch.multiple(DummyXsdType, has_mixed_content=lambda x: True):
            xsd_type = DummyXsdType()
            typed_root = TypedElement(root, xsd_type, 'text1')
            self.assertListEqual(list(etree_iter_nodes(typed_root)), result)

        comment = ElementTree.Comment('foo')
        root[1].append(comment)
        self.assertListEqual(list(etree_iter_nodes(root)),
                             result[:5] + [comment] + result[5:])

    def test_elem_iter_strings_function(self):
        root = ElementTree.XML('<A>text1\n<B1>text2</B1>tail1<B2/><B3><C1>text3</C1></B3>tail2</A>')
        result = ['text1\n', 'text2', 'tail1', 'tail2', 'text3']
        self.assertListEqual(list(etree_iter_strings(root)), result)

        with patch.multiple(DummyXsdType, has_mixed_content=lambda x: True):
            xsd_type = DummyXsdType()
            typed_root = TypedElement(elem=root, xsd_type=xsd_type, value='text1')
            self.assertListEqual(list(etree_iter_strings(typed_root)), result)

        norm_result = ['text1', 'text2', 'tail1', 'tail2', 'text3']
        with patch.multiple(DummyXsdType, is_element_only=lambda x: True):
            xsd_type = DummyXsdType()
            typed_root = TypedElement(elem=root, xsd_type=xsd_type, value='text1')
            self.assertListEqual(list(etree_iter_strings(typed_root)), norm_result)

            comment = ElementTree.Comment('foo')
            root[1].append(comment)
            self.assertListEqual(list(etree_iter_strings(typed_root)), norm_result)

        self.assertListEqual(list(etree_iter_strings(root)), result)

    def test_etree_deep_equal_function(self):
        root = ElementTree.XML('<A><B1>10</B1><B2 max="20"/>end</A>')
        self.assertTrue(etree_deep_equal(root, root))

        elem = ElementTree.XML('<A><B1>11</B1><B2 max="20"/>end</A>')
        self.assertFalse(etree_deep_equal(root, elem))

        elem = ElementTree.XML('<A><B1>10</B1>30<B2 max="20"/>end</A>')
        self.assertFalse(etree_deep_equal(root, elem))

        elem = ElementTree.XML('<A xmlns:ns="tns"><B1>10</B1><B2 max="20"/>end</A>')
        self.assertTrue(etree_deep_equal(root, elem))

        elem = ElementTree.XML('<A><B1>10</B1><B2 max="20"><C1/></B2>end</A>')
        self.assertFalse(etree_deep_equal(root, elem))

    def test_match_element_node_function(self):
        elem = ElementTree.Element('alpha')
        empty_tag_elem = ElementTree.Element('')
        self.assertTrue(match_element_node(elem))
        self.assertTrue(match_element_node(elem, '*'))
        self.assertFalse(match_element_node(empty_tag_elem, '*'))
        with self.assertRaises(ValueError):
            match_element_node(elem, '**')
        with self.assertRaises(ValueError):
            match_element_node(elem, '*:*:*')
        with self.assertRaises(ValueError):
            match_element_node(elem, 'foo:*')
        self.assertFalse(match_element_node(empty_tag_elem, 'foo:*'))
        self.assertFalse(match_element_node(elem, '{foo}*'))

        with patch.multiple(DummyXsdType, has_mixed_content=lambda x: True):
            xsd_type = DummyXsdType()
            typed_elem = TypedElement(elem=elem, xsd_type=xsd_type, value='text1')
            self.assertTrue(match_element_node(typed_elem, '*'))

    def test_match_attribute_node_function(self):
        attr = AttributeNode('a1', '10', parent=None)
        self.assertTrue(match_attribute_node(attr, '*'))
        self.assertTrue(match_attribute_node(TypedAttribute(attr, None, 10), 'a1'))
        with self.assertRaises(ValueError):
            match_attribute_node(attr, '**')
        with self.assertRaises(ValueError):
            match_attribute_node(attr, '*:*:*')
        with self.assertRaises(ValueError):
            match_attribute_node(attr, 'foo:*')
        self.assertTrue(match_attribute_node(attr, '*:a1'))
        self.assertFalse(match_attribute_node(attr, '{foo}*'))
        self.assertTrue(match_attribute_node(AttributeNode('{foo}a1', '10'), '{foo}*'))

        attr = AttributeNode('{http://xpath.test/ns}a1', '10', parent=None)
        self.assertTrue(match_attribute_node(attr, '*:a1'))

    def test_is_comment_node_function(self):
        comment = ElementTree.Comment('nothing important')
        self.assertTrue(is_comment_node(comment))
        self.assertFalse(is_comment_node(self.elem))

    def test_is_document_node_function(self):
        document = ElementTree.parse(io.StringIO('<A/>'))
        self.assertTrue(is_document_node(document))
        self.assertFalse(is_document_node(self.elem))

    def test_is_processing_instruction_node_function(self):
        pi = ElementTree.ProcessingInstruction('action', 'nothing to do')
        self.assertTrue(is_processing_instruction_node(pi))
        self.assertFalse(is_processing_instruction_node(self.elem))

    def test_node_attributes_function(self):
        self.assertEqual(node_attributes(self.elem), self.elem.attrib)
        self.assertIsNone(node_attributes('a text node'))

    def test_node_base_uri_function(self):
        xml_test = '<A xmlns:xml="http://www.w3.org/XML/1998/namespace" xml:base="/" />'
        self.assertEqual(node_base_uri(ElementTree.XML(xml_test)), '/')
        document = ElementTree.parse(io.StringIO(xml_test))
        self.assertEqual(node_base_uri(document), '/')
        self.assertIsNone(node_base_uri(self.elem))
        self.assertIsNone(node_base_uri('a text node'))

    def test_node_document_uri_function(self):

Answer:         self.assertIsNone(node_document_uri(self.elem))