Stimulator

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

自動運転シミュレータのCARLAを動かす

- はじめに -

Python APIを備えた自動車運転シミュレータである「CARLA: An Open Urban Driving Simulator」を動かすまでの記事です。

CARLAはConference on Robot Learning 2017でも発表された、Unreal Engine 4を使ったシミュレータパッケージです。
自動運転技術開発のための様々なカメラ、センサー情報をPythonで取得し、車操作に反映する事ができます。

とやかく言っても仕方ないので以下YouTubeの動画を一回参照して下さい。

www.youtube.com

本記事はCARLAの導入と触りまでを記述するものです。

特に物を作ってどうこうしてるアレではありません。

 

- CARLAについて -

CARLAは、CoRL2017発表論文のAbstにもあるように、自動運転技術の発展のためのOSSです。

作者

作者はAlexey Dosovitskiy、German Ros、Felipe Codevilla、Antonio Lopez、Vladlen Koltunらで、IntelTOYOTAの研究所、Computer Vision Centerのメンバーです。
(Toyota Research Instituteなのでシリコンバレーの方だと思います)

 

競合

競合パッケージではMicrosoftのAirSimが一番有名だと思います

CARLAと同じくUE4を利用しており、C++, Python, C# and Javaのクライアントがあります。
また、CARLA同様、カメラ画像情報だけでなく深度やSegmentationのセンサー情報を利用できます。
車だけでなくドローンも対応しており、Windowsバイナリもあるのでパパっと始められる点で優位です。

強いてCARLAを使った理由は環境(天気とか)が変えられる事くらいです。
正直buildしんどかったのでAirSimの方が良いのかなと思ったりしています。

あと他にもUdacityの教材リポジトリもありました。
GitHub - udacity/self-driving-car-sim: A self-driving car simulator built with Unity
こちらはUnityを直接触る内容のようです。
こういうのがちらほらあります。

 

CARLAパッケージ情報

Github
github.com

Document:http://carla.readthedocs.io/en/latest/

CoRL2017発表論文:http://vladlen.info/papers/carla.pdf

 

動作環境

Documentには、WindowsLinuxの項目がありますが、現在Windows版の説明部分はComing Soonとなっています。
How to build on Windows - CARLA Simulator


Linuxでは、Ubuntu 16.04かそれ以降のバージョンを推奨しています。
How to build on Linux - CARLA Simulator

他に環境構築をしていて気になった点は以下です

  • pyenvやvirtualenv環境下のPythonではPATH周りで死ぬ
    • 素直にaptでpython3-dev入れるのが現状良い
  • ストレージがそれなりに必要
    • UnrealEngine本体のbuildと素材のダウンロード
    • 50GBでやったら足りませんでした
  • sudoersアカウント欲しい
  • Unreal Engine 4がOpenGL 3.x or 4.xを使う
  • 別途Windowsが必要
    • Winからしかダウンロードできないパッケージを使う
    • .NETFrameworkも必要
    • UE4の権利関係の問題


正直注意点が多いのでクリーンでストレージも多いUbuntu 16.04が入ったハード1つ用意出来るならそれがベストです。
その中でVMWindows動かすとかが良いでしょう。


 

- CARLAインストール -

buildしていきます。
一応ドキュメントに従っていきますが、まあまあ大変です。
How to build on Linux - CARLA Simulator

 

私の環境

一応私の環境を書いておきます

MacbookPro上のVMware Fusion
Ubuntu 16.04
python3-dev
ストレージ 100GB
コア数 4
メモリ 8GB
(UE4のEditor開くだけでもmmapで1.5GBくらい確保してたのでそれなりに必要)

別途適当なWindows10端末

 

依存パッケージのインストール

CARLA依存パッケージは以下コマンドで導入
途中でrequestsが必要になったのでここでインストールしています。

sudo apt-get install clang-3.9 cmake ninja-build python-dev tzdata sed curl wget unzip autoconf libtool python3-dev libxml2-dev libxslt-dev git build-essential python3-pip python3-requests
pip3 install protobuf requests


以下を参考にUnrealEngineのbuildの依存パッケージを導入
Building On Linux - Epic Wiki

# Ubuntu 16.04
sudo apt-get install mono-mcs mono-devel mono-xbuild mono-dmcs mono-reference-assemblies-4.0 libmono-system-data-datasetextensions4.0-cil libmono-system-web-extensions4.0-cil libmono-system-management4.0-cil libmono-system-xml-linq4.0-cil cmake dos2unix clang-3.5 libfreetype6-dev libgtk-3-dev libmono-microsoft-build-tasks-v4.0-4.0-cil xdg-user-dirs


各Setupスクリプトでchmodコマンドが幾つか使われているので、自身のアカウントをsudoersに入れておく。
Ubuntuでsudoersに自身を入れるには以下

sudo gpasswd -a {アカウント名} sudo


Unreal Engineとの依存関係と互換性問題を解決するには、コンパイラC++ runtime libraryを全て統一する必要がある。
今回は素のUbuntuが用意できたのでドキュメントの通り、clang3.9とLLVMのlibc++を使用した。

sudo update-alternatives --install /usr/bin/clang++ clang++ /usr/lib/llvm-3.9/bin/clang++ 100
sudo update-alternatives --install /usr/bin/clang clang /usr/lib/llvm-3.9/bin/clang 100

update-alternativesによってclang提供のメッセージが出ればOK

 

Unreal Engine 4のインストール

UnrealEngineをcloneしてくるが、EpicGamesのGithubリポジトリはcloseしているため、開発者として登録する必要がある。

以下URLの右上からサインアップしてGithubアカウントをProfileに記載する必要がある。
https://unrealengine.com

ここ
f:id:vaaaaaanquish:20180105214109p:plain:w400

サインアップしたら登録したメールアドレスの認証も行っておく(認証してないと後述の自動車素材がダウンロードできない)


サインアップ後、右上に人型アイコンができるのでそこから開発者用Profileページへ。
「接続済みアカウント」に行って下にスクロールしていくと、Githubアカウントを記載するフォームがあるので書く。
f:id:vaaaaaanquish:20180105214406p:plain:w400


https://github.com/EpicGamesとか適当にgithubページに行くと招待状が届いている。
invitationを表示してjoin memberする
f:id:vaaaaaanquish:20180105214423p:plain:w400
(背景が黒いのは自分が拡張しているから)


これでやっと以下スクリプトが通るようになるので、clone時にGithubアカウント認証して設定スクリプトを走らせてmakeする。

makeでは[x/N]みたいな表示が何回か出るし、素材ダウンロードもあるので適当なマシンでやると4時間くらいかかる。
最初適当に50GBくらいVMで容量設定したが、make時足りなくなって途中で止まったのでGPartedを久々に使う事になった。
ストレージは多めに見積もる必要がある。

git clone --depth=1 -b 4.17 https://github.com/EpicGames/UnrealEngine.git ~/UnrealEngine_4.17
cd ~/UnrealEngine_4.17
./Setup.sh
./GenerateProjectFiles.sh
make

 
Setup.shでは、「Success」となっていてもその下にエラーメッセージが表示されている場合がある(大抵権限周りで)。
~/.configを触る権限がないとかなのでsudoersでやり直し。

Setup.shの真のSUCCESSは以下画像参照
f:id:vaaaaaanquish:20180105215246p:plain:w400


makeする時もsudoersに入ってないアカウントで作業して、sudo等の扱い間違えると権限の関係で以下辺りで死ぬ。
chmodやchownコマンドで権限を読み書きできるようにして無理やり通しても、後続のCARLAビルドする時に死ぬので諦めてやり直すのが吉。

Refusing to run with the root privileges. (rootじゃmakeできないよー)
Access to the path "~~~~~" is denied (Access権限足りんよー)
CrushReport-linux-Shippingのレシピで失敗しました

  
権限周りを丁寧にやっていてもUE4Editorのレシピだけ失敗する場合がある。

Makefile:188: commands for object 'UE4Editor' failed
 make: *** [UE4Editor] Error 137

依存パッケージのビルド順の問題だったので以下のように1つ1つ愛のある手作業でビルドしていくと通った。

make ShaderCompileWorker
make UnrealLightmass
make UnrealPak
make UE4Editor
make CrashReportClient-Linux-Shipping RequiredTools UnrealFrontend

UnrealFrontend-Linux-Shippingじゃないの…と思ったけどUnrealFrontendっぽい。
参考:Compilation error: make: *** [UE4Editor] Error 137 - UE4 AnswerHub


もしUEのbuildをやり直したい時はmake cleanがないので以下のようにARGSを設定する
(Building On Linux - Epic WikiのEnhancing the Makefile)

make ARGS=-clean

 

CALRAのインストール

CARLAをcloneしてきてbuildする。
~/CARLA/Unreal/CarlaUE4/Contentへの追加や色々ダウンロード等が入るので、これもまた1時間ほどかかる。

(後で知ったけどCompiled versionもダウンロードできるっぽい…)
Release CARLA 0.7.0 · carla-simulator/carla · GitHub


Windows端末が必要な作業が後半にあるので、Setup中に出来る所までやっておくと吉。

ここでpyenv使ってる人はfaildやskippedが出たり、protobufやpyconfig.hが見つからないよと出る。
pyenv global systemして、UE4の関連パッケージインストールからやり直せば大丈夫。

git clone https://github.com/carla-simulator/carla ~/CARLA
cd ~/CARLA
./Setup.sh

SUCCESSとなったら設定おわり
f:id:vaaaaaanquish:20180105215841p:plain:w400

 
この後まさかのWindows端末が必要になる。
これも時間がかかるのでSetup中に出来る所まで以下やっておくと吉。

~/CARLA/Unreal/CarlaUE4/Content/配下に、追加で手動で自動車素材となるAutomotiveMaterialsを入れる作業である。
http://carla.readthedocs.io/en/latest/how_to_add_automotive_materials/

WindowsにAutomotive Materialsを追加したダミーのプロジェクトを作り、プロジェクト内にあるAutomotiveMaterialsディレクトリをLinux端末にコピーしてくる(現状ライセンスの関係で自分で入れる形になっているらしくこの方法の改善に取り組んでいるらしいが…)

 
以下URLからパッケージを0$で購入してダウンロードする訳だが、そのダウンロードがWindowsのLauncherアプリからしかダウンロードできない。
Automotive Materials by Epic Games in Epic Showcase,Materials - UE4 Marketplace

Windows端末で上記のパッケージのURLにアクセスして、右上の「ダウンロード」からEpic Games Launcher自体のインストールためのInstallerを落としてくる。

そしてInstaller起動してよしなにWindowsにインストール。
インストールできたらLauncherを起動して「次へ」をおしながら、.NETFrameworkが必要だったりするので、ウィンドウの表示に従ってポチポチしていけば良い。

起動したらLauncher内でマーケットプレイスに行けるので移動して、Automotiveで検索。
バージョンを4.17に合わせてプロジェクトを作成する(最新が4.18になってたので注意)。
f:id:vaaaaaanquish:20180105220528p:plain:w400
プロジェクト作成したらダウンロードがはじまる。

上記作業が終わったら、Windowsの「プロジェクト作成」で作ったプロジェクト配下にある{プロジェクト名}/Content/AutomotiveMaterialsディレクトリをコピーして、scp等でどうにかLinuxマシンに移動する。


Linuxに送信したら、Linux端末の~/CARLA/Unreal/CarlaUE4/Content/配下に送信してきたAutomotiveMaterialsディレクトリを丸々コピーする。
(ContentディレクトリはSetup.shによって生成される)

以下のようにPathを設定しておく

echo 'export UE4_ROOT=~/UnrealEngine_4.17' >> ~/.profile
source ~/.profile


Rebuildスクリプトを走らせて、素材ファイルのリンクと周辺のビルドをする。
OpenGL 3.x環境の場合はUE4Editorの起動時の引数に-opengl3が必要になるので、Rebuild.shの編集が必要。

Rebuild.sh 最後の方(65行目くらい)にある以下を変更
- ${UE4_ROOT}/Engine/Binaries/Linux/UE4Editor "${PWD}/CarlaUE4.uproject"
+ ${UE4_ROOT}/Engine/Binaries/Linux/UE4Editor -opengl3 "${PWD}/CarlaUE4.uproject" 
UE4_ROOT=~/UnrealEngine_4.17 ./Rebuild.sh


リンク作業のためにUE4Editorが起動する(ここでOpenGLが3以上でないと詰む)
初回起動時は初期化が入るのでそれなりの時間を要する。

f:id:vaaaaaanquish:20180105221116p:plain:w400
死んでいる様子

リンク作業は以下参照
http://carla.readthedocs.io/en/latest/how_to_add_automotive_materials/

コンテンツブラウザでContent/Static/Vehicles/GeneralMaterials/MaterialRedirectorsに行って「RedirectorInstance」を開き検索フォームで「Parent」を検索。DummyCarをM_Carpaintに名前を変えて上書き保存して終わり。
https://carla.readthedocs.io/en/latest/img/materials_screenshot_00.png
https://carla.readthedocs.io/en/latest/img/materials_screenshot_01.png

多分この作業で失敗すると人や車が表示されないので何度かやってみる。

 
以下のようにxbuild周りでエラーが出た場合は、UEのmakeをやり直すか権限設定を見直す。

ERROR: UBT ERROR: Failed to produce item
not found xbuild


 

- CARLA Serverの実行と操作 -

CARLAの操作はServerを建ててPython Clientから操作という形になる

具体的にはC lang APIを叩いており、ドキュメントにある図で言うとこんな感じ
https://carla.readthedocs.io/en/latest/img/carlaserver.svg

http://carla.readthedocs.io/en/latest/how_to_run/

ドキュメントの通りbuildに成功していれば~/CARLAディレクトリ直下に以下スクリプトができているので実行すると、UE4が起動しCarla Serverが動き始める。

./CarlaUE4.sh /Game/Maps/Town01 -carla-server -benchmark -fps=15

ない場合はbuild失敗してるか、compile済みのlatestから取ってくるか。

操作用のクライアントsampleを動かのに必要なパッケージを入れる

sudo pip3 install numpy Pillow pygame

別コンソールを開いて~/CALRA/PythonClient/内にあるsampleを動かせばひとまず操作できるようになる。

python3 ~/CALRA/PythonClient/manual_control.py

f:id:vaaaaaanquish:20180109215835p:plain:w400

manual_controlの操作は以下の通り

    W  : 進む
    S  : ブレーキ
    A  : 左
    D  : 右
    Q  : バッグ
    Space : ハンドブレーキ
    R   : restart

とりあえず楽しいです。


 

- おわりに -

ひとまず動作させるまでで終わりです。

本当は年末年始でAirSimも試し、クライアント作りまでやって設定色々書こうくらいまで思っていましたが、UE4のbuildに何回も失敗したり、Ubuntuが死んだり、CARLAが起動しても人Moduleが表示されなかったりとまあまあ辛かったのでここまでになりました。


CARLAが動けばAirSimも動くはずなのでチャレンジします。


次は自身で自動運転操作するまでの工程とか、CARLAのパラメータsettingについて書いていきたいと思います。

がんばるぞ。

 

Xonshのconfigを書く

- はじめに -

//------------------------
追記 2018/06/22:

以下の通り、config.jsonはサポートされなくなり、xonshrcになりました。

WARNING! old style configuration
(/Users/xxx/.config/xonsh/config.json)
is no longer supported. 
Please migrate to xonshrc.

xonshrcは以下にまとめました
vaaaaaanquish.hatenablog.com

//------------------------


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

xonshには.xonshrcに加えて、staticなjson形式のconfigファイルで設定するパラメータがある。

本記事では、config.jsonの書き方について記載する。


xonshrcについては以下
vaaaaaanquish.hatenablog.com


 

- config.jsonとは -

config.jsonはデフォルトでは ~/.config/xonsh/config.json に配置する。

より正確なPathは $XONSH_CONFIG_DIR/config.json である。

