Stimulator

機械学習とか好きな技術話とかエンジニア的な話とかを書く

Pythonモジュールの遅延import

- はじめに -

この記事は、Xonsh Advent Calendar 2017 - Qiita 18日目の記事です。

Pythonのmoduleのloadを実際に利用する前に遅延してやろうというTipsです。

加えて、xonshのxonshrcに記載する事で、xonshの起動も早くしようという話を書いています。


アジェンダ

 

- 遅延importを実現する -

遅延させて、使いたい時にpythonスクリプトをロードしたりするには大抵importlibを使います。

そこでimportlibでxonshの起動爆速化を狙おうとしていた所、同僚に「lazyasdっていうのがあるよ」と言われて調べたら大体それで出来たので、これで良いやという感じでした。

以下には一応どちらも記述しています。

 

importlibで動的ロード -

Pythonでモジュールをスクリプト上で動的にロードするにはimportlibを使います。

31.5. importlib — The implementation of import — Python 3.6.6rc1 documentation

import importlib

os = importlib.import_module("os")
print(os.listdir("."))

machineryを使えばimportのタイミングをhookできます。
https://docs.python.org/3.6/library/importlib.html#module-importlib.machinery

遅延してロードさせる場合は、Python3.5にて追加されたimportlib.util.LazyLoaderを使うと良いです。
31.5. importlib — The implementation of import — Python 3.6.6rc1 documentation


参考:Python: モジュールを動的にロードする - CUBE SUGAR CONTAINER


 

lazyasdを使った遅延import

同僚が「xonsh使ってるならxonsh.lazyasdの中にLazyObjectやBackgroundModuleLoaderがあるのでそれ使うと良いよ」と教えてくれました。

Lazy & Self-destructive Objects (xonsh.lazyasd) — xonsh 0.6.7 documentation


その後、調べてみるとlazyasdだけパッケージとして切り離されているようです。

GitHub - xonsh/lazyasd: Lazy & self-destructive tools for speeding up module imports

pipでinstallできるのでやります

pip install lazyasd


lazyasdで一番簡易にモジュール使う時ロードを実現できるのはデコレータを付けることです。

import importlib
from lazyasd import lazyobject

@lazyobject
def os():
    return importlib.import_module('os')

# importが発生するのはココ
print(os.listdir("."))

実際にモジュールを利用する時にimportする事が割りと簡単にできました。

 
compile済みの正規表現も遅延させて読み込めます。

import re
from lazyasd import lazyobject

@lazyobject
def hoge_re():
    return re.compile('hoge')

print(hoge_re.search("hoge piyo str") is not None)

 
より良い方法として、別スレッドで読み込むload_module_in_backgroundもあります。

from lazyasd import load_module_in_background

os = load_module_in_background('os')
print(os.listdir("."))

引数のreplacementsも便利です。

 
関数を引数に取るためlambda使ったりとちょっと面倒ですが、LazyObjectLazyDictといったClassも用意されています。

from lazyasd import LazyObject
from lazyasd import LazyDict
import re

# 最初にosを使う時にglobalにosをimportする
os = LazyObject(lambda: importlib.import_module('os'), globals(), 'os')
print(os.listdir("."))

# 一回目に使う時に正規表現をcompileする
RES = LazyDict({
        'dot': lambda: re.compile('.'),
        'all': lambda: re.compile('.*'),
        'two': lambda: re.compile('..'),
        }, globals(), 'RES')
print(RES["dot"].search("hogehoge.text") is not None)

「使うか分からないので、もし使うなら最初にロードしたいな」という時に使えます。


多分以下を見るかソースコードを見ると良いです。
Lazy & Self-destructive Objects (xonsh.lazyasd) — xonsh 0.6.7 documentation


 

- xonshrcに書いていく -

ここからはxonshrcに書いておいてxonsh起動までを爆速にしてやろうという話です。

xonshであればpip installする必要もなく、xonsh.lazyasdを利用できます。

使うか分からないがimportを忘れがちなやつを全部こうしてやります

from xonsh.lazyasd import lazyobject

@lazyobject
def os():
    return importlib.import_module('os')

 
全部listに突っ込んでおけば、execしてくれるSampleです。
import asのようにしたい場合は、dictか何かにしてformat第一引数xをよしなにやれば良いです

from xonsh.lazyasd import lazyobject
import importlib

# list版
lazy_module_list = ["requests", "numpy", "pandas", "matplotlib",...]
for x in lazy_module_list:
    t = "@lazyobject\ndef {}():\n    return importlib.import_module('{}')".format(x, x)
    exec(t)

# dictならこんな感じか
lazy_module_dict = {
    'requests': 'requests',
    'sys': 'sys',
    'random': 'random',
    'shutil': 'shutil',
    'pd': 'pandas',
    'np': 'numpy',
    'plt': 'matplotlib.pyplot',
    'Path': 'pathlib.Path',
        }
for k,v in lazy_module_dict.items():
    t = "@lazyobject\ndef {}():\n    return importlib.import_module('{}')".format(k, v)
    exec(t)

これをxonshrcに書いて優勝です


いやいや、絶対コンソール使ってたら使うでしょという物はbackgroundでimportしてやりましょう。

from lazyasd import load_module_in_background

background_module_list = ["os", "sys", "random", "shutil", "linecache",...]
for x in background_module_list:
    exec("{}=load_module_in_background('{}')".format(x, x))


参考:Lazy & Self-destructive Objects (xonsh.lazyasd) — xonsh 0.6.7 documentation


 

- おわりに -

lazyasd便利なので、環境に合わせてロードするやつとかも拡張として書いていきたい所。

importlibのLoader周りはあんまりExamplesもなくて厳しいですが、まあなんとか。


Xonshアドベントカレンダーの方もよろしくお願いします。

Xonsh Advent Calendar 2017 - Qiita


 

PythonでHatenaブックマークのホットエントリを取得して表示する

- はじめに -

この記事は、Xonsh Advent Calendar 2017 - Qiita 25日目最終日の記事です。

本記事では、PythonスクリプトでHatenaブックマークのホットエントリのリストを取得、xonshへ表示する内容を記載します。

f:id:vaaaaaanquish:20171225175808p:plain

追記:2018/11/23
ptk 2.xでは、本記事のコードが動作しないため、移行のための記事とrepositoryを公開しています。
vaaaaaanquish.hatenablog.com

 

- Hatenaホットエントリの取得 -

はてなにはブログの投稿や取得、はてブ数の取得等のAPIが用意されています。
はてなブックマークドキュメント一覧 - Hatena Developer Center

上記を見る限り、Hatenaホットエントリを取得することは出来ないので、requestsでスクレイピングしてくる必要がありそうです。

import requests
import bs4

# ホットエントリページの取得、解析
res = requests.get("http://b.hatena.ne.jp/hotentry")
bs_res = bs4.BeautifulSoup(res.text, "lxml")

# はてブ数とタイトルの取得
hotentry = []
for x in bs_res.findAll("li", attrs={"class":"entry-unit"}):
    a_tag = x.find("a", attrs={"class":"entry-link"})
    if a_tag is not None:
        hotentry.append((x.find("span").text, a_tag.attrs["title"], a_tag.attrs["href"]))
# はてブ数でソート
hotentry = sorted(hotentry, key=lambda x:int(x[0]), reverse=True)

# 表示
for x in hotentry:
    print('{} || {} \n {}'.format(x[0], x[1], x[2]))

