Skip to content
代码片段 群组 项目
config.py 39.3 KB
Newer Older
openaiops's avatar
openaiops 已提交
import codecs
import dataclasses
import inspect
import json
import os
import warnings
from argparse import ArgumentParser, Action
from dataclasses import dataclass, is_dataclass
from enum import Enum
from typing import *

import yaml

from .utils import *
from .type_check import *
from .typing_ import *

__all__ = [
    'ConfigValidationError',
    'field_checker', 'root_checker', 'config_field', 'ConfigField',
    'config_params', 'get_config_params', 'ConfigMeta',
    'Config', 'validate_config', 'config_to_dict', 'config_defaults',
    'ConfigLoader', 'object_to_config',
    'format_config', 'print_config', 'save_config',
]

# general special attributes that is recognized by ``type_info``

# special attributes of a Config class
_FIELDS = '__mltk_config_fields__'  # fields
_UNBOUND_CHECKERS = '__mltk_config_unbound_checkers__'  # unbound field and root checker params
_PARAMS = '__mltk_config_params__'  # config parameters
_PARAMS_CLASS_NAME = '__ConfigParams__'  # nested class as config parameters

# special attributes of a Config classmethod
_CHECKER_PARAMS = '__mltk_config_checker_params__'


ConfigValidationError = TypeCheckError
"""Legacy name for the :class:`TypeCheckError`."""


@dataclass
class ObjectFieldCheckerParams(object):
    fields: Tuple[str, ...]
    method: classmethod
    pre: bool


@dataclass
class ObjectRootCheckerParams(object):
    method: classmethod
    pre: bool


ObjectCheckerParams = Union[ObjectFieldCheckerParams, ObjectRootCheckerParams]


def field_checker(*fields, pre: bool = False):
    """
    Decorator to register a class method as a field checker in :class:`Config`.

    The checker should be implemented as a class method, with `cls` as its
    first argument, and the field value as its second argument.  Besides,
    it may also accept `values` and `field` as keyword argument, with
    `values` receiving all field values (being a dict if ``pre = True``,
    or a `Config` instance if ``pre = False``), and `field` receiving the
    name of the field being checked.

    >>> class MyConfig(Config):
    ...     a: int
    ...     b: int
    ...     c: int
    ...
    ...     @field_checker('c')
    ...     def _checker(cls, v, values, field):
    ...         if v != values['a'] + values['b']:
    ...             raise ValueError('a + b != c')
    ...         return v

    >>> validate_config(MyConfig(a=1, b='2', c=3.0))
    MyConfig(a=1, b=2, c=3)
    >>> validate_config(MyConfig(a=1, b='2', c=4.0))
    Traceback (most recent call last):
       ...
    mltk.type_check.TypeCheckError: caused by:
    * ValueError: a + b != c

    Args:
        *fields: The fields to be checked.  "*" represents all fields.
        pre: Whether or not this checker should be run before the fields
            having been checked against the field definitions?  If :obj:`True`,
            `values` will be a dict, rather than an instance of `Config`.
            Defaults to :obj:`False`.
    """
    def wrapper(method):
        if not isinstance(method, classmethod):
            method = classmethod(method)
        if not hasattr(method, _CHECKER_PARAMS):
            setattr(method, _CHECKER_PARAMS, [])
        getattr(method, _CHECKER_PARAMS).append(
            ObjectFieldCheckerParams(fields=fields, method=method, pre=pre))
        return method
    return wrapper


def root_checker(pre: bool = False):
    """
    Decorator to register a class method as a root checker in :class:`Config`.

    The checker should be implemented as a class method, with `cls` as its
    first argument, and the object values as its second argument.
    When ``pre = True``, the values will be a dict; otherwise it will be
    an instance of `Config`.

    >>> class MyConfig(Config):
    ...     a: int
    ...     b: int
    ...     c: int
    ...
    ...     @root_checker()
    ...     def _checker(cls, values):
    ...         if values.c != values.a + values.b:
    ...             raise ValueError('a + b != c')

    >>> validate_config(MyConfig(a=1, b='2', c=3.0))
    MyConfig(a=1, b=2, c=3)
    >>> validate_config(MyConfig(a=1, b='2', c=4.0))
    Traceback (most recent call last):
       ...
    mltk.type_check.TypeCheckError: caused by:
    * ValueError: a + b != c

    Args:
        pre: Whether or not this checker should be run before the fields
            having been checked against the field definitions?  If :obj:`True`,
            `values` will be a dict, rather than an instance of `Config`.
            Defaults to :obj:`False`.
    """
    def wrapper(method):
        if not isinstance(method, classmethod):
            method = classmethod(method)
        if not hasattr(method, _CHECKER_PARAMS):
            setattr(method, _CHECKER_PARAMS, [])
        getattr(method, _CHECKER_PARAMS).append(
            ObjectRootCheckerParams(method=method, pre=pre))
        return method
    return wrapper


def config_field(type: Optional[Type] = None,
                 default: Any = NOT_SET,
                 default_factory: Callable[[], Any] = NOT_SET,
                 description: Optional[str] = None,
                 choices: Optional[Sequence[Any]] = None,
                 required: bool = True,
                 envvar: Optional[str] = None,
                 ignore_empty_env: bool = True,
                 # deprecated arguments
                 nullable: bool = NOT_SET):
    """
    Define a :class:`Config` field.

    Args:
        type: Type of the field.  Any type literal that can be recognized
            by :func:`mltk.utils.type_info`, e.g., ``Optional[int]``.
            If the field type is already specified via type annotation,
            then the type specified by this argument will be ignored.
        default: The default value of this field.
        default_factory: A function ``() -> Any``, which returns the
            default value of this field.  `default` and `default_factory`
            cannot be both specified.
        description: Description of this field.
        choices: Valid values for this field to take.
        required: Whether or not this field is required?
            If :obj:`False`, the object will pass type checking even
            when this field is not specified a value.
        envvar: The name of the environmental variable to read from.
        ignore_empty_env: Whether or not empty string from the environmental
            variable will be ignored, as if no value has been given?
        nullable: DEPRECATED.  Whether or not this field is nullable?
            Use ``Optional[T]`` as type instead of using this argument.

    Returns:
        The config field object.
    """
    if nullable is not NOT_SET:
        warnings.warn('`nullable` argument is deprecated.  Use `Optional[T]` '
                      'as type instead.', DeprecationWarning)

    # check the type argument.
    if type is None:
        # If the type annotation is adopted, it will be later overwritten.
        if default is not NOT_SET:
            ti = type_info_from_value(default)
        elif default_factory is not NOT_SET:
            ti = type_info_from_value(default_factory())
        else:
            ti = AnyTypeInfo()
    else: