kkAyatakaのメモ帳。

誰かの役に立つかもしれない備忘録。

sphinxcontrib-xlsxtableの画像対応

Excelファイルから表を作るSphinx拡張について、埋め込み画像に対応しました。

特にオプションなどは必要なく、画像が使用されたExcelファイルを指定すれば、そのまま画像が利用されます。

.. xlsx-table::
   :file: path/to/xlsx/embedded-images.xlsx

以下のようにレンダリングされます。

Embedded Images

内部処理

内部処理としては、以下のようにしています。

  • xlsxファイルを解析して、画像データを抜き出し、ファイルに保存
    • Ezcelファイル名.xlsx.mediaのフォルダを作成し、保存します
  • 画像アンカーの位置からセル (表の位置) を特定し、imageディレクティブを追加する

モジュール実行すると、以下のようになります。

% python -m sphinxcontrib.xlsxtable sample-images.xlsx --sheet v 
+-------+------------------------------------------------+
| Red   | .. image:: sample-images.xlsx.media/image1.png |
+-------+------------------------------------------------+
| Green | .. image:: sample-images.xlsx.media/image2.png |
+-------+------------------------------------------------+
| Blue  | .. image:: sample-images.xlsx.media/image3.png |
+-------+------------------------------------------------+

リンク

AnsibleでSSH接続のポートを指定する

  • ansible_port変数の値をPlaybook/インベントリで指定する
  • ansible-playbook--extra-varsで指定
  • ansible-playbook--ssh-common-args

理解すると、まあ、その通りなのだけど、だいぶ迷走したのでまとめとく。

環境

  • Ansible 2.9.6
  • Ubuntu 20.04.1 LTS (WSL2)

ansible_port変数の値を指定する

SSHで使用するポート番号は、ansible_portという「変数」で指定することができる。「変数」なので、変数を指定する方法で指定できる。

なお、ansible_ssh_portでも動作するが、2.0以降はdeprecatedになっている。

Playbookで指定

- hosts: servers
  remote_user: root
  vars:
    ansible_port: 1234

インベントリから引っ張ってくることもできる。

- hosts: servers
  remote_user: "{{ user_name }}"
  vars:
    ansible_port: "{{ ssh_port }}"

今回やりたかったのはこれ。以下のような運用を考えたため。

  • サーバーのセットアップは22で行って、それ以降は作業用ユーザー、変更したポートで行う
  • インベントリファイルを共有したい
    • セットアップでユーザー作成とポート変更を行い、同じインベントリを使って、以降の作業用Playbookを使用する

インベントリで指定

インベントリファイルでも指定できる。

[servers]
192.168.10.100 ansible_port=1234

[servers:vars]
ansible_port=1234

ansible-playbookの引数で変更

ansible-playbookの実行時に、--extra-varsで指定できる。

$ ansible-playbook -i inventory.ini playbook.yml --extra-vars "ansible_port=1234"

ansible-playbook--ssh-common-args

ansible-playbookの実行時に、--ssh-common-argsSSH実行時の引数を指定できる。ただし、sftp/scp/sshのポートの指定方法の微妙な違いが問題になる。

  • --ssh-common-argssftp/scp/sshで利用されるが、ssh-psftp/scp-Pで異なるため、厳密には利用できない
  • --ssh-extra-args等で個別に利用できるが、だいぶ面倒

--ssh-common-args "-p 1234"で動作はするものの、WARNINGとなって厳密にはうまくない。ので、ansible_port変数を指定するほうが良い。

podmanでdocker-compose使用時のビルドエラーに対応する

  • podmandocker-composeを使用すると、イメージのビルド時に「FileNotFoundError: [Errno 2] No such file or directory: 'docker':」が出る
  • イメージビルド時にdockerコマンドを実行するが、dockerコマンドが無いため
  • /usr/local/binなどにdockerという名前で、podmanを実行するシェルスクリプトを作成する

環境

  • CentOS 8 Stream
  • podman 3.3.1 (rootで実行)
  • docker-compose 1.29.2

