リファレンス:
- [mypy で静的型付け Python プログラミング]
(http://t2y.hatenablog.jp/entry/2014/12/22/004525) - コマンドラインオプション付きの which コマンドを実装してみた
前に Python で書いた which コマンドのコードに対して mypy の型アノテーションを追加して静的型チェックを試してみました。
mypy には、かなりの標準ライブラリのスタブがすでに定義されていますが、which コマンドで使っていた argparse, operator, itertools モジュールのクラスやメソッドが足りなかったので自分でスタブを定義してみました。このスタブの定義が慣れてなくてちょっと難しったです。
stubs/argparse.py
from typing import Any, List, Sequence, Undefined
class Namespace:
def __init__(self) -> None:
self.commands = Undefined(List[str])
self.is_all = Undefined(bool)
self.is_silent = Undefined(bool)
class ArgumentParser:
def set_defaults(self, **kwargs: Any) -> None: pass
def add_argument(self, *args: Sequence[str], **kwargs: Any) -> None: pass
def parse_args(self, args: Sequence[str], namespace: Namespace = None) -> Namespace: pass
stubs/itertools.py
from typing import Iterable, typevar
_T = typevar('_T')
class chain:
@classmethod
def from_iterable(cls, iterable: Iterable[Iterable[_T]]) -> Iterable[_T]: pass
stubs/operator.py
from typing import Any, Function, Sequence, typevar
_T = typevar('_T')
def itemgetter(item: _T, *items: Sequence[_T]) -> Function[[Any], _T]: pass
自分で定義したスタブを mypy からみえるようにするには環境変数を設定します。
(mypy)$ export MYPYPATH=./stubs/
実際はスタブの定義と型チェックを繰り返しながら、型アノテーションを書いていったため、うまくいかなくてちょっと戸惑いました。慣れの問題だとは思います。
最終的に which コマンドのソースに型アノテーションを追加したのが which_with_statically_typed.py です。
元のコードとの diff はこんな風になりました。
$ diff -u which.py which_with_statically_typed.py
--- which.py 2014-12-26 12:22:31.000000000 +0900
+++ which_with_statically_typed.py 2014-12-26 16:06:28.000000000 +0900
@@ -7,8 +7,10 @@
from os.path import join as pathjoin
from operator import itemgetter
+from typing import List, Sequence, Tuple # pragma: no flakes
-def search(cmd, paths, is_all=False):
+
+def search(cmd: str, paths: List[str], is_all: bool=False):
for path in paths:
for match in glob.glob(pathjoin(path, cmd)):
if os.access(match, os.X_OK):
@@ -17,7 +19,7 @@
raise StopIteration
-def parse_argument(args=None):
+def parse_argument(args: Sequence[str]=None) -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.set_defaults(is_all=False, is_silent=False, commands=[])
parser.add_argument(
@@ -29,23 +31,23 @@
help="No output, just return 0 if any of the executables are found, "
"or 1 if none are found.")
parser.add_argument("commands", nargs="*")
- args = parser.parse_args(args or sys.argv[1:])
- return args
+ namespace = parser.parse_args(args or sys.argv[1:])
+ return namespace
-def main(cmd_args=None):
+def main(cmd_args: Sequence[str]=None) -> int:
args = parse_argument(cmd_args)
if not args.commands:
print('Usage: python which.py cmd1 [cmd2 ...]')
return 0
env_paths = os.environ['PATH'].split(':')
- result = []
+ result = [] # type: List[Tuple[int, List[str]]]
for cmd in args.commands:
founds = list(search(cmd, env_paths, args.is_all))
result.append((0, founds) if founds else (1, [cmd]))
- status_code = max(map(itemgetter(0), result))
+ status_code = max(map(itemgetter(0), result)) # type: int
if not args.is_silent:
cmd_paths = [paths for ret_val, paths in result if ret_val == 0]
for cmd_path in chain.from_iterable(cmd_paths):
ソース全体は misc/which に置いています。