Skip to content

Commit 566c606

Browse files
committed
feat: add Word COM automation support for document handling and updates
1 parent 7012f39 commit 566c606

File tree

3 files changed

+67
-340
lines changed

3 files changed

+67
-340
lines changed

src/paradoc/io/word/exporter.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def __init__(self, one_doc: OneDoc, main_tmpl=MY_DOCX_TMPL, app_tmpl=MY_DOCX_TMP
2424
self.main_tmpl = main_tmpl
2525
self.app_tmpl = app_tmpl
2626
self.use_custom_docx_compile = kwargs.get("use_custom_docx_compile", True)
27+
self.enable_word_com_automation = kwargs.get("enable_word_com_automation", False)
2728

2829
def export(self, output_name, dest_file, check_open_docs=False):
2930
if self.use_custom_docx_compile:
@@ -68,13 +69,16 @@ def _compile_individual_md_files_to_docx(self, output_name, dest_file, check_ope
6869
fix_headers_after_compose(composer_main.doc)
6970

7071
print("Close Existing Word documents")
71-
if check_open_docs:
72+
if check_open_docs and self.enable_word_com_automation:
7273
close_word_docs_by_name([output_name, f"{output_name}.docx"])
7374

7475
print(f'Saving Composed Document to "{dest_file}"')
7576
composer_main.save(dest_file)
7677

77-
docx_update(str(dest_file))
78+
# Only attempt Word COM automation if explicitly enabled
79+
# This is disabled by default to avoid fatal COM errors in test/CI environments
80+
if self.enable_word_com_automation:
81+
docx_update(str(dest_file))
7882

7983
def format_tables(self, composer_doc: Document, is_appendix):
8084
for i, docx_tbl in enumerate(self.get_all_tables(composer_doc)):

src/paradoc/io/word/utils.py

Lines changed: 61 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -28,67 +28,101 @@ def open_word_win32():
2828
import sys
2929

3030
if sys.platform != "win32":
31-
return
31+
return None
3232

3333
try:
3434
import win32com.client
3535

36+
# Use early binding to avoid some COM issues
3637
word = win32com.client.DispatchEx("Word.Application")
38+
word.Visible = False
39+
return word
3740
except (ModuleNotFoundError, ImportError):
38-
logger.error(
39-
"Ensure you have you have win32com installed. "
40-
'Use "conda install -c conda-forge pywin32" to install. '
41-
f"{traceback.format_exc()}"
41+
logger.warning(
42+
"win32com not available - Word COM automation will be skipped. "
43+
'Install with "conda install -c conda-forge pywin32" if needed.'
4244
)
4345
return None
44-
except BaseException as e:
45-
logger.error(
46-
"Probably unable to find COM connection to Word application. "
47-
f"Is Word installed? {traceback.format_exc()}, {e}"
46+
except Exception as e:
47+
logger.warning(
48+
f"Unable to start Word COM automation - will be skipped. Error: {e}"
4849
)
4950
return None
50-
return word
5151

5252

5353
def docx_update(docx_file):
54+
"""
55+
Update fields and table of contents in a .docx file using Word COM automation.
56+
This is optional - if it fails, the document will still be saved correctly,
57+
just without automatically updated field numbers.
58+
"""
5459
word = open_word_win32()
5560
if word is None:
61+
logger.debug("Skipping Word COM update - automation not available")
5662
return
57-
doc = word.Documents.Open(docx_file)
63+
64+
doc = None
5865
try:
66+
# Convert to absolute path for COM
67+
abs_path = str(pathlib.Path(docx_file).absolute())
68+
doc = word.Documents.Open(abs_path, ReadOnly=False)
69+
5970
# update all figure / table numbers
6071
word.ActiveDocument.Fields.Update()
6172

6273
# update Table of content / figure / table
6374
if len(word.ActiveDocument.TablesOfContents) > 0:
6475
word.ActiveDocument.TablesOfContents(1).Update()
6576
else:
66-
logger.error("No table of contents is found")
67-
# word.ActiveDocument.TablesOfFigures(1).Update()
68-
# word.ActiveDocument.TablesOfFigures(2).Update()
77+
logger.debug("No table of contents found in document")
78+
6979
except Exception as e:
70-
raise Exception(e)
80+
logger.warning(f"Failed to update document via Word COM (non-critical): {e}")
7181
finally:
72-
doc.Close(SaveChanges=True)
82+
# Clean up in reverse order
83+
if doc is not None:
84+
try:
85+
doc.Close(SaveChanges=True)
86+
except Exception as e:
87+
logger.warning(f"Failed to close document: {e}")
7388

74-
word.Quit()
89+
if word is not None:
90+
try:
91+
word.Quit()
92+
except Exception as e:
93+
logger.warning(f"Failed to quit Word application: {e}")
7594

7695

7796
def close_word_docs_by_name(names: list) -> None:
97+
"""
98+
Close Word documents by name using COM automation.
99+
This is optional - if it fails, documents will remain open which is non-critical.
100+
"""
78101
word = open_word_win32()
79102
if word is None:
103+
logger.debug("Skipping Word document close - COM automation not available")
80104
return
81105

82-
if len(word.Documents) > 0:
83-
for doc in word.Documents:
84-
doc_name = doc.Name
85-
if doc_name in names:
86-
print(f'Closing "{doc}"')
87-
doc.Close()
88-
else:
89-
print(f"No Word docs named {names} found to be open. Ending Word Application COM session")
90-
91-
word.Quit()
106+
try:
107+
if len(word.Documents) > 0:
108+
for doc in word.Documents:
109+
try:
110+
doc_name = doc.Name
111+
if doc_name in names:
112+
logger.info(f'Closing "{doc_name}"')
113+
doc.Close()
114+
except Exception as e:
115+
logger.warning(f"Failed to close document: {e}")
116+
else:
117+
logger.debug(f"No Word docs named {names} found to be open")
118+
except Exception as e:
119+
logger.warning(f"Failed to access Word documents: {e}")
120+
finally:
121+
if word is not None:
122+
try:
123+
word.Quit()
124+
except Exception as e:
125+
logger.warning(f"Failed to quit Word application: {e}")
92126

93127

94128
def iter_block_items(parent):

0 commit comments

Comments
 (0)