prestringに複数ファイルの出力機能を付けた
prestringに複数ファイルの出力機能を付けた。prestring.output:output
を使う。
正確には以前からあったモジュールだったのだけれど、あまりにも貧弱かつ使いにくかったので書き直した。
コード
使いかたの話。例えば以下のスクリプトは、指定したディレクトリ(dst)に3つのファイルを出力する。
- projects/x.txt
- projects/y.txt
- projects/z.py
import sys from prestring.python import Module from prestring.output import output, cleanup_all # noqa dst = sys.argv[1] with output(root=dst) as fs: with fs.open("projects/x.txt", "w") as wf: print("hello x", file=wf) print("bye x", file=wf) with fs.open("projects/y.txt", "w") as wf: print("hello y", file=wf) print("bye y", file=wf) with fs.open("projects/z.py", "w", opener=Module) as m: with m.def_("hello"): m.stmt("print('hello')")
コンテキストマネージャで取れる部分はファイル入出力のtyping.IO[str]
的なものが返ってくる。ちなみに"wb"
などには対応していない ("r"
で取り出してもseekが必要だったりするかもしれない)。
openerに別の関数を渡すことで返ってくる値を変えられる。prestringのサブモジュールなのでもちろんprestring.python:Module
などが使えて欲しい(使いかたなどはこの記事では説明しないけれどふんいきで)。
あとしれっとpathlib.Pathオブジェクトにも対応している(はず)。
実行
こういう感じに。ディレクトリも自動で作ってくれる。
$ python src/main.py dst [D] create dst/projects [F] create dst/projects/x.txt [F] create dst/projects/y.txt [F] create dst/projects/z.py
ちなみにもう一回実行したときには何も出力が出ない。VERBOSE=1
を付けると親切なメッセージを表示してくれる。
$ python src/main.py dst $ VERBOSE=1 python src/main.py dst [F] no change dst/projects/x.txt [F] no change dst/projects/y.txt [F] no change dst/projects/z.py
dry-run
実際のファイル出力を避けて実行したいこともある。あるいは変更の前後の差分を確認するときに1ファイルだけの比較で済むと便利なことも多い。CONSOLE=1
という環境変数をつけて実行するとどのファイルが出力されるかだけを教えてくれる(このあたり環境変数を乱用しているけれど、prestring.output:output
のオプションに直接bool値を渡してあげても良い)。
$ CONSOLE=1 python src/main.py dst [F] update dst/projects/x.txt [F] update dst/projects/y.txt [F] update dst/projects/z.py
VERBOSE=1
付きだと出力内容の全てをターミナル上に出力してくれる。
$ VERBOSE=1 CONSOLE=1 python src/00/main.py dst/00/create # dst/00/create/projects/x.txt ---------------------------------------- hello x bye x # dst/00/create/projects/y.txt ---------------------------------------- hello y bye y # dst/00/create/projects/z.py ---------------------------------------- def hello(): print('hello')
prestring.naming
よくあるsnake_case,camelCase,kebab-case用の関数を用意しているので何かの折には便利かもしれない。
$ python -q >>> from prestring.naming import * >>> snakecase("fooBar") 'foo_bar' >>> snakecase("foo-bar") 'foo_bar' >>> camelcase("foo-bar") 'fooBar' >>> kebabcase(camelcase("foo-bar")) 'foo-bar'
特定のディレクトリ名やファイル名はkebab-caseでみたいなことがあったり、設定ファイルではcamelCaseで、ソースコード上ではsnake_caseみたいなことがあったりはするので。
さいごに
prestring.outputを更新した。複数出力自体汎用的な機能なのでパッケージを分けても良いかなと思ったけれど。良いパッケージ名が思いつかないのでとりあえずこのままprestringの中に。
やろうと思えばscaffoldのようなスクリプトを1ファイルに収められて便利。かもしれない。
mypyのLiteral typesのお供には--strict-equalityオプションを
Type hints
pythonでも型を書きたいですよね。type hintsがあります。
これが
def hello(name): return f"hello {name}!"
こう。
def hello(name: str) -> str: return f"hello {name}!"
型が指定できます。
Literal types
ところで型の指定は文字列型だけで十分ですか?特定の文字列だけに値の範囲を制限したくないですか? Lietral typesがあります。
例えばこういう関数が "hello" と "bye" だけを許したい場合には、
def greet(name: str, prefix: str = "hello") -> str: return f"{prefix} {name}!"
こう。
import typing_extensions as tx Action = tx.Literal["hello", "bye"] def greet(name: str, prefix: Action = "hello") -> str: return f"{prefix} {name}!"
ところでここで許可されていない値を渡すとmypyでエラーになります。
greet("hell", prefix="Go to")
"Go to" は "hello" でも "bye" でもないのでエラーです。こういうエラーが出ます。良いですね。
$ mypy --strict 03greet.py 03greet.py:10: error: Argument "prefix" to "greet" has incompatible type "Literal['Go to']"; expected "Union[Literal['hello'], Literal['bye']]"
ちなみに3.8以降はtyping_extensionsのinstallは不要でtypingに含まれます。
ifの条件に。。(嬉しくない)
ところでLiteral typesをif文と一緒に使ってみましょう。
import typing_extensions as tx Direction = tx.Literal["up", "down", "left", "right"] def use(d: Direction) -> None: if d == "UP": # not "up" return print("UUUUUUUUUUUUUUUUUUUU") else: return print("ELSE")
エラーになることを期待。。。
$ mypy --strict 04condition.py Success: no issues found in 1 source file
おっと、成功してしまいました。かなしい。コレはかなしい。
ちなみにTypeScriptでは。良い感じに教えてくれます。
type Direction = "up" | "down" | "left" | "right"; function use(d: Direction){ // こういうエラー // This condition will always return 'false' since the types 'Direction' and '"UP"' have no overlap. if (d == "UP") { // not "up" console.log("UUUUUUUUUUUUUUUUUUUUUUUUPPPPPPP"); } else { console.log("ELSE"); } }
どうにかできないものでしょうか?
--strict-equality
ここで --strict-equality
オプションの出番です。
--strict-equality Prohibit equality, identity, and container checks for non-overlapping types (inverse: --no-strict-equality)
実行してみると良い感じにエラーが出てくれました。
$ mypy --strict --strict-equality 04condition.py 04condition.py:7: error: Non-overlapping equality check (left operand type: "Union[Literal['up'], Literal['down'], Literal['left'], Literal['right']]", right operand type: "Literal['UP']") Found 1 error in 1 file (checked 1 source file)
やりましたね :tada:
まとめ
Literal typesのお供には--strict-equalityオプションを。