きり丸の技術日記

技術検証したり、資格等をここに残していきます。

testcontainers-pythonでMinioを扱う

testcontainers-pythonでminioを使う際に少々手間どったのでメモに残します。

なお、この記事ではtestcontainers自体のメリット等については記載いたしません。

環境

  • Python
    • 3.12
  • boto3
    • 1.34.69
  • minio
    • 7.2.5
  • testcontainers
    • 4.1.1

ゴール

boto3minioに対してファイルアップロードできたことを検証します。minioについてはtestcontainersでセットアップします。

下準備

testcontainersminiodevDependenciesに加えます。各々のパッケージマネジャで読み替えてください。

rye add --dev minio
rye add --dev testcontainers

minioの起動

まずは、access_keysecret_keyを指定します。指定しない場合のデフォルトはminioadminです。

また、localの検証で使用しているminioと同じimageを指定しましょう。理由はわかりませんが、ライブラリではminio/minio:RELEASE.2022-12-02T19-19-22Z"を指定しています。

起動ポートを修正したい場合はportを指定しましょう。基本的にはデフォルトの9000で問題ないはずです。

from testcontainers.minio import MinioContainer

async def test_01(self):
    config = MinioContainer(
        access_key="minio", secret_key="minio1234", image="minio/minio"
    )

次に、get_client()で起動後のインスタンスを取得します。commandは簡単には渡せなさそうですので、このインスタンスを元に必要なバケットを作成します。今回はtmp.localバケットを作成しました。

次にDocker環境でテストする場合は不要ですが、それ以外の環境でテストする場合はTestContainersを外部からアクセスすることになるので、外部ポートを知る必要があります。minio.get_config()["endpoint"]でエンドポイントの情報を取得してください。今回の私は環境変数のS3_URLに格納しています。

with config as minio:
    minio_client = minio.get_client()
    minio_client.make_bucket("tmp.local")
    os.environ["S3_URL"] = minio.get_config()["endpoint"]W

そのほかに取得できる情報としては次のとおりです。

{
  'endpoint': 'localhost:32841',
  'access_key': 'minio',
  'secret_key': 'minio1234'
}

boto3をセットアップする

今まで設定したaccess_keysecret_keyS3_URLboto3の作成時に渡してください。

import os

import boto3  # type: ignore

boto3.client(
    "s3",
    endpoint_url=f"http://{os.getenv('S3_URL', 'localhost:9000')}",
    aws_access_key_id="minio",
    aws_secret_access_key="minio1234",
)

ファイルアップロードする

ファイルアップロードするロジックはboto3の使い方なので省略します。詳細はソースコードを見てください。

アップロードしたファイルを検証する

minioのインスタンスを操作すればアップロードしたファイルを確認できます。

minio_client.get_object("任意のbucket", "任意のキー").data

ソースコード

終わりに

miniotestcontainersのexampleとしては問題ないのですが、実際にboto3からアクセスしようとしたときにポートが分からなくて困りました。最終的な本番環境ではS3を使用する人が多いと思うので、boto3の方がexampleとしては嬉しいんですがね…。

ただ、今回testcontainersを使用してみましたが、正直な感想としてはテストが遅くなるのでちゃんとユースケースは絞って利用したいですね。並列実行のテストをするときに、今回のtestcontainersは役立つと思われます。

参考情報

【Scrap】pytestについて

ZennのScrapsと同じような感覚の記事。間違っている可能性は十分にあります。

今回の記事ではpytestについて自分が調べたことをまとめます。

環境

  • Python
    • 3.7.9
  • pytest
    • 7.4.3
  • pytest-check
    • 2.2.2

Pytestについて

処理順番

pytestではヒットした順番で順次テストするのではなく、collectionというテスト対象のメソッドを集めた過程を経てテストを実行します。この実行前のcollectionで先頭にTestが付いているクラス、test_がついているメソッドを探しています。感覚的には、200テストを探すのに1秒くらいかかるので、テストケースが増えれば増えるほど起動までに時間がかかります。

--last-failedというオプションを付けて実行すると、前回のセッションで失敗したテストだけを実行してくれます。しかし地味にcollectionに時間がかかるので、失敗したテストが複数ファイルに跨っていない限りは、失敗したファイルだけで実行したほうが早く済みます。このオプションを付与しているときは、collection対象を減らすというコントリビュートチャンスかも。

--continue-on-collection-errorsというオプションをつけると、通常はcollection処理中に一部テストで構文エラーがあった時にはテストを実行しませんが、付与すると構文エラーがあっても他のテストを実行してくれます。基本的にはIDEやプラグインでしか使わないオプションだと思います。

テストレポートについて

メインのテストレポートについては、hook機能を使用すると書き換えられます。

@pytest.hookimpl(hookwrapper=True, trylast=True)
def pytest_runtest_makereport(item, call):
  pass

使い方は別のライブラリであるpytest-checkを見るのがいいかも。

