File: C:/Users/fred/anaconda3/Lib/site-packages/anaconda_navigator/utils/configurations.py
# -*- coding: utf-8 -*-
"""Utilities to simplify configuration files management."""
from __future__ import annotations
__all__ = ['Updater', 'JsonUpdater', 'YamlUpdater']
import abc
import contextlib
import types
import typing
import json
import yaml
KEEP: typing.Any = object()
class Updater(contextlib.AbstractContextManager, metaclass=abc.ABCMeta):
"""
Abstract helper that allows updating configuration files of general formats.
It is used to hide general actions of reading and writing files with corresponding serialization.
This class might also be used as a context manager. On successful exit - content of the configuration will be saved
automatically.
.. code-block:: python
with JsonUpdater(path='/some/file') as configuration:
configuration.content['some_property'] = 'new_value'
# configuration file is saved automatically, unless an exception raised in main body
:param path: Path to the configuration file.
"""
__slots__ = ('__content', '__path')
def __init__(self, path: str) -> None:
"""Initialize new :class:`~Updater` instance."""
self.__path: str = path
self.__content: typing.Any = self._read()
@property
def content(self) -> typing.Any: # noqa: D401
"""Content of the configuration file."""
return self.__content
@content.setter
def content(self, value: str) -> None:
"""Update `content` value."""
self.__content = value
@property
def path(self) -> str: # noqa: D401
"""Path to the configuration."""
return self.__path
@abc.abstractmethod
def _read(self) -> typing.Any:
"""
Read content of the configuration file.
.. note::
You can use value of the :attr:`~Updater.path` to locate the configuration file.
.. warning::
Do not use :attr:`~Updater.content`! This is the value that must be set by current function, and which must
not rely on the previous configuration content.
:return: Content of the configuration file.
"""
@abc.abstractmethod
def _write(self) -> None:
"""
Write new configuration to the file.
.. note::
You can use value of the :attr:`~Updater.path` to locate the configuration file, and
:attr:`~Updater.content` for the content of the configuration that should be saved.
"""
def load(self) -> None:
"""Load configuration from the file."""
self.__content = self._read()
def save(self, path: str = KEEP) -> None:
"""
Save configuration to the file.
:param path: New path to save configuration to. This will also update value of the :attr:`~Updater.path` and
affect subsequent :meth:`~Updater.load` calls (unless there would be an error writing to file).
If not provided - current :attr:`~Updater.path` will be used.
"""
path_backup: str = self.__path
if path is not KEEP:
self.__path = path
try:
self._write()
except BaseException:
self.__path = path_backup
raise
def __enter__(self) -> Updater:
"""Enter into the configuration updating context."""
return self
def __exit__(
self,
exc_type: typing.Optional[typing.Type[BaseException]],
exc_val: typing.Optional[BaseException],
exc_tb: typing.Optional[types.TracebackType],
) -> None:
"""
Exit from the configuration updating context.
If no exception raised - configuration would be saved.
"""
if exc_val is None:
self.save()
class EncodingAwareUpdater(Updater, metaclass=abc.ABCMeta):
"""
Modified :class:`~Updater`, which also allows setting encoding to read configuration with.
:param path: Path to the configuration file.
:param encoding: Encoding to use for the configuration file.
"""
__slots__ = ('__encoding',)
def __init__(self, path: str, encoding: typing.Optional[str] = None) -> None:
"""Initialize new :class:`~EncodingAwareUpdater` instance."""
self.__encoding: typing.Optional[str] = encoding
super().__init__(path=path)
@property
def encoding(self) -> typing.Optional[str]: # noqa: D401
"""Current encoding of the configuration file."""
return self.__encoding
def save(self, path: str = KEEP, encoding: typing.Optional[str] = KEEP) -> None: # pylint: disable=arguments-differ
"""
Save configuration to the file.
:param path: New path to save configuration to. This will also update value of the
:attr:`~EncodingAwareUpdater.path` and affect subsequent :meth:`~Updater.load` calls (unless there
would be an error writing to file).
If not provided - current :attr:`~EncodingAwareUpdater.path` will be used.
:param encoding: New encoding to save configuration file with. This also updates
:attr:`~EncodingAwareUpdater.encoding` in the way similar to `path` argument.
"""
encoding_backup: typing.Optional[str] = self.__encoding
if encoding is not KEEP:
self.__encoding = encoding
try:
super().save(path=path)
except BaseException:
self.__encoding = encoding_backup
raise
class JsonUpdater(EncodingAwareUpdater):
"""
Configuration updater, that supports configuration files with JSON content.
:param path: Path to the configuration file.
:param encoding: Encoding to use for the configuration file.
"""
__slots__ = ()
def _read(self) -> typing.Any:
"""
Read content of the configuration file.
:return: Content of the configuration file.
"""
stream: typing.TextIO
with open(self.path, 'rt', encoding=self.encoding) as stream:
return json.load(stream)
def _write(self) -> None:
"""Write new configuration to the file."""
stream: typing.TextIO
with open(self.path, 'wt', encoding=self.encoding) as stream:
json.dump(self.content, stream)
class YamlUpdater(EncodingAwareUpdater):
"""
Configuration updater, that supports configuration files with YAML content.
:param path: Path to the configuration file.
:param encoding: Encoding to use for the configuration file.
"""
__slots__ = ()
def _read(self) -> typing.Any:
"""
Read content of the configuration file.
:return: Content of the configuration file.
"""
stream: typing.TextIO
with open(self.path, 'rt', encoding=self.encoding) as stream:
return yaml.load(stream, yaml.FullLoader) # nosec
def _write(self) -> None:
"""Write new configuration to the file."""
stream: typing.TextIO
with open(self.path, 'wt', encoding=self.encoding) as stream:
yaml.dump(self.content, stream)