Stimulator

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

mambaやripのinstallが何故早いのか調べたメモ

- はじめに -

最近、PythonのパッケージインストーラーであるpipをRustで書き直したripというツールが公開された。

github.com

ripのREADME.mdには、flaskを題材に依存解決とインストールが1秒で終わるようなgifが貼られている。

この速さは一体どこから来ているのか調べた。

 

- 宣伝 -

来週開催の技術書典15で「エムスリーテックブック5」が出ます。

私の内容は「自作Python Package Manager入門」で、CLIツールの作り方から始まって40ページでPyPIの仕様やその背景となっている要素を把握しながら、lock、install、run、build、uploadといったサブコマンドを実装してPackage Manager開発者になろうという内容です。Python開発経験2年くらいあれば作れると思います。

本誌ではまさかの「いろんな言語のパッケージマネージャ比べてみた」というパッケージマネージャネタ被りが起きており、こちらでは同僚がCargoやnpm、poetry、go mod、pnpm、yarnといったツールの内部実装を比較しています。本記事と同様にPackage Manager完全理解者の道を歩む事ができる本になっています。

techbookfest.org

# オンライン開催

  • 会期:2023/11/11 (土) 〜2023/11/26(日)
  • 会場:技術書典オンラインマーケット

# オフライン開催

12日は家庭の事情で午後からにはなりますが多分会場に居ますのでよろしくお願いします

- ripの成り立ち -

ripは、prefix-devというOrganization配下にある。
このprefixという会社は、実はAnacondaやその周辺ツールと関係があるため、まずその成り立ちから書いておく。

Anaconda

Anacondaは、知っての通りAnacondaリポジトリや周辺のcondaエコシステムに影響を及ぼす偉大な企業である。
あまり特別な情報はないが、会社のHistoryを見ていて2012年に出来た企業だと知って「思ったより若い」って思った。
もっとこうずっとあるイメージだった。

mamba-org

2022年、condaのエコシステムに大きな影響を与えた、「Mambaプロジェクト」というものがある。
フランスにjupyterやConda-forgeの開発者が集まるQuantStackという会社があり、そこに所属していた開発者@wuoulfが主軸になったOSSプロジェクトである。QuantStackの他の開発者も多くMambaプロジェクトに参画している。
BloombergやNumFOCUSなどから出資を受けているプロジェクトでもある。

Anacondaリポジトリやconda-forgeなどのcondaエコシステムへのアクセスは、CLIツール「conda」が長く利用されてきた。condaは大部分がPythonで書かれたツールであるのに対し、Mambaプロジェクトではcondaの互換性を保った形で多くをC++で再実装したツール「mamba」を開発、提供している。mambaはcondaに比べ、依存解決やパッケージのbuild方法の変更、並列化を行う事でも高速化されており、CLIツールとして表面だけ見てもより手軽にcondaエコシステムにアクセスできるようになっている。

github.com

そもそもMambaプロジェクトは、先に挙げたような便利な代替CLIツールの再実装やそれらの高速化とは別に、以下のような問題を解決するために立ち上がったプロジェクトであり、CLIツールはその結果の1つにあたる*1

故にMambaプロジェクトでは、CLIツールのconda代替「mamba」以外に、conda packageホスティングサーバの「Quetz」mamba内で高速にPackage buildするためのconda-build代替「boa」などをそれぞれ並行して開発している。
非常に大きいプロジェクトである。

中でもCLIツールmambaの実装が、condaエコシステムユーザにとって大きな恩恵をもたらしているという話である。
内部で利用される依存解決アルゴリズム等を含むコア実装「libmamba」も、QuantStackらにより開発された後、NumPy、SciPy、Jupyter、Matplotlib、scikit-learnなどPythonやAI/ML関連のライブラリのコア開発者らが所属するコンサルティング企業Quansightによって、condaに移植される事になる。これにより、condaはv23.10.0から大幅に高速化した。
www.anaconda.com

prefix.dev

先述のconda-forgeのコア開発者でもありmambaの発案者でもある@wuoulfは、QuantStackでのmambaプロジェクト後に「prefix.dev」という会社をドイツで立ち上げる。パッケージマネージャー開発を主軸とする会社であり、毎年開催されるパッケージマネージャーの国際カンファレンス「PackageCon」の主催企業でもある。

prefix.devは、conda packageの思想を拡張した、cross-platformmulti-languageに対応した高速かつ軽量なpackage managerである「pixi」を推して開発している。

世間一般ではcondaに関する言及の殆どがPythonやRに関するものであるため「condaエコシステムはPythonのためのもの」のような誤解があるが、conda packageの仕組みはaptやrpm等と変わらない。そのため、実際はどんな言語、どんなツールでもパッケージとして配布出来るし、installの仕組みを作る事ができる。prefix.devでは、conda package形式の高速なインデックスのホスティングhuggingface.coのような実行環境まで開発しており、本気で様々な言語のPackage Managementをpixiで飲み込んでやろうと企む気持ちが伺える。これらのバックエンドは、先に示したMambaプロジェクトで開発されたツールや関連の技術が使われている。
prefix.dev

当然、prefix.devはmambaやlibmamba、conda関連のツールの開発も牽引し続けている。
その他にも、prefix.devはパッケージマネージャーに関わるツールをRustで開発している。
著名な依存解決アルゴリズムbacktrackingのRust実装「resolvelib-rs」、libsolv(CDCL)のRust実装「resolvo」、condaエコシステムとのAPIのやり取りをRustでwrapした「Rattler」、そして今回の本題の1つ「rip」など、精力的に新しいツールを開発している。

mambaが全体的にC++で書かれており、そこから得られた知見を活かして、多くをRustで書き直そう、というスタイルのようだ。イカしてる。本当に頑張って欲しい。

- condaがinstallで行うこと -

話はロジック面に入る。先に示した通り、conda packageはPython以外でも扱えるようなフォーマットであり、condaエコシステムないしconda packageの構造は、PythonPyPIのものとはかなり違う*2。中身はさておき、Package Managerが気にするべきは、そのメタデータ取得方法になる。

Pythonパッケージの場合は多くの場合METADATAファイルを読みパッケージ情報を取得するが、conda packageの場合はrepodata.json(またはcurrent_repodata.json)である。repodata.jsonを含むファイルの構成は以下のようになっている。

https://docs.conda.io/projects/conda-build/en/stable/concepts/generating-index.html より

この構成のリポジトリに対して、condaが行うinstall作業は以下のようになる。

  1. 関連するrepodata.jsonファイルを全てダウンロードしてメモリ上に乗せる*3
    • repodata.json.bz2のような圧縮形式にもアクセスできるので必要あれば展開する
    • repodata.jsonの中にdependsがあり依存関係情報が入っている
  2. すべての依存関係の中から環境で使用される可能性のあるパッケージを絞り込む
    • 例: cudaが必要な場合はそうでない物を削る
  3. SATとしてSATソルバを再帰的に実行する
    • SATソルバを呼び出す前に優先度決定、pruningを毎回実行する
      • 例: 満たす必要のなくなった依存関係を先んじて削ってしまう
      • 例: 可能な限り最新バージョンを使う
      • 例: pruningとして削除された関連ファイルが多いパッケージを優先する
      • パッケージ内の優先度をパッケージ開発者が決めれたり*4もする
  4. SATソルバの結果得られたパッケージをinstallする
    • install_scriptを走らせる
    • 必要に応じてhard link/soft linkが使い分けられる

SATソルバは、元々PicoSATだったものが、CryptoMiniSatになり、現在はlibmamba(libsolv, CDCLの拡張実装)となっている。
この場合、速度において重要なのは、SATソルバとCaching戦略と依存解決を行う優先度の3つになる。この辺りはcondaの長い歴史の中で磨かれてきたものがあり、優先度ロジックは以下の「Running the Solver」にまとまっている。1つ1つはあまり難しいものではないので以下記事を参照して欲しい。
www.anaconda.com

mambaは、condaの積み重ねてきたテクニックを踏まえつつ、大きく再実装を実施することで速度面を改善している。

- mambaでの速度改善 -

mambaの大掛かりなC++採用において、依存解決ロジック周辺の変更は速度に大きく影響している。
condaは、PicoSATのPython wrapperであるpycosat、CryptoMiniSat(msoos/cryptominisat)のPython wrapperであるpycryptosatを依存解決に利用していた。先のcondaのinstallのロジックの説明の通り、condaの実装はSATソルバと他ロジックを複数回行き来しており、依存解決はC++、優先度決定やCachingの戦略はPythonという形になっている。Pythonとのやり取りがボトルネックになるため、mambaではrepodata.jsonを取得後、前章②の段階からC++に情報を渡す実装となっている。これにより「複数回のSATソルバ呼び出し」が無くなった。また、C++を直接扱えた事で、libsolv、libarchive、libcurlなどの強力なC++資産をそのまま扱う実装にもなっている。「優先度決定等のためのPythonC++間のオブジェクトの行き来」も大幅に無くなり、これらが速度に影響を与えている*5

依存解決のロジックは、openSUSEなどで利用、開発されている信頼と実績のあるlibsolvを叩く形に変わった。
アルゴリズムは変わったが、その際のベンチマーク自体には大きな変化は現れていないようなので、上記のC++再実装によるリファクタ効果が大きかったように感じられる。

全体感としては、開発者の@wuoulfによる記事やPackageConの動画があるため、そちらを参照するとよい。
wolfv.medium.com
www.youtube.com


また、repodata.json.zstといったストリーム可能な圧縮形式を用いてパッケージ情報取得を高速化している。ファイルのダウンロードにおいても、dnfなどでも使われるlibrepoをガッツリC++で書き直した「powerloader」も開発している。これにより、ファイル取得の並列化やrestart可能な分割ダウンロード、zchunkなどをサポートしている。powerloaderを利用して、repodata.jsonが更新されても更新されたbitのみ取得する方法も実装されており、ファイルダウンロードの側面からも速度が改善されている*6

 

- ripに応用されたこと -

ここまでで得られたテクニックを利用し、Rustでまるっと書き直したのがripにあたる。
Rustで書くことで、asyncで高速に依存解決とinstallができるようにな実装になっている。依存解決はlibsolv(CDCL)のRust実装「resolvo」をprefix.devが自前で作り利用している。これによって、PythonC++な部分が無くなり*7簡素で高速な実装になっている。前述したpixiにもresolvoが使われており、issue上ではpubgrub-rs作者との交流もあり、PubGrub*8導入などさらなる進化の余地を考えられているといった所だろう。

ロジック面のポイントとして、resolvoに入っているIncremental solving*9という考え方が高速化に繋がっている*10*11
Pythonパッケージにおいては正確なメタデータPyPI APIから返ってこないため、METADATAファイルに何らかの形でアクセスする必要がある。これがいかんせん高コストである。なので優先度付きキューにメタデータ取得処理を詰め込んで、Solverは非同期的にPackageの情報を取得、追加しながら探索を行う。故にIncremental。また、その際の探索パッケージの優先度決定はPubGrubのdecision makingに似た考え方を採用しており、最新バージョンを優先しながらもなるべく依存パッケージが少なくなる方向性に向かう*12。この実装により、Solverがメタデータ取得等で殆ど止まる事なく依存解決を行う事が出来ている。またファイルの分割ダウンロード等も実装されており、resolvelib-rsやpowerloader等を作成した経験、pubgrub-rsへの貢献を経て作成された依存解決ライブラリである事が伺える。

注意点として、Pythonパッケージのフォーマットはsdist/bdistの2種類があるが、ripはsdistに対応していない。
condaエコシステムと違い、PyPIの依存解決が高コストな理由うちの1つにsdistフォーマットがある。近年ではbdist(wheel)が十分広まりつつあるので、比較的新しいバージョンを指定すれば正常かつ高速に動作するかもしれない。一方でsdist未対応につき、installできないパッケージやバージョンが存在するという欠点にも繋がっている。また、ベンチマークでsdistが依存関係に入るものと比較する場合、それを実際使わずともsdist対応パッケージマネージャーは勝てない要素が多くなるので、公正なベンチマークが求められる。この辺りが今後asyncと絡んだ時にどうなるかポイントで、実際ripがsdistに対応した時の速度がどのようになるか未知数だと思われる。

- おわりに -

今回、mambaやripについて調べた。
元々conda周り知ってないとなと思って調べてあったが、エムスリー エンジニアリングフェローの@SassaHeroが気になっていたので「技術書典の熱も余ってるし書くか〜」と思い書いておいた。


コードは読んでいるが、condaをユーザとして使い倒しているわけではないので、もし間違いがあればこのブログのリンクと一緒に参考リンクをXで呟いておいて欲しい。


Rustのパッケージマネージャーといえば一時期ryeが話題になったりもしたが、ryeはRustとはいえ中身は殆ど既存のPython資産を叩いているもので、実際既存ツールと同じ問題に行き着いている様子が伺えていた。もっとRust寄りのパッケージマネージャーだとhuakがあるが、ずっとWIP状態である*13。しかしながらripの精力的な開発を見ていると、JSやCLIツール群がそうであるように、PythonのPackage Manager周辺にもRustが増えそうな感じがしてくる。依存解決などの難しい部分をprefix.devがRust化しているというのが明らかに大きい。Linterには最近よく使われる所でRuffがあるし、env環境マネージャーもyenのように挑戦者が居る。必要最低限のツールが出揃いつつある中で、シレッとprefix.devがこのまま全てRustなPython Package Managerを出してしまう気もする。
今後もPyPA等が公に導入することはなかなか無いだろうけど、こういったconda系統を経由してサードパーティとしてRustが増えていく感じがあるのは、Pythonの多様なユースケースの結果と捉えると面白い。


なお、こういったPyPI周辺のお話を日本語で知りたい場合は「PyPI APIメタデータ取得はどうなっているのか」「sdist/bdistとはどういう歴史で生まれた何なのか」「どうPythonパッケージをインストールするのが正解か」「これからこの問題はどのようになっていくのか」を技術書典の本の方に書いたので参照されたい。

techbookfest.org


是非どうぞ

*1:Mambaプロジェクト創設者らのブログより

*2:conda installのドキュメントもしくはconda開発者の2015年スライドが全体感を掴むのに良い

*3:fetching

*4:track-features

*5:実装は読んだが私が実際にベンチマークした訳ではないので実態は不明

*6:libmamba vs classic — conda-libmamba-solverが詳しい

*7:正確にはPythonのパッケージ情報を扱うpackagingだけvendoringされているが

*8:PythonのPackage Managerを深く知るためのリンク集を参照して欲しい

*9:#349

*10:公式のブログが後日出るらしい https://prefix.dev/blog/introducing_rip#step-2-make-the-solver-lazy

*11:こちらも詳しい https://github.com/pypa/pip/issues/7406#issuecomment-583989243 https://github.com/pubgrub-rs/pubgrub/issues/138

*12:solver/decision*.rs

*13:PyPAメンバーの@uranusjrがシレッと公開していたmoltも今見たら更新されていなかった…

pipとpipenvとpoetryの技術的・歴史的背景とその展望

- はじめに -

Pythonのパッケージ管理ツールは、長らく乱世にあると言える。

特にpip、pipenv、poetryというツールの登場シーン前後では、多くの変革がもたらされた。

本記事は、Pythonパッケージ管理ツールであるpip、pipenv、poetryの3つに着目し、それぞれのツールに対してフラットな背景、技術的な説明を示しながら、所属企業内にてpoetry移行大臣として1年活動した上での経験、移行の意図について綴り、今後のPythonパッケージ管理の展望について妄想するものである。

注意:本記事はPythonパッケージ管理のベストプラクティスを主張する記事ではありません。背景を理解し自らの開発環境や状態に応じて適切に技術選定できるソフトウェアエンジニアこそ良いソフトウェアエンジニアであると筆者は考えています。

重要なポイントのみ把握したい場合は、各章の最後のまとめを読んで頂ければと思います

 


 

- Pythonのパッケージ管理ツール概論 -

まずはじめに、2020年3月現在、主に「Python パッケージ管理」等のワードでググると出てくる3つの大きなツール、pip、pipenv、poetryについて概論的に説明する。

最も表面的な概要だけ把握するために、私が社内向けに作成した資料を以下に引用する。

f:id:vaaaaaanquish:20210321112826p:plain
社内LT向けに作成した資料より加筆の上抜粋

