Ubuntu 20.04にemacsをインストール

WSL2上のUbuntu 20.04にemacsをインストールしようとして

$ sudo apt install emacs

としたところインストールできませんでした(エラーメッセージは記録し忘れました)。

お決まりの

$ sudo apt update
$ sudo apt upgrade

したあともう一度最初のコマンドを打てばインストールできました。ただし、バージョンは26.3でやや古いです(現時点で最新は27.2)。

その際のログを載せておきます。

$ sudo apt install emacs
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following additional packages will be installed:
  adwaita-icon-theme at-spi2-core emacs-bin-common emacs-common emacs-el emacs-gtk emacsen-common fontconfig
  fonts-droid-fallback fonts-noto-mono fonts-urw-base35 ghostscript gsfonts gtk-update-icon-cache hicolor-icon-theme
  humanity-icon-theme imagemagick-6-common libatk-bridge2.0-0 libatk1.0-0 libatk1.0-data libatspi2.0-0
  libavahi-client3 libavahi-common-data libavahi-common3 libcairo-gobject2 libcairo2 libcolord2 libcups2 libdatrie1
  libepoxy0 libfftw3-double3 libgd3 libgdk-pixbuf2.0-0 libgdk-pixbuf2.0-bin libgdk-pixbuf2.0-common libgif7 libgomp1
  libgraphite2-3 libgs9 libgs9-common libgtk-3-0 libgtk-3-bin libgtk-3-common libharfbuzz0b libidn11 libijs-0.35
  libjbig0 libjbig2dec0 libjpeg-turbo8 libjpeg8 liblcms2-2 liblqr-1-0 libm17n-0 libmagickcore-6.q16-6
  libmagickwand-6.q16-6 libopenjp2-7 libotf0 libpango-1.0-0 libpangocairo-1.0-0 libpangoft2-1.0-0 libpaper-utils
  libpaper1 libpixman-1-0 librest-0.7-0 librsvg2-2 librsvg2-common libsoup-gnome2.4-1 libthai-data libthai0 libtiff5
  libwayland-cursor0 libwayland-egl1 libwebp6 libwebpmux3 libxcb-render0 libxcursor1 libxkbcommon0 m17n-db
  poppler-data ubuntu-mono
Suggested packages:
  mailutils emacs-common-non-dfsg fonts-noto fonts-freefont-otf | fonts-freefont-ttf fonts-texgyre ghostscript-x
  colord cups-common libfftw3-bin libfftw3-dev libgd-tools gvfs liblcms2-utils m17n-docs libmagickcore-6.q16-6-extra
  librsvg2-bin poppler-utils fonts-japanese-mincho | fonts-ipafont-mincho fonts-japanese-gothic | fonts-ipafont-gothic
  fonts-arphic-ukai fonts-arphic-uming fonts-nanum
The following NEW packages will be installed:
  adwaita-icon-theme at-spi2-core emacs emacs-bin-common emacs-common emacs-el emacs-gtk emacsen-common fontconfig
  fonts-droid-fallback fonts-noto-mono fonts-urw-base35 ghostscript gsfonts gtk-update-icon-cache hicolor-icon-theme
  humanity-icon-theme imagemagick-6-common libatk-bridge2.0-0 libatk1.0-0 libatk1.0-data libatspi2.0-0
  libavahi-client3 libavahi-common-data libavahi-common3 libcairo-gobject2 libcairo2 libcolord2 libcups2 libdatrie1
  libepoxy0 libfftw3-double3 libgd3 libgdk-pixbuf2.0-0 libgdk-pixbuf2.0-bin libgdk-pixbuf2.0-common libgif7 libgomp1
  libgraphite2-3 libgs9 libgs9-common libgtk-3-0 libgtk-3-bin libgtk-3-common libharfbuzz0b libidn11 libijs-0.35
  libjbig0 libjbig2dec0 libjpeg-turbo8 libjpeg8 liblcms2-2 liblqr-1-0 libm17n-0 libmagickcore-6.q16-6
  libmagickwand-6.q16-6 libopenjp2-7 libotf0 libpango-1.0-0 libpangocairo-1.0-0 libpangoft2-1.0-0 libpaper-utils
  libpaper1 libpixman-1-0 librest-0.7-0 librsvg2-2 librsvg2-common libsoup-gnome2.4-1 libthai-data libthai0 libtiff5
  libwayland-cursor0 libwayland-egl1 libwebp6 libwebpmux3 libxcb-render0 libxcursor1 libxkbcommon0 m17n-db
  poppler-data ubuntu-mono
0 upgraded, 81 newly installed, 0 to remove and 0 not upgraded.
Need to get 67.2 MB of archives.
After this operation, 273 MB of additional disk space will be used.
Do you want to continue? [Y/n] Y
(以下略)

ちなみに、snapを使ってemacsをインストールする方法も本来は存在するのですが、WSL2上のUbuntu 20.04では以下のようにエラーが出ました。

