設定ファイルを扱うクラスを生成するライブラリです。


Keywords
Python, ConfigurationManager, Configure, json
License
Other
Install
pip install otsucfgmng==1.3.2.37

Documentation

概要

このライブラリはjson形式の設定ファイルの読み書きを補助するための基底クラスです。
BaseCMクラスを継承し、__defaults__, <attributes>を定義するだけで必要な操作を行えるようになります。

現在違うセクションに同じキーを持つような設定ファイルには対応していません。
対応しないことに決定しました。(理由はこちら)

このライブラリは以下の環境で作成されています。 Windows10(64bit), Python3.11.1

# 違うセクションに同じキーを持つ例
{
    'app': {'name': 'Hello'},
    'default': {'name': 'Python'}
}

インストール

インストール

pip install otsucfgmng

アップデート

pip install -U otsucfgmng

アンインストール

pip uninstall otsucfgmng

使い方

  1. otuvalidatorをインポートし、必要なバリデータ、コンバータを使用できるようにする
    • 自作クラスなどを使用したい場合にはOtsuValidatorや実際のコードを参考に定義する(参考例)
  2. otsucfgmngをインポートし、BaseCMを使用できるようにする
  3. BaseCMを継承したクラスを定義する
    1. 属性__defaults__に辞書形式で利用する属性名とその初期値を与える
    2. __hidden_options__で隠しオプションを設定する (必要に応じて)
    3. __defaults__で宣言した属性名に1.で用意したコンバータを与える
  4. 設定ファイルのパスを与えてインスタンスを作成する
  5. インスタンスの属性を書き換えて編集を行う
  6. save_cmを呼び出せば設定ファイルが出力される

実行例-設定ファイル管理クラス

作成-設定ファイル管理クラス

実行例に戻る

cfg.jsonという設定ファイルを作成していきます。

# 1.
from otsuvalidator import CBool, CInt, CPath, CString

# 2.
from otsucfgmng import BaseCM


# 3.
class ConfigurationManager(BaseCM):
    # 3.1.
    __defaults__ = {
        'app': {
            'library': 'SampleLibrary.dll',
            'scripts': 'SampleScripts.scrpt',
            'title': 'Sample Program',
            'fullscreen': False
        },
        'audio': {
            'bgm': 100,
            'bgs': 100,
            'se': 100,
            'me': 85
        }
    }

    # 3.3.
    library = CPath('dll')
    scripts = CPath('scrpt')
    title = CString(1, checker=str.istitle)
    fullscreen = CBool()
    bgm = CInt(0, 100)
    bgs = CInt(0, 100)
    se = CInt(0, 100)
    me = CInt(0, 100)


# 4.
cm = ConfigurationManager('cfg.json')

# 5.
cm.bgm = 99
cm.bgs = 50

# 6.
cm.save_cm()

上記の処理で作成されたcfg.jsonの中身は以下の通りです。

{
    "app": {},
    "audio": {
        "bgm": 99,
        "bgs": 50
    }
}

読み込み-設定ファイル管理クラス

実行例に戻る

正しい形式で出力された設定ファイルをインスタンスの生成時に与えると自動で設定を読み込みます。

from otsuvalidator import CBool, CInt, CPath, CString

from otsucfgmng import BaseCM


class ConfigurationManager(BaseCM):
    __defaults__ = {
        'app': {
            'library': 'SampleLibrary.dll',
            'scripts': 'SampleScripts.scrpt',
            'title': 'Sample Program',
            'fullscreen': False
        },
        'audio': {
            'bgm': 100,
            'bgs': 100,
            'se': 100,
            'me': 85
        }
    }
    library = CPath('dll')
    scripts = CPath('scrpt')
    title = CString(1, checker=str.istitle)
    fullscreen = CBool()
    bgm = CInt(0, 100)
    bgs = CInt(0, 100)
    se = CInt(0, 100)
    me = CInt(0, 100)

# コンテキストマネージャを使用すると自動でsave_cmされます。
with ConfigurationManager('cfg.json') as cm:
    print(cm.user_cm())
    cm.fullscreen = 'yes'
    print(cm.user_cm())
### 出力は以下のようになります ###
{'app': {}, 'audio': {'bgs': 50, 'bgm': 99}}
{'app': {'fullscreen': True}, 'audio': {'bgs': 50, 'bgm': 99}}

上記の処理で作成されたcfg.jsonの中身は以下の通りです。

{
    "app": {
        "fullscreen": true
    },
    "audio": {
        "bgs": 50,
        "bgm": 99
    }
}

隠しオプション

先ほどのコードに隠しオプションを追加します。
方法はシンプルで、__hidden_options__に対象の属性名のタプルを渡すだけです。
今回はfullscreenme属性を隠しオプションにしてみます。

from otsuvalidator import CBool, CInt, CPath, CString

from otsucfgmng import BaseCM


