kivyのログを出力するTextInputをつくってみた

タイトルではわかりにくいけど、kivyのログを反映するTextInputをつくってみたが正しいのではないかと思う。

方法

今回は、kivyのLoggerにStreamHandlerを追加して反映させることにした。委譲のAdapterっぽい感じでio.TextIOBaseを継承するTextInputIOなるものをつくり、必要なものを実装していった。

テスト

import unittest

from kivy.uix.textinput import TextInput

from app.TextInputIO import TextInputIO


class TextInputIOTestCase(unittest.TestCase):
    """
    Test for TextInputIO class
    """

    def setUp(self):
        self.text_input = TextInput()

    def test_as_context_manager(self):
        with TextInputIO(self.text_input) as f:
            pass

    def test_open_close(self):
        f = TextInputIO(self.text_input)
        f.close()

    def test_write(self):
        f = TextInputIO(self.text_input)
        f.write('TESTING TEXT INPUT\n')
        f.close()

    def test_non_readable(self):
        f = TextInputIO(self.text_input)
        self.assertRaises(OSError, f.read)
        f.close()

    def test_non_seekable(self):
        f = TextInputIO(self.text_input)
        self.assertRaises(OSError,f.seek)
        f.close()

    def test_isatty(self):
        f = TextInputIO(self.text_input)
        self.assertEqual(f.isatty(), False)
        f.close()

こんな感じで書いた。

実装

from io import TextIOBase


class TextInputIO(TextIOBase):
    """
    IO for Logger. Using TextInput as file.
    """
    def __init__(self,text_input):
        super().__init__()
        self.text_input = text_input

    def isatty(self):
        return False

    def readable(self):
        return False

    def writable(self):
        return True

    def seekable(self):
        return False

    def write(self, s):
        self.text_input.text += s

    def writelines(self, lines):
        if not isinstance(lines,list):
            raise TypeError('list')
        for s in lines:
            if not isinstance(s,str):
                raise TypeError('str')
            self.write(s)

    def close(self):
        super().close()
        self.text_input = None

バッファとかないしどうしようかなと思ったりしてた。テストに書いたように__enter__と__exit__を継承してないけど、コンテキストマネージャとして動いた。多分、親のを呼び出してるんだと思う。

使ってみた

from kivy import Logger
from kivy.app import App
from kivy.uix.button import Button
import logging
from logging import StreamHandler

from kivy.uix.gridlayout import GridLayout
from kivy.uix.textinput import TextInput

from app.TextInputIO import TextInputIO

logger = Logger
logger.setLevel(logging.DEBUG)

text_input = TextInput()
text_input_io = TextInputIO(text_input)
sh = StreamHandler(text_input_io)
fh = logging.FileHandler('app.log')
logger.addHandler(fh)


def pressed(view):
    logger.info('Application: Button was pressed!!')


class MainScreen(GridLayout):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.cols = 1
        b = Button(text="Button")
        b.bind(on_press=pressed)
        self.add_widget(b)
        self.add_widget(text_input)
        logger.addHandler(sh)


class TestApp(App):
    def build(self):
        return MainScreen()


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

テスト実行用にこんなの書いた。最初、別のロガーを使っていたのでグローバルなところにロガーの設定があるけど気にしないでほしい。

f:id:b1u3:20180621190844p:plain

そこそこ動いた。多分問題点として、スレッドセーフじゃないと思う。あとTextInputを追加する前にloggerにハンドラとして追加すると強制終了する。追加は遅延させないとダメかなと。したがって、最初のGLとかのログメッセージは反映されない。あと、最初のカラムのINFOとかが出なかった。logging.BASIC_FORMATをハンドラに設定しても解析されなかった。

やってみて思ったこと

まだまだ調べることが多そうだなって思った。最近ブログを更新する気がだいぶ出てきたので、この調子でやっていきたいなぁって。