licos-dev-sdk 0.4.2__tar.gz → 0.4.3__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.
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/DOCX.md +18 -5
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/PKG-INFO +1 -1
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/pyproject.toml +1 -1
- licos_dev_sdk-0.4.3/src/licos_dev_sdk/document_docx_preservation.py +27 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/src/licos_dev_sdk/document_docx_structure.py +20 -7
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/src/licos_dev_sdk/document_docx_table_actions.py +16 -3
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/tests/test_docx_structure.py +84 -0
- licos_dev_sdk-0.4.3/tests/test_html_docx_features.py +46 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/.gitignore +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/src/licos_dev_sdk/__init__.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/src/licos_dev_sdk/_utils.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/src/licos_dev_sdk/archive.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/src/licos_dev_sdk/chart.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/src/licos_dev_sdk/data.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/src/licos_dev_sdk/diagram.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/src/licos_dev_sdk/document.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/src/licos_dev_sdk/document_common.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/src/licos_dev_sdk/document_docx.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/src/licos_dev_sdk/document_docx_edit.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/src/licos_dev_sdk/document_docx_layout.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/src/licos_dev_sdk/document_docx_package.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/src/licos_dev_sdk/document_docx_table.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/src/licos_dev_sdk/document_read.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/src/licos_dev_sdk/document_table_export.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/src/licos_dev_sdk/image.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/src/licos_dev_sdk/model.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/src/licos_dev_sdk/observability.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/src/licos_dev_sdk/pdf_inspect.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/src/licos_dev_sdk/ppt_master_runtime.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/src/licos_dev_sdk/presentation.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/src/licos_dev_sdk/presentation_canvas.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/src/licos_dev_sdk/presentation_inspect.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/src/licos_dev_sdk/spreadsheet.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/src/licos_dev_sdk/spreadsheet_edit.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/src/licos_dev_sdk/spreadsheet_html.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/src/licos_dev_sdk/spreadsheet_inspect.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/src/licos_dev_sdk/web.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/tests/test_document_read.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/tests/test_document_spreadsheet.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/tests/test_document_table_export.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/tests/test_docx_table_edit.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/tests/test_model.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/tests/test_observability.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/tests/test_output_paths.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/tests/test_ppt_master_runtime.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/tests/test_presentation.py +0 -0
- {licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/tests/test_spreadsheet_html_edit.py +0 -0
|
@@ -1,20 +1,22 @@
|
|
|
1
|
-
# Word SDK / CLI 0.4.
|
|
1
|
+
# Word SDK / CLI 0.4.3
|
|
2
2
|
|
|
3
3
|
Cloud document execution stays in `licos-dev-sdk` and `licos-dev`. CLI commands
|
|
4
4
|
only parse arguments and call the SDK; no additional Agent-native Word tools are
|
|
5
5
|
registered. Existing `docx`, `docx-template`, `docx-read`, `docx-edit` commands and
|
|
6
6
|
their Python APIs retain their invocation contracts.
|
|
7
7
|
|
|
8
|
+
Use `licos-dev --version` to query the installed document CLI version.
|
|
9
|
+
|
|
8
10
|
| CLI command | SDK function | Input |
|
|
9
11
|
| --- | --- | --- |
|
|
10
|
-
| `docx-structure` | `read_docx_structure` | File, blocks/paragraphs, page |
|
|
12
|
+
| `docx-structure` | `read_docx_structure` | File, blocks/paragraphs/tables, page |
|
|
11
13
|
| `docx-target` | `read_docx_target` | File, exact target, text cursor |
|
|
12
14
|
| `docx-replace` | `replace_docx_text` | Source/output, revision, target, search/replacement |
|
|
13
15
|
| `docx-delete-blocks` | `delete_docx_blocks` | Source/output, revision, inclusive block range |
|
|
14
16
|
| `docx-edit-structure` | `edit_docx_structure` | Source/output, revision, small operations array |
|
|
15
17
|
| `docx-table-set` | `set_docx_cell` | Source/output, revision, table/row/column, text |
|
|
16
18
|
| `docx-table-insert-rows` | `insert_docx_rows` | Source/output, revision, table, insertion point, template row, rows |
|
|
17
|
-
| `docx-verify` | `verify_docx` | File, optional positional assertions |
|
|
19
|
+
| `docx-verify` | `verify_docx` | File, optional positional assertions and original document |
|
|
18
20
|
|
|
19
21
|
## Contract
|
|
20
22
|
|
|
@@ -28,7 +30,7 @@ their Python APIs retain their invocation contracts.
|
|
|
28
30
|
next batch uses the output and its returned revision.
|
|
29
31
|
- Batch structure operations contain only `replace_text` with `target/search/replace`
|
|
30
32
|
or `delete_blocks` with `from/to`. All targets refer to the input snapshot.
|
|
31
|
-
Invalid, missing
|
|
33
|
+
Invalid, missing or removed targets and ambiguous matches fail before publishing output.
|
|
32
34
|
- `docx-replace` expects one exact match within one paragraph in the selected
|
|
33
35
|
target. It supports text split across runs and preserves surrounding formatting.
|
|
34
36
|
`--replace-file` carries long replacement text; simple replacements need no JSON.
|
|
@@ -36,6 +38,9 @@ their Python APIs retain their invocation contracts.
|
|
|
36
38
|
`[["pump","set",2],["valve",null,3]]`. Row count is derived; each merged physical
|
|
37
39
|
cell is filled once. Row/cell/paragraph/run styles come from `--template-row`.
|
|
38
40
|
Templates that intersect vertical merges are rejected explicitly.
|
|
41
|
+
Choose exactly one of `--before N` (`before=N`) or `--append` (`append=True`).
|
|
42
|
+
Explicit append uses the current revision's row count. Table structure items
|
|
43
|
+
include `table`, `row_count`, `column_count` and a usable body block target.
|
|
39
44
|
- New editing commands default to refusing an existing output, including the
|
|
40
45
|
source itself. `--overwrite` explicitly allows replacement. Body edits are
|
|
41
46
|
atomic and keep all other ZIP part payloads unchanged. Existing `docx-edit`
|
|
@@ -43,9 +48,17 @@ their Python APIs retain their invocation contracts.
|
|
|
43
48
|
- Structure previews set `content_truncated`; `docx-target` retrieves exact text
|
|
44
49
|
without stripping whitespace and provides `next_text_offset` until `has_more=false`.
|
|
45
50
|
No partial JSON repair or automatic argument completion is performed.
|
|
46
|
-
- Verification without assertions
|
|
51
|
+
- Verification without assertions does not certify content: `content_verified=false`.
|
|
47
52
|
Positional assertions use `equals`, `contains` or `absent`. A mismatch produces
|
|
48
53
|
`verified=false` and CLI exit code 2. Layout and page breaks require visual review.
|
|
54
|
+
One CLI assertion can use a target with `--text` or `--text-file` and optional
|
|
55
|
+
`--mode`; use `--input` for multiple assertions, never both input forms.
|
|
56
|
+
- `verify_docx(..., original=...)` / `docx-verify --original` compares every ZIP
|
|
57
|
+
part payload except `word/document.xml`, plus ordered section settings in the
|
|
58
|
+
body. It reports changed/added/missing parts and `preservation_verified`; a
|
|
59
|
+
failed comparison also sets `verified=false` / exit 2. The original must be a
|
|
60
|
+
separate unmodified file. This does not certify all body run formatting or
|
|
61
|
+
rendered layout, and does not substitute for positional content assertions.
|
|
49
62
|
|
|
50
63
|
## Verification
|
|
51
64
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Compare untouched DOCX parts and section settings against an original file."""
|
|
2
|
+
|
|
3
|
+
from docx.oxml.ns import qn
|
|
4
|
+
from lxml import etree
|
|
5
|
+
|
|
6
|
+
from .document_docx_package import DOCUMENT_XML, DocxPackage
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def compare_docx_preservation(current: DocxPackage, original: DocxPackage) -> dict:
|
|
10
|
+
if current.path.samefile(original.path):
|
|
11
|
+
raise ValueError("original must be a separate, unmodified document")
|
|
12
|
+
before = {info.filename: data for info, data in original.parts if info.filename != DOCUMENT_XML}
|
|
13
|
+
after = {info.filename: data for info, data in current.parts if info.filename != DOCUMENT_XML}
|
|
14
|
+
missing = sorted(before.keys() - after.keys())
|
|
15
|
+
added = sorted(after.keys() - before.keys())
|
|
16
|
+
changed = sorted(name for name in before.keys() & after.keys() if before[name] != after[name])
|
|
17
|
+
|
|
18
|
+
def sections(package):
|
|
19
|
+
return [etree.tostring(node, method="c14n", exclusive=True)
|
|
20
|
+
for node in package.root.iter(qn("w:sectPr"))]
|
|
21
|
+
|
|
22
|
+
sections_unchanged = sections(current) == sections(original)
|
|
23
|
+
return {"original": str(original.path), "compared_parts": len(before),
|
|
24
|
+
"excluded_parts": [DOCUMENT_XML], "missing_parts": missing,
|
|
25
|
+
"added_parts": added, "changed_parts": changed,
|
|
26
|
+
"section_properties_unchanged": sections_unchanged,
|
|
27
|
+
"verified": not (missing or added or changed) and sections_unchanged}
|
|
@@ -10,6 +10,7 @@ from docx.oxml.ns import qn
|
|
|
10
10
|
from lxml import etree
|
|
11
11
|
|
|
12
12
|
from .document_docx_package import DocxPackage
|
|
13
|
+
from .document_docx_preservation import compare_docx_preservation
|
|
13
14
|
from .document_docx_table import parse_table
|
|
14
15
|
|
|
15
16
|
TEXT_TAGS = {qn("w:t"): None, qn("w:tab"): "\t", qn("w:br"): "\n", qn("w:cr"): "\n"}
|
|
@@ -69,16 +70,23 @@ def read_docx_structure(source: str | Path, *, scope: str = "blocks", offset: in
|
|
|
69
70
|
_index(offset, "offset")
|
|
70
71
|
_index(limit, "limit")
|
|
71
72
|
_index(max_chars, "max_chars")
|
|
72
|
-
if scope not in {"blocks", "paragraphs"}:
|
|
73
|
-
raise ValueError("scope must be blocks or
|
|
73
|
+
if scope not in {"blocks", "paragraphs", "tables"}:
|
|
74
|
+
raise ValueError("scope must be blocks, paragraphs or tables")
|
|
74
75
|
package = DocxPackage(source)
|
|
75
76
|
structure = _Structure(package.root)
|
|
76
|
-
elements = structure.blocks
|
|
77
|
+
elements = {"blocks": structure.blocks, "paragraphs": structure.paragraphs, "tables": structure.tables}[scope]
|
|
77
78
|
items = []
|
|
78
79
|
for number, element in enumerate(elements[offset - 1:offset - 1 + limit], offset):
|
|
79
80
|
text = _text(element)
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
item = {"target": {"paragraph" if scope == "paragraphs" else "block":
|
|
82
|
+
number if scope != "tables" else structure.blocks.index(element) + 1},
|
|
83
|
+
"type": etree.QName(element).localname,
|
|
84
|
+
"text": text[:max_chars], "content_truncated": len(text) > max_chars}
|
|
85
|
+
if element.tag == qn("w:tbl"):
|
|
86
|
+
model = parse_table(element, include_nested=False)
|
|
87
|
+
item.update(table=structure.tables.index(element) + 1, row_count=len(model.rows),
|
|
88
|
+
column_count=model.column_count)
|
|
89
|
+
items.append(item)
|
|
82
90
|
next_offset = offset + len(items)
|
|
83
91
|
return {"path": str(package.path), "revision": package.revision, "scope": scope,
|
|
84
92
|
"coordinate_systems": {"block": "top-level body blocks; a table is one block",
|
|
@@ -193,7 +201,8 @@ def delete_docx_blocks(source: str | Path, output: str | Path, *, revision: str,
|
|
|
193
201
|
operations=[{"op": "delete_blocks", "from": first, "to": last}])
|
|
194
202
|
|
|
195
203
|
|
|
196
|
-
def verify_docx(source: str | Path, *, expectations: list[dict] | None = None
|
|
204
|
+
def verify_docx(source: str | Path, *, expectations: list[dict] | None = None,
|
|
205
|
+
original: str | Path | None = None) -> dict:
|
|
197
206
|
"""Check package structure and exact positions; absent assertions do not certify content."""
|
|
198
207
|
package = DocxPackage(source)
|
|
199
208
|
structure = _Structure(package.root)
|
|
@@ -214,6 +223,10 @@ def verify_docx(source: str | Path, *, expectations: list[dict] | None = None) -
|
|
|
214
223
|
if not passed:
|
|
215
224
|
mismatches.append({"index": index, "target": expectation["target"], "mode": mode, "expected": expected,
|
|
216
225
|
"actual": actual[:1000], "actual_truncated": len(actual) > 1000})
|
|
226
|
+
preservation = compare_docx_preservation(package, DocxPackage(original)) if original is not None else None
|
|
217
227
|
return {"path": str(package.path), "revision": package.revision, "structure_verified": True,
|
|
218
|
-
"content_verified": bool(expectations) and not mismatches,
|
|
228
|
+
"content_verified": bool(expectations) and not mismatches,
|
|
229
|
+
"preservation_verified": preservation is not None and preservation["verified"],
|
|
230
|
+
"preservation": preservation,
|
|
231
|
+
"verified": not mismatches and (preservation is None or preservation["verified"]),
|
|
219
232
|
"expectation_count": len(expectations), "mismatches": mismatches}
|
{licos_dev_sdk-0.4.2 → licos_dev_sdk-0.4.3}/src/licos_dev_sdk/document_docx_table_actions.py
RENAMED
|
@@ -5,6 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
from .document_docx_edit import _apply_operation
|
|
6
6
|
from .document_docx_package import DocxPackage
|
|
7
7
|
from .document_docx_structure import _Structure, _index
|
|
8
|
+
from .document_docx_table import parse_table
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def set_docx_cell(source: str | Path, output: str | Path, *, revision: str,
|
|
@@ -19,15 +20,27 @@ def set_docx_cell(source: str | Path, output: str | Path, *, revision: str,
|
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
def insert_docx_rows(source: str | Path, output: str | Path, *, revision: str,
|
|
22
|
-
table: int,
|
|
23
|
+
table: int, rows: list[list], template_row: int,
|
|
24
|
+
before: int | None = None, append: bool = False,
|
|
23
25
|
overwrite: bool = False) -> dict:
|
|
24
|
-
"""Clone
|
|
25
|
-
|
|
26
|
+
"""Clone styled rows at an explicit position or append to the current table."""
|
|
27
|
+
if type(append) is not bool:
|
|
28
|
+
raise TypeError("append must be a boolean")
|
|
29
|
+
if (before is not None) == append:
|
|
30
|
+
raise ValueError("provide exactly one of before and append=True")
|
|
31
|
+
for name, value in (("table", table), ("template_row", template_row)):
|
|
26
32
|
_index(value, name)
|
|
33
|
+
if before is not None:
|
|
34
|
+
_index(before, "before")
|
|
27
35
|
if not isinstance(rows, list) or not 1 <= len(rows) <= 200:
|
|
28
36
|
raise ValueError("rows must contain 1-200 row arrays")
|
|
29
37
|
package = DocxPackage(source)
|
|
30
38
|
package.require_revision(revision)
|
|
39
|
+
if append:
|
|
40
|
+
tables = _Structure(package.root).tables
|
|
41
|
+
if table > len(tables):
|
|
42
|
+
raise ValueError(f"table {table} does not exist")
|
|
43
|
+
before = len(parse_table(tables[table - 1], include_nested=False).rows) + 1
|
|
31
44
|
_apply_operation(package.root, {"op": "insert_rows", "table": table, "before": before,
|
|
32
45
|
"template_row": template_row, "count": len(rows), "rows": rows})
|
|
33
46
|
return {**package.save(output, overwrite=overwrite), "inserted_rows": len(rows)}
|
|
@@ -202,3 +202,87 @@ def test_simple_cell_set_uses_revision_and_preserves_style(source, tmp_path):
|
|
|
202
202
|
set_docx_cell(source, output, revision=rev(source), table=1, row=2, column=1, text="新值")
|
|
203
203
|
assert Document(output).tables[0].cell(1, 0).text == "新值"
|
|
204
204
|
assert Document(output).tables[0].cell(1, 0).paragraphs[0].runs[0].bold
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_table_discovery_returns_body_target_and_current_dimensions(source, tmp_path):
|
|
208
|
+
doc = Document(source)
|
|
209
|
+
doc.add_table(rows=3, cols=2)
|
|
210
|
+
doc.save(source)
|
|
211
|
+
first = read_docx_structure(source, scope="tables", limit=1)
|
|
212
|
+
item = first["items"][0]
|
|
213
|
+
assert (item["target"], item["table"], item["row_count"], item["column_count"]) == ({"block": 3}, 1, 2, 3)
|
|
214
|
+
assert read_docx_structure(source)["items"][2] == item
|
|
215
|
+
second = read_docx_structure(source, scope="tables", offset=first["next_offset"], limit=1)
|
|
216
|
+
item = second["items"][0]
|
|
217
|
+
assert (item["target"], item["table"], item["row_count"], item["column_count"]) == ({"block": 6}, 2, 3, 2)
|
|
218
|
+
assert not second["has_more"]
|
|
219
|
+
assert read_docx_target(source, item["target"])["text"] == item["text"]
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def test_explicit_append_uses_current_revision_and_preserves_merged_template(source, tmp_path):
|
|
223
|
+
first, second = tmp_path / "first.docx", tmp_path / "second.docx"
|
|
224
|
+
insert_docx_rows(source, first, revision=rev(source), table=1, append=True, template_row=2, rows=[["泵", "台", 2]])
|
|
225
|
+
insert_docx_rows(first, second, revision=rev(first), table=1, append=True, template_row=1, rows=[["合并尾行", 9]])
|
|
226
|
+
table = Document(second).tables[0]
|
|
227
|
+
assert len(table.rows) == 4
|
|
228
|
+
assert table.cell(2, 0).text == "泵" and table.cell(2, 0).paragraphs[0].runs[0].bold
|
|
229
|
+
assert table.cell(3, 0).text == table.cell(3, 1).text == "合并尾行"
|
|
230
|
+
assert_parts_preserved(source, second)
|
|
231
|
+
with pytest.raises(ValueError, match="revision"):
|
|
232
|
+
insert_docx_rows(first, tmp_path / "stale.docx", revision=rev(source), table=1, append=True, template_row=2, rows=[["a", "b", 1]])
|
|
233
|
+
assert not (tmp_path / "stale.docx").exists()
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@pytest.mark.parametrize("position", [{}, {"before": 3, "append": True}, {"before": True}, {"append": 1}])
|
|
237
|
+
def test_insert_requires_one_explicit_position(source, tmp_path, position):
|
|
238
|
+
output = tmp_path / "bad.docx"
|
|
239
|
+
with pytest.raises((ValueError, TypeError)):
|
|
240
|
+
insert_docx_rows(source, output, revision=rev(source), table=1, template_row=2, rows=[["a", "b", 1]], **position)
|
|
241
|
+
assert not output.exists()
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def test_original_comparison_certifies_parts_and_sections_but_requires_content_assertions(source, tmp_path):
|
|
245
|
+
output = tmp_path / "edited.docx"
|
|
246
|
+
replace_docx_text(source, output, revision=rev(source), target={"block": 2}, search="123456", replacement="新账号")
|
|
247
|
+
report = verify_docx(output, original=source)
|
|
248
|
+
assert report["verified"] and report["preservation_verified"]
|
|
249
|
+
assert report["preservation"]["section_properties_unchanged"]
|
|
250
|
+
assert not report["content_verified"]
|
|
251
|
+
assert not verify_docx(output, original=source, expectations=[{"target": {"block": 2}, "text": "错误"}])["verified"]
|
|
252
|
+
with pytest.raises(ValueError, match="separate"):
|
|
253
|
+
verify_docx(output, original=output)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@pytest.mark.parametrize("mutation,part,field", [
|
|
257
|
+
("change", "word/header1.xml", "changed_parts"),
|
|
258
|
+
("change", "word/styles.xml", "changed_parts"),
|
|
259
|
+
("remove", "word/header1.xml", "missing_parts"),
|
|
260
|
+
("add", "word/media/extra.png", "added_parts"),
|
|
261
|
+
])
|
|
262
|
+
def test_original_comparison_detects_changes_outside_body(source, tmp_path, mutation, part, field):
|
|
263
|
+
output = tmp_path / "changed.docx"
|
|
264
|
+
with ZipFile(source) as before, ZipFile(output, "w") as after:
|
|
265
|
+
for info in before.infolist():
|
|
266
|
+
if mutation == "remove" and info.filename == part:
|
|
267
|
+
continue
|
|
268
|
+
data = before.read(info.filename)
|
|
269
|
+
after.writestr(info, data + b" " if mutation == "change" and info.filename == part else data)
|
|
270
|
+
if mutation == "add":
|
|
271
|
+
after.writestr(part, b"image")
|
|
272
|
+
report = verify_docx(output, original=source)
|
|
273
|
+
assert not report["verified"] and not report["preservation_verified"]
|
|
274
|
+
assert report["preservation"][field] == [part]
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def test_original_comparison_detects_section_layout_changes(source, tmp_path):
|
|
278
|
+
from licos_dev_sdk.document_docx_package import DocxPackage
|
|
279
|
+
|
|
280
|
+
package = DocxPackage(source)
|
|
281
|
+
section = next(package.root.iter(qn("w:sectPr")))
|
|
282
|
+
section.find(qn("w:pgMar")).set(qn("w:left"), "999")
|
|
283
|
+
output = tmp_path / "layout.docx"
|
|
284
|
+
package.save(output)
|
|
285
|
+
report = verify_docx(output, original=source)
|
|
286
|
+
assert not report["verified"]
|
|
287
|
+
assert not report["preservation"]["section_properties_unchanged"]
|
|
288
|
+
assert report["preservation"]["changed_parts"] == []
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Exercise the public HTML-to-Word API with actual document parts and images."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
from zipfile import ZipFile
|
|
5
|
+
|
|
6
|
+
from docx import Document
|
|
7
|
+
from docx.oxml.ns import qn
|
|
8
|
+
from PIL import Image
|
|
9
|
+
|
|
10
|
+
from licos_dev_sdk import create_docx
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_html_generates_native_toc_merged_table_and_embedded_images(tmp_path):
|
|
14
|
+
picture = tmp_path / "figure.png"
|
|
15
|
+
Image.new("RGB", (80, 40), "#145DA0").save(picture)
|
|
16
|
+
encoded = base64.b64encode(picture.read_bytes()).decode("ascii")
|
|
17
|
+
html = f'''<html><head><style>
|
|
18
|
+
th {{ background-color: #145DA0; color: #ffffff; }}
|
|
19
|
+
td {{ border: 1px solid #333333; }}
|
|
20
|
+
</style></head><body>
|
|
21
|
+
<h1>测试报告</h1><h2>设备统计</h2><h3>设备清单</h3>
|
|
22
|
+
<table><tr><th colspan="2">设备</th><th>数量</th></tr>
|
|
23
|
+
<tr><td rowspan="2">泵组</td><td>离心泵</td><td>2</td></tr>
|
|
24
|
+
<tr><td>阀门</td><td>3</td></tr></table>
|
|
25
|
+
<p><img src="figure.png" width="160" alt="本地图片"></p>
|
|
26
|
+
<p><img src="data:image/png;base64,{encoded}" width="160" alt="内嵌图片"></p>
|
|
27
|
+
</body></html>'''
|
|
28
|
+
output = create_docx(html, "report", content_type="html", output_dir=str(tmp_path),
|
|
29
|
+
asset_base_path=str(tmp_path), include_toc=True, header="内部资料", footer="测试页脚")
|
|
30
|
+
document = Document(output)
|
|
31
|
+
assert [p.text for p in document.paragraphs if p.style.name.startswith("Heading")] == ["测试报告", "设备统计", "设备清单"]
|
|
32
|
+
fields = [n.text for n in document.element.iter(qn("w:instrText"))]
|
|
33
|
+
assert any('TOC \\o "1-3"' in text for text in fields)
|
|
34
|
+
assert document.settings.element.find(qn("w:updateFields")).get(qn("w:val")) == "true"
|
|
35
|
+
table = document.tables[0]
|
|
36
|
+
assert len(table.rows) == 3 and len(table.columns) == 3
|
|
37
|
+
assert table.cell(0, 0)._tc is table.cell(0, 1)._tc
|
|
38
|
+
assert table.cell(1, 0)._tc is table.cell(2, 0)._tc
|
|
39
|
+
assert table.cell(2, 1).text == "阀门" and table.cell(2, 2).text == "3"
|
|
40
|
+
assert 'w:fill="145DA0"' in table.cell(0, 0)._tc.xml
|
|
41
|
+
assert len(document.inline_shapes) == 2
|
|
42
|
+
assert "内部资料" in document.sections[0].header.paragraphs[0].text
|
|
43
|
+
assert "测试页脚" in document.sections[0].footer.paragraphs[0].text
|
|
44
|
+
with ZipFile(output) as archive:
|
|
45
|
+
images = [archive.read(name) for name in archive.namelist() if name.startswith("word/media/")]
|
|
46
|
+
assert picture.read_bytes() in images
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|