Pythonは強力な構成ライブラリを実現

22470 ワード

アプリケーションは常にプロファイルを読み込み、入力が有効で、構成がない場合にデフォルト値を使用することを検証するため、このプロセスをどのように簡略化したいのでしょうか.
ファイル形式
まず,ユーザが直接書き換えるのを容易にするために,ファイルフォーマットは必ず読み取り可能である.Python標準ライブラリで読み書き可能な構成のライブラリにはconfigparser(iniファイル)、json、xmlがあり、ini、xmlファイルはタイプを問わず、文字列が読み取れるのでjsonを選択しました.ただしJSONではコメントがサポートされていないという欠点があります.この欠点を無視して、良いドキュメントを提供すればいいのです
データ検証、デフォルト
どんな形式のデータを検証し、必要なフォーマットに変換できるPythonライブラリを見つけました.
これはdictdatetimeに変換した例です.
import datetime
import trafaret as t

date = t.Dict({
    'year': t.Int,
    'month': t.Int,
    'day': t.Int
}) >> (lambda d: datetime.datetime(**d))
assert date.check({'year': 2012, 'month': 1, 'day': 12}) == datetime.datetime(2012, 1, 12)

まず、個Dict構造を宣言し、keyと値のタイプを指定する>>オペレータはカスタムの変換関数を指定することができ、変換関数にはDataErrorエラーを投げ出すことができるdate.check()検証して変換したオブジェクトに戻ることができるDictのkeyは文字列以外にも指定できるKeyタイプ、Keyデフォルト値を指定し、元の名前を別の名前に変換できる:
>>> c = t.Dict({t.Key('un', 'default_user_name', True) >> 'user_name': t.String})
>>> c.check({'un': 'Adam'})
{'user_name': 'Adam'}
>>> c.check({})
{'user_name': 'default_user_name'}

オプションのキーを宣言します'un'、デフォルト値は'default_user_name'>>オペレータ指定読み出し後名前変換'user_name'、パラメータに変換の名前を指定することもできます
コンフィグクラスの実装
trafaret変換後に得られたのはdictdict文字列タイプのkeyを使用しています.これはエディタに友好的ではなく、コードを書くときに自動的に補完されていません.また、名前を変更したいときにも自動的に一括変更できないので、アクセス属性でアクセス構成にアクセスするつもりです(.オペレータでアクセス)
様々なORMフレームワークにヒントを得て、クラス属性に構成のフィールドを宣言するつもりです.書き方は以下の通りです.
class NestedConfig(config):
    pass


class MyConfig(Config):
    bool_field = {OptionalKey(True): t.Bool}
    int_field = {OptionalKey(123): t.Int}
    nested_config = {OptionalKey({}): NestedConfig}

OptionalKey
これは実はいくつかのデフォルトのパラメータのKeyクラスを指定して、このように私達はパラメータの中でただデフォルトの値を書くだけで、変換した名前はクラスの属性の名前によって指定します
class OptionalKey(t.Key):
    def __init__(self, default, name=None):
        super().__init__(name, default, True)

宣言されたフィールドをDictに変換
Pythonのメタクラスで簡単に実現できることをクラス作成時に宣言したフィールドをスキャンし、Dict__struct__クラス属性に変換して保存します.ここではConfigの継承も実現しており、作成__struct__の際にベースクラスが既に作成されている__struct__を統合すればよいので、ベースクラスはサブクラスのフィールドを上書きできないことに注意
class ConfigMeta(type):
    """    Config  __struct__
    """

    def __new__(mcs, name, bases: Tuple[type, ...], namespace: Dict[str, Any]):
        #        
        fields = set()
        cls_struct = {}
        for key, value in list(namespace.items()):
            if type(value) is dict and len(value) == 1:
                field_key, field_checker = list(value.items())[0]
                if isinstance(field_key, t.Key):
                    del namespace[key]

                    field_key: t.Key
                    #   name       
                    if field_key.name is None:
                        field_key.name = key
                    #     to_name,       to_name   None 
                    if field_key.to_name is None:
                        field_key.to_name = key
                    assert field_key.to_name == key, f'{field_key.to_name} != {key}   key.to_name         '

                    fields.add(field_key.to_name)
                    cls_struct[field_key] = field_checker

        #        
        base_keys: List[t.Key] = []
        for base in bases:
            if issubclass(base, Config):
                for key in base.__struct__.keys:
                    #    ,       
                    if key.to_name not in fields:
                        base_keys.append(key)
                        fields.update(key.to_name)

        cls = type.__new__(mcs, name, bases, namespace)
        cls.__struct__ = t.Dict(cls_struct, *base_keys)
        return cls

