Sphinx のメンテナになって一年が経過した話

クリスマスが過ぎてから始まる Sphinx アドベントカレンダーへようこそ (嘘)
Sphinx 大型連載第二夜です。

タイトルにある通り、Sphinx のメンテナ活動をして一年が過ぎたので、その話をします。
OSS 開発者のひとつのサンプルケースとして、何かの参考になれば幸いです。

Sphinx のメンテナ活動をはじめました

去年の 12月から Sphinx のメンテナ活動をはじめました。
Python のリリースマネージャ活動が忙しかったからか原作者の Georg の活動が鈍り、
また、その後を継いだ清水川さんも忙しくて身動きが取れなくなっていたことから、
コミット権をもらっていたことだし、パートタイムで手伝うかと思ったことがきっかけでした。

以前からコミット権は持っていたものの、一切メンテナとしての活動をしていなかったので、
徐々にチケットが溜まっていく様子に後ろめたくなったのかもしれません。

増え続けるチケットの戦い

Sphinx は 2015年1月はじめに bitbucket から github にプロジェクトが移動しました。
移動した時点では 1662件だったイシューは、現在 3300件弱あります。
bitbucket 時代は開発開始から 6年間分、そして github 時代は約 2年間分なので、イシューが増加するペースはかなり増していることがわかります。
f:id:tk0miya:20161225221155p:plain

また、github 移行後の増加グラフを見ると、2015年代は 40件/月程度だったのが 2016年代は 80件/月程度に増加していることもわかります*1
f:id:tk0miya:20161225221208p:plain


対する Sphinx のメンテナの人数は、この間あまり変化がありません。
一人ないしは二人。これがアクティブな Sphinx のメンテナの人数です。
2015年末に自分が、そして 2016年に入ってから TeX 使いがひとり、メンテナとして活動をしていますが、メンテナの人数はかぎられているため、絶えず増え続けるイシューに対して常に後手に回っています。
現時点では、増加するペースのほうがやや大きく、チケットが増えていく傾向にあります。

むしろ、難しいイシューはそのまま積まれており、かんたんなイシューから解決していくことが多いので、件数だけでなく濃度も徐々に高まっているという印象があります。

メンテナ活動でやっていること

主にこんなことをやっています。

  • バグ修正
  • 機能追加
  • ドキュメント更新
  • PR レビュー
  • イシュー対応
  • 環境改善 (テスト、CI、その他)
  • 定期リリース
  • メジャーリリース
  • リリース計画立案
  • 開発者メーリングリストでのフォローアップ
  • ユーザメーリングリストでのフォローアップ (Q&A などなど)
  • 関連ライブラリ(docutils)へのパッチ投稿

他のメンテナが機能停止していることもあって、ここ一年は大体自分ひとりで動いていました*2
我ながらよく頑張った。

その他、Sphinx-Users.jp のコミュニティ活動としてこんなこともやってました。

  • Sphinx+翻訳 Hack-a-thon の開催
  • Software Design 誌での連載 (執筆は 4回ほど。レビューにはなるべく協力)
  • slack で雑談
  • 勢いに任せて Sphinx 拡張の開発 (雑誌の題材だったり、slack でのリクエストだったり)

なんというか、本当に Sphinx 漬けな生活を送ってますね。

メンテナという責任との付き合い方

挙げていくとイシューもやることも山積みのように見えますが、オープンソースという場なので、
あまり責任とかは気にせずに付き合っています。
外から見るとずーっと活動し続けているようにも見えるそうですが、
仕事が忙しいときはサボっていたり、旅行に行ったり、飲みに行ったりも適度にしています。
あとひたすらプレミアリーグ見てます。

継続的に活動し続けていくことが大事だという大義名分のもとに、
あくまでマイペースに、ときどき放置しながらも、コードを書いたりイシューに返事をしたりしてます。
ということで、あまりに根を詰めて燃え尽きたりせずに、一年を過ごせたのは良かったと思っています。

引き続き、飽きるまでマイペースに付き合っていくつもりです。

メンテナしていて得すること

僕は個人的にもお仕事でも Sphinx を使っていないので、コードそのものから得られるメリットはありません。
blockdiag を作っていたころもそうですが、どうも自分の使わないコードをいじるのが好きなようです。

強いてあげるとすれば

  • 英語の読み書き能力
  • Python 能力 (docutils パワー?)
  • ハックするネタ
  • ドキュメントネタへの興味 (出版とかフォーマットとか)
  • メンテナとしての自意識/自尊心の拡大