class ConfigurationManager(BaseCM):
    __defaults__ = {
        'app': {
            'library': 'SampleLibrary.dll',
            'scripts': 'SampleScripts.scrpt',
            'title': 'Sample Program',
            'fullscreen': False
        },
        'audio': {
            'bgm': 100,
            'bgs': 100,
            'se': 100,
            'me': 85
        }
    }
    __hidden_options__ = ('fullscreen', 'me')  # この行を追加
    library = CPath('dll')
    scripts = CPath('scrpt')
    title = CString(1, checker=str.istitle)
    fullscreen = CBool()
    bgm = CInt(0, 100)
    bgs = CInt(0, 100)
    se = CInt(0, 100)
    me = CInt(0, 100)


with ConfigurationManager('cfg.json') as cm:
    print(cm.cfg_to_str_cm(True))

出力

{
    "defaults": {
        "app": {
            "fullscreen": false,
            "library": "SampleLibrary.dll",
            "scripts": "SampleScripts.scrpt",
            "title": "Sample Program"
        },
        "audio": {
            "bgm": 100,
            "bgs": 100,
            "se": 100
        }
    },
    "user": {
        "app": {
            "fullscreen": true
        },
        "audio": {
            "bgm": 99,
            "bgs": 50
        }
    }
}

cfg.jsonここで作成されたものが存在する前提になります。
隠しオプションに設定したmeは表示されていませんが、fullscreenは表示されていることがわかるかと思います。
これはfullscreenが変更可能であることを知っている場合には隠す意味がないからです。

メソッド一覧

argparseGUIなどで設定項目を編集するための補助を想定しています。
showコマンドを作成してcfg_to_str_cmを制御するなど、自身のUIに合うように紐づけて使ってください。

名前 概要 戻り値 戻り値型
cfg_to_str_cm 設定をjson.dumpsして返す
allTrueにしている場合は標準設定も表示される
ユーザが変更していない__hidden_options__は表示されない
ユーザに設定を見せる場合にはこのメソッドを使って出力する
設定 str
load_cm 設定ファイルを読み込む
__init__から勝手に呼び出されるので、基本的に使用する必要はない
None
save_cm 現在の設定を書き出す
コンテキストマネージャを使用していれば勝手に呼び出される
None
reset_cm 各属性を初期値に戻す None
defaults_cm __defaults__のコピーを返す 初期設定 dict
user_cm ユーザが変更した属性の辞書を返す 変更された設定 dict
key_place_cm 各属性がどのセクションに属するかを記録した辞書を返す
ユーザにアクセスを許すと__hidden_options__が意味をなくす
属性の所属先 dict
attributes_cm 設定項目の一覧を返す
ユーザにアクセスを許すと__hidden_options__が意味をなくす
設定項目の一覧 set

Q&A

以下の説明でcmが登場した場合、ユーザが定義した設定ファイル管理クラスのインスタンスだと解釈してください。

なぜ異なるセクションで同名キーを持てないようにしましたか?

  1. 残念ながらライブラリ作成者の技術的な面も大きいです。
  2. 後述する理由を克服、緩和する方法が追加、あるいは理解できれば異なるセクションでの同名キーを持てるようになる可能性もなくはないです

ロガーの設定ファイルなど、異なるセクションで同名キーを持ちたい状況はありますが、それに対応する場合、アクセスが複雑になりすぎる恐れがあります。

例えば、現在の属性へのアクセスcm.<key>に加えて、辞書を持つキーをSectionクラスとして変換すればcm.<section>.<key>, cm.<sectionA>.<sectionB>.~.<key>というように管理することは技術的に可能になります。

しかしそうなると、実行例で使用したような構造の設定ではアクセスが煩雑になるデメリットもあります。
cm.fullscreenでアクセスしていたものがcm.app.fullscreenとなる等。
また、動的にクラスを生成する都合上、コード入力支援が受けられず、ヒューマンエラーのリスクも上がってしまいます。

自作クラスを使用する場合に必要なバリデータとコンバータをどう用意すればいいですか?

  1. otsuvalidator.basesからConverter, Validatorをインポートし、使用できるようにする
    1. その他必要なライブラリをインポート
  2. 自作クラスを定義する
    1. 好きなようにクラスを設計する (__eq__メソッドを定義していない場合__hidden_options__が正常に機能しない場合があります)
    2. __str__to_jsonメソッドを定義する
  3. 自作クラスに対応したValidatorを定義する(以下そのValidatorをVMyClassとする)
  4. VMyClassConverterを継承したCMyClassを定義する
  5. 使い方どおり

実行例-自作クラスの使用

作成-自作クラスの使用

実行例に戻る

my_company.jsonという設定ファイルを作成していきます。

ConfigurationManagerではjson.dumpできない属性はotsucfgmng.funcs.support_json_dump関数で変換を試みます(※独自に指定しない限り)
この関数ではto_jsonを持つクラスをcls.to_jsonで、それ以外のクラスをstr(cls)で変換します。
なので、cls.to_jsonで返る値をclsに復元できるようなConverterを定義すれば読み込み時に復元されます。

# 1. & 1.1.
from typing import Any

