Source code for confwire.build

"""Build Python objects from ``"type"``-tagged configuration dictionaries."""

from importlib import import_module
from typing import Any

#: Dotted ``"type"`` paths that are never allowed to be built, regardless
#: of caller-supplied ``blocked_types``, since they allow arbitrary
#: code/command execution.
DEFAULT_BLOCKED_TYPES = frozenset(
    {
        "os.system",
        "os.popen",
        "os.execv",
        "os.execve",
        "os.execvp",
        "os.execvpe",
        "os.spawnl",
        "os.spawnv",
        "os.remove",
        "os.unlink",
        "os.rmdir",
        "shutil.rmtree",
        "subprocess.Popen",
        "subprocess.call",
        "subprocess.run",
        "subprocess.check_call",
        "subprocess.check_output",
        "builtins.eval",
        "builtins.exec",
        "eval",
        "exec",
    }
)


[docs]def build_from_config(config_dict: dict, base_package: str | None = None, blocked_types=None): """Recursively build an object from a configuration dictionary. ``config_dict`` must contain a ``"type"`` key holding a dotted import path to the callable to build, either absolute (e.g. ``"pkg.module.Class"``) or relative (e.g. ``".module.Class"``, resolved against ``base_package``). All other keys are passed as keyword arguments to that callable. Any value that is itself a dict is walked via :func:`build_value` so nested objects (at any depth) are built before being passed to their parent. Args: config_dict (dict): The configuration to build from. If it has no ``"type"`` key, it is returned unchanged (treated as a plain data dict rather than an object description). base_package (str, optional): Base package to resolve a relative ``"type"`` path against. Required only when ``"type"`` starts with a dot. Default: None. blocked_types (set, optional): Dotted type paths that must never be built (e.g. ``"os.system"``). Defaults to :data:`DEFAULT_BLOCKED_TYPES` when not provided. Returns: Any: The instance constructed from ``config_dict``, or ``config_dict`` itself (copied) when it has no ``"type"`` key. Raises: PermissionError: If ``"type"`` matches an entry in the blocklist. ImportError: If ``"type"`` is a relative path and no ``base_package`` was supplied. ModuleNotFoundError: If the module in ``"type"`` cannot be found. AttributeError: If the module exists but the class/attribute does not. Examples: >>> build_from_config( ... {"type": "botocore.config.Config", "region_name": "us-east-1"} ... ) <botocore.config.Config object at ...> """ data = config_dict.copy() if "type" not in data: return data import_path = data["type"] blocklist = DEFAULT_BLOCKED_TYPES if blocked_types is None else blocked_types if import_path in blocklist: raise PermissionError(f"Building objects of type '{import_path}' is not allowed.") module = ".".join(import_path.split(".")[:-1]) name_of_object = import_path.split(".")[-1] del data["type"] if import_path.startswith("."): if not base_package: raise ImportError( f"Cannot resolve relative type '{import_path}' without a " "base_package to resolve it against. Pass base_package= " "to build_from_config()." ) resolved_module = import_module(module, package=base_package) else: resolved_module = import_module(module) instance = getattr(resolved_module, name_of_object) kwargs = {key: build_value(value, base_package, blocked_types) for key, value in data.items()} return instance(**kwargs)
[docs]def build_value(value: Any, base_package: str | None = None, blocked_types: set | None = None): """ Recursively build any dict value found while building a config. Used by :func:`build_from_config` to walk keyword-argument values: a dict containing ``"type"`` is built via :func:`build_from_config`, a plain dict without ``"type"`` is walked key by key looking for buildable objects nested inside it, and any other value (str, int, list, etc.) is returned unchanged. Args: value (Any): The value to inspect and, if applicable, build. base_package (str): Base package used to resolve relative ``"type"`` paths, forwarded from :func:`build_from_config`. blocked_types (set): Dotted type paths that must never be built, forwarded from :func:`build_from_config`. Returns: Any: The built object if ``value`` (or something nested inside it) describes one, otherwise ``value`` unchanged. Examples: >>> build_value({"type": "botocore.config.Config"}, None, None) <botocore.config.Config object at ...> >>> build_value({"a": 1}, None, None) {'a': 1} """ if isinstance(value, dict): if "type" in value: return build_from_config(value, base_package=base_package, blocked_types=blocked_types) return {key: build_value(val, base_package, blocked_types) for key, val in value.items()} return value