view configmix/variables.py @ 654:0d6673d06c2c

Add support for using "tomllib" (in Python's stdlib since 3.11) and "tomli" TOML packages. They are preferred if they are found to be installed. But note that the declared dependency for the "toml" extra nevertheless is the "toml" package. Because it is available for all supported Python versions. So use Python 3.11+ or install "tomli" manually if you want to use the alternate packages.
author Franz Glasner <fzglas.hg@dom66.de>
date Thu, 19 May 2022 22:10:59 +0200
parents f454889e41fa
children 57fe110c50c8
line wrap: on
line source

# -*- coding: utf-8 -*-
# :-
# :Copyright: (c) 2015-2022, Franz Glasner. All rights reserved.
# :License:   BSD-3-Clause. See LICENSE.txt for details.
# :-
"""Variable interpolation: implementation of namespaces and filters

"""

from __future__ import division, absolute_import, print_function


__all__ = []


import os
import platform
from functools import wraps

from .compat import PY2, native_os_str_to_text, text_to_native_os_str, u
from .constants import REF_NAMESPACE, NONE_FILTER, EMPTY_FILTER


_MARKER = object()


def _envlookup(name, default=_MARKER):
    """Lookup an environment variable"""
    try:
        return native_os_str_to_text(os.environ[name])
    except KeyError:
        if default is _MARKER:
            raise
        else:
            return default


def _oslookup(name, default=_MARKER):
    """Lookup some process and/or OS state """
    if name == "cwd":
        return native_os_str_to_text(os.getcwd())
    elif name == "node":
        return native_os_str_to_text(platform.node())
    else:
        if default is _MARKER:
            raise KeyError("key %r not found in the namespace" % name)
        else:
            return default


def _pylookup(name, default=_MARKER):
    """Lookup Python specific information"""
    if name == "version":
        return u(platform.python_version())
    elif name == "implementation":
        return u(platform.python_implementation())
    elif name == "version_maj_min":
        t = platform.python_version_tuple()
        return u('.'.join(t[:2]))
    elif name == "version_maj":
        t = platform.python_version_tuple()
        return u(t[0])
    else:
        if default is _MARKER:
            raise KeyError("variable %r not found in namespace PY" % name)
        else:
            return default


_varns_registry = {}
"""Namespace registry"""


def add_varns(name, fn):
    """Register a new variable namespace `name` and it's implementing
    function `fn`

    ..note:: This function checks that `name` is not the special
             namespace :data:`~configmix.constants.REF_NAMESPACE`.

    """
    if name == REF_NAMESPACE:
        raise ValueError(
            "the special namespace `%s' is not allowed here" % REF_NAMESPACE)
    _varns_registry[name] = fn


def lookup_varns(name):
    """Lookup the variable namespace `name` and return it's implementing
    function

    :param str name: the namespace name
    :returns: the implementing function
    :exception KeyError: if the namespace `name` doesn't exist

    ..note:: This function checks that `name` is not the special
             namespace :data:`~configmix.constants.REF_NAMESPACE`.

    """
    if name == REF_NAMESPACE:
        raise ValueError(
            "the special namespace `%s' is not allowed here" % REF_NAMESPACE)
    return _varns_registry[_normalized(name)]


_filter_registry = {}
"""Filter registry"""


def add_filter(name, fn):
    """Register a variable filter function with name `name` and
    implementation `fn`

    """
    _filter_registry[_normalized(name)] = fn


def lookup_filter(name):
    """Lookup a variable filter with name `name` and return it's
    implementation function

    :param str name: the logical filter name
    :returns: the implementing filter function
    :exception KeyError: if the filter cannot be found

    """
    return _filter_registry[_normalized(name)]


def filter(name):
    """Decorator for a filter function.

    Example usage::

       @filter("myfilter")
       def myfilter_impl(appconfig, variable_value):
           filtered_value = ...
           return filtered_value


    """

    def _decorator(f):

        @wraps(f)
        def _f(appconfig, v):
            return f(appconfig, v)

        add_filter(name, _f)
        return _f

    return _decorator


def _normalized(name):
    return name.replace('-', '_')


#
# Some pre-defined filter functions
#
if PY2:

    @filter("urlquote")
    def urlquote(config, v):
        """Filter function to replace all special characters in string `v`
        using the ``%xx`` escape

        """
        from urllib import quote
        return quote(v.encode("utf-8"), safe=b"").decode("utf-8")


    @filter("urlquote_plus")
    def urlquote_plus(config, v):
        """Filter function to replace all special characters (including
        spaces) in string `v` using the ``%xx`` escape

        """
        from urllib import quote_plus
        return quote_plus(v.encode("utf-8"), safe=b"").decode("utf-8")

else:

    @filter("urlquote")
    def urlquote(config, v):
        """Filter function to replace all special characters in string `v`
        using the ``%xx`` escape

        """
        from urllib.parse import quote
        return quote(v, safe="")


    @filter("urlquote_plus")
    def urlquote_plus(config, v):
        """Filter function to replace all special characters (including
        spaces) in string `v` using the ``%xx`` escape

        """
        from urllib.parse import quote_plus
        return quote_plus(v, safe="")


@filter("saslprep")
def saslprep(config, v):
    """Filter function to perform a `SASLprep` according to :rfc:`4013` on
    `v`.

    This is a Stringprep Profile for usernames and passwords

    """
    import passlib.utils
    return passlib.utils.saslprep(v)


@filter("normpath")
def normpath_impl(config, v):
    """Implementation of the `normpath` filter function"""
    return os.path.normpath(v)


@filter("abspath")
def abspath_impl(config, v):
    """Implementation of the `abspath` filter function"""
    return os.path.abspath(v)


@filter("posixpath")
def posixpath_impl(config, v):
    """Implementation of the `posixpath` filter function"""
    return v.replace(u('\\'), u('/'))


@filter("lower")
def lower_impl(config, v):
    """Implementation of the `lower` filter function"""
    return v.lower()


@filter("upper")
def upper_impl(config, v):
    """Implementation of the `upper` filter function"""
    return v.upper()


@filter(text_to_native_os_str(NONE_FILTER, encoding="ascii"))
def None_filter_impl(config, v):
    """Identity.

    The `None` filter is just a marker to not throw :exc:`KeyError`
    but return `None`. It is a no-op within the filter-chain itself.

    """
    return v


@filter(text_to_native_os_str(EMPTY_FILTER, encoding="ascii"))
def Empty_filter_impl(config, v):
    """Identity.

    The `Empty` filter is just a marker to not throw :exc:`KeyError`
    but return the empty string. It is a no-op within the filter-chain itself.

    """
    return v


# Register the default namespaces
add_varns("ENV", _envlookup)
add_varns("OS", _oslookup)
add_varns("PY", _pylookup)
try:
    from .extras import aws
except ImportError:
    pass
else:
    add_varns("AWS", aws._awslookup)