at backyard

Color my life with the chaos of trouble.

Denoのsubprocessを用いたtestでTest case is leaking resources.というエラーが出るときの対応方法

Denoのテストで下記のようにsubprocessを使って、コマンド実行結果をテストしようとした。

Deno.test({
  name: "test name",
  fn: async () => {
    const p = Deno.run({
      cmd: [ /*コマンド*/],
      stdout: "piped",
    });

    const { code } = await p.status();

    // 返り値が0であることを確認
    assertEquals(code, 0);

    const rawOutput = await p.output();
    const stdoutResult = new TextDecoder().decode(rawOutput).trim();

    // コマンドの実行結果が"foo"であることを確認
    assertEquals(stdoutResult, "foo");
  }
});

だが、このテストをdeno test --allow-run で実行しようとすると、下記のようなエラーとなる。

AssertionError: Test case is leaking resources.
Before: {
  "0": "stdin",
  "1": "stdout",
  "2": "stderr"
}
After: {
  "0": "stdin",
  "1": "stdout",
  "2": "stderr",
  "4": "child"
}

Make sure to close all open resource handles returned from Deno APIs before
finishing test case.

どうやら開放されていないリソースがあるとこのようなエラーが表示されるらしい。
インターネットで検索してみたところ、日本語で書かれた解説を見つけたので下記に貼る。
(Zennで無償で公開されている Effective Deno内の内容となる)

テストを書こう|Effective Deno

そしてこのリソース開放に関するエラーを解消するためには下記のような対象方法がある。
が、これは上で紹介した書籍内にも書かれているが、あまりおすすめされた方法ではなく、本来であればリソース開放のための処理を記載する必要がある。

Deno.test({
  name: "test name",
  fn: async () => {
    const p = Deno.run({
      cmd: [ /*コマンド*/],
      stdout: "piped",
    });

    const { code } = await p.status();

    // 返り値が0であることを確認
    assertEquals(code, 0);

    const rawOutput = await p.output();
    const stdoutResult = new TextDecoder().decode(rawOutput).trim();

    // コマンドの実行結果が"foo"であることを確認
    assertEquals(stdoutResult, "foo");
  },
  sanitizeResources: false,
  sanitizeOps: false,
});

では、今回のようなsubprocessを用いた例の場合、どうすればよいかと思い調べていたところ、aleph.js 内のとあるPRのテストの書き方が参考になった。
(しかも偶然にも上で紹介したEffective Denoの作者の方が出したPRだった)

github.com

こちらの書き方を参考に下記のようにコードを修正した。

Deno.test({
  name: "test name",
  fn: async () => {
    const p = Deno.run({
      cmd: [ /*コマンド*/],
      stdout: "piped",
    });

    const { code } = await p.status();

    // 返り値が0であることを確認
    assertEquals(code, 0);

    const rawOutput = await p.output();
    const stdoutResult = new TextDecoder().decode(rawOutput).trim();

    // コマンドの実行結果が"foo"であることを確認
    assertEquals(stdoutResult, "foo");

    // リソースを閉じる
    await p.close();
  }
});

これでリソース開放に関する表示は解消された。

以上、備忘録でした。

M1 Mac環境でのcx_Freezeの実行ファイル作成までのメモ

Pythonで配布可能な実行ファイルを作成する際に有効な手段としてcx_Freezeがある。

少し前に下記のポストでも書いたが、似た選択肢としてはpyinstallerがあるが、こちらはライセンスがGPLなので、例えば商用で利用するようなケースでは厳しくなってくる。
その点、cx_FreezeはPython Software Foundation License となっているので、GPLよりは扱いやすい。

shinshin86.hateblo.jp

目次

このポストは少々長いため、まずは目次をこちらに記載する。

M1 MacBook Airだとcx_Freezeのインストールが失敗する(ld: library not found for -lintl)

ところでM1 MacBook Airでcx_Freezeのインストールを試したところ、下記のようなエラーが出て失敗してしまった。

環境は pyenvで構築したPython 3.10.1 である。

インストール時のコマンド

pip install --upgrade cx_Freeze

エラーのメッセージを一部抜粋。

 ld: library not found for -lintl
  clang: error: linker command failed with exit code 1 (use -v to see invocation)
  error: command '/usr/bin/clang' failed with exit code 1

intel MacBook Proだとcx_Freezeのインストールは問題なさそう

