Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions docs/field-trials/2026-05-field-trial-214.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# FT214: io モジュール — StringIO / BytesIO / TextIOWrapper / BufferedReader

**日付**: 2026-05-22
**テーマ**: Python `io` モジュールの StringIO / BytesIO / TextIOWrapper / BufferedReader の実装と検証
**セキュリティ診断**: なし(214 % 3 = 2)
**クラッカーペンテスト**: なし(214 % 4 = 2)

---

## 概要

`io` モジュールは Python のストリーム I/O の基盤を提供する。今 FT では 4 つの主要クラスを HTTP API で検証した。

| API | ユースケース |
|---|---|
| `StringIO` | メモリ上のテキストバッファ(ファイルライクな読み書き・行イテレーション) |
| `BytesIO` | メモリ上のバイナリバッファ(バイト列の書き込み・デコード) |
| `TextIOWrapper` | バイナリストリームをテキストストリームに変換(エンコーディング制御) |
| `BufferedReader` | バッファリング付きバイナリ読み取り(シーク操作) |

---

## 実装したサンプルアプリ

**場所**: `/home/xi/docker/nene2-python-FT/ft214-io/`

### 主要機能

| 関数 | 概要 |
|---|---|
| `process_text_stream()` | `StringIO` にテキスト行を書き込み・読み返して文字数・行数を返す |
| `iterate_lines()` | `StringIO` をファイルのようにイテレーションして行リストを返す |
| `process_byte_stream()` | `BytesIO` にバイト列を書き込み・デコードして hex preview を返す |
| `encode_decode_roundtrip()` | `TextIOWrapper` で指定エンコーディングの往復変換を確認する |
| `seek_operations()` | `BufferedReader` + `TextIOWrapper` でシーク操作を検証する |

### エンドポイント

| メソッド | パス | 概要 |
|---|---|---|
| POST | `/io/text-stream` | StringIO テキストストリーム書き込み・読み取り |
| POST | `/io/line-iter` | StringIO 行イテレーション |
| POST | `/io/byte-stream` | BytesIO バイナリストリーム操作 |
| POST | `/io/encoding` | TextIOWrapper エンコーディング変換往復確認 |
| POST | `/io/seek` | BufferedReader + TextIOWrapper シーク操作 |

---

## 摩擦点

### F-1: `StringIO.tell()` はシーク位置を返す(文字数ではない)

**観察**: `buffer.tell()` で「書き込んだ文字数」を取得しようとすると、シーク後の現在位置が返る。

```python
with io.StringIO() as buffer:
buffer.write("abc\n") # 4 文字書き込み
char_count = buffer.tell() # → 4(書き込み後の位置 = 文字数)
buffer.seek(0)
content = buffer.read()
# この時点で buffer.tell() は content の長さと等しい(EOF 位置)
```

`buffer.tell()` のタイミングによって返る値が変わるため、「書き込み後すぐ tell()」して文字数を記録し、seek(0) / read() の後は「コンテンツ長と同じ値」になることをテストで確認した。初期実装では「read() 後に tell() == 0」と誤って期待し、テストが失敗した。

**対処**: `char_count = buffer.tell()` を `seek(0)` の前に呼ぶ。read() 後の tell() は EOF 位置(= content 長)になることを理解した上でテストを修正。

---

### F-2: `TextIOWrapper` は `write_through=True` が必要

**観察**: `BytesIO` を `TextIOWrapper` でラップして書き込む場合、バッファリングのために `getvalue()` が空になることがある。

```python
byte_buffer = io.BytesIO()
wrapper = io.TextIOWrapper(byte_buffer, encoding="utf-8")
wrapper.write("hello")
byte_buffer.getvalue() # → b'' (バッファがフラッシュされていない)
```

**対処**: `write_through=True` を指定するか、`flush()` を明示的に呼ぶことで確実にバイト列が BytesIO に書き込まれる。

```python
with io.TextIOWrapper(byte_buffer, encoding=encoding, write_through=True) as wrapper:
wrapper.write(text)
encoded_bytes = byte_buffer.getvalue() # ← write_through=True で即座に反映
```

---

## テスト結果

```
22 passed in 0.42s
```

`pytest`, `mypy --strict`, `ruff check`, `ruff format --check`, `pip-audit` すべて通過。

---

## DX Review — 6 ペルソナ

### 1. 初心者(Python 歴1年・独学中・女性・バックエンド志望)

`StringIO` は「テキストをファイルとして扱える in-memory バッファ」という説明が直感的。ファイル読み書きを学ぶ前にメモリ上で練習できる点で教育的。

**ドキュメント理解**: `tell()` の挙動(シーク位置を返す)は初心者には不明瞭。「文字数を取得するには seek(0) の前に tell() を呼ぶ」という順序依存性は文書化が必要。

