nikkie-ftnextの日記

イベントレポートや読書メモを発信

VS Code拡張ハンズオン基礎編のテキストの中の、VSCode APIを駆使してドキュメントに対してCodeLensを作成する実装を理解する

はじめに

「しらなかった〜1、nikkieです。

以前、Markdownの「歩夢」を「歩夢🎀」に置き換えるという、どうかしているVS Code拡張を自作しました。

この実装では、VSCode Conference Japan 2021 ハンズオンの基礎編のテキストを大変参考にしています。
先日もアウトプットしましたが、もう一歩、基礎編のテキストの実装の理解が進んだのでアウトプットします。

目次

VSCode APIを駆使してCodeLensを作成する(「ドキュメントを編集しよう」より)

CodeLensProviderprovideCodeLensesメソッドの実装を見ています。
正規表現を使ってCodeLensを設定する実装はすでに理解しています(先日のアウトプット)。
今回は正規表現にマッチした文字列からCodeLensを作る処理を深堀ります。
while文のところですね。

    while ((matches = regex.exec(text)) !== null) {
      // 見出しが見つかった行を抽出し、
      // その範囲をレンジとして切り出す
      const line = document.lineAt(document.positionAt(matches.index).line);
      const indexOf = line.text.indexOf(matches[0]);
      const position = new vscode.Position(line.lineNumber, indexOf);
      const range = document.getWordRangeAtPosition(
        position,
        new RegExp(titleRegex)
      );
      if (range) {
        // rangeに対するCodeLensを作り、配列codeLensesに追加する(省略)
        );
      }
    }

登場するVS Code API

while文の後からif文の前までは、以下のAPIを利用しています:

変数matchesRegExp.execメソッドの返り値)

    while ((matches = regex.exec(text)) !== null) {

  • matches.index0 から始める一致した文字列の位置
  • matches[0]文字が一致した部分の文字列全体

VSCode APIを駆使した実装読み解き

lineの取得

const line = document.lineAt(document.positionAt(matches.index).line);

入り組んでいますが、1つずつ読み解いていきましょう。

document.positionAt(matches.index)正規表現オブジェクト(titleRegex)に一致した文字が始まるPositionですね。
titleRegexに一致する文字がTextDocumentでは何行目何文字目かを取得します。
Positionlineプロパティで、何行目かだけ取得しています。

TextDocumentにおける行数が分かったので、lineAtメソッドでその行をTextLineとして取得しました。

lineについてindexOf取得

const indexOf = line.text.indexOf(matches[0]);

TextLinetextプロパティは、行を表す文字列のようです2
JavaScriptのString(文字列)のindexOfメソッドを使って、titleRegexに一致した文字列(matches[0])が最初に表れたインデックスを得ます。

その行の中の何文字目から正規表現と一致する文字列かを知ったわけですね。

positionを作る

const position = new vscode.Position(line.lineNumber, indexOf);

lineの中で正規表現と一致するインデックスの始まり(indexOf)が分かったので、Positionを初期化します。

rangeを取得する

      const range = document.getWordRangeAtPosition(
        position,
        new RegExp(titleRegex)
      );

作ったpositionを使って、position以降でtitleRegex一致する範囲を求めます。

CodeLensの作成は、求めたrangeにコマンドを設定します。
今回コマンドはrangeによらず共通(markdown-date.addDate)ですから、このrangeにコマンドを設定してCodeLensを作ります。

読み解いた結果、どうかしている拡張のバグも理解

「歩夢」を「歩夢🎀」に置き換える自作拡張ですが、「歩夢歩夢」という文字列には以下の動きをします3

現状では、「歩夢歩夢」という行には対応できていません。
コードレンズは2つ表示されるのですが、どちらをクリックしても先の歩夢に🎀追加となりました。

CodeLensが2つ表示されるのは、RegExpインスタンスexecメソッドでどちらの歩夢もマッチしているからですね。
今回理解が深まったことで、バグの動きになる理由も分かりました。

「歩夢歩夢」なので、取得されるlineとしては2回とも同じです。
lineを取得した後、line.text.indexOf(matches[0])indexOfを取得する箇所があります。
変数を展開すると以下のコードです。

const indexOf = "歩夢歩夢".indexOf("歩夢");

これって、2回とも同じ値0)になりますよね。
indexOfメソッドは検索開始位置(fromIndex)を渡す必要があるということですね!

「歩夢歩夢」の行はindexOfが常に0を返すので、作られるCodeLensは2個とも先頭の「歩夢」のrangeに対して作られます。
だから「どちらをクリックしても先の歩夢に🎀追加」となるわけですね。

終わりに

VSCode APIを駆使してCodeLensを作成する実装を読み解きました。
CodeLensの初期化に必要なRangeを正規表現と一致した文字列から取得するのは、けっこう大変なんですね。
マッチした位置が何行目の何文字目かを割り出し、再び正規表現を使ってRangeを取得する、と。

どうかしている拡張実装時はそこまで精読せずに雰囲気で使いましたが、読み解いていく中でバグの理由も分かりました!(後は直すだけ)

P.S. 1/21(土) VSCodeConJPあります!

実は最近のVS Code拡張のアウトプットの背景には、15分に収まりきらないため、1日1エントリに回しているというのがあります。

VSCodeConJP、ハイブリット開催です!
興味深いトークがたくさん4ですので、皆さまよろしければご参加ください!

追記(2023/01/19):rangeの取得をよりシンプルに置き換える提案

