ぽよメモ

レガシーシステム考古学専攻

falconでAPIのテストを書く

falconとはpythonの軽量webAPIフレームワークで,手軽にちゃちゃっとパフォーマンスの良い(らしい)APIを書けます.手軽さで言うと正直Flaskでもそんな変わらないのではと今となっては思いますが,公式曰くFlaskやBottleと比べて高速に動作するそうです.

falconframework.org

環境

  • python==3.5.2
  • falcon==1.1.0
  • gunicorn==19.4.5

基本的にPyCharmで書いています.

APIを書く

テスト駆動開発という観点から見ればまずテストから書くべきなのですが,今回は都合上先にプログラムを書いていきます.
詳しいリファレンスは本家参照*1

ライブラリのインストール

$ pip install falcon gunicorn

gunicornは書いたAPIをwebから叩くために用います.

Hello Worldする

# sample.py

import falcon
import json


def create_json_resp(d: dict) -> str:
    return json.dumps(d, indent=2)


class SampleResource(object):
    @staticmethod
    def on_get(req, resp):
        resp.body = create_json_resp({
            "title": "hello world",
            "sentence": "This is a test program."
        })
        resp.status = falcon.HTTP_200 # デフォルトで200なので無くて良い

app = falcon.API()
app.add_route("/hello", SampleResource())

これで/helloGETするとレスポンスを返すAPIができました.動かしてみましょう.

$ gunicorn sample:app

これで127.0.0.1:8000/helloを叩くと設定したjsonが返ってくると思います.

日付を与えると曜日を返すAPI

helloだけでは寂しいので,タイトルそのままのAPIを書きます.

# sample.py

import falcon
from datetime import datetime

# 省略

class WeekResource(object):
    @staticmethod
    def on_get(req, resp, _date):
        week_data = ["月", "火", "水", "木", "金", "土", "日"]
        try:
            d = datetime.strptime(_date, "%Y-%m-%d")
            resp.body = create_json_resp({
                "input": _date,
                "week": week_data[d.weekday()]
            })
        except ValueError:
            resp.body = create_json_resp({
                "error": "ValueError",
                "detail": "invalid value",
                "input": _date
            })
            resp.status = falcon.HTTP_400

app = falcon.API()
app.add_route("/hello", SampleResource())
app.add_route("/week/{_date}", WeekResource())

これで127.0.0.1:8000/week/2016-12-25とかを叩くとレスポンスのweekにはと入っているはずです.

テストする

テストを書く

公式のリファレンスによるとunittest式の書き方と,pytest式の書き方ができるようです.今回はunittestで書いていきます.

from falcon import testing
import sample


class MyTest(testing.TestCase):
    def setUp(self):
        super(MyTest, self).setUp()
        # sample.pyからappを渡す
        self.app = sample.app


class TestMySample(MyTest):
    def test_hello(self):
        hello_resp = self.simulate_get("/hello")
        self.assertEqual(hello_resp.status_code, 200)
        self.assertEqual(hello_resp.json["title"], "hello world")

    def test_week(self):
        param = "2016-12-25"
        week_resp = self.simulate_get("/week/" + param)
        self.assertEqual(week_resp.status_code, 200)
        self.assertEqual(week_resp.json["input"], param)
        self.assertEqual(week_resp.json["week"], "日")

    def test_invalid_value(self):
        # 間違った形式のパラメータを与えて正しいエラーが返ってくるかを確認
        param = "20161225"
        week_resp = self.simulate_get("/week/" + param)
        self.assertEqual(week_resp.status_code, 400)
        self.assertEqual(week_resp.json["input"], param)
        self.assertEqual(week_resp.json["error"], "ValueError")

公式に書かれているように

class falcon.testing.TestCase(methodName='runTest')
Extends unittest to support WSGI functional testing.

なので,assertEqualなどはunittestのリファレンス*2を参照すると良いと思います.

simulate_get等のメソッドについてはfalconのリファレンスが参考になります*3

テストを実行する

僕はPyCharmで書いているので,このtest_sample.pyを走らせると自動的にunittestで走らせてくれるようです.
実行すると以下の様に表示されます.

f:id:pudding_info:20161228145140p:plain

成功する場合は何も表示されませんが,例えば曜日を計算するときのメソッドにはdatetime.weekday()datetime.isoweekday()の二種類があり,返ってくる値が異なります*4
このメソッドを間違って使用してしまったと仮定し,sample.pyの該当箇所を書き換えてもう一度テストを実行してみます.

