From 0498ae98db6a76f3c57cbbd22713ad5999144f38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B3=A0=EA=B7=9C=ED=98=84?= <38392618+airmang@users.noreply.github.com> Date: Thu, 19 Feb 2026 00:20:13 +0900 Subject: [PATCH] =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BB=A4?= =?UTF-8?q?=EB=B2=84=EB=A6=AC=EC=A7=80=20=EB=8F=84=EC=9E=85=20=EB=B0=8F=20?= =?UTF-8?q?=ED=95=B5=EC=8B=AC=20=EB=AA=A8=EB=93=88=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/tests.yml | 5 ++- pyproject.toml | 1 + src/hwpx/oxml/document.py | 15 +++++-- tests/test_coverage_targets.py | 74 ++++++++++++++++++++++++++++++++++ 4 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 tests/test_coverage_targets.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fa709f7..12f65c8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -35,8 +35,11 @@ jobs: run: pyright - name: Run tests and keep summary log + env: + # 단계적 도입: 기준을 50 -> 60 -> 70으로 상향합니다. + COVERAGE_FAIL_UNDER: 50 run: | - pytest -v -ra 2>&1 | tee pytest-summary.log + pytest -v -ra --cov=hwpx --cov-report=term-missing --cov-fail-under=${COVERAGE_FAIL_UNDER} 2>&1 | tee pytest-summary.log - name: Upload pytest summary log if: always() diff --git a/pyproject.toml b/pyproject.toml index 76446c5..d2746fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ dev = [ ] test = [ "pytest>=7.4", + "pytest-cov>=5.0", ] typecheck = [ "mypy>=1.10", diff --git a/src/hwpx/oxml/document.py b/src/hwpx/oxml/document.py index 6d940f5..46f2709 100644 --- a/src/hwpx/oxml/document.py +++ b/src/hwpx/oxml/document.py @@ -2183,7 +2183,9 @@ def _create_run_for_object( default_char = self.char_pr_id_ref or "0" if default_char is not None: attrs["charPrIDRef"] = str(default_char) - return ET.SubElement(self.element, f"{_HP}run", attrs) + run = self.element.makeelement(f"{_HP}run", attrs) + self.element.append(run) + return run def add_run( self, @@ -2264,6 +2266,9 @@ def add_table( height=height, border_fill_id_ref=resolved_border_fill, ) + if type(table_element) is not type(run): + table_element = LET.fromstring(ET.tostring(table_element, encoding="utf-8")) + run.append(table_element) self.section.mark_dirty() return HwpxOxmlTable(table_element, self) @@ -2585,7 +2590,7 @@ def add_paragraph( if style_id_ref is not None: attrs["styleIDRef"] = str(style_id_ref) - paragraph = ET.Element(f"{_HP}p", attrs) + paragraph = self._element.makeelement(f"{_HP}p", attrs) if include_run: run_attrs = dict(run_attributes or {}) @@ -2594,9 +2599,11 @@ def add_paragraph( elif "charPrIDRef" not in run_attrs: run_attrs["charPrIDRef"] = "0" - run = ET.SubElement(paragraph, f"{_HP}run", run_attrs) - text_element = ET.SubElement(run, f"{_HP}t") + run = paragraph.makeelement(f"{_HP}run", run_attrs) + paragraph.append(run) + text_element = run.makeelement(f"{_HP}t", {}) text_element.text = text + run.append(text_element) self._element.append(paragraph) self._dirty = True diff --git a/tests/test_coverage_targets.py b/tests/test_coverage_targets.py new file mode 100644 index 0000000..69c743a --- /dev/null +++ b/tests/test_coverage_targets.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +import importlib +import logging +import sys +import warnings +import xml.etree.ElementTree as ET +from typing import cast + +from hwpx.document import HwpxDocument, _append_element +from hwpx.oxml.document import HwpxOxmlDocument, HwpxOxmlSection +from hwpx.opc.package import HwpxPackage + + +class _NoopResource: + pass + + +class _BrokenResource: + def flush(self) -> None: + raise RuntimeError("flush error") + + def close(self) -> None: + raise RuntimeError("close error") + + +def _minimal_document() -> HwpxDocument: + section = HwpxOxmlSection("section0.xml", ET.Element("section")) + root = HwpxOxmlDocument(ET.Element("manifest"), [section], []) + return HwpxDocument(cast(HwpxPackage, object()), root) + + +def test_append_element_uses_same_element_type() -> None: + parent = ET.Element("parent") + child = _append_element(parent, "child", {"id": "42"}) + + assert child.tag == "child" + assert child.attrib["id"] == "42" + assert parent[0] is child + + +def test_flush_and_close_resource_are_noop_without_method() -> None: + HwpxDocument._flush_resource(_NoopResource()) + HwpxDocument._close_resource(_NoopResource()) + + +def test_flush_and_close_resource_swallow_exceptions(caplog) -> None: + caplog.set_level(logging.DEBUG) + resource = _BrokenResource() + + HwpxDocument._flush_resource(resource) + HwpxDocument._close_resource(resource) + + assert "자원 flush 중 예외를 무시합니다" in caplog.text + assert "자원 close 중 예외를 무시합니다" in caplog.text + + +def test_package_module_warns_on_import_and_exports_symbols() -> None: + module_name = "hwpx.package" + sys.modules.pop(module_name, None) + + with warnings.catch_warnings(record=True) as records: + warnings.simplefilter("always") + module = importlib.import_module(module_name) + + assert records + assert any("더 이상 권장되지 않습니다" in str(record.message) for record in records) + assert module.__all__ == [ + "HwpxPackage", + "HwpxPackageError", + "HwpxStructureError", + "RootFile", + "VersionInfo", + ]