unitlog 0.0.3__tar.gz → 0.0.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: unitlog
3
- Version: 0.0.3
3
+ Version: 0.0.6
4
4
  Home-page: https://github.com/yujun2647/unitlog
5
5
  Download-URL:
6
6
  Author: walkerjun
@@ -9,6 +9,14 @@ License: Apache-2.0
9
9
  Requires-Python: >=3.6
10
10
  Description-Content-Type: text/markdown
11
11
  License-File: LICENSE
12
+ Dynamic: author
13
+ Dynamic: author-email
14
+ Dynamic: description
15
+ Dynamic: description-content-type
16
+ Dynamic: home-page
17
+ Dynamic: license
18
+ Dynamic: license-file
19
+ Dynamic: requires-python
12
20
 
13
21
  # unitlog
14
22
 
@@ -0,0 +1,16 @@
1
+ __version__ = "0.0.6"
2
+
3
+
4
+ updates = """
5
+ ### 版本说明
6
+
7
+ **✨ 核心亮点**
8
+ - **多进程支持**:我们引入了更加稳健的多进程支持,现在可以通过 spawn 方式来启动和管理多个进程,提升了应用的并行处理能力。
9
+ - **日志系统增强**:为了确保日志记录的可靠性,我们更新了日志系统,现在会自动确保日志路径目录的存在,避免了因目录问题导致的日志记录失败。
10
+
11
+ **🚀 体验优化**
12
+ - **系统稳定性提升**:通过对多进程支持的改进和日志系统的优化,我们增强了应用的稳定性和可靠性,为用户提供了更加流畅的体验。
13
+
14
+ **🐛 问题修复**
15
+ - *(本次更新没有涉及到老功能的修复,因此此部分留空)*
16
+ """
@@ -13,7 +13,10 @@ class LogBox(object):
13
13
 
14
14
  class UnitHandler(logging.StreamHandler):
15
15
  LOG_TYPE = "console"
16
- DEFAULT_LOG_QUEUE = mp.Queue()
16
+
17
+ def __init__(self, stream=None, bus_queue=None):
18
+ super().__init__(stream)
19
+ self.bus_queue = bus_queue
17
20
 
18
21
  def handle(self, record):