podmanでdocker-composeを使う

以下参照。ルートレスモードはまだ試して無いです。

イメージビルド時のエラー

手順に従ってセットアップしdocker-compose up -dを実行するとエラーとなる。少し調べると、イメージのビルド (build:/Dockerfileの指定) が含まれているとエラーになることがわかる。

  File "/usr/local/lib/python3.6/site-packages/compose/service.py", line 1133, in build
    output_stream=output_stream)
  File "/usr/local/lib/python3.6/site-packages/compose/service.py", line 1943, in build
    universal_newlines=True) as p:
  File "/usr/lib64/python3.6/subprocess.py", line 729, in __init__
    restore_signals, start_new_session)
  File "/usr/lib64/python3.6/subprocess.py", line 1364, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
FileNotFoundError: [Errno 2] No such file or directory: 'docker': 'docker'

エラーメッセージはイマイチピンと来ないのでソースコード (service.pyの1943行目あたり)をみてみると、イメージのビルドはsubprocess.Popendockerコマンドを呼び出していることがわかる。

podman環境でdockerコマンドは存在しない。ただ、コマンド自体には互換性があるので、代わりにpodmanを実行できるようにシェルスクリプトを準備することにする。

ちなみに、イメージのビルドが実行されなければエラーにならない。事前にpodman buildでイメージを作成しておくと、docker-compose実行時にイメージビルドが走らないので、エラーにならずに実行できる。

dockerシェルスクリプトを準備する

/usr/local/bin (とかのパスが通っているところ) にdockerという名前でpodmanを実行するシェルスクリプトを準備する。

# vim /usr/local/bin/docker
#!/bin/bash

podman $@

実行権限もつけとく。

# chmod +x /usr/local/bin/docker

再度docker-composeを実行。イメージのビルドが実行され、コンテナが立ち上がる。

# docker-compose up -d
Building http
STEP 1/7: FROM httpd:latest
STEP 2/7: ENV DATAACCEPTOR_HOST=127.0.0.1
--> 201026ad839
...

Creative Commons Licenseのテキストファイル

GitHubで適用する時など用。身も蓋もないけど「plaintext」で検索かければ引っかかる。

公式サイトのフッターの検索で「plaintext」を検索してもOK。

f:id:kkAyataka:20210501211546p:plain:w300

URLに「.txt」をつける

URLを見ればわかるが、Legal Text / LicenseのURLに「.txt」を付ければ辿り着ける。

以下のように、選択サイト -> サマリー -> 全文 -> テキストファイル。

  1. https://creativecommons.org/choose/
  2. https://creativecommons.org/licenses/by/4.0/
  3. https://creativecommons.org/licenses/by/4.0/legalcode
  4. https://creativecommons.org/licenses/by/4.0/legalcode.txt

CC0のテキストファイル

CC0の場合も同じ構造になっている。パブリックドメインにする場合はこっち。

CC0の場合はGitHubリポジトリを作るときにも選べる。

f:id:kkAyataka:20210501213401p:plain:w300

plusctx (処理履歴の保存と参照)

ライブラリというよりは実装アイデアという感じ。ロギングの課題を解決するために考えていところ、構造的に割と面白かったので組んでみた。

組んだ後でスタックトレースで良いのでは?とも思ったけど、まあ、マルチプラットフォームで取ったり、整形したり、任意の情報乗っけたりとか考えたら、一応用途はありそう。

サンプル

  • 関数の冒頭でPLUSCTX_CTX("name");としてコンテキストを定義する
    • コンテキストはスレッド毎 (thread_local) にスタックとして保存される
    • ブロックコープを抜けると (関数を抜けると) スタックから破棄される
  • サブ関数内でコンテキストスタックを確認すると、上位の (呼び出し元) の情報を得ることができる
#include "plusctx/plusctx.hpp"

#include <iostream>
#include <sstream>

