You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: CHANGELOG.md
+26Lines changed: 26 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -30,6 +30,17 @@ MINOR release — Phase 2 착수. RAG / LLM 파이프라인이 직접 소비하
30
30
- 문서: `docs/roadmap/v0.2.0/ir.md` (사양), `docs/design/v0.2.0/ir-design-research.md` (7개 결정 증거), `docs/implementation/v0.2.0/stages/stage-{1..5}.md`.
31
31
- 테스트: **165 passed** — IR schema/roundtrip/tables/iter/export + LangChain ir-blocks + Rust unit tests (`cargo test` 5 passed).
32
32
33
+
### Added — Binding 구조 개선 (Python wrapper class + async 진입점)
34
+
35
+
`#[pyclass(unsendable)]` 제약 안에서 가능한 최선의 binding 구조와 async 진입점을 정착. 사용자-대면 API 는 전부 보존 — 가산만 있음 (breaking 없음).
36
+
37
+
-**Python wrapper class 패턴**: Rust `_Document` 는 `#[pyclass(name = "_Document", module = "rhwp._rhwp", unsendable)]` thin core 로, Python `rhwp.Document` 는 `__slots__ = ("_inner",)` + `_from_rust` factory 로 thin core 를 감싸는 wrapper. 모든 메서드는 pass-through.
38
+
-`Document.from_bytes(data, *, source_uri=None) -> Document` — bytes 기반 생성 classmethod (Rust `_Document::from_bytes` + `py.detach` 로 GIL 해제). 네트워크 fetch / in-memory archive / `aparse` 내부 경로용.
39
+
-`rhwp.aparse(path) -> Document` async 함수 — `aiofiles` 로 파일 I/O 만 async 처리, 파싱은 event-loop 스레드에서 sync (`Document.from_bytes`). `unsendable` 제약 상 `asyncio.to_thread(parse, path)` 가 panic 하므로 이 경로가 유일하게 안전한 async 진입점.
40
+
-`[async]` optional extras 추가 — `aiofiles>=23`. 미설치 시 `aparse` 호출 시점에 명시적 `ImportError` (silent fallback 없음).
41
+
-`HwpLoader.aload` / `alazy_load` async override — `rhwp.aparse` 위에 구축. 공통 yield 로직 `_yield_documents` 헬퍼로 sync/async 공유.
42
+
-`python/rhwp/_rhwp.pyi` 신규 — Rust extension (`_Document`, `version`, `rhwp_core_version`) 의 Python 측 타입 stub.
43
+
33
44
### Changed — Phase 2 계획 전환
34
45
35
46
- 원안의 CLI 도구 (`rhwp` 바이너리) 는 **폐기**. 업스트림 `edwardkim/rhwp` 의 Rust 바이너리가 같은 이름을 점유하므로 충돌 방지 + Python 고유 가치 (RAG / LangChain 통합) 에 집중. 상세: `docs/roadmap/v0.2.0/ir.md` §방향 전환 배경.
@@ -41,6 +52,21 @@ MINOR release — Phase 2 착수. RAG / LLM 파이프라인이 직접 소비하
41
52
- Python **3.9 지원 드랍** — `requires-python = ">=3.10"`, `pyo3` feature 를 `abi3-py39` → `abi3-py310` 으로 전환, CI 매트릭스에서 `3.9` 제거. Python 3.9 는 2025-10-31 EOL 이후 보안 패치가 중단된 상태 (> 6 개월 경과). 기존 공개 API 는 전부 호환 — 3.9 사용자는 PyPI 의 `rhwp-python 0.1.x` 를 계속 사용 가능.
42
53
-`rhwp.ir.schema.load_schema()` 의 `Traversable.joinpath()` 호출을 chain 패턴 (`joinpath(a).joinpath(b)`) 으로 정리 — `*descendants` 가변 인자 시그니처가 표준 라이브러리에 도입된 시점이 버전별로 달라 typeshed 기준 pyright 가 py3.9/3.10/3.11 에서 `reportCallIssue` 를 내는 문제 제거.
43
54
55
+
### Changed — IR 매핑 구조 (Rust → Python 이전)
56
+
57
+
IR 합성 (HTML 직렬화 / cell role 분류 / inline run 폴백) 을 Rust 에서 Python 으로 이전. IR 진화 시 `maturin rebuild` 회피 + Python-only 기여자 진입장벽 제거. 외부 API (`Document.to_ir()`, `to_ir_json()`, `iter_blocks` 등) 모두 동일, 캐시 identity (`doc.to_ir() is doc.to_ir()`) 유지.
58
+
59
+
-`src/ir.rs` 527 → 254 줄. raw 평탄화 + UTF-16↔codepoint 변환만 담당. `#[derive(IntoPyObject)]` struct 5개 (`RawDocument` / `RawParagraph` / `RawTable` / `RawCell` / `RawCharRun`) 로 PyDict 자동 생성.
60
+
-`python/rhwp/ir/_mapper.py` 신규 — raw dict → `HwpDocument` 합성. Rust 에 있던 `escape_html` / `cell_role` / `table_to_html` / `build_inline_runs` 폴백 로직 전부 이전.
61
+
-`python/rhwp/ir/_raw_types.py` 신규 — Rust struct 미러 `TypedDict` 5개. nested 구조에서 `BaseModel` 대비 약 2.5× 빠른 internal raw record (공식 벤치마크 기준).
62
+
-`tests/test_ir_mapper.py` 신규 — Rust 에서 사라진 `#[cfg(test)]` 단위 테스트 (escape 순서, cell role 3갈래, inline run 폴백 정책) 의 Python 측 보존.
63
+
-`tests/test_from_bytes.py` 신규 — bytes 기반 생성 검증.
64
+
-`tests/test_async.py` 신규 — `aparse` + `aiofiles` 경로 검증 + 미설치 시 `ImportError` 검증.
65
+
66
+
### Documentation
67
+
68
+
-`CLAUDE.md` async direction 섹션 갱신 — `asyncio.to_thread(rhwp.parse, path)` 가 `unsendable` 제약 상 panic 함을 실험으로 확인. forbidden vs supported async 패턴, `aparse` + `aiofiles` 권장 경로, 향후 upstream `RefCell` 변경 시 재검토 가능성 안내.
- GIL release via `py.detach` in `parse()` / `render_pdf()` / `export_pdf()` — keep this pattern when adding new CPU/IO-bound methods
42
+
- PyO3 `#[pyclass(unsendable)]`: `_Document` is bound to its creation thread (upstream `DocumentCore` holds `RefCell` fields — `!Sync`). Same-thread worker pattern (`parse + consume + return primitives` inside one thread) works; `asyncio.to_thread(rhwp.parse, path)` does NOT — the Future resolves on the main thread and first attribute access panics with `_rhwp::document::PyDocument is unsendable, but sent to another thread`
43
+
- GIL release via `py.detach` in `_Document::from_bytes` / `render_pdf()` / `export_pdf()` — keep this pattern when adding new CPU/IO-bound methods
44
44
-`abi3-py310` feature: **one wheel covers 3.10–3.13+**. Don't bind to Python version-specific C API
45
45
46
46
### Async direction
47
47
- Python-surface APIs for I/O and integrations are **async-first**: when adding LangChain / LlamaIndex / Haystack loaders, implement `aload` / `alazy_load` / async counterparts alongside sync versions
48
-
- Rust core is sync; bindings may release the GIL so `asyncio.to_thread(rhwp.parse, path)` works. Document this pattern in user-facing docs rather than wrapping in Python
48
+
-**Forbidden pattern**: `asyncio.to_thread(rhwp.parse, path)` — `_Document` is unsendable (see Rust+Python hybrid build note above), the returned Document panics on main-thread access. `async fn` in `#[pymethods]` is also incompatible (PyO3 requires `Send + 'static` futures)
49
+
-**Supported async pattern**: `aparse(path)` uses `aiofiles.open()` for the file read on the event-loop thread, then calls `Document.from_bytes(data)` on the same thread. Document never crosses a thread boundary. Optional dep: `pip install rhwp[async]` — missing `aiofiles` raises `ImportError` (no silent fallback)
50
+
-**Document instance-level async methods (`doc.ato_ir()` etc.) are NOT provided** — they would require thread offload which unsendable forbids. For async code, `await rhwp.aparse(path)` once, then call sync methods on the Document directly (these are fast, in-memory, GIL-holding operations)
51
+
- If upstream rhwp ever replaces its `RefCell` caches with thread-safe synchronization, revisit this — `unsendable` could then be dropped, enabling true `async fn pymethods`
49
52
50
53
### Tests
51
54
- Real HWP fixtures live in the submodule: `external/rhwp/samples/aift.hwp` (HWP5), `table-vpos-01.hwpx` (HWPX). `tests/conftest.py` + `benches/bench_gil.py` reference this path
0 commit comments