import inspect
from functools import reduce
from types import MappingProxyType
from typing import Dict, Generic, List, Mapping, Sequence, TypeVar, cast
from . import types # Structural/Abstract types
from .errors import (
AlreadyRegisteredAugmentation,
InvalidAugmentationName,
UndefinedProfile,
)
from .profile import Profile, ProfileAugmentation
from .transformations import apply
T = TypeVar("T")
EMPTY = MappingProxyType({}) # type: ignore
[docs]
class BaseTranslator(Generic[T]):
"""Translator object that follows the public API defined in
:class:`ini2toml.types.Translator`. See :doc:`/dev-guide` for a quick explanation of
concepts such as plugins, profiles, profile augmentations, etc.
Arguments
---------
ini_loads_fn:
function to convert the ``.ini/.cfg`` file into an :class:`intermediate
representation <ini2toml.intermediate_repr.IntermediateRepr>` object.
Possible values for this argument include:
- :func:`ini2toml.drivers.configparser.parse` (when comments can be simply
removed)
- :func:`ini2toml.drivers.configupdater.parse` (when you wish to preserve
comments in the TOML output)
toml_dumps_fn:
function to convert the :class:`intermediate representation
<ini2toml.intermediate_repr.IntermediateRepr>` object into (ideally)
a TOML string.
If you don't exactly need a TOML string (maybe you want your TOML to
be represented by :class:`bytes` or simply the equivalent :obj:`dict`) you can
also pass a ``Callable[[IntermediateRepr], T]`` function for any desired ``T``.
Possible values for this argument include:
- :func:`ini2toml.drivers.lite_toml.convert` (when comments can be simply
removed)
- :func:`ini2toml.drivers.full_toml.convert` (when you wish to preserve
comments in the TOML output)
- :func:`ini2toml.drivers.plain_builtins.convert` (when you wish to retrieve a
:class:`dict` equivalent to the TOML, instead of string with the TOML syntax)
plugins:
list of plugins activation functions. By default no plugin will be activated.
profiles:
list of profile objects, by default no profile will be pre-loaded (plugins can
still add them).
profile_augmentations:
list of profile augmentations. By default no profile augmentation will be
preloaded (plugins can still add them)
ini_parser_opts:
syntax options for parsing ``.ini/.cfg`` files (see
:mod:`~configparser.ConfigParser` and :mod:`~configupdater.ConfigUpdater`).
By default it uses the standard configuration of the selected parser (depending
on the choice of ``ini_loads_fn``).
Tip
---
Most of the times the usage of :class:`~ini2toml.translator.Translator`
(or its deterministic variants ``LiteTranslator``, ``FullTranslator``) is preferred
over :class:`~ini2toml.base_translator.BaseTranslator` (unless you are vendoring
``ini2toml`` and wants to reduce the number of files included in your project).
"""
profiles: Dict[str, types.Profile]
plugins: List[types.Plugin]
def __init__(
self,
ini_loads_fn: types.IniLoadsFn,
toml_dumps_fn: types.IReprCollapseFn[T],
plugins: Sequence[types.Plugin] = (),
profiles: Sequence[types.Profile] = (),
profile_augmentations: Sequence[types.ProfileAugmentation] = (),
ini_parser_opts: Mapping = EMPTY,
):
self.plugins = _deduplicate_plugins(plugins)
self.ini_parser_opts = ini_parser_opts
self.profiles = {p.name: p for p in profiles}
self.augmentations: Dict[str, types.ProfileAugmentation] = {
(p.name or p.fn.__name__): p for p in profile_augmentations
}
self._loads_fn = ini_loads_fn
self._dumps_fn = toml_dumps_fn
for activate in self.plugins:
activate(self)
[docs]
def loads(self, text: str) -> types.IntermediateRepr:
return self._loads_fn(text, self.ini_parser_opts)
[docs]
def dumps(self, irepr: types.IntermediateRepr) -> T:
return self._dumps_fn(irepr)
def __getitem__(self, profile_name: str) -> types.Profile:
"""Retrieve an existing profile (or create a new one)."""
if profile_name not in self.profiles:
profile = Profile(profile_name)
if self.ini_parser_opts:
profile = profile.replace(ini_parser_opts=self.ini_parser_opts)
self.profiles[profile_name] = profile
return self.profiles[profile_name]
[docs]
def augment_profiles(
self,
fn: types.ProfileAugmentationFn,
active_by_default: bool = False,
name: str = "",
help_text: str = "",
):
"""Register a profile augmentation function to be called after the
profile is selected and before the actual translation (see :doc:`/dev-guide`).
"""
name = (name or fn.__name__).strip()
InvalidAugmentationName.check(name)
AlreadyRegisteredAugmentation.check(name, fn, self.augmentations)
help_text = help_text or fn.__doc__ or ""
obj = ProfileAugmentation(fn, active_by_default, name, help_text)
self.augmentations[name] = obj
def _add_augmentations(
self, profile: types.Profile, explicit_activation: Mapping[str, bool] = EMPTY
) -> types.Profile:
for aug in self.augmentations.values():
if aug.is_active(explicit_activation.get(aug.name)):
aug.fn(profile)
return profile
[docs]
def translate(
self,
ini: str,
profile_name: str,
active_augmentations: Mapping[str, bool] = EMPTY,
) -> T:
UndefinedProfile.check(profile_name, list(self.profiles.keys()))
profile = cast(Profile, self[profile_name])._copy()
# ^--- avoid permanent changes and conflicts with duplicated augmentation
self._add_augmentations(profile, active_augmentations)
ini = reduce(apply, profile.pre_processors, ini)
irepr = self.loads(ini)
irepr = reduce(apply, profile.intermediate_processors, irepr)
toml = self.dumps(irepr)
return reduce(apply, profile.post_processors, toml)
def _deduplicate_plugins(plugins: Sequence[types.Plugin]) -> List[types.Plugin]:
deduplicated = {_plugin_name(p): p for p in plugins}
return list(deduplicated.values())
def _plugin_name(plugin: types.Plugin) -> str:
mod = inspect.getmodule(plugin)
modname = getattr(mod, "__name__", str(mod))
name = getattr(plugin, "__qualname__", getattr(plugin, "__name__", "**plugin**"))
return f"{modname}:{name}"