あたりが得したことでしょうか。

特に、英語については反射能力がついたと強く感じています。
飛んできたイシューを片っ端から読んで、コメント書いてを繰り返しているので
英語に対するハードルがかなり下がって、フットワークが軽くなっています。
語彙や単語などはあまり変わっていないのですが、これもひとつのレベルアップと言ってもよいはずです。

あとは、ハックネタが提供されたことで常に Sphinx のことばかり考えるようになってますね。
何者かになりきれない自分にはいい素材が提供されたと思っています。

メンテナをして損したこと

とにかく時間が吸い取られます。
blockdiag や testing.mysqld など、他にもメンテナンスすべきパッケージがあるはずなのですが、
そっちに時間を割くことができていません。
Sphinx 以外の他のこともやってみたい、というぼんやりとした考えもあるのですが、
イシューの物量に押し流されているところもあります。

メンテナに必要な知識

先程挙げたように、やるべきことは山積みで、そして広範囲です。
コードをいじるにしても、docutils や Python そのものの知識もそうですが
出力フォーマット(HTML や LaTeXEPUB)や Python のコード解析、フレームワーク化など、
Sphinx が扱っている分野そのものがとても広いので「この知識を持っていればよい」というものではありません。
むしろそんなスーパーマンはいないので、みんなの力を結集して解決していきたいところです。

ですので、なにかひとつでも継続的に関わることができるのであれば、
胸を張って手伝っていけばよいと思います。

最近、usaturn 氏がイシューのトリアージに参加するようになりました。

イシューに溢れかえった Sphinx では、再現確認や一時切り分け、単なる質問の対処などであっても
やるべき作業が減るのでとても助かります。

メンテナであれば〜〜ができなくてはならない、なんて自分でハードルを上げずに
やれることを手伝っていくスタンスから始めるのが良いと思います。

ひとつ付け加えるとするなら、英語の能力があると心理的なハードルが下がります。
でも、先日 Google 翻訳がパワーアップをして、かなりハードルを下げてくれたので
そんなに気にしなくても良いかもしれません。

OSS メンテナのモチベーション/キャリアプラン

Sphinx のメンテナとして活動していることは誇らしくもあるのですが、
一方で自分のスキルアップ、キャリアアップには役立っていないと感じている面もあります。

Web フレームワークやインフラなど仕事のメインとなるツールではないこと、
自分の業務には(いまは)ほとんど関係のないツールであること、
ドキュメンテーションというニッチな(あまり人気のない分野の)ツールであることなど、
メンテナ活動を続けることが自分のキャリアにあまり貢献をしていないと感じることがあり、
たまに隣の芝生が青く見えることがあります。

この記事は、そうした自分を写した鏡を見たような気分になりました。
A Million Hello Worlds - steps to phantasien

また、ドキュメンテーションツールとしても、markdown 全盛のこの世の中、
Sphinx に将来があるのかどうか、自分自身あまり信じられていないこともあって
もやーっとしている思いがないわけでもありません
*3






では、モチベーションってなんだろう、と考えたのがこちら:

パソコン通信の時代から、誰かに使ってもらえるツールを作ることに憧れていたので、
それを考えるといまはそれを満たしているので満足してるんじゃないか、と自己分析しています。

結論:僕の OSS 活動はキャリアと関係なかった。



まとめ

飽きるまではもうしばらくはやっていくつもりですが、一人の力だけではどうにもならないので、
Sphinx プロジェクトでは手伝ってくれる人を募集しています。

興味がある人は声を掛けてください。

そういえば、来年(2017年)は Sphinx-Users.jp の会長をふたたび務めることになりました。
いまも十分に活動しているので、基本的にはいまの状態を継続できるよう、ほどよくやっていこうと思います。
でも、なにか面白そうなネタがあれば顔を出していこうと思うので、ネタがあれば振ってください。

*1:このうち幾つかは開発が活発化したことによって、開発者自身がイシューを作ったり、PR を投げ合ったりしたものが含まれます。ですので、純粋に倍増ではありません

*2:途中から参画した TeX 使いの人は TeX に関するイシュー、バグ修正まわりを中心に活動していました。なので、厳密にはこのうち一部はふたりでやってます。

*3:かといって markdown は仕様が明確ではない上に方言が多いので、とても好きにはなれないのですが

