tree-sitter-analyzer 0.9.9__py3-none-any.whl → 1.0.0__py3-none-any.whl
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.
Potentially problematic release.
This version of tree-sitter-analyzer might be problematic. Click here for more details.
- tree_sitter_analyzer/__init__.py +132 -132
- tree_sitter_analyzer/api.py +542 -533
- tree_sitter_analyzer/cli/commands/base_command.py +181 -181
- tree_sitter_analyzer/cli/commands/partial_read_command.py +139 -139
- tree_sitter_analyzer/cli/info_commands.py +124 -121
- tree_sitter_analyzer/cli_main.py +327 -328
- tree_sitter_analyzer/core/analysis_engine.py +584 -584
- tree_sitter_analyzer/file_handler.py +212 -210
- tree_sitter_analyzer/formatters/base_formatter.py +169 -167
- tree_sitter_analyzer/interfaces/cli.py +535 -535
- tree_sitter_analyzer/mcp/server.py +655 -650
- tree_sitter_analyzer/mcp/tools/__init__.py +30 -30
- tree_sitter_analyzer/mcp/utils/path_resolver.py +414 -194
- tree_sitter_analyzer/output_manager.py +257 -253
- tree_sitter_analyzer/project_detector.py +330 -330
- tree_sitter_analyzer/security/boundary_manager.py +260 -251
- tree_sitter_analyzer/security/validator.py +257 -246
- tree_sitter_analyzer/table_formatter.py +710 -708
- tree_sitter_analyzer/utils.py +335 -336
- {tree_sitter_analyzer-0.9.9.dist-info → tree_sitter_analyzer-1.0.0.dist-info}/METADATA +30 -4
- {tree_sitter_analyzer-0.9.9.dist-info → tree_sitter_analyzer-1.0.0.dist-info}/RECORD +23 -23
- {tree_sitter_analyzer-0.9.9.dist-info → tree_sitter_analyzer-1.0.0.dist-info}/WHEEL +0 -0
- {tree_sitter_analyzer-0.9.9.dist-info → tree_sitter_analyzer-1.0.0.dist-info}/entry_points.txt +0 -0
tree_sitter_analyzer/utils.py
CHANGED
|
@@ -1,336 +1,335 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Utilities for Tree-sitter Analyzer
|
|
4
|
-
|
|
5
|
-
Provides logging, debugging, and common utility functions.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import atexit
|
|
9
|
-
import logging
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
from
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
env_level
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
#
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
self.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
"
|
|
250
|
-
"
|
|
251
|
-
"
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
log_func
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
perf_logger.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
perf_logger.
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
self.
|
|
322
|
-
self.
|
|
323
|
-
self.
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
self.
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
self.target_logger.setLevel(self.old_level)
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Utilities for Tree-sitter Analyzer
|
|
4
|
+
|
|
5
|
+
Provides logging, debugging, and common utility functions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import atexit
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
from functools import wraps
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Configure global logger
|
|
17
|
+
def setup_logger(
|
|
18
|
+
name: str = "tree_sitter_analyzer", level: int = logging.WARNING
|
|
19
|
+
) -> logging.Logger:
|
|
20
|
+
"""Setup unified logger for the project"""
|
|
21
|
+
# Get log level from environment variable
|
|
22
|
+
env_level = os.environ.get("LOG_LEVEL", "").upper()
|
|
23
|
+
if env_level == "DEBUG":
|
|
24
|
+
level = logging.DEBUG
|
|
25
|
+
elif env_level == "INFO":
|
|
26
|
+
level = logging.INFO
|
|
27
|
+
elif env_level == "WARNING":
|
|
28
|
+
level = logging.WARNING
|
|
29
|
+
elif env_level == "ERROR":
|
|
30
|
+
level = logging.ERROR
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(name)
|
|
33
|
+
|
|
34
|
+
if not logger.handlers: # Avoid duplicate handlers
|
|
35
|
+
# Create a safe handler that writes to stderr to avoid breaking MCP stdio
|
|
36
|
+
handler = SafeStreamHandler()
|
|
37
|
+
formatter = logging.Formatter(
|
|
38
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
39
|
+
)
|
|
40
|
+
handler.setFormatter(formatter)
|
|
41
|
+
logger.addHandler(handler)
|
|
42
|
+
|
|
43
|
+
# Also log to a local file for debugging when launched by clients (e.g., Cursor)
|
|
44
|
+
# This helps diagnose cases where stdio is captured by the client and logs are hidden.
|
|
45
|
+
try:
|
|
46
|
+
file_handler = logging.FileHandler(
|
|
47
|
+
"cursor_mcp_server.log", encoding="utf-8"
|
|
48
|
+
)
|
|
49
|
+
file_handler.setFormatter(formatter)
|
|
50
|
+
logger.addHandler(file_handler)
|
|
51
|
+
except Exception as e:
|
|
52
|
+
# Never let logging configuration break runtime behavior; log to stderr if possible
|
|
53
|
+
if hasattr(sys, "stderr") and hasattr(sys.stderr, "write"):
|
|
54
|
+
try:
|
|
55
|
+
sys.stderr.write(
|
|
56
|
+
f"[logging_setup] file handler init skipped: {e}\n"
|
|
57
|
+
)
|
|
58
|
+
except Exception:
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
logger.setLevel(level)
|
|
62
|
+
|
|
63
|
+
return logger
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class SafeStreamHandler(logging.StreamHandler):
|
|
67
|
+
"""
|
|
68
|
+
A StreamHandler that safely handles closed streams
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(self, stream=None):
|
|
72
|
+
# Default to sys.stderr to keep stdout clean for MCP stdio
|
|
73
|
+
super().__init__(stream if stream is not None else sys.stderr)
|
|
74
|
+
|
|
75
|
+
def emit(self, record: Any) -> None:
|
|
76
|
+
"""
|
|
77
|
+
Emit a record, safely handling closed streams
|
|
78
|
+
"""
|
|
79
|
+
try:
|
|
80
|
+
# Check if stream is closed before writing
|
|
81
|
+
if hasattr(self.stream, "closed") and self.stream.closed:
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
# Check if we can write to the stream
|
|
85
|
+
if not hasattr(self.stream, "write"):
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
super().emit(record)
|
|
89
|
+
except (ValueError, OSError, AttributeError):
|
|
90
|
+
# Silently ignore I/O errors during shutdown
|
|
91
|
+
pass
|
|
92
|
+
except Exception:
|
|
93
|
+
# For any other unexpected errors, use handleError
|
|
94
|
+
self.handleError(record)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def setup_safe_logging_shutdown() -> None:
|
|
98
|
+
"""
|
|
99
|
+
Setup safe logging shutdown to prevent I/O errors
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def cleanup_logging() -> None:
|
|
103
|
+
"""Clean up logging handlers safely"""
|
|
104
|
+
try:
|
|
105
|
+
# Get all loggers
|
|
106
|
+
loggers = [logging.getLogger()] + [
|
|
107
|
+
logging.getLogger(name) for name in logging.Logger.manager.loggerDict
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
for logger in loggers:
|
|
111
|
+
for handler in logger.handlers[:]:
|
|
112
|
+
try:
|
|
113
|
+
handler.close()
|
|
114
|
+
logger.removeHandler(handler)
|
|
115
|
+
except Exception as e:
|
|
116
|
+
if hasattr(sys, "stderr") and hasattr(sys.stderr, "write"):
|
|
117
|
+
try:
|
|
118
|
+
sys.stderr.write(
|
|
119
|
+
f"[logging_cleanup] handler close/remove skipped: {e}\n"
|
|
120
|
+
)
|
|
121
|
+
except Exception:
|
|
122
|
+
...
|
|
123
|
+
except Exception as e:
|
|
124
|
+
if hasattr(sys, "stderr") and hasattr(sys.stderr, "write"):
|
|
125
|
+
try:
|
|
126
|
+
sys.stderr.write(f"[logging_cleanup] cleanup skipped: {e}\n")
|
|
127
|
+
except Exception:
|
|
128
|
+
...
|
|
129
|
+
|
|
130
|
+
# Register cleanup function
|
|
131
|
+
atexit.register(cleanup_logging)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# Setup safe shutdown on import
|
|
135
|
+
setup_safe_logging_shutdown()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# Global logger instance
|
|
139
|
+
logger = setup_logger()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def log_info(message: str, *args: Any, **kwargs: Any) -> None:
|
|
143
|
+
"""Log info message"""
|
|
144
|
+
try:
|
|
145
|
+
logger.info(message, *args, **kwargs)
|
|
146
|
+
except (ValueError, OSError) as e:
|
|
147
|
+
if hasattr(sys, "stderr") and hasattr(sys.stderr, "write"):
|
|
148
|
+
try:
|
|
149
|
+
sys.stderr.write(f"[log_info] suppressed: {e}\n")
|
|
150
|
+
except Exception:
|
|
151
|
+
...
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def log_warning(message: str, *args: Any, **kwargs: Any) -> None:
|
|
155
|
+
"""Log warning message"""
|
|
156
|
+
try:
|
|
157
|
+
logger.warning(message, *args, **kwargs)
|
|
158
|
+
except (ValueError, OSError) as e:
|
|
159
|
+
if hasattr(sys, "stderr") and hasattr(sys.stderr, "write"):
|
|
160
|
+
try:
|
|
161
|
+
sys.stderr.write(f"[log_warning] suppressed: {e}\n")
|
|
162
|
+
except Exception:
|
|
163
|
+
...
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def log_error(message: str, *args: Any, **kwargs: Any) -> None:
|
|
167
|
+
"""Log error message"""
|
|
168
|
+
try:
|
|
169
|
+
logger.error(message, *args, **kwargs)
|
|
170
|
+
except (ValueError, OSError) as e:
|
|
171
|
+
if hasattr(sys, "stderr") and hasattr(sys.stderr, "write"):
|
|
172
|
+
try:
|
|
173
|
+
sys.stderr.write(f"[log_error] suppressed: {e}\n")
|
|
174
|
+
except Exception:
|
|
175
|
+
...
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def log_debug(message: str, *args: Any, **kwargs: Any) -> None:
|
|
179
|
+
"""Log debug message"""
|
|
180
|
+
try:
|
|
181
|
+
logger.debug(message, *args, **kwargs)
|
|
182
|
+
except (ValueError, OSError) as e:
|
|
183
|
+
if hasattr(sys, "stderr") and hasattr(sys.stderr, "write"):
|
|
184
|
+
try:
|
|
185
|
+
sys.stderr.write(f"[log_debug] suppressed: {e}\n")
|
|
186
|
+
except Exception:
|
|
187
|
+
...
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def suppress_output(func: Any) -> Any:
|
|
191
|
+
"""Decorator to suppress print statements in production"""
|
|
192
|
+
|
|
193
|
+
@wraps(func)
|
|
194
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
195
|
+
# Check if we're in test/debug mode
|
|
196
|
+
if getattr(sys, "_testing", False):
|
|
197
|
+
return func(*args, **kwargs)
|
|
198
|
+
|
|
199
|
+
# Redirect stdout to suppress prints
|
|
200
|
+
old_stdout = sys.stdout
|
|
201
|
+
try:
|
|
202
|
+
sys.stdout = (
|
|
203
|
+
open("/dev/null", "w") if sys.platform != "win32" else open("nul", "w")
|
|
204
|
+
)
|
|
205
|
+
result = func(*args, **kwargs)
|
|
206
|
+
finally:
|
|
207
|
+
try:
|
|
208
|
+
sys.stdout.close()
|
|
209
|
+
except Exception as e:
|
|
210
|
+
if hasattr(sys, "stderr") and hasattr(sys.stderr, "write"):
|
|
211
|
+
try:
|
|
212
|
+
sys.stderr.write(
|
|
213
|
+
f"[suppress_output] stdout close suppressed: {e}\n"
|
|
214
|
+
)
|
|
215
|
+
except Exception:
|
|
216
|
+
...
|
|
217
|
+
sys.stdout = old_stdout
|
|
218
|
+
|
|
219
|
+
return result
|
|
220
|
+
|
|
221
|
+
return wrapper
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class QuietMode:
|
|
225
|
+
"""Context manager for quiet execution"""
|
|
226
|
+
|
|
227
|
+
def __init__(self, enabled: bool = True):
|
|
228
|
+
self.enabled = enabled
|
|
229
|
+
self.old_level: int | None = None
|
|
230
|
+
|
|
231
|
+
def __enter__(self) -> "QuietMode":
|
|
232
|
+
if self.enabled:
|
|
233
|
+
self.old_level = logger.level
|
|
234
|
+
logger.setLevel(logging.ERROR)
|
|
235
|
+
return self
|
|
236
|
+
|
|
237
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
238
|
+
if self.enabled and self.old_level is not None:
|
|
239
|
+
logger.setLevel(self.old_level)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def safe_print(message: str, level: str = "info", quiet: bool = False) -> None:
|
|
243
|
+
"""Safe print function that can be controlled"""
|
|
244
|
+
if quiet:
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
level_map = {
|
|
248
|
+
"info": log_info,
|
|
249
|
+
"warning": log_warning,
|
|
250
|
+
"error": log_error,
|
|
251
|
+
"debug": log_debug,
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
log_func = level_map.get(level.lower(), log_info)
|
|
255
|
+
log_func(message)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def create_performance_logger(name: str) -> logging.Logger:
|
|
259
|
+
"""Create performance-focused logger"""
|
|
260
|
+
perf_logger = logging.getLogger(f"{name}.performance")
|
|
261
|
+
|
|
262
|
+
if not perf_logger.handlers:
|
|
263
|
+
handler = SafeStreamHandler()
|
|
264
|
+
formatter = logging.Formatter("%(asctime)s - PERF - %(message)s")
|
|
265
|
+
handler.setFormatter(formatter)
|
|
266
|
+
perf_logger.addHandler(handler)
|
|
267
|
+
perf_logger.setLevel(logging.DEBUG) # Change to DEBUG level
|
|
268
|
+
|
|
269
|
+
return perf_logger
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# Performance logger instance
|
|
273
|
+
perf_logger = create_performance_logger("tree_sitter_analyzer")
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def log_performance(
|
|
277
|
+
operation: str,
|
|
278
|
+
execution_time: float | None = None,
|
|
279
|
+
details: dict[Any, Any] | str | None = None,
|
|
280
|
+
) -> None:
|
|
281
|
+
"""Log performance metrics"""
|
|
282
|
+
try:
|
|
283
|
+
message = f"{operation}"
|
|
284
|
+
if execution_time is not None:
|
|
285
|
+
message += f": {execution_time:.4f}s"
|
|
286
|
+
if details:
|
|
287
|
+
if isinstance(details, dict):
|
|
288
|
+
detail_str = ", ".join([f"{k}: {v}" for k, v in details.items()])
|
|
289
|
+
else:
|
|
290
|
+
detail_str = str(details)
|
|
291
|
+
message += f" - {detail_str}"
|
|
292
|
+
perf_logger.debug(message) # Change to DEBUG level
|
|
293
|
+
except (ValueError, OSError) as e:
|
|
294
|
+
if hasattr(sys, "stderr") and hasattr(sys.stderr, "write"):
|
|
295
|
+
try:
|
|
296
|
+
sys.stderr.write(f"[log_performance] suppressed: {e}\n")
|
|
297
|
+
except Exception:
|
|
298
|
+
...
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def setup_performance_logger() -> logging.Logger:
|
|
302
|
+
"""Set up performance logging"""
|
|
303
|
+
perf_logger = logging.getLogger("performance")
|
|
304
|
+
|
|
305
|
+
# Add handler if not already configured
|
|
306
|
+
if not perf_logger.handlers:
|
|
307
|
+
handler = SafeStreamHandler()
|
|
308
|
+
formatter = logging.Formatter("%(asctime)s - Performance - %(message)s")
|
|
309
|
+
handler.setFormatter(formatter)
|
|
310
|
+
perf_logger.addHandler(handler)
|
|
311
|
+
perf_logger.setLevel(logging.INFO)
|
|
312
|
+
|
|
313
|
+
return perf_logger
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class LoggingContext:
|
|
317
|
+
"""Context manager for controlling logging behavior"""
|
|
318
|
+
|
|
319
|
+
def __init__(self, enabled: bool = True, level: int | None = None):
|
|
320
|
+
self.enabled = enabled
|
|
321
|
+
self.level = level
|
|
322
|
+
self.old_level: int | None = None
|
|
323
|
+
self.target_logger = (
|
|
324
|
+
logging.getLogger()
|
|
325
|
+
) # Use root logger for compatibility with tests
|
|
326
|
+
|
|
327
|
+
def __enter__(self) -> "LoggingContext":
|
|
328
|
+
if self.enabled and self.level is not None:
|
|
329
|
+
self.old_level = self.target_logger.level
|
|
330
|
+
self.target_logger.setLevel(self.level)
|
|
331
|
+
return self
|
|
332
|
+
|
|
333
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
334
|
+
if self.enabled and self.old_level is not None:
|
|
335
|
+
self.target_logger.setLevel(self.old_level)
|