dataclassをconfigparserでまとめて代入する

# 序文 
さて、私の一番アクセスの多い記事はKindle for PCのSSを撮る奴なんですが、なんていうか、とにかく突貫で自分だけよければそれでいい、と言う思想の元に作られたもので、それ以外の事はあんまり考えてはいませんでした。

そんなわけで、~~ネタも無いし~~アレを改修しようと思いました。

### ここから言い訳
で、一番の問題はスクリーンキャプチャ部分です
動作速度が遅い事、マルチモニタ非対応な事、ここを改善するのはそれなりに意味があるはずです。
でも、この部分の改修は色々問題がありました

Pythonで高速にスクリーンショットを撮るためには、D3DShotと言うのがPyPIにあります。ですがこれはPython3.8までです。対応バージョンだけを書き換えることで、簡単にそれ以降のバージョンに対応できるそうなのですが(3.10での動作は確認済)、
>気軽にPyPIにあるD3Dshotをpipでインストールしないでください。その環境のPILが壊れます。

ImageGrabと同様に、マルチモニタ非対応な事もあり、使うのは正直どうなんだろうなー、と・・

そうなると、別のものを考えるしかありません。Windowsは、Windows10 1803からWindows Graphics Captureと言うAPIが提供されていて、これを使用するのがベストです。
ところが、これは通常のWin32APIでなく、Windows Runtimeとやらで提供されています。MicrosoftがそれをPythonで使用するwinrtってのを出しているのですが、これもPython3.9対応までで更新が止まっています。
>なお、最近このwinrtのコミュニティバージョンのwinsdkと言う物があるのを知りました。いずれWindows Graphics Captureを使用したものも出るのかもしれません

で、考えを変えました、というか、これに付いてはとりあえず諦めました。で、その他の部分の改修をしようと

# 前置き終わり
以前の`kindless.py`は、ソースコードにパラメータを書いていました。マジックナンバーこそ殆ど使っていませんが、ユーザーフレンドリーだとは言い難い作りです。
Qiitaなんだからそれでいい、毎回vscodeから実行って言うのも一理あるのですが、良く使うものに関してはやっぱりスタンドアロンで動作してほしいです。
そうなるとソースコードを弄るわけにはいかず、設定ファイルか何かでやるべきでしょう。

この時に取れる手段としては
+ コマンドラインオプション
+ JSONファイル
+ YAML(TOML)ファイル
+ iniファイル
辺りが考えられます

今回、iniファイルを選択しました。
で、`configparser`を入れました。設定などの変数は`dataclass`でまとめました。

で、読み込みのコードを書いたのですが・・・

```Python:
@dataclasses.dataclass
class Config:
    window_title : str
    capture_wait : float
.
.
.
def config_read()
    window_title = cfg.get( 'KINDLESS' , 'window_title' )
    capture_wait = cfg.getfloat( 'KINDLESS' , 'capture_wait' )
.
.
.
return Config(window_title = window_title, capture_wait = capture_wait)
```
ぐぬぬぬぬぬ
何度も何度も同じ事書かないといかんの超面倒くさい！！！