Sphinx に mypy の type annotation を導入した話

今年はアドベントカレンダーにも参加していないし、こたつに入ってだらだら過ごそうかと思っていたら、なんか書けと煽られました。年末ですね。

何を書こうか思考をめぐらした結果、mypy を Sphinx に導入した話でも書くことにします。
mypy については @t2y の 紹介記事翻訳記事が非常に参考になりました。
ですので、この記事は @t2y へのアンサーソングです。

なお、「導入した話」と名付けてはみたものの、まだ 100% 対応したというわけではないので、試行錯誤の様子と愚痴を書き留めていきます。

Sphinx に type annotation をつけてみた

Sphinx では現在、 master ブランチに対して type annotation がつけられています。
一方、stable ブランチ、つまり現行リリース版である 1.5 系には追加されていません。
これが type annotation を追加する PR です。

github.com

Sphinx はそれなりの規模のコードベースがあるため、差分もかなり大きいものになっています。
mypy について調べつつ、実験しつつ、仕事をこなしつつ、一週間ぐらいで片付けた記憶があります。

本来であれば絶賛メンテナンス中の stable ブランチに入れた方が運用しやすそうだったのですが、
1.5 のリリースに向けた feature freeze 中で、こんなにでかい差分を取り込む勇気がなかったので
日和って master ブランチに突っ込むことにしました。

その結果、

  1. stable ブランチでバグ修正した
  2. master ブランチに取り込む
  3. アノテーションを(必要に応じて)調整する

というステップを踏むことになりました。
できれば安定版にアノテーションを書くと良いでしょう。

導入するときに試行錯誤したあれこれ

アノテーションの書き方

SphinxPython 2.7 と 3.4 以降をサポートしています。
そのため、導入するときはどういう書き方がよいのか、あれこれ試しました。

本来は Python 3 で導入された関数アノテーションを使って型を宣言していくとよいのでしょうが、
Python 2.7 では文法エラーになってしまいます。

ですので、Python2 でも利用できるようコメントベースのアノテーションを利用することにしました。

def docname_join(basedocname, docname):
    # type: (unicode, unicode) -> unicode
    return posixpath.normpath(
        posixpath.join('/' + basedocname, '..', docname))[1:]

コメントベースのアノテーションは関数の定義の直後(docstring の前)、変数定義の直後に # type: 宣言を書きます。
詳しくは mypy のドキュメントを読んでください。

mypy の設定

Sphinx での設定は mypy.ini にまとめました。
どこかの記事で見かけた設定をそのまま持ってきたものです。
一点、意識的に設定したのは python_version = 2.7 です。

当初、python 3.5 モードで動かそうと思っていたのですが、妙な型エラーがでてしまったのでとても追いかける気力になれなかったことに起因しています。

import codecs

filename = u'example.dat'
with codecs.open(filename, 'r', encoding='utf-8') as fd:
    pass
$ mypy --python-version 2.7 example.py
example.py:4: error: Argument 1 to "open" has incompatible type "unicode"; expected "str"
$ mypy --python-version 3.5 example.py
example.py:4: error: "StreamReaderWriter" has no attribute "__enter__"
example.py:4: error: "StreamReaderWriter" has no attribute "__exit__"
from docutils import nodes

root = nodes.Node()
root += nodes.Text()
$ mypy -s --python-version 2.7 example.py
# 何も言われない
$ mypy -s --python-version 3.5 example.py
example.py:3: error: "module" has no attribute "Node"
example.py:4: error: "module" has no attribute "Text"

前者は標準ライブラリの型を定義している typeshed の定義不足っぽいのですが、後者はさっぱり謎です。
深追いはしなかったのですが、いきなり心が折れそうになりました。

str と unicode

みんなだいすき str と unicode の違いとも格闘しました。

$ cat example.py
from six import text_type

var1 = 'abc'  # type: str
var2 = text_type(var1)  # type: str
var3 = u'def'  # type: str
$ mypy example.py
example.py:4: error: Incompatible types in assignment (expression has type "unicode", variable has type "str")
example.py:5: error: Incompatible types in assignment (expression has type "unicode", variable has type "str")

unicode リテラルに str 型だというヒントを与えると蹴り飛ばされます。
(オプションは省略されていますが、ここからは前述の mypy.ini を使っています。記憶が確かなら)