intelチップのMacBook Proで同様の環境( pyenv経由で構築したPython 3.10.1 )を再現し、こちらでインストールしたところ、intelチップのmacでは問題なくcx_Freezeのインストールは完了した。

実行ファイルを作成し、下記のtkinterのサンプルアプリケーションが動くところまで確認している。

cx_Freeze/cx_Freeze/samples/Tkinter at main · marcelotduarte/cx_Freeze · GitHub

GitHubでissueを立てたところ、作者の方が相談に乗ってくれた

エラー内容でググっても私と同じような問題が出なかったことや、同様の問題がGitHubのissueにはなかったようなので、issueを立ててみたところ、作者の方が色々と相談に乗ってくれた。

こちらのissue内でより詳細なエラーログや試行したことの内容はすべて記載されているので、より詳細に内容を知りたい方はissueを参照してください。

github.com

M1 MacBook Airでcx_Freezeのインストールを行い、配布可能な実行ファイルを作成するまでのメモ

M1 Macでcx_Freezeを使う際は miniforge 経由でインストールすると動くようだった。

miniforgeを選択した理由

pyenv経由ではminiconda環境を構築することも可能だが、残念ながらminicondaはarm64(m1 mac)には対応していないようだったので、M1でも動く環境であるminiforgeを選択した次第である。

github.com

私は今回Python 3.10.1の環境でcx_Freezeを動かしたかったので、下記のようにセットアップをしてcx_Freezeのインストールを行った。
(実際にはまず3.9系のインストールから行うなど他のことも行っているが、そちらについてはissue内のログを参照いただけたらと思います)

# miniforgeのセットアップ
pyenv install miniforge3-4.10.3-10

# 指定したディレクトリ内のみでminiforge環境を有効化
pyenv local miniforge3-4.10.3-10

# 仮想環境構築
export SDKROOT=/Library/Developer/CommandLineTools/SDKs/MacOSX11.1.sdk
conda create -n cx310conda -c conda-forge python=3.10 libpython-static -y
conda init zsh
source ~/.zshrc
conda activate cx310conda

# cx_Fre
conda install -c conda-forge c-compiler importlib-metadata -y
pip install --no-binary :all: --pre cx_Freeze -v

なお、cx_Freezeのページを見ると下記のようなインストール方法も紹介されていたが、

conda install -c conda-forge cx_freeze

私の環境ではこの方法でインストールしようとしても、

PackagesNotFoundError: The following packages are not available from current channels:

というエラーが出てしまい、installできなかったため、上記のやり方でインストールしている。

なお、こちらについては作者の方でconda-forgeにosx-arm64のビルドを登録リクエストしてくれたようなので、そのうち conda-forge でもインストールが可能になるかもしれない。

というわけで、miniforge を用いることでm1 macでもcx_Freezeが使えるようになった。

下記のキャプチャは実際にcx_Freezeを使って作成したtkinterのサンプルアプリを実行したところ。

f:id:shinshin86:20220113204137p:plain
tkinterのサンプルアプリ

追記: Apple Silicon用のビルドがconda intallを用いたインストールにも対応した

現在はconda install でもM1(Apple Silicon) Mac用のビルドがダウンロードできるようになった。そのため下記のコマンドでインストールが可能。

conda install -c conda-forge cx_freeze

cx_Freezeを用いてmac環境で配布可能な実行ファイルを作成する方法

次にmac環境で配布可能な実行ファイルを作成する方法だが、これも作者の方に教えてもらったのだが、mac環境で配布可能な実行ファイルを作成する方法は下記のようだ。

python setup.py bdist_mac

https://cx-freeze.readthedocs.io/en/latest/setup_script.html#bdist-mac

これで上に貼ったtkinterのサンプルアプリケーションをcx_Freezeを用いて配布可能な実行ファイルとしてコンパイルすることができた。

f:id:shinshin86:20220113204003p:plain
cx_Freezeを使って配布可能な状態となった実行ファイル

作者の方には色々と相談に乗ってくれて感謝である。

以上のようなやり方で、miniforgeを使うことでM1 Macでもcx_Freezeを扱うことができるので、興味ある方はぜひ利用してみてください。

JavaScriptで動的に関数の引数を与える場合はスプレッド構文を使えばよかった

最近、こういう使い方もできるのかと学んだので備忘録がてら残しておこうと思ったのだが、書き始めてからMDNを参照したら、自分がこれから書こうと思ったそのままの例が載っていて、自分の知識の無さに幻滅した次第。

developer.mozilla.org

せっかく書き出したのでこちらのポストはそのまま公開しておくが、まあJavaScriptの基本でした。

以下、本文。

const arr = [1,2,3]
console.log(...arr)

このコードを実行すると、実行結果は下記となる。

1 2 3

で、下記のようなコードがあった場合、sum関数の返り値は6となる。

function sum(n1,n2,n3) {
  if(n1 && n2 && n3) {
    return n1 + n2 + n3;
  } else if (n1 && n2) {
    return n1 + n2;
  } else {
    return "足し算をするための引数が足りません";
  }
}


const arr = [1,2,3]
console.log(...arr)
console.log(sum(...arr))

配列をスプレッド構文で展開した場合 1 2 3 となるが、これはそのまま関数の引数として良さげに渡すことが可能なようだ。

これを使えば動的に引数を関数に渡すことができると思ったのだが、最初にも書いたとおり、JavaScriptの基本だった。
「こういう使い方もできるんだ!」とドヤってポストしなくてよかったと思った。

colors.jsの今回の問題で我々が対応すべきことについての個人的なメモ(実務的な部分のみ)

colors というnpmパッケージの最新versionについて意図的に悪意のあるコードが実行されるようになっている。

こちらの問題のあるversionなどの具体的な内容についてはazuさんが分かりやすくまとめてくれているので、こちらを参照してみるのが良い。

zenn.dev

npmパッケージで依存しているパッケージは多いので、影響はでかい。例えばFirebase CLIも一時期影響を受けていたようだ。

qiita.com

目次

追記にも書いたが、日本時間の2022年1月11日現在、悪意あるバージョン(1.4.1や1.4.2)はnpmからは削除されており、colors.jsをインストールしても問題ない状態になっている。

影響のあるversionと対応方法

なお、影響のあるversionについて引用させていただくと、

  • 1.4.44-liberty-2
  • 1.4.1
  • 1.4.2

となっており、 ^1.0.0 などのversion指定で入っている場合、2022年1月10日時点での1.x.xの最新である1.4.2が選択される状態となっているよう。

colorsを利用しているかは下記のコマンドで確認が可能。

npm ls colors

yarn why colors

もし利用している場合、下記のようにバージョン固定でインストールをするなどの対応が必要。
(1.4.0 は問題ないバージョンであることが確認されており、また、npmでは現在publishしてから24時間以上経過したパッケージのunpublishは基本的にできないようになっているため、問題ないバージョンであることは保証される形となっている。ここらへんも詳細は上の記事を読んでください)

# npm install
npm install colors@1.4.0

# yarn add
yarn add colors@1.4.0

また、colorsはよく使われているパッケージのため、依存の依存に入っていることも少なくない。

こちらについては基本的にパッケージの更新待ちとなるため、更新が来たら npm update で更新する。

動く状態の package-lock.json がある場合は、現状はそのままにしておくのが良い。

package-lock.json 内の colors1.4.0 に直接書き換える方法もあるようだが、これは良いやり方かはちょっとわからない。
(こちらは上に貼った記事内にも記載がある。コードのdiffなども載っているのでそちらの参考をしてみてください)

なお、colorsのリポジトリは下記。

github.com

colors.jsの作者が書いたfaker.jsについても最新版では空の状態となっているので、こちらについても最新版を入れると動かないものと思われる。

https://github.com/Marak/faker.jsgithub.com

最後に

私は作者の心情を計り知ることはできないし、よってそこについては何も書かない。

が昨晩、この問題が発覚して自身でもGitHubのissueを見たり、Twitterでの様々な方の発言などを読んだりしていた。
Twitterには、様々な角度・立場からの発言が溢れていて読んでいてすごく考えさせられるものも多かった。

また、偶然にも、ちょうど昨日日中にPHPの現場でOSSについての話を聞いていて、OSSの難しさについてもちょっと考えたりしていたのもすごい偶然だった。

php-genba.shin1x1.com

色々と自分の中でも様々な考えが交差するが、うまくそれを言葉にするのが難しい。

よってここで筆を置こうと思う。

追記:2022年1月11日時点で、悪意あるバージョン(1.4.1や1.4.2)はnpmからは削除されている

2022年1月11日時点で、悪意あるバージョン(1.4.1や1.4.2)はnpmからは削除されており、現在はcolors.jsをインストールしても問題のないバージョンがインストールされるようになっている。

colors - npm

またこちらの問題については下記のように報告されているので、そちらのリンクも貼っておく。

github.com

tkinterでLabelのテキストを変更する方法

tkinterのLabelのテキストを変更する方法に関する備忘録。

Labelのテキストを変更する方法は簡単で、tk.Label で生成したLabelインスタンスtext 属性を変更することで、テキストを変更することができる。

具体的には

label["text"] = "変更後のテキスト"

といった形で変更可能。

tkinterでLabelのテキストを変更するサンプル

サンプルコードを下に記載する。

import tkinter as tk
from datetime import datetime

class App:

    def __init__(self, root):
        self.root = root
        root.title("Label text change sample")
        root.geometry("250x100")

        self.datetime_label = tk.Label(root, text="")
        self.datetime_label.pack()

        tk.Button(text="Display datetime", width=20, command=self.display_datetime).pack()
        root.mainloop()

    def display_datetime(self):
        self.datetime_label["text"] = datetime.now()


def main():
    root = tk.Tk()
    app = App(root)
    app.root.mainloop()


if __name__ == "__main__":
    main()

このコードを実行して、Display datetime ボタンを押すと、現在時刻がLabel上に表示される。

f:id:shinshin86:20220108212520g:plain
実際にこのアプリを動作させたところ

なお、こちらのコードはGitHubにも上げている。

github.com

Labelのテキストを変更する際に気をつけたいところ

これは個人的に一瞬ハマりかけたところなのだが、tkinterではLabelなどの生成時にメソッドチェーン的な形でpackgrid をつなげて書くことができる。

例えば下記のような形。
(上に貼ったサンプルコードの一部を引用している)

tk.Button(text="Display datetime", width=20, command=self.display_datetime).pack()

このように1行で書けるのは便利なので、ついついなんでもこう書いてしまいがちだが、このpackgrid などの返り値は NoneType であることに注意したい。

つまり、下記のように書くとエラーとなる。
(これも上のコード内の引用)

self.datetime_label = tk.Label(root, text="").pack()

こうやって書いてしまうと、self.datetime_label には LabelのインスタンスではなくNoneType が入るため、このままアプリを起動してボタンを押すと、

TypeError: 'NoneType' object does not support item assignment

というエラーが出てしまう。

一瞬なぜエラーになったのか分からなかったが、落ち着いて考えてみればNoneTypeが代入されてしまっているだけということに気づいた。

ついついメソッドチェーンで繋げて書くのは便利なので使いがちだが、こういう細かいところで気をつけていかねばと思った次第である。

pyinstallerをmacで利用したときに"OSError: Python library not found: libpython3.10m.dylib, libpython3.10.dylib, Python3, .Python, Python" というエラーが出た場合の対応手順メモ

pyinstallerを利用したときのエラー対応に関する備忘録です。

目次

環境

エラーが起きた際の私の環境は以下。

事象

pyinstallerを用いて下記のようなコマンドで実行ファイルを生成しようとしたら、

pyinstaller --onefile --noconsole --icon=icon.png main.py

下記のようなエラーが出て失敗した

OSError: Python library not found: libpython3.10m.dylib, libpython3.10.dylib, Python3, .Python, Python

対応方法

pyenvを用いてPython環境の構築は行っていたが、PYTHON_CONFIGURE_OPTS="--enable-shared" というオプションを付けて、再度上書きでインストールする。

PYTHON_CONFIGURE_OPTS="--enable-shared" pyenv install 3.10.1

その後、一度pyinstallerをuninstallして、再度install。

pip uninstall pyinstaller
pip install pyinstaller

これでpyinstallerを利用したときに発生していたエラーは解消し、実行ファイルの生成が行えた。

以上備忘録でした。

余談: pyinstallerのライセンスはGPLなので商用利用などには注意

ちなみにこれらの備忘録を書きながらpyinstallerを色々と試していたのですが、ドキュメントを読んでいたときにpyinstallerのライセンスはGPLだということに気づいた。

pyinstaller.readthedocs.io

そのため商用利用などするときには注意が必要。

一応下記のような形で回避する方法もあるようだが、ポスト内にも書かれているように自己責任で、ということみたい。

spoonblog.hatenablog.com

pyinstallerはぐぐると日本語の情報もたくさん出てくるし、メジャーなやり方のようだが、ここについては気をつけたいところ。