Configクラスのコンストラクタ
コンストラクション関数は、dictを1つ受け入れ、__struct__変換してインスタンス属性に付与します.ここではついでに,入力キーワードパラメータ構造Configをサポートした.また、check()前に未知のキーを削除しないとエラーになるので注意してください
パラメータはdictなので、ここではネストConfigをサポートしています.原理はtrafaretのcheckerが元のデータ型を受け入れて変換したデータ型を返すことで、Pythonのクラスは工場関数として使えるのでConfigクラスはcheckerとしてネストできます
class Config(metaclass=ConfigMeta):

    #     , trafaret  
    __struct__: t.Dict

    def __init__(self, raw: dict=None, **kwargs):
        #     dict       
        if raw is None:
            raw = {}
        raw = dict(raw, **kwargs)

        #      key,  check()  
        known_keys = {key.name for key in self.__struct__.keys}
        for key in list(raw.keys()):
            if key not in known_keys:
                del raw[key]

        raw = self.__struct__.check(raw)
        for key, value in raw.items():
            setattr(self, key, value)

Configをdictに戻す
保存時にはConfig変換dict、JSONがサポートします.ネストがサポートされているためConfigここでは下位のConfigにも変換dictここでは他のJSONでサポートされていないデータ型については処理していないのでConfigにはJSONでサポートされているタイプしか含まれていません.そうでなければ保存できません.他のデータ型を使用する場合は@property
    def to_dict(self):
        res = {key.to_name: getattr(self, key.to_name) for key in self.__struct__.keys}
        #   dict list    , Config  dict
        queue: List[Union[dict, list]] = [res]
        while queue:
            node = queue.pop(0)
            for index, value in (node.items() if isinstance(node, dict)
                                 else enumerate(node)):
                if isinstance(value, Config):
                    node[index] = value.to_dict()
                elif isinstance(value, (dict, list)):
                    queue.append(value)
        return res

フルソース
その後、読み取りと保存の構成を加えると完了します.以下は完全なソースコードです.
# -*- coding: utf-8 -*-

"""    
"""

import json
from typing import Union, List, Dict, Any, Tuple

import trafaret as t


class OptionalKey(t.Key):
    def __init__(self, default, name=None):
        super().__init__(name, default, True)


class ConfigMeta(type):
    """    Config  __struct__
    """

    def __new__(mcs, name, bases: Tuple[type, ...], namespace: Dict[str, Any]):
        #        
        fields = set()
        cls_struct = {}
        for key, value in list(namespace.items()):
            if type(value) is dict and len(value) == 1:
                field_key, field_checker = list(value.items())[0]
                if isinstance(field_key, t.Key):
                    del namespace[key]

                    field_key: t.Key
                    #   name       
                    if field_key.name is None:
                        field_key.name = key
                    #     to_name,       to_name   None 
                    if field_key.to_name is None:
                        field_key.to_name = key
                    assert field_key.to_name == key, f'{field_key.to_name} != {key}   key.to_name         '

                    fields.add(field_key.to_name)
                    cls_struct[field_key] = field_checker

        #        
        base_keys: List[t.Key] = []
        for base in bases:
            if issubclass(base, Config):
                for key in base.__struct__.keys:
                    #    ,       
                    if key.to_name not in fields:
                        base_keys.append(key)
                        fields.update(key.to_name)

        cls = type.__new__(mcs, name, bases, namespace)
        cls.__struct__ = t.Dict(cls_struct, *base_keys)
        return cls


