Source code for file_config.utils

# Copyright (c) 2019 Stephen Bunn <stephen@bunn.io>
# ISC License <https://choosealicense.com/licenses/isc>

import re
import base64
import typing
import itertools
import collections
from enum import Enum
from functools import lru_cache

import attr

from .constants import CONFIG_KEY, REGEX_TYPE_NAME


[docs]class Types(Enum): """ An enum which keeps a record of top-level type grouping names. .. note:: Specifically implemented for ease of use with the ``TYPE_MAPPINGS`` and helps reduce the use of **magic** strings used throughout the ``is_x_type`` methods. """ NULL = "null" BOOL = "bool" BYTES = "bytes" INTEGER = "integer" NUMBER = "number" STRING = "string" ARRAY = "array" OBJECT = "object" ENUM = "enum" UNION = "union"
COMPILED_PATTERN_TYPE = type(re.compile("")) TYPE_MAPPINGS = { "builtins": { Types.NULL: (type(None),), Types.BOOL: (bool,), Types.BYTES: (bytes, bytearray), Types.INTEGER: (int,), Types.NUMBER: (float,), Types.STRING: (str,), Types.ARRAY: (list, tuple, set, frozenset), Types.OBJECT: (dict,), Types.ENUM: (Enum,), }, "typing": { Types.STRING: (typing.AnyStr,), Types.ARRAY: (typing.List, typing.Tuple, typing.Set, typing.FrozenSet), Types.OBJECT: (typing.Dict, typing.ChainMap, typing.NamedTuple), Types.UNION: (typing.Union,), }, "collections": { Types.STRING: (collections.UserString,), Types.ARRAY: (collections.UserList,), Types.OBJECT: ( collections.ChainMap, collections.Counter, collections.OrderedDict, collections.UserDict, ), }, }
[docs]def _get_types(type_): """ Gathers all types within the ``TYPE_MAPPINGS`` for a specific ``Types`` value. :param Types type_: The type to retrieve :return: All types within the ``TYPE_MAPPINGS`` :rtype: list """ return list( itertools.chain.from_iterable( map(lambda x: TYPE_MAPPINGS[x].get(type_, []), TYPE_MAPPINGS) ) )
[docs]def encode_bytes(bytes_): """ Encodes some given bytes into base64 using utf-8. :param bytes bytes_: The bytes to encode :return: The bytes encoded base64 string :rtype: str """ return base64.encodebytes(bytes_).decode("utf-8")
[docs]def decode_bytes(string): """ Decodes a given base64 string into bytes. :param str string: The string to decode :return: The decoded bytes :rtype: bytes """ if is_string_type(type(string)): string = bytes(string, "utf-8") return base64.decodebytes(string)
[docs]def is_config_var(var): """ Checks if the given value is a valid ``file_config.var``. :param var: The value to check :return: True if the given value is a var, otherwise False :rtype: bool """ return ( isinstance(var, (attr._make.Attribute, attr._make._CountingAttr)) and hasattr(var, "metadata") and CONFIG_KEY in var.metadata )
[docs]def is_config_type(type_): """ Checks if the given type is ``file_config.config`` decorated. :param type_: The type to check :return: True if the type is config decorated, otherwise False :rtype: bool """ return ( isinstance(type_, type) and hasattr(type_, "__attrs_attrs__") and hasattr(type_, CONFIG_KEY) )
[docs]def is_config(config_instance): """ Checks if the given value is a ``file_config.config`` instance. :param config_instance: The instance to check :return: True if the given instance is a config, otherwise False :rtype: bool """ return isinstance(config_instance, object) and is_config_type(type(config_instance))
[docs]@lru_cache() def is_compiled_pattern(compiled_pattern): """ Checks if the given value is a compiled regex pattern. :param compiled_pattern: The value to check :return: True if the given value is a compiled regex pattern, otherwise False :rtype: bool """ return isinstance(compiled_pattern, COMPILED_PATTERN_TYPE)
[docs]@lru_cache() def is_builtin_type(type_): """ Checks if the given type is a bulitin type. :param type_: The type to check :return: True if the type is a bulitin, otherwise False :rtype: bool """ # NOTE: supported builtin types return isinstance(type_, type) and getattr(type_, "__module__", None) == "builtins"
[docs]@lru_cache() def is_enum_type(type_): """ Checks if the given type is an enum type. :param type_: The type to check :return: True if the type is a enum type, otherwise False :rtype: bool """ return isinstance(type_, type) and issubclass(type_, tuple(_get_types(Types.ENUM)))
[docs]@lru_cache() def is_typing_type(type_): """ Checks if the given type is a ``typing`` module type. :param type_: The type to check :return: True if the type is part of the ``typing`` module, otherwise False :rtype: bool """ return getattr(type_, "__module__", None) == "typing"
[docs]@lru_cache() def is_collections_type(type_): """ Checks if the given type is a ``collections`` module type :param type_: The type to check :return: True if the type is part of the ``collections`` module, otherwise False :rtype: bool """ return ( isinstance(type_, type) and getattr(type_, "__module__", None) == "collections" )
[docs]@lru_cache() def is_regex_type(type_): """ Checks if the given type is a regex type. :param type_: The type to check :return: True if the type is a regex type, otherwise False :rtype: bool """ return ( callable(type_) and getattr(type_, "__name__", None) == REGEX_TYPE_NAME and hasattr(type_, "__supertype__") and is_compiled_pattern(type_.__supertype__) )
[docs]@lru_cache() def is_union_type(type_): """ Checks if the given type is a union type. :param type_: The type to check :return: True if the type is a union type, otherwise False :rtype: bool """ if is_typing_type(type_) and hasattr(type_, "__origin__"): # NOTE: union types can only be from typing module return type_.__origin__ in _get_types(Types.UNION) return False
[docs]@lru_cache() def is_null_type(type_): """ Checks if the given type is a null type. :param type_: The type to check :return: True if the type is a null type, otherwise False :rtype: bool """ return type_ in _get_types(Types.NULL)
[docs]@lru_cache() def is_bool_type(type_): """ Checks if the given type is a bool type. :param type_: The type to check :return: True if the type is a bool type, otherwise False :rtype: bool """ return type_ in _get_types(Types.BOOL)
[docs]@lru_cache() def is_bytes_type(type_): """ Checks if the given type is a bytes type. :param type_: The type to check :return: True if the type is a bytes type, otherwise False :rtype: bool """ return type_ in _get_types(Types.BYTES)
[docs]@lru_cache() def is_string_type(type_): """ Checks if the given type is a string type. :param type_: The type to check :return: True if the type is a string type, otherwise False :rtype: bool """ string_types = _get_types(Types.STRING) if is_typing_type(type_): return type_ in string_types or is_regex_type(type_) return type_ in string_types
[docs]@lru_cache() def is_integer_type(type_): """ Checks if the given type is a integer type. :param type_: The type to check :return: True if the type is a integer type, otherwise False :rtype: bool """ return type_ in _get_types(Types.INTEGER)
[docs]@lru_cache() def is_number_type(type_): """ Checks if the given type is a number type. :param type_: The type to check :return: True if the type is a number type, otherwise False :rtype: bool """ return type_ in _get_types(Types.NUMBER)
[docs]@lru_cache() def is_array_type(type_): """ Checks if the given type is a array type. :param type_: The type to check :return: True if the type is a array type, otherwise False :rtype: bool """ array_types = _get_types(Types.ARRAY) if is_typing_type(type_): return type_ in array_types or ( hasattr(type_, "__origin__") and type_.__origin__ in array_types ) return type_ in array_types
[docs]def is_object_type(type_): """ Checks if the given type is a object type. :param type_: The type to check :return: True if the type is a object type, otherwise False :rtype: bool """ object_types = _get_types(Types.OBJECT) if is_typing_type(type_): return type_ in object_types or ( hasattr(type_, "__origin__") and type_.__origin__ in object_types ) return type_ in object_types
[docs]def typecast(type_, value): """ Tries to smartly typecast the given value with the given type. :param type_: The type to try to use for the given value :param value: The value to try and typecast to the given type :return: The typecasted value if possible, otherwise just the original value """ # NOTE: does not do any special validation of types before casting # will just raise errors on type casting failures if is_builtin_type(type_) or is_collections_type(type_) or is_enum_type(type_): # FIXME: move to Types enum and TYPE_MAPPING entry if is_bytes_type(type_): return decode_bytes(value) return type_(value) elif is_regex_type(type_): return typecast(str, value) elif is_typing_type(type_): try: base_type = type_.__extra__ except AttributeError: # NOTE: when handling typing._GenericAlias __extra__ is actually __origin__ base_type = type_.__origin__ arg_types = type_.__args__ if is_array_type(type_): if len(arg_types) == 1: item_type = arg_types[0] return base_type([typecast(item_type, item) for item in value]) else: return base_type(value) elif is_object_type(type_): if len(arg_types) == 2: (key_type, item_type) = arg_types return base_type( { typecast(key_type, key): typecast(item_type, item) for (key, item) in value.items() } ) else: return base_type(value) else: return base_type(value) else: return value