docker で「no space left on device」エラーになった際の調べごと

  • docker desktop の GUI で Disk Image がほぼ満杯になっていた
  • docker image ls を見たら、イメージの容量が大きいせいっぽい
  • docker image の容量を大きい順にソートして合計容量を見る方法(もっと良い方法あるかも?)
docker images --format "table {{.Size}}\t{{.Repository}}:{{.Tag}}\t{{.ID}}" | \
  sed -e '1d' | sort -h -r | \
  awk 'BEGIN{sum=0.0} {unit=substr($1, length($1)-1, length($1)); vol=substr($1, 1, length($1)-2); if(unit=="MB"){sum+=vol/1024}else if(unit=="kB"){sum+=vol/1024/1024}else{sum+=vol}; print $0} END{print "Total size: " sum "GB"}'

python のデコレータ関係が気になったので調べてみた

前回の記事で Flask アプリの unittest で redirect が想定通りできているかをテストする際は、
test_clientwith ブロックの中で request.path を見ないとダメだよ
と、書いたのですが、redirect のテストのたびに毎回この with ブロックを書くのは面倒だなと思い
デコレーターでできないか試してみました。

意外と大変だったのでメモしておきます。

元コード

import unittest
from request import request

class TestFlaskBlog(unittest.TestCase):
    def setUp(self):
        """ テスト開始前の初期設定 """
        self.app = create_app(test_config={
            'TESTING':True,
        })
        self.client = self.app.test_client()

    def test_error_handle_404(self): 
        # 不明な url の場合はログインページにリダイレクトされる
        client = self.client
        with client: # ←これ毎回書くのダルくね?
             ret = client.get('/hoge', follow_redirects=True)
             assert request.path == '/login'

修正後のコード

import unittest
from request import request
from functools import wraps

class TestFlaskBlog(unittest.TestCase):
    def setUp(self):
        """ テスト開始前の初期設定 """
        self.app = create_app(test_config={
            'TESTING':True,
        })
        self.client = self.app.test_client()

    def with_client(self):
        def wrapper(test_func):
            @wraps(test_func)
            def inner(*args, **kwargs):
                with self.test_client():
                    return test_func(*args, **kwargs)
            return inner
        return wrapper

    @with_client
    def test_error_handle_404(self):
        # 不明な url の場合はログインページにリダイレクトされる
        ret = self.client.get('/hoge', follow_redirects=True)
        assert request.path == '/login'

デコレータとはなんぞや

正直今までデコレータの理解から逃げていたので今回はいい勉強になりました。
まず、デコレータの基本的な使い方。

def wrapper(func):
    def inner(*args, **kwargs):
         print("デコりました!")
         return func(*args, **kwargs)
    return inner

@wrapper
def func(a, b)
    print(a + b)

これで func(1, 2) を実行すると以下のような出力になります。

デコりました!
3

と、こんな感じで、複数の関数でお決まりの前処理のようなものがある場合に
デコレータが活躍します。

wraps ってなんぞや

これは前回紹介した本「ゼロからFlaskがよくわかる本」の中でしれっと使われていたのでが
何のためのものなのか調べてみました。

これをつけないと、 デコレータをつけた関数名を参照しようとしたときに、
デコレータのついている関数ではなく、デコレータになっている関数の名前が表示されてしまうようです。

def wrapper(func):
    """ デコります """
    def inner(*args, **kwargs):
         print("デコりました!")
         return func(*args, **kwargs)
    return inner

@wrapper
def func(a, b)
    """ デコられます """
    print(a + b)

print(func.__name__)
print(func.__doc__)

<関数>.__name__ で関数名が取得できるはずですが、これを実行すると...

wrapper
デコります

となります。func 関数くんが自分を見失ってしまいました。
これを防ぐのが wraps になります。

def wrapper(func):
    """ デコります """
    @wraps(func)
    def inner(*args, **kwargs):
         print("デコりました!")
         return func(*args, **kwargs)
    return inner

@wrapper
def func(a, b)
    """ デコられます """
    print(a + b)

print(func.__name__)
print(func.__doc__)

実行結果

func
デコられます

ラッパーとなる関数で self を使いたい

デコレータの記法を使うには、ラッパーとなる関数には関数オブジェクトのみを渡すようにする必要があるようです。

そのため、ラッパーとなる関数のコアの部分は残しておいて、外側から追加の変数を渡す必要があります。

今回は、test_client がテスト開始時に作成され、それを self.client で保持しているため、 self をラッパー関数の引数に加える必要がありました。
そのため、最終的にラッパー関数は以下のようになりました。