class Config(metaclass=ConfigMeta):
    """        ,           ,    

            JSON     ,           property
               , :

        name = {OptionalKey(0): t.Int}

        , :

        nested_config = {OptionalKey({}): Config}

      

        nested_config = {OptionalKey({}): t.Dict >> Config}

    """

    #     , trafaret  
    __struct__: t.Dict

    def __init__(self, raw: dict=None, **kwargs):
        #     dict       
        if raw is None:
            raw = {}
        raw = dict(raw, **kwargs)

        #      key,  check()  
        known_keys = {key.name for key in self.__struct__.keys}
        for key in list(raw.keys()):
            if key not in known_keys:
                del raw[key]

        raw = self.__struct__.check(raw)
        for key, value in raw.items():
            setattr(self, key, value)

    def __repr__(self):
        return repr(self.to_dict())

    @classmethod
    def from_file(cls, filename):
        """              
        """
        try:
            with open(filename, encoding='utf-8') as f:
                return cls(json.load(f))
        except FileNotFoundError:
            return cls()

    @classmethod
    def from_str(cls, s):
        return cls(json.loads(s))

    def to_dict(self):
        res = {key.to_name: getattr(self, key.to_name) for key in self.__struct__.keys}
        #   dict list    , Config  dict
        queue: List[Union[dict, list]] = [res]
        while queue:
            node = queue.pop(0)
            for index, value in (node.items() if isinstance(node, dict)
                                 else enumerate(node)):
                if isinstance(value, Config):
                    node[index] = value.to_dict()
                elif isinstance(value, (dict, list)):
                    queue.append(value)
        return res

    def save(self, filename):
        with open(filename, 'w', encoding='utf-8') as f:
            json.dump(self.to_dict(), f, ensure_ascii=False, indent=2)

使用方法
class BaseConfig(Config):
    #           
    override_field = {OptionalKey(1): t.Int}
    #         
    inherited_field = {OptionalKey(2): t.Int}


class NestedConfig(Config):
    test = {OptionalKey(3): t.Int}


class MyConfig(BaseConfig):
    #        
    override_field = {OptionalKey(True): t.Bool}
    #   Config
    nested_config: NestedConfig = {OptionalKey({}): NestedConfig}
    nested_config_list: List[NestedConfig] = {OptionalKey([{'test': 123}]):
                                              t.List(NestedConfig)}
    #       [1, 5] 
    int_field = {OptionalKey(1): t.Int(1, 5)}
    #       {1, 2, 3} 
    int_field2 = {OptionalKey(1): t.Int >>
                  (lambda x: x if x in (1, 2, 3) else t.DataError('int_field2  {1, 2, 3} '))}


#   Config
config = MyConfig({'int_field': 3, 'nested_config': {'test': 0}})
# {'inherited_field': 2, 'override_field': True,
#  'nested_config': {'test': 0}, 'nested_config_list': [{'test': 123}],
#  'int_field': 3, 'int_field2': 1}
print(repr(config))
#         
config = MyConfig(int_field=4)
# {'inherited_field': 2, 'override_field': True,
#  'nested_config': {'test': 3}, 'nested_config_list': [{'test': 123}],
#  'int_field': 4, 'int_field2': 1}
print(repr(config))
#          
print(config.int_field)  # 4
print(config.nested_config.test)  # 3
#        
# trafaret.dataerror.DataError: {'override_field': DataError(value should be True or False)}
# config = MyConfig({'override_field': 1})
# trafaret.dataerror.DataError: {'int_field2': DataError(int_field2  {1, 2, 3} )}
# config = MyConfig({'int_field2': 4})