Django 2.0 + Channels 2.1 でチュートリアルやっていく(2)
前回のおさらい
前回は簡単なプロジェクトのセットアップで終わっていました.これから実際にWebSocketを捌く実装をやっていくことになります.
Tutorial Part 2: Implement a Chat Server
chat roomのviewを書く
chat/templates/room.html
を作成します.下記コードをコピペでOKです.
<!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <title>Chat Room</title> </head> <body> <textarea id="chat-log" cols="100" rows="20"></textarea><br/> <input id="chat-message-input" type="text" size="100"/><br/> <input id="chat-message-submit" type="button" value="Send"/> </body> <script> var roomName = {{ room_name_json }}; var chatSocket = new WebSocket( 'ws://' + window.location.host + '/ws/chat/' + roomName + '/'); chatSocket.onmessage = function(e) { var data = JSON.parse(e.data); var message = data['message']; document.querySelector('#chat-log').value += (message + '\n'); }; chatSocket.onclose = function(e) { console.error('Chat socket closed unexpectedly'); }; document.querySelector('#chat-message-input').focus(); document.querySelector('#chat-message-input').onkeyup = function(e) { if (e.keyCode === 13) { // enter, return document.querySelector('#chat-message-submit').click(); } }; document.querySelector('#chat-message-submit').onclick = function(e) { var messageInputDom = document.querySelector('#chat-message-input'); var message = messageInputDom.value; chatSocket.send(JSON.stringify({ 'message': message })); messageInputDom.value = ''; }; </script> </html>
簡単にコードを読むとjavascriptでWebSocketクライアントを生成し,メッセージの送信を行おうとしていることがわかります.
次にこれを返すviewをchat/views.py
に定義します.
from django.shortcuts import render from django.utils.safestring import mark_safe import json def index(request): return render(request, 'chat/index.html', {}) def room(request, room_name): return render(request, 'chat/room.html', { 'room_name_json': mark_safe(json.dumps(room_name)) })
chat/urls.py
へルーティングを追加します.
from django.urls import path from . import views urlpatterns = [ path('', views.index, name='index'), # チュートリアルの元コードは→url(r'^(?P<room_name>[^/]+)/$', views.room, name='room'), path('<slug:room_name>/', views.room, name='room'), ]
動かしてみます.
$ python manage.py runserver
127.0.0.1:8000/chat/lobby/
へアクセスしてみます.このlobby
というのはチャットルームの名前になります(なんでもいい).
以下の様なページが表示されます.
またコンソールに下記の様なエラーが出ます.これは想定内のエラーです.
[2018/05/11 00:04:55] HTTP GET /chat/lobby/ 200 [0.03, 127.0.0.1:63086] 2018-05-11 00:04:55,440 - ERROR - ws_protocol - [Failure instance: Traceback: <class 'ValueError'>: No application configured for scope type 'websocket' /Users/pudding/ghq/github.com/pddg/channels_tutorial1/env/lib/python3.6/site-packages/twisted/internet/defer.py:500:errback /Users/pudding/ghq/github.com/pddg/channels_tutorial1/env/lib/python3.6/site-packages/twisted/internet/defer.py:567:_startRunCallbacks /Users/pudding/ghq/github.com/pddg/channels_tutorial1/env/lib/python3.6/site-packages/twisted/internet/defer.py:653:_runCallbacks /Users/pudding/ghq/github.com/pddg/channels_tutorial1/env/lib/python3.6/site-packages/twisted/internet/defer.py:1442:gotResult --- <exception caught here> --- /Users/pudding/ghq/github.com/pddg/channels_tutorial1/env/lib/python3.6/site-packages/twisted/internet/defer.py:1384:_inlineCallbacks /Users/pudding/ghq/github.com/pddg/channels_tutorial1/env/lib/python3.6/site-packages/twisted/python/failure.py:422:throwExceptionIntoGenerator /Users/pudding/ghq/github.com/pddg/channels_tutorial1/env/lib/python3.6/site-packages/daphne/server.py:186:create_application /Users/pudding/ghq/github.com/pddg/channels_tutorial1/env/lib/python3.6/site-packages/twisted/python/threadpool.py:250:inContext /Users/pudding/ghq/github.com/pddg/channels_tutorial1/env/lib/python3.6/site-packages/twisted/python/threadpool.py:266:<lambda> /Users/pudding/ghq/github.com/pddg/channels_tutorial1/env/lib/python3.6/site-packages/twisted/python/context.py:122:callWithContext /Users/pudding/ghq/github.com/pddg/channels_tutorial1/env/lib/python3.6/site-packages/twisted/python/context.py:85:callWithContext /Users/pudding/ghq/github.com/pddg/channels_tutorial1/env/lib/python3.6/site-packages/channels/staticfiles.py:41:__call__ /Users/pudding/ghq/github.com/pddg/channels_tutorial1/env/lib/python3.6/site-packages/channels/routing.py:58:__call__ ] Exception in callback AsyncioSelectorReactor.callLater.<locals>.run() at /Users/pudding/ghq/github.com/pddg/channels_tutorial1/env/lib/python3.6/site-packages/twisted/internet/asyncioreactor.py:287 handle: <TimerHandle when=253833.004511811 AsyncioSelectorReactor.callLater.<locals>.run() at /Users/pudding/ghq/github.com/pddg/channels_tutorial1/env/lib/python3.6/site-packages/twisted/internet/asyncioreactor.py:287> Traceback (most recent call last): File "/usr/local/var/pyenv/versions/3.6.0/Python.framework/Versions/3.6/lib/python3.6/asyncio/events.py", line 126, in _run self._callback(*self._args) File "/Users/pudding/ghq/github.com/pddg/channels_tutorial1/env/lib/python3.6/site-packages/twisted/internet/asyncioreactor.py", line 290, in run f(*args, **kwargs) File "/Users/pudding/ghq/github.com/pddg/channels_tutorial1/env/lib/python3.6/site-packages/twisted/internet/tcp.py", line 289, in connectionLost protocol.connectionLost(reason) File "/Users/pudding/ghq/github.com/pddg/channels_tutorial1/env/lib/python3.6/site-packages/autobahn/twisted/websocket.py", line 128, in connectionLost self._connectionLost(reason) File "/Users/pudding/ghq/github.com/pddg/channels_tutorial1/env/lib/python3.6/site-packages/autobahn/websocket/protocol.py", line 2467, in _connectionLost WebSocketProtocol._connectionLost(self, reason) File "/Users/pudding/ghq/github.com/pddg/channels_tutorial1/env/lib/python3.6/site-packages/autobahn/websocket/protocol.py", line 1096, in _connectionLost self._onClose(self.wasClean, WebSocketProtocol.CLOSE_STATUS_CODE_ABNORMAL_CLOSE, "connection was closed uncleanly (%s)" % self.wasNotCleanReason) File "/Users/pudding/ghq/github.com/pddg/channels_tutorial1/env/lib/python3.6/site-packages/autobahn/twisted/websocket.py", line 171, in _onClose self.onClose(wasClean, code, reason) File "/Users/pudding/ghq/github.com/pddg/channels_tutorial1/env/lib/python3.6/site-packages/daphne/ws_protocol.py", line 146, in onClose self.application_queue.put_nowait({ AttributeError: 'WebSocketProtocol' object has no attribute 'application_queue'
ブラウザのコンソールにも下記の様なエラーが出ているかと思います*1.
WebSocket connection to 'ws://127.0.0.1:8000/ws/chat/lobby/' failed: Error during WebSocket handshake: net::ERR_CONNECTION_RESET
Consumerを書く
DjangoがHttpリクエストを受け,urlの設定を元にview関数を呼び出してリクエストを処理するように,channelsはWebSocketのコネクションを受け入れると,consumerを呼び出しイベントを処理します.
今回は非常に簡単な,送られてきた文字列をオウム返しするだけのconsumerを作成します.
chat/consumers.py
を作成します.
from channels.generic.websocket import WebsocketConsumer import json class ChatConsumer(WebsocketConsumer): def connect(self): self.accept() def disconnect(self, close_code): pass def receive(self, text_data): text_data_json = json.loads(text_data) message = text_data_json['message'] self.send(text_data=json.dumps({ 'message': message }))
WebsocketConsumer
というそのものズバリなクラスを継承し,独自のconsumerを作成します.
receive()
で送られてくるjson形式の文字列を処理して中からmessageを取り出し,またjsonに埋め込んで返します.簡単な作りです.
次にchat/routing.py
を新しく作り,以下の様に記述します.
from django.urls import path from . import consumers websocket_urlpatterns = [ # 元のコードは→url(r'^ws/chat/(?P<room_name>[^/]+)/$', consumers.ChatConsumer), path('ws/chat/<slug:room_name>/', consumers.ChatConsumer), ]
更に,前回作成しておいたchannels_tutorial1/routing.py
も下記の様に編集します.
from channels.auth import AuthMiddlewareStack from channels.routing import ProtocolTypeRouter, URLRouter from chat import routing application = ProtocolTypeRouter({ # (http->django views is added by default) 'websocket': AuthMiddlewareStack( URLRouter( routing.websocket_urlpatterns ) ), })
このとき,ProtocolTypeRouter
はサーバへのアクセスのプロトコルを見て,websocketであればAuthMiddlewareStack
に接続を投げます.
AuthMiddlewareStack
はAuthenticationMiddleware
のような働きをし,認証ユーザへの参照を付与します.その後URLRouter
へ渡され,これが接続するパスを見てルーティングするconsumerを決めます.
ではここで動かして確認してみます.
$ python manage.py runserver
同じくhttp://127.0.0.1:8000/chat/lobby
にアクセスし,適当にメッセージを打って送ります.例ではhoge
とかfuga
とかpiyo
とか送ったときの図です.
わーい!返ってきた〜〜〜
しかし,別のタブを開いて同じチャットルームを開いても,タブ間でのやりとりはまだできません.これを実行可能にするためにchannel layerを使用します.
channel layerを有効化する
channel layerはconsumerのインスタンスが互いに,またはDjangoのオブジェクトと通信できるようにする一種の通信システムです,channel layerは下記の様な事象を抽象化します.
- channel.メッセージボックスのようなもので,名前が付けられている.名前を持つ人はここにメッセージを送信できる.
- group.これは関連するchannelの集まりで,同じように名前が付けられている,名前を持つ人はこのgroupに自由にchannelを追加し,全てのchannelにメッセージを送信できる.
consumerインスタンスはそれぞれ一意な名前が自動生成されており,channel layerで互いに通信できるようになっています.
このchannel layerのバックエンドにRedisを使用します.チュートリアルではdockerでredisサーバを起動していますが,起動できれば何でも良いと思います*2.
$ docker run -d -p 6379:6379 redis
channelsでredisを扱うためのパッケージを導入します.
$ pip install channels_redis
settings.py
にchannel backendの設定を記述します.
CHANNEL_LAYERS = { 'default': { 'BACKEND': 'channels_redis.core.RedisChannelLayer', 'CONFIG': { "hosts": [('127.0.0.1', 6379)], }, }, }
- Redisサーバが起動
- ChannelバックエンドにRedisを指定
までできれば,簡単にコマンドラインで動作を確かめます.
$ python manage.py shell >>> import channels.layers >>> channel_layer = channels.layers.get_channel_layer() >>> from asgiref.sync import async_to_sync >>> async_to_sync(channel_layer.send)('test_channel', {'type': 'hello'}) >>> async_to_sync(channel_layer.receive)('test_channel') {'type': 'hello'}
このとき,例えば一度receiveした後に再びreceiveすると入力されるまで待ちます(Control+Cで終了します).
確認が出来たところでconsumerを書き直し,consumerのインスタンス間で通信できるようにします.
from asgiref.sync import async_to_sync from channels.generic.websocket import WebsocketConsumer import json class ChatConsumer(WebsocketConsumer): def connect(self): self.room_name = self.scope['url_route']['kwargs']['room_name'] # 元のコードは→self.room_group_name = 'chat_%s' % self.room_name self.room_group_name = 'chat_{}'.format(self.room_name) # Join room group async_to_sync(self.channel_layer.group_add)( self.room_group_name, self.channel_name ) self.accept() def disconnect(self, close_code): # Leave room group async_to_sync(self.channel_layer.group_discard)( self.room_group_name, self.channel_name ) # Receive message from WebSocket def receive(self, text_data, **kwargs): text_data_json = json.loads(text_data) message = text_data_json['message'] # Send message to room group async_to_sync(self.channel_layer.group_send)( self.room_group_name, { 'type': 'chat_message', 'message': message } ) # Receive message from room group def chat_message(self, event): message = event['message'] # Send message to WebSocket self.send(text_data=json.dumps({ 'message': message }))
cunsumerインスタンスは作成時に接続されてきたURLのパスに従ってグループに所属し,グループ内でインスタンス間のメッセージ送受信が出来るようになります.
サーバを起動して確かめます.
$ python manage.py runserver
ブラウザのタブを2つ並べて開き,メッセージを送信します.どちらの画面にも応答が返ってくることが分かります.
だんだんWebSocketをやっているという気持ちになってきましたね.次はPart3です.
Django 2.0 + Channels 2.1 でチュートリアルやっていく(1)
はじめに
channelsの公式チュートリアルを順番にやっていこうというだけのものです.Djangoの基礎知識に関しては一切触れません.ご了承ください.
そろそろWebSocketもやらないとね?って天の声に言われたので,今回はTutorial Part 1: Basic Setupからやっていきます.Django 2.0を使用しているので,チュートリアルとところどころコードが違います.
環境構築
あらかじめ断っておくとWindowsではやりません.
仮想環境としてvenvを使用していきます.
$ python3 -V 3.6.0 $ python3 -m venv env $ source env/bin/activate
必要なライブラリをインストールします
$ pip install --upgrade pip $ pip install django channels $ pip freeze asgiref==2.3.0 async-timeout==2.0.1 attrs==18.1.0 autobahn==18.4.1 Automat==0.6.0 channels==2.1.1 constantly==15.1.0 daphne==2.1.1 Django==2.0.5 hyperlink==18.0.0 idna==2.6 incremental==17.5.0 pytz==2018.4 six==1.11.0 Twisted==18.4.0 txaio==2.10.0 zope.interface==4.5.0
今回作るもの
In this tutorial we will build a simple chat server. It will have two pages:
・ An index view that lets you type the name of a chat room to join.
・ A room view that lets you see messages posted in a particular chat room.The room view will use a WebSocket to communicate with the Django server and listen for any messages that are posted.
とあるように,チャットアプリケーションを作成します.
仕様
- 2つのページを作る
- 参加するチャットルームの名前を入力する初期ページ
- 特定のルームに投稿されたメッセージを見れるルームページ
- ルームのページではWebSocketを使用してDjangoサーバとの通信を行う
Tutorial Part1: Basic Setup
アプリケーション初期設定
プロジェクト名はchannels_tutorial1
,アプリケーション名はchat
とします.
$ django-admin startproject channels_tutorial1 $ cd channels_tutorial1 $ python manage.py startapp chat $ tree -L 2 . ├── channels_tutorial1 │ ├── __init__.py │ ├── __pycache__ │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── chat │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ ├── models.py │ ├── tests.py │ └── views.py └── manage.py 4 directories, 11 files
settings.py
を日本語環境向けに編集します.106~108行目あたりです.
LANGUAGE_CODE = 'ja-jp' TIME_ZONE = 'Asia/Tokyo'
シークレットキーがそのまま埋め込まれているのが精神的に良くないのでdotenvで分離します.
$ pip install python-dotenv
manage.py
と同じディレクトリに下記のような.env
ファイルを用意します.値は適当に入れてください.
SECRET_KEY=f%1(dv)6-)sadf3-c+vr*8b1j&2=^38o^&i!ekni6%=y^sxogsdif DEBUG=True
dotenv.load_dotenv
で.env
ファイルをロードします.
import os import dotenv BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # .envを読み込み dotenv.load_dotenv(os.path.join(BASE_DIR, '.env')) SECRET_KEY = os.getenv('SECRET_KEY') DEBUG = os.getenv('DEBUG', 'False').lower() == 'true'
INSTALLED_APPS
にchat
アプリを追加します
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'chat' ]
初期ページ追加
chat
ディレクトリの中にテンプレートを入れるためのtemplates
ディレクトリを作成し,更に中にchat
ディレクトリを作成,index.html
を配置します.
$ pwd /path/to/channels_tutorial1 $ cd chat $ mkdir templates $ cd templates $ mkdir chat $ touch chat/index.html
index.html
を下記のように編集します.
<!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <title>Chat Rooms</title> </head> <body> What chat room would you like to enter?<br/> <input id="room-name-input" type="text" size="100"/><br/> <input id="room-name-submit" type="button" value="Enter"/> </body> <script> document.querySelector('#room-name-input').focus(); document.querySelector('#room-name-input').onkeyup = function(e) { if (e.keyCode === 13) { // enter, return document.querySelector('#room-name-submit').click(); } }; document.querySelector('#room-name-submit').onclick = function(e) { var roomName = document.querySelector('#room-name-input').value; window.location.pathname = '/chat/' + roomName + '/'; }; </script> </html>
chat/views.py
に下記の様にindex.html
をrenderして返す関数を定義します.
from django.shortcuts import render def index(request): return render(request, 'chat/index.html', {})
chat/urls.py
を作成し,下記の様にindexへのurlpatternを定義します.
from django.urls import path from .views import index urlpatterns = [ # 元のコードは→url(r'^$', views.index, name='index'), path('', index, name='index'), ]
settings.py
と同階層にあるurls.py
がルートになっているので,ここからchat/urls.py
を取り込みます.
from django.contrib import admin from django.urls import path, include from chat import urls as chat_urls urlpatterns = [ # 元のコードは→url(r'^chat/', include('chat.urls')), path('chat/', include(chat_urls)), # 元のコードは→url(r'^admin/', admin.site.urls), path('admin/', admin.site.urls), ]
ここまで出来たら一度runserver
で動かして,http://127.0.0.1:8000/chat/
にアクセスしてみます.
$ python manage.py runserver System check identified no issues (0 silenced). May 05, 2018 - 01:07:36 Django version 2.0.5, using settings 'channels_tutorial1.settings' Starting development server at http://127.0.0.1:8000/ Quit the server with CONTROL-C.
下記の様なページが表示されると思います.
ASGI/Channelsサーバの起動
ここまでは標準のDjangoの機能を使用してきましたが,ここからはchannelsモジュールを使用して行きます.
まず,channelsが用いるルーティングファイルを記述します.これはurls.py
のようなものに相当するようです.channels_tutorial1/routing.py
を作成し下記の様に編集します.
from channels.routing import ProtocolTypeRouter application = ProtocolTypeRouter({ # (http->django views is added by default) })
INSTALLED_APPS
にchannels
を追加し,ASGI_APPLICATION
を定義します.
INSTALLED_APPS = [ # 省略 'chat', 'channels', ] ASGI_APPLICATION = 'channels_tutorial1.routing.application'
runserver
してみると,先ほどとは違う,ASGI/Channelsサーバが起動します((チュートリアルではこれがいくつかのrunserverをラップしてしまうようなコマンドに対して競合する可能性を示唆しています.例えば静的ファイルを配信する設定を簡便にするwhitenoise
などがその筆頭のようで,その場合はchannels
をINSTALLED_APPS
の先頭に持ってくる,それらのアプリケーションを削除するなどが対策として上げられています.)).
$ python manage.py runserver Performing system checks... System check identified no issues (0 silenced). May 05, 2018 - 01:16:55 Django version 2.0.5, using settings 'channels_tutorial1.settings' Starting ASGI/Channels version 2.1.1 development server at http://127.0.0.1:8000/ Quit the server with CONTROL-C. 2018-05-05 01:16:55,548 - INFO - server - HTTP/2 support not enabled (install the http2 and tls Twisted extras) 2018-05-05 01:16:55,548 - INFO - server - Configuring endpoint tcp:port=8000:interface=127.0.0.1 2018-05-05 01:16:55,555 - INFO - server - Listening on TCP address 127.0.0.1:8000
再びhttp://127.0.0.1:8000
にアクセスし,表示されれば完了です.次回Part 2に続きます.
Travis CI + Codecov + NoseでCIしてSlackに通知
きっかけ
これまでもTravis CIを使ってきていて、それ自体にはそんなに戸惑うことは無かったんですが、カバレッジも取得したくなったりしたので少し調べたら日本語情報があまりなかったのでここに残しておくことにしました。
環境
開発環境は以下
必要なPythonモジュールなど
- nose 1.3.7
- codecov 2.0.15
- coverage 4.5.1
各種サービスのセットアップ
Travis CI
Circle CIがどうにも複雑に見えてしまいずっとTravisを使っているため、今回もTravisです。
特に難しいところはなく、普通にサインアップしてCIしたいリポジトリを有効にするだけです。Build only if .travis.yml is present
にチェックを入れておくと良いと思います。
また、シークレットなトークンを暗号化するのに使うtravis
というコマンドをgem
でインストールします。
$ gem install travis $ travis login
.travis.ymlの文法エラーでCIをパスできないとめちゃくちゃ悲しくなるのでlint
コマンドを覚えると良いと思います。
$ cat .travis.yml language: python python: - "3.5" - "3.6" script: - nosetests -v $ travis lint Hooray, .travis.yml looks valid :)
Codecov
Codecovはカバレッジを表示してくれるWebサービスです。Pythonにはcodecov
というpipでインストールできるモジュールがあります*1。
同様のサービスとしてCoverallsなどがありますが、あまりデザインが好みで無かった上、Python用のパッケージが2種類あってうーんという感じだったのでこちらにしました*2。
上記リンクから、Githubアカウント、Gitlabアカウント、Bitbucketアカウントでサインアップできます。それぞれのオンプレ版にも対応しているみたいなので、かなり良さそうです。また、Teamでの使用もパブリックリポジトリなら無制限?のようです。プライベートリポジトリでの利用を検討している場合はリポジトリごと、またはユーザ数単位での課金になるようです*3。
有効にするリポジトリを選択するとトークンが表示され、カバレッジリポートのアップロードをしろみたいな画面になりますが、Travis CIから使うなら無視して良いです。
設定はもう終わりです(すごい)。
リポジトリのセットアップ
テストする
僕はずっとnoseを使っているので、今回もこれで行きます。フォルダ構成は以下の様にしました。student_portal_crawler
が今回テストするモジュールです。
$ tree -L 3 -I venv . ├── LICENSE ├── README.md ├── requirements.txt └── student_portal_crawler ├── __init__.py ├── browser.py ├── page ├── parser └── test ├── __init__.py └── test_script.py
noseのインストールおよびテスト実行は下記の様にします。テストの書き方についてはここでは解説しません。
$ pip install nose # テストの実行 $ nosetests ............................. ---------------------------------------------------------------------- Ran 20 tests in 0.680s OK # テストの実行とカバレッジの計算 $ pip install coverage $ nosetests --with-coverage
このままでは使用しているモジュール全てのカバレッジを取得してしまうので、--cover-package
オプションで取得するモジュールを指定してやります。今回の場合はstudent_portal_crawler
になります。
$ nosetests --with-coverage --cover-package=student_portal_crawler ............................. Name Stmts Miss Branch BrPart Cover --------------------------------------------------------------------------------------- student_portal_crawler/__init__.py 1 0 0 0 100% student_portal_crawler/browser.py 20 0 2 0 100% student_portal_crawler/page/__init__.py 1 0 0 0 100% student_portal_crawler/page/base.py 34 4 6 1 78% student_portal_crawler/parser/__init__.py 4 0 0 0 100% student_portal_crawler/parser/base.py 38 0 10 0 100% student_portal_crawler/parser/lec_info.py 18 0 6 0 100% student_portal_crawler/parser/static.py 5 0 0 0 100% student_portal_crawler/parser/utils.py 9 0 2 0 100% student_portal_crawler/shibboleth_login/__init__.py 1 0 0 0 100% student_portal_crawler/shibboleth_login/login.py 53 2 8 0 97% --------------------------------------------------------------------------------------- TOTAL 184 6 34 1 95% ---------------------------------------------------------------------- Ran 29 tests in 0.319s OK
.travis.ymlを書く
シンプルにPythonのバージョンいくつかでテストし、パスしたらCodecovにカバレッジを送信、Slackに結果を通知します。
まずは自分のSlackのIntegrationからTravis CI
を追加します。Hubotとかで節約している人も居るかとは思いますが、僕は面倒なので豪華に行きます。
追加したら投稿するチャンネルを決め、トークンを取得します。
Travis CIでの使い方についての説明の中に、トークンの暗号化についての項目があるのでコピペして実行します。事前に.travis.ymlがないとエラーが出るので作成しておきます。
$ touch .travis.yml $ travis encrypt "{{チーム名}}:{{トークン}}" --add notifications.slack $ cat .travis.yml notifications: slack: secure: npdNlesznnxrC7KsuB7....
Travis CIで利用可能なPythonバージョンは複数有り、最新は3.8-dev
やnightly
まで使える*4ようなのですが、
Recent Python development branches require OpenSSL 1.0.2+. As this library is not available for Trusty, 3.7-dev, 3.8-dev, and nightly do not work (or use outdated archive).
という記述があり、どうやったら使えるのか分からず3.6-dev
までしか使っていません…*5
最終的に下記の様にしました。
language: python python: - "3.5" - "3.5-dev" - "3.6" - "3.6-dev" install: - pip install -r requirements.txt - pip install codecov coverage script: - nosetests -v --with-coverage --cover-package=student_portal_crawler after_success: - codecov notifications: email: false slack: secure: npdNlesznnxrC7K...
after_success
でcodecov
コマンドを実行することで、Codecovにカバレッジリポートが送信されます。トークンなどを加える必要は有りません。
また、Slackへの通知のみで十分なのでメール通知はオフにしています。
.coveragercを書く
このままではテストコードまで含めたカバレッジがcodecovに表示されてしまったり、テストしないコード(デバッグ用のステートメントなど)まで含まれてしまうため、.coveragerc
という設定ファイルを置いて制御します。
[run] branch = True source = student_portal_crawler [report] exclude_lines = if TYPE_CHECKING: if __name__ == .__main__.: ignore_errors = True omit = student_portal_crawler/test/*
typing
モジュールを使用して型ヒントの定義をしているので、常にFalse
となり実行されないif TYPE_CHECKING:
という文が所々に現れるのでこれを排除しています。
また、実際の実行コード(if __name__ == '__main':
)も排除しています。
READMEへバッジを貼る
CI回してカバレッジ取得する最大の目的といっても過言ではない、READMEへのバッジを貼ります。
Travis CI
https://travis-ci.org/ユーザ名/リポジトリ名
にアクセスし、リポジトリ名の横に表示されているバッジをクリックすると、形式を選択してコピーできます。
Codecov
https://codecov.io/gh/ユーザ名/リポジトリ名/settings/badge
にアクセスすると形式を選択してコピーできます
後はpushすればCIが走ります。
結果を確認
Githubへpushしてしばらく待つとテストが完了します。
こんな風にSlackに通知が来ます。
Codecovに自動でカバレッジが送信され、以下の様に表示されます。
また、リンクを開いていくとどのモジュールのカバレッジが低いのか丸わかりな上、各ファイルのどの行がテストされていないのかも一発で分かります。
まとめ
取り扱いが面倒なことが多いトークン等の設定が必要ないため、Codecov + Travis CIはなかなかオススメの組み合わせかと思います。
どんどんunittestを書いて徳を積んでいきましょう!