Boto3の補完を可能にするboto3-stubsを使ってみる

Boto3を使用したコードを書いていて補完や型チェックが効かなくて困ったことはないでしょうか? 自分はメソッドの引数や返り値を確認するために公式リファレンスとコードを行ったり来たりしながら、コードを書いてました。 そんな悩みを解決してくれるパッケージ「boto3-stubs」を使用してみます。
2023.02.22

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Boto3とインテリセンス

Boto3はAWSを使う上でとても便利なPythonのパッケージです。

ただ、個人的には一つだけ難点があってそれは補完が効かないことです。 AWSには数多のサービスがあってその各メソッドを覚えておくなんてことは自分には無理でした。

その都度、公式のBoto3のドキュメントを参照し、引数や返り値を確認するのが常となってました。

あー、Boto3でも補完が効いたらな。
でも、こんな引数で呼び足す形でクライアントのインスタンス作成してたら補完なんてできるわけないよな。

クライアントの作成

import boto3

s3 = boto3.client('s3') # <- 引数で返ってくるインスタンスの種類が変わるので、実行までクラスを判別できないと思っていた

そんな思いに応えてくれるパッケージがありました。

近しいパッケージでboto-stubsというのがありますが、それとは別です。

boto3-stubs自体はBoto3のタイプアノテーション用に型情報を提供してくれるパッケージなのですが、その副次作用として補完が使えるようになります。

当然、mypyを使用した型チェックも可能です。(すばらしい!)

インストール

pipやCondaを利用してインストールすることが可能です。 このパッケージをインストール後、エディターが対応していれば補完を行うことができます。 自分はVSCodeを使っているのですが、無事補完が効くようになりました。

インストールは以下のように使用する、AWSサービスの単位でインストールします。 インストールする際はBoto3と同じバージョンのものを使うようにしましょう。

インストール

pip insall boto3-stubs['s3']

ちなみにVSCodeの場合は「AWS Boto3」という拡張を使ってこのパッケージをインストールすることも可能です。

拡張機能をインストール後にVS Codeの拡張で「AWS Boto3: Auto-Discover Boto3 services in current project」というコマンドを実行すると、現在のプロジェクト内で使用しているAWSのサービス用の型情報のパッケージをリストアップしてくれます。

チェックがついているものがインストールされます。

補完がどのくらい効くのか確認する

client

Boto3ではboto3.clientを使用してAWSサービスのクライアントを作成可能です。 これにはローレベルのAPIがメソッドとして備わっていて、その補完が効くのか試してみましょう。

以下はサービスのクライアントを作成するときのサンプルコードです。

service

import boto3

s3 = boto3.client('s3') # <- ここではS3のクライアントを作成している。

s3.と打ってみて補完が出てくるかを見てみます。

公式リファレンスにあるメソッドが列挙されています。 引数までしっかりと教えてくれています。 少し意地悪してDynamoDBのクライアントも作成しましたが、混同することなく補完が効いています。

resource

Boto3では先程のclient以外にも、resourceAPIというものがあります。 こちらは、より高レベルなAPIとなっていて、よく使うようなアクセスパターンをメソッドとして実装し、より手軽に使えるようになっています。 以下ではS3について取り上げますが、代表的なリソースとしては「Bucket,Object」など馴染みのある単語のリソースがあります。

resource

import boto3

s3 = boto3.resource('s3')

bucket = s3.Bucket('xxx')

objs = bucket.objects.all()

どんなリソースがあるのか補完を確認してみます。

次にリソースにどんなメソッドが実装されているのか補完を確認します。

これについても公式リファレンスと同様のものが表示されています。

paginator

Boto3ではリスト形式で複数のアイテムが返ってくるようなAPIに関してはページネータを利用できる場合があります。

ページネータについてはこちらの記事の後半が参考になるかと思います。

以下のように使用したいメソッド名を引数に渡して、ページネータを作成します。 ページネータのpaginateメソッドに、使用したい関数と同じ引数を渡すことで、イテレータが返ってきます。

paginator

import boto3

s3 = boto3.client('s3')
paginator = s3.get_paginator('list_objects_v2')

pages = paginator.paginate(Bucket='xxx') # <- ここはlist_objects_v2の引数と同じ

ページネータについて補完が効くか見てみます。

画像中のオレンジの枠で囲った部分に注目してほしいのですが、しっかりとlist_objects_v2と同じものになってます。 しかもちゃんと、ページネーションに使用するPaginationConfigまで引数にあります!

ここまででも十分すごいのですが、更に便利なことがあります。

返ってきた結果についても補完が効きました。 Dict型で返ってくるのですが、どんなキーがあるのか補完してくれます。

型チェック

最初の紹介でも述べたように、Boto3のタイプアノテーションとして利用できるのでmypyを利用した型チェックも可能です。

以下のようなコードで実行しています。 bucket.objcets.all()で得られる結果はObjectSummaryであり、Objectとは同一で無いはずです。 なので、このコードで型チェックを行うと、エラーが発生するのが期待されます。

type_check.py

import boto3

s3 = boto3.resource('s3')
bucket = s3.Bucket('xxxxx')
objs = bucket.objects.all()


from mypy_boto3_s3.service_resource import Object
def get_obj_key(obj: Object):
    return obj.key

obj_keys = [get_obj_key(o) for o in objs]

実際に型チェックしてみます。

mypy

$ mypy type_check.py
type_check.py:12: error: Argument 1 to "get_obj_key" has incompatible type "ObjectSummary"; expected "Object"  [arg-type]
Found 1 error in 1 file (checked 1 source file)

しっかりとエラーになりました。

Union型を使用してみる

ObjectObjectSummaryは両方ともkeyプロパティを持っているので、get_obje_keyはどちらでも使えるようにしたいです。 Union型を使用してみます。

以下のコードならば型チェックは通るはずです。

type_check.py

import boto3
from typing import Union

s3 = boto3.resource('s3')
bucket = s3.Bucket('xxxxx')
objs = bucket.objects.all()


from mypy_boto3_s3.service_resource import Object, ObjectSummary
def get_obj_key(obj: Union[Object, ObjectSummary]): # <- Union型に変更
    return obj.key

obj_keys = [get_obj_key(o) for o in objs]

実際に型チェックしてみます。

mypy

$ mypy type_check.py
Success: no issues found in 1 source file

無事成功しました。

Union型に不要な型を入れてみる

最後に少し意地悪して、keyプロパティを持たないBucket型をUnionに混ぜてみます。 これは当然型チェックは通らないはずです。

type_check.py

import boto3
from typing import Union

s3 = boto3.resource('s3')
bucket = s3.Bucket('xxxxx')
objs = bucket.objects.all()


from mypy_boto3_s3.service_resource import Object, ObjectSummary, Bucket
def get_obj_key(obj: Union[Object, ObjectSummary, Bucket]): # <- Bucket型を追加
    return obj.key

obj_keys = [get_obj_key(o) for o in objs]

実際に型チェックしてみます。

mypy

$ mypy type_check.py
type_check.py:11: error: Item "Bucket" of "Union[Object, ObjectSummary, Bucket]" has no attribute "key"  [union-attr]
Found 1 error in 1 file (checked 1 source file)

期待通り失敗してくれました。

最後に

Boto3の場合clientメソッドなどの仕様から、動的に型が決まるので、補完や型チェックは難しいだろうなと思い込んでいました。 ですが、boto3-stubsはそれを可能にしてくれました。 これでBoto3を使ったコードの開発もより効率化されるかと思います。