他にも str から作成された正規表現unicode から作成された正規表現で、それぞれ引数の型が微妙に違うというボディブローをくらうことがあります。

import re

str_pat = re.compile('abc')
str_pat.match('abc')
str_pat.match(u'abc')

unicode_pat = re.compile(u'abc')
unicode_pat.match('abc')
unicode_pat.match(u'abc')
$ mypy example.py
example.py:5: error: Argument 1 to "match" of "Pattern" has incompatible type "unicode"; expected "str"

救いはありません。
正規表現にかぎらず、あちこちの関数で unicode を受け入れられないという警告が出ます。
なお、型の世界では警告が出ますが、スクリプトを実行するともちろんちゃんと動作します。
type annotation というきっちりとした世界にも、本音と建前という人間くささが潜んでいると思うと、こころが暖かくなりますね。

Python 3.5 モードで動かすと str に統一されるので、この苦しみからは解放されるはずなのですが、先ほど書いたとおり 3.5 は 3.5 で茨の道感があります。

救いの神 type: ignore

既存の巨大なアプリケーションに対してアノテーションを書いていくのは、非常に根気が必要です。
mypy に不慣れだというのを棚に上げて前に進まないと、徒労感が積み重なっていきます。
そんなときに、人類の救世主として登場するのが # type: ignore アノテーションです。

このアノテーションの手にかかれば、あんなに解決に苦労した警告も、たちどころに解決します。

$ cat example.py
import re

str_pat = re.compile('abc')
str_pat.match('abc')
str_pat.match(u'abc')  # type: ignore

unicode_pat = re.compile(u'abc')
unicode_pat.match('abc')
unicode_pat.match(u'abc')
$ mypy example.py
# 何も言われない

やりましたね。ひとまずカバー率を上げるために、心を鬼にして先に進みましょう。
ちなみに Sphinx はまだカバー率が 100% に達していないので、そのまま塩漬けになっています。

List と Tuple

話はちょっと代わりますが、「ユーザ名、メールアドレス、得点」というデータがあるとき、あなたならどういうデータ構造を作りますか?
リストを使いますか? タプル? それともクラスを定義する?
また、イベントを進めると得点が加算されるような場合はどうでしょう。

Sphinx では、いくつかの箇所で簡単なデータのペア/トリオを表現するのにリスト(配列)やタプルを使っていました。また、途中でデータの内容が変わるようなところにはリストが使われていました。

さて、こうったデータ構造を使っている場合、type annotation はおもむろに右ストレートを放ってきます。
mypy の type annotation では

  • リストは無限個のデータ列。データ型をひとつだけ指定する。
  • タプルは有限個のデータ列。データ型はそれぞれ指定する。

という前提があるため、さきほど例に上げた「ユーザ名、メールアドレス、得点」というデータはリストでは表現しづらいもののひとつです。

こうした場合の回避策のひとつに Union があります。
Union は複合型を表します。

# リストの場合
user1 = [username, email, score]  # type: List[Union[unicode, int]]
# タプルの場合
user2 = (username, email, score)  # type: Tuple[unicode, unicode, int]

Union を使うと「文字列、文字列、数値のリスト」ではなく「文字列か数値のいずれかのリスト」として表現できます。

Union 最高!と言いたいところですが、これを書いた瞬間に型情報が "あいまい" になる欠点があります。
user1[0] も user1[1] も user1[2] も「文字列か数値のいずれか」のデータになります。
そのため、たとえば unicode を受け取るような関数に指定すると型エラーが発生します。

$ cat example.py
from typing import Union

def hello(name):
    # type: (unicode) -> None
    print("Hello %s." % name)

user = ["tk0miya", "tk0miya@example.com", 100]  # type: List[Union[unicode, int]]
hello(user[0])
$ mypy example.py
example.py:9: error: Argument 1 to "hello" has incompatible type "Union[unicode, int]"; expected "unicode"

さて、果たして Union は最高でしょうか?

個人的な結論としては、Union は容量用法をよく守ってインターフェースに対してのみ使うべきで、
データに対しては使うべきではないです。

List と Tuple と Dict と Set と Sequence と Iterable と Iterator と Generator 。

データ集合を扱う、いわゆるコンテナ的なデータをあらわす型はいくつもあります。
List、Tuple、Dict、Set というデータ型そのものと、Sequence と Iterable という振る舞いを表す抽象型、そして Iterator と Generator という動的なデータ集合の操作インターフェースが該当します。
データに対してアノテーションする際はこの違いを意識する必要はないのですが、関数に対してアノテーションを書く場合はこの微妙な違いを意識する必要があります。