**事故リスク(中)**: `TextIOWrapper` を閉じると内包する `BytesIO` も閉じられる。`with` ブロックを抜けた後に `BytesIO.getvalue()` を呼ぶ必要があり、`with` ブロック内で `getvalue()` を取得するか、`BytesIO` の参照を外部で保持する必要がある。

**規約の使いやすさ**: コンテキストマネージャ(`with io.StringIO() as buffer:`)は Python の標準パターンとして自然。

### 2. ロースキル経験者(Python 歴3-4年・スクリプト系・男性・SES)

`StringIO` はテストでファイル I/O をモックするのに頻繁に使う。`BytesIO` は画像・PDF の処理でよく登場する。`TextIOWrapper` でのエンコーディング変換は実務的に有用。

**コピペ可能性**: `process_byte_stream` / `encode_decode_roundtrip` のパターンは画像バイト列 → レスポンス変換などに直接応用できる。

**拡張時の罠**: `TextIOWrapper` は `close()` 時に内包ストリームも閉じる。外部の `BytesIO` を再利用したい場合は `closefd=False` や事前に `getvalue()` する必要がある。

**事故リスク(低)**: エンコーディングの検証を `Literal` 型で行うため、サポート外エンコーディングは 422 で遮断。

### 3. フロントエンド寄り(React/TS 歴4年・バックエンド転向中・ノンバイナリ)

Node.js の `Buffer` クラスと類似した概念。JS の `Uint8Array` / `TextDecoder` に対応する。`StringIO` は `ReadableStream` に近い感覚で理解できる。

**エラーレスポンスの質**: `UnicodeEncodeError` を 422 に変換して `{field, message, code}` で返すため、フロントエンドがエンコーディングエラーを適切に処理できる。

**Python 固有概念**: `seek()` の単位がバイト(`BufferedReader`)か文字(`StringIO`)かがストリーム種別によって異なる点は学習コストあり。

**事故リスク(低)**: `Literal` 型によるエンコーディング名の制約で無効なエンコーディングは 422。

### 4. バックエンド経験者(Django/FastAPI 歴5-6年・男性・リードエンジニア)

Django の `InMemoryUploadedFile` や `ContentFile` は内部で `BytesIO` / `StringIO` を使う。`TextIOWrapper` のエンコーディング変換はファイルアップロード処理でよく使うパターン。

**他フレームワークとの差異**: nene2 のデモアプリは io ストリームを HTTP API でラップしているが、実際の利用場面は「メモリ上でのファイル処理」や「テスト用モック」が主流。API のデモとして適切。

**nene2 の薄さへの評価**: `Literal["utf-8", "utf-16", "latin-1", "ascii"]` でエンコーディングを制限している点は実用的。本番では `chardet` などで動的検出も検討できる。

**事故リスク(低)**: バリデーションが Pydantic + 手動チェックで二重保護。

### 5. シニアエンジニア(設計・コードレビュー担当・女性・10-12年)

**コードレビューチェックポイント**:
- `TextIOWrapper` を `with` 文で使う場合、内包する `BytesIO` が閉じられるタイミングに注意(`getvalue()` は with ブロック内で呼ぶ)
- `StringIO.tell()` のタイミング依存性 — 書き込み後すぐ呼ぶか、EOF 位置として解釈するかを明確に
- `write_through=True` を使わない場合、明示的 `flush()` が必要

**チームでの安全なパターン**: `encode_decode_roundtrip` の `roundtrip_ok` フラグで変換の往復確認をする設計は堅牢。エンコーディングミスを早期検出できる。

**事故リスク(低)**: 入力バリデーションが Pydantic + Literal 型で徹底されており安全。

### 6. 設計者(nene2-python 設計ポリシー目線)

**CLAUDE.md ポリシー整合性**: `Literal` 型でエンコーディングを制限するパターンは FT211/FT213 から継続しており一貫性がある。`frozen=True, slots=True` のレスポンス dataclass も標準に準拠。

**初心者でも安全な API 達成度**: `hex_data` フィールドに `max_length=MAX_BYTE_LENGTH * 2` を設定(hex 文字は 1 バイトあたり 2 文字)。バイト列長の制限が適切に変換されている。

**改善提案**: `TextIOWrapper` の `closefd` / `write_through` など非自明なパラメータは、デモコードに短いコメントを添えると初心者の理解を助ける(ただし CLAUDE.md の「コメントは WHY のみ」ポリシーに従い、非自明な理由がある場合のみ)。
5 changes: 3 additions & 2 deletions docs/field-trials/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,19 +247,20 @@
| [FT211](2026-05-field-trial-211.md) | typing モジュール — TypedDict / Protocol / runtime_checkable / get_type_hints / Literal | | |
| [FT212](2026-05-field-trial-212.md) | dataclasses モジュール — field / asdict / astuple / replace / __post_init__ | 🔍 | |
| [FT213](2026-05-field-trial-213.md) | abc モジュール — ABC / abstractmethod / register / __subclasshook__ | 🔒 | |
| [FT214](2026-05-field-trial-214.md) | io モジュール — StringIO / BytesIO / TextIOWrapper / BufferedReader | | |