上記コードでクローリングしてきた結果が以下のように出ます

1635 || Pythonの学び方と,読むべき本を体系化しました2018〜初心者から上級者まで - Lean Baseball 
 http://shinyorke.hatenablog.com/entry/python2018
627 || コンピューターで全漢字使用可に 6万字コード化 | NHKニュース 
 https://www3.nhk.or.jp/news/html/20171224/k10011270111000.html
567 || 日本テレビのみなさまへ、生活保護についての悪意のある番組放送はやめてください(大西連) - 個人 - Yahoo!ニュース 
 https://news.yahoo.co.jp/byline/ohnishiren/20171224-00079667/
534 || アニメ犯罪を追う海外ドラマ「ANIME CRIMES DIVISION」がカオスすぎて面白い「お前は地下遊戯王の危険さをわかっていない」 - Togetter 
 https://togetter.com/li/1183065
534 || みずほ銀行、人事評価と結びついた金融商品の押し売りの実態がNHKより流出 : 市況かぶ全力2階建 
 http://kabumatome.doorblog.jp/archives/65905156.html
...

常々良さそうです。
この結果を結果を使っていきたい。


 

- xonshのセレクタで選択したらBrowserで開く -

以下の記事で、python prompt toolkit (ptk)を利用した、シェル上で対話的選択する方法を記載しました。

vaaaaaanquish.hatenablog.com


これを利用して、xonshではてなホットエントリを取得してセレクトし、Browserで開くスクリプトとしてxonsh向けに書いてみます。

# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from prompt_toolkit.application import Application
from prompt_toolkit.buffer import Buffer
from prompt_toolkit.document import Document
from prompt_toolkit.enums import DEFAULT_BUFFER
from prompt_toolkit.interface import CommandLineInterface
from prompt_toolkit.key_binding.manager import KeyBindingManager
from prompt_toolkit.keys import Keys
from prompt_toolkit.layout.containers import Window
from prompt_toolkit.layout.controls import BufferControl
from prompt_toolkit.layout.margins import ScrollbarMargin
from prompt_toolkit.shortcuts import create_eventloop
from prompt_toolkit.filters import IsDone
from prompt_toolkit.layout.controls import TokenListControl
from prompt_toolkit.layout.containers import ConditionalContainer, ScrollOffsets, VSplit, HSplit
from prompt_toolkit.layout.screen import Char
from prompt_toolkit.layout.dimension import LayoutDimension as D
from prompt_toolkit.mouse_events import MouseEventTypes
from prompt_toolkit.token import Token
from prompt_toolkit.styles import style_from_dict
import webbrowser
import requests
import bs4

def _get_hotentry():
    res = requests.get("http://b.hatena.ne.jp/hotentry")
    bs_res = bs4.BeautifulSoup(res.text, "lxml")

    hotentry = []
    for x in bs_res.findAll("li", attrs={"class":"entry-unit"}):
        a_tag = x.find("a", attrs={"class":"entry-link"})
        if a_tag is not None:
            hotentry.append((x.find("span").text, a_tag.attrs["title"], a_tag.attrs["href"]))
    hotentry = sorted(hotentry, key=lambda x:int(x[0]), reverse=True)
    hotentry = [('{} || {}'.format(x[0], x[1]), x[2]) for x in hotentry]
    return hotentry

def _open_url(url):
    webbrowser.open(url)

def _if_mousedown(handler):
    def handle_if_mouse_down(cli, mouse_event):
        if mouse_event.event_type == MouseEventTypes.MOUSE_DOWN:
            return handler(cli, mouse_event)
        else:
            return NotImplemented
    return handle_if_mouse_down


class InquirerControl(TokenListControl):
    selected_option_index = 0
    answered = False
    choices = TokenListControl

    def __init__(self, hotentrys, **kwargs):
        self.choices = [x[0] for x in hotentrys]
        self.urls = [x[1] for x in hotentrys]
        super(InquirerControl, self).__init__(self._get_choice_tokens, **kwargs)

    @property
    def choice_count(self):
        return len(self.choices) 

    def _get_choice_tokens(self, cli):
        tokens = []
        T = Token

        def append(index, label):
            selected = (index == self.selected_option_index)

            @_if_mousedown
            def select_item(cli, mouse_event):
                self.selected_option_index = index
                self.answered = True
                cli.set_return_value(None)

            token = T.Selected if selected else T
            tokens.append((T.Selected if selected else T, ' > ' if selected else '   '))
            if selected:
                tokens.append((Token.SetCursorPosition, ''))
            tokens.append((T.Selected if selected else T, '%-24s' % label, select_item))
            tokens.append((T, '\n'))

        for i, choice in enumerate(self.choices):
            append(i, choice)
        tokens.pop()  # Remove last newline.
        return tokens

    def get_selection(self):
        return self.choices[self.selected_option_index], self.urls[self.selected_option_index]


def _hotentry():
    hotentry = _get_hotentry()
    ic = InquirerControl(hotentry)

    def __get_prompt_tokens(cli):
        tokens = []
        T = Token
        tokens.append((Token.QuestionMark, '?'))
        tokens.append((Token.Question, ' hotentrys '))
        if ic.answered:
            tokens.append((Token.Answer, ' ' + ic.get_selection()[0]))
            _open_url(ic.get_selection()[1])
        else:
            tokens.append((Token.Instruction, ' (Use arrow keys)'))
        return tokens

    layout = HSplit([
        Window(height=D.exact(1), content=TokenListControl(__get_prompt_tokens, align_center=False)),
        ConditionalContainer(
            Window( ic, width=D.exact(43), height=D(min=3), scroll_offsets=ScrollOffsets(top=1, bottom=1)),
            filter=~IsDone())])
    
    manager = KeyBindingManager.for_prompt()
    @manager.registry.add_binding(Keys.ControlQ, eager=True)
    @manager.registry.add_binding(Keys.ControlC, eager=True)
    def _(event):
        event.cli.set_return_value(None)
    @manager.registry.add_binding(Keys.Down, eager=True)
    def move_cursor_down(event):
        ic.selected_option_index = (
            (ic.selected_option_index + 1) % ic.choice_count)
    @manager.registry.add_binding(Keys.Up, eager=True)
    def move_cursor_up(event):
        ic.selected_option_index = (
            (ic.selected_option_index - 1) % ic.choice_count)
    @manager.registry.add_binding(Keys.Enter, eager=True)
    def set_answer(event):
        ic.answered = True
        event.cli.set_return_value(None)

    inquirer_style = style_from_dict({
        Token.QuestionMark: '#5F819D',
        Token.Selected: '#FF9D00',  # AWS orange
        Token.Instruction: '',  # default
        Token.Answer: '#FF9D00 bold',  # AWS orange
        Token.Question: 'bold',
    })

    _app = Application(
        layout=layout,
        #buffers=buffers,
        key_bindings_registry=manager.registry,
        mouse_support=True,
        #use_alternate_screen=True
        style=inquirer_style
    )

    _eventloop = create_eventloop()
    try:
        cli = CommandLineInterface(application=_app, eventloop=_eventloop)
        cli.run(reset_current_buffer=False)
    finally:
        _eventloop.close()

aliases["hotentry"] = _hotentry


これでxonsh上でのhotentryコマンドが作れました。
いざ実行してみます。

 
f:id:vaaaaaanquish:20171225175447g:plain


クールっぽい。
Browserで開くならあんまり意味ない気もしなくもないので、textだけでよしなに取ってくるマンを作って組み合わせたい所ですね。