19
22
  """ without acquiring lock
@@ -39,7 +42,7 @@ class UnitHandler(logging.StreamHandler):
39
42
  msg = self.format(record)
40
43
  # issue 35046: merged two stream.writes into one.
41
44
  log_msg = msg + self.terminator
42
- UnitHandler.DEFAULT_LOG_QUEUE.put(self.wrap_msg(log_msg))
45
+ self.bus_queue.put(self.wrap_msg(log_msg))
43
46
  except RecursionError: # See issue 36272
44
47
  raise
45
48
  except Exception:
@@ -53,8 +56,8 @@ class UnitConsoleHandler(UnitHandler):
53
56
  class UnitFileHandler(UnitHandler):
54
57
  LOG_TYPE = "file"
55
58
 
56
- def __init__(self, log_filepath, mode):
57
- super().__init__()
59
+ def __init__(self, log_filepath, mode, bus_queue=None):
60
+ super().__init__(bus_queue=bus_queue)
58
61
  self.log_filepath = log_filepath
59
62
  self.mode = mode
60
63
 
@@ -0,0 +1,299 @@
1
+ import os
2
+ import sys
3
+ import inspect
4
+ import atexit
5
+ import logging
6
+ import traceback
7
+ from queue import Empty
8
+ import multiprocessing as mp
9
+ from multiprocessing.synchronize import Event
10
+
11
+ from unitlog.handlers import LogBox, UnitFileHandler, UnitConsoleHandler
12
+
13
+
14
+
15
+ def is_under_testing():
16
+ """
17
+ 检查当前是否处于单元测试环境 (unittest 或 pytest)。
18
+ 原理:向上遍历栈帧,如果发现调用链中有 unittest 或 pytest 的相关文件,则认为是测试环境。
19
+ """
20
+ # 简单的白名单检查
21
+ for frame_info in inspect.stack():
22
+ module_name = frame_info.frame.f_globals.get('__name__', '')
23
+ # 如果调用栈里包含 unittest 或 pytest,说明是在测试
24
+ if module_name and ('unittest' in module_name or 'pytest' in module_name):
25
+ return True
26
+ return False
27
+
28
+
29
+
30
+ class PoxyConsoleLogWriter(object):
31
+
32
+ def __init__(self, stream=sys.stdout):
33
+ self.stream = stream
34
+
35
+ def emit(self, log_msg):
36
+ self.stream.write(log_msg)
37
+ self.stream.flush()
38
+
39
+ def close(self):
40
+ self.stream.close()
41
+
42
+
43
+ class PoxyFileLogWriter(PoxyConsoleLogWriter):
44
+ def __init__(self, log_filepath, file_mode="a"):
45
+ super().__init__(stream=open(log_filepath, file_mode))
46
+
47
+
48
+ class UnitLog(object):
49
+
50
+ def __init__(self):
51
+ self.started: Event = mp.Event()
52
+ self.stopped: Event = mp.Event()
53
+ self.log_num = mp.Value('i', 0)
54
+ self.worker = None
55
+ self.bus_queue = None
56
+ self._proxy_handler_map = {}
57
+
58
+ def _init_proxy_handler(self, log_box: LogBox) -> PoxyConsoleLogWriter:
59
+ hkey = f"{log_box.log_type}-{log_box.log_filepath}"
60
+ if hkey not in self._proxy_handler_map:
61
+ if log_box.log_type == "console":
62
+ self._proxy_handler_map[hkey] = PoxyConsoleLogWriter()
63
+ elif log_box.log_type == "file":
64
+ abs_log_filepath = os.path.abspath(log_box.log_filepath)
65
+ dir_path = os.path.dirname(abs_log_filepath)
66
+ if not os.path.exists(dir_path):
67
+ os.makedirs(dir_path, exist_ok=True)
68
+
69
+ self._proxy_handler_map[hkey] = PoxyFileLogWriter(
70
+ log_filepath=abs_log_filepath,
71
+ file_mode=log_box.file_mode
72
+ )
73
+ else:
74
+ raise TypeError(f"Unsupported log type: {log_box.log_type}")
75
+ return self._proxy_handler_map[hkey]
76
+
77
+ def listening_log_msg(self, bus_queue):
78
+ while True:
79
+ self.started.set()
80
+ try:
81
+ log_box: LogBox = bus_queue.get(timeout=0.1)
82
+ except Empty:
83
+ if self.stopped.is_set():
84
+ break
85
+ continue
86
+ except KeyboardInterrupt:
87
+ continue
88
+ try:
89
+ handler = self._init_proxy_handler(log_box)
90
+
91
+ handler.emit(log_box.log_msg)
92
+ if os.environ.get("ENV-TEST", "prod") == "test":
93
+ self.log_num.value += 1
94
+ except Exception as e:
95
+ print(f"unexpect exception: {e}\n "
96
+ f"{traceback.format_exc()}")
97
+ if os.environ.get("ENV-TEST", "prod") == "test":
98
+ print(f"all log num: {self.log_num.value}")
99
+
100
+ def register_logger(self, name, level=logging.INFO,
101
+ console_log=True, file_log=False, file_log_mode="a",
102
+ log_filepath=None,
103
+ parent_logger_name=None,
104
+ force_all_console_log_to_file=False) -> logging.Logger:
105
+
106
+ caller_frame = inspect.currentframe().f_back
107
+ caller_module_name = caller_frame.f_globals.get('__name__')
108
+ caller_func_name = caller_frame.f_code.co_name
109
+
110
+ # ---------------------------------------------------------
111
+ # 规则 1: 严禁全局裸奔 (必须包在函数里)
112
+ # ---------------------------------------------------------
113
+ if caller_func_name == '<module>':
114
+ raise RuntimeError(
115
+ "⛔️ [禁止全局调用] 你必须把此函数放在一个 def 函数内部调用,\n"
116
+ """
117
+ 例如
118
+ def main():
119
+ register_logger()
120
+
121
+ if __name__ == '__main__':
122
+ main()
123
+ """
124
+ "严禁在脚本顶层直接执行,否则会导致多进程 spawn 时的无限递归。"
125
+ )
126
+
127
+ # ---------------------------------------------------------
128
+ # 规则 2: 必须是 Main 入口,或者是 单元测试
129
+ # ---------------------------------------------------------
130
+ # 如果是直接运行脚本,name 是 __main__ -> 通过
131
+ # 如果是 unittest,name 是 文件名 -> 但 is_under_testing() 为真 -> 通过
132
+ # 如果是 spawn 子进程,name 是 文件名 -> 且不在测试栈中 -> 拦截
133
+ if caller_module_name != '__main__' and not is_under_testing():
134
+ raise RuntimeError(
135
+ f"⛔️ [禁止导入执行] 检测到当前模块名为 '{caller_module_name}'。\n"
136
+ "此函数只能在 'if __name__ == \"__main__\":' 入口下执行,\n"
137
+ "或者是通过单元测试(unittest/pytest)执行。\n"
138
+ "禁止在多进程 spawn 导入阶段或作为普通模块被 import 时执行。"
139
+ )
140
+
141
+ print(f"✅ 安全检查通过 (调用者: {caller_module_name}, 环境安全)")
142
+
143
+ if not self.started.is_set():
144
+ self.bus_queue = mp.Queue()
145
+ self.worker = mp.Process(target=self.listening_log_msg, args=(self.bus_queue, ), daemon=True)
146
+ self.worker.start()
147
+ if not self.started.wait(timeout=3):
148
+ raise ValueError("unit log process is not started")
149
+
150
+ logger = logging.getLogger(name)
151
+ logger.setLevel(level)
152
+ if parent_logger_name is not None:
153
+ parent_logger = logging.getLogger(parent_logger_name)
154
+ logger.parent = parent_logger
155
+ logger.propagate = True
156
+ else:
157
+ logger.propagate = False
158
+
159
+ simple_formatter = logging.Formatter(
160
+ fmt="%(asctime)s [line:%(lineno)d] %(levelname)s %(message)s",
161
+ datefmt="%a, %d %b %Y %H:%M:%S"
162
+ )
163
+ full_formatter = logging.Formatter(
164
+ fmt="%(asctime)s %(filename)s [line:%(lineno)d] %(levelname)s "
165
+ "%(message)s",
166
+ datefmt="%a, %d %b %Y %H:%M:%S"
167
+ )
168
+ if console_log:
169
+ console_handler = UnitConsoleHandler(bus_queue=self.bus_queue)
170
+ console_handler.setFormatter(simple_formatter)
171
+ logger.handlers.append(console_handler)
172
+ if file_log:
173
+ assert log_filepath, "log_filepath must be set"
174
+ os.makedirs(os.path.dirname(log_filepath), exist_ok=True)
175
+ file_handler = UnitFileHandler(log_filepath, mode=file_log_mode, bus_queue=self.bus_queue)
176
+ file_handler.setFormatter(full_formatter)
177
+ logger.handlers.append(file_handler)
178
+ logger.info("\nLog_filename: {}".format(log_filepath))
179
+
180
+ if force_all_console_log_to_file: # 强制控制所有标准输出到 文件
181
+ self.force_all_console_log_to_file(log_filepath)
182
+
183
+ return logger
184
+
185
+
186
+ @classmethod
187
+ def force_all_console_log_to_file(cls, log_filepath):
188
+ # ==========================================
189
+ # 新增函数:重定向底层 C/C++ 输出
190
+ # ==========================================
191
+ def _redirect_c_libraries_output(log_path):
192
+ """
193
+ 使用 os.dup2 强制将底层 C/C++ 的 stdout/stderr 重定向到日志文件。
194
+ 解决 sherpa-onnx, PyQt, OpenCV 等 C 库打印无法被 Python 捕获的问题。
195
+ """
196
+ # 1. 打开日志文件 (使用 append 模式)
197
+ # 这里的 buffer 设置为 0 (unbuffered) 或者 line buffered,确保 C 代码崩溃前能写入
198
+ try:
199
+ # 打开文件获取文件描述符
200
+ # distinct file object specifically for low-level redirection
201
+ log_file = open(log_path, 'a+')
202
+ log_fd = log_file.fileno()
203
+
204
+ # 2. 刷新 Python 的缓冲区,防止重定向导致之前的日志丢失
205
+ sys.stdout.flush()
206
+ sys.stderr.flush()
207
+
208
+ # 3. 核心:重定向 FD 1 (stdout) 和 FD 2 (stderr)
209
+ # 这一步之后,所有的 C printf/std::cout 都会直接写进文件
210
+ os.dup2(log_fd, 1)
211
+ os.dup2(log_fd, 2)
212
+
213
+ # 保持 log_file 对象引用,防止被垃圾回收导致 FD 关闭
214
+ return log_file
215
+ except Exception as e:
216
+ print(f"Failed to redirect C logs: {e}")
217
+ return None
218
+
219
+ class Logger(object):
220
+ def __init__(self, filename):
221
+ self.terminal = sys.stdout # 记录原来的控制台,防止 IDE 里看不到了
222
+ self.log = open(filename, "a", encoding="utf-8") # 'a' 追加模式
223
+
224
+ def write(self, message):
225
+ # 1. 尝试写回控制台(方便开发调试)
226
+ try:
227
+ if self.terminal:
228
+ self.terminal.write(message)
229
+ except:
230
+ pass # 打包成 no console 后这里可能会报错,直接忽略
231
+
232
+ # 2. 写入文件
233
+ try:
234
+ self.log.write(message)
235
+ # 【关键】立即刷新缓冲区,否则崩溃瞬间可能来不及写入文件
236
+ self.log.flush()
237
+ except:
238
+ pass
239
+
240
+ def flush(self):
241
+ # 兼容性函数,必须保留
242
+ try:
243
+ if self.terminal:
244
+ self.terminal.flush()
245
+ self.log.flush()
246
+ except:
247
+ pass
248
+
249
+ # 3. 【关键改动】区分环境进行重定向
250
+ # 判断是否是打包后的环境
251
+ # 逻辑:如果是 PyInstaller 打包 (frozen) 或者 环境变量 MIT_LOG=1,都视为需要重定向
252
+ FORCE_ALL_CONSOLE_LOG_TO_FILE = os.environ.get("FORCE_ALL_CONSOLE_LOG_TO_FILE") == "1"
253
+ IS_FROZEN = getattr(sys, 'frozen', False) or FORCE_ALL_CONSOLE_LOG_TO_FILE
254
+ print(f"FORCE_ALL_CONSOLE_LOG_TO_FILE: {FORCE_ALL_CONSOLE_LOG_TO_FILE} \t IS_FROZEN: {IS_FROZEN}")
255
+
256
+ if IS_FROZEN:
257
+ # --- 打包环境 (Exe) ---
258
+ # 1. 接管 Python 层面 (你原来的做法)
259
+ sys.stdout = Logger(log_filepath)
260
+ sys.stderr = sys.stdout
261
+
262
+ # 2. 接管 C/C++ 层面 (新增的做法)
263
+ # 这会让 sherpa-onnx 的报错也进文件
264
+ # 注意:在 Linux/Mac 上非常有效,Windows 上通常也有效
265
+ _c_log_ref = _redirect_c_libraries_output(log_filepath)
266
+
267
+ print(f"Native C++ stdout/stderr redirected to {log_filepath}")
268
+
269
+ else:
270
+ # --- 开发环境 (IDE) ---
271
+ # 在开发时,我们通常不希望 C++ 输出消失在控制台
272
+ # 所以这里我们 *只* 使用你原来的 Logger 记录 Python print
273
+ # 这样 IDE 控制台里既能看到 Python print,也能看到 C++ print (IDE 会自己捕获 FD)
274
+ # 同时 Python print 也会被写入文件
275
+
276
+ # 如果你确实希望开发时文件里也有 sherpa-onnx 的日志,
277
+ # 你可以把上面的 redirect_c_libraries_output 打开,
278
+ # 但代价是你的 PyCharm 控制台里那行红色的警告会消失。
279
+ sys.stdout = Logger(log_filepath)
280
+ sys.stderr = sys.stdout
281
+
282
+ DEFAULT_LOG = UnitLog()
283
+
284
+ register_logger: UnitLog.register_logger = DEFAULT_LOG.register_logger
285
+ atexit.register(lambda: DEFAULT_LOG.stopped.set())
286
+
287
+
288
+
289
+ def main():
290
+
291
+ _logger = logging.getLogger("test")
292
+ register_logger(name="test", level=logging.DEBUG,
293
+ log_filepath="./temp/test.log")
294
+ _logger.info("lllll")
295
+
296
+
297
+
298
+ if __name__ == "__main__":
299
+ main()
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: unitlog
3
- Version: 0.0.3
3
+ Version: 0.0.6
4
4
  Home-page: https://github.com/yujun2647/unitlog
5
5
  Download-URL:
6
6
  Author: walkerjun
@@ -9,6 +9,14 @@ License: Apache-2.0
9
9
  Requires-Python: >=3.6
10
10
  Description-Content-Type: text/markdown
11
11
  License-File: LICENSE
12
+ Dynamic: author
13
+ Dynamic: author-email
14
+ Dynamic: description
15
+ Dynamic: description-content-type
16
+ Dynamic: home-page
17
+ Dynamic: license
18
+ Dynamic: license-file
19
+ Dynamic: requires-python
12
20
 
13
21
  # unitlog
14
22
 
@@ -1 +0,0 @@
1
- __version__ = "0.0.3"
@@ -1,135 +0,0 @@
1
- import os
2
- import sys
3
- import atexit
4
- import logging
5
- import traceback
6
- from queue import Empty
7
- import multiprocessing as mp
8
- from multiprocessing.synchronize import Event
9
-
10
- from unitlog.handlers import (LogBox, UnitHandler, UnitFileHandler,
11
- UnitConsoleHandler)
12
-
13
-
14
- class PoxyConsoleLogWriter(object):
15
-
16
- def __init__(self, stream=sys.stdout):
17
- self.stream = stream
18
-
19
- def emit(self, log_msg):
20
- self.stream.write(log_msg)
21
- self.stream.flush()
22
-
23
- def close(self):
24
- self.stream.close()
25
-
26
-
27
- class PoxyFileLogWriter(PoxyConsoleLogWriter):
28
- def __init__(self, log_filepath, file_mode="a"):
29
- super().__init__(stream=open(log_filepath, file_mode))
30
-
31
-
32
- class UnitLog(object):
33
-
34
- def __init__(self):
35
- self.started: Event = mp.Event()
36
- self.stopped: Event = mp.Event()
37
- self.log_num = mp.Value('i', 0)
38
- self.worker = mp.Process(target=self.listening_log_msg, daemon=True)
39
- self._proxy_handler_map = {}
40
-
41
- def _init_proxy_handler(self, log_box: LogBox) -> PoxyConsoleLogWriter:
42
- hkey = f"{log_box.log_type}-{log_box.log_filepath}"
43
- if hkey not in self._proxy_handler_map:
44
- if log_box.log_type == "console":
45
- self._proxy_handler_map[hkey] = PoxyConsoleLogWriter()
46
- elif log_box.log_type == "file":
47
- abs_log_filepath = os.path.abspath(log_box.log_filepath)
48
- dir_path = os.path.dirname(abs_log_filepath)
49
- if not os.path.exists(dir_path):
50
- os.makedirs(dir_path, exist_ok=True)
51
-
52
- self._proxy_handler_map[hkey] = PoxyFileLogWriter(
53
- log_filepath=abs_log_filepath,
54
- file_mode=log_box.file_mode
55
- )
56
- else:
57
- raise TypeError(f"Unsupported log type: {log_box.log_type}")
58
- return self._proxy_handler_map[hkey]
59
-
60
- def listening_log_msg(self):
61
- while True:
62
- self.started.set()
63
- try:
64
- log_box: LogBox = UnitHandler.DEFAULT_LOG_QUEUE.get(timeout=0.1)
65
- except Empty:
66
- if self.stopped.is_set():
67
- break
68
- continue
69
- except KeyboardInterrupt:
70
- continue
71
- try:
72
- handler = self._init_proxy_handler(log_box)
73
-
74
- handler.emit(log_box.log_msg)
75
- if os.environ.get("ENV-TEST", "prod") == "test":
76
- self.log_num.value += 1
77
- except Exception as e:
78
- print(f"unexpect exception: {e}\n "
79
- f"{traceback.format_exc()}")
80
- if os.environ.get("ENV-TEST", "prod") == "test":
81
- print(f"all log num: {self.log_num.value}")
82
-
83
- def register_logger(self, name, level=logging.INFO,
84
- console_log=True, file_log=False, file_log_mode="a",
85
- log_filepath=None,
86
- parent_logger_name=None) -> logging.Logger:
87
- if not self.started.is_set():
88
- self.worker.start()
89
- if not self.started.wait(timeout=3):
90
- raise ValueError("unit log process is not started")
91
-
92
- logger = logging.getLogger(name)
93
- logger.setLevel(level)
94
- if parent_logger_name is not None:
95
- parent_logger = logging.getLogger(parent_logger_name)
96
- logger.parent = parent_logger
97
- logger.propagate = True
98
- else:
99
- logger.propagate = False
100
-
101
- simple_formatter = logging.Formatter(
102
- fmt="%(asctime)s [line:%(lineno)d] %(levelname)s %(message)s",
103
- datefmt="%a, %d %b %Y %H:%M:%S"
104
- )
105
- full_formatter = logging.Formatter(
106
- fmt="%(asctime)s %(filename)s [line:%(lineno)d] %(levelname)s "
107
- "%(message)s",
108
- datefmt="%a, %d %b %Y %H:%M:%S"
109
- )
110
- if console_log:
111
- console_handler = UnitConsoleHandler()
112
- console_handler.setFormatter(simple_formatter)
113
- logger.handlers.append(console_handler)
114
- if file_log:
115
- assert log_filepath, "log_filepath must be set"
116
- file_handler = UnitFileHandler(log_filepath, mode=file_log_mode)
117
- file_handler.setFormatter(full_formatter)
118
- logger.handlers.append(file_handler)
119
- logger.info("\nLog_filename: {}".format(log_filepath))
120
-
121
- return logger
122
-
123
-
124
- DEFAULT_LOG = UnitLog()
125
-
126
- register_logger = DEFAULT_LOG.register_logger
127
- atexit.register(lambda: DEFAULT_LOG.stopped.set())
128
-
129
- if __name__ == "__main__":
130
- import time
131
-
132
- _logger = logging.getLogger("test")
133
- register_logger(name="test", level=logging.DEBUG,
134
- log_filepath="./temp/test.log")
135
- _logger.info("lllll")
File without changes
File without changes
File without changes
File without changes
File without changes