from collections.abc import Mapping as MappingABC
from typing import (
Any,
BinaryIO,
Dict,
IO,
Iterable,
Iterator,
Mapping,
MutableMapping,
Optional,
Set,
TextIO,
Tuple,
TypeVar,
Union,
)
from .reading import load
from .writing import dump
from .xmlprops import dump_xml, load_xml
T = TypeVar("T")
[docs]class Properties(MutableMapping[str, str]):
"""
A port of |java8properties|_ that tries to match its behavior as much as is
Pythonically possible. `Properties` behaves like a normal
`~collections.abc.MutableMapping` class (i.e., you can do ``props[key] =
value`` and so forth), except that it may only be used to store `str`
values.
Two `Properties` instances compare equal iff both their key-value pairs and
:attr:`defaults` attributes are equal. When comparing a `Properties`
instance to any other type of mapping, only the key-value pairs are
considered.
.. versionchanged:: 0.5.0
`Properties` instances can now compare equal to `dict`\\s and other
mapping types
:param data: A mapping or iterable of ``(key, value)`` pairs with which to
initialize the `Properties` instance. All keys and values in ``data``
must be text strings.
:type data: mapping or `None`
:param Optional[Properties] defaults: a set of default properties that will
be used as fallback for `getProperty`
.. |java8properties| replace:: Java 8's ``java.util.Properties``
.. _java8properties: https://docs.oracle.com/javase/8/docs/api/java/util/Properties.html
"""
def __init__(
self,
data: Union[None, Mapping[str, str], Iterable[Tuple[str, str]]] = None,
defaults: Optional["Properties"] = None,
) -> None:
self.data: Dict[str, str] = {}
#: A `Properties` subobject used as fallback for `getProperty`. Only
#: `getProperty`, `propertyNames`, `stringPropertyNames`, and `__eq__`
#: use this attribute; all other methods (including the standard
#: mapping methods) ignore it.
self.defaults = defaults
if data is not None:
self.update(data)
def __getitem__(self, key: str) -> str:
return self.data[key]
def __setitem__(self, key: str, value: str) -> None:
self.data[key] = value
def __delitem__(self, key: str) -> None:
del self.data[key]
def __iter__(self) -> Iterator[str]:
return iter(self.data)
def __len__(self) -> int:
return len(self.data)
def __repr__(self) -> str:
return (
"{0.__module__}.{0.__name__}({1.data!r}, defaults={1.defaults!r})".format(
type(self), self
)
)
def __eq__(self, other: Any) -> bool:
if isinstance(other, Properties):
return self.data == other.data and self.defaults == other.defaults
elif isinstance(other, MappingABC):
return dict(self) == other
else:
return NotImplemented
[docs] def getProperty(
self, key: str, defaultValue: Optional[T] = None
) -> Union[str, T, None]:
"""
Fetch the value associated with the key ``key`` in the `Properties`
instance. If the key is not present, `defaults` is checked, and then
*its* `defaults`, etc., until either a value for ``key`` is found or
the next `defaults` is `None`, in which case `defaultValue` is
returned.
:param str key: the key to look up the value of
:param Any defaultValue: the value to return if ``key`` is not found in
the `Properties` instance
:rtype: str (if ``key`` was found)
"""
try:
return self[key]
except KeyError:
if self.defaults is not None:
return self.defaults.getProperty(key, defaultValue)
else:
return defaultValue
[docs] def load(self, inStream: IO) -> None:
"""
Update the `Properties` instance with the entries in a ``.properties``
file or file-like object.
``inStream`` may be either a text or binary filehandle, with or without
universal newlines enabled. If it is a binary filehandle, its contents
are decoded as Latin-1.
.. versionchanged:: 0.5.0
Invalid ``\\uXXXX`` escape sequences will now cause an
`InvalidUEscapeError` to be raised
:param IO inStream: the file from which to read the ``.properties``
document
:return: `None`
:raises InvalidUEscapeError: if an invalid ``\\uXXXX`` escape sequence
occurs in the input
"""
self.data.update(load(inStream))
[docs] def propertyNames(self) -> Iterator[str]:
r"""
Returns a generator of all distinct keys in the `Properties` instance
and its `defaults` (and its `defaults`\’s `defaults`, etc.) in
unspecified order
:rtype: Iterator[str]
"""
for k in self.data:
yield k
if self.defaults is not None:
for k in self.defaults.propertyNames():
if k not in self.data:
yield k
[docs] def setProperty(self, key: str, value: str) -> None:
"""Equivalent to ``self[key] = value``"""
self[key] = value
[docs] def store(self, out: TextIO, comments: Optional[str] = None) -> None:
"""
Write the `Properties` instance's entries (in unspecified order) in
``.properties`` format to ``out``, including the current timestamp.
:param TextIO out: A file-like object to write the properties to. It
must have been opened as a text file with a Latin-1-compatible
encoding.
:param Optional[str] comments: If non-`None`, ``comments`` will be
written to ``out`` as a comment before any other content
:return: `None`
"""
dump(self.data, out, comments=comments)
[docs] def stringPropertyNames(self) -> Set[str]:
r"""
Returns a `set` of all keys in the `Properties` instance and its
`defaults` (and its `defaults`\ ’s `defaults`, etc.)
:rtype: Set[str]
"""
names = set(self.data)
if self.defaults is not None:
names.update(self.defaults.stringPropertyNames())
return names
[docs] def loadFromXML(self, inStream: IO) -> None:
"""
Update the `Properties` instance with the entries in the XML properties
file ``inStream``.
Beyond basic XML well-formedness, `loadFromXML` only checks that the
root element is named ``properties`` and that all of its ``entry``
children have ``key`` attributes; no further validation is performed.
:param IO inStream: the file from which to read the XML properties
document
:return: `None`
:raises ValueError: if the root of the XML tree is not a
``<properties>`` tag or an ``<entry>`` element is missing a ``key``
attribute
"""
self.data.update(load_xml(inStream))
[docs] def storeToXML(
self,
out: BinaryIO,
comment: Optional[str] = None,
encoding: str = "UTF-8",
) -> None:
"""
Write the `Properties` instance's entries (in unspecified order) in XML
properties format to ``out``.
:param BinaryIO out: a file-like object to write the properties 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)
:return: `None`
"""
dump_xml(self.data, out, comment=comment, encoding=encoding)
[docs] def copy(self) -> "Properties":
"""
.. versionadded:: 0.5.0
Create a shallow copy of the mapping. The copy's `defaults` attribute
will be the same instance as the original's `defaults`.
"""
return type(self)(self.data, self.defaults)