- おわりに -

 
よさそうなセレクタの使い方ができました。

今年はWebスクレイピング記事で技術記事歴代一位のはてブ数を獲得した年でもあるので、良い記事で締めくくれていると自負しています。

vaaaaaanquish.hatenablog.com


アドベントカレンダーも盛り上がって、xonsh関連の日本語情報はかなり充実したと思います。
Xonsh Advent Calendar 2017 - Qiita

皆さんxonshでよい年末を。


 

Python Prompt Toolkitで対話的な選択コマンドを作る

- はじめに -

この記事は、Xonsh Advent Calendar 2017 - Qiita 23日目の記事です。

遅れ気味ですが、Python Prompt Toolkit (以下、ptk)を利用して、対話型のセレクタ(上下矢印で回答を選択できるやつ)を作りたいなと思います。

名前が分からないんですが、selectコマンドみたいなやつです。

f:id:vaaaaaanquish:20171225155520g:plain:w400:h170


ptkが扱えれば、xonshにも安易に応用できるため、xonshを扱う上で覚えておきたいアイデアです。


追記:2018/11/23
ptk 2.xでは、本記事のコードが動作しないため、移行のための記事とrepositoryを公開しています。
vaaaaaanquish.hatenablog.com


 

- 選択コマンド -

Pythonで擬似的なselectコマンドを作るには、printとreadlineを組み合わせて作る方法や、ptk等のコンソール生成moduleを利用する方法があります。

python-prompt-toolkitリポジトリの中にも以下のように、選択できるメニューを生成するにはというissueが立っています、
How do I create a menu? · Issue #281 · prompt-toolkit/python-prompt-toolkit · GitHub

上記issueに対して、以下でsampleコードが示されています。
sample for custom control based on TokenListControl by markfink · Pull Request #427 · prompt-toolkit/python-prompt-toolkit · GitHub
sample for custom control based on TokenListControl by markfink · Pull Request #427 · prompt-toolkit/python-prompt-toolkit · GitHub


具体的には、ptkのfull-screen-layout APIを利用して、選択用の画面を作ってあげて、その中でkeyバインドで操作できるようにするというものです。
https://github.com/jonathanslenders/python-prompt-toolkit/blob/master/examples/full-screen-layout.py

full_screen_appのdocsは以下
python-prompt-toolkit/full_screen_apps.rst at master · prompt-toolkit/python-prompt-toolkit · GitHub


 
以下に、上記を参考にしながら対話selectorによってコマンドを選択、コピー、実行するスクリプトを示します。
prompt_toolkitに加えて、クリップボードを利用するためにpyperclipモジュールを使っているので、必要に応じてpipすると良いです。

# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from prompt_toolkit.application import Application
from prompt_toolkit.buffer import Buffer
from prompt_toolkit.document import Document
from prompt_toolkit.enums import DEFAULT_BUFFER
from prompt_toolkit.interface import CommandLineInterface # ptk 1.x
from prompt_toolkit.key_binding.manager import KeyBindingManager # ptk 1.x
from prompt_toolkit.keys import Keys
from prompt_toolkit.layout.containers import Window
from prompt_toolkit.layout.controls import BufferControl
from prompt_toolkit.layout.margins import ScrollbarMargin
from prompt_toolkit.shortcuts import create_eventloop
from prompt_toolkit.filters import IsDone
from prompt_toolkit.layout.controls import TokenListControl
from prompt_toolkit.layout.containers import ConditionalContainer, ScrollOffsets, VSplit, HSplit
from prompt_toolkit.layout.screen import Char
from prompt_toolkit.layout.dimension import LayoutDimension as D
from prompt_toolkit.mouse_events import MouseEventTypes
from prompt_toolkit.token import Token
from prompt_toolkit.styles import style_from_dict

# セレクトアイテム
choices = ['ls', 'ifconfig', 'pwd', 'who']
# 質問文
string_query = ' Command Select '
# 操作説明
inst = ' (Use arrow keys)'

# 選択した際に実行する関数
def selected_item(text):
    # クリップボードにコピー
    # 必要 : pip install pyperclip
    import pyperclip
    pyperclip.copy(text)
    # Command実行
    import subprocess
    res = subprocess.call(text)
    print(res)


# 以下セレクタ実装

# マウス操作が入った時に落とすためのデコレータ
def if_mousedown(handler):
    def handle_if_mouse_down(cli, mouse_event):
        if mouse_event.event_type == MouseEventTypes.MOUSE_DOWN:
            return handler(cli, mouse_event)
        else:
            return NotImplemented
    return handle_if_mouse_down

# セレクトアイテムを受け取って選択させるための制御Class
# controlsのTokenListControlを継承する
class InquirerControl(TokenListControl):
    selected_option_index = 0
    answered = False
    choices = []

    def __init__(self, choices, **kwargs):
        self.choices = choices
        super(InquirerControl, self).__init__(self._get_choice_tokens, **kwargs)

    @property
    def choice_count(self):
        return len(self.choices)

    def _get_choice_tokens(self, cli):
        tokens = []
        T = Token

        def append(index, label):
            selected = (index == self.selected_option_index)

            @if_mousedown
            def select_item(cli, mouse_event):
                # bind option with this index to mouse event
                self.selected_option_index = index
                self.answered = True
                cli.set_return_value(None)

            token = T.Selected if selected else T
            tokens.append((T.Selected if selected else T, ' > ' if selected else '   '))
            if selected:
                tokens.append((Token.SetCursorPosition, ''))
            tokens.append((T.Selected if selected else T, '%-24s' % label, select_item))
            tokens.append((T, '\n'))

        for i, choice in enumerate(self.choices):
            append(i, choice)
        tokens.pop()  # Remove last newline.
        return tokens

    def get_selection(self):
        return self.choices[self.selected_option_index]

# インスタンス生成
ic = InquirerControl(choices)

# prompt情報の取得
def get_prompt_tokens(cli):
    tokens = []
    T = Token
    tokens.append((Token.QuestionMark, '?'))
    tokens.append((Token.Question, string_query))
    if ic.answered:
        # 選択した値を取得
        tokens.append((Token.Answer, ' ' + ic.get_selection()))
        # 任意関数実行
        selected_item(ic.get_selection())
    else:
        tokens.append((Token.Instruction, inst))
    return tokens


# 疑似レイアウトをトークンリストから設定
layout = HSplit([
    Window(height=D.exact(1),
           content=TokenListControl(get_prompt_tokens, align_center=False)),
    ConditionalContainer(
        Window(
            ic,
            width=D.exact(43),
            height=D(min=3),
            scroll_offsets=ScrollOffsets(top=1, bottom=1)
        ),
        filter=~IsDone())])

# keyバインディングの設定
manager = KeyBindingManager.for_prompt()
@manager.registry.add_binding(Keys.ControlQ, eager=True)
@manager.registry.add_binding(Keys.ControlC, eager=True)
def _(event):
    event.cli.set_return_value(None)
@manager.registry.add_binding(Keys.Down, eager=True)
def move_cursor_down(event):
    ic.selected_option_index = (
        (ic.selected_option_index + 1) % ic.choice_count)
@manager.registry.add_binding(Keys.Up, eager=True)
def move_cursor_up(event):
    ic.selected_option_index = (
        (ic.selected_option_index - 1) % ic.choice_count)
@manager.registry.add_binding(Keys.Enter, eager=True)
def set_answer(event):
    ic.answered = True
    event.cli.set_return_value(None)

# Color Font Style
inquirer_style = style_from_dict({
    Token.QuestionMark: '#5F819D',
    Token.Selected: '#FF9D00',
    Token.Instruction: '',
    Token.Answer: '#FF9D00 bold',
    Token.Question: 'bold',
})

# layoutを選択モデルにしたAppを設定
app = Application(
    layout=layout,
    key_bindings_registry=manager.registry,
    mouse_support=True,
    style=inquirer_style
)

# eventloopで実行、終了時close
eventloop = create_eventloop()
try:
    cli = CommandLineInterface(application=app, eventloop=eventloop)
    cli.run(reset_current_buffer=False)
finally:
    eventloop.close()

 
実行してみます。
f:id:vaaaaaanquish:20171225155917p:plain

概ね良さそうです。
様々な関数と組み合わせられるので、疑似pecoみたいな感じの物も実装できそうです。


 

- おわりに -

Python Prompt Toolkitを利用したセレクタの実装でした。

もちろん、ptkを利用しているxonsh上でも動くので、xonshで自作スニペットセレクタを作ったり、ssh先を選んだりする際に表示させるようなものが作れそうだなと思っています。

次の記事では、これを利用したxonsh関数を作って書きたいと思います。

 
Xonsh Advent Calendar 2017 - Qiita


追記:
この記事を利用してxonshでselectコマンドを作る記事を書きました
vaaaaaanquish.hatenablog.com

 

xonshのCore Eventsまとめ

- xonshのCore Eventsとは -

XonshのCore Eventsは、xonshを自分で改修していく上で大事な「xonshの動作をtriggerとして発火するもの」です。

xonshの良いところは、EventsをPythonの関数のデコレータとして記述する事だけで「EventをHookして何かを実行する」という事が可能になる所です。


公式ドキュメントのsampleを例に見てみます。

# cdなど移動系コマンドのイベントをトリガーにadd_to_fileが起動する
@events.on_chdir
def add_to_file(olddir, newdir, **kw):
    with open(g`~/.dirhist`[0], 'a') as dh:
        print(newdir, file=dh)

上記コードでは、ディレクトリ移動が発生したタイミングで移動情報をファイルに追記するようになっています。
参考 : Tutorial: Events — xonsh 0.6.0.dev151 documentation


Core Eventsを把握しておくことは、Xonshの理解に繋がる訳です。


 

- Core Events -

以降はCore Eventsについて記述しておくものです。

主に以下に記載されたxonshに内装されているEventsについてです。
Core Events — xonsh 0.6.0.dev151 documentation

 

設定周り

rcファイル読み込み前

on_pre_rc() -> None
http://xon.sh/events.html#on-pre-rc-none
設定ファイルとなるxonshrcを読み込む前に発火するイベント
正確には以下の中で発火するので、起動時で言うとon_post_initの直前
https://github.com/xonsh/xonsh/blob/88647b4a8f6f2611dbb03370879221b1ca9fa89a/xonsh/main.py#L251

 

rcファイル読み込み後

on_post_rc() -> None
http://xon.sh/events.html#on-post-rc-none
設定ファイルとなるxonshrcを読み込んだ後に発火するイベント
正確には以下の中で発火するので、起動時で言うとon_post_initの直前
https://github.com/xonsh/xonsh/blob/88647b4a8f6f2611dbb03370879221b1ca9fa89a/xonsh/main.py#L251

 

初期化後

on_post_init() -> None
http://xon.sh/events.html#on-post-init-none
Xonshスクリプトや対話コンソール等のための初期化が済んだ後に発火するイベント
対話コンソールやスクリプト等関係なく発火する
この後対話コンソールとして起動した場合は、on_pre_cmdloopが発火する

 

cmdloopに入る時

on_pre_cmdloop() -> None
http://xon.sh/events.html#on-pre-cmdloop-none
xonshrcや各moduleのimportを終えて、対話コンソールが入力待ち状態に入る前に発火するイベント
正確な場所は以下
https://github.com/xonsh/xonsh/blob/88647b4a8f6f2611dbb03370879221b1ca9fa89a/xonsh/main.py#L347

 

cmdloopから抜けた時

on_post_cmdloop() -> None
http://xon.sh/events.html#on-post-cmdloop-none
xonshがコマンド入力やPythonスクリプトを受け続ける状態を抜けた時に発火するイベント

 

ptkを読み込んだ時

on_ptk_create(prompter: Prompter, history: PromptToolkitHistory, completer: PromptToolkitCompleter, bindings: KeyBindingManager)
http://xon.sh/events.html#on-ptk-create-prompter-prompter-history-prompttoolkithistory-completer-prompttoolkitcompleter-bindings-keybindingmanager
xonshが利用するptkを読み込んだ時に、ptkを渡してくれる
ptkを使ったキーバインドの設定等でよく使う
正確には以下がinitされた時に発火するイベント
https://github.com/xonsh/xonsh/blob/72f3bc0d089ea91d4e5288bb1c44ebfbe81db43e/xonsh/ptk/shell.py#L34

 

exitする前

 
on_exit() -> None
http://xon.sh/events.html#on-exit-none
正確にはmain_xonsh内のfinally内でこのイベント発火だけ発生して、その後はxonshが落ちて別のlogin shellが起動される
https://github.com/xonsh/xonsh/blob/88647b4a8f6f2611dbb03370879221b1ca9fa89a/xonsh/main.py#L347
このイベントを起点にExceptionを落とすとlogin shell出ない
上記on_post_cmdloopの直後に発火する形だが、違いは対話コンソールの時以外でもon_exitは発火する点である
スクリプトなどでもhookしたい場合に使う


 

moduleをインポートする時

インポートする前

on_import_pre_create_module(spec: ModuleSpec) -> None
http://xon.sh/events.html#on-import-pre-create-module-spec-modulespec-none
import hogeする前に発火するイベント
指定した”hoge”がspecとして渡される
 
on_import_pre_exec_module
http://xon.sh/events.html#on-import-pre-exec-module-module-module-none
loader.create_moduleではなくloader.exec_moduleによってimportされる前に発火するイベント
Pythonでは主にexec_moduleが使われていくようになる(Python 3.4でexec_moduleが追加され、3.6以降はメインで使われるように)
動作は上と同じ

インポートする後

 
on_import_post_create_module(module: Module, spec: ModuleSpec) -> None
http://xon.sh/events.html#on-import-post-create-module-module-module-spec-modulespec-none
import hogeした後に発火するイベント
create_moduleしたhogeの内容がmodule、hogeがspecとして渡される
 
on_import_post_exec_module
http://xon.sh/events.html#on-import-post-exec-module
loader.create_moduleではなくloader.exec_moduleによってimportされた後に発火するイベント

 

moduleを探す前

on_import_pre_find_spec(fullname: str, path: str, target: module or None) -> None
http://xon.sh/events.html#on-import-pre-find-spec-fullname-str-path-str-target-module-or-none-none
moduleを探すためのimportlib.util.find_specが利用される前に発火するイベント
以下find_specの引数に指定した名前やPathが渡される
https://docs.python.org/3.6/library/importlib.html#importlib.util.find_spec

 

moduleを探した後

on_import_post_find_spec(spec, fullname, path, target) -> None
http://xon.sh/events.html#on-import-post-find-spec-spec-fullname-path-target-none
moduleを探すためのimportlib.util.find_specが利用された後に発火するイベント
module本体や名前、Pathなどが渡される

 

環境変数を触った時

新規環境変数作成時

on_envvar_new(name: str, value: Any) -> None
http://xon.sh/events.html#on-envvar-new-name-str-value-any-none
環境変数作成時に発火するイベント
新しい変数名と値が渡される
on_envvar_new内で環境変数を変更すると、変数アプデ -> on_envvar_new -> 変数アプデ -> on_envvar_new -> … とループに陥るので注意
 
 

環境変数アップデート時

on_envvar_change(name: str, oldvalue: Any, newvalue: Any) -> None
http://xon.sh/events.html#on-envvar-change-name-str-oldvalue-any-newvalue-any-none
環境変数アップデート時に発火するイベント
変数名と前の値と新しい値が渡される
on_envvar_newと同じくループに注意

 

コマンド実行時

ディレクトリ移動時

on_chdir(olddir: str, newdir: str) -> None
http://xon.sh/events.html#on-chdir-olddir-str-newdir-str-none
cdコマンド等でディレクトリが移動した際に発火するイベント
ディレクトリの移動先と移動元が渡される

   

コマンド実行(コマンド変換時)

on_transform_command(cmd: str) -> str
http://xon.sh/events.html#on-transform-command-cmd-str-str
コマンドを実行する前に発火するイベント
以下で発火するため、複数行の場合繰り返し実行されたりする
https://github.com/xonsh/xonsh/blob/f05c4d86b8529703832c2f0bb5fe2790dd3c5b66/xonsh/shell.py#L60
正確には以下のpushメソッドで「compileされる時」であり、実行時ではない。
https://github.com/xonsh/xonsh/blob/72f3bc0d089ea91d4e5288bb1c44ebfbe81db43e/xonsh/ptk/shell.py#L123

 

コマンド実行前

on_precommand(cmd: str) -> None
http://xon.sh/events.html#on-precommand-cmd-str-none
コマンド実行前に発火するイベント
入力された文字列が渡される

 

コマンド実行後

on_postcommand(cmd: str, rtn: int, out: str or None, ts: list) -> None
http://xon.sh/events.html#on-postcommand-cmd-str-rtn-int-out-str-or-none-ts-list-none
コマンド実行後に発火するイベント
コマンド名やioが渡される

 

一行処理前

on_pre_prompt() -> None
http://xon.sh/events.html#on-pre-prompt
(ドキュメントにはon_first_promptとなっているがミスか)
対話コンソールで一行を読み込んで処理する前に発火するイベント
正確な場所は以下で、入力をhistoryに追加した後である
https://github.com/xonsh/xonsh/blob/5cb3e8dd5545a7448b2a393379e3405c1942d1e0/xonsh/readline_shell.py#L276

 

一行処理後

on_post_prompt() -> None
http://xon.sh/events.html#on-post-prompt
対話コンソールで一行を読み込んで処理した後に発火するイベント


  

- おわりに -

Core Eventsは大事なEventで実装されているイベント以外にも、自前でEventを継承してXonsh内にOverrideさせる事もできるので、xonshの動作をかなり拡張することが可能である。

また、逐一追っていくとXonshの動作のデバッグにもなるので、一度触ってみると吉。

 
アドベントカレンダーまだ空いてるから頼むという所で記事はおしまいです。

qiita.com

 

XonshのException発生時のtracebackを見やすくする

- はじめに -

この記事は、Xonsh Advent Calendar 2017 - Qiita 15日目の記事です。

完全に遅刻しています。

3日間xonsh本家のコードを読みながら「あーでもないこーでもない」とやっており遅れました。

結論を先に述べてから、後半でその経緯も話します。

 

Xontribにしておいたので簡単に使えると思います。



アジェンダ

 

- PythonのStackTraceを見やすくする -

PythonのStackTraceを見やすくする方法を以下の記事に書きました。

vaaaaaanquish.hatenablog.com

この中でも、私が最も見やすいと感じたbacktraceを使って、xonshのTraceBack表示を短くします。

他のパッケージや、自作のシンタックスハイライト用parserでも上手くいくと思います。


 

- xonshのstderrにbacktraceを適応する -

結論から言うと以下のコードを~/.xonshrcに差し込むのが早いと思います。

import xonsh.tools
import backtrace
import sys

# backtraceパッケージの_flush()をOverride
# 元コード : https://github.com/nir0s/backtrace/blob/f2c8683ec53e4fa48ea8c99c196b201bf22fda3e/backtrace.py#L36
def __flush(message):
    st = message + '\n'
    sys.stderr.buffer.write(st.encode(encoding="utf-8"))
backtrace._flush=__flush

# xonshのprint_exception()をOverride
# 元コード : https://github.com/xonsh/xonsh/blob/230f77b2bc64cbc3e04837377252793f5d09b9ba/xonsh/tools.py#L798
def _print_exception(msg=None):    
    tpe, v, tb = sys.exc_info()
    backtrace.hook(tb=tb, tpe=tpe, value=v)
    if msg:
        msg = msg if msg.endswith('\n') else msg + '\n'
        sys.stderr.write(msg)
xonsh.tools.print_exception = _print_exception

※上記xonshrcは最小限のコードです


こんな感じになり、コンソール汚染が減ってハッピーです。
一部ユーザ名を加工しています。
f:id:vaaaaaanquish:20171218231935p:plain

特に後者のpandasのkey errorなどは、本来30行近いエラーが出力されるのですが、半分程度に収まっていますし、かなり作業がしやすいかと思います。

 
backtrace._flush()は元のソースコードが以下のようになっています。

def _flush(message):
    sys.stderr.write(message + '\n')
    sys.stderr.flush()

後述しますが、xonshはsys.stderr.bufferが使える環境であればsys.stderr.buffer.write、使えなければsys.stderr.writeを利用してエラー内容を出力するようになっています(ここの調査に1日半かかりました)。
よって、backtraceの_flushをOverrideする時は、適切にPython環境に合わせてどちらかを選択して書き換えてやるのが正解です。

上記のxonshrcより丁寧に、sys.version_infoでバージョンチェックをして書いてやると良いです。
私はPython2以下は小学生に馬鹿にされるので使わないため利用してません。

 
また、xonshではエラー発生時に「tracebackを取得して、ログに保存して、メッセージを表示する」といった内容をまとめたxonsh.tools.print_exceptionを必ず呼ぶようになっています。
上記xonshrcコードでは、そのxonsh.tools.print_exceptionの」ほぼ全てをそぎ取ってbacktraceに任せるという形を取っていますが元のソースコードはそこそこ長くちゃんとしています。

元の機能をなるべく壊さないようOverrideする場合は、元ソースでtraceback.print_exc()となっている所をsys.exc_infoからのbacktrace.hockに書き換えてやるのが一番良いと思います。


 

- さらに見やすくするためにcoloramaのStyleを記述する -

backtraceパッケージでは、coloramaというANSIカラーコードを利用したカラーリングパッケージを使って、さらにStyleを変更する事が可能です。

上記xonshrcのbacktrace.hookする部分で、reverse(逆順表示)やstrip_path(ファイル名のみ表示)、styleのパラメータを設定してやれば良いです。
私は以下のようにしています。