例えば、連続したデータを受け取る場合は Iteratable を指定します。
Iterable は List, Tuple, Dict, Set, Iterator, Generator を受け取ることができます。

一方、インデックス指定でのアクセス可能な場合は Sequence を使います。
Sequence には List, Tuple, Dict が該当します。

関数インターフェースには抽象型を指定しておくと、その関数を使う側のコードの自由度が上がるので、
引数のアノテーションにはなるべく抽象型を使って指定すると良いでしょう。
(返り値は型が定まるはずなので、具体的な型を指定する方がよさそうです)

アノテーションで実装の歪みに気づく

ちなみに、Sphinx にはビルド対象のファイル名の集合として docnames というデータがあります。
この docnames 、あるところでは List として、あるところでは Set として、またあるところでは Tuple としてデータを定義したり、操作したりしています。
そのため、これらの整合性を取るのが非常に難しく、具体的なデータ型をアノテーションしても怒られ、また抽象的な型を指定しても怒られるという、ひどい状況になっています。
これはアノテーションだけでは解決できず、コードを直す必要がある貯め、現時点でも型エラーが出続けたままになっています。

このように、型アノテーションをつけていくと、実装の歪みに気づくことができます。
辛い現実ですが、涙を拭いて立ち上がりましょう。
助けてください。

ダウンキャスト

いまだに書き方がわかっていないもののひとつにダウンキャストがあります。

$ cat example.py
class Base(object): pass

class Foo(Base): pass

class Bar(Base):
    def say(self):
        print("Hello!")

items = {}  # type: Dict[unicode, Base]
items['foo'] = Foo()
items['bar'] = Bar()

items['bar'].say()
$ mypy example.py
example.py:14: error: "Base" has no attribute "say"

Sphinx では、あちこちにこうした「共通の親クラスを返す」インターフェースが定義されています。
すべてのデータに対して共通の処理を書く場合は、これで特に構わないのですが、
上記のように特定のインスタンスの処理を呼び出したい場合は、これをきれいに書く術が見つかっていません。

ドキュメントによると Cast が使えそうな気がしています。いずれ時間を取って試してみる予定です。

type annotation と flake8 の衝突

mypy はアノテーションコメントの途中での改行を認めていないため、一行に書かねばなりません。
そのため、複雑な構造をしたデータや、多くの引数を取る関数の場合、アノテーションコメントが長くなります。
その結果、flake8 と衝突します。

結論としては、NOQA コメントを使ってくさいものに蓋をします。

        self.versionchanges = {}    # type: Dict[unicode, List[Tuple[unicode, unicode, int, unicode, unicode, unicode]]]  # NOQA

定義があまりに長い場合は、それそのものがコードのあやしいにおいのひとつかもしれません。
クラスや namedtuple を定義すると、型的にも実装的にもシンプルになる可能性があるので、
記述が長い場合は実装を見直すと良いかもしれません。
また、alias を使って型に名前をつけるのも良さそうです。

循環 import 問題

ユーザ定義の型をアノテーションするには、そのクラスが import されている必要があります。
import しないと undefined だと、肘打ちされます。

$ cat example.py
obj = None  # type: Sphinx
$ mypy example.py
example.py:1: error: Name 'Sphinx' is not defined

しかし、型アノテーションするために import を繰り返していくと、そのうち循環 import が発生してしまいます。
アノテーションを書くためにプログラムが壊れてしまうという本末転倒なできごとです。

これを回避するために Sphinx では、次のように回避をしています。
mypy は静的なコード解析をするので、実際に import をしなくともアノテーションできるという特性を生かして、 "import したふり" をします。

if False:
    # For type annotation
    from typing import Any, Callable, IO, Iterable, Iterator, Tuple, Type, Union  # NOQA
    from docutils.parsers import Parser  # NOQA
    from docutils.transform import Transform  # NOQA
    from sphinx.builders import Builder  # NOQA
    from sphinx.domains import Domain  # NOQA

また、flake8 先生に踏みつけられないように NOQA をするという涙ぐましい努力も同時に行います。

ad-hoc 過ぎますが、いずれ mypy が改善されることを期待して、積極的に目をそらして行きていきましょう。

mypy を実行する際はコード全体に対して実行する