f:id:pudding_info:20161228150556p:plain

このようにどこでfailしたのかを教えてくれます.

コンソールから叩く場合は以下の様にします.

$ python -m unittest test_sample.py

この場合も全てのテストに成功すると以下の様に

f:id:pudding_info:20161228150932p:plain

失敗するとこのようにエラーを吐いてくれます.

f:id:pudding_info:20161228151009p:plain

カバレッジも計測できますが,それは他の優良なサイトにお任せします*5

まとめ

webAPIはいちいち立ち上げて,自分でパラメータ打ち込んで,ブラウザとにらめっこして〜なんてしなくてもデバッグできますし,こうすべきでしょう.これまで手動でやってたなんてとても言えない.
テストケースはこれで十分なのかとか,むしろそっちの方が難しいので,せめてc0カバレッジくらいは…まあせいぜい「思ったように動いてくれる」ことを確かめられる,程度の気持ちでいた方が良さそうです.

Karabiner-Elements 0.90.64での複数デバイス間でのプロファイル切り替え

バージョン0.90.68現在,上手くいっていないようです.結局自分の自作スクリプトで切り替えさせています(安定)
実は前の時にも,自分ので走らせたジョブが動きっぱなしだった…?無いとは思いますが…混乱させる情報を掲載して申し訳ないです. いつのバージョンからか,デバイス間でのプロファイル切り替え機能はデフォルトで実装されるようになっていました.最高.好き.
というわけで以下の記事は不要となりました.多数のアクセスをしていただきありがとうございました.消しはしませんが,不要である旨を記載しておきます.

poyo.hatenablog.jp

ただまだ使い方がイマイチ分からないというか,切り替えが明示的に示されていない,というのがあり,ちょっと実験してみることに.

これは2016/11/8現在,karabiner-elementsの最新バージョン0.90.64での記事になります.近い将来なんの参考にもならなくなる可能性が高いです.

karabiner.jsonの構造

以下のような形になっていました.

{
    "profiles": [
        {
            "devices":[
                {
                     "disable_built_in_keyboard_if_exists": false,
                     "identifiers": {
                         "is_keyboard": true,
                         "is_pointing_device": false,
                         "product_id": 8209,
                         "vendor_id": 1241
                     },
                     "ignore": false,
                     "keyboard_type": 42
                 },
                 {
                     "disable_built_in_keyboard_if_exists": false,
                     "identifiers": {
                         "is_keyboard": true,
                         "is_pointing_device": false,
                         "product_id": 603,
                         "vendor_id": 1452
                     },
                     "ignore": true,
                     "keyboard_type": 42
                 },                 {
                     "disable_built_in_keyboard_if_exists": false,
                     "identifiers": {
                         "is_keyboard": true,
                         "is_pointing_device": false,
                         "product_id": 65535,
                         "vendor_id": 1452
                     },
                     "ignore": true,
                     "keyboard_type": 0
                 }
            ],
             "fn_function_keys": {
                 "f1": "vk_consumer_brightness_down",
                 "f10": "mute",
                 "f11": "volume_down",
                 "f12": "volume_up",
                 "f2": "vk_consumer_brightness_up",
                 "f3": "vk_mission_control",
                 "f4": "vk_launchpad",
                 "f5": "vk_consumer_illumination_down",
                 "f6": "vk_consumer_illumination_up",
                 "f7": "vk_consumer_previous",
                 "f8": "vk_consumer_play",
                 "f9": "vk_consumer_next"
             },
             "name": "Profile 1",
             "selected": true,
             "simple_modifications": {
                 "FROM": "TO"
             }
         },
         {
             "comment": "以下同様の構造でプロファイルとデバイスが格納"
         }
    ]
}

僕の場合,このようになっていましたが,元からあったkarabiner.jsonを新バージョンに書き直した形跡*1が見られたので,人によって異なるかもしれません.
要するに一つのプロファイルの中には

  • name: プロファイル名
  • device: デバイスの情報
  • selected: 有効(true)か無効(false)か
  • fn_function_keys: F1キー等の挙動(デフォルトではfn同時押し時の挙動)
  • simple_modifications: キーのマッピング

が含まれています.

プロファイル切り替え

デフォルト時

デフォルトだとKarabiner-Elementsのdevicesタブの中は以下のようになっていました.Keyboard Typeは"ISO","ANSI","JIS","default"という4択から選べるようです.また,Appleの内蔵キーボード/トラックパッド以外にもう一つ,共通のVendor IDを持つ名前不明のデバイスが検出されていますが,これがなんなのかはわかりませんでした.
一つ検出されているマウスのマークがついたデバイスは接続しているMX Masterです.(名前が不明になるのは少し不便.せめて名前つけれるとわかりやすいと思うんですが…)

