Skip to content

Commit 7ae1210

Browse files
committed
Add Utilities
添加 `open_dir_in_explorer()`、`get_sys_python()`、`QtFileOpen` 等实用函数与工具类;
1 parent b884a94 commit 7ae1210

3 files changed

Lines changed: 252 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Licensed under the GPLv3 License: https://www.gnu.org/licenses/gpl-3.0.html
2+
# For details: https://github.com/muziing/Py2exe-GUI/blob/main/README.md#license
3+
4+
from .open_qfile import QtFileOpen
5+
from .platform_specifc_funcs import get_sys_python, open_dir_in_explorer
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
# Licensed under the GPLv3 License: https://www.gnu.org/licenses/gpl-3.0.html
2+
# For details: https://github.com/muziing/Py2exe-GUI/blob/main/README.md#license
3+
4+
import locale
5+
import os
6+
import pathlib
7+
import warnings
8+
from typing import Optional, Union
9+
10+
from PySide6.QtCore import QFile, QIODevice
11+
12+
13+
class QtFileOpen:
14+
"""
15+
通过 QFile 读写文件的上下文管理器 \n
16+
使与 Python 的 with 语句风格统一 \n
17+
18+
使用举例:
19+
20+
with QtFileOpen("./test.txt", "rt", encoding="utf-8") as f:
21+
print(f.read())
22+
"""
23+
24+
def __init__(
25+
self,
26+
file: Union[str, bytes, os.PathLike[str]],
27+
mode="r",
28+
encoding: Optional[str] = None,
29+
):
30+
"""
31+
:param file: 文件路径
32+
:param mode: 打开模式(暂时只支持文本读取)
33+
:param encoding: 文本文件编码
34+
"""
35+
36+
# 预处理文件路径,防止在 Windows 平台下 QFile 只能处理 '/' 导致的问题
37+
file_path = self.deal_path(file)
38+
39+
# 分析模式是否合法、返回正确的 FileIo 类实例
40+
# https://docs.python.org/zh-cn/3/library/functions.html#open
41+
if "b" not in mode:
42+
# 文本模式
43+
self.io_obj = PyQTextFileIo(file_path, mode, encoding)
44+
else:
45+
# 二进制模式(暂不支持)
46+
# self.io_obj = PyQByteFileIo(file, mode)
47+
raise ValueError("暂不支持该模式")
48+
49+
def __enter__(self):
50+
return self.io_obj
51+
52+
def __exit__(self, exc_type, exc_val, exc_tb):
53+
self.io_obj.close()
54+
55+
@classmethod
56+
def deal_path(cls, path: Union[str, bytes, os.PathLike[str]]) -> str:
57+
"""
58+
预处理文件路径,确保使用 / 风格
59+
:param path: 文件路径
60+
:return: 使用正斜杠(/)的路径字符串
61+
"""
62+
63+
# 若路径以字节串传入,则先处理成字符串
64+
if isinstance(path, bytes):
65+
path = str(path, encoding="utf-8")
66+
67+
return str(pathlib.PurePath(path).as_posix())
68+
69+
70+
class PyQTextFileIo:
71+
"""
72+
将 QFile 中处理文本文件读写的部分封装成 Python的 io 风格
73+
目前只支持读取,不支持写入
74+
"""
75+
76+
def __init__(
77+
self,
78+
file: Union[str, bytes, os.PathLike[str]],
79+
mode,
80+
encoding: Optional[str] = None,
81+
):
82+
self._file = QFile(file)
83+
84+
if encoding is not None:
85+
self.encoding = encoding
86+
else:
87+
# 用户未指定编码,则使用当前平台默认编码
88+
self.encoding = locale.getencoding()
89+
90+
self.mode = self._parse_mode(mode)
91+
self._file.open(self.mode)
92+
93+
@classmethod
94+
def _parse_mode(cls, py_mode: str) -> QIODevice:
95+
"""
96+
解析文件打开模式,将 Python open() 风格转换至 QIODevice.OpenModeFlag
97+
https://docs.python.org/zh-cn/3/library/functions.html#open
98+
https://doc.qt.io/qt-6/qiodevicebase.html#OpenModeFlag-enum
99+
:return: mode
100+
"""
101+
102+
qt_mode: QIODevice = QIODevice.Text
103+
104+
# 暂不支持写入
105+
if "r" in py_mode and "+" not in py_mode:
106+
qt_mode = qt_mode | QIODevice.ReadOnly
107+
elif "w" in py_mode:
108+
qt_mode = qt_mode | QIODevice.WriteOnly
109+
elif "+" in py_mode:
110+
qt_mode = qt_mode | QIODevice.ReadWrite
111+
112+
if "x" in py_mode:
113+
qt_mode = qt_mode | QIODevice.NewOnly
114+
115+
return qt_mode
116+
117+
def readable(self) -> bool:
118+
"""
119+
当前文件是否可读 \n
120+
:return: isReadable
121+
"""
122+
123+
return self._file.isReadable()
124+
125+
def read(self, size: int = -1) -> str:
126+
"""
127+
从流中读取至多 size 个字符并以单个 str 的形式返回。 如果 size 为负值或 None,则读取至 EOF。 \n
128+
https://docs.python.org/3/library/io.html#io.TextIOBase.read
129+
:param size: 读取的字符数,负值或 None 表示一直读取直到 EOF
130+
:return: 文件中读出的文本内容
131+
"""
132+
133+
if not self.readable():
134+
raise OSError(f"File '{self._file.fileName()}' is not Readable.")
135+
136+
if size < 0 or size is None:
137+
# 读取文件,并将 QByteArray 转为 str
138+
text = str(self._file.readAll(), encoding=self.encoding)
139+
else:
140+
# 已知问题:性能太差
141+
# PySide6.QtCore.QIODevice.read(maxlen) 以字节而非字符方式计算长度,行为不一致
142+
# 而 QTextStream 对字符编码支持太差,许多编码并不支持
143+
text = str(self._file.readAll(), encoding=self.encoding)[0:size] # 性能太差
144+
145+
return text
146+
147+
def readline(self, size: int = -1, /) -> str:
148+
"""
149+
模仿 io.TextIOBase.readline 的行为,读取文件中的一行。 \n
150+
https://docs.python.org/3/library/io.html#io.TextIOBase.readline
151+
:param size: 如果指定了 size ,最多将读取 size 个字符。
152+
:return: 单行文本
153+
"""
154+
155+
if not self.readable():
156+
raise OSError(f"File '{self._file.fileName()}' is not Readable.")
157+
158+
if self._file.atEnd():
159+
warnings.warn(
160+
f"Trying to read a line at the end of the file '{self._file.fileName()}'.",
161+
Warning,
162+
stacklevel=1,
163+
)
164+
return ""
165+
else:
166+
if size == 0:
167+
return ""
168+
else:
169+
line = str(self._file.readLine(), encoding=self.encoding)
170+
if size < 0:
171+
return line
172+
else:
173+
return line[0:size]
174+
175+
def readlines(self, hint: int = -1, /) -> list[str]:
176+
"""
177+
模仿 Python 中 io.IOBase.readlines() 的行为,返回由所有行组成的字符串列表。 \n
178+
Known issue: slower than `readline()` \n
179+
https://docs.python.org/3/library/io.html#io.IOBase.readlines
180+
:param hint: 要读取的字符数
181+
:return: 文本内容所有行组成的列表
182+
"""
183+
184+
if not self.readable():
185+
raise OSError(f"File '{self._file.fileName()}' is not Readable.")
186+
187+
if hint <= 0 or hint is None:
188+
temp = str(self._file.readAll(), encoding=self.encoding)
189+
all_lines = temp.splitlines(keepends=True)
190+
else:
191+
all_lines = []
192+
char_num = 0
193+
while char_num <= hint and not self._file.atEnd():
194+
new_line = self.readline()
195+
all_lines.append(new_line)
196+
char_num += len(new_line)
197+
198+
return all_lines
199+
200+
def close(self) -> None:
201+
"""
202+
关闭打开的文件对象
203+
"""
204+
205+
self._file.close()
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Licensed under the GPLv3 License: https://www.gnu.org/licenses/gpl-3.0.html
2+
# For details: https://github.com/muziing/Py2exe-GUI/blob/main/README.md#license
3+
4+
"""
5+
一些因操作系统不同而在具体实现上有差异的功能函数
6+
注意,由于开发者没有苹果电脑,所有关于 macOS 的功能均未经过验证
7+
"""
8+
9+
import subprocess
10+
11+
from ..Constants import PLATFORM, RUNTIME_INFO
12+
13+
14+
def open_dir_in_explorer(dir_path: str) -> None:
15+
"""
16+
在操作系统文件资源管理器中打开指定目录 \n
17+
:param dir_path: 待打开的目录路径
18+
"""
19+
20+
if RUNTIME_INFO.platform == PLATFORM.windows:
21+
import os # fmt: skip
22+
os.startfile(dir_path) # type: ignore
23+
elif RUNTIME_INFO.platform == PLATFORM.linux:
24+
subprocess.call(["xdg-open", dir_path])
25+
elif RUNTIME_INFO.platform == PLATFORM.macos:
26+
subprocess.call(["open", dir_path])
27+
28+
29+
def get_sys_python() -> str:
30+
"""
31+
获取系统默认 Python 解释器的可执行文件位置 \n
32+
:return: Python 可执行文件路径
33+
"""
34+
35+
if RUNTIME_INFO.platform == PLATFORM.windows:
36+
cmd = "powershell.exe (Get-Command python).Path" # PowerShell
37+
elif RUNTIME_INFO.platform in (PLATFORM.linux, PLATFORM.macos):
38+
cmd = "which python3"
39+
else:
40+
raise ValueError("Current OS is not supported.")
41+
42+
return subprocess.getoutput(cmd)

0 commit comments

Comments
 (0)