pyinstallerがGPLで使えないなら、cx_Freezeという選択肢?

pyinstallerのだいたいツールとしてはcx_Freezeというツールもあるようだ。

github.com

こちらはまだ触っていないが、ライセンスはPython Software Foundation Licenseとなっている。

追記: ちょっと触ってみたが自身の環境だとcx_FreezeはM1 Macだと動かずにInter Macだと動いた。

調べながらのメモはtwitterに投稿しているので、そちらをこちらにもはる。

さらに追記。

その後、色々とあり、M1 Mac環境でもcx_Freezeを使えるようになった。

そちらについては下記のポストにまとめているので参照してみてください。

shinshin86.hateblo.jp

tkinterとsqlite3を使ってGUIアプリを作ってみる

tkinterとsqlite3(SQLite)を使ってみた際の備忘録です。

sqlite3でdatetimeを(擬似的に)扱う方法については下記を参考にしている。

qiita.com

色々と実装例を見ていると、フロントエンドとバックエンドで構成するほうが良さそうだが、今回はサンプルということで一つのコードにまとめてしまっている。

作ったアプリは下記のようなメッセージを登録・表示できるだけのアプリ。

f:id:shinshin86:20220105232147p:plain

アプリの動作概要としては、

  • 画面上部のフォームにメッセージを入力して、Save to SQLite ボタンを押すことでsqlite3を使ってSQLiteにデータを書き込む。
  • また、画面下部にある View all data ボタンを押すことでSQLite内のすべてのデータを取得してListbox内に表示する。

以上のようなとてもシンプルなアプリ。

実際のソースコードは下記となる。

import tkinter as tk
import tkinter.messagebox as tkm
import sqlite3
from datetime import datetime

DB_NAME = "msg.db"

# msg.dbが作られていない場合は作成を行う
def init_db_table():
    # set converter
    detect_types = sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES
    sqlite3.dbapi2.converters['DATETIME'] = sqlite3.dbapi2.converters['TIMESTAMP']

    conn = sqlite3.connect(DB_NAME)
    cur = conn.cursor()
    sql_statement = "CREATE TABLE IF NOT EXISTS msg (id INTEGER PRIMARY KEY, message TEXT, created_at datetime)"
    cur.execute(sql_statement)
    conn.commit()
    conn.close()


# DB内のデータを全件取得
def view():
    conn = sqlite3.connect(DB_NAME)
    cur = conn.cursor()
    cur.execute("select * from msg")
    raws = cur.fetchall()
    conn.close()
    return raws


# DBにデータを追加する
def insert(message):
    conn = sqlite3.connect(DB_NAME)
    cur = conn.cursor()
    sql_statement = "insert into msg values(NULL, ?, ?)"
    cur.execute(sql_statement, (message, datetime.now()))
    conn.commit()
    conn.close()


# insert関数を通じて入力されたmessageを保存するための関数
def save_msg(message):
    insert(message)
    tkm.showinfo("Save", "Save message")


# view関数を通じて取得した全件分のデータを整形してListboxに表示するための関数
def view_msg(listbox):
    listbox.delete(0, tk.END)
    for row in view():
        data = "ID: " + str(row[0]) + ", Message: " + row[1] + ", CreatedAt:" + str(row[2])
        listbox.insert(tk.END, data)


# アプリ起動時にDBが作られていない場合はDBの初期化を行う
init_db_table()

# GUIの初期化
root = tk.Tk()

label = tk.Label(root, text="Your message")
label.pack()

text_form = tk.Entry(root)
text_form.pack()

save_button = tk.Button(root, text="Save to SQLite", width=20, command=lambda: save_msg(text_form.get()))
save_button.pack()

listbox = tk.Listbox(root, width=50, height=15)
listbox.pack()

view_button = tk.Button(root, text="View all data", width=20, command=lambda: view_msg(listbox))
view_button.pack()


root.title("sqlite3 sample")
root.geometry("600x400")
root.mainloop()

コードについては以上となる。

tkinterのより詳細な使い方については、以前下記の記事でもまとめているので、よろしければご覧ください。

shinshin86.hateblo.jp

あと、上のポストにも書いているのだけど、tkinterで日本語入力が行えないことの解決方法についてはZennにも記載していますので、そちらも併せて参照してみてください。
(原因を調べた限り、Mactkinterを使おうとすると結構この問題については遭遇する方も多そうなので、こちらのリンクもはらせてもらいます)

zenn.dev