from otsuvalidator import CNoneable
from otsuvalidator.bases import Converter, Validator

from otsucfgmng import BaseCM


# 2.
class Person:
    # 2.1.
    def __init__(self, name: str, age: int, gender: str):
        self.name = name
        self.age = age
        self.gender = gender

    def show_profile(self):
        print(self)

    # 2.2.
    def __str__(self) -> str:
        data = (
            ('名前', self.name),
            ('年齢', self.age),
            ('性', self.gender),
        )
        prof = []
        for k, v in data:
            prof.append(f'{k}\t: {v}')
        prof = '\n'.join(prof)
        return prof

    def to_json(self) -> dict:
        data = {'name': self.name, 'age': self.age, 'gender': self.gender}
        return data


# 3.
class VPerson(Validator):
    def validate(self, value: Any) -> Person:
        if type(value) is not Person:
            msg = self.ERRMSG('Person型である必要があります', value)
            raise TypeError(msg)
        return value


# 4.
class CPerson(VPerson, Converter):
    def validate(self, value: Any) -> Person:
        if type(value) is not Person:
            try:
                if isinstance(value, dict):
                    value = Person(**value)
                else:
                    raise TypeError
            except:
                msg = self.ERRMSG('Person型として扱える必要があります。', value)
                raise TypeError(msg)
        return super().validate(value)

    def super_validate(self, value: Any) -> Person:
        return super().super_validate(value)


# 5.
class ConfigurationManager(BaseCM):
    __defaults__ = {
        'president': Person('山田太郎', 28, '男'),
        'employee': {
            'director': Person('部長花子', 28, '女'),
            'manager': Person('課長夢', 28, '女'),
            'chief': Person('係長次郎', 28, '男')
        }
    }
    president = CPerson()
    director = CNoneable(CPerson())
    manager = CNoneable(CPerson())
    chief = CNoneable(CPerson())


with ConfigurationManager('my_company.json') as cm:
    cm.president = Person('乙八', 28, '男')
    cm.director = Person('部長夢', 28, '女')
    cm.manager = None
    cm.chief = None

上記の処理で作成されたmy_company.jsonの中身は以下の通りです。

{
    "employee": {
        "chief": null,
        "manager": null,
        "director": {
            "name": "部長夢",
            "age": 28,
            "gender": ""
        }
    },
    "president": {
        "name": "乙八",
        "age": 28,
        "gender": ""
    }
}

定義通りPersonクラスはperson.to_jsonを通して辞書形式に変換されていることがわかります。

読み込み-自作クラスの使用

実行例に戻る

# 1. & 1.1.
from typing import Any

from otsuvalidator import CNoneable
from otsuvalidator.bases import Converter, Validator

from otsucfgmng import BaseCM


# 2.
class Person:
    # 2.1.
    def __init__(self, name: str, age: int, gender: str):
        self.name = name
        self.age = age
        self.gender = gender

    def show_profile(self):
        print(self)

    # 2.2.
    def __str__(self) -> str:
        data = (
            ('名前', self.name),
            ('年齢', self.age),
            ('性', self.gender),
        )
        prof = []
        for k, v in data:
            prof.append(f'{k}\t: {v}')
        prof = '\n'.join(prof)
        return prof

    def to_json(self) -> dict:
        data = {'name': self.name, 'age': self.age, 'gender': self.gender}
        return data


# 3.
class VPerson(Validator):
    def validate(self, value: Any) -> Person:
        if type(value) is not Person:
            msg = self.ERRMSG('Person型である必要があります', value)
            raise TypeError(msg)
        return value


# 4.
class CPerson(VPerson, Converter):
    def validate(self, value: Any) -> Person:
        if type(value) is not Person:
            try:
                if isinstance(value, dict):
                    value = Person(**value)
                else:
                    raise TypeError
            except:
                msg = self.ERRMSG('Person型として扱える必要があります。', value)
                raise TypeError(msg)
        return super().validate(value)

    def super_validate(self, value: Any) -> Person:
        return super().super_validate(value)


# 5.
class ConfigurationManager(BaseCM):
    __defaults__ = {
        'president': Person('山田太郎', 28, '男'),
        'employee': {
            'director': Person('部長花子', 28, '女'),
            'manager': Person('課長夢', 28, '女'),
            'chief': Person('係長次郎', 28, '男')
        }
    }
    president = CPerson()
    director = CNoneable(CPerson())
    manager = CNoneable(CPerson())
    chief = CNoneable(CPerson())


with ConfigurationManager('my_company.json') as cm:
    for i, key in enumerate(('president', 'director', 'manager', 'chief')):
        if i:
            print('-' * 75)
        print(f'{key}\n{getattr(cm,key)}')
### 出力は以下のようになります ###
president
名前    : 乙八
年齢    : 28
性      : 男
---------------------------------------------------------------------------
director
名前    : 部長夢
年齢    : 28
性      : 女
---------------------------------------------------------------------------
manager
None
---------------------------------------------------------------------------
chief
None