Enchan1207 / blueprintpy

汎用パッケージコンフィグCLIジェネレータ

Home Page:https://enchan1207.github.io/blueprintpy/index.html

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

テンプレート展開コンテンツの処理

Enchan1207 opened this issue · comments

(from #1)

jinjaを通すのか通さないのか、通した後のオブジェクトをどう管理するのか…

こうしたいわけですよね つまり

flowchart LR
    id1(Content\nインスタンス) --> id2(展開する準備ができたファイル)
    id1 -- Jinja2 --> id2
    id2 --> 展開先にコピー
Loading

全部open()で読んでBytesIO(Jinjaを通すならstr.encode->BytesIO)オブジェクトを渡してコピーみたいにすればクラス構造的には扱いやすい (ContentとBytesIOを持つクラス(PreparedContent?)を用意すれば終わり)んだけど、

正直jinjaを通す必要もないファイルをわざわざPythonに通して書き込むってのはただのアホなので…

変更する必要のないファイルは全部shutil.copyfileで横流ししちゃっていいのはそれはそうなんだけど 統一しにくくなるよなあと…

それか、どっちにも展開先の絶対パスが必須ってのを考慮して
こういうのもいいかもしれない

class PreparedContent:
    def __init__(self, dest_path: Path, source: Optional[Path] = None, extract_object: Optional[bytes] = None) -> None:
        """展開可能なファイルコンテンツを生成します.

        Args:
            dest_path (Path): 展開先の絶対パス.
            source (Optional[Path], optional): 展開するファイルの絶対パス.
            extract_object (Optional[bytes], optional): 展開するオブジェクトのバイト列.
        """

        self.dest_path = dest_path
        self.source = source
        self.extract_object = extract_object

この子はただのデータクラスでしかないけど、例えば
ContentBuilder(template_base:Path, extract_root:Path, arguments: List[Argument]).build(content: Content) -> PreparedContent
みたいなビルダーを用意してあげれば、
ArgsHandlerで値をセットしたList[Argument]とCLIで明示された展開先とテンプレート名から求められる展開元をContentBuilderに渡してインスタンス化して
Config.contents.build(Content)に通して List[PreparedContent]を生成して
ContentExtractor.extract(extract_root:Path, prepared: List[PreparedContent])みたいにすれば…

意外といけそうじゃないですか?

パスが指すディレクトリの有無および自動作成はContentExtractorが担当すればいいし、
ContentBuilderはContentのパス(src, dest)をf文字列展開して読み込んで必要に応じてjinjaに通してPreparedContentを生成すればいいから…

結構適度に責任の分離ができてるんじゃないかと

ファイル出力を完全にContentExtractorに切り分けられてるのはでかいと思う

現状の構成がこんな感じで

classDiagram

class Config
Config : String name
Config : Argument[] args
Config : Content[] contents

class Argument
Argument : String name
Argument : String description
Argument : String? argtype
Argument : String? default_value

class Content
Content : String src
Content : String dest

class ConfigLoader
ConfigLoader : load(String config_json_str)$ Config

class ArgsHandlerBase
<<interface>> ArgsHandlerBase
ArgsHandlerBase : XXXXArgsHandler[] handlers$
ArgsHandlerBase : handle_args(Argument[] args)* Argument[]

class XXXXArgsHandler
XXXXArgsHandler : handle_args(Argument[] args) Argument[]

XXXXArgsHandler ..|> ArgsHandlerBase
XXXXArgsHandler <-- ConfigLoader : imports

Config <-- ConfigLoader : creates
Config o-- Argument
Config o-- Content
Loading

Builder, Extractorを追加するとこんな感じになるのか

classDiagram

class PreparedContent
PreparedContent : Path dest_path
PreparedContent : Path source
PreparedContent : bytes? extract_object

class ContentBuilder
ContentBuilder : Path template_base
ContentBuilder : Path extract_root
ContentBuilder : Argument[] arguments
ContentBuilder : build(Content content) PreparedContent

class ContentExtractor
ContentExtractor : extract(Path extract_root, PreparedContent[] prepared)

ContentBuilder --> PreparedContent: creates
ContentExtractor --> PreparedContent: extracts(places)
Loading

あとはCLIが ConfigLoaderからArgument[]を引っ張ってきてArgsHandlerに投げて
その出力とその他の情報からContent[]PreparedContent[]にして
Extractorに配置してもらう…

さっきのをまとめるとこんな感じかな?

Contentが持つコンテンツファイルはjinjaを通すべきかどうか」は BuilderではなくContentが示すべきでは?

例えば 「偶然にも .j2 という拡張子のファイルを作りたくなってしまった」 みたいな状況にも耐えたくない?

なんだろ content_typeとか? use_template_engineとか?

とりあえず依存関係にJinjaを登録 Argumentにvalue持たせるの忘れてたので追加 Contentにもuse_template_engineを追加

んでPreparedContentとBuilderの実装だけちょっと

テストケースをどうするか悩んでるんですよね、実際にテンプレートファイルを置いて一次環境を構成しないといけない

Builder.env: Jinja2.EnvironmentをinjectableにしてFileSystemLoader以外を突っ込むようにしてもいいかと思ったけど
実際にファイル生成して触って置き換えてってやった方がいいんかなあ

PackageLoaderDictLoaderなるものが存在するらしい?
パッケージ単位でテンプレートを読み込めるとかDictでファイルの対応作れるとかなんとか

結局 Builder.env を注入可能にすることになるんだよな それでいいのかという

envじゃなくてJinja2.BaseLoaderを注入できるようにすれば 少しはまともにならんか

とりあえず注入可能にした テストケース書いてみるか…

テスト書いてて気づいたんですが、ConfigWriterの需要が高まってきてますね…
まあ本流で使うことはなさそうなんですけど…

内部クラスとして持っておくのは悪いことじゃないと思う
作るか

よく考えると ConfigLoaderであってReaderではないから Writerが対義語なのはちょっと変だな
1E+2歩譲ってそれはいいとして、Loader.load()の対義語ってなんだ? Writer.write だとファイルに書き込むまでやっちゃいそうなニュアンスだけどそこまではやりたくないし(テストができないため)…
Loaderの方もやってる処理的にはloadというかparseだから parseの対義語にしてみるか

えーparseの対義語…

But in the general case, the opposite of parse is ǝsɹɐd
しかし一般的には、parseの反対はǝsɹɐdです

(引用: StackOverflow)

…ってやかましいわい

この質問についてた別の回答で unparsecomposeserializeassemble とか挙げられてますね

こーりゃserializeかなあ

戻り値は… strでもいいけど厳密な比較とかしようと思うとDict[str, Any]のほうがいいのかな?

モックだけ作った

これ各クラスSerializable準拠したほうがいい説ないですかね…
ConfigSerializerの処理だけ重すぎる

serialized_args = [reduce(lambda p, n: p | n, list([{key: getattr(arg, key)} if getattr(arg, key) is not None else {} for key in ['name', 'description', 'argtype', 'default_value']])) for arg in config.args]

流石に頭おかしくて笑う

Configにargs_handler持たせてないからシリアライズしきれないじゃん

せめてシリアライザのパスだけもらうようにするか

とりあえずシリアライズはできた あとはテストケースだ

いよいよ 何をやっているのか全くわからないテストケース になってしまったな()
現時点での処理は以下の通り…

  • 適当な値を持たせたArgumentインスタンスを10個生成
  • 適当な値を持たせたContentインスタンスを同数生成 ここで、転送先(Content.dest)は同じ位置にある引数の名前とし、展開元(Content.src)が指すファイルの中身は同じ位置にある引数の情報をシリアライズ化したものとする
  • コンフィグを生成し、jsonに変換 モックアップ生成処理ここまで
  • jsonからコンフィグを復元し、ContentBuilderを生成
  • ビルダーにConfig.contentsをひとつずつ通してPreparedContentを生成
  • PreparedContentと最初に生成したContentを比較する テスト処理ここから
    • 展開元が等しいか (PreparedContent.source == Content.src?)
    • 展開先パスの引数展開は正しく行われているか (PreparedContent.dest == {同じ位置にある引数の名前} ?)
    • 展開元のファイルは参照可能で、なおかつ中身の値が想定通りか (PreparedContent.sourceの中身から生成した引数 == {同じ位置にある引数} ?)

詰め込みすぎて何が何だかわからなくなってますね…

あとはあれか jinjaがまともに動くかを検証すればとりあえずビルダーは完成か

Argumentのハンドル結果、PreparedArgumentみたいにしてみます?

ほいjinjaのテスト追加 こっちはもう説明いらんやろ…()

でもやっぱりテストケースっていいですね 不具合がどんどん出てくる

Extractor作るぞーー

ほいとりあえず生成
テスト書きます

書けた!

やってみて思うけどこれバリデーション系どっかに挟んだほうがいいかもしれんな
このままだと全部CLIの方でやることになる

とりあえずこのissueでやることはやったんじゃないですかね