prestringに複数ファイルの出力機能を付けた

github.com

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オプションを。