chore: 升级项目适配 Python 3.10+ 并重构代码

1.  移除对 Python 3.8/3.9 的支持,更新 tox、pyproject.toml 配置
2.  替换 typing 导入为 collections.abc 标准库类型
3.  重构 CLI 系统从 argparse 迁移到 typer
4.  优化代码格式与类型注解,修复多处类型兼容问题
5.  更新依赖声明,移除 graphlib_backport 兼容包
This commit is contained in:
2026-07-06 22:14:59 +08:00
parent d493c6233e
commit fca6f17a5c
30 changed files with 825 additions and 4796 deletions
+5 -3
View File
@@ -884,9 +884,11 @@ Date: Mon, {i + 1} Jan 2024 12:00:00 +0000
Body {i}
""")
with patch("sys.argv", ["emlmanager", "--dir", str(tmp_path), "--port", "8080"]), patch.object(
emlmanager, "ThreadingHTTPServer"
) as mock_server, patch("threading.Thread"):
with (
patch("sys.argv", ["emlmanager", "--dir", str(tmp_path), "--port", "8080"]),
patch.object(emlmanager, "ThreadingHTTPServer") as mock_server,
patch("threading.Thread"),
):
# Don't actually start the server
mock_server_instance = Mock()
mock_server.return_value = mock_server_instance
+15 -9
View File
@@ -103,9 +103,11 @@ class TestInstallEmbedPython:
output_dir = tmp_path / "python"
# Create a mock cache file that doesn't exist (force download)
with patch("platform.machine", return_value="x86_64"), patch(
"urllib.request.urlretrieve"
) as mock_urlretrieve, patch("zipfile.ZipFile") as mock_zipfile:
with (
patch("platform.machine", return_value="x86_64"),
patch("urllib.request.urlretrieve") as mock_urlretrieve,
patch("zipfile.ZipFile") as mock_zipfile,
):
# Mock successful download
mock_urlretrieve.return_value = None
mock_zip_instance = MagicMock()
@@ -190,9 +192,11 @@ class TestInstallEmbedPython:
"""Should handle different Python versions."""
output_dir = tmp_path / "python"
with patch("platform.machine", return_value="x86_64"), patch(
"urllib.request.urlretrieve"
) as mock_urlretrieve, patch("zipfile.ZipFile") as mock_zipfile:
with (
patch("platform.machine", return_value="x86_64"),
patch("urllib.request.urlretrieve") as mock_urlretrieve,
patch("zipfile.ZipFile") as mock_zipfile,
):
mock_zip_instance = MagicMock()
mock_zipfile.return_value.__enter__.return_value = mock_zip_instance
@@ -205,9 +209,11 @@ class TestInstallEmbedPython:
"""Should create cache directory and file."""
output_dir = tmp_path / "python"
with patch("platform.machine", return_value="x86_64"), patch(
"urllib.request.urlretrieve"
) as mock_urlretrieve, patch("zipfile.ZipFile") as mock_zipfile:
with (
patch("platform.machine", return_value="x86_64"),
patch("urllib.request.urlretrieve") as mock_urlretrieve,
patch("zipfile.ZipFile") as mock_zipfile,
):
mock_urlretrieve.return_value = None
mock_zip_instance = MagicMock()
mock_zipfile.return_value.__enter__.return_value = mock_zip_instance
+5 -3
View File
@@ -247,9 +247,11 @@ class TestPdfOcr:
input_file.write_bytes(b"PDF content")
output_file = tmp_path / "ocr.pdf"
with patch("fitz.open") as mock_fitz_open, patch("PIL.Image.frombytes"), patch(
"pytesseract.image_to_string"
) as mock_ocr:
with (
patch("fitz.open") as mock_fitz_open,
patch("PIL.Image.frombytes"),
patch("pytesseract.image_to_string") as mock_ocr,
):
mock_doc = MagicMock()
mock_page = MagicMock()
mock_page.rect = MagicMock(width=800, height=600)
+4 -3
View File
@@ -121,9 +121,10 @@ class TestPipUninstall:
def test_pip_uninstall_with_wildcard(self) -> None:
"""Should handle wildcard in package name."""
with patch.object(dev, "_expand_wildcard_packages", return_value=["numpy", "numpy-core"]), patch(
"subprocess.run"
) as mock_run:
with (
patch.object(dev, "_expand_wildcard_packages", return_value=["numpy", "numpy-core"]),
patch("subprocess.run") as mock_run,
):
mock_run.return_value = MagicMock(returncode=0)
dev.pip_uninstall(["numpy*"])
assert mock_run.called
+48 -24
View File
@@ -37,36 +37,48 @@ class TestTakeScreenshotFull:
def test_take_screenshot_full_windows(self, tmp_path: Path) -> None:
"""Should take full screenshot on Windows."""
with patch.object(Constants, "IS_WINDOWS", True), patch.object(Constants, "IS_MACOS", False), patch.object(
Path, "home", return_value=tmp_path
), patch("subprocess.run") as mock_run:
with (
patch.object(Constants, "IS_WINDOWS", True),
patch.object(Constants, "IS_MACOS", False),
patch.object(Path, "home", return_value=tmp_path),
patch("subprocess.run") as mock_run,
):
mock_run.return_value = MagicMock(returncode=0)
media.take_screenshot_full()
assert mock_run.called
def test_take_screenshot_full_macos(self, tmp_path: Path) -> None:
"""Should take full screenshot on macOS."""
with patch.object(Constants, "IS_WINDOWS", False), patch.object(Constants, "IS_MACOS", True), patch.object(
Path, "home", return_value=tmp_path
), patch("subprocess.run") as mock_run:
with (
patch.object(Constants, "IS_WINDOWS", False),
patch.object(Constants, "IS_MACOS", True),
patch.object(Path, "home", return_value=tmp_path),
patch("subprocess.run") as mock_run,
):
mock_run.return_value = MagicMock(returncode=0)
media.take_screenshot_full()
assert mock_run.called
def test_take_screenshot_full_linux(self, tmp_path: Path) -> None:
"""Should take full screenshot on Linux."""
with patch.object(Constants, "IS_WINDOWS", False), patch.object(Constants, "IS_MACOS", False), patch.object(
Path, "home", return_value=tmp_path
), patch("subprocess.run") as mock_run:
with (
patch.object(Constants, "IS_WINDOWS", False),
patch.object(Constants, "IS_MACOS", False),
patch.object(Path, "home", return_value=tmp_path),
patch("subprocess.run") as mock_run,
):
mock_run.return_value = MagicMock(returncode=0)
media.take_screenshot_full()
assert mock_run.called
def test_take_screenshot_full_linux_scrot_fallback(self, tmp_path: Path) -> None:
"""Should fallback to scrot when gnome-screenshot not found."""
with patch.object(Constants, "IS_WINDOWS", False), patch.object(Constants, "IS_MACOS", False), patch.object(
Path, "home", return_value=tmp_path
), patch("subprocess.run") as mock_run:
with (
patch.object(Constants, "IS_WINDOWS", False),
patch.object(Constants, "IS_MACOS", False),
patch.object(Path, "home", return_value=tmp_path),
patch("subprocess.run") as mock_run,
):
mock_run.side_effect = [FileNotFoundError(), MagicMock(returncode=0)]
media.take_screenshot_full()
assert mock_run.call_count == 2
@@ -80,36 +92,48 @@ class TestTakeScreenshotArea:
def test_take_screenshot_area_windows(self, tmp_path: Path) -> None:
"""Should take area screenshot on Windows."""
with patch.object(Constants, "IS_WINDOWS", True), patch.object(Constants, "IS_MACOS", False), patch.object(
Path, "home", return_value=tmp_path
), patch("subprocess.run") as mock_run:
with (
patch.object(Constants, "IS_WINDOWS", True),
patch.object(Constants, "IS_MACOS", False),
patch.object(Path, "home", return_value=tmp_path),
patch("subprocess.run") as mock_run,
):
mock_run.return_value = MagicMock(returncode=0)
media.take_screenshot_area()
assert mock_run.called
def test_take_screenshot_area_macos(self, tmp_path: Path) -> None:
"""Should take area screenshot on macOS."""
with patch.object(Constants, "IS_WINDOWS", False), patch.object(Constants, "IS_MACOS", True), patch.object(
Path, "home", return_value=tmp_path
), patch("subprocess.run") as mock_run:
with (
patch.object(Constants, "IS_WINDOWS", False),
patch.object(Constants, "IS_MACOS", True),
patch.object(Path, "home", return_value=tmp_path),
patch("subprocess.run") as mock_run,
):
mock_run.return_value = MagicMock(returncode=0)
media.take_screenshot_area()
assert mock_run.called
def test_take_screenshot_area_linux(self, tmp_path: Path) -> None:
"""Should take area screenshot on Linux."""
with patch.object(Constants, "IS_WINDOWS", False), patch.object(Constants, "IS_MACOS", False), patch.object(
Path, "home", return_value=tmp_path
), patch("subprocess.run") as mock_run:
with (
patch.object(Constants, "IS_WINDOWS", False),
patch.object(Constants, "IS_MACOS", False),
patch.object(Path, "home", return_value=tmp_path),
patch("subprocess.run") as mock_run,
):
mock_run.return_value = MagicMock(returncode=0)
media.take_screenshot_area()
assert mock_run.called
def test_take_screenshot_area_linux_scrot_fallback(self, tmp_path: Path) -> None:
"""Should fallback to scrot -s when gnome-screenshot not found."""
with patch.object(Constants, "IS_WINDOWS", False), patch.object(Constants, "IS_MACOS", False), patch.object(
Path, "home", return_value=tmp_path
), patch("subprocess.run") as mock_run:
with (
patch.object(Constants, "IS_WINDOWS", False),
patch.object(Constants, "IS_MACOS", False),
patch.object(Path, "home", return_value=tmp_path),
patch("subprocess.run") as mock_run,
):
mock_run.side_effect = [FileNotFoundError(), MagicMock(returncode=0)]
media.take_screenshot_area()
assert mock_run.call_count == 2
+15 -9
View File
@@ -27,9 +27,11 @@ class TestSshCopyId:
pub_key = tmp_path / "id_rsa.pub"
pub_key.write_text("ssh-rsa AAAAB3...")
with patch.object(Path, "expanduser", return_value=pub_key), patch(
"subprocess.run", side_effect=FileNotFoundError
), pytest.raises(SystemExit):
with (
patch.object(Path, "expanduser", return_value=pub_key),
patch("subprocess.run", side_effect=FileNotFoundError),
pytest.raises(SystemExit),
):
system.ssh_copy_id("localhost", "user", "password")
def test_ssh_copy_id_timeout(self, tmp_path: Path) -> None:
@@ -37,9 +39,11 @@ class TestSshCopyId:
pub_key = tmp_path / "id_rsa.pub"
pub_key.write_text("ssh-rsa AAAAB3...")
with patch.object(Path, "expanduser", return_value=pub_key), patch(
"subprocess.run", side_effect=subprocess.TimeoutExpired("cmd", 30)
), pytest.raises(SystemExit):
with (
patch.object(Path, "expanduser", return_value=pub_key),
patch("subprocess.run", side_effect=subprocess.TimeoutExpired("cmd", 30)),
pytest.raises(SystemExit),
):
system.ssh_copy_id("localhost", "user", "password")
def test_ssh_copy_id_process_error(self, tmp_path: Path) -> None:
@@ -47,9 +51,11 @@ class TestSshCopyId:
pub_key = tmp_path / "id_rsa.pub"
pub_key.write_text("ssh-rsa AAAAB3...")
with patch.object(Path, "expanduser", return_value=pub_key), patch(
"subprocess.run", side_effect=subprocess.CalledProcessError(1, "cmd")
), pytest.raises(SystemExit):
with (
patch.object(Path, "expanduser", return_value=pub_key),
patch("subprocess.run", side_effect=subprocess.CalledProcessError(1, "cmd")),
pytest.raises(SystemExit),
):
system.ssh_copy_id("localhost", "user", "password")
def test_ssh_copy_id_success(self, tmp_path: Path) -> None: