きり丸の技術日記

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

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にはエラー出さないで欲しいです。素直に明示的な構文だけをエラーとしてくれると非常に嬉しいのですが…。動的型付言語という性質上、難しいのかもしれません。

同じJSONやDictでも生成されるJWTは変わることがある(Pythonで例示)

同一のJSONやDictを与えているはずなのに、生成されたJWTが変わってしまったというメモ。

なお、ライブラリの特性である可能性はあるので、すべてのJWTライブラリで発生するわけではありません。

環境

  • Python
    • 3.11
  • python-jose
    • 3.3.0

原因

JSONやDictとしては等価だったが、JSONを文字列化(シリアライズ)したタイミングでキーの順番が変わってしまったため。シリアライズした結果を元にencodeするので、生成されるJWTが等価にならない。

事象発生時のシリアライズ

  • {"id": 1, "exp": 1710152710, "iss": "kirimaru"}
  • {"exp": 1710152710, "id": 1, "iss": "kirimaru"}

対応

JWTに関しては検証を行わない。JWTからJSONの復元ができることのみを確認する。

動作確認時のコード

次の3つを確認していました。

  • JSON(Dict)のキーが異なった場合でも等しいこと
  • キーをソートしたら同一のJWTが生成されること
  • 異なるJWTから同一のJSONを得られること
from jose import jwt

async def test_expected_same_token():
    KEY = "SECRET"
    ALGORITHM = "HS256"
    json_token = {
        "id": 1,
        "exp": 1710152710,
        "iss": "kirimaru"
    }

    pydantic_token = {
        "exp": 1710152710,
        "id": 1,
        "iss": "kirimaru"
    }

    # NOTE: JSON のキーの順番が違ってもTrue になる
    assert json_token == pydantic_token

    # NOTE: JWTの payload の順番が違ったら別のトークンになるのでソートしたら True になる
    json_encode = jwt.encode(dict(sorted(json_token.items())), KEY, algorithm=ALGORITHM)
    pydantic_encode = jwt.encode(dict(sorted(pydantic_token.items())), KEY, algorithm=ALGORITHM)
    assert json_encode == pydantic_encode

    # NOTE: ソートしなくても別のトークンから同じJSONに戻せることのチェック
    json_encode = jwt.encode(json_token, KEY, algorithm=ALGORITHM)
    pydantic_encode = jwt.encode(pydantic_token, KEY, algorithm=ALGORITHM)
    json_decode = jwt.decode(json_encode, KEY, algorithms=ALGORITHM)
    pydantic_decode = jwt.decode(pydantic_encode, KEY, algorithms=ALGORITHM)

    assert json_decode == pydantic_decode

ソースコード

事象発生時のpython-joseではなく、PyJwtでGitHubでは公開しています。

終わりに

複数の言語を使用しているアプリケーションを運用しているので、別アプリケーションで生成したJWTを元に、他アプリケーションでも同一のJWTを生成していることを期待してハマりました。

パラメータとしてJSONを期待しているなら、キーのソートまでライブラリ側でやってほしいのですがね…。PullRequestを出そうかと思っていましたが、python-joseの最終マージが2023/05/04だったので、取り込まれそうになかったのもありPullRequestは作ってません。

もし興味があれば該当箇所までは判別したので、PullRequestを出してみてください。

テストのメールドメインはexamle.comのほかにもある(RFC 2606)

テスト時に使用するメールドメインについてexample.comを使用していますが、メールドメインで制御しているコードを検証したい時にほかにも安全に使えるドメインがないかを調べたときのメモ。

すでに飽和している情報ですが自分のメモのために残します。

テストで使えるトップレベルドメイン

RFC 2606

RFC 2606で定義されているものは次の4つです。

  • .test
  • .example
  • .invalid
  • .localhost

使い分けについては、RFC 2606で提案されています。

IANA(ICANN)

ICANN(国際的な非営利法人)にて管理しているトップレベルドメインがあります。

  • テスト
  • 测试
  • 測試
  • испытание
  • परीक्षा
  • δοκιμή
  • 테스트
  • பரிட்சை

等々。ほかに3つありますが、アラビア語等の右横書き言語(RTL言語)でのマークダウンの書き方が分からなかったので、知りたい方はIANAのページを調べてください。

IANAとICANNについて

IANA(Internet Assigned Numbers Authority)が管理していたものを、ICANNが管理するようになりました。現在では、IANAという名称自体は、ICANNの機能名のひとつとなっています。

テストで使えるセカンドレベルレベルドメイン

RFC 2606

RFC 2606で定義されているものは次の3つです。

  • example.com
  • example.net
  • example.org

どうしてもそれ以外が使いたい時

どうしてもそれ以外のメールドメインが使用したい場合、DNS Lookupで確認してください。応答がなければ、おそらくそのコマンドを実行している段階では存在しないDNSでしょう。

$ nslookup example.com
Server:         XXX.XXX.XXX.XXX
Address:        XXX.XXX.XXX.XXX#53

Non-authoritative answer:
Name:   example.com
Address: 93.184.216.34
Name:   example.com
Address: 2606:2800:220:1:248:1893:25c8:1946

$ nslookup example.com2
Server:         XXX.XXX.XXX.XXX
Address:        XXX.XXX.XXX.XXX#53

** server can't find example.com2: NXDOMAIN

備考

ドメインは右から解釈するので、トップレベルドメインさえ例示したものにすれば問題ないです。

aaa.bbb.example等々。

終わりに

難しいことは考えずにexample.comexample.netを使用すればよいです。ただ、co.jpについては国際的な予約済みドメインではないので、注意しましょう。

なお、例示可能なドメインとしてJPRSで定義しているので、example.co.jpを使用しても問題はありません。

参考情報