アノテーションを書き始めたとき、最初はサブディレクトリに対して mypy を実行していました。

$ mypy sphinx/util
...

そして、あるサブディレクトリの型エラーがなくなったら、次のディレクトリに進むということを繰り返しました。
そうして、ファイル単位、ディレクトリ単位でアノテーションをつけていったのですが、
ひととおりアノテーションをつけ終わったあと、コード全体に対して mypy を実行すると型エラーが検出されました。

$ mypy sphinx
# => sphinx/util で型エラー!

これは、mypy の -s オプション (--silent-imports) を使って import しているコードの型チェックをスキップしていることによるものです。
コード全体に対して mypy を実行すると、これまで参照していなかったコードも使って型チェックをおこなうようになるため、一度アノテーションをつけた部分についても型エラーが検出されることがあります。

mypy はやや実行に時間がかかるため、アノテーションのつけはじめの段階では、
ディレクトリ単位などで書いていき、型エラーを減らしていくアプローチがよいと思いますが、
ある段階で全体に対して実行すると良いでしょう。

ある程度件数を減らしていったはずなのに、あるモジュールのアノテーションを書くと、また揺り戻しのように件数が増えたりして、心が折れそうになりますが涙をこらえてがんばりましょう。

まとめ

Sphinxアノテーションを導入しようとして試行錯誤して苦しんだ記録を残しました。
まだ型エラーは残っていますし、すべてのデータ、インターフェースにアノテーションしたわけではありませんが、今のところこんな感じになっています。

$ grep -r type: sphinx | wc -l
    3166
$ grep -r 'type: ignore' sphinx | wc -l
     397

この記事では、試行錯誤した内容をひたすら書き残したので、つらいだけで、あまり便利そうには見えないのですが、
アノテーションを加えることで、実装の見直しになったり、バグに気づくきっかけとなりました。

また、こうして意識的に型を書いてみることで、確かに型付けは便利であることの気づきにもなりました。
Sphinx は既に巨大なコードベースがあるため、他の言語に引っ越すことは容易ではないので、
こうしてアノテーションでヒントを与えるアプローチは非常に役に立つと感じています。

まだ型エラーが存在するため CI に組み込むところまではたどり着いていないのですが、
引き続き改善を続けていけると良いと考えています。


この記事がこれから mypy を試す人の助けになることを祈って。

参考:
ちょうど、先日も mypy の導入記事を見かけました。見てない方は、一緒に見るといいと思います。
mypyやっていくぞ - Qiita

2014-15年を振り返る / 2016年の抱負

あけましておめでとうございます。2年ぶりの抱負エントリです。
去年は正月の時期を逃してしまい、仕方がないから旧正月に書くことにするか!と思っていたら仕事が忙しくて完全にタイミングを逸してしまいました。

2014年の抱負 として挙げたのは

  • blockdiag のバグ/タスクを減らす
  • なにか使えるツールをリリースする (少なくとも一本以上)
  • 技術書を読み続ける
  • 新しい言語でなにかツールを書く
  • コードを書く時間を維持する
  • 環境整備をしよう
  • あたらしいツール/フレームワーク/サービスに触る
  • 体重を落とす (目標 -3kg)

の8個でした。

この 2年間は遊んで過ごしていたこともあって、あまり成果らしい成果はでていないので
振り返るほどでもないのですが、反省を兼ねてざっと振り返ります。

blockdiag のバグ/タスクを減らす:✕

完全に放置状態ですね。2年間まるまる手を付けていません。

なにか使えるツールをリリースする (少なくとも一本以上):△

調べてみると

  • 2014
    • sphinxcontrib-cacoo
    • sphinxcontrib-astah
    • sphinxcontrib-visio
    • numfig feature (Sphinx)
  • 2015
    • flake8-config
    • sphinxcontrib-imagehelper
    • testing.elasticsearch
    • sphinxcontrib-apiblueprint
    • sphinxcontrib-markdown

とパッケージを作っていたみたい。
どれもちょっとしたツールで、使えるツールかと言われると悩ましいですね。

そういえば、Sphinx の numfig を作った流れでコミッターになりました。
あまり活動していなかったのですが、年末からぱたぱたと活動を再開しています。

あと、ずっと git 嫌いだったのですが bitbucket の凋落っぷりをみて、あきらめて git + github にスイッチしました。

技術書を読み続ける:◯

