travis-ci.orgでCIを動かしていたリポジトリをtravis-ci.comに移行

久々にtravis-ci.orgにアクセスしたら "Since June 15th, 2021, the building on travis-ci.org is ceased. Please use travis-ci.com from now on." とメッセージが出たので、慌ててtravis-ci.comに移行したときのメモ。あとから思い出しながら書いたので抜けがあるかもしれません。

  1. Migrating repositories to travis-ci.com - Travis CI の "GitHub Apps initial setup" に従ってアカウントの準備
  2. https://travis-ci.com/account/repositories にいって、"Manage repositories on GitHub"ボタンを押して、"All repositories"を選択してSaveボタンを押すも、なぜかtravis-ci.orgにて動かしていた3つのリポジトリだけがリポジトリリストに現れない。3つのリポジトリだけを選択しても "We couldn’t find any active repositories you have access to." などとメッセージが出て失敗。大いに困る。
  3. https://travis-ci.com/account/migrate にいくと、travis-ci.orgにて動かしていた3つのリポジトリが表示されている。しかしグレーアウトされていて選択できない。
  4. 下の方にあったボタン( beta という文字列があったような…メモを取らなかった ) を押して、出てきたダイアログ (これも詳細を失念 ) を押すと、リポジトリを移行できそうな画面に遷移(下図)。ここで"Migrate selected repositories to travis-ci.com"ボタンを押すことでリポジトリを移行できた。

f:id:minus9d:20210615225841p:plain

Python 3のsubprocess.run()の使い方

Python 3で外部コマンドを呼ぶにはsubprocessモジュールで提供されているrun()を使うことが推奨されるのですが、毎回使い方を調べてしまっているので自分用にまとめます。順を追っていかないと引数の意味を理解しづらいところがあるので、冗長ですが簡単な例から書いていきます。なお、run()が導入されたのはPython 3.5なので、それより前のバージョンでは使えません。CygwinPython 3.8.3で動作確認しています。

基本

呼び出される側のスクリプトとして、以下のようなhello.pyを用意します。これは標準出力に「こんにちは」、標準エラー出力に「こんばんは」と出力するだけのトイプログラムです。

import sys

print("こんにちは", file=sys.stdout)
print("こんばんは", file=sys.stderr)

さっそくhello.pyをsubprocess.run()`を使って呼び出してみます。もっともシンプルな呼び出し方は

res = subprocess.run(['python3', 'hello.py'])

ですが、これだとresの中には

CompletedProcess(args=['python3', 'hello.py'], returncode=0)

という情報しか入っていません。これでは肝心の標準出力結果、標準エラー出力結果は得られません。

標準出力結果、標準エラー出力結果を得るためには、以下のようにcapture_output=Trueをつけます(Python 3.7以降限定;Python 3.6以前では代わりにstdout=subprocess.PIPEstderr=subprocess.PIPE の2つを指定)。

res = subprocess.run(['python3', 'hello.py'], capture_output=True)
print("return code: {}".format(res.returncode))
print("captured stdout: {}".format(res.stdout.decode()))
print("captured stderr: {}".format(res.stderr.decode()))

出力結果は以下です。res.stdoutで標準出力結果を、res.stderr標準エラー出力結果を得られていることがわかります。

return code: 0
captured stdout: こんにちは

captured stderr: こんばんは

ここで注意なのは、res.stdoutres.stderrは、ともに文字列ではなくバイトデータであることです。なのでdecode()を呼んで文字列に変換してやる必要があります。

これは面倒なので、以下のようにtext=Trueをつけると、文字列が返ってくるようになり便利です(Python 3.7以降;Python 3.3 ~ 3.6では代わりにuniversal_newlines=Trueをつける)。

res = subprocess.run(['python3', 'hello.py'], capture_output=True, text=True)
print("return code: {}".format(res.returncode))
print("captured stdout: {}".format(res.stdout))  # こんにちは
print("captured stderr: {}".format(res.stderr))  # こんばんは

明に文字列のエンコード方法を指定したいときは、text=Trueの代わりにencoding='utf8'などとつけても良いようです。

標準出力と標準エラー出力とをまとめてキャプチャ

標準出力と標準エラー出力とをまとめてキャプチャするには、stdout=subprocess.PIPE, stderr=subprocess.STDOUT と指定します(参考:subprocess — Subprocess management — Python 3.9.5 documentationIf you wish to capture and combine both streams into one, use stdout=PIPE and stderr=STDOUT instead of capture_output.

res = subprocess.run(['python3', 'hello.py'],
                     stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                     text=True)
print("captured stdout & stderr: {}".format(res.stdout))

ただ、これには大きな欠点があります。標準出力、標準エラー出力それぞれの出力の順番が保存されずマージされてしまいます。上記の例では、

captured stdout & stderr: こんばんは
こんにちは

と、順序が逆に表示されてしまいました。

上記の例だけに限っていうと、python - How can I make subprocess get the stdout and stderr in order? - Stack Overflow にある通り、['python3', '-u', 'hello.py']-uオプションを入れてunbufferedを強制することで期待通りの順序になりました。しかし、一般論でいうと、順序を常に保存するかんたんな方法はなさそうでした。

中身の理解はできていませんが参考になりそうなリンクを張っておきます。

例外処理

呼び出したコマンドが成功したかどうかは、run()の戻り値のreturncodeを確認します。0なら成功、非0なら失敗です。

例として、実行すると異常終了するraise_error.pyを以下のように作成します。

import sys
sys.exit(1)

これの戻り値を確認する例が以下です。

res = subprocess.run(['python3', 'raise_error.py'])
print("return code: {}".format(res.returncode))  # 1が表示される

run()check=Trueを追加することで、呼び出したコマンドが失敗したときに例外を返すようにすることができます。

$ python3 subprocess_test.py
return code: 1
------------------------------------------------------------
Traceback (most recent call last):
  File "subprocess_test.py", line 46, in <module>
    res = subprocess.run(['python3', 'raise_error.py'], check=True)
  File "/usr/lib/python3.8/subprocess.py", line 512, in run
    raise CalledProcessError(retcode, process.args,
subprocess.CalledProcessError: Command '['python3', 'raise_error.py']' returned non-zero exit status 1.

check=Trueをつけるべきかどうかは趣味と場合によりそうです。

コマンドに引数を与える

呼び出される側のスクリプトとして、1からNまでの整数の和を返すsum_1_to_n.pyを用意します。nは標準入力で受け取る仕様です。

n = int(input())
print(n * (n + 1) // 2)

コンソールで

$ echo 10 | python3 sum_1_to_n.py

とするのと同じノリでsubprocess.run()を使う場合、コマンドをリストで表現する方法ではパイプ(|)を使えないので、以下のようにshell = Trueを使ってコマンドを文字列として表現する必要があります。

res = subprocess.run('echo 10 | python3 sum_1_to_n.py', shell=True, capture_output=True, text=True)
print("captured stdout: {}".format(res.stdout))  # 55と表示される

しかし、一般にshell=Trueと指定すると脆弱性が発生するので推奨されません。個人のスクリプトでは便利なのでよくやってしまいますが…

上記の例だと、input=10というのを使うと、標準入力に10を指定したのと同じになってパイプを回避できます。

res = subprocess.run(['python3', 'sum_1_to_n.py'], input='10', text=True, capture_output=True)
print("captured stdout: {}".format(res.stdout))

パイプを回避するより一般的な例が、 この処理Pythonでどう書く? - エムスリーテックブログ の「パイプを使う」に記載されています。subprocess.run()より低レベルな関数subprocess.Popen()を使うのですが、私は理解できていません。

その他の便利オプション

  • cwdオプションによりコマンド実行時のワーキングディレクトリを変更できます
  • envオプションによりコマンド実行時の環境変数を変更できます

参考URL

Python 3で現在の日時を表示

毎回調べてしまっているのでメモ。

import datetime

now = datetime.datetime.now()
print(now)

2021-05-31 21:10:18.332820

というふうに表示できるのだけれど、個人的には秒までで十分なことが多いので、以下のようにするのが多いです。

import datetime

now = datetime.datetime.now()
print(now.strftime('%Y-%m-%d %H:%M:%S'))

表示例は以下です。

2021-05-31 21:10:18

Wandbox で試せます。

Python 3のunittestを使い始めるためのメモ

Pythonunittestをちゃんと使ったことがなかったので、使い方について調べてまとめました。Windows 10のAnaconda Python 3.8.5で動作確認しています。

もっとも小さな例

被テストコードとテストコードとを同一のスクリプトに記述した小さな例を以下に示します。実際はこんなことはほぼないと思いますが。

import unittest


def add(x, y):
    return x + y


class AddTest(unittest.TestCase):

    def test_add(self):
        self.assertEqual(add(1, 2), 3)


def main():
    unittest.main()


if __name__ == '__main__':
    main()

ここで、被テストコードが関数add()、テストコードがAddTestです。

テストコードは以下の作法にのっとって作成される必要があります。

  • unittest.TestCaseクラスを継承したサブクラスを作成(上記のAddTestクラス)
  • サブクラスの中に、testで始まるメソッドを作成
    • メソッド名がtestで始まらないと実行されないので注意
  • testで始まるメソッドの中で、self.assertEqual()self.assertTrue()などを呼び出してテストを記述

このスクリプトmain.pyとして保存しpython main.pyと実行すると、テストが無事走ります。

.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

Pythonパッケージに対するテスト

もう少しリアルな例として、あるPythonパッケージに対してテストを行う例を考えます。

ディレクトリ構造

以下のディレクトリ構造でコードが配置されているものとします。

root_dir/
    ├─comp_prog_lib/
    │  │  __init__.py
    │  │
    │  └─math/
    │          prime.py
    │          __init__.py
    │
    └─tests/
        │  __init__.py
        │
        └─math_tests/
                test_prime.py
                __init__.py

root_dirは、例えばGitのリポジトリに相当するような上位ディレクトリであるものとします。

comp_prog_libは、被テスト対象であるPythonパッケージです。ここでは競技プログラミングのためのライブラリという意味でcomp_prog_libと名付けています。comp_prog_libの下には、競技プログラミングのためのモジュールがある階層構造とともに配置されているものとします。

testsは、comp_prog_libをテストするためのコードです。テストコードは普通ライブラリとは分離して配置したくなるはずなので、comp_prog_libと並べて配置します。

prime.pyのコードを以下に示します。整数n素数かどうかを判定する関数が実装されています。

"""
素数に関する関数を提供
"""


def is_prime(n: int):
    """nが素数か否かを判定"""
    if n <= 1:
        return False
    i = 2
    while i * i <= n:
        if n % i == 0:
            return False
        i += 1
    return True

test_prime.pyのコードを以下に示します。整数1から16までの素数判定が正しく行えているかをチェックするテストが実装されています。

"""
prime.pyのテストスクリプト
"""
import unittest

from comp_prog_lib.math.prime import is_prime


class IsPrimeTest(unittest.TestCase):

    def test_is_prime(self):
        primes = set([2, 3, 5, 7, 11, 13])
        for i in range(1, 17):
            if i in primes:
                self.assertTrue(is_prime(i))
            else:
                self.assertFalse(is_prime(i))

__init__.pyはすべてパッケージを構成するための特殊ファイルで、中身は空です。

Test Discoveryによるテスト実行

テストを実行するには、root_dir/cdした状態で、

$ python -m unittest discover tests

と実行します。すると、testsディレクトリ以下に存在する、ファイル名がtestで始まるテストスクリプトが自動で収集され実行されます。これはTest Discoveryと呼ばれる仕組みです。実行結果は以下です。

.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

なお、tests/以下のディレクトリ構造には注意が必要そうです。最初、上記でmath_tests/と示していたディレクトリ名をmath/にしていたのですが、この状態でテスト実行コマンドを実行すると、

E
======================================================================
ERROR: math.test_prime (unittest.loader._FailedTest)
----------------------------------------------------------------------
ImportError: Failed to import test module: math.test_prime
Traceback (most recent call last):
  File "(環境名)\lib\unittest\loader.py", line 436, in _find_test_path
    module = self._get_module_from_name(name)
  File "(環境名)\lib\unittest\loader.py", line 377, in _get_module_from_name
    __import__(name)
ModuleNotFoundError: No module named 'math.test_prime'; 'math' is not a package


----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (errors=1)

というかなりわかりにくいエラーが出て失敗しました。tests/mathcomp_prog_lib/mathと名前衝突していたのがよくなかったようで、tests/mathtests/math_testsと名前を変えることでエラーが消えました(参考)。正直、理屈はよくわかっていません。

ディレクトリ構造についての補足

comp_prog_lib/パッケージのテストがうまくいった理由について説明します。Pythonは、カレントディレクトリが暗黙的にsys.pathに加えられる仕様を持ちます。なのでroot_dir/cdした状態では、comp_prog_libがimport可能な状態になります。なのでtest_prime.pyfrom comp_prog_lib.math.prime import is_primeというimport文が正しく動きます。

これは場合によっては害になることもありえます。例えば、comp_prog_libに相当する部分をpip installなどでインストール可能なように設定していた場合、ローカルのcomp_prog_libをテストしているのかインストール済のcomp_prog_libをテストしているのかが不明瞭になります。この問題を避けるため、Pytestのドキュメント では、comp_prog_libsrcディレクトリの下に移動させるベストプラクティスを推奨しています。これはunittestでも通じる話だと思います。

参考URL

matplotlibでグラフを保存するときのテンプレート

サーバにてmatplotlibを使ってグラフを作成し、ファイルに保存することがよくあるのですが、実装するたびにググりまくって非効率なので、私が高確率で使う機能をテンプレ化しました。

さっそくコードを以下に示します。

import matplotlib
import matplotlib.pyplot as plt
import numpy as np


# ウィンドウを開けない環境でもグラフを作成する
# https://stackoverflow.com/questions/39305810/matplotlib-use-required-before-other-imports-clashes-with-pep8-ignore-or-fix
matplotlib.pyplot.switch_backend('Agg')


def plot_test():
    # プロットしたいデータ系列を2つ作成
    x1 = np.linspace(0, 3, 50)
    y1 = np.sin(x1)

    x2 = np.linspace(0, 4, 70)
    y2 = np.cos(x2)

    # 描画先を作成
    # figsize=(8, 6)により、画像保存時のサイズが800x600になる
    # (figsizeを指定しないとデフォルトで640x480)
    fig, ax = plt.subplots(figsize=(8, 6))

    # 折れ線グラフを2系列プロット
    ax.plot(x1, y1, label='sin curve', marker='o', markersize=3)
    ax.plot(x2, y2, label='cos curve', marker='o', markersize=3)

    # グラフタイトル、X軸タイトル、Y軸タイトルを設定
    ax.set_title('sin and cos curves')
    ax.set_xlabel('x axis')
    ax.set_ylabel('y axis')

    # グリッド線を表示
    ax.grid(axis='both')

    # 凡例を表示
    ax.legend()

    # X軸、Y軸の表示範囲を手動で調整(オプション)
    ax.set_xlim(-0.1, 4.1)
    ax.set_ylim(-1.1, 1.1)

    # グラフの周囲の余計な空白を除去
    fig.tight_layout()

    # ファイルに保存
    fig.savefig('plot.png') 

    # 後始末
    # これがないと、グラフを大量に作成したとき
    # "RuntimeWarning: More than 20 figures have been opened."というメッセージが出る
    # https://stackoverflow.com/questions/45933886/python-plt-close-or-clear-figure-does-not-work
    plt.close(fig)


if __name__ == '__main__':
    plot_test()
        

出力されるplot.pngは以下です。

f:id:minus9d:20210302231718p:plain

大体の説明はコード中に書いてしまいました。以下は補足です。

matplotlib.pyplot.switch_backend('Agg')

上の1行は、サーバのようにウィンドウを開けないような環境でmatplotlibを使うときの定型文です(意味はよくわかってません)。以前は

import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt

という定型文を書いていたのですが、 python - matplotlib.use required before other imports clashes with pep8. Ignore or fix? - Stack Overflow によると、この書き方でよいようです(注:まだ試せていません)。

参考URL

std::vectorのresizeとassignの違い (C++)

恥ずかしながらC++vectorassign(n, val)という関数を知らなかったのでメモです。

よく似た関数に、resize(n, val)という関数があります。以下のコードで挙動を比較してみます。

#include <iostream>
#include <vector>

void print_vector(const std::vector<int> &vec) {
    for (auto e: vec) {
        std::cout << e << ", ";
    }
    std::cout << std::endl;
}

int main(){
    std::vector<int> vec1{1, 2, 3};
    vec1.resize(10, 777);
    print_vector(vec1);

    std::vector<int> vec2{1, 2, 3};
    vec2.assign(10, 777);
    print_vector(vec2);
    
    return 0;
}

結果は以下です。

1, 2, 3, 777, 777, 777, 777, 777, 777, 777, 
777, 777, 777, 777, 777, 777, 777, 777, 777, 777, 

resize(n, val)assign(n, val)も、vectorの長さをnに変える点では同じです。 違いは、resize(n, val)ではvectorの延長された部分のみ値がvalになるのに対し、 assign(n, val)ではvectorのすべての部分で値がvalになることです。

もしvectorオブジェクトのサイズが0の場合はresize(n, val)assign(n, val)も結果は同じになりますが、 assign(n, val)を使うほうがより意図を明確に表せます。

Wandboxへのリンクで実際に実行できます。

参考URL

PowerShellのTab補完をbashのようにする

Windows PowerShell (この記事の執筆時点でバージョンは5.1) でTabキーを押したときの補完方法をbashのようにする方法について調べました。

デフォルトでの補完方法

まず、デフォルトでのWindows PowerShellの補完方法について説明します。例として、20210101.txt, 20210103.txtという2つのファイルがあるディレクトリでWindows PowerShellのシェルを開いている場合を考えます。

Tabによる補完

1つ目の補完はTabを使う方法です。Windows PowerShell2を押したあとにTabを押すと

> .\20210101.txt

と表示され、さらにもう一度Tabを押すと

> .\20210103.txt

が表示されます。これは伝統的なコマンドプロンプトの補完方法と同じです。

Ctrl + Spaceによる補完

2つ目の補完方法はCtrl + Spaceを使う方法です。Windows PowerShell2を押したあとにCtrl + Spaceを押すと、2つのファイル名の共通prefixである2021010の部分までが確定して、かつ、1個目の候補である20210101.txtの残り部分 (1.txt) が選択済である状態になります(言葉で説明しづらいので実際に試してみてください)。

Windows PowerShellの補完方法がどうなっているかを調べるにはGet-PSReadLineKeyHandlerというコマンドレットを使います。デフォルト設定では以下でした。

コンプリート機能
========

Key        Function            Description
---        --------            -----------
Ctrl+Space MenuComplete        入力候補が 1 つだけ存在する場合は、その候補で入力を補完します。それ以外の場合は、入力候
                               補のメニューから項目を選択して入力を補完します。
Tab        TabCompleteNext     次の入力候補を使用して入力を補完します
Shift+Tab  TabCompletePrevious 前の入力候補を使用して入力を補完します

Tabによる補完方法を変える

デフォルトのTabの補完方法は耐え難いものがあるので、Tabbashzshのような補完ができるように変更します。その方法が PowerShellでbash-like, zsh-likeなタブ補完 - Qiita に書いてありました。

bash風の補完

> Set-PSReadLineKeyHandler -Key Tab -Function Complete

zsh風の補完(デフォルトでCtrl+Spaceによりなされる補完と同じ)

> Set-PSReadLineKeyHandler -Key Tab -Function MenuComplete

ただし、この設定はWindows Powershellのウィンドウを閉じると消えてしまいます。恒久的にこの設定を有効にしたければ、プロファイル(.bash_profile的な設定ファイル)を編集する必要があります。プロファイルは何個も種類があって正直よくわかってないですが、私は$PROFILE.CurrentUserAllHosts

Set-PSReadLineKeyHandler -Key Tab -Function Complete

を設定しておきました。