そんなわけで書き直したのがこちら
```Python:dataclass.py (Example)
import dataclasses
import configparser
import os.path as osp

homedir = osp.join(osp.expanduser('~'),'kindle_scan')
default_ini_name = 'kindless.ini'
default_section = 'KINDLESS'

@dataclasses.dataclass
class KindleSSConfig:
    window_title : str = 'Kindle'
    execute_filename : str = 'KINDLE.EXE'
    nextpage_key : str = 'left'
    fullscreen_key : str = 'f11'
    pagejump_key : list = dataclasses.field(default_factory=lambda: ['ctrl','g'])

    pagejump : str = '1'

    short_wait : float = 0.1
    long_wait : float = 0.2
    capture_wait : float = 0.35
    timeout_wait : float = 5.0
    fullscreen_wait : float = 5.0

    left_margin : int = 0
    right_margin : int = 0

    base_save_folder : str = homedir
    overwrite : bool = True
    trim_after_capture : bool = False
    examination_data_save : bool = True
    zipped_file : bool = False
    force_move_first_page : bool = True

    file_extension : str = '.png'
    grayscale_threshold : int = 2

    grayscale_margin_top : int = 1
    grayscale_margin_bottom : int = 16
    grayscale_margin_left : int = 1
    grayscale_margin_right : int = 1

    trim_margin_top : int = 1
    trim_margin_bottom : int = 16
    trim_margin_left : int = 1
    trim_margin_right : int = 1

def read_config(dataclass: type, ini: str) -> object:
    def keycombination(strk : str)->list:
        lst = strk.split('+')
        return [i.strip() for i in lst]
    
    def fileextension(ext : str)->str:
        return ext if ext[0] == '.' else '.' + ext

    special_function = { 'pagejump_key' : keycombination , 'file_extension' : fileextension }

    # ここから下はほぼ固定
    if ini == '':
        ini = default_ini_name
    if not osp.exists(ini):
        raise FileNotFoundError('ini file not found')

    section_name = default_section
    r = dataclass()    

    config = configparser.ConfigParser()
    config.read(ini, encoding= 'utf-8')
        
    for k in vars(r):
        if k in special_function.keys():
            try:
                setattr(r, k, special_function[k]( config.get( section_name, k ))) 
            except configparser.NoOptionError as e:
                print('WARNING : {} NO OPTION'.format(k))
                pass
        else:
            attr = type(getattr(r, k))
            try:
                if attr is int:
                    setattr(r, k, config.getint( section_name, k))
                elif attr is float:
                    setattr(r, k, config.getfloat( section_name, k))
                elif attr is bool:
                    setattr(r, k, config.getboolean( section_name, k))
                elif attr is str:
                    setattr(r, k, config.get( section_name, k))
            except configparser.NoOptionError as e:
                print('WARNING : {} NO OPTION'.format(k))
    return r

def main():
    # testcode
    cfg = read_config(KindleSSConfig,'')

    for i in vars(cfg):
        print(i, getattr(cfg,i), type(getattr(cfg,i)))

if __name__ == '__main__':
    main()
```

```ini:kindless.ini (Example)
[KINDLESS]
window_title = Kindle
execute_filename = kindle.exe
nextpage_key = left
fullscreen_key = f11
pagejump_key = ctrl + g
pagejump = 1
short_wait = 0.1
long_wait = 0.2
capture_wait = 0.35
timeout_wait = 5.0
fullscreen_wait = 5.0
left_margin = 1
right_margin = 1
base_save_folder = e:\kindle_ss\
overwrite = True
trim_after_capture = True
examination_data_save = True
zipped_file = True
force_move_first_page = True
file_extension = png
grayscale_margin_top = 0
grayscale_margin_bottom = 0
grayscale_margin_left = 0
grayscale_margin_right = 0
trim_margin_top = 1
trim_margin_bottom = 16
trim_margin_left = 0
trim_margin_right = 0
grayscale_threshold = 2
```

dataclassには初期値~~が必須~~です。~~と、いうのも、型アノテーションだけだと、何の型か調べる方法がわからないので……~~
で、read_configの直後の関数定義と
` special_function = { 'pagejump_key' : keycombination , 'file_extension' : fileextension }`は、特定のキーに対して行う処理です
keycombinationは、"+"で分けてListにして格納します
fileextensionは、頭に"."がなければ付け加えます

それ以外はdataclassに入れた初期値を元に型を指定して読み込みます

なお、セクションはdefault_sectionで指定した物以外使えません。正直複雑なものに関してはiniファイル以外の選択肢を採る方が良いと思います

後、read_configの第一引数は、「クラス名」で、「インスタンス名」ではありません

初期値を使わない場合

`for k in vars(r):` を
`for k, attr in r.__annotations__.items():`に

`attr = type(getattr(r, k))`を削除

すれば良いと思われます
ちゃんと調べる方法あった