// コンテキストスタックから文字列を生成
// スタックはスレッド毎に管理
std::string get_ctx_string() {
    const auto stack = plusctx::get_context_stack();

    std::ostringstream ss;
    for (const auto c : stack) {
        ss << ">" << c->name;
    }

    return ss.str();
}

void sub1();

// main
int main() {
    PLUSCTX_CTX("main");

    // mainを詰んだのでmainだけ
    std::cout << get_ctx_string() << std::endl; // >main

    sub1();

    // コンテキストの寿命はブロック単位なので、
    // sub1コンテキストは削除されてる
    std::cout << get_ctx_string() << std::endl; // >main
}

void sub1() {
    PLUSCTX_CTX("sub1");

    // mainから呼び出されたことがわかる
    std::cout << get_ctx_string() << std::endl; // >main>sub1
}

plusctx::Contextクラス

ロギング用途を想定しているので、コンテキストには関数名やファイル名などを保存している。コンテキスト情報の取り扱いについては既定していないので、どう使うかは別の問題。

// 任意の名前だけでなく、関数名やファイル名も保存する
class Context {
public:
    Context(
        const std::string name,
        const std::string func_name,
        const std::string rich_func_name,
        const std::string file_name,
        const std::size_t line_no
    );

// 簡易にインスタンス化できるようにマクロを準備
#define PLUSCTX_CTX(name) plusctx::Context plusctx_ctx__( \
    name, \
    __func__, \
    __FUNCSIG__, \
    plusctx::detail::shorten_file_name(__FILE__), \
    __LINE__)

その他

  • 個人的に使うかは微妙なラインなのだけど、アイデアは残しておきたかったので組んでみた
  • 実際の運用場面ではアプリケーション段階で決めることが多い。継承して拡張しても良いし、同コンセプトで定義しなおすこともありそう
  • コンテキストの生成時 (コンストラクタ) トレースログ出すとかもあり
  • アプリケーションデータの伝搬のほうも考えたけど、こっちは全体デザインに影響するので適用範囲が限られるのと、簡単にグローバル変数になるので運用つらそう

Google Testでデータドリブンテスト

他の (言語の) テスティングフレームワークで、同一のテストを異なるデータで実行するデータドリブンテストの機能があって、これGoogle Testでも無いんかいな、と調べたところあった。

Google Testでは「Value-Parameterized Tests」として説明されている。説明内に「data-driven testing」って記述があるので、検索すれば引っかかる。

Value-Parameterized Tests

使用する機能さえ把握してしまえば難しくない。シンプルに書けるようにうまく設計されてる。

// テストデータクラス
struct MyTestParam {
    int v1;
    int v2;
    int ok_v;

    MyTestParam(
        const int v1,
        const int v2,
        const int ok_v
    ) : v1(v1),
        v2(v2),
        ok_v(ok_v) {
    }
};

// TestWithParamにテストデータのクラスを渡して、継承する
class MyTest : public testing::TestWithParam<MyTestParam> {
};

// TEST_Pでテストを書く
// GetParamでテストデータクラスのインスタンスを得る
TEST_P(MyTest, test) {
    const auto p = GetParam();

    EXPECT_EQ(p.v1 + p.v2, p.ok_v);
}

// データをインスタンス化して、テストを作る
INSTANTIATE_TEST_SUITE_P(OkTest, MyTest,
    testing::Values(
        MyTestParam(1, 2, 3),
        MyTestParam(2, 3, 5)
    )
);

実行結果。

Note: Google Test filter = */MyTest.*
[==========] Running 2 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 2 tests from OkTest/MyTest
[ RUN      ] OkTest/MyTest.test/0
[       OK ] OkTest/MyTest.test/0 (0 ms)
[ RUN      ] OkTest/MyTest.test/1
[       OK ] OkTest/MyTest.test/1 (0 ms)
[----------] 2 tests from OkTest/MyTest (2 ms total)

[----------] Global test environment tear-down
[==========] 2 tests from 1 test suite ran. (3 ms total)
[  PASSED  ] 2 tests.