f:id:pudding_info:20161108161609p:plain

バイスの接続状態ごとにプロファイルを分けているわけではないようで,たとえばこの状態で有効になっているプロファイルは以下のようになっていました.

"devices": [
    {
         "disable_built_in_keyboard_if_exists": false,
         "identifiers": {
             "is_keyboard": true,
             "is_pointing_device": false,
             "product_id": 65535,
             "vendor_id": 1452
          },
          "ignore": false,
          "keyboard_type": 0
      }
],
"fn_function_keys": {
    "f1": "vk_consumer_brightness_down",
    "f10": "mute",
    "f11": "volume_down",
    "f12": "volume_up",
    "f2": "vk_consumer_brightness_up",
    "f3": "vk_mission_control",
    "f4": "vk_launchpad",
    "f5": "vk_consumer_illumination_down",
    "f6": "vk_consumer_illumination_up",
    "f7": "vk_consumer_previous",
    "f8": "vk_consumer_play",
    "f9": "vk_consumer_next"
},
"name": "Default",
"selected": true,
"simple_modifications": {
    "caps_lock": "left_control"
}

karabiner.json上では,vendor_idproduct_idはそれぞれ10進数表記されています.上記のプロファイルのdevicesに表示されている"vendor_id": 1452"product_id": 65535は16進数に直すとそれぞれ0x05ac0xffffとなり,例の不明なApple製デバイスを示していることになります.予想ではdevicesの中に内蔵キーボード/トラックパッドも書き込まれるのではないかと思ったのですが,なぜかそうはならず…うーん…
その後色々やっていたら表示されるようになりました.おそらくトリガーとしてはデバイスのkarabiner-elements上での設定に何かしらの変更(Keyboard Typeを変えた,チェックを外した/つけた,など)をすると書き込まれるようです.

ちなみに内蔵キーボード/トラックパッドのKeyboard TypeをDefaultにしておくとUSとして認識されるのでJISの人はJISを選択する必要があります.

外部キーボード接続

外部キーボードを接続すると以下のように表示されました.Majestouch2 黒軸テンキーレスを接続しています.

f:id:pudding_info:20161108165242p:plain

接続した状態で,横のチェックマークを外すと,そのデバイスではキーの変更が反映されません(つまり,外部キーボードを接続した状態でプロファイルを設定し,Appleの内蔵キーボード/トラックパッドのチェックを外すと内蔵キーボードではデフォルトのままで扱うことが出来ます.).
このチェックマークはkarabiner.jsonでいうとdevicesの中にあるignoreにあたり,チェックマークを外すとtrueになります.

ただ外部キーボードを接続した/切断しただけではプロファイルの変更は反映されないようで,外部キーボードから何かしらの挙動があったとき,つまり文字を打とうとしたりするときに初めてこの変更は反映されるようです*2

複数プロファイル作成手順

まず,デフォルトの状態で(何も外部デバイスを接続していない状態で)プロファイルを作成します.このとき,必ず一度,内蔵キーボードの設定を触っておいてください(Keyboard Typeを変える,チェックマークを付けたり外したりする,など).
これをしておかないと,プロファイルのdevices内に内蔵キーボードが記述されず,プロファイルがやたら増えていったりすることになります*3

karabiner.jsonを確認してdevices内に現在の状態のデバイスが記述されていることを確認したら,外部キーボードを接続します.

  1. 適当なキーを押したり文字入力しようとするとマウス以外のデバイスに再度チェックが付きます.
  2. 外部キーボードのKeyboard TypeをJISに変えます(Defaultで良い人は外部キーボードのチェックを一度外し,再度付けます)
  3. 内蔵キーボードのチェックを外します.
  4. キーの設定をします

この手順ならば,karabiner.jsonの中には2つのプロファイルが作られ,外付けキーボード無しのプロファイルのdevicesには内臓キーボードが,外付けキーボードを挿した時のプロファイルのdevicesには内臓キーボードと外付けキーボードが,それぞれ登録された状態になっているはずです.
外付けキーボードを抜き差しして文字入力してみたりしてプロファイルが切り替わるのを確認してください.