多くは後述するが、ここでスライドが主張している重要な点を文字におこしておくと以下のようになる。

  • pipenvはPyPAが開発しているツールで同じくPyPAが開発するpipの機能を補っている
  • pipenvとpoetryの大きな"機能"の違いはパッケージのbuild・publishをサポートしているかどうか
  • pipenv、poetry以外に他にも多くのパッケージ管理ツールが存在する

本記事のタイトルを解消する上で重要な点は上2つであり、これらについてpip、pipenv、poetryの概要を示しながら本章で触れていく。本記事においては、Anacondaや他ツールについては本筋ではないため、本章の最後に簡単にまとめて触れる。

pip

pipは、最も基本的なPythonのパッケージ管理ツール、より正確にはパッケージインストーラである。PyPI*1より、Pythonモジュールをインストールするためのツールであり、Pythonの拡充提案、プロセス、設計、標準を決める文書群であるPEP内の、PEP 453によって、Pythonが公式にサポートするツールとなっている。Python3.4からは、Pythonに内包されデフォルトでインストールされる。
github.com


パッケージ管理の側面から見た場合は、以下のようなrequirements.txtというファイルを記述し、そのファイルで利用するモジュールのバージョンを管理する形式を取る。

numpy<=1.0.0
pandas==1.1.5
jupyterlab>=2.0.0
...

pipは具体的に依存treeを生成するような依存解決は行わず、順番にインストールしていき、サブモジュールの依存関係に問題があった場合は上書きする事で解決する。上記であればpandasやjupyter labによってnumpyはバージョンアップしたものが入る事になる。

pipと後述するpipenvの関係において、上記を背景とし一般的なプログラミング言語がプロダクト開発でそれを必要とするのと同様に「依存関係を解決する」「lockファイルを生成しサブモジュールを含む全てのモジュールバージョンをhashで管理する」事が求められた結果pipenvが開発された、という認識は間違いではない。実際、上記のrequirements.txtだけではパッケージングを行う事は出来ない事は想像できるだろう。
長らくユーザはsetup.pyやsetup.cfgを書き、PyPAが別途作成するwheelsetuptoolsといったツールでパッケージング、また別途PyPAが作成するtwinを用いてパッケージレジストリであるPyPIにアップロードするというライフサイクルに倣っており、バージョン固定や開発環境管理は各ユーザに委ねられていた。pipenvやそれら以外のツールについて具体的には後述するが、pip自体が手続き的なバージョン管理方法やパッケージングの方法を持っていないが故に、別ツールとしてパッケージングツールが作られていった結果として、今多くのパッケージング管理関連ツールが生まれ続けるPythonの環境になっているとも言える*2

 
2021年においては、パッケージ管理の側面で、上記の前提に加えてpipの2つの大きな変更を把握しておく必要がある。それがpyproject.tomlと2020-resolverである。

 

pyproject.toml (PEP 518)

1つ目に、PEP 518で制定されたpyptoject.tomlというフォーマットがある。後述のpoetryで利用されている事は広く知られているが、pipもまたpyproject.toml対応しているという点が重要になる。

pyptoject.tomlは、以下のような単一のtomlファイルをrequirements.txtやそれらを参照するsetup.pyの代替とする事が出来るようになっている*3

[install_requires]
pandas = "*"

[build-system]
requires = ["setuptools", "wheel", "toml"]

これはpip単体では大きな旨味はないが、pyproject.tomlが単一のファイルでパッケージのメタデータや依存ライブラリ、フォーマッタやenv環境、buildの設定を記述できる事を踏まえ、後述のpoetryのようなPEP 518対応のパッケージングツールと組み合わせる事で、ファイル及びツールを小さく集約する事が可能になっている*4

かなり簡単に言えば、これだけ書いとけば開発関連の設定はオールオッケーなsettings.tomlである。

pyproject.tomlについてより詳しくは後述するが、現在最も注目される仕様の1つであり、把握しておくと良い。

 

2020-resolver

pipにおける重要な変更として、2つ目に、現在テスト段階にある依存解決を行うリゾルバ、2020-resolverのリリースがある。

先述の通り、pipは強い依存解決を行わないツールだった。故にpip単体では、依存モジュールのバージョンの違いと、それらのインストールの前後関係によって問題が発生してしまう*5。対して2020-resolverは、シンプルなbacktrackingを利用した依存解決リゾルバであり、強い依存解決を事前に行う事で、pipのインストールの前後関係による課題を解消するものである。

リリースブログより各リンクを追う形で、多くの議論が追える。
pyfound.blogspot.com
 
 

さて、ここで本記事で最も重要な比較の話をするため、依存解決というテーマに触れておく。

多くのパッケージ管理ツールが行う重要な機能の1つに依存解決がある。パッケージの依存解決を行うリゾルバ(resolver)が、非常に難しい問題に挑んでいる事は、dependency hellという言葉と共によく知られている通りである。

en.wikipedia.org

依存解決リゾルバのアルゴリズムでは、SAT*6における教科書的なbacktrackingアルゴリズム*7を利用した解決方法が知られている*8。有名所では、RubyのBundlerが依存解決のために利用しているMolinilloというライブラリがそのresolverのbacktrackingロジックを持っていたり、RustにおけるCargoのコアロジックにあたるrust-semverでもDFS(深さ優先探索)とbacktrackingが利用されている。言語系の依存解決リゾルバが〜〜〜という話ではなくOpenSUSEのLibzyppでもminiSATベースのライブラリであるlibsolvが利用されている。
 
また、近年では2018年にGoogleのNatalie Weizenbaum(@nex3)Dartのパッケージ管理ツールのために提案したPubGrub*9というアルゴリズムが話題になっている。PubGrubは、backtrackingの拡張アルゴリズムであり*10、非互換性のあるパッケージを見つけた時に効率的に探索を行うUnit Propagation*11を組み合わせる形で実現されている。PubGrubは効率的かつ解釈可能な非互換性を見つけられる点でより優れたロジックで、SwiftPMにマージされていたり、先に登場したCargo内でも議論があったり、RubyにもPubGrubベースのgel-rbという新しいパッケージ管理ツールが出てきていたり、Elm版の開発等も進んでおり、後述するpoetryでもPubGrubが利用されている

PubGrubの提案者による解説記事。Dart側のドキュメントと照らし合わせながら読むと、Unit Propagation、Conflict Resolution、Decision Makingの対応が頭に入りやすく分かりやすい。
medium.com

 
本題のpip 2020-resolverでは、シンプルなbacktrackingが採用されている。このロジックは以下で議論されている。
github.com

ここでは、PubGrubベースのMixology*12やbacktrackingベースのResolveLibZazo、他にも先述したlibsolvMicrosoft Researchが提案したZ3などを比較し検討している。途中でpoetryのメンテナである@sdispaterがpoetryのリゾルバ部分を書き出したrepoを提示していたり依存解決リゾルバのアドバイスを行っている所も面白い。
 
議論の結果、APIインターフェースやコミュニティ、開発速度の観点から、PyPAメンバーである@uranusjrが作成していたbacktrackingベースのresolvelibが使われる事になった事がわかる。解説は以下のブログ。
pradyunsg.me

ここでの選定理由として、ピュアPythonありコミュニティないし支援企業がついている中で開発速度にとって嬉しい事が大きい要素になっている。
pip 2020-resolverのプロジェクトにはCZI社*13Mozilla*14による金銭的なサポートが入っており、PyPAにとっても重要なテーマであったことから開発速度が得られるに越したことはない背景があった。
pyfound.blogspot.com
(PyPAのメンバーであるソフトウェアエンジニアらが仕事の傍らボランティアで行ってきたpipの開発について、この2020-resolverの開発終了まで金銭面でサポートする内容になっている)

 
この2020-resolverで、pipは強い依存解決を行うようになり、長らくpipで言われていた

“Why does pip download multiple versions of the same package over and over again during an install?”.
(なぜpipは同じパッケージの複数のバージョンを何回もダウンロードするのか?)

という大きな課題が解消し、速度も向上する事になる。この背景について、PyPAのユーザガイドにも長文で綴られているので、見ておくと良い。
pip.pypa.io
(Python2もサポート終了で早く止めろと言われており各位には早く止めてもらいたい)

 
 
更に少し横道に逸れるが、2020-resolverへの変更によってpip installで依存解決が失敗したらResolution Impossible エラーを出すようになる。これにより、多くのCI/CDが止まってしまったり、OSSに「あなたのパッケージpipでインストールできないんだけど」というissueが増えるのではといった懸念が思い付く。これらは、2020-resolverの展開方法として以下のissue等で議論されている。
github.com
大きな問題があればissueを追うか、ユーザガイドが示す通り、『How do I ask a good question?』を読んだ上で以下のコミュニティの何れかで質疑すると良い。

 

2020-resolverは2020年10月からデフォルトになっており、21.0には完全に古いリゾルバを使えなくなる*15今までpipenv等が依存解決を行っていた所にパッケージインストーラーだったpipが依存解決を行うようになるわけなので、全体に影響する大きな変化の1つであると言える。

 

pipenv

pipenvは、pipに対して、依存解決およびlockファイル生成、env環境制御機能の提供を行う、PyPAが開発するパッケージ管理ツールである。依存解決およびlockファイル生成にはpip-tools、env管理はvirtualenvを利用しており、それらを1つのPipfileと呼ばれる独自フォーマットで管理できるツールとなっている。
github.com

Pipfileは以下のようなフォーマットになっている。

[dev-packages]
coverage = "*"
yapf = "*"
mypy = "==0.580"

[packages]
numpy = "<=1.0.0"
pandas = "==1.1.5"

[requires]
python_version = "3.9"

ここでは、開発用のパッケージ、モジュールが利用するパッケージ、envの設定を記述する事ができる。

pipとの大きな違いは、先に挙げた依存解決、lockファイル生成、env環境の制御である。

依存解決アルゴリズムでは、pip-tools内で先程の2020-resolverと同様のbacktrackingを利用している*16
github.com
pipenvは、pip-toolsが持つlockファイル生成の機能をwrapしている状態であると言える。

env環境については、同じくPyPAが開発するvirtualenvの機能をwrapし、env環境を提供する形となっている。
github.com
先に示した通り、pipが依存解決を行わない動作をしていた頃に、依存解決の機能に加え、一般的なソフトウェア開発に必要なenv環境の提供を可能にしたツールである。

 
pipenvはpip同様、パッケージングについては完全に別で処理する思想を持っている。setup.pyやsetup.cfgを記述し、それぞれ別のツール(wheeltwin)を利用してbuild, publishを行いPyPIに公開するライフサイクルとなっている。publishに関連したそれぞれのツールもまたPyPAが開発しており、このような目的毎に分割されたツールを組み合わせたりwarpしたりしてパッケージングすることが、PyPAが示すPythonパッケージングのライフサイクルの主たる形であると言える*17

 

poetry

poetryは、pipenvに対して先のpyproject.tomlの仕様を拡張を利用し、依存解決からlockファイル生成、env環境管理、パッケージングまでを行えるようにしたパッケージ管理ツールである。先のPipenvと違い、PyPAではなく、1ユーザである@sdispaterが主導し開発されている。
github.com

READMEには『Why?』という章がありそこでは、pipenvに対する課題とpoetryを作成した経緯が書かれている。要約すると以下の3点になる。

  • setup.pyやrequirements.txt、Pipfileを導入せずcargoやcomposerのようにbuildしpublishしたい
  • pipenvの意思決定のいくつかが好みでなかった
  • より良い依存解決リゾルバの導入

 
まず、1つ目の課題を解決したのが、先にも紹介したpyproject.tomlである。pyproject.tomlには以下のようなセクションを記載する事で、パッケージングに関する情報を付与でき、poetryがこの情報を読む事でbuild、publishが出来るようになっている。

[tool.poetry.dependencies]
python = "^3.6.2"
pandas = "*"

[tool.poetry.dev-dependencies]
coverage = "*"
yapf = "*"
mypy = "==0.580"

[tool.poetry]
name = "sample"
description = "This is sample"
authors = ["author"]
version = "1.0.0"
readme = "README.md"
homepage = "https://github.com/author/sample"
repository =  "https://github.com/author/sample"
documentation = ""
keywords = ["Example"]

[build-system]
requires = ["poetry"]
build-backend = "poetry.masonry.api"

[tool.poetry]となっている箇所は、poetry独自の拡張セクションである。pyproject.tomlとしてフォーマットが決まっている事で、拡張セクションを追加出来るようになっており、そちらを上手く利用した形になる。

  
依存解決については、先述の通り、backtrackingを採用せず、PubGrubを最初から採用している。これによって、依存解決を高速に行えるだけでなく、backtrackingが依存ツリーを戻る量で制限しているような枝を探索する事ができるようになっている。具体的には、READMEに書いてある以下のような例でpipenvが失敗する依存解決を遂行できるようになっている。

pipenv install oslo.utils==1.4.0

実際はpipenvが利用するpip-toolsのリゾルバとの比較になるが、このリゾルバの違いによる問題については、pipenvとpoetryの違いに関する章で後述する。

 

Anacondaなど他ツール

少し記事タイトルと逸れるが、現状を知る意味で概論として他のツールにも触れておく。

 
ここまで書いてきたように、Pythonのパッケージ管理エコシステムは、常々PyPAという団体によって整備され、PEPを通して制定され、PyPAが、インストーラとなるpipを代表にパッケージングであるsetuptoolsやwheel、PyPIへのアップロードはtwine、仮想環境はvirtualenv、依存固定はpipenvとそれぞれ作ってきた経緯がある。

そんな中で、PyPA以外の全く別のツールや似通ったシステムが栄枯盛衰を繰り返しているのは、常々人が作るソフトウェアの難しさに触れる事に近しい。最初から誰もが満足するツールなど作れないし、Python自体のユーザが増えた事によるOSS界隈の情勢の変化、影響範囲の拡大、速度、文化など様々な要素がひしめき合った結果、poetryやその他多くのツールが生まれたと言える。


中でもAnacondaは、pipがまだ多くのライブラリのインストールに失敗しバージョンやenvを管理する方法が定まって無かった中で、挑戦的な方法を取ることで出てきたパッケージング管理ツールである。

Anacondaは、Anaconda,Incという企業が開発、運用している点やその他GUIやenv提供の機能を持つ特徴こそあるが、パッケージ管理の側面でも他ツールとは少し違う独特なツールである。
www.anaconda.com

Anacondaは、Anacondaリポジトリ*18なるpypiのミラーサーバから、「環境に応じた最も良いもの」をインストールする機能を持ったパッケージ管理ツールである*19。最も象徴的な例として、Pythonに近い界隈では「numpyが利用するBLASの違い」についての話題が毎年2,3度バズっている。Anacondaがインストールしたnumpyが早いというものだ。Google検索「numpy anaconda 早い」と検索し上から記事を見ていけば概ね把握できるだろう。
www.google.co.jp
他にもCUDAとDeep Learning関連ライブラリのバージョンを照合する機能など、手元の環境とライブラリの依存関係を照合する知識がなくとも構築出来るのが1つの"売り"であると言える。

反面、過去何度も多くの開発者やツールが「環境に応じた最も良い物を提供する」事に挑戦した結果と同様に、依存関係の差異に苦しんだり環境を破壊する結果になることもある。特にpipと併用する事がHardlinkによって困難な点が最も大きい*20。技術的なメリット・デメリットについては以下の記事には目を通しておくと良い。
www.anaconda.com

大きな企業単位であれば十二分な基盤やVMを上手く組み合わせAnacondaを利用した理想の環境で開発できる企業もあるだろうが、その破壊的な機能や近年企業規模に応じて有償化するようになり、導入におけるデメリットも存在するし、もちろん逆に良いこともある。

Anacondaは、過去pipのインストールがまだ不安定だった時期に登場し、依存解決や独自のpackage registryの提供で大きく貢献した。また依存解決リゾルバの改修にも積極的かつ、ミニマムなAnacondaにあたるminicondaも存在しており、大きな一つの勢力であると言える。

 
他ツールでは、先程から出てきているpip-toolsを利用する方法、rustで書かれたpyptoject.tomlで管理可能なpyflow等が現状の選択肢に入る。海外ではflitも人気があるようだ。


 
pip-toolsは、pipenvやpoetryと違い、env管理は別のツールで実施するという思想で作られているツールに当たる。その概念は、公式の以下の図が明確に表している。

f:id:vaaaaaanquish:20210320225037p:plain:w350
pip-tools公式repoより

ユーザはrequirements.inというrequirements.txtと同様のformatで、自身が利用したいモジュールのバージョンを記載する。pip-toolsが提供するpip-compileコマンドは、inファイルから依存する全てのモジュールのバージョンが固定された状態で記載されたrequirements.txtを生成するジェネレータの役割を果たす。また、hashを利用した固定にも対応している。ここで生成されたrequirements.txtファイルを利用してpip-syncコマンドでenv環境に流すという構想である。この思想に則りさえすれば、env環境と切り離せるため、複数人での開発等を考えた時、個々に手慣れたenv系のツールを使ってもらえば済むというメリットがある。

 

pyflowは、rustで書かれたPythonパッケージ管理ツールである。
github.com
機能としてもpoetryが採用するPEP 518、pyproject.tomlによる管理方法に加え、PEP 582にあたるカレントディレクトリに__pypackages__というディレクトリを作り仮想環境のように扱う方針に対応している*21。PEP 582はpythonlocflitpdmといったパッケージ管理ツールで採用されている新しい方針である。これによってユーザは、ほぼenvについて意識する事無く複数のPythonや開発環境を切り替える事ができるようになる。このようなPEP 582の採用も目新しいpyflowであるが、私自身はパッケージ管理ツールがPythonで書かれている理由はほぼないと考えており、依存解決やインストーラ含めて、より高速な言語で書かれて欲しいという気持ちもあり一目置いている。新興のツールという事もあり、現状では未対応のissueを見てpoetryと比較し利用していないが、挑戦的な選択肢として強く推せるツールとなる。

 
また他にも、pyproject.tomlを利用せずsetup.pyを含めて良い管理を目指すhatchや、オープンソースのソフトウェア構築ツールであるSConsに則り作成されているensconsRedHatが作成したpipenvとpoetryのいいとこ取りをしたminipipenv等も存在し、小さく機能を収める方向にいくか、理想の依存解決、複合ツールとして大きくなっていくか、どの仕様を採用するのか、違いが見え隠れする状態である。何度も言うが、これらの選択は、その場その場の要件次第でもあると思うので、利用用途の多くなったPythonという言語にとって、これらのツールが多く出てくる事自体は喜ばしい事である*22

 

まとめ

ここで一度話をまとめる。冒頭で示した主張には以下のような詳細に分かれる。

  • pipenvはPyPAが開発しているツールで同じくPyPAが開発するpipの機能を補っている
    • pipenvは依存解決器の機能を持つが、pipにもその機能が搭載されつつある
    • pipenvはvertualenvやwheel、twine含めてPyPAが作るエコシステムの一部である
    • pipenvはresolverとしてpip-tools内のbacktrackingを利用している
  • pipenvとpoetryの大きな"機能"の違いはパッケージのbuild・publishをサポートしているかどうか
    • poetryは基本的な機能はpipenvと同じ
    • poetry独自の機能はpyprojcet.tomlの拡張セクションによるもの
    • pyproject.tomlには各種設定を詰め込む事ができる
    • poetryはresolverとしてはpipenvと違いPubGrubを利用している
  • pipenv、poetry以外に他にも多くのパッケージ管理ツールが存在する
    • pip-tools、Anaconda 、flit辺りが人気
    • envやパッケージングなどの機能をどこまでをサポートするかの違い
    • package registryの違い、pyprojcet.toml(PEP 518)やPEP 582の採用の違い、リゾルバの違い


ここで、パッケージング管理を取り巻く重要なPEPを以下にまとめておく。

  • PEP 453 : pipをPythonが公式サポート
  • PEP 518PEP 517 : pyproject.tomlおよびbuildシステムの制定
  • PEP 582 : __pypackages__を利用した仮想開発環境の提案


PyPAが作っているか否か、小さく機能を収める方向にいくか、理想の依存解決、複合ツールとして大きくなっていくか、どの仕様、PEP、どのリゾルバを採用するのに各ツールの違いがある。

  

- pipenvとpoetryの技術的・歴史的背景 -

本章では、ネット上に多くあるpipenvとpoetryに関する議論で出てきがちな疑問、不満を以下の通りに分類し、それらに対してそれぞれ背景を追う形で紐解く。

  • パッケージングについて
  • lock生成までの速度
  • pipenvは開発がここ2年滞っていた件
  • pipenvのissue対応
  • PyPAの開発フロー
  • 活動や対応に関するゴシップ

上記を追う中で、poetryがどのような技術を採用し、pipenvが何故そうしないのか、逆にpipenvのどこが良いのかを詳しく見る。

パッケージングについて

poetryはpipenvと違い、pyproject.tomlを利用する事で、指定のbuild systemないし、独自のセクションを利用して、pyproject.tomlのみでパッケージング出来るという話がある。pipenvとpoetryの機能の違いで最もよく挙げられる点である。
「pipenvもpyproject.toml採用して」という意見も多い。

 

自系列を追うと、PEP 517が2017年9月、pipenv 1stリリースは2017年1月であり、前後関係としてPyPAがPipfileについて検討している間はpyproject.tomlのPEPのacceptに至っていない事がわかる。

また、pyproject.tomlを使えば全てのパターンのbuildが行えるかと言われると、元々setup.pyを書いていた事を考えれば当然そんな事はない。setup.pyでC言語で書いたサブモジュールをbuildしたり、外部のライブラリとリンクする実装などが代表的である。PyPAとしては、そういった問題の解決のためにwheelsetuptoolsも作っている訳なので、役割を考えればsetup.pyやrequirements.txtが担っていた役割とpipenvが持つ依存解決、バージョン固定、env管理という問題は別であるという認識になるのは自然である。

実際、poetryはそういったユースケースに対してbuildを提供する方法として、build = "build.py"のような機能を一応持っている。ただ以下issueの通り、ドキュメントには未だ記載はなく機能として安定、サポートされている状態ではない。
github.com
build scriptを柔軟に書けるというのは、wrapperとしての立ち位置を取る事が多いPythonという言語において非常に重要であるし、tomlだけで管理できるものはないだろう。またbuild scriptの実行もPyPAが長年かけてPEPで制定、整備、開発してきた部分であるので、それらへの対応を新しい開発者やツールが対応するのは相当な熱量がないと難しい部分もある。

pyproject.tomlに集約されたり、パッケージングまで行える事で差別化が図られる一方で、こういった多くのbuildにどこまで対応するかがツールによって変わってくるよという話になる*23

 

lock生成までの速度

「pipenv lockが遅い」「poetry lockが早い」というのは、よく挙げられる話題の1つである。

lockファイル生成速度に関してpipenvのissueが乱立していたり、Slack、DiscodeSNS各所で議論されている。
#356#1785#1886#1896#1914#2284#2873#3827#4260#4430#4457、…


まず、先にpipenvとpoetryの依存解決リゾルバのアルゴリズムの違いがある事を挙げた。そうすると「pipenvもpoetryの実装を真似すれば?」「pipenvもpoetryのようにPubGrubをベースとした依存解決を行えば良いのではないか?」という意見を思い付くのは自然だろう。

この意見を述べる前には、PubGrubアルゴリズムの特性と、その裏側で利用するhash値の生成方法の違いを知る必要がある。

 
一般的に依存解決を行いhash値によって利用モジュールのバージョンを固定する方法として、package registryがパッケージに対して返すhashを利用する方法がある。Node.jsのnpmやrubyrubygemsも同様の方法を取っている。Pythonにおけるpackage registryはPyPIになるが、PyPIAPIjson-apiへ移行したこと、またその内容にセキュリティ上の懸念がある事が、pipenvとpoetryの依存解決方法のアルゴリズム、そして実装の違いに影響している。
 

まず前提として、PyPIは2017年12月にそれまでのHTMLを利用したAPIエンドポイントをLegacy APIとし、json-apiの提供を開始した。これは、他のpackage registry同様の内容で、特定パッケージのhash値を含めて返してくれるというものである。
warehouse.pypa.io
json-api提供開始時期の問題もあって、poetryははじめからこのjson-apiが返す結果の多くを信頼し、依存解決を行っている。一方pipenvはこのAPIの結果を使わず自前でhash値を計算している。ここがpipenvとpoetryの根本的なlockファイル生成速度の差に繋がっていると言える。


さて、このjson-apiはpipenv開発開始以降に提供開始されたものではあるものの、pipenvに対しても「このjson-apiの結果を信頼し依存解決を行い、バージョンをlockするように変更すれば良い」という意見に繋がる。これにもいくつかの問題がある。

json-apiの1つ目の問題として、このjson-apiがPEPによって標準化されていないという点がある。先程のjson-apiのドキュメントからLegacy APIのページに行くと以下のような文言がある

The Simple API implements the HTML-based package index API as specified in PEP 503.

Legacy APIについてはPEP 503、その内容については、メタデータのバージョン2.0にあたるPEP 426に書かれており、メタデータjson互換オブジェクトを定義した2.1にあたるPEP 566もあるが、json-apiについて記載されたPEPは現状存在しない。PyPAとしてpipenvにこのAPIの結果を利用できない1つ目の理由になる。一方この問題は既にdiscuss.python.orgで起案されているので、こちらをウォッチすると良いだろう。
discuss.python.org


json-apiの2つ目の問題として、PyPI(正確にはhttps://pypi.org/)のartifact hashの一部が一度インストールしてみないと分からないという点がある。PyPIは、一度アップロードしたバージョンを同じバージョンで上書きする事はもちろんできない。一方、build済みのパッケージをtar.gzにdumpしたwheel形式以外に、buildスクリプト含めたsdist形式をアップロードできる。後者は、そこに正しくメタデータが設定されていない限り、特定バージョンのartifact hash自体は一度インストールしてsetup.pyを動かすまで未知になる。この未知のhashが厄介者になる。

PyPIは、一時的なhashを返してはくれるので、全幅の信頼を置いて依存解決の情報として使う事もできるし、全く信頼せず依存解決に必要な各バージョンのbuildスクリプトをダウンロードして手元でbuildしてhash値を計算するという事もできる。poetryは一部をjson-apiとし一部を手元でbuildする方針を取っており、pipenvは先述のPEP問題を含めて後者のように完全に手元で全てのパッケージをbuildする方針になっていた。これだけでも、pipenvが遅い理由が容易に想像付くはずだ。

 

この違いはあるものの、pipenvは、buildしたパッケージを出来る限りキャッシュして高速化する改修を過去何度も行っているだけでなく、2020年5月のReleaseではJSON-APIではなくURLフラグメントからhashを計算する形で依存解決の高速化を行っている。
github.com
これでかなり早くなった事が実感できるはずである。他にも #2618#1816#1785 #2075辺りの議論を追うと良い。

そしてもちろん、poetryも一部のパッケージを手元でbuildしている訳なので、そのパッケージを挙げて「lockが遅い」と指摘するissueが存在するし、poetryのドキュメントにも記載がある。
github.com
python-poetry.org
要は、実装の問題というより、思想としてartifact hashをどう考えるか、どう処理するのかの違いにlockファイル生成の速度の差があると言える。

そして先に出てきた、依存解決リゾルバのアルゴリズムの違いも、この問題に起因している。手元で多くのパッケージをbuildする方針の場合、PubGrubが、依存解決を行いながら、その場に応じてパッケージをbuildする機能を持つ必要がある。PubGrubは、そのアルゴリズムの特性上、途中でbuild処理をしながらその結果を比較するロジックを実装する事に向いていない。これについては、先程のpipの2020-resolverの議論でもpoetryの作者が指摘しており、poetryの作者がMixologyとして依存解決リゾルバのロジックをpoetryから切り出したにも関わらず、2020-resolverのロジックとして採用されなかったという背景にもつながっている。ここで、適切にbuildしながら依存解決を行うPythonによるPubGrub実装があれば話が進む可能性はもちろんあり、pipgrip等のOSS実装が出始めてはいるといった状況である。

 
さて、この非決定的なartifact hashについては、PyPIメンテナが書いた以下のブログに理由を含めて記載されている。
dustingram.com
より過去のhash自体の議論については以下を追うと良い。

 
この問題が完全に解消されるには、「下位互換を完全に切ったPyPIのミラーが作られる」だとか「setup.pyを捨て全てがpyproject.tomlになる良い方法が提案される」だとか「PyPIサーバ内でbuildが走る」だとか複数の道があるわけだが、歴史の長いPythonパッケージングの問題やbuildスクリプトが走る事でのセキュリティの懸念、サーバの金銭面などを全て解決されるには時間がかかるのは当然と言える。

 

pipenvは開発がここ2年滞っていた件

pipenvには「開発が滞っている」「2年更新のない死んだプロジェクト」などの指摘が多くある。実際、2018年11月(v2018.11.26)から2020年5月までReleaseがなかった。2020年5月のReleaseも、当初予定していた2020年3月から4月21日に延期され、4月29日に延期、その後5月にReleaseされたという背景があり、この辺りを挙げ開発が滞っているとする指摘も多い。

これらは、2020年のRelease Noteやtracking issueを見るのが良い。
discuss.python.org
tracking issue: https://github.com/pypa/pipenv/issues/3369

ここまで示した通り、pipenvはPyPAが開発する多くの他プロジェクト、pipやvirtualenv、setuptoolsに強く依存しているし、その意思決定の多くがPEPという大きなプロセスを通して行われている。空白の2年間の如くpipenvが止まっていたように見えるだけで、関連度が高く、依存した多くのプロジェクトの改修を行っていた訳で、開発が滞っているという指摘自体が間違っていると言える。

なによりPyPAはNumFOCUSといった支援団体があるとはいえ全員ボランティアであるし、Release Noteを書いているDan Ryanは仕事をしながら20%ルールの範囲で開発を行っていると書いてあるしで、外から多くを求めるだけというのは間違いだろう。またこのRelease Noteには「Other changes in the project」という章がある。それはProcess changes、Communications changes、Release cadence & financial supportの3つに分かれており、PyPIへの貢献のプロセス、コミュニケーションを変える事を約束し、別の形で「パッケージングエコシステムに対して」貢献できる形を提供すると発表している。

議論やcommitmentに至る程の背景知識がないが、このパッケージング問題に金銭面で貢献したいといった場合は、以下を読みdonate.pypi.orgに行くと良いだろう。
pyfound.blogspot.com

 

pipenvのissue対応

poetryが個人開発から始まっているのに対して、pipenvがPyPA主導である事から、issueへの対応の違いが指摘される事がある。「pipenvはissue対応が遅い」といった意見だ。これは、PyPAが元々issueを多く使っていない事に起因しているが、pipenvでたまに返信があるissueもあったり、ツールも分散しているので一見ではissue利用の思想について把握しづらいとも言える。

さて、先にpipの2020-resolverの問題の相談先として以下を挙げた。

これに加えてパッケージングに関しては、以下のような場所の議論を追う必要がある。


他PyPA関連のプロジェクトの多くの議論場所は以下のページにまとめられており、Slackからメーリングリスト、freenodeをチェックする事が求められる。
packaging.python.org

一見混乱するかもしれないが、開発者からしてみれば、接触確認アプリCOCOAがissueを利用していないにも関わらず未対応であった事を指摘していた人と何ら変わらないので、正しく議論したい場合は念入りに読み込んで、適切な場所に意見を投稿すべき、という話になる。

 

PyPAの開発フロー

PyPAの開発フローしんどい問題も少しだけある。もちろん影響範囲が大きいし、PEPや歴史的背景を持つ大きなプロダクトなので当然ではある。

例えば、実際「pipenvに一度貢献したがもうやりたくない」といった意見もある。
github.com
フローと歴史的背景を多く要求されるだけでなく、交渉の末MRを出すもcloseになる事がある。実際に私も過去python devのSlackでパッケージング管理問題に触れた時、多くの回答とフローを用意され尻込みしてしまった経験を持っている。

 
また、PyPAのツールにおいて少し特殊なのは、Vendoringという開発手法を用いている事が挙げられる。
これはRustやGo等ではgit submoduleのような形でよく使われる手法で、利用するモジュールを依存関係として取るのではなく、同一のRepository内に含めてpatchを当てて開発していく手法である。日本語であれば以下が詳しい。
qiita.com

pipenvのrepogitoryの以下ディレクトリを見ると、vendoringされたツールが多くあるのが分かる。
https://github.com/pypa/pipenv/tree/master/pipenv/vendor
これは、LICENSEやネストといった既知の問題こそあるものの、PyPAが作るツールのような「ある依存モジュールが突然PyPIから消えて動かなくなったら困る」といった再現性が必要な場面で有用な手法である。

一方で、ことPythonにおいてはvendoringに関連した文化やツールがそもそもないので、pipenvにその問題を指摘するissueが立ったり、vendor先のツールに「このrepoメンテする意味ある?」といったissueが立ったりしている。
github.com
github.com
当然upstreamとして機能し取り込まれる可能性があるので意味はあるのだが、Pythonという言語内でのvendoringの文化として浸透していない証拠とも言える。


ただ、開発フローに関しては、個々時々に応じて対処していけば良い問題であって本質的ではない。貢献する気概を持って貢献するだけだろう。そして何より、先述の2020年5月のRelease Noteを使って体制を改善するとまで言っていることからも、PyPAが真摯にこの課題に向き合っているのが分かるので、期待する所でもある。

 

活動や対応に関するゴシップ

issue対応や開発フローも技術的な背景から遠いが、pipenvやpoetryの議論の歴史も存在しており、それらを知る事で適切にコミュニティとコミュニケーションが取れると思うので、短くまとめておく。

昔々、当然PyPAメンバーはpipenvを推していて、メインメンテナであったKenneth ReitzがPyCon2018で発表するなどしていた。一時期pipenvのドキュメントのトップには以下のような文言が存在した。

the officially recommended Python packaging tool from Python.org, free (as in freedom)

冒頭のtheを含めて直訳すると「Python.orgが公式に推奨する唯一のPython パッケージングツール」というニュアンスになる。「PyPAは公式に推奨されているのか」「唯一のツールなのか」等、文言やその他の活動に対して揚げ足を取るような形で、機能や対応に不満を持っていたユーザの意見が噴出した。十分でない機能をREADMEで揶揄するプロジェクトが出たり、ブログやSNSでも話題になっていた

以下のようなタイトルのissueもあり、コメントやリアクションを見るになかなか殺伐としているのが伺える。
If this project is dead, just tell us · Issue #4058 · pypa/pipenv · GitHub

そんな最中、作者のKenneth Reitzが「Python.orgが公式に推奨する唯一のPython パッケージングツール」の文言をmaster commitで削除したりした*24事や、攻撃的な意見が辛く精神的に病を抱えている事を告白する記事を公開した。

そしてredditやissueで、全ての不満と共にKenneth Reitzのパフォーマンス説を唱える人が出たり、そういった人達と本当に技術について議論したい人が入り混じり炎上、PyPAメンバーやPythonメンテナが対応する事になった。

これらについてはPyPA、Pythonそれぞれから回答があり、以下を参照すると良い。
github.com
github.com


誤解を招かないため、更に書くと、pipenvやpoetryの不仲などと言った事もなく、上記redditスレ内でもpoetry作者とも話した上で設計思想の違いを認識している事が書かれているし、pipの2020-resolverを決定する際には両メンテナ、Pythonメンテナがそれぞれのリゾルバについてや依存解決のコツを投稿しているので、決して悪い方向に進んでいるような話ではない。
実際、現在のPythonのパッケージングに関するチュートリアルには、pipenvやAnaconda 、poetryを適切に選定する事が良いとされている。
packaging.python.org
技術的には本質ではないものの、知っておく事で、コミュニティがこういった攻撃的な投稿に関する話題に敏感である事を鑑みながら、丁寧に投稿できるようになればと思う程度の話である。

 

まとめ

本章のpipenvとpoetryの違いについて、以下にまとめる。

  • build/publishの機能の違いはpyproject.toml誕生の前後関係とPyPAのエコシステムの思想からくるもの
  • lockファイル生成速度は、PyPIjson-apiの結果をどこまで信頼し、どこまでパッケージを自分でbuildするかに関係
    • 依存解決リゾルバの違いもここに縛られている側面がある
    • 解決するにはいくつかの方法こそあるが、下位互換やセキュリティ、金銭的な側面の課題もある
  • Pipenvはissueではなくメーリングリストを利用して開発が進んでいる
    • 徐々にissueでの議論も進んでおり開発フローの改善も進む
    • poetryの方が開発体制が良いという訳では必ずしもない
  • それぞれゴシップ的な炎上を経験しておりディスカッションに参加する場合は丁寧な理解をしておくと良い

念の為フェアに補足しておくと、pipenvには、PyPIにアップロードされたパッケージが変更されていないか等の脆弱性を確認するPEP 508に則ったpipenv checkの機能もある*25。ある意味特徴的な機能の1つだ。
pipenv.pypa.io
これらを加味しながら、適切にツールを選ぶ事が良いと考えられる。

 

- poetry大臣としての活動記録 -

ここまで、Pythonのパッケージング管理ツールについて取り扱ってきた。そして、私が所属する企業、チームでも、実際にpipenvによるパッケージ管理とデプロイ、CI/CDが使われていた。しかし、運用する中で前述のようなパッケージング管理ツールの違いを踏まえて、社内の課題と見比べ、poetryへの移行を行う形となった。本章は、その移行作業の目的や判断基準、感想を綴るものである。

重要な分岐点になった、以下の3つのセクションについて書いていく。

  • VPN必須なpipenv lockがかなり遅かった
  • 乱立するファイルと管理場所
  • gokartなどOSSとの管理方法統一

 

VPN必須なpipenv lockがかなり遅かった

私が所属する企業では、社内のPyPIを通してpipによるインストールを行っている。そして、社内PyPIへの接続にはVPNが必須であった。

ちょうどコロナで全社でのリモート化が進み、在宅勤務が増えた事でVPNの負荷が増大、それに伴ってpipenv lockに膨大な時間がかかるようになっていた。基本的に1時間以上は基本、大きなrepoでは2,3時間を要して、途中で接続がタイムアウトしてしまうような事もあった。実際、深夜の障害対応中に「hot fixでバージョン戻してReleaseしましょう」「では、lockします」「・・・(終わりは3時だな)」といった会話が発生するなど、本当にあった怖い話もいくつか体験しており、流石にlockファイル生成速度の問題を看過できなくなっていた。


先述した通り、pipenvの設計思想の課題の1つである、has値確認のためのbuild実装に起因するもので、根本的な解決が難しいことも考慮し、別ツールへの移行を考える必要があった。検討したのは、pip-tools、poetry、flitであった。中でもpip-toolsは強く検討したが、各々がenvを設定するコストやpyproject.tomlが今後主流になるだろうという憶測から、poetryとflitに絞って検証を行った。

移行当初、flitが抱えていた課題として、パッケージのバージョニング機能が不十分であるという課題が存在した。具体的には、gitタグやVERSION.txtを通してパッケージのバージョンを決める方法の有無で、これによってCIによるバージョンやpublishを行う所作が、所属チーム内では広く行われていた。

poetryには、poetry-dynamc-versioningという拡張が存在し、こちらを利用する事で、上記問題が解決できる事が分かったため、以降先としてpoetryを利用していく運びとなった。
github.com

 

乱立するファイルと管理場所

所属チームでは、基本1人1プロジェクトで開発を進めており、そのプロジェクト数は大小や稼働率、廃止撤退様々あれど36個にも登る*26

私が入社当初、プロジェクトテンプレート等はなく、各々自らが知るツールを使ってRepositoryを作っていた。その後、私がcookiecutterによるプロジェクトテンプレートを作ったものの、そういうった背景を鑑みないrepoは増え、プロジェクト管理は以下のようなファイルに分散していた。

  • requirements.txt
  • setup.cfg
  • setup.py
  • VERSION.txt
  • Pipfile
  • Pipfile.lock
  • pip.conf
  • code_analysis.conf
  • yapf.ini
  • MANIFEST.in

中にはバージョン情報がrequirements.txtとPipfileに分かれて書かれており、初見ではどこで指定されているモジュールがproductionで使われているのか分からないようなプロジェクトも存在した。poetryとpoetry-dynamc-versioning、toxというテストを統一的に管理するツールを導入する事で、以下の3つに全てのファイルを集約できる事が大きな後押しになった。

  • pyproject.toml
  • poetry.lock
  • tox.ini

github.com

これは、最近Preferred Networks社のブログでも語られたような内容なので、普遍的に多くの企業で課題になっているだろう。
tech.preferred.jp
(PFNはそういった課題に対してpysenのようなツールを公開するにまで至っているのだから流石である)

私の所属企業では、poetry、poetry-dynamc-versioning、toxを利用する事によって既存のメンバーだけでなく、新しいメンバーに対しても混乱を産まず、いくつかの設定の場所と使い方を覚えるだけで開発に専念できるようになり、多くの問題を解消することができた。企業によっては、インフラやCI、要件の関係で他ツールを選ぶこともあるだろうとは感じるが、横串でツールの要件が決まっている事での開発速度の差は何より大きい。

 

gokartなどOSSとの管理方法統一

私の所属企業として内部で使われるツールを外向けのOSSとして公開しているが、チームメンバー全員が使うにも関わらずメンテナが一部のメンバーに偏ってしまっていた課題があった。様々な理由こそあったが、日頃使っているツールとの差異が大きく、パッケージングやフォーマッタのツールに社内との微妙な違いがあり、またbuildやpublishをメンバー個人ができない状況で、新規に開発に参入しづらいという課題が大きく存在した。

そういった背景から、社内でpoetryを推進した後、外部のOSSをpoetryに移行する運びで解消する事にした。
github.com

 

活動全体の結果

先述のような前提を踏まえ、私が2020年4月にpoetry移行をスタート。

f:id:vaaaaaanquish:20210326164741p:plain
2020年4月に自己宣言

1年かけて、コツコツと様々なプロジェクトにPull requestを出し、先日稼働中のプロジェクト全てがpoetryに移行した形になった。

f:id:vaaaaaanquish:20210329002427p:plain
ROI勘定


Pipfileからpyproject.tomlの移行は、toml形式を互いに採用しているところからもほぼ困難なく移行する事ができた。

移行時は、pyproject.tomlがデファクトスタンダードにならなかった場合にpyproject.tomlに多くの設定が集約される事でロックインされてしまうのではという懸念を持っていたが、現状多くのフォーマッタやlinter、testツールがpyproject.tomlをサポートする流れになっており、良い選択だったと言える。

poetry-dynamic-versioningを利用した、pythonパッケージ用のrepoの設定の仕方の記事を書くなどした。
vaaaaaanquish.hatenablog.com

また、同僚の @SassaHero がyapfをpyproject.tomlの対応をするなどした。

チーム管理のOSSであるgokartには、poetryやtoxが導入、かなり少ないツールと設定ファイルで多くの処理が行えるようになった。結果社内外からのコミッターも増えて万歳という結果になった。

 

まとめ

pipenvからpoetryへの移行を行った。要点としては以下のようになる。

  • 社のVPN起因でpipenvのlock速度に課題があり技術的解決も困難であったため乗り換えを決意した
  • 乗り換え先の選定においてはversioning等の既存のCI、OSSの移植の簡易さを考慮した
  • 数十のプロジェクトで1年単位での移行となった
  • 結果として課題が解決するだけでなくpyproject.tomlへの情報集約、OSSの盛り上がり等の副次的効果も得られた

poetry移行により、この先少なくとも5年は持つ開発環境が作れただろう。何よりlockやinstall速度が改善した事が成功の証と言える。

しかし、再三記述した通り、あくまでパッケージツールであって、かつ安定した状態とも言えないため、今後どうなるかは定かではない。移行作業も多くの時間がかかるし、新しいツールも続々出てくる。ベストプラクティスといった言葉に惑わされず、より丁寧な技術選定の上で今の自分たちにあった選択が出来るようにすべき、ということがよく分かる。
 
 

- おわりに -

本記事では、pip、pipenv、poetryという3つのパッケージ管理ツールについて、技術的、歴史的背景をまとめ、それらを元にpoetry移行を行った経験を綴った。

そもそもパッケージ管理が最初から全て上手くいっているプログラミング言語などない。フロントエンド系だって、様々な設定ファイルやメタプログラミングを通してpackage.jsonやtsconfig.jsonに行き着いているし、JVM系のようにhash照合の概念がなくpackage registryをSpringが立てているようなところもある。package registryを捨て、gitのコードベースに依存解決を行っている言語もあれば、RustのCargoを見れば、fmtからtest、doctest、build、パッケージ依存解決、パッケージングを行えるたった1つのツールをメンテしている。それぞれ見れば、それぞれに課題がある。

各ツールを組み合わせ、自分の最強のbuild環境を作る上では、ある意味Pythonの体制は良く出来ていて、動的言語との相性も良い。一方で、出てくる不満についてもよく分かる。

 
私個人の本音を言えば、PythonにもCargoくらい全ての事が詰まったツールは欲しい。


特にpyproject.tomlベースでPEP 582も扱いたいし、できればtestやlinterも1つのツールに収まっていて、スクリプトで管理するのはごく一部でロックインされず気軽に乗り換えられるのが嬉しいが、そんなモンスターのようなツールのメンテは想像するだけで骨が折れる。PyPIjson-apiがPEPを通るか、Anacondaのようにpackage registoryを構える偉大な団体が出てくるかといった依存解決についての解消も必要だろう。

一方で、これは作ってない人のお気持ちであって、pyproject.tomlとbuild script以外にPythonを書くツールも出てきており、pyproject.toml以外の選択肢がパッケージングという観点以外のlinterやapi config、その他設定を起因に出てきた時移行できるよう、ロックインを避けるようにしていくのがベターだろう。

PythonC/C++のwrapperとして多くの機能を持っている事も加味すれば、今後もbuild scriptは必須になるだろうし、元来、パッケージ管理ツールといった代物はプロダクトコードとは別のレイヤーに居るのだから、あとはプロジェクトのサイズ感や運用状況と相談し、ベストプラクティス等と言わず、そういったツールに固執せず、粛々と運用し、適切に時期をみてツールを変更していくだけという話である。

 
何より私はPyPAのコアメンバーでもなければ、パッケージ管理ツールの主要なコミッターでもないので、大きく発言する資格はないし、donate.pypi.orgNumFORCUSを支援するか、issueやcommitやコメントを重ねるか、自分で理想のパッケージを作るかをやっていかないといけない。それがソフトウェアエンジニアとしての常である。頑張りましょう。

 
かなり長くなってしまったので、整合性が取れていなかったり誤認に基づく間違いがあるかもしれない。
できれば、Twitterはてブに書いて頂ければ幸いです。
ちゃんと議論したい場合は、文中の通り適切な場所にどうぞ。


// -- 2021/03/29 追記 --

GitHubがpackage registoryに成り得るサービスを展開していて、Pythonもfuture workに入っている。ここにindexされ、常用が始まるのを機に後方互換が一気に変わって、依存解決が超簡単になって、管理もGitHubないしMSがやってくれるというコースもワンチャンあると思っている。その場合、ユーザが利用するツールがどう転ぶかは未知数だ。などとGitHub Packagesが出てから思い続けて、future workになって2年経つので、いつになるかはよくわからない。一応future workになった時点で2021年の3月以降とされているから、今後なにかある可能性は高い。

 

- 参考文献 -


脚注や文章内リンク外で参考にしたもの・本記事を理解する上で読むと良いもの

f:id:vaaaaaanquish:20210329011401p:plain:w0

*1:サードパーティPythonモジュールがアップロードされているリポジトリ

*2:統一的でない事に対する悪い意味合いではなく目的に応じてツールが分離されたエコシステムであるという意

*3:pip install -r pyproject.toml出来ないよねといった議論はあるがsetuptoolsと共に徐々に対応されていくだろう https://github.com/pypa/pip/issues/8049

*4:PyPAが出すチュートリアルにはsetup.pyやsetup.cfgも適切に使えとは書いてある https://packaging.python.org/tutorials/packaging-projects

*5:何かを入れた後にサブモジュールをインストールしなおすと治るといった現象にPythonユーザであれば当たった事があるはず

*6:充足可能性問題 - Wikipedia

*7:バックトラッキング - Wikipedia

*8:私は高専時代Knuth先生のThe Art of Computer Programmingで挫折したことがあり教科書的と言える知識量ではないかもしれない…

*9:Dart開発者向けのドキュメントはここ pub/solver.md at master · dart-lang/pub · GitHub

*10:のように私は見える

*11:Unit propagation - Wikipedia

*12:poetryが採用している

*13:Chan Zuckerberg Initiative - マーク・ザッカーバーグと妻のPriscillaChanが持つ投資会社

*14:言わずもがなfirefox作ってる会社

*15:今はまだ--use-deprecated=legacy-resolverで古いリゾルバを利用できる

*16:自系列的にはpipenvの方が先に依存解決をはじめている

*17:主たる形であってPyPAは他の方法を否定はしていない

*18:Anaconda Cloudやその他のリポジトリが存在するがここでは総称する

*19:より具体的にはwheelを選んでくれる

*20:もちろんenv毎に容量が節約される等のメリットもある

*21:poetryでも議論されておりメンテナも好意的 PEP 582 support · Issue #3691 · python-poetry/poetry · GitHub

*22:乱立しているだろうという観点はさておき

*23:私も手前xonshのxontrib等はpoetry化できないし、逆にsetup.pyのメンテを引き剥がす作業も行っている

*24:「他に適切な表現があるため」としており間違った行為ではない

*25:依存解決とは別の意味でセキュリティ関連の解説が必要なため、どうしても誤解を招かず書くことが難しかったので後述する形となった

*26:A~Zで偉人の名前でプロジェクトを管理していたが、今は一周し花の名前で2週目になっている

機械学習パイプライン構築を楽にするgokart-pipelinerを作った

- はじめに -

luigi、gokartで作ったtaskのパイプライン構築をちょっと楽にする(かもしれない)管理するためのツールを作った。

github.com

近年、MLOpsの一部である機械学習のためのパイプラインを構築するためのツールは飽和状態にあるけどそれらと比較してどうなのという話も書く。

gokart-pipelinerを使ってみる

gokartはエムスリー株式会社が開発している機械学習パイプラインOSSである。gokart自体、使った事がないし興味もないという人も居るかもしれないが、一度以下に提示するgokart-pipelinerの例を見てほしい。

# pip install gokart_pipeliner
import luigi
import gokart
from gokart_pipeliner import GokartPipeliner

# taskを定義する
class TaskA(gokart.TaskOnKart):
    def run(self):
        self.dump(['foo'])

class TaskB(gokart.TaskOnKart):
    before_task = gokart.TaskInstanceParameter()
    text = luigi.Parameter()

    def run(self):
        x = self.load('before_task')
        self.dump(x + [self.text])

# パラメータとパイプラインを書く
params = {'TaskB': {'text': 'bar'}}
pipeline = [TaskA, TaskB]

# run
gp = GokartPipeliner()
gp.run(pipeline, params)

luigiやgokartでは、1つのタスクを1classで表現する。TaskAは ["foo"] を保存するだけのタスク。TaskBはparameterに設定したbefore_taskを読み込んで、同じくparameterに設定した text を末尾に追加するタスク。GokartPipelinerがその2つのタスクをよしなに接続し、パラメータと共に実行している。

gokartの良さ

手前味噌にgokartの宣伝をするならば、この時以下のようなメリットがある

  • それぞれのTaskの以下データが別々にハッシュ値付きでpklファイルに保存される
    • self.dumpしたデータ
    • importした全てのmoduleのversion
    • Taskの処理時間
    • Task内で使われた全てのrandom_seed
    • 出力されるログ
    • Taskのクラス変数として設定された全てのparameterの値
  • TaskBのparameterを変えて実行した時は別のハッシュ値で上記ファイルが生成される
  • TaskAに新たにparameterを追加した場合はTaskBのハッシュ値も変わり依存関係を考慮して両方rerunされる
  • TaskA、TaskB間は上記の出力を中間ファイルとしてやり取りされるためメモリに優しい
  • dumpの出力ファイル形式を拡張できる(デフォルトでもcsv、zip、feather、png、…などをサポート)
  • 入出力時のpandas.DataFrameの型、columnチェック機能がある
  • 保存ファイルのディレクトリ構成がPythonスクリプトの構成から自動的に決まる
  • 基本的なnumpy、randomのシードは自動で固定化される
  • SOLID原則をなるべく守りながらコーディングできる
  • 保存された出力データとparameter、hashの管理はthunderboltなる別ライブラリでPython上で行える
  • 並列にタスクが動作してもredis経由でTaskがロックされる

機械学習モデリングにおいて、何をロードして、何を出力し、どのように繋げるか以外の殆どを自動的に決定し保存する仕組みになっている。また、その上でクラス単位でTaskを作る事によるソフトウェア開発における単一責任の原則などを守りやすくなっている。

近年は、機械学習パイプラインツールの戦国時代でもある。他の多種多様なライブラリと比較しても、モデリング時やproductionの再現性のための中間出力が多いし、Pythonコード上で見た時、デコレータや謎のメソッドがチェーンされまくったコードよりは保守性が高くなるはずだ(gokartを学ぶコストさえ払えば)。

例えば「pandas.DataFrameのtext columnから文字列の長さのcolumnを生成する」という処理は、無闇に大きな関数やスクリプトにせず、1つのファイルに1つのTaskとして考えて以下のように書いていく。

class CalcTextLengthTask(gokart.TaskOnKart):
    target = gokart.TaskInstanceParameter()
    __version = luigi.IntParameter(default=1)

    def run(self):
        df = self.load_data_frame('target', required_columns={'id', 'text'}, drop_columns=True)
        df['text_length'] = df['text'].str.len()
        self.dump(df[['id', 'text_length']])

gokartにおいては、1Taskの規模感さえ一致すれば、この書き方以外でコードを書くのは難しく、コードレビューや保守が行いやすい。また、このCalcTextLengthTaskの入力となるtarget taskを変える事で、hash値等のメリットを享受しながら使い回す事ができる。例えば機械学習モデルの汎用的なTrainタスクを書いておいてTaskInstanceParameterのみ変えるといった具合に。

また、parameterで中間ファイルのhash値が変わる事を利用して、__versionのようなパラメータを雑に付けてあげれば、「長さを測る前にstripしてからという処理に変更」した時にversion=2としてcommitしておくことで、あとからgitのlogをblameしたり、出力されるhash値付きのpklファイルを見比べる事でデバッグが行いやすくなる。


加えて、wrapしているluigiとの比較は以下のスライドを参考に。
gokartの運用と課題について - Speaker Deck
何もないluigiを書くよりも書きやすいと思えるはずである。

 

gokart-pipelinerの意義

gokartでモデリングしたり、コンペに出たり、会社での本番運用を重ねていく中でいくつか課題になってきた以下のような点を解決しようとしたのがgokart-pipelinerである。

  • パラメータとパイプラインが密結合しすぎ
  • パイプラインライブラリなのにやればやるほどrequiresメソッドが複雑になる
  • jupyter notebookと行き来するのがダルい
パラメータとパイプラインが密結合しすぎ

パラメータとTaskの動作が分離しているパイプラインは、近年の流行となりつつある。

特にFacebook社の公開したHydraはかなり大きな転機だったように感じる。yamlとデコレータを軸としたパラメータ管理で、yamlファイルさえ管理していればどんなパイプラインを書いても良いし、かなり管理も楽である。

一方でデコレータは闇魔法を生みやすいし、デコレートした謎の巨大な関数を見るのはツライので、もう少しコーディングに制約を持たせた形のパイプラインを作りたいと思っていた。(他人の書いたHydra+mlflowのコード見るのしんどすぎない?)

luigiにもconfigParserを使ったiniやyamlファイルを読んでパラメータとする機能はある。しかし、gokartにもTaskInstanceParameterというやつが居る。これ自体はtaskを依存関係の一部と捉えられる良い機構ではあるものの、Parameterという扱いとしてyamlのように一箇所で管理できないネックがあった。

gokart-pipelinerの場合を見てみる。

from gokart_pipeliner import GokartPipeliner
from ExampleTasks import *

pipeline = [TaskA, {'task_b': TaskB, 'task_c': TaskC}, TaskD]
params = {'TaskA': {'param1':0.1, 'param2': 'sample'}, 'TaskD': {'param1': 'foo'}}

gp = GokartPipeliner()
gp.run(predict, params=params)

pipelineは「Task同士のTaskInstanceParameterによる依存関係のみい」を表し、paramsは「各タスクのそれぞれのluigi.Parameter」を表していて、切り分けられている。元々のluigiのconfig形式にも対応しているので、configファイル、pipeline、paramsをそれぞれ考えつつ、この構成だけ保存しておけば一元管理もできる。

パイプラインライブラリなのにやればやるほどrequiresメソッドが複雑になる

gokartの機能としてrequiresというクラスメソッドがある。これは、読み込むデータを指定するメソッドで、requiresが返す値でluigiが依存タスクを決めている。

先程のタスクをgokart-pipelinerを考えず書いた場合は以下のようになる

class CalcTextLengthTask(gokart.TaskOnKart):
    target = gokart.TaskInstanceParameter()

    def requires(self):
        return self.target

    def run(self):
        df = self.load_data_frame('target', required_columns={'id', 'text'}, drop_columns=True)
        df['text_length'] = df['text'].str.len()
        self.dump(df[['id', 'text_length']])

このrequiresは以下のようにlistやdictを返したりもできる。

    def requires(self):
        return {'target': self.target, 'model': self.clone(MakeModelTask)}

更に複雑に、依存関係やパラメータ、分岐を書く事もできる。

    def requires(self):
        data = TrainTestSplit(data=MakeData(path='/'), split)
        if self.parameter_a == 'var':
            task = MakeModelTask(data=data, param_a=0.1, param_b='foo')
        else:
            task= MakeModelTask(data=self.data)
        return {
            'data': data,
            'model': self.clone(task)}

この状態では、コンペのような実験とコーディングを繰り返す時に、依存関係がどうなってるか把握するのがどんどんしんどくなる。gokartに依存関係Treeを出力する機能があるが、流石にしんどい。こういった複雑なrequiresを集約するエンドポイントになるTaskを作ったりするが、次はそのエンドポイントからしか全体が実行できなくなったりしていくし、エンドポイントが増えれば増えるほど、どのファイルを見て回ればいいか分からなくなる。その上で途中にTaskを挿入したいとなったら、と考えるとただただ辛くなる。

なので、そもそもrequiresを書かない制約を付ければ良い。


例えば、gokart-pipelineでの先程の例を考える。

pipeline = [TaskA, {'task_b': TaskB, 'task_c': TaskC}, TaskD]
params = {'TaskA': {'param1':0.1, 'param2': 'sample'}, 'TaskD': {'param1': 'foo'}}

ここでTaskDは、リストの1つ前のdictを引数にするように、以下のように書いたクラスである。

class TaskD(gokart.TaskOnKart):
    task_b = gokart.TaskInstanceParameter()
    task_c = gokart.TaskInstanceParameter()
    param1 = luigi.Parameter()

    def run(self):
        b = self.load('task_b')    # list
        c = self.load('task_c')    # list
        data = b + c + [self.param1]
        self.dump(data)

requiresメソッドは、TaskInstanceParameterのパラメータ名から、gokart-pipelineが生成する。この制約によって複雑なrequiresを書かれる事もなく、pipelineのlistだけを変数やdictを使って上手く書いてやれば良いだけになる。

jupyter notebookと行き来するのがダルい

gokart自体をjupyter notebookで動かす方法はあるものの、かなりハックじみた方法となる*1

gokart-pipelinerはjupyter notebook上で動く。
gokart-pipeliner/Example.ipynb at main · vaaaaanquish/gokart-pipeliner · GitHub

Task同士は中間ファイルでやり取りされるのでメモリをバカ食いしないし、classである事さえ意識して一般的なソフトウェア開発の心得に沿って書けば、ここで書いたコードをそのままproductionコードにするのも容易になる。

もちろんタスクを動かした後、thunderboltを使って出力ファイルをメモリに読み込むなどして、jupyter上で触っても良い。
github.com

future work

今後上手く使えてきたらできそうなこと

  • pipelineのリストの書き方のベストプラクティスを探る
  • runの返り値として出力データを返したりできるようにする
  • pipelineを決めたら並列化できる所を自動で並列に動作させたりする
  • jupyter notebookからも別のプロセスとして動かす(Taskが動く最中もnotebookが実行できる)

おわりに

試しに作ってみた段階なので、これからコンペに出たり本番運用に使ってみたりして調整していきたい。

gokartは良いぞという話を散々書いたが、gokartは中間ファイルを経由するだけにDataFrameを良く使うテーブルデータでは使いやすく、画像コンペのような所では全然良さを発揮できなかったりする。いやいや画像音声テーブルなんでも自分のツールは共通化しておきたいわ、という人に不向きという所をなんとか改善できればと思ってはいる。

なんとなく形になったら、gokartにマージしてもらって、gokartの定番となっても良い気もする。

がんばろう。

 

*1:機械学習プロジェクト向けPipelineライブラリgokartを用いた開発と運用 - エムスリーテックブログ https://www.m3tech.blog/entry/2019/09/30/120229 を参照すると良い

「実用的でないPythonプログラミング」がよかった

はじめに

2020/8/12に発売されたImpractical Python Projects: Playful Programming Activities to Make You Smarterの日本語訳書である、「実用的でないPythonプログラミング」をひょんな事から献本していただく事になった。(訳者が同僚である)



ありがちなプログラミング初学者向けの本から1段上がった中級者向けの良い本だと感じたので、当ブログでたまにやっている筆者、訳者に媚びを売るシリーズの一貫として、感想を記す。


 

書籍の概要

実用的でないPythonプログラミング」は、想定する中級レベルのアルゴリズムの問題を例に取り、Pythonでの美しいコードの書き方や、コンピュータサイエンスにおける基礎知識、オブジェクト指向プログラミングの必要性を説いていく形で執筆されている。


以下は、章ごとに出される問題とそこでのキーワードをまとめてみたものだ。パッと見渡してもコンピュータサイエンスの基礎知識をなぞっているのが、よくわかる。

1.【乱数による名前生成】Pylint、DocString
2.【回文の検出】semordnilap、cProfile、ハッシュテーブル、再帰
3.【アナグラムを解く】Sort、collections、モジュールの分割、トライグラム、バイグラム
4.【暗号解読】ルート転置式暗号、レールフェンス暗号、総当たり攻撃と探索
5.【暗号解読と生成】null暗号、語彙のリストと安全性
6.【MS Wordの取扱】OSの差異、フォント、カーニング、トラッキングpython-docx、ヴィジュネル暗号
7. 【良いネズミの育成、金庫破り】組み合わせ最適化遺伝的アルゴリズム
8. 【俳句の音節の検出】自然言語処理コーパス、CMUdict、nltk
9.【俳句の生成】マルコフ連鎖デバッグ、スキャフォールド、logging、チューリングテスト
10.【銀河系のモデリングフェルミパラドックス多項式、ドレイクの方程式、グラフィカルモデル、ユークリッド距離、NumPy、SciPy、tkinter
11.【モンティ・ホール問題】モンテカルロシミュレーションオブジェクト指向プログラミング、tkinter
12.【老後資金シミュレータ作成】確率的サンプリング、モンテカルロシミュレーション実応用
13.【火山活動シミュレーション】pygameOpenGL、math、ラジアン、三角法、ベクトル
14.【万有引力ケプラーの法則のシミュレーション】複雑な問題のソースコードへの落とし込み、ゲーム作成
15.【天体画像の精細化】画像処理、画像補正、スタッキング、pillow
16.【不正検知】ベンフォードの法則、matplotlib

 
実際に上記のような問題に当たる上でも、ユニークな問題の与えられ方をする書籍でもある。

例えば、暗号の章では実際の南北戦争時代などのエピソードを例題として扱っていたり、回文ではハリーポッターを引用している(酷いネタバレでもある)。モンティ・ホールゲームの図解なども、複雑な問題を受け止めやすいような図解があり、優しい大学の講義を彷彿とさせる。

f:id:vaaaaaanquish:20200830175434j:plain:w400
モンティ・ホールゲームに関する例題の図

ユニークな問題に対して、解答となるソースコードが全て公開されている点も大きく評価できる。
中級者向けの問題だけあって、実際に問題を解く場合にはそれなりの量のコーディングが必要となるが、解答とそのコードの解説を数行単位で行ってくれており、最悪自分でPythonを書かなくても理解に及ぶだろう。

アルゴリズムについても図解や数式の解説が散りばめられており、例題と合わせて利用することで、前述したキーワードに関連する内容がスッと入ってくるようになっていると感じた。

f:id:vaaaaaanquish:20200830175438j:plain:w400
回文に関するアルゴリズムの図解

これらを課題のように進める中で、計算量の肥大化、コーディングにおけるオブジェクト指向の重要性、インターフェースなどを意識させる形にもなっており、最後の14〜16章あたりにおいては、それなりに大きなGUI付きのプログラムを作成できるようになっている。
この進め方も、より一般的なコンピュータサイエンスの講義に近いものを感じる。

 
また、何より素晴らしいのは、章ごとに書籍以上の情報を参照したい場合のURLが書かれている点だ。

f:id:vaaaaaanquish:20200830180516j:plain:w400
さらに読むなら

引用に加えて、専門書以外でこれが出来ている書籍は多くはない。様々なCSの問題に対して興味を持つための足がかりにもなるだろう。


 

どういう人がターゲットか

前述した、問題とキーワードに対して感じる内容は、概ね「バックグラウンドとしてコンピュータサイエンスに近い学科を卒業しているか」に依る印象がある。

私自身は高専から大学院までCSの学科に所属していた事もあり、上記キーワードに対して「大学1~3年程度で演習に出てくるワード一覧である」と読みながら感じた。情報工学科などを出た大学生であれば、記憶にうっすらある状態であろうとも、調べて思い出しながらであればスラスラ問題を解く事ができるのではないだろうか。

CS出身者はある意味ターゲット層には遠いかもしれない。
昔懐かしい問題演習として購入してみる形であれば、面白い題材が詰まった良い本であるとも言える。

 
レベル感としては、競技プログラミングより少し応用を意識している(ユーザインタフェースの作成、オブジェクト指向への誘導など)。
一方で問題を汲み取り、アルゴリズムソースコードに落とし込むことを鍛える側面もあり、競技プログラミングに入る前後で力を付けるために読んでも良さそうである。

 
CS出身者でない方には非常に読んで実践してみて欲しい書籍ではある。
ただし、大学の1年分の講義を集約したような書籍ではあるので、時間がかかる事を覚悟してトレーニングとしておすすめしたい。加えて、初学者向けの書籍とは違って、1単語1単語丁寧に解説してくれている訳ではないので、ある程度未知の単語を調べながら、知識欲に従って楽しく進めると良さそうである。

本書籍を読むことで、アルゴリズムに関連するトレーニングができるだけでなく、新しい知識への取っ掛かりも得られるはずである。
あと、CS出身者の謎のあるある会話に入り込めたりするメリットもありそうだなと感じた。

 

おわりに

実用的でないPythonプログラミング」のタイトルにもある通り、例えばWebサービスを構築するエンジニアが日常的にこの書籍の知識を使うかと言われたら「実用的ではない」と答えるだろう。近年では、ソフトウェアエンジニアも多様化してきているので、ある側面で実用的でないのは確かである。

一方で、コンピュータサイエンスが実用的でないかどうかに対する答えは、訳者が「実用的でないと言いつつ実用的な本になった」とあとがきで語っている通り、プログラミングを行う上で基礎知識として必要なものにもなりうる。
 
私個人としては、Python初学者向けの本を読んだ後、基礎を固める上で重要な項目が散りばめられた書籍でもあると思うし、多くのソフトウェアエンジニアが学んでおくべき内容である、とも思う。現に近年では、IT企業においてこのようなコンピュータサイエンスに関連したコーディング試験を面接で実施するのは一般的になっているし、自分が所属する企業でも、バックグラウンドに関係なく本書籍に乗っているようなレベルの問題を解く力は必須になっていると感じる。


こういったコンピュータサイエンスを書籍の中でも、問題に寄ったドリルのような書籍である。

 
ソフトウェアエンジニアの皆様には、是非ともベースラインを高める意味でも、トレーニングの意味でも実際に手に取って読んで問題に挑んでみる事をおすすめしたい。

 


 

poetryを利用した動的なバージョン管理とGitHub ActionsによるPyPIへのrelease

はじめに

この記事を読んで出来る事

  • poetryによる外部モジュールバージョン管理
  • poetry-dynamic-versioningによる動的なバージョン付与
  • GitHub Actionsを利用したPython周りの基本的なCI/CD設定
  • GitHubのReleaseタグ付与をTriggerとしたPyPIへのアップロード

今後私がPythonで何かライブラリ作ろうと思ったらこれを実施するぞというメモです

 

 

poetryによるモジュールバージョン管理

バージョンをGitHubのタグで管理したい事の方が多いはず。
setup.pyを利用する場合は、一般的にsetuptools_scmを使うが、poetryはsetup.pyのようにbuild時にスクリプトを組み込むのは基本できないので、poetryの実装にpatchを当てる形で動的にバージョンを取得する。そのためのライブラリが以下のpoetry-dynamic-versiong。patchを当てている箇所はここ
github.com

git repository以外にもsubversionに対応していたり、独自のバージョンフォーマットも扱えるので、基本的にはこちらで問題なさそう。

 
pyproject.tomlを書いていく。

[tool.poetry]
name = "module_name"
version = "0.0.0"  # using poetry-dynamic-versioning
description = "sample tool"
authors = ["vaaaaanquish <6syun9@gmail.com>"]
license = "MIT"
readme = "README.md"
homepage = "https://github.com/vaaaaanquish/module_nam"
repository = "https://github.com/vaaaaanquish/module_nam"
documentation = ""

[tool.poetry-dynamic-versioning]
enable = true
style = "pep440"

[tool.poetry.dependencies]
python = "^3.7"
pandas = "^0.25.0"
matplotlib = "*"

[tool.poetry.scripts]
my-script = 'module_name:main'

[build-system]
requires = ["poetry"]
build-backend = "poetry.masonry.api"

 
githubのrelease tagで 「v0.0.1」 というフォーマットを採用していれば、以下を追記するだけで良い

[tool.poetry-dynamic-versioning]
enable = true
style = "pep440"

tool.poetry配下のversionが残る事だけが気がかりだが、「0.0.0」のようにしておけば問題にはならなそう。現状外すとpoetry build時にエラーとなる。

「v0.0.0」以外のformatを使っている場合は、READMEのConfigurationを参考にpattern、もしくはformatを設定する。

tag v0.0.2が打たれたrelease
tag v0.0.2が打たれたrelease

 
poetry-dynamic-versiongを使う場合には、poetryのdependenciesとしてではなく、poetryと同じレイヤーのPythonでinstallする必要がある。

pip install poetry
pip install poetry-dynamic-versioning

# poetry install
poetry update
poetry build

出力は以下のようになり、github release tagと連動してmodule生成が出来ている事が確認できる。

Building module_name (0.0.2)
 - Building sdist
 - Built module_name-0.0.2.tar.gz

 - Building wheel
 - Built module_name-0.0.2.whl


 

PyPIへのアップロード

手元からPyPIにpushするにはpoetry publishを使う方法が一般的。
以下のような形でpublishすれば良い。

poetry publish --build --username {hoge} --password {piyo}

基本的にはあまり手元からやりたくないので、GitHubにmasterマージされたりしたタイミングで自動でPyPIにアップロードしてもらいたい。後述する。


 

GitHab Actionsを用いたCI/CD

GitHub Actionsの設定ファイルを書いて、release tagをトリガーにして動的にアップロードするようにする。

# This workflows will upload a Python Package using Twine when a release is created
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries

name: Upload Python Package

on:
  release:
    types: [created]

jobs:
  deploy:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    - name: Set up Python
      uses: actions/setup-python@v1
      with:
        python-version: '3.x'
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install poetry poetry-dynamic-versioning twine
    - name: Build and publish
      env:
        TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
        TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
      run: |
        poetry publish --build --username $TWINE_USERNAME --password $TWINE_PASSWORD

releaseタグが打たれた事をトリガーにしてmasterのコードをPyPIにpushするやつ。.github/workflows/python_publish.ymlにしてrepositoryに含める。
repo内の Settings > Secrets からユーザ名とパスワードを追加する。

repo内の Settings &gt; Secrets からユーザ名とパスワードを追加する。
repo内の Settings > Secrets からユーザ名とパスワードを追加する。

 

その他GitHubでやること

flake8、yapfなりのフォーマッタのCIを別途作る。
Settings > Branch protection rule からCIの必須化とmasterにpushできない設定をしておく。

Branch protection rule
Branch protection rule


PRからGitHub Actionsのbuildタスクが通らないとmergeできない事を確認する。

PRからGitHub Actionsのbuildタスクを確認
PRからGitHub Actionsのbuildタスクを確認

これで一連の設定はおわり。

setup.cfgやsetup.py、pipfileで書いていたものがほぼ不要になるし、PyPIのREADMEが勝手にMarkdownレンダリングされたものに変わっていたり、本当にやることが減って何かと便利。pipenvより早い、PEP 518 で規格化されている。良い感じ。
 

機械学習モデリングの広辞苑的書籍「Kaggleで勝つデータ分析の技術」が良かったので筆者に媚を売る

- はじめに -

当ブログでは恒例になっている、献本されたので筆者に媚を売るシリーズです。

今回は10/9に発売予定の「Kaggleで勝つデータ分析の技術」という書籍なんですが、既に発売前にしてAmazonベストセラー1位。豪華著者陣とKaggleにおいては日本有数の起業と言っても過言ではない、DeNA株式会社の豪華レビュワー。筆者がブログを書いていたりu++さんがめちゃくちゃ丁寧な書評を書いていたり、Kaggle Grand Master各位の薦めツイートも出てきた段階で、もう私が媚を売る必要すらないと思いますが、良かったので感想だけでも残しておければと思います。

Kaggleで勝つデータ分析の技術

Kaggleで勝つデータ分析の技術

端的に言えば、テーブルデータにおける機械学習モデリング、データ分析の広辞苑+αな書籍で、筆者らがブログやSNS等で述べている通り「暗黙知を洗い出す」ような良書でした。

 

 

- どんな内容だったか -

前置きとして「広辞苑的な書籍」と表現した通り、Kaggle、データ分析における重要なワードとその適切な範囲までの解説がセットで広く書かれた書籍です。

詳細なアルゴリズムや数理的な背景までは説明していませんが、その分幅広く適切な深さの解説、コラム、ソースコードや過去コンテストの実例がついています。

f:id:vaaaaaanquish:20191005205334j:plain:w400
数式を最小限にしつつも実例を交えて各評価指標やアルゴリズムを紹介

コードや背景については分量は少なめですが、私が読んだ技術書の中でもズバ抜けて参考文献を非常に丁寧にまとめており、気になる所が後追い出来るようになっているだけでなく、ソースコードGitHubで公開されるとの事なので、基本的に読んだ後に手を動かして体感する事も可能そうです。


また、「決定木の気持ち」「過去コンペで話題になった手法」といったKaggler同士の中でのミームについても解説が入っています。Kaggle自体のコミュニティ性から言っても、技術情報や共通のミーム、周囲の戦略の収集は、コンテストに挑むにあたって重要な事でもあるので、今からでも書籍を読んで把握しておいて損はないでしょう。

f:id:vaaaaaanquish:20191005210157j:plain:w400
基本的なコンテストでの分析フロー等が図付きで解説されている

 
私自身、機械学習やデータ分析のブログやSNS投稿、スライド、発表や同人誌などで情報収集をする事は多々ありますが、「あなたの言ってる定義微妙に違ってない…?」と思う事は少なくありません(私が毎回正解を言っている確信がある訳でもありません)。そういった機械学習モデルや統計モデルを活用するコンテストやプロジェクトが一般的に広まってきたという段階で、各ワードに対する認識の共通化を図れる良い書籍です。

この一冊とGoogle翻訳があれば、Kaggle上のDiscussionやKagger Slack、オフラインイベントでの会話をかなり的確に把握できるようになるとも感じます。そういった場で、今まで曖昧に流してきたワードがあるような方にオススメの一冊です。

 
書籍のまえがきには、「ビジネス的な側面について触れない」とも書かれており、事実内容もその通りではありましたが、弊社の機械学習チームではこの書籍に出てくるような単語を前提の共通ワード、コンテキストとして扱う場面が多くあるなと読んでいて思いましたし、コンペに出る事を目的としない場合でも自力を上げる知識が多く含まれています

 
個人的には、これほど「タスクの評価」「モデルの評価」「Validationの設計」について広く用語がまとまった書籍は他に見たことがありませんし、実務でモデルの評価について考えている人はここに書いてある事は前提として一通り把握しておくべきだとも思います。実務だとここに事業的な評価やシステムの話が入ってくると思いますが、まず前提として知っておかないとモデルも構築できないと思います。

 
私自身、これを読みながら以下のようなツイートをしており、書籍と公開されるGithubを合わせれば、自然言語処理100本ノック、画像処理100本ノックに続く、手を動かして機械学習を広く知るコンテンツの1つともなりそうです。


知の高速道路が整備されるのは良いことです。

 

- 読んだらKaggleで勝てるか -

読んだだけでは勝てそうにありません。

 
この書籍は広辞苑のように大きく知識を広げ、ともすればコンテスト中に逆引きできる良書籍ですが、実際には多くの知見、バックグラウンドがその背景に詰め込まれた書籍でもあります。

例えば、「基本的なライブラリ(numpy, pandas, sklearn, ..., etc)の使い方」から「予測確率」「次元削減」といったワード、果ては「教師あり、なし学習」辺りまで、機械学習、統計の教科書的な知識は前提知識として必要としてきます。プラスでモデルの背景が分かっていないと「ん?」となる部分もいくつかあります。これは、それらがきちんとしている著者らが書いたからこそ正確かつ客観性、再現性のある書籍になっているという話でもあります。「舟を編む」ではないですが、これ程の機械学習、データ分析の範囲を適切な表現と深さで書いている本はほとんどありませんし、流石と言わんばかりです。
 
そういった事もあって、前提知識がないと出来ない事、例えば「アルゴリズムわからないので機械学習モデルどれ選べばいいか分からない」「可視化の結果どの方向性に行くのが正しいか分からない」という課題を解決してくれる書籍では無いので、「これでKaggle始めよう!」という人には前提として他にもいくつかの書籍やブログと一緒に読んでいく必要があるかなと思いました


全体が薄い訳ではなく、「xgboostのアルゴリズム、チューニング」「ベイズ最適化」については大きく3~5ページが取られています。筆者としてもKaggleで重要なBoostingとチューニングに限っては丁寧に解説したかったのだとも思います。特に近年では業界内でも有名なブログや企業、学術機関からも、不要なパラメータのチューニングやLeakageを代表に、コンテストでは一般的にバッドプラクティスとされる話が多く出回るようになりましたし、注力して書かれていると感じます。

逆に言うと、このxgboostの丁寧な解説と同量またはそれ以上のバックグラウンドが、他全ての章に書かれた技術についても本来ある訳でもあります。この書籍から分からなかった単語をググって、勉強して、コンテストに出てという形を想定した足掛かり的な書籍でもあると感じました。

 
当たり前ですが、実際に手を動かして、体感して、コンテストに時間を捧げてsubmitしない事には勝てないので、やっていきの気持ちが大事です。というか知ってるだけで勝てるなら私だってk…

 

- どんな書籍と読むと良さそうか -

基本的には、書籍内の参考文献が非常に優れておりその中のスライドやブログ、書籍を参考にすると良いと思います。実際この付録に書籍の3割くらいの価値が詰まっていると思います。

実際この付録のURL一覧だけでGitHubスター100はあげたいレベルです。

 
中でも、Kaggleでプラスになると考えて抜粋すると、「特徴量エンジニアリング」辺り。「データ解析のための統計モデリング入門」は一緒に買っておくと間違いないと思います。

機械学習のための特徴量エンジニアリング ―その原理とPythonによる実践 (オライリー・ジャパン)

機械学習のための特徴量エンジニアリング ―その原理とPythonによる実践 (オライリー・ジャパン)

はじパタやプロフェッショナルシリーズも悪くないですが、上の2つは直接的にKaggleや分析に効くいい本だと思います。
 
参考文献外だと「仕事ではじめる機械学習」「機械学習のエッセンス」はこの本の支えになる知識が多いと感じました。持ってなければ是非一緒に。

仕事ではじめる機械学習

仕事ではじめる機械学習

 
最近は書籍も多い(献本も多い)ですが、低いレベルのまとめ的な書籍も少なくないです。そういう意味でも「Kaggleで勝つデータ分析の技術」の厳選された参考文献と過去コンテスト情報は読んでおくべきだと思います。


 

- おわりに -

 
数年前、偶然同僚にKaggle Grand Masterが居たので気になってKaggleを始めましたが、まさかこんな良質で立派な書籍が出たりする程大きくなるとは…正直想像していませんでした。しかも、Expertになってからボーッとしてたら、数年で幾人もの人に追い抜かれ、何も言えない人になってしまいました。悔しいです。

DeNA株式会社に蔓延する攻撃的なSNSアカウントから「Expertが書いてる書評だから無意味」と言われそうなので、ブログはこの辺にしておきます。

 
媚を売られる側になれるよう、私も頑張ります。


Kaggleで勝つデータ分析の技術

Kaggleで勝つデータ分析の技術

 

追記:2019/10/05
らしいです。これは失礼しました。

xonshのEnvironment Variablesの全て

- はじめに -

以下の記事でxonshのEnvironment Variablesの大体の日本語訳を書いた。

xonshrcを書く - Stimulator

しかし、上記の記事は2017年次の物かつ、要約的な記事のため、より詳細に見て開発に以降できるよう本体へのコードや掴みどころをメモした記事を書いておく。

本家ドキュメントは下記なので参照のこと。

xon.sh
 

 
 

- Windowsに関連するもの -

Windowsの人はまずここ見て設定。
xon.sh

$ANSICON

Windowsのcmd.exe(コマンドプロンプト)において、ANSI escape sequencesを表示するためのアプリケーションであるansicon*1を利用するかどうかの設定。ansiconを利用する場合にTrueにしておくと、$TITLEをプロンプトのタイトルに設定するようになる。実装でいうと以下のような感じ。

import ctypes
ctypes.windll.kernel32.SetConsoleTitleW(env.get("TITLE"))

 

$INTENSIFY_COLORS_ON_WIN

Windowsのcmd.exeを利用する時に、colorを見やすくするかどうかのフラグ。青色がシアンに置き換わったりする。
そもそも近年でcolor style自体がかなり柔軟になったので、不要かも。細かく指定しない場合には使える。

 

$WIN_UNICODE_CONSOLE

Trueであれば、WindowsでのUnicodeサポートを有効にする。win_unicode_consoleなる外部ライブラリをimportしているだけ。以下必須。

pip install win_unicode_console

 

$PATHEXT

環境変数のPATHを見に行った時に、実行可能ファイルとして判定する拡張子のリスト。
".EXE"のように大文字で指定する。xonsh上でWINDOWS判定されていれば、[".COM", ".EXE", ".BAT", ".CMD"]がデフォルトで入るので、それ以外に必要であれば追加する。

 

- xonsh本体の動作に関するもの -

$XONSHRC

rcファイルへのpathのリスト。['~/xonshrc', '~/.config/xonsh/rc.xsh']のように複数あれば前方から複数読み込まれるし、ファイルがなければ読み込まれず終わる。

$VIRTUAL_ENV

アクティブなPythonへのPath。pythonやpipコマンドが指すところでもあり、xonsh用のpythonバージョン管理ツールであるvoxも参照するpathになる。
デフォルトでは変数が設定されていない状態なので注意。

 

$XONSH_HISTORY_SIZE

historyのサイズの指定。「(8128, "commands")」「"8128 commands"」のようなsetか文字列で、保存するコマンド数(commands)、保持する履歴ファイル数(files)、許可される秒数(s)、バイト数(b)の単位のどれかと値を指定する。

$XONSH_HISTORY_BACKEND

historyの保存形式。デフォルトはjsonだが、sqlを設定すればsqliteをバックエンドに選べる。設定は以下を見ると良い。
https://xon.sh/tutorial_history_backend.html

 

$SUPPRESS_BRANCH_TIMEOUT_MESSAGE

Trueであれば、VC_BRANCH_TIMEOUTに設定された時間を過ぎてもPROMPTの文字列、branch名、色の生成が終わって無かった場合に強制的にタイムアウトとしメッセージを表示する。PROMPTに{curr_branch}でブランチ名を表示させようとするが、git repoが巨大で…という時に起こる。

 

$VC_BRANCH_TIMEOUT

gitのブランチ名などをPROMPTに表示する時にタイムアウトする秒数。デカいrepo触ったりする時に設定しておくと良い。

 

$VC_HG_SHOW_BRANCH

Trueであれば、Mercurialのブランチを表示する。gitでなくMercurial使っている場合はこちら。

 

$UPDATE_OS_ENVIRON

Trueであれば、xonshのEnvが追加、削除された場合にその変更をos側の環境変数にも加える。デフォルトはfalse。
中身は「os.environ.update」を少し工夫して呼んでいるだけなので、個別にos側に追加したい時は不要。WindowsとかOSの環境変数が影響の大きい環境では使える場合があるかも。

 

$VI_MODE

Trueであれば、シェルの操作がvi風になる。iでinsert、escでnormalモードになるなど。
実装自体はprompt toolkitの機能で、現行のモードを判定するfilterとして実装されている以下の部分から追っていくと良い。
https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/prompt_toolkit/filters/cli.py

 

$XDG_DATA_HOME

英語では「Open desktop standard data home dir」となっているが、普通にセッションログやスクリプトの実行ログが入る場所へのpathというだけ。以下のXONSH_DATA_DIRに近いが、こちらのPathはxonsh用ではなく、一般的な環境全体用。
すこし踏み入ればpygmentsを使った実装になっていることが分かるが、難しい事はほとんどしておらず整備もあまりされていない印象。pygmentsすごい。

 

$XONSH_DATA_DIR

セッションログやスクリプトの実行ログが入る場所へのpath。
デフォルトでは「$XDG_DATA_HOME + "/xonsh"」 が設定されている。

 

$XONSH_CACHE_EVERYTHING

Trueであれば、全てのコード(コマンド含め)をキャッシュに保存する。
ここで言っているキャッシュというのは、historyではなく、そのセッションにおけるキャッシュで、できるだけコンパイルして保存しておいて次回のコマンド実行を早めるというもの。

 

$XONSH_CACHE_SCRIPTS

Trueであれば、スクリプト実行をキャッシュに保存する。
機構自体は上記のXONSH_CACHE_EVERYTHINGと同じだが、こちらはexecを通るようなスクリプトを実行した場合のキャッシュ。

 

$XONSH_TRACEBACK_LOGFILE

XONSH_SHOW_TRACEBACKがTrueの場合に、ログファイルとして残す先。ファイル名か不要の場合はNoneにしておく。

 

$XONSH_STORE_*

Trueであればhistoryに標準入出力を保存する。

$XONSH_STORE_STDIN

xonshの!()や![]オペレータを使って実行されたものをhistoryに保存する。

$XONSH_STORE_STDOUT

stderrやstdoutをhistoryに保存する。

$XONSH_DATETIME_FORMAT

ログやhistoryなど多くの場所で使われるdatetimeのフォーマット。デフォルトでは「"%Y-%m-%d %H:%M"」。
利用先は多いが、実装はtools.pyの中に収まっているだけでシンプル。

 

$XONSH_DEBUG

デバッグモードの指定。1であればimport情報、2であれば入力変換、コマンド置換の情報、3以上であればPLY解析メッセージと情報が増えていく。
よくissueを立てると「$XONSH_DEBUG=3 で一回ログ出して見て」とよく言われたりする。
実装自体は、参照されている部分が多いので変更は大変そうだが、追加はlogging足すだけなので参考に。

 

$XONSH_ENCODING

xonshのサブプロセスで利用されるエンコーディング。デフォルトでは「sys.getdefaultencoding()」が入る。
I/O制御で使われているTeeクラス、入力を作るreadline辺りが主に利用している。

 

$XONSH_ENCODING_ERRORS

Pythonエンコーディングエラーが出た時の処理。多くの場所で共通して使われている。
入る値は以下を参考に。
https://docs.python.org/3/library/codecs.html#error-handlers

 

$XONSH_PROC_FREQUENCY

連続したパイプラインを実行する時、キューを読み込むためにxonshプロセススレッドがスリープする秒数。
スリープタイムというよりは、コマンドを連続で実行した時、スレッド同士のキューが詰まった場合にタイムアウトする時間のイメージ。

 

- コマンド補完に関するもの -

$UPDATE_COMPLETIONS_ON_KEYPRESS

Trueであれば、キー入力時に毎回補完候補を出すようになる。例えば補完であればTABを押さなくてもキー入力毎に候補を表示できるようになる。
PROMPTの表示を評価するUPDATE_PROMPT_ON_KEYPRESSも別にあるので混同に注意。

f:id:vaaaaaanquish:20180622142703g:plain
キー入力時評価の例

実装自体はprompt toolkitの機能で、xonshはこの変数をフラグとしてptkの引数となるcomplete_while_typingに投げているだけである。その仕組み自体を把握するにはptkを追う必要がある。詳細は以下。
https://python-prompt-toolkit.readthedocs.io/en/latest/pages/asking_for_input.html?highlight=complete_while_typing#complete-while-typing

 

$AUTO_SUGGEST

Trueの時にfish shellのようなグレーアウトの補完候補を出す*2

f:id:vaaaaaanquish:20190824001450p:plain
AUTO_SUGGEST

この機能はxonshのコアライブラリであるpython prompt toolkit内の実装なので、そちらを参考にすると良い。
https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/docs/pages/asking_for_input.rst#auto-suggestion

表示されている補完候補を確定するときは「右矢印」「ctrl+e」がデフォルトで設定されている。変更する場合は以下が参考になる。
xonsh[ptk]で、Suggestionを確定するキーバインドを設定する - Qiita

下記のAUTO_SUGGEST_IN_COMPLETIONSとも関連しており、両者がTrueになっている場合、AUTO_SUGGESTで表示される候補をTab補完から除く等の機能があるので設定はよしなに。

 

$AUTO_SUGGEST_IN_COMPLETIONS

Trueの時、Tabキーで補完候補を表示する。$UPDATE_COMPLETIONS_ON_KEYPRESSがTrueの時は、Tab以外のキーでもキー入力時に補完候補が動的に表示される。

AUTO_SUGGEST同様、xonshというよりはpython prompt toolkitの機能であり、そちらを呼び出すかどうかのフラグにあたる。
https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/docs/pages/asking_for_input.rst#autocompletion

 

$COMPLETIONS_DISPLAY

Pythonのコードの補完を表示するか、もしくは表示形式をどうするか。

none or false

表示しない。stringの"none"や"false"でも良いし、PythonのNoneでも、boolのFalseでも良い。

single

1列で補完候補を表示する。stringで"single"。

f:id:vaaaaaanquish:20190824214546p:plain
$COMPLETIONS_DISPLAY = "single"

multi or true

複数列で表示する。デフォルト値。stringで"multi"、"true"とするか、boolのTrueを指定する。

f:id:vaaaaaanquish:20190824214823p:plain
$COMPLETIONS_DISPLAY = "multi"

readline

GNU Readlineの動作を再現した表示にする。stringの"readline"。

f:id:vaaaaaanquish:20190824214905p:plain
$COMPLETIONS_DISPLAY = "readline"


引数の型が柔軟なのは歴史的経緯。こちらはPythonの補完のみでbashの補完はこのEnvに依存していないので注意。

 

$COMPLETIONS_MENU_ROWS

補完を出す行数の指定。stringの"multi"だと5行で横幅最大まで補完を省略して表示する。int値を指定しても良い。

 

$COMPLETION_IN_THREAD

補完をasync/awaitで表示する。
実装自体はprompt toolkitのAppの引数にフラグを入れているだけで、非同期補完の詳細を見るにはptkのasync_promptの実装を見ると良い。
https://python-prompt-toolkit.readthedocs.io/en/master/pages/asking_for_input.html#asynchronous-completion
https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/prompt_toolkit/shortcuts/prompt.py#L909

 

$COMPLETION_QUERY_LIMIT

補完メニューの表示件数。最大件数では無く、これを超えたら補完が表示される閾値のイメージ。
xonshやprompt toolkitの機能というよりは、GNU Readlineのrl_completion_query_itemsをglobal変数として入れておくという実装なので、詳しくは以下を読む。
GNU Readline Library - Programming with GNU Readline

 

$COMPLETIONS_CONFIRM

タブ補完メニューが表示されている時、FalseだとEnterでコマンドを直接実行、TrueだとEnterでコマンドを確定のみして確認状態にする。
実装はprompt toolkitのkeybindにfilterを設定しているので、理解したければptkのkeybindingsを理解してからが良い。

 

$BASH_COMPLETIONS

bashのcompletionを利用するために、bash_completionスクリプトへのPATHを設定するための変数。sshの補完やgitの補完が含まれるため、基本的には必須だと思う。
listかtupleで最初に有効だったものが利用される。

例えばMacではbrewbashの補完が導入できる。

brew install bash-completion2

こちらは、"/usr/local/share/bash-completion/bash_completion" にファイルが配置される。デフォルトでBASH_COMPLETIONSにこのpathが入っているはずなので、基本的にはインストールだけで設定できるはず。

デフォルトだといくつか設定されており、それらを走査する実装になっているため、もし一意に絞れるようであれば絞るかソートで前に持ってきておくと良い。

 

$COMPLETIONS_BRACKETS

Trueの時、シェル入力時にPythonの括弧の補間を有効にする。実装自体はシンプルなので読めば大体分かると思う。

 

$FUZZY_PATH_COMPLETION

Trueであれば、pathをTab補完する時にfuzzyに補完する。あいまい補完。

> $FUZZY_PATH_COMPLETION = True
> ls desko 
./Desktop

具体的な実装としてはlevenshteinを使っており、以下を見れば良い。
https://github.com/xonsh/xonsh/blob/master/xonsh/tools.py#L854

 

$GLOB_SORTED

Trueであれば補完の結果をソートしてから表示する。

 

$SUBSEQUENCE_PATH_COMPLETION

Trueであれば、pathの補完において ~/w/t のような入力でも拡大解釈されて ~/work/tmp も候補に上がるようになる。
ディレクトリが長い時に雑に打って移動する時に使える。

 

$SUGGEST_COMMANDS

Trueであれば、無効なコマンドを入力した時に「もしかして?」を表示する。

f:id:vaaaaaanquish:20190825222513p:plain
$SUGGEST_COMMANDS=True

 

$SUGGEST_MAX_NUM

SUGGEST_COMMANDSがTrueの時に表示するコマンド数。負の値を入れておくと制限なしになる。

 

$SUGGEST_THRESHOLD

SUGGEST_COMMANDSやFUZZY_PATH_COMPLETIONでレーベンシュタイン距離を計算して、その閾値として扱う値。デフォルトは3。

 

$XONSH_AUTOPAIR

Trueであれば、括弧、括弧、引用符を自動挿入する。
keybindingsのfilterとして実装されているので、prompt toolkitにおけるfilterが何かを追ってから読むと大体実装もわかる。

 

$XONSH_HISTORY_MATCH_ANYWHERE

Trueであれば、上矢印で履歴を参照する時、現在の入力を検索語として接頭語以外でも補完する。
実装自体はprompt toolkitのbufferにおけるhistory matchなので、以下を参照すると良い。
https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/prompt_toolkit/buffer.py#L911

 

- ファイル、ディレクトリ操作に関するもの -

$AUTO_CD

Trueにしておくとcdコマンドを打たなくてもディレクトリ名だけで移動できる。

 

$CDPATH

bash, zsh等のCDPATHと同じ。cdコマンドが利用する相対パス。CDPATHによく使うディレクトリを入れておけば、cd hogeでショートカットのように移動できるので便利。一方で補間含めて全ての相対パスがこちらを参照するようになるので、使う場合は意図しないディレクトリに移動しないよう注意が必要。

 

$AUTO_PUSHD

DOSで言う所のディレクトリスタックに対して、cd時に自動でpushdする。pushd、popd、dirsコマンド*3を良く使うのであれば良い。

 

$DIRSTACK_SIZE

DOSで言う所のディレクトリスタックの最大保存数。実装自体は至極簡単なので読めば分かるはず。

  

$PUSHD_MINUS

ディレクトリスタックの操作pushd、popdに関するフラグ。Falseが通常のシェルコマンドのpushdで、Trueの場合はpushdとpopdが逆になる。

 

$PUSHD_SILENT

Trueならディレクトリスタックにpushdした時に表示する。デフォルトのFalseなら表示しない。

 

$DOTGLOB

Trueなら「*」や「**」を使ってglobした時にドットで始まるドットファイルを含むようにする。
実装はシンプルにreplaceで変換しているだけなので、読めば分かるはず。

 

- 表示に関するもの -

$PROMPT

xonshというよりはpython prompt toolkitの機能であり、ptk内のAppに対する引数に与える文字列に当たる。
https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/docs/pages/asking_for_input.rst#adding-a-bottom-toolbar

背景色や文字色も変更できるので、以下のドキュメントを参考にすると良い。
Asking for input (prompts) — prompt_toolkit 3.0.0 documentation
More about styling — prompt_toolkit 3.0.0 documentation

$PROMPTの表示。詳しく設定したい場合は、以下のチュートリアルを見ながら設定を進めると良い。
https://xon.sh/tutorial.html#customizing-the-prompt

 

$PROMPT_FIELDS

$PROMPTに対してオリジナルのフォーマットを関数等で指定できる。詳しく設定したい場合は、以下のチュートリアルを見ながら設定を進めると良い。
https://xon.sh/tutorial.html#customizing-the-prompt

 

$RIGHT_PROMPT

xonshというよりはpython prompt toolkitの機能であり、ptk内のAppに対する引数に与える文字列に当たる。
https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/docs/pages/asking_for_input.rst#adding-a-bottom-toolbar

背景色や文字色も変更できるので、以下のドキュメントを参考にすると良い。
Asking for input (prompts) — prompt_toolkit 3.0.0 documentation
More about styling — prompt_toolkit 3.0.0 documentation

右側のPROMPTに表示する文字列。詳しく設定したい場合は、以下のチュートリアルを見ながら設定を進めると良い。
https://xon.sh/tutorial.html#customizing-the-prompt

 

$BOTTOM_TOOLBAR

bottom toolbarを表示する文字列。空ならbottom toolbar無しになる。

f:id:vaaaaaanquish:20190824172521p:plain
bottom toolbar


xonshというよりはpython prompt toolkitの機能であり、ptk内のAppに対する引数に与える文字列に当たる。
https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/docs/pages/asking_for_input.rst#adding-a-bottom-toolbar

背景色や文字色も変更できるので、以下のドキュメントを参考にすると良い。
Asking for input (prompts) — prompt_toolkit 3.0.0 documentation
More about styling — prompt_toolkit 3.0.0 documentation

詳しく設定したい場合は、以下のチュートリアルを見ながら設定を進めると良い。
https://xon.sh/tutorial.html#customizing-the-prompt

 

$XONSH_GITSTATUS_*

xonshのgit statusのシンボル。gitコマンドがない場合はそれぞれsetされないので注意。

 

$XONSH_GITSTATUS_HASH

ハッシュ値。「git describe --always」と「git rev-parse --short HEAD」の値が違っていたら、XONSH_GITSTATUS_HASH+後者の値をHASHとして表示する実装。以下参照。
https://github.com/xonsh/xonsh/blob/master/xonsh/prompt/gitstatus.py#L89
デフォルト値は":"。

$XONSH_GITSTATUS_BRANCH

ブランチ名。デフォルト値は"{CYAN}"。

$XONSH_GITSTATUS_OPERATION

rebase中だとかmerge中だとかの状態シンボル。
[https://github.com/xonsh/xonsh/blob/master/xonsh/prompt/gitstatus.py#L104[
デフォルト値は"{CYAN}"。

$XONSH_GITSTATUS_STAGED

stageに乗っている時のシンボル。デフォルト値は"{RED}●"。

$XONSH_GITSTATUS_CONFLICTS:

コンフリクトが置きている時のシンボル。デフォルト値は"{RED}×"。

$XONSH_GITSTATUS_CHANGED

変更がある状態のシンボル。デフォルト値は"{BLUE}+"。

$XONSH_GITSTATUS_UNTRACKED

gitをトラックしていない時のシンボル。デフォルト値は"…"。

$XONSH_GITSTATUS_STASHED

stashした状態のシンボル。デフォルト値は"⚑"。

$XONSH_GITSTATUS_CLEAN

git上記の状態以外でclearな時のシンボル。デフォルト値は"{BOLD_GREEN}✓"。

$XONSH_GITSTATUS_AHEAD

masterブランチとのズレがaheadな状態を示すシンボル。デフォルト値は"↑·"。

$XONSH_GITSTATUS_BEHIND

masterブランチとのズレがbehindな状態を示すシンボル。デフォルト値は"↓·"。

 

$UPDATE_PROMPT_ON_KEYPRESS

Trueであれば、キー入力ごとにPROMPT表示を評価する。PROMPTに時計を実装したり、gitの状態を表示する時に有用。
実装もprompt toolkitにprompt周りの設定を関数で渡すか、stringで渡すかの違いでしかないのでシンプル。
実際評価しているのはptkなので、以下辺りから追っていくと良い。
https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/prompt_toolkit/shortcuts/prompt.py

 

$PROMPT_REFRESH_INTERVAL

PROMPTを更新する秒数を入れる。実装自体はprompt toolkitにあり、以下のサンプルのようにbottom_toolbarやrpromptでリアルタイムに更新される時計を作ったりできる。
https://github.com/prompt-toolkit/python-prompt-toolkit/blob/2.0/examples/prompts/clock-input.py

私が追加した変数なのだが、もちろん画面全体が指定秒でリフレッシュされて更新される訳なので、シェルの動作が重くなってしまう。(rpromptだけとか出来るようになっていくと良いな…)

 

$COLOR_INPUT

入力しているコマンドにsyntax highlightingをつける。実装自体は、prompt toolkit及びpygmentsによる実装なので以下を参考にすると良い。
https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/prompt_toolkit/lexers/pygments.py#L145

 

$COLOR_RESULTS

出力結果にsyntax highlightingをつける。実装自体は、上記のCOLOR_INPUTと同じくprompt toolkit及びpygmentsによる実装なので以下を参考にすると良い。
https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/prompt_toolkit/lexers/pygments.py#L145

 

$PRETTY_PRINT_RESULTS

Tanaka Akira氏が作っているrubyのprettyprint.rbを使って、出力を表示するかどうかのフラグ。
以下のドキュメントの通り、単体でもapiとして使えるようにはなっている。
https://xon.sh/api/pretty.html

 

$DYNAMIC_CWD_WIDTH

$PROMPTに表示するディレクトリを省略する時の文字数閾値
(float, str)のtupleで、0番目に閾値を入れる。float('inf')を入れる事もできる。
1番目にはstringの"%"かそれ以外が入る。"%"を入れた場合はShellの画面サイズに対する0番目の値のパーセンテージになる。"%"以外を入れたらシンプルに0番目がintになって文字数で閾値になる。

 

$DYNAMIC_CWD_ELISION_CHAR

$PROMPTに表示するディレクトリ名が長い場合に省略形として出す文字列。省略する時の文字数の閾値はDYNAMIC_CWD_WIDTHに依存。

 

$INDENT

複数行入力のインデント文字列

f:id:vaaaaaanquish:20190825181526p:plain
$INDENT="...."
実装は、改行時やkeybindで指定の文字列を挿入しているだけ。

 

$LS_COLORS

bash, zsh同様、lsコマンドの出力の色指定。基本的には他のシェルのものが参考になるし、実装を見て色名でのstring指定もできる。

 

$XONSH_COLOR_STYLE

xonshのcolorを統一的に変更できるthemeのような機能。適応できるstyleは「xonfig styles」で確認できる。
最近はPTK_STYLE_OVERRIDESやPROMPT_TOOLKIT_COLOR_DEPTHで柔軟に変更できるようになったが、このtheme機能も簡単に良い感じにできるので良さはある。

 

$PTK_STYLE_OVERRIDES

補完の表示色や、背景色、プロンプトの色などあらゆる色を設定するところ。

f:id:vaaaaaanquish:20190825200508p:plain
$PTK_STYLE_OVERRIDES
設定できる項目とデフォルト値の対応は以下の通り

  • "completion-menu": "bg:ansigray ansiblack"
  • "completion-menu.completion": ""
  • "completion-menu.completion.current": "bg:ansibrightblack ansiwhite"
  • "scrollbar.background": "bg:ansibrightblack"
  • "scrollbar.arrow": "bg:ansiblack ansiwhite bold"
  • "scrollbar.button": "bg:ansiblack"
  • "auto-suggestion": "ansibrightblack"
  • "aborting": "ansibrightblack"

デフォルト値参考:https://github.com/laloch/xonsh/blob/master/xonsh/style_tools.py#L347

 

$PROMPT_TOOLKIT_COLOR_DEPTH

色の深度を決定できる変数。ドキュメントには書いていないが、コード上では以下の値が指定できる。

  • "DEPTH_1_BIT"
  • "MONOCHROME"
  • "DEPTH_4_BIT"
  • "ANSI_COLORS_ONLY"
  • "DEPTH_8_BIT"
  • "DEFAULT"
  • "DEPTH_24_BIT"
  • "TRUE_COLOR"

 

$MULTILINE_PROMPT

コマンドを改行した際に表示される幅寄せ用の文字。

f:id:vaaaaaanquish:20190825184844p:plain
$MULTILINE_PROMPT="@"

 

$TITLE

ターミナルのタイトルを設定する。実装自体は以下のように非常にシンプルで、これで設定できないターミナルエミュレータを使っている場合は無意味。

if ON_WINDOWS and "ANSICON" not in env:
    kernel32.SetConsoleTitleW(t)
else:
    with open(1, "wb", closefd=False) as f:
        f.write("\x1b]0;{0}\x07".format(t).encode())
        f.flush()

iTerm2でタブ名が変わる例

f:id:vaaaaaanquish:20190825234117p:plain
$TITLE="Hoge"

 

$XONSH_SHOW_TRACEBACK

Trueであれば、xonshコマンドがエラーの時にtracebackを全て表示する。長くなるのでデフォルトはFalse。

 

$XONSH_STDERR_*

stderrの出力に接頭語、接尾語を付ける。stderrの色を設定したい時などに使える。
stdio周りはxonsh独自にTeeクラスが実装されていて、そこを通るのでそちらも参考に。

$XONSH_STDERR_POSTFIX

stderrの出力に付く接頭語。

$XONSH_STDERR_PREFIX

stderrの出力に付く接尾語。

 

- コマンドやxonshスクリプトの動作、変数の扱いに関するもの -

$EXPAND_ENV_VARS

Trueであれば「$var」「${var}」「%var%」をサブプロセスモードで自動で展開する。
実装自体はbuiltins.__xonsh__.envの中にあれば変換するし、なければそのまま文字列で利用するとなっている。

> $EXPAND_ENV_VARS=True
> echo "my home is $HOME"
my home is /Users/shunsuke.kawai

> $EXPAND_ENV_VARS=False
> echo "my home is $HOME"
my home is $HOME

あまりFalseにする必要はないと思うが、format string等で不具合が出た時にはこちらの設定を確認すると良い。

 

$FOREIGN_ALIASES_OVERRIDE

zsh上でxonshコマンドでxonshを起動する等、外部シェルから呼び出されている場合にxonshrc等で設定するaliasを外部シェルのaliasに対してOverrideするかどうか。
扱いが難しく xonshが起動した時点でaliasesの読み込みは行われてしまうので、xonshrcやxonsh上のコマンドとして設定しても動作しない。
呼び出し元のbashzsh環境変数としてFOREIGN_ALIASES_OVERRIDEを設定しておく必要があるので注意。

 

$FOREIGN_ALIASES_SUPPRESS_SKIP_MESSAGE

外部シェルのaliasが既に存在する時、xonsh上でOverrideしようとすると警告を出すaliasコマンドが一部存在する。その時のメッセージを表示しないようにするためのフラグ。
基本的にはTrueにしておいて問題ないと思う。

 

$HISTCONTROL

履歴に保存するコマンドを選択する。
setの中にstringで"ignoreerr"を入れておくと、終了ステータスが0以外の正常終了しなかったコマンドを履歴に追加しない設定となる。
setの中にstringで"ignoreerr"を入れておくと、同じコマンドを連続で入力した時に保存しない設定となる。

 

$LANG

historyファイル等で利用する文字コード。あくまでxonsh内での文字コードなので、デフォルトのutf-8以外にする必要に迫られる場合は殆どないはず。

 

$SHELL_TYPE

シェルのコアライブラリを選択する。以下の4つのstringが指定できる。

  • "readline"
  • "prompt_toolkit"
  • "random": ランダムにptkかreadlineを起動する
  • "best": 環境に応じてptkかreadlineを起動する

基本的にというか"prompt_toolkit"でないとxonshの恩恵の殆どが受けられないので一択だと思う…readlineだとほぼbash…randomとかどういう時に使うんだろう…

 

$RAISE_SUBPROC_ERROR

Trueであれば、サブプロセス経由のコマンドが異常終了した場合にsubprocess.CalledProcessErrorを発生させる。デフォルトはFalse。
何も表示せず、ステータスコードを返さず終了するようなモジュールを使う時に役立つかも…?

 

$XONSH_APPEND_NEWLINE

Trueであれば、コマンドの結果の末尾に改行を追加する。
普通のシェルの動作として使う場合はTrueで、「xonsh -c hoge.py」のようにしてxonshを動かす場合に余計な改行が入ってしまうのでFalseにするためのもの。
デフォルト値にはXONSH_INTERACTIVEの値が入っており、基本的にインタラクティブシェルだったらTrueになるようになっている。

 

- キーバインドや操作に関するもの -

$IGNOREEOF

Trueであれば、「ctrl + d」でシェルを終了しない。
コマンド入力を待機し続けるループをbreakするかしないかという実装。
https://github.com/xonsh/xonsh/blob/master/xonsh/ptk2/shell.py#L186

 

$MOUSE_SUPPORT

Trueであれば、シェル上でマウス操作を許可する。
実装自体はprompt toolkitの機能で、ptkの方を見に行くと良い。以下のexampleを実行すれば分かるように、マウスクリックでカーソルを移動させたりできる。
https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/examples/prompts/mouse-support.py
エディタやセレクターっぽい機能を実装する時にも使えるかも。

 

- フラグや変数として利用するもの -

$LOADED_RC_FILES

xonshrcが読み込まれたかどうかのフラグをlistにしたもの。n番目が何のrcファイルなんだ…

 

$OLDPWD

一つ前に居たディレクトリの文字列が入る。起動時はcdするまでEnvがセットすらされてないので注意が必要。

 

$PATH

各シェルで言う所のいわゆるPATH。binとかスクリプトは個々に入っているディレクトリを見に行って実行する。
listなのでappendとかで追加する。bulitins.__xonsh__.env["PATH"]からもよく参照される。

 

$TERM

基本的にはユーザが設定する事はなく、ターミナルエミュレータが与える変数。以下のようにWindowsエミュレータの判定とかに使われる。
https://github.com/xonsh/xonsh/blob/master/xonsh/base_shell.py#L500

例えば以下のissueのように、Windows+AnacondaをWSL上から触っていてターミナルエミュレータが認識できない場合等に強制的に指定する場合に使う。
Unknown Terminal type using conda env (Windows 10 --> WSL) · Issue #2525 · xonsh/xonsh · GitHub
指定する時はドキュメントの通り、早い段階(xonsh起動時の引数、xonshrcの上部)で設定しないと他設定が読み込まれる。

 

$XONSH_INTERACTIVE

xonshがインタラクティブシェルとして起動しているか、「xonsh -c hoge.py」のようなスクリプト実行形式で起動しているかのフラグ。

 

$XONSH_LOGIN

xonshがログインシェルとして起動しているかのフラグ。

 

$XONSH_SOURCE

xonshスクリプト実行時、スクリプトへの絶対pathが入る変数。スクリプト実行時意外はセットされていない。

- おわりに -

大体これで1.0.0までカバーできるはず。

今回「XDG_CONFIG_HOME」「XONSH_CONFIG_DIR」「XONSH_HISTORY_FILE」など非推奨やもう機能していないものを抜いているので注意。PRを出して改修するのが良さそう。