Skip to content

Commit 6e3bc4c

Browse files
author
CoderWanFeng
committed
Merge branch 'main' into develop
2 parents 67cc1cc + 26824d4 commit 6e3bc4c

4 files changed

Lines changed: 218 additions & 4 deletions

File tree

office/api/ppt.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,17 @@
1515
https://www.python-office.com
1616
"""
1717

18-
# 导入处理PPT的模块
19-
import poppt
18+
def _load_poppt():
19+
try:
20+
import poppt
21+
except ModuleNotFoundError as exc:
22+
if exc.name != "poppt":
23+
raise
24+
raise ModuleNotFoundError(
25+
"PPT处理功能依赖 poppt,该功能仅支持安装了 Microsoft PowerPoint "
26+
"和 poppt 的 Windows 环境。"
27+
) from exc
28+
return poppt
2029

2130

2231
def ppt2pdf(path: str, output_path=r'./'):
@@ -31,6 +40,7 @@ def ppt2pdf(path: str, output_path=r'./'):
3140
Returns:
3241
None
3342
"""
43+
poppt = _load_poppt()
3444
poppt.ppt2pdf(path=path, output_path=output_path)
3545

3646

@@ -47,6 +57,7 @@ def ppt2img(input_path: str, output_path=r'./', merge: bool = False):
4757
Returns:
4858
None
4959
"""
60+
poppt = _load_poppt()
5061
poppt.ppt2img(input_path=input_path, output_path=output_path, merge=merge)
5162

5263

@@ -63,4 +74,5 @@ def merge4ppt(input_path: str, output_path=r'./', output_name: str = 'merge4ppt.
6374
Returns:
6475
None
6576
"""
77+
poppt = _load_poppt()
6678
poppt.merge4ppt(input_path=input_path, output_path=output_path, output_name=output_name)

office/api/wechat.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,18 @@
1616
"""
1717

1818

19-
import PyOfficeRobot
19+
def _load_py_office_robot():
20+
try:
21+
import PyOfficeRobot
22+
except ModuleNotFoundError as exc:
23+
if exc.name != "PyOfficeRobot":
24+
raise
25+
raise ModuleNotFoundError(
26+
"微信自动化功能依赖 PyOfficeRobot,该功能仅支持安装了桌面微信 "
27+
"和 PyOfficeRobot 的 Windows 环境。"
28+
) from exc
29+
return PyOfficeRobot
30+
2031

2132
def send_message(who: str, message: str):
2233
"""Send message to specified contact.
@@ -30,6 +41,7 @@ def send_message(who: str, message: str):
3041
Returns:
3142
None
3243
"""
44+
PyOfficeRobot = _load_py_office_robot()
3345
PyOfficeRobot.chat.send_message(who=who, message=message)
3446

3547

@@ -46,6 +58,7 @@ def send_message_by_time(who, message, time):
4658
Returns:
4759
None
4860
"""
61+
PyOfficeRobot = _load_py_office_robot()
4962
PyOfficeRobot.chat.send_message_by_time(who=who, message=message, time=time)
5063

5164

@@ -61,6 +74,7 @@ def chat_by_keywords(who, keywords):
6174
Returns:
6275
None
6376
"""
77+
PyOfficeRobot = _load_py_office_robot()
6478
PyOfficeRobot.chat.chat_by_keywords(who=who, keywords=keywords)
6579

6680

@@ -76,6 +90,7 @@ def send_file(who, file):
7690
Returns:
7791
None
7892
"""
93+
PyOfficeRobot = _load_py_office_robot()
7994
PyOfficeRobot.file.send_file(who=who, file=file)
8095

8196

@@ -87,6 +102,7 @@ def group_send():
87102
Returns:
88103
None
89104
"""
105+
PyOfficeRobot = _load_py_office_robot()
90106
PyOfficeRobot.group.send()
91107

92108

@@ -109,6 +125,7 @@ def receive_message(who='文件传输助手', txt='userMessage.txt', output_path
109125
Returns:
110126
None: function result is saving messages to specified file and path / 函数的执行结果是将消息保存到指定的文件和路径中
111127
"""
128+
PyOfficeRobot = _load_py_office_robot()
112129
PyOfficeRobot.chat.receive_message(who=who, txt=txt, output_path=output_path)
113130

