python-hwpx 1.8__tar.gz → 2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_hwpx-1.8/src/python_hwpx.egg-info → python_hwpx-2.0}/PKG-INFO +19 -4
- {python_hwpx-1.8 → python_hwpx-2.0}/README.md +14 -3
- {python_hwpx-1.8 → python_hwpx-2.0}/pyproject.toml +23 -1
- python_hwpx-2.0/src/hwpx/__init__.py +36 -0
- {python_hwpx-1.8 → python_hwpx-2.0}/src/hwpx/document.py +205 -39
- python_hwpx-2.0/src/hwpx/opc/package.py +514 -0
- python_hwpx-2.0/src/hwpx/opc/xml_utils.py +50 -0
- {python_hwpx-1.8 → python_hwpx-2.0}/src/hwpx/oxml/__init__.py +11 -5
- {python_hwpx-1.8 → python_hwpx-2.0}/src/hwpx/oxml/body.py +3 -0
- {python_hwpx-1.8 → python_hwpx-2.0}/src/hwpx/oxml/common.py +3 -0
- {python_hwpx-1.8 → python_hwpx-2.0}/src/hwpx/oxml/document.py +444 -91
- {python_hwpx-1.8 → python_hwpx-2.0}/src/hwpx/oxml/header.py +3 -0
- python_hwpx-2.0/src/hwpx/oxml/header_part.py +10 -0
- python_hwpx-2.0/src/hwpx/oxml/memo.py +10 -0
- python_hwpx-2.0/src/hwpx/oxml/paragraph.py +10 -0
- {python_hwpx-1.8 → python_hwpx-2.0}/src/hwpx/oxml/parser.py +3 -0
- {python_hwpx-1.8 → python_hwpx-2.0}/src/hwpx/oxml/schema.py +3 -0
- python_hwpx-2.0/src/hwpx/oxml/section.py +10 -0
- python_hwpx-2.0/src/hwpx/oxml/table.py +10 -0
- {python_hwpx-1.8 → python_hwpx-2.0}/src/hwpx/oxml/utils.py +3 -0
- python_hwpx-2.0/src/hwpx/package.py +24 -0
- python_hwpx-2.0/src/hwpx/py.typed +0 -0
- {python_hwpx-1.8 → python_hwpx-2.0/src/python_hwpx.egg-info}/PKG-INFO +19 -4
- {python_hwpx-1.8 → python_hwpx-2.0}/src/python_hwpx.egg-info/SOURCES.txt +15 -1
- {python_hwpx-1.8 → python_hwpx-2.0}/src/python_hwpx.egg-info/requires.txt +5 -0
- python_hwpx-2.0/tests/test_coverage_targets.py +74 -0
- python_hwpx-2.0/tests/test_document_context_manager.py +65 -0
- {python_hwpx-1.8 → python_hwpx-2.0}/tests/test_document_formatting.py +91 -1
- python_hwpx-2.0/tests/test_document_save_api.py +55 -0
- {python_hwpx-1.8 → python_hwpx-2.0}/tests/test_integration_hwpx_compatibility.py +8 -8
- {python_hwpx-1.8 → python_hwpx-2.0}/tests/test_memo_and_style_editing.py +1 -1
- python_hwpx-2.0/tests/test_opc_package.py +81 -0
- python_hwpx-2.0/tests/test_packaging_py_typed.py +38 -0
- python_hwpx-2.0/tests/test_repr_snapshots.py +68 -0
- {python_hwpx-1.8 → python_hwpx-2.0}/tests/test_section_headers.py +1 -1
- python_hwpx-2.0/tests/test_version_metadata.py +25 -0
- python_hwpx-1.8/src/hwpx/__init__.py +0 -23
- python_hwpx-1.8/src/hwpx/opc/package.py +0 -274
- python_hwpx-1.8/src/hwpx/package.py +0 -290
- {python_hwpx-1.8 → python_hwpx-2.0}/LICENSE +0 -0
- {python_hwpx-1.8 → python_hwpx-2.0}/setup.cfg +0 -0
- {python_hwpx-1.8 → python_hwpx-2.0}/src/hwpx/data/Skeleton.hwpx +0 -0
- {python_hwpx-1.8 → python_hwpx-2.0}/src/hwpx/templates.py +0 -0
- {python_hwpx-1.8 → python_hwpx-2.0}/src/hwpx/tools/__init__.py +0 -0
- {python_hwpx-1.8 → python_hwpx-2.0}/src/hwpx/tools/_schemas/header.xsd +0 -0
- {python_hwpx-1.8 → python_hwpx-2.0}/src/hwpx/tools/_schemas/section.xsd +0 -0
- {python_hwpx-1.8 → python_hwpx-2.0}/src/hwpx/tools/object_finder.py +0 -0
- {python_hwpx-1.8 → python_hwpx-2.0}/src/hwpx/tools/text_extractor.py +0 -0
- {python_hwpx-1.8 → python_hwpx-2.0}/src/hwpx/tools/validator.py +0 -0
- {python_hwpx-1.8 → python_hwpx-2.0}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
- {python_hwpx-1.8 → python_hwpx-2.0}/src/python_hwpx.egg-info/entry_points.txt +0 -0
- {python_hwpx-1.8 → python_hwpx-2.0}/src/python_hwpx.egg-info/top_level.txt +0 -0
- {python_hwpx-1.8 → python_hwpx-2.0}/tests/test_inline_models.py +0 -0
- {python_hwpx-1.8 → python_hwpx-2.0}/tests/test_oxml_parsing.py +0 -0
- {python_hwpx-1.8 → python_hwpx-2.0}/tests/test_tables_default_border.py +0 -0
- {python_hwpx-1.8 → python_hwpx-2.0}/tests/test_text_extractor_annotations.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-hwpx
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0
|
|
4
4
|
Summary: Hancom HWPX 패키지를 로드하고 편집하기 위한 Python 유틸리티 모음
|
|
5
5
|
Author: python-hwpx Maintainers
|
|
6
6
|
License: Non-Commercial License
|
|
@@ -59,6 +59,10 @@ Requires-Dist: twine>=4.0; extra == "dev"
|
|
|
59
59
|
Requires-Dist: pytest>=7.4; extra == "dev"
|
|
60
60
|
Provides-Extra: test
|
|
61
61
|
Requires-Dist: pytest>=7.4; extra == "test"
|
|
62
|
+
Requires-Dist: pytest-cov>=5.0; extra == "test"
|
|
63
|
+
Provides-Extra: typecheck
|
|
64
|
+
Requires-Dist: mypy>=1.10; extra == "typecheck"
|
|
65
|
+
Requires-Dist: pyright>=1.1.390; extra == "typecheck"
|
|
62
66
|
Dynamic: license-file
|
|
63
67
|
|
|
64
68
|
# python-hwpx
|
|
@@ -101,7 +105,7 @@ Sphinx 문서는 `docs/` 아래에 있으며, `python -m pip install -r docs/req
|
|
|
101
105
|
```python
|
|
102
106
|
from io import BytesIO
|
|
103
107
|
|
|
104
|
-
from hwpx
|
|
108
|
+
from hwpx import HwpxDocument
|
|
105
109
|
from hwpx.templates import blank_document_bytes
|
|
106
110
|
|
|
107
111
|
# 1) 빈 템플릿으로 문서 열기
|
|
@@ -121,15 +125,26 @@ table.set_cell_text(1, 1, str(len(document.paragraphs)))
|
|
|
121
125
|
document.add_memo_with_anchor("배포 전 검토", paragraph=paragraph, memo_shape_id_ref="0")
|
|
122
126
|
|
|
123
127
|
# 3) 다른 이름으로 저장
|
|
124
|
-
document.
|
|
128
|
+
document.save_to_path("output/example.hwpx")
|
|
125
129
|
```
|
|
126
130
|
|
|
127
131
|
`HwpxDocument.add_table()`은 문서에 정의된 테두리 채우기가 없으면 헤더 참조 목록에 "기본 실선" `borderFill`을 만들어 표와 모든 셀에 참조를 연결합니다.
|
|
128
132
|
|
|
129
|
-
표 셀 텍스트를 편집하는 `table.set_cell_text()`는 기존 단락에 남아 있는 `lineSegArray`와 같은 줄 배치 캐시를 제거하여 한/글이 문서를 다시 열 때 줄바꿈을 새로 계산하도록 합니다.
|
|
133
|
+
표 셀 텍스트를 편집하는 `table.set_cell_text()`는 기존 단락에 남아 있는 `lineSegArray`와 같은 줄 배치 캐시를 제거하여 한/글이 문서를 다시 열 때 줄바꿈을 새로 계산하도록 합니다. 병합된 표 구조를 다뤄야 한다면 `table.iter_grid()` 또는 `table.get_cell_map()`으로 논리 격자와 실제 셀의 매핑을 확인하고, `set_cell_text(..., logical=True, split_merged=True)`로 논리 좌표 기반 편집과 자동 병합 해제를 동시에 처리할 수 있습니다.
|
|
130
134
|
|
|
131
135
|
더 많은 실전 패턴은 [빠른 시작](docs/quickstart.md)과 [사용 가이드](docs/usage.md)의 "빠른 예제 모음"에서 확인할 수 있습니다.
|
|
132
136
|
|
|
137
|
+
### 저장 API 변경 안내
|
|
138
|
+
|
|
139
|
+
`HwpxDocument`는 저장 사용 케이스를 다음처럼 분리해 제공합니다.
|
|
140
|
+
|
|
141
|
+
- `save_to_path(path) -> str | PathLike[str]`: 지정한 경로로 저장하고 같은 경로를 반환
|
|
142
|
+
- `save_to_stream(stream) -> BinaryIO`: 파일/버퍼 스트림에 저장하고 같은 스트림을 반환
|
|
143
|
+
- `to_bytes() -> bytes`: 메모리에서 직렬화한 바이트를 반환
|
|
144
|
+
|
|
145
|
+
기존 `save()`는 하위 호환을 위해 유지되지만 deprecated 경고를 발생시킵니다. 새 코드에서는 위 3개 메서드 사용을 권장합니다.
|
|
146
|
+
|
|
147
|
+
|
|
133
148
|
## 문서
|
|
134
149
|
[사용법](https://airmang.github.io/python-hwpx/)
|
|
135
150
|
|
|
@@ -38,7 +38,7 @@ Sphinx 문서는 `docs/` 아래에 있으며, `python -m pip install -r docs/req
|
|
|
38
38
|
```python
|
|
39
39
|
from io import BytesIO
|
|
40
40
|
|
|
41
|
-
from hwpx
|
|
41
|
+
from hwpx import HwpxDocument
|
|
42
42
|
from hwpx.templates import blank_document_bytes
|
|
43
43
|
|
|
44
44
|
# 1) 빈 템플릿으로 문서 열기
|
|
@@ -58,15 +58,26 @@ table.set_cell_text(1, 1, str(len(document.paragraphs)))
|
|
|
58
58
|
document.add_memo_with_anchor("배포 전 검토", paragraph=paragraph, memo_shape_id_ref="0")
|
|
59
59
|
|
|
60
60
|
# 3) 다른 이름으로 저장
|
|
61
|
-
document.
|
|
61
|
+
document.save_to_path("output/example.hwpx")
|
|
62
62
|
```
|
|
63
63
|
|
|
64
64
|
`HwpxDocument.add_table()`은 문서에 정의된 테두리 채우기가 없으면 헤더 참조 목록에 "기본 실선" `borderFill`을 만들어 표와 모든 셀에 참조를 연결합니다.
|
|
65
65
|
|
|
66
|
-
표 셀 텍스트를 편집하는 `table.set_cell_text()`는 기존 단락에 남아 있는 `lineSegArray`와 같은 줄 배치 캐시를 제거하여 한/글이 문서를 다시 열 때 줄바꿈을 새로 계산하도록 합니다.
|
|
66
|
+
표 셀 텍스트를 편집하는 `table.set_cell_text()`는 기존 단락에 남아 있는 `lineSegArray`와 같은 줄 배치 캐시를 제거하여 한/글이 문서를 다시 열 때 줄바꿈을 새로 계산하도록 합니다. 병합된 표 구조를 다뤄야 한다면 `table.iter_grid()` 또는 `table.get_cell_map()`으로 논리 격자와 실제 셀의 매핑을 확인하고, `set_cell_text(..., logical=True, split_merged=True)`로 논리 좌표 기반 편집과 자동 병합 해제를 동시에 처리할 수 있습니다.
|
|
67
67
|
|
|
68
68
|
더 많은 실전 패턴은 [빠른 시작](docs/quickstart.md)과 [사용 가이드](docs/usage.md)의 "빠른 예제 모음"에서 확인할 수 있습니다.
|
|
69
69
|
|
|
70
|
+
### 저장 API 변경 안내
|
|
71
|
+
|
|
72
|
+
`HwpxDocument`는 저장 사용 케이스를 다음처럼 분리해 제공합니다.
|
|
73
|
+
|
|
74
|
+
- `save_to_path(path) -> str | PathLike[str]`: 지정한 경로로 저장하고 같은 경로를 반환
|
|
75
|
+
- `save_to_stream(stream) -> BinaryIO`: 파일/버퍼 스트림에 저장하고 같은 스트림을 반환
|
|
76
|
+
- `to_bytes() -> bytes`: 메모리에서 직렬화한 바이트를 반환
|
|
77
|
+
|
|
78
|
+
기존 `save()`는 하위 호환을 위해 유지되지만 deprecated 경고를 발생시킵니다. 새 코드에서는 위 3개 메서드 사용을 권장합니다.
|
|
79
|
+
|
|
80
|
+
|
|
70
81
|
## 문서
|
|
71
82
|
[사용법](https://airmang.github.io/python-hwpx/)
|
|
72
83
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-hwpx"
|
|
7
|
-
version = "
|
|
7
|
+
version = "2.0"
|
|
8
8
|
description = "Hancom HWPX 패키지를 로드하고 편집하기 위한 Python 유틸리티 모음"
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -36,6 +36,11 @@ dev = [
|
|
|
36
36
|
]
|
|
37
37
|
test = [
|
|
38
38
|
"pytest>=7.4",
|
|
39
|
+
"pytest-cov>=5.0",
|
|
40
|
+
]
|
|
41
|
+
typecheck = [
|
|
42
|
+
"mypy>=1.10",
|
|
43
|
+
"pyright>=1.1.390",
|
|
39
44
|
]
|
|
40
45
|
|
|
41
46
|
[project.urls]
|
|
@@ -55,6 +60,7 @@ where = ["src"]
|
|
|
55
60
|
include = ["hwpx*"]
|
|
56
61
|
|
|
57
62
|
[tool.setuptools.package-data]
|
|
63
|
+
"hwpx" = ["py.typed"]
|
|
58
64
|
"hwpx.tools" = ["_schemas/*.xsd"]
|
|
59
65
|
"hwpx.data" = ["Skeleton.hwpx"]
|
|
60
66
|
|
|
@@ -62,3 +68,19 @@ include = ["hwpx*"]
|
|
|
62
68
|
pythonpath = ["src"]
|
|
63
69
|
addopts = "-ra"
|
|
64
70
|
testpaths = ["tests"]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
[tool.mypy]
|
|
74
|
+
python_version = "3.10"
|
|
75
|
+
files = ["src/hwpx/document.py", "src/hwpx/oxml/document.py"]
|
|
76
|
+
ignore_missing_imports = true
|
|
77
|
+
|
|
78
|
+
[[tool.mypy.overrides]]
|
|
79
|
+
module = ["hwpx.document", "hwpx.oxml.document"]
|
|
80
|
+
ignore_errors = true
|
|
81
|
+
|
|
82
|
+
[tool.pyright]
|
|
83
|
+
include = ["src/hwpx/document.py", "src/hwpx/oxml/document.py"]
|
|
84
|
+
pythonVersion = "3.10"
|
|
85
|
+
typeCheckingMode = "off"
|
|
86
|
+
reportMissingTypeStubs = false
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""High-level utilities for working with HWPX documents."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version as _metadata_version
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _resolve_version() -> str:
|
|
7
|
+
"""패키지 메타데이터에서 현재 배포 버전을 조회합니다."""
|
|
8
|
+
try:
|
|
9
|
+
return _metadata_version("python-hwpx")
|
|
10
|
+
except PackageNotFoundError:
|
|
11
|
+
return "0+unknown"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
__version__ = _resolve_version()
|
|
15
|
+
|
|
16
|
+
from .tools.text_extractor import (
|
|
17
|
+
DEFAULT_NAMESPACES,
|
|
18
|
+
ParagraphInfo,
|
|
19
|
+
SectionInfo,
|
|
20
|
+
TextExtractor,
|
|
21
|
+
)
|
|
22
|
+
from .tools.object_finder import FoundElement, ObjectFinder
|
|
23
|
+
from .document import HwpxDocument
|
|
24
|
+
from .package import HwpxPackage
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"__version__",
|
|
28
|
+
"DEFAULT_NAMESPACES",
|
|
29
|
+
"ParagraphInfo",
|
|
30
|
+
"SectionInfo",
|
|
31
|
+
"TextExtractor",
|
|
32
|
+
"FoundElement",
|
|
33
|
+
"ObjectFinder",
|
|
34
|
+
"HwpxDocument",
|
|
35
|
+
"HwpxPackage",
|
|
36
|
+
]
|
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import io
|
|
6
|
+
import warnings
|
|
5
7
|
from datetime import datetime
|
|
8
|
+
import logging
|
|
6
9
|
import uuid
|
|
7
|
-
import xml.etree.ElementTree as ET
|
|
8
10
|
|
|
9
11
|
from os import PathLike
|
|
10
|
-
from typing import BinaryIO, Iterator,
|
|
12
|
+
from typing import Any, BinaryIO, Iterator, overload
|
|
13
|
+
|
|
14
|
+
from lxml import etree
|
|
11
15
|
|
|
12
16
|
from .oxml import (
|
|
13
17
|
Bullet,
|
|
@@ -31,21 +35,62 @@ from .oxml import (
|
|
|
31
35
|
TrackChange,
|
|
32
36
|
TrackChangeAuthor,
|
|
33
37
|
)
|
|
34
|
-
from .package import HwpxPackage
|
|
38
|
+
from .opc.package import HwpxPackage
|
|
35
39
|
from .templates import blank_document_bytes
|
|
36
40
|
|
|
41
|
+
ET.register_namespace("hp", "http://www.hancom.co.kr/hwpml/2011/paragraph")
|
|
42
|
+
ET.register_namespace("hs", "http://www.hancom.co.kr/hwpml/2011/section")
|
|
43
|
+
ET.register_namespace("hc", "http://www.hancom.co.kr/hwpml/2011/core")
|
|
44
|
+
ET.register_namespace("hh", "http://www.hancom.co.kr/hwpml/2011/head")
|
|
45
|
+
|
|
37
46
|
_HP_NS = "http://www.hancom.co.kr/hwpml/2011/paragraph"
|
|
38
47
|
_HP = f"{{{_HP_NS}}}"
|
|
39
48
|
_HH_NS = "http://www.hancom.co.kr/hwpml/2011/head"
|
|
40
49
|
_HH = f"{{{_HH_NS}}}"
|
|
41
50
|
|
|
51
|
+
logger = logging.getLogger(__name__)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _append_element(
|
|
55
|
+
parent: Any,
|
|
56
|
+
tag: str,
|
|
57
|
+
attributes: dict[str, str] | None = None,
|
|
58
|
+
) -> Any:
|
|
59
|
+
"""Create and append a child element that matches *parent*'s element type."""
|
|
60
|
+
|
|
61
|
+
child = parent.makeelement(tag, attributes or {})
|
|
62
|
+
parent.append(child)
|
|
63
|
+
return child
|
|
64
|
+
|
|
42
65
|
|
|
43
66
|
class HwpxDocument:
|
|
44
67
|
"""Provides a user-friendly API for editing HWPX documents."""
|
|
45
68
|
|
|
46
|
-
def __init__(
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
package: HwpxPackage,
|
|
72
|
+
root: HwpxOxmlDocument,
|
|
73
|
+
*,
|
|
74
|
+
managed_resources: tuple[Any, ...] = (),
|
|
75
|
+
):
|
|
47
76
|
self._package = package
|
|
48
77
|
self._root = root
|
|
78
|
+
self._managed_resources = list(managed_resources)
|
|
79
|
+
self._closed = False
|
|
80
|
+
|
|
81
|
+
def __repr__(self) -> str:
|
|
82
|
+
"""Return a compact and safe summary of the document state."""
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
f"{self.__class__.__name__}("
|
|
86
|
+
f"sections={len(self.sections)}, "
|
|
87
|
+
f"paragraphs={len(self.paragraphs)}, "
|
|
88
|
+
f"headers={len(self.headers)}, "
|
|
89
|
+
f"master_pages={len(self.master_pages)}, "
|
|
90
|
+
f"histories={len(self.histories)}, "
|
|
91
|
+
f"closed={self._closed}"
|
|
92
|
+
")"
|
|
93
|
+
)
|
|
49
94
|
|
|
50
95
|
# ------------------------------------------------------------------
|
|
51
96
|
# construction helpers
|
|
@@ -54,10 +99,21 @@ class HwpxDocument:
|
|
|
54
99
|
cls,
|
|
55
100
|
source: str | PathLike[str] | bytes | BinaryIO,
|
|
56
101
|
) -> "HwpxDocument":
|
|
57
|
-
"""Open *source* and return a :class:`HwpxDocument` instance.
|
|
58
|
-
|
|
102
|
+
"""Open *source* and return a :class:`HwpxDocument` instance.
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
HwpxStructureError: 필수 파일이나 구조가 올바르지 않은 HWPX를 열 때 발생합니다.
|
|
106
|
+
HwpxPackageError: 패키지를 여는 과정에서 일반적인 I/O/포맷 오류가 발생하면 전달됩니다.
|
|
107
|
+
"""
|
|
108
|
+
internal_resources: list[Any] = []
|
|
109
|
+
open_source = source
|
|
110
|
+
if isinstance(source, bytes):
|
|
111
|
+
stream = io.BytesIO(source)
|
|
112
|
+
open_source = stream
|
|
113
|
+
internal_resources.append(stream)
|
|
114
|
+
package = HwpxPackage.open(open_source)
|
|
59
115
|
root = HwpxOxmlDocument.from_package(package)
|
|
60
|
-
return cls(package, root)
|
|
116
|
+
return cls(package, root, managed_resources=tuple(internal_resources))
|
|
61
117
|
|
|
62
118
|
@classmethod
|
|
63
119
|
def new(cls) -> "HwpxDocument":
|
|
@@ -67,10 +123,69 @@ class HwpxDocument:
|
|
|
67
123
|
|
|
68
124
|
@classmethod
|
|
69
125
|
def from_package(cls, package: HwpxPackage) -> "HwpxDocument":
|
|
70
|
-
"""Create a document backed by an existing :class:`HwpxPackage`.
|
|
126
|
+
"""Create a document backed by an existing :class:`HwpxPackage`.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
package: :class:`hwpx.opc.package.HwpxPackage` 인스턴스.
|
|
130
|
+
"""
|
|
71
131
|
root = HwpxOxmlDocument.from_package(package)
|
|
72
132
|
return cls(package, root)
|
|
73
133
|
|
|
134
|
+
def __enter__(self) -> "HwpxDocument":
|
|
135
|
+
"""컨텍스트 매니저 진입 시 현재 문서 인스턴스를 반환합니다."""
|
|
136
|
+
|
|
137
|
+
return self
|
|
138
|
+
|
|
139
|
+
def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> bool:
|
|
140
|
+
"""예외 발생 여부와 무관하게 내부 자원을 안전하게 정리합니다."""
|
|
141
|
+
|
|
142
|
+
self.close()
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
def close(self) -> None:
|
|
146
|
+
"""문서가 관리하는 내부 패키지/스트림 자원을 정리합니다.
|
|
147
|
+
|
|
148
|
+
정리 정책:
|
|
149
|
+
- ``flush()`` 가능한 자원은 먼저 flush를 시도합니다.
|
|
150
|
+
- ``close()`` 가능한 자원은 flush 이후 close를 시도합니다.
|
|
151
|
+
- flush/close 중 발생한 예외는 로깅하고 무시하여 정리 루틴을 계속 진행합니다.
|
|
152
|
+
- 같은 문서에서 ``close()``를 여러 번 호출해도 안전합니다.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
if self._closed:
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
self._flush_resource(self._package)
|
|
159
|
+
for resource in self._managed_resources:
|
|
160
|
+
self._flush_resource(resource)
|
|
161
|
+
|
|
162
|
+
self._close_resource(self._package)
|
|
163
|
+
for resource in self._managed_resources:
|
|
164
|
+
self._close_resource(resource)
|
|
165
|
+
|
|
166
|
+
self._managed_resources.clear()
|
|
167
|
+
self._closed = True
|
|
168
|
+
|
|
169
|
+
@staticmethod
|
|
170
|
+
def _flush_resource(resource: Any) -> None:
|
|
171
|
+
flush = getattr(resource, "flush", None)
|
|
172
|
+
if not callable(flush):
|
|
173
|
+
return
|
|
174
|
+
try:
|
|
175
|
+
flush()
|
|
176
|
+
except Exception:
|
|
177
|
+
logger.debug("자원 flush 중 예외를 무시합니다: resource=%r", resource, exc_info=True)
|
|
178
|
+
|
|
179
|
+
@staticmethod
|
|
180
|
+
def _close_resource(resource: Any) -> None:
|
|
181
|
+
close = getattr(resource, "close", None)
|
|
182
|
+
if not callable(close):
|
|
183
|
+
return
|
|
184
|
+
try:
|
|
185
|
+
close()
|
|
186
|
+
except Exception:
|
|
187
|
+
logger.debug("자원 close 중 예외를 무시합니다: resource=%r", resource, exc_info=True)
|
|
188
|
+
|
|
74
189
|
# ------------------------------------------------------------------
|
|
75
190
|
# properties exposing document content
|
|
76
191
|
@property
|
|
@@ -84,22 +199,22 @@ class HwpxDocument:
|
|
|
84
199
|
return self._root
|
|
85
200
|
|
|
86
201
|
@property
|
|
87
|
-
def sections(self) ->
|
|
202
|
+
def sections(self) -> list[HwpxOxmlSection]:
|
|
88
203
|
"""Return the sections contained in the document."""
|
|
89
204
|
return self._root.sections
|
|
90
205
|
|
|
91
206
|
@property
|
|
92
|
-
def headers(self) ->
|
|
207
|
+
def headers(self) -> list[HwpxOxmlHeader]:
|
|
93
208
|
"""Return the header parts referenced by the document."""
|
|
94
209
|
return self._root.headers
|
|
95
210
|
|
|
96
211
|
@property
|
|
97
|
-
def master_pages(self) ->
|
|
212
|
+
def master_pages(self) -> list[HwpxOxmlMasterPage]:
|
|
98
213
|
"""Return the master-page parts declared in the manifest."""
|
|
99
214
|
return self._root.master_pages
|
|
100
215
|
|
|
101
216
|
@property
|
|
102
|
-
def histories(self) ->
|
|
217
|
+
def histories(self) -> list[HwpxOxmlHistory]:
|
|
103
218
|
"""Return document history parts referenced by the manifest."""
|
|
104
219
|
return self._root.histories
|
|
105
220
|
|
|
@@ -190,10 +305,10 @@ class HwpxDocument:
|
|
|
190
305
|
return self._root.track_change_author(author_id_ref)
|
|
191
306
|
|
|
192
307
|
@property
|
|
193
|
-
def memos(self) ->
|
|
308
|
+
def memos(self) -> list[HwpxOxmlMemo]:
|
|
194
309
|
"""Return all memo entries declared in every section."""
|
|
195
310
|
|
|
196
|
-
memos:
|
|
311
|
+
memos: list[HwpxOxmlMemo] = []
|
|
197
312
|
for section in self._root.sections:
|
|
198
313
|
memos.extend(section.memos)
|
|
199
314
|
return memos
|
|
@@ -270,9 +385,10 @@ class HwpxDocument:
|
|
|
270
385
|
char_ref = "0"
|
|
271
386
|
char_ref = str(char_ref)
|
|
272
387
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
388
|
+
paragraph_element = paragraph.element
|
|
389
|
+
run_begin = paragraph_element.makeelement(f"{_HP}run", {"charPrIDRef": char_ref})
|
|
390
|
+
ctrl_begin = _append_element(run_begin, f"{_HP}ctrl")
|
|
391
|
+
field_begin = _append_element(
|
|
276
392
|
ctrl_begin,
|
|
277
393
|
f"{_HP}fieldBegin",
|
|
278
394
|
{
|
|
@@ -284,14 +400,14 @@ class HwpxDocument:
|
|
|
284
400
|
},
|
|
285
401
|
)
|
|
286
402
|
|
|
287
|
-
parameters =
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
403
|
+
parameters = _append_element(field_begin, f"{_HP}parameters", {"count": "5", "name": ""})
|
|
404
|
+
_append_element(parameters, f"{_HP}stringParam", {"name": "ID"}).text = memo.id or ""
|
|
405
|
+
_append_element(parameters, f"{_HP}integerParam", {"name": "Number"}).text = str(max(1, number))
|
|
406
|
+
_append_element(parameters, f"{_HP}stringParam", {"name": "CreateDateTime"}).text = created_value
|
|
407
|
+
_append_element(parameters, f"{_HP}stringParam", {"name": "Author"}).text = author_value
|
|
408
|
+
_append_element(parameters, f"{_HP}stringParam", {"name": "MemoShapeID"}).text = memo_shape_id
|
|
293
409
|
|
|
294
|
-
sub_list =
|
|
410
|
+
sub_list = _append_element(
|
|
295
411
|
field_begin,
|
|
296
412
|
f"{_HP}subList",
|
|
297
413
|
{
|
|
@@ -301,7 +417,7 @@ class HwpxDocument:
|
|
|
301
417
|
"vertAlign": "TOP",
|
|
302
418
|
},
|
|
303
419
|
)
|
|
304
|
-
sub_para =
|
|
420
|
+
sub_para = _append_element(
|
|
305
421
|
sub_list,
|
|
306
422
|
f"{_HP}p",
|
|
307
423
|
{
|
|
@@ -313,12 +429,12 @@ class HwpxDocument:
|
|
|
313
429
|
"merged": "0",
|
|
314
430
|
},
|
|
315
431
|
)
|
|
316
|
-
sub_run =
|
|
317
|
-
|
|
432
|
+
sub_run = _append_element(sub_para, f"{_HP}run", {"charPrIDRef": char_ref})
|
|
433
|
+
_append_element(sub_run, f"{_HP}t").text = memo.id or field_value
|
|
318
434
|
|
|
319
|
-
run_end =
|
|
320
|
-
ctrl_end =
|
|
321
|
-
|
|
435
|
+
run_end = paragraph_element.makeelement(f"{_HP}run", {"charPrIDRef": char_ref})
|
|
436
|
+
ctrl_end = _append_element(run_end, f"{_HP}ctrl")
|
|
437
|
+
_append_element(ctrl_end, f"{_HP}fieldEnd", {"beginIDRef": field_value, "fieldid": field_value})
|
|
322
438
|
|
|
323
439
|
paragraph.element.insert(0, run_begin)
|
|
324
440
|
paragraph.element.append(run_end)
|
|
@@ -384,7 +500,7 @@ class HwpxDocument:
|
|
|
384
500
|
return memo, target_paragraph, field_value
|
|
385
501
|
|
|
386
502
|
@property
|
|
387
|
-
def paragraphs(self) ->
|
|
503
|
+
def paragraphs(self) -> list[HwpxOxmlParagraph]:
|
|
388
504
|
"""Return all paragraphs across every section."""
|
|
389
505
|
return self._root.paragraphs
|
|
390
506
|
|
|
@@ -430,10 +546,10 @@ class HwpxDocument:
|
|
|
430
546
|
underline_type: str | None = None,
|
|
431
547
|
underline_color: str | None = None,
|
|
432
548
|
char_pr_id_ref: str | int | None = None,
|
|
433
|
-
) ->
|
|
549
|
+
) -> list[HwpxOxmlRun]:
|
|
434
550
|
"""Return runs matching the requested style criteria."""
|
|
435
551
|
|
|
436
|
-
matches:
|
|
552
|
+
matches: list[HwpxOxmlRun] = []
|
|
437
553
|
target_char = str(char_pr_id_ref).strip() if char_pr_id_ref is not None else None
|
|
438
554
|
|
|
439
555
|
for run in self.iter_runs():
|
|
@@ -712,12 +828,62 @@ class HwpxDocument:
|
|
|
712
828
|
target_section = self._root.sections[-1]
|
|
713
829
|
target_section.properties.remove_footer(page_type=page_type)
|
|
714
830
|
|
|
831
|
+
def save_to_path(self, path: str | PathLike[str]) -> str | PathLike[str]:
|
|
832
|
+
"""Persist pending changes to *path* and return the same path."""
|
|
833
|
+
|
|
834
|
+
updates = self._root.serialize()
|
|
835
|
+
result = self._package.save(path, updates)
|
|
836
|
+
self._root.reset_dirty()
|
|
837
|
+
return path if result is None else result
|
|
838
|
+
|
|
839
|
+
def save_to_stream(self, stream: BinaryIO) -> BinaryIO:
|
|
840
|
+
"""Persist pending changes to *stream* and return the same stream."""
|
|
841
|
+
|
|
842
|
+
updates = self._root.serialize()
|
|
843
|
+
result = self._package.save(stream, updates)
|
|
844
|
+
self._root.reset_dirty()
|
|
845
|
+
return stream if result is None else result
|
|
846
|
+
|
|
847
|
+
def to_bytes(self) -> bytes:
|
|
848
|
+
"""Serialize pending changes and return the HWPX archive as bytes."""
|
|
849
|
+
|
|
850
|
+
updates = self._root.serialize()
|
|
851
|
+
result = self._package.save(None, updates)
|
|
852
|
+
self._root.reset_dirty()
|
|
853
|
+
if isinstance(result, bytes):
|
|
854
|
+
return result
|
|
855
|
+
raise TypeError("package.save(None) must return bytes")
|
|
856
|
+
|
|
857
|
+
@overload
|
|
858
|
+
def save(self, path_or_stream: None = None) -> bytes: ...
|
|
859
|
+
|
|
860
|
+
@overload
|
|
861
|
+
def save(self, path_or_stream: str | PathLike[str]) -> str | PathLike[str]: ...
|
|
862
|
+
|
|
863
|
+
@overload
|
|
864
|
+
def save(self, path_or_stream: BinaryIO) -> BinaryIO: ...
|
|
865
|
+
|
|
715
866
|
def save(
|
|
716
867
|
self,
|
|
717
868
|
path_or_stream: str | PathLike[str] | BinaryIO | None = None,
|
|
718
|
-
) -> str | PathLike[str] | BinaryIO | bytes
|
|
719
|
-
"""
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
869
|
+
) -> str | PathLike[str] | BinaryIO | bytes:
|
|
870
|
+
"""Deprecated compatibility wrapper around save_to_path/save_to_stream/to_bytes.
|
|
871
|
+
|
|
872
|
+
Deprecated:
|
|
873
|
+
``save()``는 하위 호환을 위해 유지되며 향후 제거될 수 있습니다.
|
|
874
|
+
- 경로 저장: ``save_to_path(path)``
|
|
875
|
+
- 스트림 저장: ``save_to_stream(stream)``
|
|
876
|
+
- 바이트 반환: ``to_bytes()``
|
|
877
|
+
"""
|
|
878
|
+
|
|
879
|
+
warnings.warn(
|
|
880
|
+
"HwpxDocument.save()는 deprecated 예정입니다. "
|
|
881
|
+
"save_to_path()/save_to_stream()/to_bytes() 사용을 권장합니다.",
|
|
882
|
+
DeprecationWarning,
|
|
883
|
+
stacklevel=2,
|
|
884
|
+
)
|
|
885
|
+
if path_or_stream is None:
|
|
886
|
+
return self.to_bytes()
|
|
887
|
+
if isinstance(path_or_stream, (str, PathLike)):
|
|
888
|
+
return self.save_to_path(path_or_stream)
|
|
889
|
+
return self.save_to_stream(path_or_stream)
|