見出し画像

responderで理解するWebサーバー #4 デバッグ

こんにちは、Webエンジニアのjuri-tです。

「あれ、APIの仕様に沿って書いてるのに思ったように動かない・・」「サンプル通りに書いたのに動かない・・」なんてことは、プログラムを書いていると日常茶飯事だと思います。私もしょっちゅうあります。頭の中では「なんでや工藤!」とか「どういうことだってばよ!」とか言ってます。ときどきSlackにも漏れてます。

そんな日常を揶揄してプログラマーの世界にはこんな格言があります。

プログラムは思った通りに動かない。書いた通りに動く。

さて、思った通りに動かないとき、みなさんはどうしますか?

そうです、デバッグですね。タイトルの時点でネタバレですね。そんなわけでresponderにおけるデバッグについて書きたいと思います。

デバッグ(debug)とは、コンピュータプログラムや電気機器中のバグ・欠陥を発見および修正し、動作を仕様通りのものとするための作業である。

出典:Wikipedia

今回のサンプル

import responder

api = responder.API()

@api.route("/")
async def index(req, resp):
   if req.method == 'GET':
       resp.content = api.template("index.html")
   else:
       resp.status_code = 404
       resp.content = api.template("404_not_found.html")

if __name__ == '__main__':
   api.run()

簡単なサンプルを用意しました。server.pyとして保存してください。

<!Doctype html>
<html lang="ja">
<head>
 <title>Index</title>
</head>
<body>
<h1>Hello, World</h1>
</body>
</html>
<!Doctype html>
<html lang='ja'>
<head>
 <title>Not Found</title>
</head>
<body>
<h1>Not Found</h1>
</body>
</html>

templateは好きなものを使ってください。一応私が使ったサンプルも合わせて載せておきます。上がindex.htmlで、下が404_not_found.htmlです。

server.py
templates
  - 404_not_found.html
  - index.html

ファイル構成としてはこんな感じです。テンプレートを置くフォルダを変える方法は以前紹介してますので、よければこちらもご参照ください。


uvicorn server:api --reloadで開発効率をあげる

上のサンプルはpythonの実行ファイルとして以下のように起動できます。

$ python server.py

一方、以下のようにuvicornを使って起動することもできます。なければpipでinstallしてください。

$ uvicorn server:api --reload

responderは内部でuvicornを呼んでいるので動作的には大差ないんですが、--reloadオプションをつけることで、ファイル変更を検知して再起動してくれるのでいちいちサーバーを再起動する手間がなくなり開発がかなり楽になります。このときportを指定しないとデフォルトの8000番ポートになります。前者のpython server.pyだと5042番になるので、気をつけましょう。

ちなみに--debugオプションでも同じ動作になります。ドキュメントには記載されていないようです。

閑話休題。

さぁ、とりあえず実行してみましょう

uvicornでサーバーを立ち上げましたか?http://localhost:8000にアクセスしてみましょう。Hello, Worldが表示されましたか?されませんね。Not Foundが出たはずです。さぁ、デバッグしましょう。

伝家の宝刀 printデバッグ

デバッグと一口にいっても実際にはいくつか種類があります。まずはなんといってもprintデバッグです。読んで字の如く実行処理の中にprintによる標準出力を仕込むことで処理を追う方法です。メリットはシンプルで簡単にできることですね。

import responder

api = responder.API()

@api.route("/")
async def index(req, resp):
   print(f'req.method: {req.method}')
   if req.method == 'GET':
       resp.content = api.template("index.html")
   else:
      resp.status_code = 404
      resp.content = api.template("404_not_found.html")

if __name__ == '__main__':
   api.run()

今回、怪しいポイントは一箇所しかないので、変数を出力しています。

INFO: Waiting for application shutdown.
INFO: Finished server process [7746]
INFO: Started server process [7762]
INFO: Waiting for application startup.
req.method: get
INFO: ('127.0.0.1', 59814) - "GET / HTTP/1.1" 200

上のようなメッセージが表示されているかと思います。見ると、「req.methodの中身のgetは小文字になっているのか・・」となり、めでたく原因がわかりました。これがprintデバッグです。

import responder

api = responder.API()

@api.route("/")
async def index(req, resp):
   if req.method == 'get':
       resp.content = api.template("index.html")
   else:
       resp.status_code = 404
       resp.content = api.template("404_not_found.html")

if __name__ == '__main__':
   api.run()

これで問題なくindex.htmlが表示されてますね。

奥義 pdbデバッグ

さて、printデバッグでも個人差はありますがそこそこ頑張れます。しかし、条件分岐が複雑だったりオブジェクトが複数存在しているなど、printデバッグだとどうしても効率が低いことがあります。そのときはデバッガーを入れましょう。Pythonのデバッガーといえばpdbですね。

ちなみにpdbデバッグの使い方については言及しませんのであしからず。

import responder

api = responder.API()

@api.route("/")
async def index(req, resp):
   from pdb import set_trace; set_trace()
   if req.method == 'get':
       resp.content = api.template("index.html")
   else:
       resp.status_code = 404
       resp.content = api.template("404_not_found.html")

if __name__ == '__main__':
   api.run()

pdbデバッグを仕込みましたが、このままこのパスにリクエストを送っても処理は中断できません。500エラーが返ってきてるでしょう。

主なデバッグ方法は3つあります。

IDEを使う

これはJetBrainのIntellij IDEAのスクショですが、ブレークポイントを設定して、Debug実行すればデバッグできます。実はpdbの設定も不要です。他のIDEでも出来ると思います。IDEを使えるならこれが楽ですね。

よく知らないんですが、IDEはなぜデバッグできるのでしょうね?IDEがなくても出来るのでしょうか?

テストクライアントを使う

import responder

api = responder.API()

@api.route("/")
async def index(req, resp):
   from pdb import set_trace;
   set_trace()
   if req.method == 'get':
       resp.content = api.template("index.html")
   else:
       resp.status_code = 404
       resp.content = api.template("404_not_found.html")

def test_index():
   client = api.session()
   response = client.get("/")
   assert response.status_code == 200

if __name__ == '__main__':
   test_index()

test_indexという関数を追加して、スクリプト実行時に呼び出すようにしました。このファイルを実行するとpdbデバッグを仕込んだところでデバッグできたかと思います。

APIインスタンスにはsessionメソッドというものがあります。これはHTTPセッションとは何の関係もなく、公式リファレンスに書いてあるようにresponderのAPIのテストができるhttpクライアントです。

内部的にはStartletteのTestClientのインスタンスを返しており、PythonのhttpクライアントであるrequestsライブラリのAPIが使えます。したがって、requestsライブラリのように、getメソッドを送ることでデバッグできるという寸法です。これならIDEなくても簡単ですね。

テストコードを書く

import pytest
import server

@pytest.fixture
def api():
   return server.api

def test_index(api):
   response = api.requests.get("/")
   assert response.status_code == 200

server.pyと同じフォルダにtest_server.pyとして保存しましょう。pytestは別途インストールしてください。

$ pytest

とすれば、set_traceした箇所でpdbデバッグできたかと思います。テストクライアントと大差ない労力でした。pytestを追加でインストールするぐらいでしたが、ほぼ公式リファレンス通りです。

公式のテストコードがpytestだったのでpytestを使いましたが、他のテストライブラリでも大差ないでしょう(たぶん)。非同期対応してないライブラリだともしかしたらちょっと辛いかもしれません。

デバッグ完了

はい、という感じでデバッグすれば「わかったで工藤!」と言えます。めでたしめでたし。

サポートありがとうございます。頂いたご支援は美味しいものを食べに行きます。