SANDFISH FACTORY

技術ブログです。python・vuejsを愛でる日々について綴ります

MSBuildでコードスタイル分析のIDE0005を有効にする

.NETのコード分析のブログの続きを書こうとしたら、まだ解決されていない不具合に遭遇したので解決方法を記載しておきます。

環境

同じ環境を構築するには前回のブログを実施してください。

原因

Compiler does not report unused imports when XML doc comments are disabled · Issue #41640 · dotnet/roslyn · GitHub

IDE上では問題ないですが、MSBuildでビルドしようとするときにGenerateDocumentationFileが有効でないとIDE0005(不要な using ディレクティブを削除する)が無視されます。
以下の設定をMSBuildのプロジェクトファイルに追記してください。

<!-- Workaround for https://github.com/dotnet/roslyn/issues/41640 -->
<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
  <NoWarn>$(NoWarn);CS1591;CS1573</NoWarn>
</PropertyGroup>

サンプルコード

前回のブログで作成した.csprojファイルに上記の設定を追記します。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
      <NoWarn>$(NoWarn);CS1591;CS1573</NoWarn>
  </PropertyGroup>
  <ItemGroup>
    <None Remove="Microsoft.CodeAnalysis.NetAnalyzers" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="7.0.0">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
  </ItemGroup>
</Project>

Program.csにはエラーを発生させるため、using System.IO を追記します。

using System;
using System.IO; // 追記

namespace SampleCode
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

.editorconfigにはIDE0005を有効にする設定を追記します。

[*.{cs,vb}]
dotnet_diagnostic.IDE0005.severity = warning

以下のコマンドを実行します。
EnforceCodeStyleInBuildを有効にしてコードスタイル分析を実施します。
また、TreatWarningsAsErrorsを有効にしてwarrningが一つでもあればエラーとします。

dotnet build SampleCode.csproj -target:"clean;rebuild" -property:"EnforceCodeStyleInBuild=true;TreatWarningsAsErrors=true"

こんな感じでエラーが出ます。

Macの環境で.NET ソース コード分析に入門してみる

お久しぶりです。
PyConJPの感想を書くだけのブログです。

そう言っておきながら、今回の記事のタイトルと整合性が取れていないのですが、いま興味持っているネタの起点はPyConJPなんです。

speakerdeck.com

こちら、去年のPyConJP2022のセッションです。
たまたま興味持ったセッションでASTを絡めた内容が多く、自分の環境でも試してみたくなりました。
Pythonで同じように試しても良いのですが、業務ではC#を使っているので、C#を題材に同じようにASTに絡めた内容としてコード分析をやってみようかなと思っています。

ただ、大きな問題として私の自宅環境のPCはMacなんですよね。

大抵、C#で開発を行う方の環境はWindowsだとは思いますが、まずはMac環境で.NET ソース コード分析がうまく動くところまでやってみたいと思います。

.NET ソース コード分析の概要を理解する

.NET FrameworkがサポートしているOSはWindowsです。
.NETはクロスプラットフォームオープンソース開発者用プラットフォームなので、Macの環境でも動かすことは可能です。

.NET (と .NET Core) - 概要 | Microsoft Learn

可能ではありますが、IDEであるVisual StudioWindows環境の方が便利な機能も多いので、普通に開発するのであれば、Windowsが良いと思います。

コード解析にはroslynを用いたものを利用します。
roslynは .NETコンパイラプラットフォームなので、.NET環境(つまりMacでも可)で実行可能です。

GitHub - dotnet/roslyn: The Roslyn .NET compiler provides C# and Visual Basic languages with rich code analysis APIs.

基本的にはこちらの概要に書かれている内容をもとに検証していきたいと思います。

.NET のコード分析 | Microsoft Learn

環境を構築する

Visual Studio for Macをインストールする

手順はこちらです。
私はVisual Studio 2022 for Macをインストールしました。
そんなに困ることはないと思います。

Visual Studio for Mac をインストールする - Visual Studio for Mac | Microsoft Learn

検証用のプロジェクトを作る

Visual Studioを起動したら、新規を実行します。 プロジェクトテンプレートは検証用なので、コンソールアプリケーション(C#)を選択します。
コード分析は.NET5以降で標準で組み込まれているので、ターゲットフレームワークは.NET5以降の方が良いです。
ただ今回の検証はサクッと動かしたいので、該当のNuGetパッケージをインストールして環境構築するため、.NET5より前のバージョンで環境構築しました。(.NuGet追加するとコード分析するルールセットが簡単に手に入るため)
.NET5以降をターゲットとする場合は以下の一部の工程は無視しちゃってください。
プロジェクト名はSampleCodeとしました。

NuGetでコード分析のパッケージを追加する

この工程は.NET5より前のバージョンとしたので実施します。
.NET5以降であれば不要です。

前述の.NET ソース コード分析の概要にも記載がありますが、Microsoft.CodeAnalysis.NetAnalyzersを追加します。
ソリューション(もしくはプロジェクト)を選択して右クリックしてNuGetパッケージの管理を選択します。

Microsoft.CodeAnalysis.NetAnalyzersを検索し、該当のNuGetを選択してパッケージを追加します。

プロジェクトをビルドしてみる

ソリューションをビルドしてみます。 特に警告も出ることなく、ビルドが成功します。

.NET ソース コード分析を動かしてみる

ルールセットをプロジェクトに追加します。
ルールセットはNuGetパッケージ内に含まれています。ユーザーディレクトリ配下に.nugetディレクトリがあります。

.nuget/packages/microsoft.codeanalysis.netanalyzers/<パッケージのバージョン番号>/editorconfigにEditorConfigで定義されたコード分析のルール定義が入っています。
わかりやすくeditorconfig/AllRulesEnabledに入っている「.editorconfig」をプロジェクト配下にコピーします。

この状態でソリューションをクリーンし、ビルドし直すとコード分析された結果が警告として出力されます。

まとめ

今回はサクッと検証するための手順をまとめました。
意外とサクッとできたのですが、ルールセットも複数あるので、それらを検証したのちにコード分析の内容を深掘りしたいと思います。

PyCon JP 2022に参加してきました。

お久しぶりです。
もはや、PyConJPの感想を書くだけのブログと化しました。

現在の仕事ではPythonを使っていないですが、前職で使っていたこともあってPyCon JPは2019から参加していました。

今年はオフラインでも開催だったので、コロナ禍でのオフラインテックカンファレンスってどんな感じになるんだろうか?というの体験しようと今回参加してきました。

2022.pycon.jp

参加した感想などをつらつらと書いてみたいと思います。
詳細な感想は、後で資料を見ながら書きたいと思います。(特にAST周りの話題が上がったセッション)

感染予防対策の観点

  • 接触を減らす工夫などは所々に配慮されていました。Coffee breakのおやつも個別包装になっていました。
  • 前回もとても良かったなと思ったのですが、カンファレンスのTシャツを現地配布ではなく、外部サービスを用いたのはとてもよかったなと思いました。
  • 大部屋でレシーバーで聞きながら開催する方式でした。この形式が増えている感じがしたので、この辺りも感染予防対策なのかなって思いました。机があったら嬉しかったですが、きっとアルコール除菌とかしないといけないから厳しいのかなーって個人的に思いました。
  • 今回のパーティはきっと同窓会っぽいノリかなって思ったので、知り合い皆無の私は参加希望をしなかったですが、古参と新参のどちらも参加するのは工夫は必要そうだなって思いました。これは世の中の全ての組織体で起こっていそうで、まだ解は出ていなくてムズイよなーって感想です。
  • パーティ自体はSNSの写真とか見るとビュッフェ形式というより、ちっちゃいワンプレートみたいのだったっぽく、なるほどなーって思いました。

聴講したセッション

2019〜2021はWeb開発に関するセッションにほぼ参加することが多かったのですが、今回はcoreやtipsのセッションを選ぶようにしました。

1日目

2日目

選んだセッションでASTに関する話題が多かったです。これまでは違った内容にフォーカスして聞くことが多かったのですが、最近の仕事内容の変遷に伴い、ASTにとても興味を持ちました。
今回採択されなかったのですが、OAuthのテーマでCfPは出していて、OAuthに対する興味関心が高かった2022年の私ですが、OAuthのセッションが他のセッションと重なってしまったので、後で資料を見ようと思いました。

まとめ

前と全く同じ状態に戻すのは難しい中で、いろんな工夫でオフラインでの開催が実施されていたなって感想を持ちました。
スタッフの皆さんの創意工夫だなーって感じました。

あと、pythonとの関わりが2019年からなので、オンラインの時期の方が実際は長いため、顔見知りが皆無な私ですが、唯一知っている方に遭遇して、これかーって(オフラインで会うのも良いよねってやつ)体験も出来ました。

FastAPI+SvelteでWebSocketを動かしてみた

Python Advent Calendarの14日目です。

今年はWebSocketを用いたChatアプリを作って遊んでいて、そこで学んだ結果をPyConJPのプロポーザルに出してみました。
結果は採択されなかったのですが、学んだことをアウトプットしないのは、もったいないかなと思ったので、一部をAdvent Calendarのネタとしてみました。

ネタとしてはFastAPIのWebSocketを用いてChatアプリを構築するです。
フロントエンドはSvelteで実現しています。

プロポーザルのアウトライン

最初は、FastAPIでChatアプリを作るつもりはなく、AWSAmazon API GatewayAWS Lambda、Amazon DynamoDBで実現する予定でした。
AWS CDKを用いて、アプリもインフラも全てpythonで制御する構成で考えていました。

docs.aws.amazon.com

ただ、以下の課題があったので方針を切り替えました。

  • API GatewayのWebSocket APIの仕様で、バイナリがサポートされていなかった(※1)
  • AWS CDKではWebSocket APIの構築はlow level APIで、high level APIがまだ提供開始になっていなかった(※2)

それであれば、コンテナでFastAPIを起動する方が良いだろうと考えを切り替えました。

※1 WebSocket API のバイナリメディアタイプの使用 - Amazon API Gateway
※2 @aws-cdk/aws-apigatewayv2 module · AWS CDK

今回はその切り替えた後のChatアプリの一部分の説明です。肝心のバイナリ部分のデータ送受信やCDKは別の機会に説明します。

FastAPIでChatアプリを実現する

技術スタック

今回は以下の技術スタックで実現しました。

  • Python3.9
  • FastAPI 0.7.0
  • Svelte 3.0.0

個人で開発する時のフロントエンド技術スタックはSvelteを利用する場合が増えてきました。
小さく利用したいというニーズ(Vueだとちょっと初動がコッテリしている)と、ReactはCSSに対するサポートが現時点では薄いので、Svelteを選択しています。

FastAPIとSvelteをinstall

手順は公式サイトに記載されている内容で実施しました。
chatappディレクトリにbackendのモジュールをchatapp-frontendにfrontendのモジュールを配置する構成です。

FastAPI

mkdir chatapp
cd chatapp
mkdir src
python -m venv venv
source venv/bin/activate
pip install fastapi
pip install uvicorn[standard]

Svelte • サイバネティクスで強化されたWebアプリ

cd src
mkdir chatapp
npx degit sveltejs/template chatapp-frontend
cd chatapp-frontend
node scripts/setupTypeScript.js
npm install -D uuid
npm install -D @types/uuid

svelteのビルド結果をbackend側に出力する

frontendに閉じた確認をしたい場合はchatapp-frontendで npm run dev を実行して確認します。
backendと連携したい場合は、 chatapp-frontendで npm run build でビルドし、ビルド結果をbackend側に出力します。
その構成にするために rollup.config.jsを修正します。
outputのfileでビルド結果の出力先が定義されているので、こちらをbackend側のパスに変更します。

output: {
    sourcemap: true,
    format: 'iife',
    name: 'app',
    file: '../chatapp/public/build/bundle.js'
}

chatappの実装

FastAPI側の構成はチュートリアルを参考にしました。

websocketでchatのメッセージを受け取る箇所はchat.pyとして切り出し、APIRouterとして紐付けています。

from datetime import datetime
import json

from fastapi import APIRouter, WebSocket, WebSocketDisconnect
from app.websocket import ConnectionManager

router = APIRouter()
manager = ConnectionManager()

@router.websocket("/chat/{client_id}")
async def websocket_endpoint(websocket: WebSocket, client_id: str):
    await manager.connect(websocket)
    try:
        while True:
            data = await websocket.receive_text()
            record = json.loads(data)
            record["receive_timestamp"] = datetime.now().strftime('%Y/%m/%d %H:%M:%S')
            await manager.broadcast(json.dumps(record))
    except WebSocketDisconnect:
        manager.disconnect(websocket)
        print(f"Client #{client_id} left the chat")

main.pyはchap.pyのrouterをFastAPIのappに紐付けています。
またsvelteのエントリーポイントであるindex.htmlを読み込むため、staticファイルの設定を行なっています。

import uvicorn
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.staticfiles import StaticFiles

from app.chat import router as chatRouter

app = FastAPI()
app.include_router(chatRouter)

app.mount("/", StaticFiles(directory="public"), name="static")

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

フロントエンドでchatの内容を入力する箇所はcontentEditable属性を用いました。

Bindings / Contenteditable bindings • Svelte Tutorial

ctrl + Enterで入力が終了するように制御を入れています。
実際のコードは以下を参照してください。

github.com

chatappの実行

以下のコマンドを実行して、FastAPIを動かします。

cd fastapi_websocket/src/chatapp
uvicorn main:app --reload

こんな感じで入力エリアが表示されます。
ctrl + Enterで確定すると次の入力エリアが追加されます。

f:id:sandfish03:20211214073555p:plain
chatapp実行

まとめ

簡単なプログラムではありますが、FastAPIのドキュメントだけで大枠のWebSocketでやりとりするアプリを作りました。 FastAPIの説明のドキュメントなので、フロントエンドの箇所はPython上に直接書いていたので、ブログではその部分をSvelteに置き換えて試すことはできました。
まだまだ、雑な作りなので、少しずつ改善していきたいと思います。

PyConJP2021で一番楽しみにしていたセッションをもとに改めて振り返った

PyCon JP 2021 Advent Calendarの4日目です。

仕事で少しだけ役割が追加されたので、ここ1週間はあたふたしていました。
内容がだいぶ薄いカルピスのような感じになってしまって、申し訳ないです。
発表者の方のスライドはとても学びがあるので、そちらを見てください。

PyCon JPの2日間を通しての参加レポートは既に作成済みです。
PyCon JP 2021に参加してきました。 - SANDFISH FACTORY

今回は、一番楽しみにしていたセッションについて書きます。

High Performance FastAPI

speakerdeck.com

今回の発表で一番楽しみにしていたトークです。

パフォーマンスチューニングについて想定した原因とそれに対する対応という感じで説明がなされてとても良いトークでした。
パフォーマンスチューニングの心得は考え方の共有って意味でも自チームにも共有したいなって思いました。(pythonじゃないけど)

これまでの経験でもI/O boundやCPU boundでキャッシュすれば良いよねってやつは対応したことあるけど、Cythonを使ったケースや処理系を変える対応はしたことがなかったので、事例と解決策をみれたのはよかったです。
自身のエンジニア力の弱さが露呈したので、日々精進だなーって思いました。
ここまでは元の参加レポートの記載内容のままです。

セッションを聞いた上で深掘りした内容

ここからが今回聞いてみた上で 深掘りした内容です。

Locust

私がこれまでの経験でpythonで負荷をかけるためのツールを利用したことがなかったので、今回のセッションで説明のあったLocustは勉強になりました。

docs.locust.io

さらっと調べた感じだとCSVで結果を出力できるみたいなので、便利そうに思いました。
グラフも出力できたらより良いなって思いましたけど、CSVで出力されるので良しなにって感じなのかなって思いました。

docs.locust.io

py-spy

ボトルネックを調査するためのプロファイラです。
Rustで書かれていて、実行中のPythonプログラムのプロファイルとデバッグを可能にすることを目的としていますとgithub上には書かれています。
この辺りはこのツールを利用するで大きなメリットだなと思いました。

github.com

負荷ツールと同じなんですが、この手のツールを最近は使っていないので、だいぶ感覚を忘れてしまったなと説明を聞いていて思いました。
スライドの説明を参考に今年の年末は負荷ツールとプロファイラを使ってみて以前の感覚を思い出さないとなーと感じました。

あとはgithub上のFAQを読んでも内容が頭に入ってこなかったので、精進します。

asyncio

asyncioを全く使いこなせていなかったので、asycioに対応しているかきちんと調べないといけないんだなと理解しました。
http通信でも既にあるライブラリを参考にして検証することが多かったので、きちんと意識が向いていなかったです。
他のセッションでも似た説明を聞いたので、今後は意識して対応したいと思います。

まとめ

繰り返しになってしまうのですが、今回のPyConJP2021では全体を通して自分が知らないことが多くて学びが多かったです。
ただ、実際動かしてみないと理解が弱いので、今回のAdvent Calendarの機会で振り返ったことで年末に触ってみたいなと思います。  

アーカイブが公開されていたので、リンクを貼っています。(2021/12/05更新)

youtu.be

以上、だいぶ薄味な感想レポートですが、PyCon JP 2021 Advent Calendarの4日目の投稿とします。
ありがとうございました。

PyCon JP 2021に参加してきました。

今年は全然、ブログ書いていないですね。
久しぶりの投稿です。
2つ前の投稿が去年のPyConJPの内容ですよ。。。

10/15(金)-10/16(土)の開催でDay1はオンサイトありのハイブリッド開催でした。(スゴイ!!)
去年からPyConJPの開催日が決まったらすぐに有給の事前申請をしているので、Day1から参加しました。

今年もプロポーザルには応募してみたのですが、採択はされませんでした。
去年は初めての発表で準備にドキドキして半分、心ここに有らずだったので、今年は落ち着いて各発表を見ることができました。

基調講演

谷合 廣紀氏

プロ棋士で研究者でエンジニアってすごいなーって感想しか出てこないです。
行動の速さや深さが違うなーって思いました。
将棋のデータや周辺ライブラリって結構揃っているんだなって思いました。
ITの恩恵を受けた将棋って違った面白さが出てきそうって思いました。(去年の基調講演でも思いましたが)

alu.jp

Mr. Brandt Bucher

structural pattern matching のお話でした。 実際に取り込まれるまでの歴史の話や、設計・実装の話でとても面白かったです。
チュートリアルで1つのPEPがあるのは知りませんでした。

きちんとこの辺りの仕様を把握したいなと思わせるトークでした。
あと、毎回翻訳が最高だなって思います。

Day1

ASGIアプリケーション入門 - こわくないasyncio基礎と非同期IO - FastAPIを例に

speakerdeck.com

ASGIアプリケーションの話です。DjangoCongressでも聞いていたんですが、FastAPI部分を聞きたかったので参加しました。
前半部分はasyncioの入門としてはわかりやすくまとまっているので、とても良いです。
asyncioを対応しているライブラリを利用しないと恩恵を受けづらいのは、私が完全に失念していたんですが、説明があってよかったです。

Pythonで始めるドキュメント・インテリジェンス入門

speakerdeck.com

ドキュメント・インテリジェンスという言葉は知らなかったので、興味本位で聞いてみました。
3年くらい前にDeepLearning関連に甘噛みしていて、今は離れてしまったのですが、完全に浦島太郎状態でした。
色んなサービスやライブラリが提供されていて、へぇーとか、すごいなという感嘆の気持ちしか出てきませんでした。

alu.jp

Python x DDD!! - Python で学ぶ実践的なドメイン駆動設計とレイヤード・アーキテクチャ

speakerdeck.com

私見ですが、DDDって正解はないと思っていて、概念を元にそれをどう実装に落としたかってそれぞれの経験や考え方次第だと思っています。
その辺りの説明が発表の中で含まれていて、とても学びが多かったです。

こういった設計思想の概念を注入して実装に落とすときに、名前の付け方やクラスの切り方、モジュールの切り方は毎回悩むので、質問してみました。
pythonで行った場合、こう考えているよって答えてもらえて有難かったです。

あと、完全に横道にそれた話なんですが、そういえば自分はDtoってクラス名につけないなって個人的な気づきがありました。ViewModelにはつけるのにEntity、VO、Dtoにはつけないなと。どこかで言語化してみたいなって思いました。

Day2

Pythonによるアクセスログ解析入門

speakerdeck.com

アクセスログ解析って重要ですよね。
pythonでどうやっているかも気になったけど、事例としての説明があって面白かったです。
あと、アクセスログ解析を活かすためにはきちんと基盤を整備しないとねって思いました。

High Performance FastAPI

speakerdeck.com

今回の発表で一番楽しみにしていたトークです。

パフォーマンスチューニングについて想定した原因とそれに対する対応という感じで説明がなされてとても良いトークでした。
パフォーマンスチューニングの心得は考え方の共有って意味でも自チームにも共有したいなって思いました。(pythonじゃないけど)

これまでの経験でもI/O boundやCPU boundでキャッシュすれば良いよねってやつは対応したことあるけど、Cythonを使ったケースや処理系を変える対応はしたことがなかったので、事例と解決策をみれたのはよかったです。
自身のエンジニア力の弱さが露呈したので、日々精進だなーって思いました。

alu.jp

実装で知るasyncio -イベントループの正体とは

docs.google.com

スライドの言葉を引用。

もし難しく感じたらラッキー。それこそが PyCon JP に参加する価値です

難しかったので、ラッキーでしたし、とても学びが多いトークでした。(理解できたとは言っていない)

濃密な情報が整理されているので、きちんと理解できるように何度もスライドを見直して理解したいなって思いました。 コルーチンやタスクなどは浅い理解はしていましたが、実際にどのように動いているのかの説明があって、理解はできなかったですが、きちんと理解したいという気持ちが湧きました。

PEPやコードを読めって感じかもですが、この濃密な情報を整理するのにどういう風にしたのかは知りたいなって思いました。

参加してみて

私は仕事でpythonを使っていないので、2日間を通して学びの多いカンファレンスでした。
タイムテーブルを俯瞰してどの発表を見ようかな?考えた時に目が引かれたのは以下の3つでした。

  • FastAPI(個人的に使っているよなー)
  • asyncio(きちんと内容を理解してないよなー)
  • DDD(pythonに適用した場合ってどうなんだろう?)

特に非同期処理やDDDは他の言語で関わったことがありましたが、pythonだとどうなんだろう?って思った中で、知見が共有されたのはとてもよかったです。
(非同期処理は下の図がきちんと理解できるように頑張ります )

https://raw.githubusercontent.com/rhoboro/events/main/pycon.jp.20211016/asyncio_flow.png

最後に

スタッフの皆さんお疲れ様でした。
オンサイトの開催もありつつ、オンラインも同時開催でとても大変だったと思いますが、概ね順調に開催されたと思います。
振り返ると課題もあったとは思いますが、次回の開催(TBDでしたが)に繋がったらより良いカンファレンスになるかと思います。

個人的に今回のカンファレンスで一番Goodだなって思ったのはグッズがsuzuriで注文できたことでした。
suzuriで注文できるアナウンスがあったときにめっちゃええやん!って思いました。
好きなものを好きだけ注文できるのは良いですね。

あと、パーティフードでピザが届くのは良いですね。
ピザハットの1人前ピザセットはとても美味しかったです。

おまけ

この動画はゴイゴイスーでした。

www.youtube.com

FastAPIのWebSocketで利用するデコレータを間違えた

今回は小ネタです。凡ミスをして時間を潰してしまったので、その想いを供養するためブログに投稿しました。

きっかけ

普段、仕事ではSlackを利用していますが、そういえばチャットアプリってどうやって作っているんだろう?とふと思いました。
チャットアプリで検索してみると簡易サンプルではWebSocketを利用しているケースが多かったです。

WebSocketをきちんと触ったことがなかったので、勉強がてらコードを書いていました。
サーバーサイドの実装も必要そうだけど、どうせなら使ったことのないFastAPIを利用してみるかーと軽い気持ちで触ったことで凡ミスが生まれてしまいました。

FastAPIの説明はしないので、詳しくはドキュメントを見てください。

fastapi.tiangolo.com

チュートリアルをやってみた

最近のチュートリアルはとても充実していますね。

fastapi.tiangolo.com

難なく実施できました。
FastAPIでWebSocket完全に理解したってやつです(←わかっていない)

過ちの始まり

チュートリアルをやっていくと、一つのmain.pyにコードを全てを書いているので、アプリとして作るときどうするんだろう?という疑問が湧きました。
当然、この問いに対する説明もきちんと説明がされています。流石ですね。

fastapi.tiangolo.com

詳細を省く&若干加工していますが、チュートリアルでは

# main.py
from fastapi import FastAPI

app = FastAPI()

fake_items_db = {"plumbus": {"name": "Plumbus"}}

@app.get("/")
async def read_items():
    return fake_items_db

というFastAPIのオブジェクト(app)に対してパスを追加する実装を

# routers/other.py
from fastapi import APIRouter

router = APIRouter()

fake_items_db = {"plumbus": {"name": "Plumbus"}}

@router.get("/")
async def read_items():
    return fake_items_db

# main.py
from fastapi import FastAPI
from .routers import other

app = FastAPI()

app.include_router(other.router)

APIRouterにパスを追加し、APIRouterをFastAPIのオブジェクトに追加するという対応に変更しています。

なるほど。これでroutingしたい単位でモジュールを分けることができるのか!と理解した私は、WebSocketでどのようになるかを試してみようと思い、チャレンジしてみました。

Bigger ApplicationのWebSocket版

いくつかの要素を混ぜ込んでチャレンジしてみました。要素は以下の通りです。

  • チュートリアルのBigger ApplicationのWebSocket版を実装してみる
  • prefixを利用する
  • パスパラメータを利用する

403エラーで接続できない

そんなに大きいコードではないので、ささっと書いて試しみましたが、

# main.py
import uvicorn
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
import other
app = FastAPI()
app.include_router(other.router)

html = """
<!DOCTYPE html>
<html>
    <head>
        <title>Chat</title>
    </head>
    <body>
        <h1>WebSocket Chat</h1>
        <h2>Your ID: <span id="ws-id"></span></h2>
        <form action="" onsubmit="sendMessage(event)">
            <input type="text" id="messageText" autocomplete="off"/>
            <button>Send</button>
        </form>
        <ul id='messages'>
        </ul>
        <script>
            var client_id = Date.now()
            document.querySelector("#ws-id").textContent = client_id;
            var ws = new WebSocket(`ws://localhost:8000/ws/hello/${client_id}`);
            ws.onmessage = function(event) {
                var messages = document.getElementById('messages')
                var message = document.createElement('li')
                var content = document.createTextNode(event.data)
                message.appendChild(content)
                messages.appendChild(message)
            };
            function sendMessage(event) {
                var input = document.getElementById("messageText")
                ws.send(input.value)
                input.value = ''
                event.preventDefault()
            }
        </script>
    </body>
</html>
"""


@app.get("/")
async def get():
    return HTMLResponse(html)

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)
# other.py
from fastapi import APIRouter, WebSocket
router = APIRouter(prefix="/ws")


@router.websocket_route("/hello/{client_id}")
async def hello(websocket: WebSocket, client_id: int):
    await websocket.accept()
    await websocket.send_text(f"Router Hello! {client_id}")
    response = await websocket.receive_text()
    print(response)
    await websocket.close()
    print("Router Closed")

そしていざ動かしてみると

f:id:sandfish03:20210530191709p:plain
WebSocketで403エラー

403エラーでつながらない。。。
チュートリアルのWebSocketのコードだと問題なく動作するのになぜか、Bigger Applicationのコードを真似てみたら動かない。

そんなに複雑なコードでもないので、もしやそもそもAPIRouterでWebSocketの組み合わせは悪いのでは?と思い検索したら、Issueが上がっていました。

github.com

どうやら、バグのようでWebSocketでAPIRouterを利用する際、prefixがうまく機能していないようです。
仕方ないので、prefixを利用するのは諦めました。

今度はパスパラメータの解析ができない

other.pyのprefixを外し、デコレータにprefixのパスを追加してみて、今度こそうまく動くと思いました。

# other.py
from fastapi import APIRouter, WebSocket
router = APIRouter()


@router.websocket_route("/ws/hello/{client_id}")
async def hello(websocket: WebSocket, client_id: int):
    await websocket.accept()
    await websocket.send_text(f"Router Hello! {client_id}")
    response = await websocket.receive_text()
    print(response)
    await websocket.close()
    print("Router Closed")

