テンプレート展開コンテンツの処理
Enchan1207 opened this issue · comments
こうしたいわけですよね つまり
flowchart LR
id1(Content\nインスタンス) --> id2(展開する準備ができたファイル)
id1 -- Jinja2 --> id2
id2 --> 展開先にコピー
全部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
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)
あとは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
以外を突っ込むようにしてもいいかと思ったけど
実際にファイル生成して触って置き換えてってやった方がいいんかなあ
PackageLoader、DictLoaderなるものが存在するらしい?
パッケージ単位でテンプレートを読み込めるとか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)
…ってやかましいわい
この質問についてた別の回答で unparse、compose、serialize、assemble とか挙げられてますね
こーりゃ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でやることはやったんじゃないですかね