安定して新宿 Book-a-thon を開催しています。

読んだのは以下の本らしい。

読んだ中では日本語組版処理の要件が本当におもしろかった。
普段慣れ親しんでいる書籍について、考え方やルールを理解できてとてもいい資料です。
(厳密には本ではないけど、まあいいじゃない)

あと、読む方ではなく、書く方もちょっとやりました。
Software DesignSphinx の連載記事に 2本ほど寄稿しました。

新しい言語でなにかツールを書く:✕

golang を使ってみようとちょっと触ってみたけど、完成していないし、あまり身についた気がしませんね。

コードを書く時間を維持する:✕

サボってました。

環境整備をしよう:△ or ✕

ローカルにあれこれ入れたくないとずっと嫌がっていた homebrew を入れることにしたのは
ビッグチェンジだったかもしれません。

あたらしいツール/フレームワーク/サービスに触る:✕

あまり触っていません。
数少ないいじってみたサービスのうち、手に馴染んでるのは slack と trello ぐらいでしょうか。

体重を落とす (目標 -3kg):◯

がくっと落ちました。-5kg ぐらい。
完全に ingress のおかげです。
最近は忙しくて ingress をやる時間がとれないのですが、その後安定しているのでリバウンドはなさそうな感じ。

まとめ / 2016年の抱負

一昨年の抱負の成果は ○ 2つ、△ 2つ、× 4つでした。
ダメダメですね。


というわけで、さっくり今年の抱負。

  • Sphinx のバグ/タスクを減らす
    • しばらくコミッタ活動しようと思います
  • blockdiag のバグ/タスクを減らす
    • 今年こそ…
  • なにか使えるツールをリリースする (少なくとも一本以上)
    • API blueprint や sphinxcontrib-markdown の v1.0 リリースをしたいところ。
  • 技術書を読み続ける
    • 引き続きもの
  • コードを書く時間を維持する
    • 最近さぼっているので引き続き。
  • 体重維持

tox で環境変数をセットする方法

先日リリースされた tox-2.0.0 から、コマンドを実行する際に環境変数がフィルタされるようになりました。

(new) introduce environment variable isolation: tox now only passes the PATH and PIP_INDEX_URL variable from the tox invocation environment to the test environment and on Windows also SYSTEMROOT, PATHEXT, TEMP and TMP whereas on unix additionally TMPDIR is passed.
http://tox.readthedocs.org/en/latest/changelog.html

いままでテストの制御に環境変数を使ったりすることがありましたが、
tox-2.0.0 ではそのままでは動作しないことになりました。

$ RUN_ALL_TESTS=1 tox   # すべてのテストを実行する
...
$ SKIP_FOOBAR_TEST=1 tox   # あるテストをスキップする
...

ここで登場したのが ``passenv`` という設定です。
参照する環境変数を tox.ini に列挙します。

[testenv]
passenv= RUN_ALL_TESTS SKIP_FOOBAR_TEST

* などを用いた glob マッチにも対応しているそうです。
なお、複数列挙する場合はスペース区切りで並べる必要があります。
他の設定項目のように空行区切りをしてしまうと正しく反映されません。

余談

Travis CI 上で coveralls を実行するとトークンの設定とかそういったものが不要で簡単なのですが、
coveralls コマンドは環境変数を見て情報を得ているらしく、tox 経由で実行する場合は tox.ini に

[testenv]
passenv= TRAVIS*

という設定をしておかないと

You have to provide either repo_token in .coveralls.yml, or launch via Travis

と言われてカバレッジデータをアップロードしてくれないことに気づきました。
oh...

古い pip で --use-wheel を指定してパッケージを入れるとコマンドが生成されないことがある件。

古い pip には wheel まわりの扱いに問題があるようで、
--use-wheel オプションを指定してパッケージを入れたときに
コマンドが生成されないことがあるようです。

具体的には drone.io の環境に入っている pip-1.4.1 では問題が発生しました。
そのため、

pip install --use-mirrors --upgrade wheel
pip install --use-mirrors --use-wheel detox

detox

のようなスクリプトを実行すると

/home/ubuntu/.build.sh: line 52: detox: command not found

などと言われてしまいます。

解決策は pip を新しくすることなので、wheel を入れるときに pip も新しくするとよいでしょう。

pip install --use-mirrors --upgrade pip wheel