このファイルは、xonshrcより前に読み込まれ、外部シェルからのデータロードや、xonshrc内でも採用されるような重要な環境変数の設定、xontribの記載などを行うものである。

以下参考
http://xon.sh/xonshconfig.html


 

- configを書く -

現状keyとして読み込まれるのは、envxontribsforeign_shellsの3つ。

ここに書いておくと良い点も含めて下記に示す。

 

env

xonshrcファイルでも設定できるけど環境変数

静的なjsonファイルというだけなので、現状xonshとしてenv設定出来てメリットがあるのは以下くらいか。

{
    "env": {
        "XONSH_DEBUG": 1,
        "FOREIGN_ALIASES_OVERRIDE": True
     }
}

XONSH_DEBUGでデバッグ設定しておけば、xonshrc等のデバッグができる。
FOREIGN_ALIASES_OVERRIDEは、外部のShellのエイリアスを優先するかどうか。

Examples等では$EDITORとか設定してるけど、xonshrcの方が管理しやすい良い気がする。
Windowsなら$ALLUSERSPROFILEにrcファイルへのPathを書いておいてもよい。
Python系ならjupyterやpyenvへのPathなんかはここで書いても良い(気がする)。

 

xontribs

xontribsは、xonshの拡張であるxontribをlistで書いておくところ。

xonshrcでは以下のようにloadしないといけない所、パッケージ名をlistで書くだけになるのでちょっと楽。

xontrib load hoge

Sampleで自分のやつ。
名前を書いておくだけでよい。

{
    "xontribs": ["z","docker_tabcomplete","fzf-widgets"]
}

xontribは以下で書いたので参考に
vaaaaaanquish.hatenablog.com

 

foreign_shells

foreign_shellsでは、subprocessで呼び出す外部シェルの情報が記述できる。

より正確には、以下で呼び出されるforeign_shell_data()内で、subprocessとしてshellが一度起動され、該当shellのenvとaliasを読み込んでくる。
xonsh/foreign_shells.py at adcd20f72fcbe6962533cb3ad78a4a9ec396e150 · xonsh/xonsh · GitHub

 
list形式でdictを設定していく。dict中のkeyとしては以下が設定できる。
http://xon.sh/xonshconfig.html#foreign-shells

{
    "foreign_shells":[
        {
            # 起動したいshell
            "shell": "/bin/zsh",
            # 対話起動するか(しなくてもenv読み込めるshellなら不要)
            "interactive": True,
            # ユーザとしてlogin必要か
            "login": False,
            # 起動shellにおけるenvコマンド
            "envcmd": "env",
            # 起動shellにおけるaliasコマンド
            "aliascmd": "alias",
            # 起動するshellの引数(str)
            "extra_args": [],
            # 手動で起動shellにenv設定するためにdict
            "currenv": {},
            # 例外を安全に処理するか(Falseにするとraiseされる)
            "safe": True,
            # 色々読み込む前後にshellで実行させるコマンド
            "prevcmd": "",
            "postcmd": "",
            # 呼び出すshellのrcみたいなもん(以下参考に)
            # https://github.com/xonsh/xonsh/blob/adcd20f72fcbe6962533cb3ad78a4a9ec396e150/xonsh/foreign_shells.py#L33:title
            "funcscmd": "",
            # 呼び出したshellでsourceするファイル
            "sourcer": "",
            # 該当shellでスクリプトを実行する時のコマンド
            "runcmd": "",
            # 該当shellでexit-on-errorを設定するためのコマンド
            # "set -e"や"if errorlevel 1 exit 1"
            "seterrprevcmd": "",
            "seterrpostcmd": ""
        }
    ]
}

foreign_shellsで既に設定しているshell(xonshに乗り換える前に使っていた色々)をそのまま流用できたりする。


 

おわりに

あんまり書く機会がないかも知れないが、configについて記述した。

env、xontribs、foreign_shellsしか今の所書く事はないがxonshrcがちょっと簡略化される所がメリットか。


ひとまずアドベントカレンダー埋めた感じです。

qiita.com


 

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