OkTest/MyTest.test/0といった感じで出力される。この文字列でフィルタ出来るので、--gtest_filter=*/MyTest.*/0で、1つ目のデータだけで実行できる。

テストデータに名前を付ける (出力する)

/0/1の部分の文字列を置き換える。仕組み上任意の文字列...とまではできないのだけど、ある程度読みやすくなる。

簡単なのがテストデータに対してoperator<<を用意してあげる方法。合わせて、INSTANTIATE_TEST_SUITE_Pの最後に、testing::PrintToStringParamName()を渡す。

struct MyTestParam {
    std::string desc; // 追加
    int v1;
    int v2;
    int ok_v;

    MyTestParam(
        const std::string & desc, // こっちも更新
        const int v1,
        const int v2,
        const int ok_v
    ) : desc(desc),
        v1(v1),
        v2(v2),
        ok_v(ok_v) {
    }
};

// operator<<を作って、単純にdescを返す
std::ostream & operator<<(std::ostream & stream, const MyTestParam & p) {
    return stream << p.desc;
}

class MyTest : public testing::TestWithParam<MyTestParam> {
};

TEST_P(MyTest, test) {
    const auto p = GetParam();

    EXPECT_EQ(p.v1 + p.v2, p.ok_v);
}

INSTANTIATE_TEST_SUITE_P(OkTest, MyTest,
    testing::Values(
        MyTestParam(
            "1p2eq3", // テスト名をつける
            1, 2, 3
        )),
    testing::PrintToStringParamName() // 追加
);

実行結果。/0だったところに指定したテスト名が出力されている。

Note: Google Test filter = */MyTest.*
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from OkTest/MyTest
[ RUN      ] OkTest/MyTest.test/1p2eq3
[       OK ] OkTest/MyTest.test/1p2eq3 (0 ms)
[----------] 1 test from OkTest/MyTest (1 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (2 ms total)
[  PASSED  ] 1 test.

テストデータだけだと何を狙ったデータなのかの情報がロストする。コメントでもいいけど、なんとなく把握できるテスト名を適当につけとくと、コード的にもテスト結果的にも内容を把握する手助けになる。

sphinxcontrib-xlsxtableに行・列指定オプションを追加

  • 使用する / 使用しない、行 / 列を指定するオプションを追加
  • v1.0.0としてリリース

Excelファイルから表を作るSphinx拡張について、v1.0.0としてリリースしました。 が、足りない機能があるような無いような...(まあ、必要性がはっきりしたら追加すれば...)

使用する行 / 列と使用しない行 / 列の指定

include-rowsexclude-rowsinclude-columnsexclude-columnsを使って、指定できるようにしました。

.. xlsx-table::
   :file: path/to/xlsx/file.xlsx
   :include-rows: 1-2 4 8
   :exclude-rows: 3 5-7
.. xlsx-table::
   :file: path/to/xlsx/file.xlsx
   :include-columns: A-B 4
   :exclude-columns: C 5-6

矩形範囲指定のオプションは実装していませんが、オプションの組み合わせで同等のことはできます。

.. B2-D3の範囲

.. xlsx-table::
   :file: path/to/xlsx/file.xlsx
   :include-rows: 2-3
   :include-columns: B-D

表の分割表示を想定

Excelを表として取り込むときに分割して取り込むのを想定していました。 可読性もですが、特にPDF化を想定した場合、ページに収める細工が必要そうで。

.. xlsx-table:: 表1
   :file: path/to/xlsx/file.xlsx
   :include-columns: A-G

.. xlsx-table:: 表2
   :file: path/to/xlsx/file.xlsx
   :include-columns: H-N

また、既存のExcelファイルをSphinx化するときに都合よく取り出したり。 メタデータも同時に記載されている場合に、メタデータとデータの表を分けるとかはありかと。

includeかexcludeか

include指定は明示的ですが、Excelファイルを更新しても、自動更新されなくなります。 逆にexcludeはExcelファイルにしたがって更新されますが、表示が崩れるとも言えます。 用途に合わせた指定が必要です。

リンク