114131

@@ -123,4 +140,5 @@ def chat_robot(who='程序员晚枫'):
123140
Returns:
124141
None
125142
"""
143+
PyOfficeRobot = _load_py_office_robot()
126144
PyOfficeRobot.chat.chat_robot(who=who)

office/api/word.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,17 @@
1515
https://www.python-office.com
1616
"""
1717

18-
import poword
18+
def _load_poword():
19+
try:
20+
import poword
21+
except ModuleNotFoundError as exc:
22+
if exc.name != "poword":
23+
raise
24+
raise ModuleNotFoundError(
25+
"Word处理功能依赖 poword,该功能仅支持安装了 Microsoft Word "
26+
"和 poword 的 Windows 环境。"
27+
) from exc
28+
return poword
1929

2030

2131
def docx2pdf(path: str, output_path: str = None):
@@ -32,6 +42,7 @@ def docx2pdf(path: str, output_path: str = None):
3242
"""
3343
if output_path is None:
3444
output_path = path
45+
poword = _load_poword()
3546
poword.docx2pdf(path=path, output_path=output_path)
3647

3748
def merge4docx(input_path: str, output_path: str, new_word_name: str = 'merge4docx'):
@@ -47,6 +58,7 @@ def merge4docx(input_path: str, output_path: str, new_word_name: str = 'merge4do
4758
Returns:
4859
None
4960
"""
61+
poword = _load_poword()
5062
poword.merge4docx(input_path=input_path, output_path=output_path, new_word_name=new_word_name)
5163

5264

@@ -63,6 +75,7 @@ def doc2docx(input_path: str, output_path: str = r'./', output_name: str = None)
6375
Returns:
6476
None
6577
"""
78+
poword = _load_poword()
6679
poword.doc2docx(input_path=input_path, output_path=output_path, output_name=output_name)
6780

6881

@@ -79,6 +92,7 @@ def docx2doc(input_path: str, output_path: str = r'./', output_name: str = None)
7992
Returns:
8093
None
8194
"""
95+
poword = _load_poword()
8296
poword.docx2doc(input_path=input_path, output_path=output_path, output_name=output_name)
8397