$ sudo snap install emacs --classic
Interacting with snapd is not yet supported on Windows Subsystem for Linux.
This command has been left available for documentation purposes only.

emacsの検索・置換に関するメモ

emacsを使うとき、検索はC-s、置換はM-%を使う以上のことをこれまでしてきませんでした。より高度な検索・置換の仕方をまとめました。

検索

単語単位で検索

いくつか方法があります。ユースケースに応じて使い分けてください。

1つ目の方法は、C-s M-s w としてから、検索したい単語を入力する方法です。( 参考 )

2つ目の方法は、C-sでのインクリメンタルサーチ中に、M-s w とする方法です。これで単語単位の検索をするか否かのフラグがトグルします。( 参考 )

3つ目の方法は、まず検索したい語にカーソルがある状態でC-s C-wとしてその語を選択状態にした上で、M-s wとする方法です。( 参考

コマンドが覚えられない場合は、メニューバーの"Options" → "Default Search Options" → "Whole Words"にチェックをつける方法も可です。なお、no windowモードでemacsを起動している場合は、F10でメニューを開けます。

大文字・小文字を区別して検索

これもいくつか方法があります。

1つ目の方法は、C-s M-cとしてから、検索したい単語を入力する方法です。

2つ目の方法は、C-sでのインクリメンタルサーチ中に、M-c とする方法です。これでcase-sensitiveで検索をするか否かのフラグがトグルします。( 参考 )

これも、メニューバーの"Options" → "Default Search Options" → "Ignore Case"をアンチェックする方法も可です。

正規表現で検索

C-u C-sとしてから、検索したい正規表現を入力します。

正規表現は、Pythonに組み込まれているような高度なバージョンではないようです。例えば数字一文字を表す\dは使えず、[0-9]とする必要があります。詳細は ここ を参照。

過去の検索キーワードを呼び出す

C-sでのインクリメンタルサーチ中にM-pまたはM-nです。

参考

文字列検索に関するフラグに関するコマンドは C-s C-h bで調べられます

置換

単語単位で置換

C-u M-%ののち、置換前の単語を入力してEnter、置換後の単語を入力してEnterです。 ( 参考 )

大文字・小文字を区別して置換

デフォルトだと置換前の単語をすべて小文字で入力すると、大文字・小文字をいい感じに変換してくれます (参考) が、これがおせっかいだという場合は、大文字・小文字を区別して置換したくなります。

https://emacs.stackexchange.com/questions/12780/how-to-perform-case-sensitive-query-replace を見る限り、ある単語の検索の途中に、さっき紹介した M-c のようなコマンドでフラグを切り替える方法は用意されてないようでした。

メニューバーの"Options" → "Default Search Options" → "Ignore Case"をアンチェックする方法以上に簡単な方法は今のところ見つけられていません。

正規表現で置換 (query-replace-regexp)

C-M-% もしくは M-x replace-regexp としてから、検索したい正規表現を入力してEnter、置換後の正規表現を入力してEnterです。

C-M-%の入力方法は以下のどちらかです。

  • Escを先押ししたあと、Ctrl + Shiftを押しながら5を押す
  • Ctrl + Alt + Shiftを押しながら5を押す

過去の置換キーワードを呼び出す

置換中にM-pまたはM-nです。

置換前・置換後の文字列を変更

eで置換前、Eで置換後の文字列を変更可能です(参考)。上の「過去の置換キーワードを呼び出す」と組み合わせると便利です。

scikit-imageを使ってサンプル画像を簡単に取得

scikit-imageというPythonのライブラリを使うと、サンプル画像をさっと取ってくることができます。

コード例は以下です。

import skimage


print("scikit-image version: {}".format(skimage.__version__))
coffee_image = skimage.data.coffee()
print(coffee_image.shape)

Google Colabでの実行例を以下に示します。縦400px、横600px、カラー画像がNumPy形式で取得されました。dtypeはnp.uint8です。色順はRGBです。

0.16.2
(400, 600, 3)

どんな画像が取得できたかを見てみます。

import matplotlib.pyplot as plt


plt.imshow(coffee_image)
plt.title('coffee_image')
plt.show()

結果は以下です。 f:id:minus9d:20210731232017p:plain

他にどんな画像を取得できるかは 公式API を見てください。バージョンにより微妙にAPIが異なる(例えば0.19.xで提供されているcat()はscikit-iamge 0.16.2では存在しない)ため要注意です。

取得可能な画像を無理矢理全列挙するコードを以下に示します。

import skimage
import matplotlib.pyplot as plt


for i in dir(skimage.data):
  attr = getattr(skimage.data, i)
  if callable(attr) and not i.startswith('_'):
    try:
        img = attr()
        plt.imshow(img)
        plt.title("{} (shape {})".format(i, img.shape))
        plt.show()
    except Exception:
      pass

結果のうち冒頭部分を以下に示します。

f:id:minus9d:20210802213331p:plain

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