厄介なのがshort test summary infoという箇所については、一苦労が必要です。次の処理箇所がpytestshort test summary infoのメッセージを作成している箇所です。メインのエラーメッセージをstrで渡すこともできますが、その場合はAttributeErrorの処理に入ってしまって値が取得できません。

try:
    # Type ignored intentionally -- possible AttributeError expected.
    msg = rep.longrepr.reprcrash.message  # type: ignore[union-attr]
except AttributeError:
    pass

そのため、pytestに合わせて次のように値を渡す必要がありました。

# ASIS
try:
    raise AssertionError(report.longrepr)
except AssertionError:
    excinfo = ExceptionInfo.from_current()
call.excinfo = excinfo
# TOBE
from _pytest.reports import ExceptionChainRepr
from _pytest._code.code import ExceptionRepr, ReprFileLocation

try:
    raise AssertionError(report.longrepr)
except AssertionError as e:
    excinfo = ExceptionInfo.from_current()
    reprcrash = ReprFileLocation(item.nodeid, 0, str(e))
    reprtraceback = ExceptionRepr(reprcrash, excinfo)
    chain_repr = ExceptionChainRepr([(reprtraceback, reprcrash, str(e))])
    report.longrepr = chain_repr

call.excinfo = excinfo

もうちょっと頭のいいやり方があったかもしれないのですが、上のようにしてlogreprstrではなくExceptionChainReprで渡せばshort test summary infoに値を渡せました。

その件はこちらのPull Requestに入れています。

オプションについて

とりあえず実行時につけているオプション。このセクションには中身は無いです。

# -p no:warnings はPython側の機能です
addopts = ["-p no:warnings", "-p no:logging", "--last-failed", "-s"]

もっと便利になりそうなオプションがあったら追加します。

量が多くて読み切れていないです。

Pytest-Checkについて

PythonAssertion Rouletteにならないように回避するライブラリ。

JavaでのSoftAssertionsと同じような処理をします。

こちらの紹介記事に関しては、PullRequestがmergeされたら記載する予定です。

ソースコード

なし

終わりに

正直。pytest-checkへのPullRequestを作成したときに色々と調べたメモです。今後もPytest関連のライブラリにコントリビュートするかは置いておいて、一瞬だけ使用する知識としておくのももったいないのでScrapという形で記事化しました。

参考文献

Pythonのclassは暗黙的にobjectを継承している(reportMissingSuperCallの対応)

pyrightを使用している際に、reportMissingSuperCallが発生したので対応していました。

error: Method "__init__" does not call the method of the same name in parent class (reportMissingSuperCall)

ただし、コード上は特に何も継承していません。

class Hoge:
    def __init__(self, name: str):
        self.name = name

このエラーメッセージを対応するために調査した内容をメモします。

環境

  • Python
    • 3.12

原因

掲題のとおりです。Python2ではclassを作成する際に明示的にobjectを継承する必要がありましたが、Python3からは暗黙的にobjectを継承するようになりました。

# Python3の暗黙的継承
class Hoge:
    def __init__(self, name: str):
        self.name = name

# Python2の明示的継承
class Hoge(object):
    def __init__(self, name: str):
        self.name = name

ただ、具体的にPython3が暗黙的にobjectを継承しているかどうかについては、ヘルプページに記載されていませんでした。私は読み取れませんでしたがChatGPTによれば、この行が暗黙的にobjectを継承していることを示しているそうです。

Pythonのヘルプページ

Classes provide a means of bundling data and functionality together. Creating a new class creates a new type of object, allowing new instances of that type to be made. Each class instance can have attributes attached to it for maintaining its state.

ChatGPTの回答

全てのクラスが object クラスから派生するというPythonのクラス理論が「暗黙的に」言及されています

この説明は、クラスがデータと機能を結びつける手段を提供し、新しいクラスの作成が新しいオブジェクトタイプの作成を可能にすること、新しいタイプのインスタンスが作れるようになること、各クラスのインスタンスが状態を維持するための属性を割り当てることが可能であることを言います。

これらの特徴は全てのPythonクラスに共通し、これらの特徴を提供するのが object クラスということになります。したがって、全てのクラスが暗黙的に object クラスを継承していると理解して間違いないです

reportMissingSuperCallの対応

__init__は継承元となったobjectにて定義されているので、エラーメッセージにしたがって素直に呼び出しましょう。

class Hoge:
    def __init__(self, name: str):
        super().__init__()
        self.name = name

ソースコード

ちゃんとobjectを継承していることを確認しています。なお、このテストコードで本当に検証しきれているかは自信ないです。

終わりに

基本的にどのプログラミング言語もインスタンスの同値比較をするためにequalを持つと便利であり、そのためには基底クラスとしてequalを定義したObjectを継承していることが多いということは知っていました。

ただ、Pythonではオブジェクト指向でないためか、Objectを継承しているという認識がありませんでした。

正直なところ、暗黙的な構文はpyrightにはエラー出さないで欲しいです。素直に明示的な構文だけをエラーとしてくれると非常に嬉しいのですが…。動的型付言語という性質上、難しいのかもしれません。