複数プロファイル作成のポイント

  1. 用意するプロファイルのdevicesにそれぞれ全てのデバイスが組み込まれていること(適切にignoreが設定されていること)
  2. ごっちゃになってきたらkarabiner.jsonをバックアップし一旦削除してやり直す
  3. トライアンドエラー

ポイントもクソもない感じになってしまった…

JISキーボード上の表記について

JISキーボード上の表記と,karabiner-elements内での表記はわかりづらくなってしまっているので簡単なまとめを.これは2016/11/8現在の最新stableバージョン0.90.64での情報です.

JISキーボード上の表記 Karabiner-Elementsでの表記
Windowsキー left_command
Left Alt left_option
変換 PCキーボードの変換キー
無変換 PCキーボードの無変換キー
かな PCキーボード上のかなキー
applicationキー*4 application
半角/全角 grave_accent_and_tilde (`)

急いでいてきっちり確認していなかったので右側Altとか右側Ctrlとかはまだ確認取れていません…そのうち追記します.
また,ScreenLockとPause/Breakにはそれぞれディスプレイ輝度の上げ下げが割り振られていました(たぶん).
HomeやEnd,Delete等はそのまま動いていたように記憶しています.

これまでのバージョンでの不具合

僕の環境では

  1. karabiner-elementsの起動中,英数入力のときだけなぜか強制US配列になる
  2. マウス(MX Master)の設定を行っていたソフトウェア(Logicool Options)の設定をオーバーライドし,機能をデフォルトに戻す

という不具合が生じていました.バージョン0.90.64現在,この二つの不具合は

  1. キーボード配列を選択できる
  2. マウスは設定によりデフォルトで無視

という方法で解決しています.
過去のバージョンではどうにも不具合が起きていた人も,アップデートしてみることをオススメします.

まとめ

おおむねGUIのみで完結できるようになり,かなりできることの幅が広がりましたが,US配列のユーザーとしては過去の「Command空打ちで英数/かな切り替え」がまだ使えないため,Sierraへのアプデを断念している人も多いかと思います.早めに実装されるといいなぁ……

読みにくい記事になり申し訳ありません.また追加情報があれば更新していきたいです.間違ってたらコメントください…

*1:プロファイル名が継承されていました.しかし二種類あったプロファイルのうちsimple_modificationsの中身が継承されていたのは片方のみだったため,よくわかりません…完全に継承しているわけではないかも

*2:きっちりと計測したわけではなく,karabiner-elementsのGUIを見ている限りでは,ということです

*3:外部キーボードを接続→キーボードの設定は触らず,内蔵キーボードのチェックを外す→キーを設定→USBキーボードを抜く,とすると一つ新しいプロファイルが作られます.するとこのプロファイルのdevices内にはUSBキーボードは記述されておらず,次の接続時にまた設定が空の新しいプロファイルが作られました.

*4:Majestouchだと電卓?メモ帳?みたいなマークが付いているキー

AngularJSでパスにスペースを含むimgをbackground-imageに指定する

例えば/hoge/fuga/contain space image.jpgみたいなjpegファイルを表示したいとき,ng-srcに値を渡せば問題ありません.
しかし,これをbackground-imageに指定したいとき,つまりAngularJSでCSSを使って画像を表示したいとき,ディレクティブを自作するのが一番良い,というのがいくつかstackoverflowのページを何個か見て得た答えです.

いくつかの例: stackoverflow.com stackoverflow.com

しかしこれは全てスペースを含んだパスではうまく動作しません.結果として値を渡された後,

background-image: url(/hoge/fuga/contain space image.jpg);

となり,スペース部分でエラーを吐くからです.

というわけで以下のようなディレクティブを作成しました.

var app = angular.module("myApp", []);

app.directive('backImg', function(){
    return function(scope, element, attrs){
        attrs.$observe('backImg', function(value) {
            element.css({
                'background-image': 'url("' + value +'")'
            });
        });
    };
});

普通に考えてみれば簡単な話だったのですが,要するにurl()にファイルパスを文字列として渡せれば良いわけで,このようにくくってやればいいわけです.
使い方は以下.

<ons-template id="sample.html">
<ons-page ng-controller="SampleController">
  <ons-toolbar>
    <div class="center">{{ data.title }}</div>
  </ons-toolbar>
    <div back-img="{{data.path}}"></div>
</ons-page>
</ons-template>
app.controller("SampleController", ["$scope", function($scope){
  $scope.data = {
    title: "SampleImage",
    path: "/path/to/contain space image.jpg"
  }
}]);

これで半日潰したあの日が懐かしいです.