(略)
    def with_client(self):
        def wrapper(test_func):
            @wraps(test_func)
            def inner(*args, **kwargs):
                with self.test_client():
                    return test_func(*args, **kwargs)
            return inner
        return wrapper
(略)

最後に

この記事書いてる途中で思ったけど、 redirect テスト用の共通関数作った方が早くね...?
まあ、勉強になったからヨシとします。

「ゼロからFlaskがよくわかる本」を読み終わった感想

最近手を動かす系の本から遠ざかっていたので、 web アプリ関連の勉強も兼ねて 「ゼロからFlaskがよくわかる本」を読んでみました。

www.amazon.co.jp

kindle unlimited で無料で読めるのに解説が結構詳しくてありがたかったです。

内容はブログ用の web アプリケーションを Flask で作るというものでした。

いくつか詰まって自力で解決した部分と感動した部分を備忘録として載せておきます。 環境は Mac です。

pipenv を VSCode で使う

$ pip install pipenv

$ pipenv --python 3.8 # Pipfile が作られる

$ pipenv shell # 仮想環境に入る

$ which python # 仮想環境で使用している python のパスがわかる
# このパスを VSCode のインタープリターのパスに登録すると pipenv で install したパッケージを元に予測してくれる

$ pipenv install <パッケージ名> # pip install の代わり
# 初めてパッケージを install すると Pipfile.lock が作成される

$ pipenv requirements # install したパッケージ群が見られる。 Pipfile.lock がないとエラーを吐かれる。

仮想環境作るのめっちゃ楽になってる。。すごい。

pipenv で環境変数を設定する

本書では .env環境変数の設定を書いておけば flask run でも自動的に反映されるかのように書いてあったがうまくいかなかった。以下のように実行しないと .env で設定した環境変数が反映されない。

$ pipenv run flask run

Jinja2 のフォーマットを VSCode が認識できるようにする

  1. 「Better Jinja」というプラグインを入れる marketplace.visualstudio.com
  2. setting.json に以下を加える

    "emmet.includeLanguages": {
     "jinja-html": "html"
    },
    "[jinja-html]": { "editor.formatOnSave": false },
    
  3. VSCode 最下部のバーにある「言語モードの選択」をクリック

  4. 「Jinja HTML」を選択
  5. {{ }}{% %} に色がついたり、exte ぐらいまで打つと {% extends "" %} を予測してくれたりする。

textarea で入力したテキストをデータベースから取得して HTML で表示する

普通にやると、改行の \r がただのスペースとして表示されてしまう。

HTML に渡す前に \r<br> に変える必要がある

from flask import Markup
# text の改行コードをマークアップ言語に置き換える
text = Markup(text.replace('\r', '<br>'))

redirect ができているかを unittest でテストする

本書のテストでは、 flash のメッセージを見てログイン・ログアウトの成功・失敗を確認するだけだったので、他の処理のテストの仕方が気になった。

test_clientwith ブロックの中でないと、 request が使えないので注意。

import unittest
from request import request

class TestFlaskBlog(unittest.TestCase):
    def setUp(self): # こういう名前の関数を作っておくとテスト開始前に勝手に実行してくれる
        """ テスト開始前の初期設定 """
        self.app = create_app(test_config={
            'TESTING':True,
        })
        self.client = self.app.test_client()
    
    def test_error_handle_404(self): # テスト用のメソッドは、先頭が test_ で始まらないと流れてくれない
        # 不明な url の場合はログインページにリダイレクトされる
        client = self.client
        with client:
            ret = client.get('/hoge', follow_redirects=True)
            assert request.path == '/login'

coverage パッケージすげー

coverage というパッケージを使うと unittestcoverage コマンド経由で流してくれて、見やすいレポートまで作ってくれる。

カバレッジが簡単に出せるのもすごいけど、html でどの行を今のテストでカバーできていないかが視覚的にわかるのがとても嬉しい。テスト駆動開発全然できてないけど、これだったらできそう(?)

$ pipenv install coverage

$ coverage run -m unittest

$ coverage report -m # CLI 上でカバレッジのレポートを出力する
Name                     Stmts   Miss  Cover   Missing
------------------------------------------------------
blog/__init__.py            14      0   100%
blog/config.py               6      0   100%
blog/models/entries.py      16      4    75%   22-24, 28
blog/scripts/db.py           8      0   100%
blog/views/auth.py          30      0   100%
blog/views/entries.py       54     25    54%   21, 28-36, 43-48, 58-60, 66-73, 78-82
------------------------------------------------------
TOTAL                      128     29    77%

$ coverage html # レポート用の html を作成する