lineを取得するときにpositionAtメソッドを使っているので、その返り値のpositionを使ってgetWordRangeAtPositionすれば、簡潔な実装に置き換えられるのでは?と気づきました。

-      const line = document.lineAt(document.positionAt(matches.index).line);
-      const indexOf = line.text.indexOf(matches[0]);
-      const position = new vscode.Position(line.lineNumber, indexOf);
+      const position = document.positionAt(matches.index);
      const range = document.getWordRangeAtPosition(
        position,
        new RegExp(titleRegex)
      );

試したところ、これまで通り動き、どうかしている拡張ではバグが解消しました🙌
ハンズオンテキストにプルリクチャンス!


  1. 引き続きお気に入りのこちらから
  2. 型のstringAPIのドキュメントにはリンクしていなかったので、「JavaScriptのStringのことなのかな」と理解しました
  3. 引用元はどうかしている拡張自作の記事です(「終わりに」の直前から)
  4. 1/21(土)にハイブリッド開催のVS Code Conference Japan 2022 - 2023、プレイベントのセッション紹介を聞いて、私すごく楽しみなんです! #vscodejp - nikkie-ftnextの日記

git commit --amendで楽々コミット修正

はじめに

楽してこーぜ! nikkieです。

開発中はコマンドラインからgit commitすることが多いのですが、最近--amendオプションが非常に便利と気付きました。
知ったことで開発を捗らせてくれているgit commit --amendの世界へ、皆さまをご案内します。

目次

伝えたいこと:git commit --amendで簡単にコミットを修正できるぞ

--amendオプションを付けると、直前1のコミットを修正できます。
以下のシーンで使えます:

  • 直前のコミットのコミットメッセージを修正したいとき:git commit --amend
  • 直前のコミット自体に変更を追加したいとき
    1. 加えたい変更をステージング
    2. git commit --amend --no-edit

git commit --amendとは(Git Guidesより)

いろいろできるgit commitコマンド。
GitHubが公開しているGit Guidesを参照してみましょう。

この記事で取り上げる--amendについては「Updating Commits With Git Commit Amendgit commit amendでコミットを更新する)」にあります。

While git commit --amend does change history, it only changes the most recent commit on your current branch.

(意訳) git commit --amendは歴史を改変するけれども、変えるのは現在のブランチの最新のコミットだけだ