import backtrace
from colorama import init, Fore, Style
STYLES = {
    'backtrace': Fore.YELLOW + '{0}',
    'error': Fore.RED + Style.BRIGHT + '{0}',
    'line': Fore.RED + Style.BRIGHT + '{0}',
    'module': '{0}',
    'context': Style.BRIGHT + Fore.GREEN + '{0}',
    'call': Fore.RED + '--> ' + Fore.YELLOW + Style.BRIGHT + '{0}',
}
backtrace.hook(reverse=True, strip_path=True, styles=STYLES, tb=tb, tpe=tpe, value=v)

これで以下のようにさらに情報量が減り、順番もトップを見るだけという感じになりました。
f:id:vaaaaaanquish:20171218230912p:plain

やったね。


 

- どのようにprint_exceptionが呼ばれているか -

ここからは蛇足です。
数日詰まっていた理由と解決に至った流れを書いておくものです。

 
以下のtools.print_exception()があらゆるException発生時に呼ばれる事はDocを見ても明確である。
xonsh/tools.py at 230f77b2bc64cbc3e04837377252793f5d09b9ba · xonsh/xonsh · GitHub
Tools (xonsh.tools) — xonsh 0.6.0.dev151 documentation

そして、xonshのshell上で入力される全てのコマンドやPythonスクリプトは、base_shell.default()にてコンパイルされ実行される。
https://github.com/xonsh/xonsh/blob/ff05ec33a22c1688674616a84ef66d65cef5b3c5/xonsh/base_shell.py#L313

default内3行目のpushメソッドが入力されたコードをcompileするものである。
ptk構成のshellの場合、以下_pushでOverrideされている。
https://github.com/xonsh/xonsh/blob/ff05ec33a22c1688674616a84ef66d65cef5b3c5/xonsh/ptk/shell.py#L123

正確にpushがOverrideされている箇所を示すと以下cmdloop。
https://github.com/xonsh/xonsh/blob/ff05ec33a22c1688674616a84ef66d65cef5b3c5/xonsh/ptk/shell.py#L145

 
pushは降ってきたスクリプトPythonのexecer.compileに投げてコンパイルして返すものである。

push時に@(x=1/0)とすればSyntaxErrorをコンパイラが検知してExceptionを返す。
しかし、@(1/0)は算術なので実行時にExceptionが出る(ここでかなり躓いた)。

 
実行時のExceptionというのは、ここ(run_compiled_code)で走った結果起こったものである
https://github.com/xonsh/xonsh/blob/ff05ec33a22c1688674616a84ef66d65cef5b3c5/xonsh/base_shell.py#L330

xonsh.codecache.run_compiled_codeはただのexecするだけの関数である
https://github.com/xonsh/xonsh/blob/f544e63699a19d3990d2abf1ff082c9b5f48176d/xonsh/codecache.py#L58


pushもrun_compiled_codeもException発生時、tools.print_exceptionが呼ばれており、一見違いが分かりにくい。

ここで出てくるのが独自のstreem io用のClassであるTee。
このTeeは、Pythonのバージョンの違い(sys.hoge.bufferのあるなし)を考慮しつつ、stdoutとstderrを同じIOで処理しながら、エラー前に$XONSH_STDERR_PREFIX、後に$XONSH_STDERR_POSTFIXを付けて、ログに保存しながら出力するためのものである。
https://github.com/xonsh/xonsh/blob/ff05ec33a22c1688674616a84ef66d65cef5b3c5/xonsh/base_shell.py#L217

以下で_TeeStdインスタンス作った時sys.stderrやsys.stdoutを関連付けしてる。
https://github.com/xonsh/xonsh/blob/ff05ec33a22c1688674616a84ef66d65cef5b3c5/xonsh/base_shell.py#L238
https://github.com/xonsh/xonsh/blob/ff05ec33a22c1688674616a84ef66d65cef5b3c5/xonsh/base_shell.py#L132


この_TeeStdがsys.stderr.buffer.write(byte入力)を利用しているため、backtraceが利用するsys.stderr.write(string入力)とで噛み合わない上、Teeのインスタンス生成タイミングはコードをcompileした後なので「@(x=1/0)の時はcoloramaのcolorが適応されるけど、@(1/0)の時は適応されない何で…」という事象が発生し、xonshの動作を逐一追うことになった。

これが全貌。


 

- おわりに -

最終的にbacktrace._flushもsys.stderr.buffer.writeに合わせてやる事で解決したけど、絶対もっとスマートに出来ると思う。
というかTeeのインスタンス作るタイミングは、普通にissue立てても良さそう。


Xonshアドベントカレンダーも空きの日のネタを用意してたのに書けてないしなかなか…という感じ。

誰か書いて下さい…

qiita.com

 

PythonのException発生時のTracebackを綺麗に見る

- はじめに -

PythonOSSパッケージ等を利用していると、Exceptionが発生した際に表示されるTraceback(正確にはスタックトレース)がかなり長い場合がある。

例えば、以下の簡易なコード実行で表示されるTracebackの行数は30近くなる。

import pandas as pd
df = pd.DataFrame(dict(a=[1,2,3]))
df['b']

引用 : python - Shorten large stack traces when using libraries - Stack Overflow


より複雑なプログラムにおいては、この比ではない。
にも関わらず、記述ミスのようにTraceback上位部にエラーの重要な内容がある場合もあれば、パッケージ内部のValidationで下位部が重要な場合もある。

得てしてPython開発環境として利用されるxonsh等の対話コンソール上やJupyter notebook等で100行近いエラーが出てきた時は、少なからず気持ちが折れる。

これらは、様々な言語の資産の利用などから引き起こされる長さである。
また設計上、過度なWrappingが行われた結果長くなっている場合も多々ある。

本記事は、主にPython3系において、Exceptionをcatchした時のTracebackを出来る限り綺麗に表示させ、あくまで開発のために人に優しい環境にするためのTipsやパッケージをまとめるものである。


※ 長く書いたが要約すると「xonshで作業してる時にクソ長いエラーが出てきてコンソール汚染されて腹立つから何とかしたい」と思っていたが調べてるうちに知見が溜まったのでまとめるという話


結論から言うとbacktraceかTBVaccineが良さそう。
 
もくじ:

 

- Tracebackの表示 -

そもそもPythonにおけるStackTraceは、Exception発生時にsys.last_tracebackに変数にtracebackオブジェクトとして格納される。

その中身をよしなに参照するためにsys.exc_info()が用意されている。
sys.exc_info()は返り値が(value, type, traceback)となっている長さ3のtupleなので、それらをtraceback.format_hogeに投げて以下のように直接中身を参照する事ができる。
(もちろんこんな実装を実際にしている人は居ないと思うが)

import sys
import traceback

try:
    x = 1 / 0    # ゼロ除算
except Exception as e:
    t, v, tb = sys.exc_info()
    print(traceback.format_exception(t,v,tb))
    print(traceback.format_tb(e.__traceback__))

# >>> ['Traceback (most recent call last):\n', '  File "<stdin>", line 2, in <module>\n', 'ZeroDivisionError: division by zero\n']
# >>> ['  File "<stdin>", line 2, in <module>\n']


tracebackモジュールには、traceback.format_exctraceback.print_excが用意されているので実際はこちらで十分である。

import traceback

try:
    x = 1 / 0    # ゼロ除算
except:
    print(traceback.format_exc())    # いつものTracebackが表示される
    traceback.print_exc()                 # これでも同じ

 
参考 : 29.9. traceback — Print or retrieve a stack traceback — Python 3.6.4 documentation


 

tracebacklimitの利用

Pythonでは、sys.tracebacklimitでトレースバック情報のレベル値を設定できる(0〜1000)。