f:id:sandfish03:20210530193416p:plain
パスパラメータ解析エラー

パスパラメータのclient_idが存在していなくて、エラーになっています。
チュートリアルのhttpのgetメソッドでは上手くいっているのに、なんでWebSocketで動かないのか。

これもprefixと同じで上手く動かないのか。。。
当初考えたBigger ApplicationのWebSocket版は無理なのかなーと思いました。
ここで諦めるのは悔しいので、デバッグしてみて何かおかしなところないか調べてみようと一念発起しました。

バグ爆誕

地道なステップ実行して、何が原因でエラーになっているのか調べてみました。

f:id:sandfish03:20210530203010p:plain

そして、怪しそうな箇所に到達しました。
pathも正しいな。
pathフォーマットも正しいな。
WebSocketのオブジェクトなのにscopeがhttpになっているぞ?
後続の処理で、ここがhttpになっているから、パス解析の処理に移動していないっぽいぞ。。。

あれっ?ASGIサーバの情報を正しく処理されていない?

f:id:sandfish03:20210530203448p:plain

出ているログからもここで例外をキャッチしているな。。。

そして、もう一度該当のコード:starlette/routing.pyを見てみる。

そっか。怪しそうな箇所のコードってStarletteのコードなのか。
まぁ継承しているから変ではないんだけど。。。 starlette/routing.pyか。気になるな。。。
長くなるので割愛しますが、正常に動作するコードではfastapi/routing.pyでした。

fastapi/routing.py!?

f:id:sandfish03:20210530194612p:plain

んっ!?

f:id:sandfish03:20210530194909p:plain

んっ!?んっ!?

もしかして f:id:sandfish03:20210530200021p:plain

これをこうして

f:id:sandfish03:20210530195316p:plain

えいやっ!!

f:id:sandfish03:20210530195428p:plain

動いた!!!!

と、同時に私の感情↓

alu.jp

学んだこと

FastAPIは必要条件に書いてある通り、Starletteを利用しています。

FastAPI は巨人の肩の上に立っています。

Web の部分はStarlette
データの部分はPydantic

FastAPIでStarletteを利用しているので、当然、FastAPIが想定した通りに利用しないと動作しません。
パッとみた感じ、Starletteのクラスなどを継承しているため、今回のデコレータなど参照できてしまいますが、動作しない可能性もあります。
FastAPIのデコレータやメソッドを利用する場合は、 FastAPIのモノかきちんと確認した方が良さそうです。(私は)

とういうことで、凡ミスで1週間時間を潰してしまいましたーー。

これにて凡ミス供養とします。