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
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ table.set_cell_text(1, 1, str(len(document.paragraphs)))
document.add_memo_with_anchor("배포 전 검토", paragraph=paragraph, memo_shape_id_ref="0")

# 3) 다른 이름으로 저장
document.save("output/example.hwpx")
document.save_to_path("output/example.hwpx")
```

`HwpxDocument.add_table()`은 문서에 정의된 테두리 채우기가 없으면 헤더 참조 목록에 "기본 실선" `borderFill`을 만들어 표와 모든 셀에 참조를 연결합니다.
Expand All @@ -67,6 +67,17 @@ document.save("output/example.hwpx")

더 많은 실전 패턴은 [빠른 시작](docs/quickstart.md)과 [사용 가이드](docs/usage.md)의 "빠른 예제 모음"에서 확인할 수 있습니다.

### 저장 API 변경 안내

`HwpxDocument`는 저장 사용 케이스를 다음처럼 분리해 제공합니다.

- `save_to_path(path) -> str | PathLike[str]`: 지정한 경로로 저장하고 같은 경로를 반환
- `save_to_stream(stream) -> BinaryIO`: 파일/버퍼 스트림에 저장하고 같은 스트림을 반환
- `to_bytes() -> bytes`: 메모리에서 직렬화한 바이트를 반환

기존 `save()`는 하위 호환을 위해 유지되지만 deprecated 경고를 발생시킵니다. 새 코드에서는 위 3개 메서드 사용을 권장합니다.


## 문서
[사용법](https://airmang.github.io/python-hwpx/)

Expand Down
10 changes: 8 additions & 2 deletions docs/api_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,14 @@

#### 영속성(Persistence)

- `save(path_or_stream=None)`
- 변경된(dirty) XML 파트를 내부 패키지를 통해 직렬화하고, 변경 사항을 원본 소스, 새 경로 또는 파일과 유사한 객체에 씁니다.
- `save_to_path(path) -> str | PathLike[str]`
- 변경된 XML 파트를 지정한 경로에 저장하고 입력 경로를 그대로 반환합니다.
- `save_to_stream(stream) -> BinaryIO`
- 변경된 XML 파트를 파일과 유사한 바이너리 스트림에 저장하고 입력 스트림을 그대로 반환합니다.
- `to_bytes() -> bytes`
- 변경된 XML 파트를 직렬화한 HWPX ZIP 바이트를 반환합니다.
- `save(path_or_stream=None) -> str | PathLike[str] | BinaryIO | bytes`
- 하위 호환용 래퍼입니다. 내부에서 `save_to_path()`/`save_to_stream()`/`to_bytes()`를 호출하며 `DeprecationWarning`을 발생시킵니다.

***

Expand Down
65 changes: 58 additions & 7 deletions src/hwpx/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
from __future__ import annotations

import io
import warnings
from datetime import datetime
import logging
import uuid

from os import PathLike
from typing import Any, BinaryIO, Iterator
from typing import Any, BinaryIO, Iterator, overload

from lxml import etree

Expand Down Expand Up @@ -822,12 +823,62 @@ def remove_footer(
target_section = self._root.sections[-1]
target_section.properties.remove_footer(page_type=page_type)

def save_to_path(self, path: str | PathLike[str]) -> str | PathLike[str]:
"""Persist pending changes to *path* and return the same path."""

updates = self._root.serialize()
result = self._package.save(path, updates)
self._root.reset_dirty()
return path if result is None else result

def save_to_stream(self, stream: BinaryIO) -> BinaryIO:
"""Persist pending changes to *stream* and return the same stream."""

updates = self._root.serialize()
result = self._package.save(stream, updates)
self._root.reset_dirty()
return stream if result is None else result

def to_bytes(self) -> bytes:
"""Serialize pending changes and return the HWPX archive as bytes."""

updates = self._root.serialize()
result = self._package.save(None, updates)
self._root.reset_dirty()
if isinstance(result, bytes):
return result
raise TypeError("package.save(None) must return bytes")

@overload
def save(self, path_or_stream: None = None) -> bytes: ...

@overload
def save(self, path_or_stream: str | PathLike[str]) -> str | PathLike[str]: ...

@overload
def save(self, path_or_stream: BinaryIO) -> BinaryIO: ...

def save(
self,
path_or_stream: str | PathLike[str] | BinaryIO | None = None,
) -> str | PathLike[str] | BinaryIO | bytes | None:
"""Persist pending changes to *path_or_stream* or the original source."""
updates = self._root.serialize()
result = self._package.save(path_or_stream, updates)
self._root.reset_dirty()
return result
) -> str | PathLike[str] | BinaryIO | bytes:
"""Deprecated compatibility wrapper around save_to_path/save_to_stream/to_bytes.

Deprecated:
``save()``는 하위 호환을 위해 유지되며 향후 제거될 수 있습니다.
- 경로 저장: ``save_to_path(path)``
- 스트림 저장: ``save_to_stream(stream)``
- 바이트 반환: ``to_bytes()``
"""

warnings.warn(
"HwpxDocument.save()는 deprecated 예정입니다. "
"save_to_path()/save_to_stream()/to_bytes() 사용을 권장합니다.",
DeprecationWarning,
stacklevel=2,
)
if path_or_stream is None:
return self.to_bytes()
if isinstance(path_or_stream, (str, PathLike)):
return self.save_to_path(path_or_stream)
return self.save_to_stream(path_or_stream)
55 changes: 55 additions & 0 deletions tests/test_document_save_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from __future__ import annotations

from io import BytesIO
from pathlib import Path

import pytest

from hwpx.document import HwpxDocument


def test_to_bytes_returns_hwpx_bytes() -> None:
document = HwpxDocument.new()

output = document.to_bytes()

assert isinstance(output, bytes)
assert output.startswith(b"PK")


def test_save_to_path_returns_same_path(tmp_path: Path) -> None:
document = HwpxDocument.new()
output_path = tmp_path / "saved.hwpx"

result = document.save_to_path(output_path)

assert result == output_path
assert output_path.exists()


def test_save_to_stream_returns_same_stream() -> None:
document = HwpxDocument.new()
stream = BytesIO()

result = document.save_to_stream(stream)

assert result is stream
stream.seek(0)
assert stream.read(2) == b"PK"


def test_save_deprecated_wrapper_warns_and_routes_to_new_methods(tmp_path: Path) -> None:
document = HwpxDocument.new()

with pytest.deprecated_call(match="deprecated"):
path_result = document.save(tmp_path / "wrapped-path.hwpx")
assert path_result == tmp_path / "wrapped-path.hwpx"

with pytest.deprecated_call(match="deprecated"):
bytes_result = document.save()
assert isinstance(bytes_result, bytes)

stream = BytesIO()
with pytest.deprecated_call(match="deprecated"):
stream_result = document.save(stream)
assert stream_result is stream