ちなみに新しい pip を --use-wheel でアップデートすると、
pip コマンドが使えなくなるというオチが待っているので、
pip の更新は wheel を使わずに行いましょう。

sphinxcontrib-googleanalytics-0.1 と sphinx-1.3 の組み合わせで Sphinx が落ちる件。

表題の組み合わせだと、こんなエラーを吐いて Sphinx さんが死ぬ。

Traceback (most recent call last):
File "/Users/tkomiya/work/blockdiag.com/lib/python2.7/site-packages/sphinx/cmdline.py", line 244, in main
opts.warningiserror, opts.tags, opts.verbosity, opts.jobs)
File "/Users/tkomiya/work/blockdiag.com/lib/python2.7/site-packages/sphinx/application.py", line 143, in __init__
self.setup_extension(extension)
File "/Users/tkomiya/work/blockdiag.com/lib/python2.7/site-packages/sphinx/application.py", line 449, in setup_extension
if not ext_meta.get('version', None):
AttributeError: 'Sphinx' object has no attribute 'get'

Sphinx-1.3 から、Sphinx 拡張の `setup()` 関数は「拡張モジュールのメタデータ(dict)」か
「何も返さない」ことを期待しているのだけど、
sphinxcontrib-googleanalytics はなぜか Sphinx オブジェクトを返しているので
こんなエラーを吐くことになる。
Sphinx-1.3 以前は返り値はとくに見ていなかったので何も起きなかったのだけど、
バージョンアップですれ違いが起きてしまった。

根本的な解決は sphinxcontrib-googleanalytics が Sphinx-1.3 に対応して、
いい感じの返り値を返すように修正されることなんだけど、
このモジュール、長らくメンテされていないのでいつになるのかとても怪しい…。

というわけで、conf.py でさっくり monkey patch を当てることにした。

# adhoc: Fix sphinxcontrib-googleanalytics does not work with Sphinx-1.3
from sphinxcontrib import googleanalytics
original_setup = googleanalytics.setup

googleanalytics.setup = lambda app: original_setup(app) and None

とてもひどい。けど動く。でもひどい。

しかたがないので、あとで Sphinx 側を直すことにしよう。

rails-erd でいい感じの ER図を出す

諸事情で ER図をとても作りたくなることってよくありますよね。

rails で開発しているプロジェクトだと rails-erd というツールを使うと
モデル定義から簡単に ER図っぽいものを生成できるのですが、
なにもオプション指定をしないと複雑で読みづらいものができてしまうことがあります。

  • Form オブジェクトが表示されてしまう
  • 使っている gem のクラス (audit など) が表示されてしまう
  • created_by や updated_by など、users モデルの参照がごちゃごちゃしてしまう
  • has_many :through の関係が点線で表示されている
  • カラムの表示がアルファベット順になっている

また、表示されている内容をよく見ると id や created_at などの標準カラムや、
foreign_key が貼られている参照カラムなども表示されていません。

このあたり、ちょっと整理しないと **ER図** としてはちょっと読みづらいですよね。

というわけで、ちょっと手を入れて見やすい図にする工夫をしてみました。

bundle exec rake erd filetype=dot attributes=foreign_keys,primary_keys,timestamps,inheritance,content indirect=false sort=false
ruby -e '\
    dot = open("erd.dot").read
    dot.gsub!(/^\s*".*?::.*?;\n/m, "")  # remove Ruby classes (Form, Audit, ...)
    dot.gsub!(/^.*? -> ".*?::.*?;\n/, "")  # remove relation to Ruby classes
    dot.gsub!(/^(\s*m_User -> .*?;\n)\1{2}/m, "")  # remove created/updated/deleted_by releations
    print dot
' > output.dot
dot -Tpdf -o erd.pdf output.dot
dot -Tpng -o erd.png output.dot

工夫してるところは以下のとおりです。

  • rails_erd
    • 出力内容を加工するために画像ではなく dot 形式で出力
    • attributes オプションを指定して、すべてのカラムを表示させる
    • indirect=False を指定して、has_many :throught の点線を表示しない
    • sort=False を指定して、カラムをソートさせない
  • フィルタ処理
    • Form オブジェクトや gem など、Ruby のクラスっぽいもの (:: を含むもの)を除外
    • User モデルへの参照は読みづらいので削除


大したことはやってないのですが、あとで忘れそうなので備忘として。

※ 諸事情でお手伝いしたやつなので、具体的なサンプルはお見せできません。あしからず。