次のコミットにamendがめっちゃ便利(extremely useful)と紹介しています。

  • リモートにまだpushされていないコミット (Haven't been pushed to the remote yet)
  • コミットメッセージにスペルミスがあるコミット (Have a spelling error in the commit message)
  • 含めたい変更を含んでいないコミット (Don't contain the changes that you'd like to contain)

nikkieとgit commit --amend

--amendは直前のコミットメッセージの修正だと長いこと思い込んでいました。

直前のコミット自体が修正できると気付いたのは、ふとした偶然です。
次のコミットに含めたい変更をステージングした状態で、最新のコミットメッセージのtypoに気付きました。
そこでgit commit --amendでメッセージを編集したところ、ステージングしていた変更がありません。
コミットメッセージだけを修正したのではなく、コミット自体を修正していたのです!

これに気付いてからはコミットを作るときに、より肩の力が抜けるようになりました。
コミット自体はgit rebase -iで修正できるのでたまにrebaseすることがありました。
これを最新のコミットについてやるのはちょっと手数が多いため、頻繁に使わなくてもいいように確認してコミットしていました。

しかし、git commit --amendを知った今、確認を一切しなくてもコミットできます!
なぜなら、直すのはめっちゃ簡単だからです。

このアウトプットを機に知った--no-edit

変更をステージングした状態でgit commit --amendするとコミットメッセージの編集に入ります。
--no-editオプションを指定すると、これをスキップできることをドキュメント2を追っていて知りました!

Use the selected commit message without launching an editor.
For example, git commit --amend --no-edit amends a commit without changing its commit message.

(意訳) エディタを立ち上げることなく、選択されたコミットメッセージを使う。
例えば、git commit --amend --no-editはコミットメッセージを変えることなくコミットを修正する

コミットメッセージ編集用のエディタを閉じるという手数が減るので、ますます気軽にコミットできます🙌

Oh Shit, Git!?!でも紹介

少し前にTwitterで回ってきた「Oh Shit, Git!?!」3にも--amendの紹介がありました4

終わりに

git commit --amendが直前のコミット自体を変更していることに気付き、さらに--no-editオプションを知り、ますます楽々開発できそうです。

一度行った操作を簡単に変更できる(戻せたり修正できたりする)って、気張らずに操作できるので「私にとってはすごい価値だな〜」と感じます。
他にもないか探してみよう!

この記事は、Oh Shit, Git!?!にもある、以下を承知している前提で書いています。

コミットをamendするのはローカルコピーだけにしておくこと、じゃないとひどい目にあうから。

git commit --amendで楽々コミット修正できますが、その恩恵が受けられるのはpushする前だけです。
念押しですが、ここだけはご注意ください。


  1. 「最新」と同じ意味で使っています
  2. https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---no-edit
  3. 上品なバージョンは https://dangitgit.com/ja だそうです。お好みでどうぞ
  4. 今回のアウトプットを機に思い出して確認しています

VS Code拡張ハンズオン基礎編のテキストの中の、正規表現を使ってCodeLensを設定する実装を理解する

はじめに

「そうなんだ〜1、nikkieです。

以前、Markdownの「歩夢」を「歩夢🎀」に置き換えるという、どうかしているVS Code拡張を自作しました。

この実装では、VSCode Conference Japan 2021 ハンズオンの基礎編のテキストを大変参考にしています。
このたび、基礎編のテキストの実装の理解が一歩進んだのでアウトプットします。

目次

CodeLensを設定する箇所を正規表現で探す(「ドキュメントを編集しよう」より)

本節では、ドキュメント上に表示されるコードレンズのボタンを押して、ドキュメントを編集する拡張機能を作ります。

ドキュメント上にCodeLens(コードレンズ)のボタンを表示するために正規表現を使っています。
該当するコードは以下です:

この記事では、「正規表現で、見出しを探す」コメント以降のコードを深堀っていきます。

document.getText()でファイルの全行を取得する

以下ではファイルの内容を全部取得します。

const text = document.getText();

Get the text of this document.

https://code.visualstudio.com/api/references/vscode-api#TextDocument

ファイルの全行が取得されていて、そこには改行文字も含みます。
textが指す文字列は、以下のテンプレートリテラル2のようなイメージです。

const text = `# s

ほげほげ

## t

ふがふが`;

疑問:CodeLensは何個表示されるんだろう?

Python大好きnikkieは、JavaScript(TypeScript)のコードがパッとは読めません。
ハンズオンのコードは正規表現にマッチする数と同じ数のCodeLensができると理解しています。
ただ、上で出したMarkdownのように、見出しが2行ある(###がある)とき、「正規表現にN個マッチして、CodeLensはN個表示される」のNはいくつになるんでしょう?

正規表現は、正規表現リテラル3/^#+\s/gで表されています。
この正規表現リテラルからRegExpインスタンスが初期化され、execメソッドを使ったwhile文があります。

考え込むより手を動かしてみようと、ブラウザの開発者モードを使いました。
以下の関数を定義し、いくつかのtextを渡してみます。

function matchTitleRegex(text) {
  const titleRegex = /^#+\s/g;
  const regex = new RegExp(titleRegex);
  let matches;
  while ((matches = regex.exec(text)) !== null) {
    console.log(`Found ${matches[0]}. Index of matches: ${matches.index}. Next starts at ${regex.lastIndex}`)
  }
  console.log("End");
}

回答:文字列先頭が1つ以上の#で始まればCodeLensは1個、始まっていなければ0個

「こうじゃないかな」という仮説を持って読んでいた方は、一致しましたか?
動きを確認し、理由を考えていきましょう。

文字列先頭に#の並びで始まる行がある場合

先ほどのtextを渡してみます。

const text = `# s

ほげほげ

## t

ふがふが`;

console.logによる出力は以下です。

matchTitleRegex(text);

Found # . Index of matches: 0. Next starts at 2
End
  • 1箇所マッチした
  • 0-1文字目がマッチした(文字列先頭
  • 2文字目以降はマッチしない

見出しのレベルを入れ替えても、1箇所マッチは変わりません

const text2 = `## t

ほげほげ

# s

ふがふが`;
matchTitleRegex(text2);

Found ## . Index of matches: 0. Next starts at 3
End

#で始まるタイトルがいくつあったとしても、文字列先頭のタイトルだけにマッチするように実装しているのですね。

文字列先頭に#の並びで始まる行がない場合

const text3 = `ほげほげ

# s

ふがふが`;
matchTitleRegex(text3);

End

マッチしませんでした!
正規表現にマッチしないので、text3の内容のMarkdownファイルにはCodeLensは1つも表示されません。

読み解き/^#+\s/g

JavaScript正規表現のドキュメントを確認しながら読み解きます。

^

^アサーションの1つです。
アサーション - JavaScript | MDN

^ 入力の先頭に一致します。(略) 例えば /^A/ は "an A" の 'A' には一致しませんが、"An E" の 'A' には一致します。

#+

+数量詞の1つ。
数量詞 - JavaScript | MDN

x+ 直前のアイテム "x" の 1 回以上の繰り返しに一致します。

#+#の1回以上の繰り返しですね。

小まとめ:^#+

ここまでをまとめると、^#+は「文字列の先頭にある#の1回以上の繰り返し」です。

\s

\s文字クラスの1つ。

\s ホワイトスペース 1 文字に一致します。例えば空白、タブ、改ページ、改行、その他の Unicode 空白文字などです。

小まとめ:^#+\s

「文字列の先頭にある#の1回以上の繰り返しとそれに続く空白文字1つ」

g

末尾のgはフラグです。

フラグを用いた高度な検索」より

正規表現には、グローバル検索や大文字小文字を区別しない検索などの機能を実現する 6 種類のオプションフラグがあります。

g グローバル検索を行います。

つまり/^#+\s/gとは

「文字列の先頭にある#の1回以上の繰り返しとそれに続く空白文字1つ」のグローバル検索です。
グローバル検索してもアサーション^によって文字列の先頭としかマッチしないので、上で見たようにマッチは1回か0回です。

  • マッチ1回:文字列先頭でマッチした
  • マッチ0回:先頭でマッチしなかった

mフラグを追加すると、#で始まる各行にマッチ!

gフラグの他にmフラグがあります。(フラグを用いた高度な検索

m 複数行の検索です。

アサーション^について、引用で(略)とした部分、以下のように書かれています!

複数行フラグが true にセットされている場合は、改行文字の直後にも一致します。

つまり、各行の先頭とマッチするかを見ていくことになります。
動きが変わることを確認しましょう。

mフラグ追加の動作確認

function matchTitleRegexV2(text) {
  const titleRegex = /^#+\s/gm;  // g -> gm に変えました
  const regex = new RegExp(titleRegex);
  let matches;
  while ((matches = regex.exec(text)) !== null) {
    console.log(`Found ${matches[0]}. Index of matches: ${matches.index}. Next starts at ${regex.lastIndex}`)
  }
  console.log("End");
}
const text4 = `ほげほげ

# s

ふがふが

## t`;
matchTitleRegexV2(text4);

Found # . Index of matches: 6. Next starts at 8
Found ## . Index of matches: 17. Next starts at 20
End

インデックス6が3行目の#、インデックス17が7行目の##(の左側)です。
各行にマッチしていますね!(CodeLensをタイトルの各行に表示できます🙌)

終わりに

ファイルの中で正規表現にマッチする箇所全てにCodeLensを表示する拡張の実装を読み解きました。
実装自体は文字列先頭とのマッチで実装しているので、グローバル検索の結果、マッチは1または0回です。
正規表現を読み解いていき、mフラグと^アサーションで各行の先頭とマッチさせられることまで見ました。

どうかしている拡張実装時はそこまで精読せずに雰囲気で使いましたが、1つ1つ読み解いていると気付きがありますね!

今回お世話になったのはMDNのドキュメントですが、ドキュメントに当たる前にざっくり理解するため、以下の書籍にも当たりました。


  1. 前回のVS Code拡張ネタエントリから引き続き…
  2. 変数の値も埋め込めます
  3. スラッシュで囲まれたパターンからなる正規表現リテラルを使用します。」 ref: 正規表現 - JavaScript | MDN

Sphinxのliteralincludeディレクティブのオプションを完全理解! シンタックスハイライトしたコードを別ファイルから柔軟に含められます

はじめに

よろしくね、私の味方! nikkieです。

Sphinxシンタックスハイライトしたコードを表示する1際、コードが別のファイルにあるときは literalinclude ディレクティブを使っています。
このディレクティブ、ドキュメントを読んだところ知らなかったオプションを知り、もっと便利に使えそうでした。
バックアップとして現時点の理解をアウトプットします。

目次

Sphinxliteralincludeディレクティブ

https://www.sphinx-doc.org/ja/master/usage/restructuredtext/directives.html#directive-literalinclude

.. literalinclude:: example.py

のようにして、example.pyにあるコードをドキュメントに含められます。
Sphinxプロジェクトで書いているreSTとは別のファイルにあるソースコードを、コピペすることなくliteralincludeドキュメントに含められるので、私には大変重宝しています。

例えばsphinx-revealjsで発表資料を作るとき、サンプルコード群はリポジトリルートでディレクトリを分けてliteralincludeしています2
reSTの中の、例えばcode-blockディレクティブのコードはフォーマットが難しいですが、サンプルコードをreSTから切り離すとフォーマットしやすくなります。

ドキュメントでは、以下のような使用例(オプション指定例)が示されています。

.. literalinclude:: example.rb
   :language: ruby
   :emphasize-lines: 12,15-18
   :linenos:

オプションはたくさんあり、オススメのものを以下に述べていきます。

literalincludeディレクティブとcode-blockディレクティブで共通のオプション

行番号表示

linenos

linenosはフラグで、指定すると行番号が表示されます。

lineno-start

lineno-startは整数と一緒に指定します。
行番号は指定した整数から始まります。

languageシンタックスハイライト指定

シンタックスハイライトに使うPygmentsのlexerのshortnameを指定します。
いろいろ見た中では、以下が参照しやすいように思われます。

強調したい行を指定 emphasize-lines

コードのうち強調したい行の行番号を指定します。
:emphasize-lines: 12,15-18のように、カンマ区切り、また範囲はハイフンで表せます。
lineno-startを指定したり、後述する一部だけ含めたりしても、含めるコードの1行目を強調するなら:emphasize-lines: 1です。
ドキュメントに含める部分を一番上から1,2,...行目と数えるようです。

その他(未検証含む)

caption

指定したキャプションを付けられます。
code-blockディレクトリとの差分は、:caption:とだけ指定した場合、literalincludeディレクティブに指定したファイル名がキャプションとして使われる3ようです。

name(素振り材料)

implicit target nameを指定し、refロールなどでコードを参照できるようです。

コードの一部だけをinclude(code-blockディレクティブにはないオプション)

クラス・関数・メソッド単位 pyobject

もしPythonモジュールの場合には、 pyobject オプションを使用してクラス、関数、メソッドの単位でインクルードすることもできます:

.. literalinclude:: example.py
   :pyobject: awesome_function

literalincludeに指定したファイルがPythonモジュールのとき限定ですが、クラス・関数・メソッド単位で一部だけ含められます!
pyobjectオプションはカンマ区切りをサポートしていないので、複数のクラス・関数・メソッドを含めたいときは、literalincludeディレクティブを複数指定して使っています。

Pythonモジュールをフォーマットしても、pyobjectオプションなら含める箇所がズレる(後述)ようなこともありません

行数指定 lines

lines オプションを使って行番号を正確に指定することでも部分的なインクルードを行えます:

.. literalinclude:: example.py
   :lines: 1,3,5-10,20-

1行目、3行目、5〜10行目、20行目以降が含められます。
行番号を表示したとき、1行目、3行目、5〜10行目の部分の行番号は飛び飛びにはならず、1〜7と連番になります。

linesオプションはPythonモジュール以外を部分的に含める際に重宝します。
ですが、含めるファイルをフォーマットして空行が入った/除かれた場合にズレてしまうので、注意が必要です。

特定の文字列の後から/前までを含められる

今回ドキュメントを読んで一番の驚きはこちら。
注釈の例ですが、以下のPythonモジュールについて

if __name__ == "__main__":
    # [initialize]
    app.start(":8000")
    # [initialize]

:start-after: [initialize]:end-before: [initialize]と指定すると、コメントの間の行が含められます

  • :start-after: pattern:patternの行の次から
  • :start-at: pattern:patternの行から(patternの行を含む)
  • :end-before: pattern:patternの行の前まで
  • :end-at: pattern:patternの行まで(patternの行を含む)

linesオプションの指定と違って、フォーマットが変わってもincludeされる部分は影響を受けなさそうで、結構使いやすそうな印象です。
これは使っていきたい!

素振りしたファイルはこちら:
https://raw.githubusercontent.com/ftnext/sphinx-playground/17821e32ac7fc3f1232d339f0d4de2bf37a1430e/code-samples/source/literalinclude-practice.rst

その他(未検証含む)

素振り材料も含めて記載します。

差分表示 diff
.. literalinclude:: example.py
   :diff: example.py.orig

unified diff formatで差分が表示されます。

前後にコード行を追加 prepend,append(素振り材料)

例えば、 <?php/?> マーカーを含まないPHPコードをハイライトする際などに役立ちます。

lineno-match

ファイルの一部を表示するように指定した場合、ファイルの行番号をそのまま表示するように指定することもできます。その場合は、 lineno-match オプションを設定して下さい。

(上の素振りで試しましたが)ファイルの行番号がそのまま表示されました。
以下のただし書きも納得です。

ただし、このオプションは選択された行が切れ目なくつながっている場合でのみ有効です。

終わりに

存在を知ってから積極的に利用しているliteralincludeディレクティブ、ドキュメントにあたったことで知識の整理と新たな発見がありました。
start-afterend-beforeが特にヤバいです!
今まで使っていて感じていた、かゆいところにきれいに手が届きそう!

reSTにソースコードをコピペせずDRYを保って扱えるliteralincludeディレクティブはオススメです。
たくさんのオプションによって非常に柔軟に使えることが分かりました。


  1. 先日、全部で4つあるとアウトプットしました。
  2. https://github.com/ftnext/2022_slides sourceにreSTを置き、samplecodeにPythonモジュールを置いています
  3. In addition, it supports the caption option; however, this can be provided with no argument to use the filename as the caption. (「追加のオプション」の直後のパラグラフより)

Sphinxには、シンタックスハイライトしたコードの表示の仕方が「4つ」ある

はじめに

Sphinx、全然理解せずに使ってたな… nikkieです。

ドキュメント変換ツールSphinx、私は執筆や発表資料作成でもヘビーユースしています。
シンタックスハイライトしたコードの表示はcode-blockディレクティブ1literalincludeディレクティブを使ってきたのですが、このたびあと2つあることを知りました。
このエントリの流れとしては知った順に見ていきますが、「終わりに」に4つをまとめています。

目次

前提

Sphinxのドキュメントのユーザガイドの中に、reStructuredTextについてのページ2があります。
その中の「ディレクティブ」というドキュメントの「コードサンプルの表示」セクションを参照しました。
ここに4つ紹介されています

There are multiple ways to show syntax-highlighted literal code blocks in Sphinx:

動作環境

Sphinx 6.1.3で検証しました。

今回素振りしたreSTファイルはこちらにあります:

すでに知っていたcode-blockディレクティブ

https://www.sphinx-doc.org/ja/master/usage/restructuredtext/directives.html#directive-code-block

これまでずっと、以下のような形でコードを書いていました3

.. code-block:: python

    >>> print("spam")
    spam

このディレクティブはSphinxディレクティブです。
ドキュメント中のoptions部分をぜひ見ていただきたいのですが、コードのスタイルをoptionできめ細かに指定できます。
行番号を表示したり、指定した行を強調したり、クロスリファレンスできるようにしたりといろいろできます。

すでに知っていたliteralincludeディレクティブ

https://www.sphinx-doc.org/ja/master/usage/restructuredtext/directives.html#directive-literalinclude

ドキュメントに含めたいソースコードがファイル中にある場合literalincludeディレクティブを使っています。

example.pyのコードをドキュメントに含める例です:

.. literalinclude:: example.py

このディレクティブもSphinxディレクティブです。
code-blockディレクティブと同様のoptionに加えて、指定した行の範囲や関数のコードだけを含めることもできます!4
私の中では大変重宝しています。

この2つの私の中での使い分けですが、

  • ファイルがあるならliteralinclude
    • 最初は手元にファイルがなかったとしても、コード量が多かったり何度も使ったりするならファイルにしてliteralincludeに指定します
  • ファイルが必要なければcode-blockに書く

としています。

存在を初めて知ったdoctest block

https://www.sphinx-doc.org/ja/master/usage/restructuredtext/basics.html#rst-doctest-blocks

なんとPython対話モードからreSTにコピー&ペーストするだけで、doctest blockとして扱われます!

>>> 1 + 1
2

いままで対話モードからcode-blockディレクティブの中にコピペしていましたが、もっと少ない手順で済んだなんて!
これからは非常にお世話になりそうです。

doctest blockは(Sphinxが依存している)docutilsのディレクティブです。
https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#doctest-blocks

存在を初めて知ったliteral block

https://www.sphinx-doc.org/ja/master/usage/restructuredtext/basics.html#literal-blocks

2重のコロン(::)で終えた行に続くインデントされた行がliteral blockです。

::

    >>> 1 + 1
    2

前の段落から続けることもできる::

    >>> print("this is a Literal block")
    this is a Literal block

doctest blockに書けるのは対話モードのPythonだけですが、literal block(や他の2つのディレクティブ)にはPython以外のコードも書けます。
literal blockではコードのスタイルはきめ細かには指定できないため、きめ細かく指定したければcode-blockディレクティブの出番ですね。

literal blockもdocutilsのディレクティブです。
https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#literal-blocks

highlightディレクティブというものがある!

https://www.sphinx-doc.org/ja/master/usage/restructuredtext/directives.html#directive-highlight

シンタックスハイライトに使う言語(正確にはPygmentsのlexer5)を指定するディレクティブです。

あるhighlightディレクティブの後は(次のhighlightディレクティブまで)指定した言語でシンタックスハイライトされます。
literal blockも言語指定をしないcode-blockも、highlightディレクティブでの指定が適用されました。
code-blockディレクティブしか知らなかったので、毎回言語を指定してきましたが、highlightディレクティブを知ったおかげで解放されそうです。

また、highlightディレクティブのoption linenothresholdは興味深いです。
「N行以上のコードには一律で行番号表示する」と指定できるoptionと理解しました。
私は行番号を頻繁に使うので、highlightディレクティブのおかげでますます楽ができそうです。

.. highlight:: rest
    :linenothreshold: 1

reSTのハイライトを指定(行番号付きで指定)

::

    Doctest blockの例
    ====================

    >>> 1 + 1
    2

.. code-block::

    Literal blockの例です::

        ここがLiteral block

終わりに

シンタックスハイライトされたコードをSphinxで表示する方法を、ようやく完全に理解しました。

  • Pythonの対話モードを貼り付けるだけで済む、doctest block
  • 末尾::の行の後にインデントしてコードを書く、literal block
  • code-blockディレクティブに続けてコードを書く(きめ細かくスタイル指定できる)
  • ファイルに書いたコードをliteralincludeディレクティブで含める(code-block同様きめ細かくスタイル指定できる)

手持ちの武器が増えた感覚です。
いままではcode-blockディレクティブ一本槍で結構無茶をしていたんだな〜

今後作るSphinx製ドキュメントで使っていって、4つを使い分けられる状態を目指します。
よろしくね、私の味方!

P.S. parsed-literalディレクティブ

シンタックスハイライトはされないのですが、コード中にインラインマークアップやリンクを入れられると知りました6

.. parsed-literal::

    >>> # コメントの一部を **強調**
    >>> 1 + 1
    2
    >>> # `doctestのドキュメント <https://docs.python.org/ja/3/library/doctest.html>`__
    >>> print("parsed-literalディレクティブの練習です")
    parsed-literalディレクティブの練習です

parsed-literalディレクティブというそうで、docutilsのディレクティブです。
https://docutils.sourceforge.io/docs/ref/rst/directives.html#parsed-literal


  1. 「ディレクティブ」は.. <ディレクティブ>::と書いて、インデントした行に内容を書くものというざっくり理解です(精緻にする伸びしろはあると思います)
  2. https://www.sphinx-doc.org/ja/master/usage/restructuredtext/index.html
  3. 先日のdoctestの記事でも使っています。
  4. literalincludeディレクティブについてだけで1エントリ書けそうな気がしますね(書けたら書きます)
  5. https://pygments.org/docs/lexers/
  6. Sphinxをはじめよう』付録A.30より。ドキュメントと見比べる中で気付きました

Sphinxで作るドキュメント中のPythonコード(実行例)をdoctestでテストしよう! 不安は退屈に変わるのです

はじめに

エクスペクト・パトローナム!1 nikkieです。

久しぶりのSphinxネタです。
Sphinxを使ってチュートリアルや発表資料を作る中で、Pythonの対話モードの実行例を書くことがあります。
内容を調整しているうちに、気づかず実行例を壊してしまうことがあったのですが、Python標準ライブラリのdoctestでテストできることを知ってから、実行例を壊してしまう不安から解放されました!

目次

この記事で扱うツール

Sphinxと実行例

ドキュメント変換ツールSphinx2
ルールに従って記述されたテキストファイルをHTMLやPDF(など)に変換できます。

"ルール"としてよく使われるのがreST(reStructuredText)3
テキストのままでも(=Sphinxで変換しなくても)読みやすいマークアップです。

reSTにはソースコードも表示できます4
私がよく使うのはcode-blockディレクティブ!

.. code-block:: python

    print("spam")

pythonのように言語を指定5することで、シンタックスハイライトされます!

Pythonの対話モードの実行例もこんな感じで書いています。

.. code-block:: python

    >>> print("spam")
    spam

doctest

標準ライブラリに含まれるモジュールの1つ6

doctest モジュールは、対話的 Python セッションのように見えるテキストを探し出し、セッションの内容を実行して、そこに書かれている通りに振舞うかを調べます。

「対話的 Python セッションのように見えるテキスト」とは、簡単に言えば、>>>で始まる行やそれに続く行のことです。
つまり、上でcode-blockディレクティブで書いた実行例はdoctestでテストできるんです!

コマンドは以下のようになります7

python -m doctest file [file ...]

伝えたいこと

nikkieはSphinxをヘビーユースしており、reSTを書いて

に変換しているのですが、doctestを組み合わせることで、これらすべてで実行例をテストできます。
うっかり壊していたときはdoctestが失敗するので、すぐ気付いて修正できます!
これが私にはすっごく便利なんです。

nikkieのSphinx × doctest使用例

PyCon JP 2022「Pythonとアスタリスク」のスライドから紹介します。

通常ユースケース

https://raw.githubusercontent.com/ftnext/2022_slides/f0c4cf1aec1e88ca48b58ca2cf69a3ab15a75b70/source/pyconjp/as_unpack_operator.rst.txt より

.. code-block:: python

    >>> [*(1, 2), 3]
    [1, 2, 3]

reSTのコメントを使って前処理(や後処理)を追加する

https://raw.githubusercontent.com/ftnext/2022_slides/f0c4cf1aec1e88ca48b58ca2cf69a3ab15a75b70/source/pyconjp/in_keyword_only_parameters.rst.txt より

.. doctestを通すための変数定義
    >>> weight_diff, time_diff = 0.5, 3

.. code-block:: python

    >>> def flow_rate(weight_diff, time_diff, period=1, units_per_kg=1):
    ...     ...

    >>> flow_rate(weight_diff, time_diff, 3600, 2.2)

reSTでは..で始まった行は空行が入るまでコメントアウトされます。
これを利用して、紙面に出したくはないけれどdoctestを通すために必要なコードを実行例として続けます。

上記のreSTに対してdoctestを実行すると、flow_rate関数が呼び出せることが確認できます。
変数weight_difftime_diffが指す値がなにかはここではあまり重要ではない8ので、スライドに出さないためにコメントとしています。

私がreSTのコメントを使って前処理・後処理を追加するのは以下のようなシーンです。

  • 実行例を通すのに必要な前処理や通った後の後処理
    • 例:前処理として空ファイルを作る。実行例はそれに書き込み。後処理でそのファイルを削除
  • 実行例として紹介する関数やクラスの呼び出しの前に、reSTのコメントを使って関数やクラスを定義する
    • 私はliteralincludeディレクティブ9も好んで使うため、実行例に含めたコードだけをコメントとしてあらかじめ定義しておくことが多い

doctestを実行するときに、ある実行例をスキップも可能

実行例のうちテストで実行したくないものは # doctest: +SKIP というコメントでスキップできます。
https://raw.githubusercontent.com/ftnext/2022_slides/f0c4cf1aec1e88ca48b58ca2cf69a3ab15a75b70/source/pyconjp/as_binary_arithmetic_operator.rst.txt より

.. code-block:: python

    >>> CouplableStr("ぽむ")  # doctest: +SKIP
    'ぽむ'
    >>> CouplableStr("ゆう") * CouplableStr("ぽむ")  # doctest: +SKIP
    'ゆうぽむ'

code-blockディレクティブにlexerとしてpythonを指定すると、このコメントは紙面に出ません10

上のコードをスキップした理由としては、ソースコードのファイルのdocstringでもdoctestで検証できているので、「発表資料のreSTにわざわざクラス定義をコメントとして持ってくるまでもないかな」と判断してです。

このコメントの書式は https://docs.python.org/ja/3/library/doctest.html#directives で説明されています。
doctestの「ディレクティブ」という概念で、SKIPオプション11を(+で)onにしています。

doctestで不安は退屈に変わります!

ケント・ベックの『テスト駆動開発』には以下の言葉があります12

テストは不安を退屈に変える賢者の石だ。(Kindle の位置No.3198)

reSTに書いた実行例、doctestを使うまでは「気づかぬうちに壊しているんじゃないか」と不安でした。
doctestを使い始めると、その不安はなくなり、退屈に変わっています。
「コードはPythonこのバージョンで動作確認しています」という検証も兼ねるので便利です。
doctestのおかげで、私は本当に助かっています!

執筆中は頻繁に手動で実行しますが、CIでdoctestを実行するのもオススメです13

終わりに

reSTに書いた実行例をdoctestでテストする方法を紹介しました。
これは本当に便利で、執筆やプレゼン準備にreSTを使っている方は、実行例が含まれていたらぜひ一度試していただきたいです。
私のアウトプットの大部分はdoctestに支えてもらっています。

今回はcode-blockディレクティブに実行例を書くケースを紹介しましたが、Sphinxの拡張にもsphinx.ext.doctestがあるみたいです。
sphinx.ext.doctest -- ドキュメント内の簡易テスト — Sphinx documentation
これは今後の素振りトピックですね、もっと助けてもらえるかも(わくわく)



  1. エクスペクト・パトローナム|魔法ワールド|ワーナー・ブラザース
  2. https://pypi.org/project/Sphinx/
  3. https://www.sphinx-doc.org/ja/master/usage/restructuredtext/index.html
  4. https://www.sphinx-doc.org/ja/master/usage/restructuredtext/directives.html#showing-code-examples ここを参照したところcode-blockディレクティブ以外にも方法があると、執筆を機に知りました
  5. https://www.sphinx-doc.org/ja/master/usage/restructuredtext/directives.html#directive-code-block にPygmentsがサポートするlexerを指定するとあります。lexerの一覧:https://pygments.org/docs/lexers/
  6. Python実践レシピ』でも紹介されていますね(16.1)
  7. 詳しくは python -m doctest -h やドキュメントで確認してください
  8. period引数やunits_per_kg引数の話をしています
  9. https://www.sphinx-doc.org/ja/master/usage/restructuredtext/directives.html#directive-literalinclude
  10. 何によってコメントが除かれているのか仕組みはよく分かっていないのですが、大変重宝しています
  11. https://docs.python.org/ja/3/library/doctest.html#doctest.SKIPSKIP フラグは一時的に実行例を"コメントアウト"するのにも使えます。
  12. みんなのPython勉強会#88のやっとむさんによる「手軽なpytestでテストを活用しよう!」、テストコードに関係する知識が結び付き、刺激的でした #stapy - nikkie-ftnextの日記 でも紹介しました
  13. GitHub Actionsの例です:https://github.com/ftnext/2022_slides/blob/f0c4cf1aec1e88ca48b58ca2cf69a3ab15a75b70/.github/workflows/doctests.yml

vsceコマンドで、自作VS Code拡張をMarketplaceに公開する

はじめに

せつ菜ちゃんの方が大事なの!?1、nikkieです😱

先日、新しい概念を最小限にした、自作VS Code拡張の公開手順を共有しました。

今回はその続編、vsceコマンドだけで拡張を公開する手順をアウトプットします。

目次

前提

新しい概念を最小限にした手順は実施済み(vsixファイルは作れる状態)とします。
つまり

とします。

新しい概念を最小限にした手順では、Webブラウザを操作してvsixファイルをアップロードして公開しました。
今回は、Webブラウザでアップロードする代わりにvsceコマンドだけで拡張を公開します。

動作環境

  • node v16.14.2
  • npm 8.5.0
  • vsce 2.11.0

拡張公開するためのvsceサブコマンド

コマンドとしては、以下の3つを順番に実行することになります。

# vsixファイル作成(新しい概念を最小限にした手順でも使用)
vsce package
# publisherのリストに<publisher name>を追加
vsce login <publisher name>
# 拡張公開(アップロードして公開されます)
vsce publish

ただしvsce loginにはPersonal Access Tokenの入力が必要です。

vsce loginで必要なPersonal Access Token

Publishing Extensionsのドキュメントを参照しましょう。

Get a Personal Access Token」で方法が紹介されています。

  1. Azure DevOpsで組織(organization)を作る
  2. Azure DevOpsの組織でPersonal Access Tokenを作る

Azure DevOpsで組織(organization)を作る

Publishing Extensionsの「Get a Personal Access Token」から案内された、以下のドキュメントを参照しました。

  1. https://go.microsoft.com/fwlink/?LinkId=307137 からサインイン
  2. 組織(organization)だけを作ります

組織everlasting-diaryを作りました(https://dev.azure.com/everlasting-diary/)。

サインインにあたっては、Visual Studio Code Marketplaceにpublisherを作るのに使ったのと同じマイクロソフトアカウントを使う必要があります2

組織を作った後にプロジェクトの作成が案内されますが(Create a project to get started)、VS Code拡張の公開にあたってはプロジェクトは必須ではないようです。
私は必須なものだけを作る主義3なので、Azure DevOpsのプロジェクトは作らずに進めました。

Azure DevOpsの組織でPersonal Access Tokenを作る

再びPublishing Extensionsの「Get a Personal Access Token」を参照しましょう(スクリーンショットも豊富です)。

右上「User settings」(人と小さい歯車アイコン)の中の「Personal access tokens」から作ります(画像は「Get a Personal Access Token」をどうぞ)。

設定項目(画像は「Get a Personal Access Token」をどうぞ)

  • Name
  • Organization
    • Publishing Extensionsのドキュメントでは、All accessible organizations(アクセス可能な全組織)の設定が案内される
    • nikkieの場合は、組織everlasting-diaryを指定して問題なく動いている
      • 権限は可能な限り狭くしたい派
      • 組織は1つだけなので、複数組織を持っているなら「All accessible organizations」を選ぶべきなのかもしれません
  • オプショナル:Expiration(トークンの有効期限)を延ばす
  • Scopes
    • 「Custom defined」を選択する
    • 左下に「Show all scopes」が表示されていたらクリック(「Show less scopes」に表示が変わる)
    • MarketplaceのManageだけを選択
      • ブラウザの検索機能でMarketplaceを探しました

以上の設定で作成したPersonal Access Tokenをコピーして控えます。
これをvsce login <publisher name>で使うのです!

Expiration(トークンの有効期限)に達したらメールで通知されます。
私はそのトークンをEditして有効期限を延ばしています4

知っていると便利かもなvsceサブコマンド

拡張公開自体はpackageloginpublishでできます。
これら以外で私が使うサブコマンドを共有します。

# publisherのリストから<publisher name>を削除
vsce logout <publisher name>

# publisherのリストを表示
# (publisherのリストは vsce login / logout で変更される)
vsce ls-publishers

Publishing Extensionsのドキュメントでは、他のサブコマンドも紹介されます:

終わりに

vsceコマンドでVS Code拡張を公開する方法を共有しました。
VS Code拡張公開まわりの概念に慣れたら、vsceコマンドを使ってCLI操作で拡張公開していきましょう!

ここで紹介した手順で公開した自作拡張はこちらです🎀

コマンドで拡張公開の先は自動化かなと思います。
公開の自動化についても以下のドキュメントでCIツールとその例がいくつか紹介されています(GitHub Actionsなど)。

タグを打ったことを契機に、そのバージョンの拡張が公開されたらとてもいい感じですよね。
CIツールを使った公開の自動化、取り組んでみて知見を得たらアウトプットしてみます(結構素振りが必要そう)


  1. こちら、大好きです。
  2. You need to login in with the same Microsoft account you used to create the Personal Access Token in the previous section. ref: Publishing Extensionsの「Create a publisher」
  3. こちらで表明しています:「あってもなくても同じなら捨てる」という片付けの考え方が(こんまりメソッドと)プログラミングに通じていました - nikkie-ftnextの日記
  4. ここで紹介したコマンドをGitHub Actionsで実行する場合は、トークンの有効期限の扱いはより適切な方法があるかもしれません(アイデアがある方は共有いただけると大変助かります)