---

## セキュリティ診断実施済み一覧(🔒)

FT3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60, 63, 66, 69, 72, 75, 78, 81, 84, 87, 90, 93, 96, 99, 102, 105, 108, 111, 114, 117, 120, 121, 124, 127, 130, 133, 136, 139, 142, 145, 148, 151, 154, 157, 160, 163, 166, 169, 172, 174, 177, 180, 183, 186, 189, 192, 195, 198, 201, 204, 207, 210, 213

合計: **71件**(213 FT 中 約 33%)
合計: **71件**(214 FT 中 約 33%)

## クラッカーペンテスト実施済み一覧(🔍)

FT172, FT176, FT180, FT184, FT188, FT192, FT196, FT200, FT204, FT208, FT212

---

*最終更新: 2026-05-22 (FT213 / v1.8.90)*
*最終更新: 2026-05-22 (FT214 / v1.8.91)*
16 changes: 8 additions & 8 deletions docs/todo/current.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
# TODO — current

最終更新: 2026-05-22
現状: **v1.8.90 安定版 / FT213(abc)完了**
現状: **v1.8.91 安定版 / FT214(io)完了**

---

## 状態サマリー

v1.8.90 完了済み。FT213(abc — ABC / abstractmethod / register / __subclasshook__)完了。
セキュリティ診断で FT212 と同一の `Infinity`/`NaN` 非標準 JSON → 500 DoS を発見・修正(F-2 HIGH)。
`_sanitize_value()` + カスタム `RequestValidationError` ハンドラーで 422 に修正。フィールドトライアルループは FT214 以降も継続中。
v1.8.91 完了済み。FT214(io — StringIO / BytesIO / TextIOWrapper / BufferedReader)完了。
摩擦点: StringIO.tell() のタイミング依存性(書き込み後 vs read 後で値が変わる)、TextIOWrapper の write_through=True が必要。フィールドトライアルループは FT215 以降も継続中。

---

Expand All @@ -33,6 +32,7 @@ v1.8.90 完了済み。FT213(abc — ABC / abstractmethod / register / __subcl

| バージョン | 主な内容 |
|---|---|
| v1.8.91 | FT214: io — StringIO / BytesIO / TextIOWrapper / BufferedReader(tell() タイミング依存性・TextIOWrapper write_through=True)|
| v1.8.90 | FT213: abc — ABC / abstractmethod / register / __subclasshook__(セキュリティ診断: Infinity/NaN DoS 修正・__subclasshook__ mypy 回避策)|
| v1.8.89 | FT212: dataclasses — field / asdict / astuple / replace / __post_init__(Infinity/NaN 500 DoS 発見・修正)|
| v1.8.88 | FT211: typing — TypedDict / Protocol / get_type_hints / Literal(isinstance 後の型絞り込み・Literal+Pydantic で type:ignore 排除)|
Expand Down Expand Up @@ -64,21 +64,21 @@ v1.8.90 完了済み。FT213(abc — ABC / abstractmethod / register / __subcl

## フィールドトライアル進捗

**実施済み**: FT1〜FT213(全 213 件)
**実施済み**: FT1〜FT214(全 214 件)

索引: [`docs/field-trials/INDEX.md`](../field-trials/INDEX.md)

**次のアクション**:
- FT214 を開始(214 % 3 = 2 → セキュリティ診断なし、214 % 4 = 2 → クラッカーペンテストなし)
- テーマ候補: `io` モジュール(StringIO / BytesIO / TextIOWrapper / BufferedReader
- FT215 を開始(215 % 3 = 2 → セキュリティ診断なし、215 % 4 = 3 → クラッカーペンテストなし)
- テーマ候補: `struct` モジュール(pack / unpack / calcsize / Struct

---

## 明日以降の優先タスク

| 優先度 | Issue | タスク | 種別 |
|---|---|---|---|
| 高 | — | FT214 実施(セキュリティ診断なし、クラッカーペンテストなし) | FT |
| 高 | — | FT215 実施(セキュリティ診断なし、クラッカーペンテストなし) | FT |
| 中 | [#539](https://github.com/hideyukiMORI/nene2-python/issues/539) | handler の response_model 統一 | enhancement |
| 中 | [#540](https://github.com/hideyukiMORI/nene2-python/issues/540) | FT ループの目的・終着点を明文化 | docs |
| 中 | [#541](https://github.com/hideyukiMORI/nene2-python/issues/541) | PyPI 公開フロー検証(uv publish) | enhancement |
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "nene2-python"
version = "1.8.90"
version = "1.8.91"
description = "NENE2 Python — minimal API framework following NENE2's design philosophy"
readme = "README.md"
license = {text = "MIT"}
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading