import re
from typing import Tuple, Union, Optional, Type, Pattern, Any, List
from xml.etree.ElementTree import Element
from gemma import BearingAbstract, NullNameError, bearing
from gemma.extensions.typing import FactoryType
[docs]class XElm(BearingAbstract[Tuple[str, Union[int, slice]]]):
REGEX: Pattern = re.compile(r"<(.+)>")
NAME_TYPES = [tuple, str]
[docs] def __init__(
self,
name: Union[str, Tuple[str, Union[int, slice]], BearingAbstract],
factory: Optional[Type[FactoryType]] = None,
):
"""
Fetches and places ``xml.etree.ElementTree.Element`` objects from / on parent
Elements.
:param name: tag name, index of Element
:param factory: Type for filling empty bearings during a place.
"""
name_cast: Tuple[str, Union[int, slice]]
if isinstance(name, str):
name_cast = name, 0
elif isinstance(name, BearingAbstract):
name_cast = name.name, 0
else:
name_cast = name
if not isinstance(name_cast[1], (int, slice)):
raise TypeError("index of name must me int or slice")
super().__init__(name_cast, factory)
def __str__(self) -> str:
return f"<{self.tag}, {self.index}>"
@property
def tag(self) -> str:
"""
:return: tag name of element
"""
return self.name[0]
@property
def index(self) -> Union[int, slice]:
"""
:return: index of element (defaults to "0")
"""
if isinstance(self.name, tuple):
return self.name[1]
[docs] def fetch(self, target: Element) -> Union[Element, List[Element]]:
"""
Fetches ``xml.etree.ElementTree.Element`` from ``target``
:param target: ``Element`` to fetch from.
:returns: ``Element with matching name``
Equivalent to ``target.findall(self.tag)[self.index]``
Example:
>>> elm_fetcher = XElm("a")
>>> fetched = elm_fetcher.fetch(root)
>>> fetched.tag
'a'
>>> fetched.attrib
{'one': '1', 'two': '2'}
"""
# need this here for when fallback is testing fetch methods
if not isinstance(target, Element):
raise NullNameError(f"{target} is not xml element")
if self.index == 0:
element = target.find(self.tag)
if element is None:
raise self._null_name_error()
return element
else:
element_list = target.findall(self.tag)
try:
return element_list[self.index]
except IndexError:
raise self._null_name_error()
[docs] def place(self, target: Element, value: Element, **kwargs: dict) -> None:
"""
Inserts ``value`` on ``target``
:param target: ``Element`` to fetch from.
:param value: to insert
:returns: None
Equivalent to ``target.insert(self.index, self.tag)``
Example:
>>> elm_placer = XElm(("b", 0))
>>> elm_placer.place(root, Element("new node"))
>>> from xml.etree.ElementTree import tostring
>>> print(tostring(root))
'<root>...<b four="4" three="3">/...<new node /></root>'
"""
# need this here for when fallback is testing place methods
if not isinstance(target, Element):
raise NullNameError(f"target is not xml element: {target}")
if not isinstance(value, Element):
raise NullNameError(f"value is not xml element: {value}")
try:
kwargs["place_factory"]
except KeyError:
pass
else:
target.insert(-1, value)
return
target_element = self.fetch(target)
target_element.insert(-1, value)
[docs] @classmethod
def name_from_str(cls, text: str) -> Tuple[str, Union[int, slice]]:
"""
Tag and index (if present).
:param text: text to be converted
:return: name, index
Allowed conventions:
- <name>
- <name, 0>
- <name, 1:4>
If no index is passed, ``0`` will be assumed.
"""
name: str = super().name_from_str(text)
try:
name, index = name.split(",")
except ValueError:
return name, 0
name = name.strip(" ")
index = index.strip(" ")
try:
this_slice = _string_to_slice(index)
except ValueError:
return name, int(index)
else:
return name, this_slice
[docs] def init_factory(self) -> FactoryType:
"""
As :func:`BearingAbstract.init_factory`, but will return xml elements
with :func:`BearingAbstract.name` loaded as tag name.
:return: initialized type.
"""
if self.factory_type is None:
raise TypeError()
if issubclass(self.factory_type, Element):
# mypy is complaining here even though this is a valid return type.
return self.factory_type(self.tag) # type: ignore
else:
return super().init_factory()
def _null_name_error(self) -> NullNameError:
"""pre-loaded NullNameError with message"""
return NullNameError(f"could not find index {self.index} for tag {self.tag}")
def _string_to_slice(text: str) -> slice:
slice1, slice2 = text.split(":")
slice1 = slice1.strip(" ")
slice2 = slice2.strip(" ")
if not slice1:
slice1_int = None
else:
slice1_int = int(slice1)
if not slice2:
slice2_int = None
else:
slice2_int = int(slice2)
return slice(slice1_int, slice2_int)
def xbearing(
name: Any,
bearing_classes: Optional[List[Type[BearingAbstract]]] = None,
bearing_classes_extra: Optional[List[Type[BearingAbstract]]] = None,
) -> BearingAbstract:
"""
As :func:`bearing`, but inserts :class:`XElm` at head of
``bearing_classes_extra``
"""
if bearing_classes_extra is None:
bearing_classes_extra = list()
if XElm not in bearing_classes_extra:
bearing_classes_extra.insert(0, XElm)
return bearing(
name,
bearing_classes=bearing_classes,
bearing_classes_extra=bearing_classes_extra,
)