from typing import (
AnyStr,
BinaryIO,
Callable,
Dict,
IO,
Iterable,
Iterator,
Mapping,
Optional,
Tuple,
Type,
TypeVar,
Union,
overload,
)
import xml.etree.ElementTree as ET
from xml.sax.saxutils import escape, quoteattr
from .util import itemize
T = TypeVar("T")
@overload
def load_xml(fp: IO) -> Dict[str, str]:
...
@overload
def load_xml(fp: IO, object_pairs_hook: Type[T]) -> T:
...
@overload
def load_xml(fp: IO, object_pairs_hook: Callable[[Iterator[Tuple[str, str]]], T]) -> T:
...
[docs]def load_xml(fp, object_pairs_hook=dict): # type: ignore[no-untyped-def]
r"""
Parse the contents of the file-like object ``fp`` as an XML properties file
and return a `dict` of the key-value pairs.
Beyond basic XML well-formedness, `load_xml` only checks that the root
element is named "``properties``" and that all of its ``<entry>`` children
have ``key`` attributes. No further validation is performed; if any
``<entry>``\s happen to contain nested tags, the behavior is undefined.
By default, the key-value pairs extracted from ``fp`` are combined into a
`dict` with later occurrences of a key overriding previous occurrences of
the same key. To change this behavior, pass a callable as the
``object_pairs_hook`` argument; it will be called with one argument, a
generator of ``(key, value)`` pairs representing the key-value entries in
``fp`` (including duplicates) in order of occurrence. `load_xml` will then
return the value returned by ``object_pairs_hook``.
:param IO fp: the file from which to read the XML properties document
:param callable object_pairs_hook: class or function for combining the
key-value pairs
:rtype: `dict` or the return value of ``object_pairs_hook``
:raises ValueError: if the root of the XML tree is not a ``<properties>``
tag or an ``<entry>`` element is missing a ``key`` attribute
"""
tree = ET.parse(fp)
return object_pairs_hook(_fromXML(tree.getroot()))
@overload
def loads_xml(s: AnyStr) -> Dict[str, str]:
...
@overload
def loads_xml(fp: IO, object_pairs_hook: Type[T]) -> T:
...
@overload
def loads_xml(
s: AnyStr, object_pairs_hook: Callable[[Iterator[Tuple[str, str]]], T]
) -> T:
...
[docs]def loads_xml(s, object_pairs_hook=dict): # type: ignore[no-untyped-def]
r"""
Parse the contents of the string ``s`` as an XML properties document and
return a `dict` of the key-value pairs.
Beyond basic XML well-formedness, `loads_xml` only checks that the root
element is named "``properties``" and that all of its ``<entry>`` children
have ``key`` attributes. No further validation is performed; if any
``<entry>``\s happen to contain nested tags, the behavior is undefined.
By default, the key-value pairs extracted from ``s`` are combined into a
`dict` with later occurrences of a key overriding previous occurrences of
the same key. To change this behavior, pass a callable as the
``object_pairs_hook`` argument; it will be called with one argument, a
generator of ``(key, value)`` pairs representing the key-value entries in
``s`` (including duplicates) in order of occurrence. `loads_xml` will then
return the value returned by ``object_pairs_hook``.
:param Union[str,bytes] s: the string from which to read the XML properties
document
:param callable object_pairs_hook: class or function for combining the
key-value pairs
:rtype: `dict` or the return value of ``object_pairs_hook``
:raises ValueError: if the root of the XML tree is not a ``<properties>``
tag or an ``<entry>`` element is missing a ``key`` attribute
"""
elem = ET.fromstring(s)
return object_pairs_hook(_fromXML(elem))
def _fromXML(root: ET.Element) -> Iterator[Tuple[str, str]]:
if root.tag != "properties":
raise ValueError("XML tree is not rooted at <properties>")
for entry in root.findall("entry"):
key = entry.get("key")
if key is None:
raise ValueError('<entry> is missing "key" attribute')
yield (key, entry.text or "")
[docs]def dump_xml(
props: Union[Mapping[str, str], Iterable[Tuple[str, str]]],
fp: BinaryIO,
comment: Optional[str] = None,
encoding: str = "UTF-8",
sort_keys: bool = False,
) -> None:
"""
Write a series ``props`` of key-value pairs to a binary filehandle ``fp``
in the format of an XML properties file. The file will include both an XML
declaration and a doctype declaration.
:param props: A mapping or iterable of ``(key, value)`` pairs to write to
``fp``. All keys and values in ``props`` must be `str` values. If
``sort_keys`` is `False`, the entries are output in iteration order.
:param BinaryIO fp: a file-like object to write the values of ``props`` to
:param Optional[str] comment: if non-`None`, ``comment`` will be output as
a ``<comment>`` element before the ``<entry>`` elements
:param str encoding: the name of the encoding to use for the XML document
(also included in the XML declaration)
:param bool sort_keys: if true, the elements of ``props`` are sorted
lexicographically by key in the output
:return: `None`
"""
# This gives type errors <https://github.com/python/typeshed/issues/4793>:
# fptxt = codecs.lookup(encoding).streamwriter(fp, errors='xmlcharrefreplace')
# print('<?xml version="1.0" encoding={0} standalone="no"?>'
# .format(quoteattr(encoding)), file=fptxt)
# for s in _stream_xml(props, comment, sort_keys):
# print(s, file=fptxt)
fp.write(
'<?xml version="1.0" encoding={0} standalone="no"?>\n'.format(
quoteattr(encoding)
).encode(encoding, "xmlcharrefreplace")
)
for s in _stream_xml(props, comment, sort_keys):
fp.write((s + "\n").encode(encoding, "xmlcharrefreplace"))
[docs]def dumps_xml(
props: Union[Mapping[str, str], Iterable[Tuple[str, str]]],
comment: Optional[str] = None,
sort_keys: bool = False,
) -> str:
"""
Convert a series ``props`` of key-value pairs to a `str` containing an XML
properties document. The document will include a doctype declaration but
not an XML declaration.
:param props: A mapping or iterable of ``(key, value)`` pairs to serialize.
All keys and values in ``props`` must be `str` values. If
``sort_keys`` is `False`, the entries are output in iteration order.
:param Optional[str] comment: if non-`None`, ``comment`` will be output as
a ``<comment>`` element before the ``<entry>`` elements
:param bool sort_keys: if true, the elements of ``props`` are sorted
lexicographically by key in the output
:rtype: str
"""
return "".join(s + "\n" for s in _stream_xml(props, comment, sort_keys))
def _stream_xml(
props: Union[Mapping[str, str], Iterable[Tuple[str, str]]],
comment: Optional[str] = None,
sort_keys: bool = False,
) -> Iterator[str]:
yield '<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">'
yield "<properties>"
if comment is not None:
yield "<comment>" + escape(comment) + "</comment>"
for k, v in itemize(props, sort_keys=sort_keys):
yield "<entry key={0}>{1}</entry>".format(quoteattr(k), escape(v))
yield "</properties>"