8498
def docx4imgs(word_path, img_path):
@@ -93,4 +107,5 @@ def docx4imgs(word_path, img_path):
93107
Returns:
94108
None
95109
"""
110+
poword = _load_poword()
96111
poword.docx4imgs(word_path=word_path, img_path=img_path)
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
"""Tests for optional platform-specific imports."""
2+
3+
from contextlib import contextmanager
4+
import importlib
5+
import os
6+
import sys
7+
import tempfile
8+
import types
9+
import unittest
10+
from unittest import mock
11+
12+
13+
SUPPORTED_DEPENDENCIES = [
14+
"poexcel",
15+
"popdf",
16+
"poimage",
17+
"pofile",
18+
"povideo",
19+
"poocr",
20+
"pomarkdown",
21+
"wftools",
22+
"pospider",
23+
]
24+
25+
WINDOWS_ONLY_DEPENDENCIES = [
26+
"poppt",
27+
"poword",
28+
"PyOfficeRobot",
29+
]
30+
31+
MANAGED_MODULE_PREFIXES = (
32+
"office",
33+
"pocode",
34+
)
35+
36+
WINDOWS_ONLY_LOADERS = [
37+
(
38+
"office.api.word",
39+
"_load_poword",
40+
"poword",
41+
"win32com",
42+
"Word处理功能依赖 poword",
43+
),
44+
(
45+
"office.api.ppt",
46+
"_load_poppt",
47+
"poppt",
48+
"comtypes",
49+
"PPT处理功能依赖 poppt",
50+
),
51+
(
52+
"office.api.wechat",
53+
"_load_py_office_robot",
54+
"PyOfficeRobot",
55+
"uiautomation",
56+
"微信自动化功能依赖 PyOfficeRobot",
57+
),
58+
]
59+
60+
61+
def _is_managed_module(name):
62+
return any(name == prefix or name.startswith(f"{prefix}.") for prefix in MANAGED_MODULE_PREFIXES)
63+
64+
65+
class TestOptionalImports(unittest.TestCase):
66+
"""Check that Windows-only dependencies do not break package imports."""
67+
68+
@contextmanager
69+
def _optional_import_test_environment(self):
70+
module_names = (
71+
SUPPORTED_DEPENDENCIES
72+
+ WINDOWS_ONLY_DEPENDENCIES
73+
+ ["pocode", "pocode.api", "pocode.api.color"]
74+
)
75+
original_modules = {
76+
name: sys.modules.get(name)
77+
for name in list(sys.modules)
78+
if name in module_names or _is_managed_module(name)
79+
}
80+
original_home = os.environ.get("HOME")
81+
82+
try:
83+
with tempfile.TemporaryDirectory() as temp_home:
84+
mark_dir = os.path.join(temp_home, ".python-office")
85+
os.makedirs(mark_dir)
86+
with open(os.path.join(mark_dir, "first_run_mark"), "w", encoding="utf-8") as mark_file:
87+
mark_file.write("already checked")
88+
89+
os.environ["HOME"] = temp_home
90+
91+
for name in list(sys.modules):
92+
if _is_managed_module(name):
93+
sys.modules.pop(name, None)
94+
95+
for name in SUPPORTED_DEPENDENCIES:
96+
sys.modules[name] = types.ModuleType(name)
97+
98+
sys.modules["pocode"] = types.ModuleType("pocode")
99+
sys.modules["pocode.api"] = types.ModuleType("pocode.api")
100+
color_module = types.ModuleType("pocode.api.color")
101+
color_module.random_color_print = lambda *args, **kwargs: None
102+
sys.modules["pocode.api.color"] = color_module
103+
104+
for name in WINDOWS_ONLY_DEPENDENCIES:
105+
sys.modules.pop(name, None)
106+
107+
yield
108+
finally:
109+
if original_home is None:
110+
os.environ.pop("HOME", None)
111+
else:
112+
os.environ["HOME"] = original_home
113+
114+
for name in list(sys.modules):
115+
if name in module_names or _is_managed_module(name):
116+
sys.modules.pop(name, None)
117+
118+
for name, module in original_modules.items():
119+
if module is not None:
120+
sys.modules[name] = module
121+
122+
def test_import_office_without_windows_only_dependencies(self):
123+
with self._optional_import_test_environment():
124+
importlib.import_module("office")
125+
126+
def test_loader_preserves_dependency_internal_import_errors(self):
127+
with self._optional_import_test_environment():
128+
importlib.import_module("office")
129+
original_import = __import__
130+
131+
for module_name, loader_name, dependency_name, internal_name, message in WINDOWS_ONLY_LOADERS:
132+
with self.subTest(dependency_name=dependency_name):
133+
api_module = importlib.import_module(module_name)
134+
loader = getattr(api_module, loader_name)
135+
136+
def import_with_missing_internal_dependency(
137+
name, globals=None, locals=None, fromlist=(), level=0
138+
):
139+
if name == dependency_name:
140+
raise ModuleNotFoundError(
141+
f"No module named '{internal_name}'",
142+
name=internal_name,
143+
)
144+
return original_import(name, globals, locals, fromlist, level)
145+
146+
with mock.patch("builtins.__import__", side_effect=import_with_missing_internal_dependency):
147+
with self.assertRaises(ModuleNotFoundError) as error:
148+
loader()
149+
150+
self.assertEqual(internal_name, error.exception.name)
151+
self.assertNotIn(message, str(error.exception))
152+
153+
def test_loader_reports_missing_windows_only_dependency(self):
154+
with self._optional_import_test_environment():
155+
importlib.import_module("office")
156+
157+
for module_name, loader_name, dependency_name, _, message in WINDOWS_ONLY_LOADERS:
158+
with self.subTest(dependency_name=dependency_name):
159+
api_module = importlib.import_module(module_name)
160+
loader = getattr(api_module, loader_name)
161+
162+
with self.assertRaises(ModuleNotFoundError) as error:
163+
loader()
164+
165+
self.assertIn(message, str(error.exception))
166+
167+
168+
if __name__ == "__main__":
169+
unittest.main()

0 commit comments

Comments
 (0)