diff --git a/office/__init__.py b/office/__init__.py index dfa1eec..a52987e 100644 --- a/office/__init__.py +++ b/office/__init__.py @@ -13,6 +13,7 @@ from office.api import ppt from office.api import tools from office.api import video +from office.api import web from office.api import wechat from office.api import word from office.api import markdown diff --git a/office/api/web.py b/office/api/web.py index f24d36a..af6bc22 100644 --- a/office/api/web.py +++ b/office/api/web.py @@ -1,6 +1,23 @@ # -*- coding:utf-8 -*- -import pospider + +def _load_pospider(): + try: + import pospider + except ModuleNotFoundError as exc: + if exc.name != "pospider": + raise + raise ModuleNotFoundError( + "网页转电子书功能依赖 pospider,请先安装 pospider。" + ) from exc + try: + pospider.url.url2ebook + except AttributeError as exc: + raise ImportError( + "当前 pospider 版本不提供 url.url2ebook,请安装兼容版本。" + ) from exc + return pospider + def url2ebook(url, tile): """将指定的URL转换为电子书格式。 @@ -14,4 +31,5 @@ def url2ebook(url, tile): Returns: None,但会生成电子书文件 """ + pospider = _load_pospider() pospider.url.url2ebook(url=url, tile=tile) diff --git a/tests/test_code/test_optional_imports.py b/tests/test_code/test_optional_imports.py index 227863e..b329343 100644 --- a/tests/test_code/test_optional_imports.py +++ b/tests/test_code/test_optional_imports.py @@ -1,4 +1,4 @@ -"""Tests for optional platform-specific imports.""" +"""Tests for optional dependency imports.""" from contextlib import contextmanager import importlib @@ -19,13 +19,13 @@ "poocr", "pomarkdown", "wftools", - "pospider", ] -WINDOWS_ONLY_DEPENDENCIES = [ +OPTIONAL_DEPENDENCIES = [ "poppt", "poword", "PyOfficeRobot", + "pospider", ] MANAGED_MODULE_PREFIXES = ( @@ -33,7 +33,7 @@ "pocode", ) -WINDOWS_ONLY_LOADERS = [ +OPTIONAL_LOADERS = [ ( "office.api.word", "_load_poword", @@ -55,6 +55,13 @@ "uiautomation", "微信自动化功能依赖 PyOfficeRobot", ), + ( + "office.api.web", + "_load_pospider", + "pospider", + "requests", + "网页转电子书功能依赖 pospider", + ), ] @@ -63,13 +70,13 @@ def _is_managed_module(name): class TestOptionalImports(unittest.TestCase): - """Check that Windows-only dependencies do not break package imports.""" + """Check that optional dependencies do not break package imports.""" @contextmanager def _optional_import_test_environment(self): module_names = ( SUPPORTED_DEPENDENCIES - + WINDOWS_ONLY_DEPENDENCIES + + OPTIONAL_DEPENDENCIES + ["pocode", "pocode.api", "pocode.api.color"] ) original_modules = { @@ -101,7 +108,7 @@ def _optional_import_test_environment(self): color_module.random_color_print = lambda *args, **kwargs: None sys.modules["pocode.api.color"] = color_module - for name in WINDOWS_ONLY_DEPENDENCIES: + for name in OPTIONAL_DEPENDENCIES: sys.modules.pop(name, None) yield @@ -119,16 +126,18 @@ def _optional_import_test_environment(self): if module is not None: sys.modules[name] = module - def test_import_office_without_windows_only_dependencies(self): + def test_import_office_without_optional_dependencies(self): with self._optional_import_test_environment(): - importlib.import_module("office") + office = importlib.import_module("office") + + self.assertIsNotNone(office.web) def test_loader_preserves_dependency_internal_import_errors(self): with self._optional_import_test_environment(): importlib.import_module("office") original_import = __import__ - for module_name, loader_name, dependency_name, internal_name, message in WINDOWS_ONLY_LOADERS: + for module_name, loader_name, dependency_name, internal_name, message in OPTIONAL_LOADERS: with self.subTest(dependency_name=dependency_name): api_module = importlib.import_module(module_name) loader = getattr(api_module, loader_name) @@ -150,11 +159,11 @@ def import_with_missing_internal_dependency( self.assertEqual(internal_name, error.exception.name) self.assertNotIn(message, str(error.exception)) - def test_loader_reports_missing_windows_only_dependency(self): + def test_loader_reports_missing_optional_dependency(self): with self._optional_import_test_environment(): importlib.import_module("office") - for module_name, loader_name, dependency_name, _, message in WINDOWS_ONLY_LOADERS: + for module_name, loader_name, dependency_name, _, message in OPTIONAL_LOADERS: with self.subTest(dependency_name=dependency_name): api_module = importlib.import_module(module_name) loader = getattr(api_module, loader_name) diff --git a/tests/test_code/test_web.py b/tests/test_code/test_web.py index 452faf8..64b6841 100644 --- a/tests/test_code/test_web.py +++ b/tests/test_code/test_web.py @@ -1,18 +1,43 @@ -"""Web功能测试模块。 - -该模块包含对python-office库中Web相关功能的单元测试。 -""" +"""Tests for the web API wrapper.""" +import importlib.util +from pathlib import Path +from types import SimpleNamespace import unittest +from unittest.mock import Mock, patch + + +def load_web_api(): + module_path = Path(__file__).resolve().parents[2] / "office" / "api" / "web.py" + spec = importlib.util.spec_from_file_location("web_api_under_test", module_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +class TestWeb(unittest.TestCase): + def test_url2ebook_delegates_to_pospider(self): + web = load_web_api() + url2ebook = Mock() + pospider = SimpleNamespace(url=SimpleNamespace(url2ebook=url2ebook)) + + with patch.object(web, "_load_pospider", return_value=pospider): + web.url2ebook("https://example.com/article", tile="测试") + + url2ebook.assert_called_once_with( + url="https://example.com/article", + tile="测试", + ) + + def test_loader_reports_incompatible_pospider(self): + web = load_web_api() + + with patch("builtins.__import__", return_value=SimpleNamespace()): + with self.assertRaises(ImportError) as error: + web._load_pospider() -from office.api import web + self.assertIn("不提供 url.url2ebook", str(error.exception)) -# todo: 功能暂时未实现 -class TestWechat(unittest.TestCase): - """Web功能测试类。 - - 该类包含对Web相关API的单元测试方法。 - """ - def test_url2ebook(self): - web.url2ebook('https://www.zhihu.com/question/36810597', title='测试') +if __name__ == "__main__": + unittest.main()