そして0から1000と書いたが、この機能は既知のバグとしてPython 3.xで機能しない。
Issue 12276: 3.x ignores sys.tracebacklimit=0 - Python tracker

一応Noneにすることで、値とエラー内容だけ出せる。

import sys

sys.tracebacklimit=None
x = 1 / 0

# >>> ZeroDivisionError: division by zero

これなら str(Exception) で十分。
2系を使っていないならあまり恩恵が得られない。

また、traceback.format_exc(limit=1)等とした場合も同様に見える。


参考 : 29.1. sys — System-specific parameters and functions — Python 3.6.4 documentation

 

ColoredTracebackでシンタックスハイライト

Tracebackの表示にカラーリングするパッケージがある。

github.com

導入はpip

pip install colored-traceback
pip install colorama    # Windows環境下の場合

基本的には import colored_traceback.always としておけば良い

f:id:vaaaaaanquish:20171214053335p:plain


基本的にSyntaxはIPythonならサポートしてくれてるので、主にコンソールで作業する時用に。

 

Pygmentsでシンタックスハイライト

上記と同じ事がPygmentsでもできる(こちらの方が一般的か)。
Available lexers — Pygments

以下のようにsys.excepthookをOverrideするための関数を作ってやればよい。

import sys
import traceback
from pygments import highlight
from pygments.lexers import get_lexer_by_name
from pygments.formatters import TerminalFormatter

def myexcepthook(type, value, tb):
    tbtext = ''.join(traceback.format_exception(type, value, tb))
    lexer = get_lexer_by_name("pytb", stripall=True)
    formatter = TerminalFormatter()
    sys.stderr.write(highlight(tbtext, lexer, formatter))

sys.excepthook = myexcepthook

エラーがカラーリングされて見やすくなる。


   

- パッケージの利用 -

StackTrace表示を見やすくするための関連パッケージを示す。
backtraceが短く表示するやつで、それ以降は基本的に詳細表示のパッケージにあたる。

 

backtrace

StackTraceを短くしてくれるパッケージ。
上記colored-tracebackを使ってカラーリングもされる。

github.com

導入はpipで。Winの場合は上記colored-tracebackのcoloramaを先にinstallしておくと吉。

pip install backtrace


「はじめに」に記載のコードを実行してみる。

import pandas as pd
import backtrace

backtrace.hook(
    reverse=True,         # 逆順
    strip_path=True    # ファイル名のみ
)

df = pd.DataFrame(dict(a=[1,2,3]))
df['b']

f:id:vaaaaaanquish:20171214060800p:plain

これくらい分かれば何とかなるなって気もしなくもない。
何より慣れたpandasの30行近いエラーがこれに収まるなら良い。
基本的にはこれで作業して、backtrace.unhook() するのが良さそう。


また、sys.exc_info()の返り値を渡す形にすればコンソール、IPython上でも利用できる

import pandas as pd
import backtrace
import sys

try:
    df = pd.DataFrame(dict(a=[1,2,3]))
    df['b']
except:
    tpe, v, tb = sys.exc_info()
    backtrace.hook(reverse=True, strip_path=True, tb=tb, tpe=tpe, value=v)

ただし、中身がcoloramaでカラーリングしてるので色は変わらない。
この辺colored-tracebackに修正していけばかなり使えそう。

 

better-exceptions

Exceptionを見やすくするパッケージ。

github.com

エラー発生時に変数に格納されている値やClassをちゃんと出してくれて見やすい(?)
https://github.com/Qix-/better-exceptions/raw/master/screenshot.png


導入は以下

pip install better_exceptions

export BETTER_EXCEPTIONS=1  # Linux / OSX
setx BETTER_EXCEPTIONS 1       # Windows


試しにこんな感じのをやってみる

import better_exceptions

def zero(x):
    y = x/0
    return y

t = zero(10)
print(t)

ちなみにスクリプト実行時のみなので、対話コンソール上やIPythonでは現状使えない。
f:id:vaaaaaanquish:20171214100150p:plain:w500
それっぽい

しかしここからpandasのExceptionの方に適応すると以下
f:id:vaaaaaanquish:20171214101256p:plain:w400
厳しい

複雑になるとちょっと厳しいものがある。

 

TBVaccine

上記better-exceptionsよりちょっと見やすいやつ

github.com

https://github.com/skorokithakis/tbvaccine/raw/master/misc/after-vars.png

導入はよしなにpipで入れてTBVACCINE変数を設定しておくか、明示的にimportする。

pip install tbvaccine
export TBVACCINE=1

 
上記Githubの画像では設定された変数の中身まで出してるけど、以下のようにshow_varsをFalseにしておけば出ない。

import tbvaccine
tbvaccine.add_hook(isolate=False, show_vars=False)
import pandas as pd

df = pd.DataFrame(dict(a=[1,2,3]))
df['b']

f:id:vaaaaaanquish:20171214111250p:plain
良い感じである。


上記したsysモジュールっぽくも使えるのも良いところ。

from tbvaccine import TBVaccine
try:
    x = 1 / 0
except:
    print(TBVaccine(isolate=False, show_vars=False).format_exc())

これならxonshでも動くし、良い感じかもしれないと思っている。
IPythonでも動くし良さ。
 
 

その他調べたやつ

tracebackturbo

雰囲気はTBVaccineっぽい。変数の中身まで見たい時と見たくない時があるよなあって思うけど消せなさそう。
github.com
3系はこっち :
GitHub - cxcv/python-tracebackturbo3: A drop-in replacement for the python3 traceback module that enables dumping of the local variable scope aside normal stack traces.

 

git-stacktrace

pinterestのgitリポジトリはじめて見た。
stacktraceと一緒に、問題が発生した箇所のGitの修正履歴をコミット単位で表示してくれる。
github.com
毎日Git使ってたら便利なのかも。
今回は趣向と外れすぎたのでノータッチ。

 

python-tblib

tracebackをPickleで固めてraiseしていくやつ。
multiprocessで処理をした時のtracebackが見やすくなる。
github.com
今回は趣向と外れすぎたのでノータッチ。いつか触る。

 

Skip-Traceback(jupyter)

jupyterの拡張でトレースバックを表示せずエラーの種類とメッセージのみ表示するやつ。
github.com
今回は趣向と外れすぎたのでノータッチ。

IPythonならまだmagic commandのdebugやpdbデバッグする方が普通に良い気がする。
JupyterまたはiPython Notebookでデバッグをする方法 - Qiita
IPython の豊富な機能を使いこなす (2) - Qiita

IPythonならultratb.ColorTBも使えるけどなあ…
Module: core.ultratb — IPython 3.2.1 documentation


 

- おわりに -

結論私の求めていた、短く良い感じにExceptionを表示する方法はbacktraceかTBVaccine辺りだろう。

個人的にbacktraceの表示形式が好きなので、ここを起点にxontribを作っていくぞという気持ち。

IPythonにも活用できたら、Jupyter notebookでミスって実行しちゃった時に出る長いエラーとおさらば出来る気がする…


 
//--- 以下参考 ---
29.9. traceback — Print or retrieve a stack traceback — Python 3.6.4 documentation
traceback — Exceptions and Stack Traces — PyMOTW 3
__exit__ must accept 3 arguments: type, value, traceback — Python Anti-Patterns documentation

PythonユーザのためのJupyter[実践]入門

PythonユーザのためのJupyter[実践]入門

blog.dscpl.com.au
d.hatena.ne.jp


 
追記:
xonshに導入する記事も書きました
vaaaaaanquish.hatenablog.com
なんとかしてJupyter notebookもbacktraceにできないかなあと思っています。

xonshにおけるxontribの紹介

- はじめに -

この記事は、Xonsh Advent Calendar 2017 - Qiita 14日目の記事です。

xonshにおけるいわゆる拡張機能であるところのxontribについて書いていきます。

「オススメXontrib!」と行きたい所ですが、そもそも2017年末時点で公開されているxontribは少ないのでほぼ全てです。

 

- xontribとは -

xontribはxonshの拡張群です。

例えばhogeパッケージをロードするには、以下Commandを入力します。

xontrib load hoge

~/.xonshrcに上記Commandを直接書くか、~/.config/xonsh/config.json内にあるxontribsという名前のlistにパッケージ名を書いておくと、xonshセッション起動時にロードされます。

手前味噌ですが、私も作っています。


現行のxontribパッケージについては、公式ドキュメントにまとまっています。
Xontribs — xonsh 0.9.11 documentation
この中にあるやつないやつで、少しだけ便利になるやつだけ紹介していきます。

[2019/06/29] 続編で作る側になろうという記事も書きました
vaaaaaanquish.hatenablog.com


 

- 補完 -

apt_tabcomplete

apt-getコマンドをtabで補完できるようにします

# 導入
pip install xonsh-apt-tabcomplete
xontrib load apt_tabcomplete

apt-getのinstall、remove、apt-cacheのsearchの補完がサポートされています。
github.com

Ubuntuなどaptを良く使う環境でxonshを使う時は入れておくと良いと思います。

 

docker-tabcomplete

dockerコマンド周りをtabで補完できるようにします

# 導入
pip install xonsh-docker-tabcomplete
xontrib load docker_tabcomplete

docker imageの補完などがxonsh上で上手くいかないのですが、こちらを導入することで解決します。
github.com

dockerで複数のコンテナを扱いながら開発している場合に便利です。

 

scrapy-tabcomplete

scrapyコマンド周りをtabで補完できるようにします

# 導入
pip install xonsh-scrapy-tabcomplete
xontrib load  scrapy_tabcomplete

scrap crawlコマンドやscrapy checkコマンドの結果のcacheから、次のコマンドを補完してくれます。

github.com

scrapyをコンソール上で使っている人(そんな人が居るかどうかは別として)には使えると思います。

 

fzf-widgets

sshコマンドの補完、historyの検索をサポートしてくれます。

公式のGIFが大体全てを説明してくれています。
https://raw.githubusercontent.com/shahinism/xontrib-fzf-widgets/master/docs/cast.gif

# 導入
pip install xontrib-fzf-widgets
xontrib load fzf-widgets

github.com

historyや補完はもともと強力なxonshですが、sshのconfigから補完は一応デフォでやってくれないので便利。

 

thefuck

コマンドを打ち間違えた時に「fuck」と入力すればコマンド候補を入力してくれるxontrib。
xonshrcの書き方の記事にもある$SUGGEST_COMMANDSをもうちょっと便利にする感じ。

# 導入
pip install xontrib-thefuck
xontrib load thefuck

xonsh記事ではないけど以下見ると大体の概要がわかります
thefuckのインストール方法 - Qiita

github.com


  

- UI -

powerline

おなじみpowerlineのサポートxontribです
見た目を色々できます

https://github.com/santagada/xontrib-powerline/raw/master/screenshot.png

# 導入
pip install xontrib-powerline
xontrib load powerline

プロンプトの右側を $PL_PROMPT、下側を $PL_TOOLBAR で設定します。
whoやbranchなどはセクションです。
pl_available_sectionsコマンドで全ての使えるセクションを表示できます。pl_build_promptコマンドで再ロードします。

$PL_PROMPT = '!'    # 使わない時
$PL_TOOLBAR = 'who>time'

xonshrcの書き方の記事でも紹介した $PROMPT も使えます。

github.com

iterm2ではpowerlineフォントが化けたり、表示が重なったりします。
その時は以下で対応できます。
iTerm2+powerline文字化け対策めも - Qiita

一回やってしまえばカッケーコンソールでxonshできます。

// 追記:2019/09/15
現在開発が止まっているようでした(開発者とも連絡がつかない状態)。私が別途xontrib-traceback2というのを作っています。
github.com

 

prompt-vi-mode

promptの表示に「vi-modeかどうか」を加えます。

# 導入
pip install xontrib-prompt-vi-mode
import xontrib.prompt_vi_mode

導入後は、xonshrcの書き方の記事でも出た、$PROMPTや$RIGHT_PROMPTに "{vi_mode}" もしくは "{vi_mode_not_input}"を突っ込むだけです。

こんな感じ
f:id:vaaaaaanquish:20171213175148p:plain

github.com

仕方ないですがimportなところに注意です。
私はptkのショートカットを登録してINSERTとNORMALを切り替えています。
vim開けばよくね…」と言われればそれはそう。


 

- タスク、操作 -

z

ディレクトリ移動を潤滑にするxontrib。
/home/work に行ったことがあれば $ z work とだけ入力すれば高確率で移動できる。

# 導入
pip install xontrib-z
xontrib load z

基本的にはcacheされたとこに行きます

github.com

アドベントカレンダー内の以下でも紹介されていて、zshのz.shを上手く書き換えているので一読すると吉です。
qiita.com

 

autoxsh

.autoxsh」というスクリプトファイルをディレクトリに設置しておくと、そのディレクトリにcdした時に毎回そのスクリプトが実行されるようになります。

# 導入
pip install xonsh-autoxsh
xontrib load autoxsh

初回の実行時には以下autoxshを許容し実行するかのメッセージが出るのでYesで実行

Unauthorized ".autoxsh" file found in this directory. Authorize and invoke? (y/n/ignore): y

github.com

.autoxshは普通のxonshスクリプトが記述できるので、lsコマンドの自動実行や、注意喚起のprint、envの切り替えを記入しておくと便利です。

 

schedule

スケジューラです。
コマンドにtimeオブジェクトを投げておくと、指定時間に実行したり、delayをかけて実行したりできます。

# 導入
pip install xontrib-schedule
xontrib load schedule

以下scheduleパッケージwrapperです
schedule — schedule 0.4.0 documentation

import time

def func():
    print("20秒経ちました")

schedule.when(time.time()+20).do(func)

時間指定のwhenと、遅延実行のdelayが使えます。

github.com

これに似た方法を上手く使えば「xonshrcに書いておいて最速でxonshを起動しつつ、よく使うパッケージを遅れて読み込む」なんてこともできそうです。いつかやりたい。


 

- おわりに -

その他にも、xonsh用のenvである"vox"用のxontribxonsh上でcondaを扱うためのxontrib簡易エディタxoを提供するxontribコマンドを保存していくxontrib…などが使えそうです。

voxはまだ使った事無く、anacondaは宗教上無理。エディタはvimだしな…となって今の記事に収まっています。


Githubリポジトリを見てもらえば分かりますが、xontribは数行Pythonを書けばできてしまいます。
xonshでは実際色んなイベントをデコレータを付けることでキャッチできるので、自身でxontribを作っていくのもかなり簡易だと感じます。
 

xonsh アドベントカレンダーも後半戦に入り、空きが出てきたのでなんとか登録だけでもお願いします!
後半戦のどこかでxontrib作れたらいいなと思っています。

qiita.com