siren-debug 0.5.0__py2.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.
siren/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ try:
2
+ import builtins
3
+ except ImportError: # Python 2
4
+ import __builtin__ as builtins
5
+
6
+ from .core import siren, trace, info, set_quiet, set_logfile, set_enabled, get_config, diff, breakpoint_debug
7
+
8
+ __all__ = ["siren", "trace", "info", "set_quiet", "set_logfile", "set_enabled", "get_config", "diff", "breakpoint_debug"]
9
+
10
+ builtins.siren = siren
11
+
12
+ siren.trace = trace
13
+ siren.info = info
14
+ siren.set_quiet = set_quiet
15
+ siren.set_logfile = set_logfile
16
+ siren.set_enabled = set_enabled
17
+ siren.get_config = get_config
18
+ siren.diff = diff
19
+ siren.breakpoint = breakpoint_debug
siren/_output.py ADDED
@@ -0,0 +1,10 @@
1
+ # -*- coding: utf-8 -*-
2
+ import sys
3
+
4
+
5
+ def safe_print(text):
6
+ try:
7
+ print(text)
8
+ except UnicodeEncodeError:
9
+ encoding = getattr(sys.stdout, "encoding", None) or "ascii"
10
+ print(text.encode(encoding, errors="replace").decode(encoding, errors="replace"))
siren/autoload.py ADDED
@@ -0,0 +1,74 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Toggle automatic injection of `siren` into builtins for every Python
4
+ process started from the current environment (venv), by managing a
5
+ .pth file in site-packages.
6
+
7
+ Usage:
8
+ siren-autoload on # enable
9
+ siren-autoload off # disable
10
+ siren-autoload status # check current state
11
+ """
12
+ from __future__ import print_function
13
+
14
+ import io
15
+ import os
16
+ import sys
17
+ import sysconfig
18
+
19
+ from ._output import safe_print
20
+
21
+ PTH_FILENAME = "siren-autoload.pth"
22
+ PTH_CONTENT = (
23
+ 'import sys; exec("try:\\n import siren\\nexcept Exception:\\n pass")\n'
24
+ )
25
+
26
+ COLOR = "\033[38;2;255;105;180m"
27
+ RESET = "\033[0m"
28
+ EMOJI = "🧜‍"
29
+
30
+
31
+ def _pth_path():
32
+ return os.path.join(sysconfig.get_path("purelib"), PTH_FILENAME)
33
+
34
+
35
+ def enable():
36
+ path = _pth_path()
37
+ with io.open(path, "w", encoding="utf-8") as f:
38
+ f.write(PTH_CONTENT)
39
+ return path
40
+
41
+
42
+ def disable():
43
+ path = _pth_path()
44
+ if os.path.exists(path):
45
+ os.remove(path)
46
+ return True
47
+ return False
48
+
49
+
50
+ def is_enabled():
51
+ return os.path.exists(_pth_path())
52
+
53
+
54
+ def main():
55
+ args = sys.argv[1:]
56
+ command = args[0] if args else "status"
57
+
58
+ if command == "on":
59
+ path = enable()
60
+ safe_print("{}[{} SIREN AUTOLOAD]{} enabled -> {}".format(COLOR, EMOJI, RESET, path))
61
+ elif command == "off":
62
+ removed = disable()
63
+ message = "disabled" if removed else "was already disabled"
64
+ safe_print("{}[{} SIREN AUTOLOAD]{} {}".format(COLOR, EMOJI, RESET, message))
65
+ elif command == "status":
66
+ state = "enabled" if is_enabled() else "disabled"
67
+ safe_print("{}[{} SIREN AUTOLOAD]{} {}".format(COLOR, EMOJI, RESET, state))
68
+ else:
69
+ print("Usage: siren-autoload [on|off|status]")
70
+ sys.exit(1)
71
+
72
+
73
+ if __name__ == "__main__":
74
+ main()
siren/clean.py ADDED
@@ -0,0 +1,188 @@
1
+ # -*- coding: utf-8 -*-
2
+ from __future__ import print_function
3
+
4
+ import io
5
+ import os
6
+ import re
7
+ import sys
8
+ import tokenize
9
+ import traceback
10
+ from datetime import datetime
11
+
12
+ from ._output import safe_print
13
+
14
+ TOKEN_NAME = "siren"
15
+
16
+ IMPORT_RE = re.compile(r"^\s*from\s+siren\s+import\s+siren(?:\s+as\s+\w+)?\s*(?:#.*)?$")
17
+
18
+ # ANSI para rosa
19
+ COLOR = "\033[38;2;255;105;180m"
20
+ RESET = "\033[0m"
21
+ EMOJI = "🧜‍"
22
+
23
+ IGNORE_DIRS = {
24
+ "venv",
25
+ ".venv",
26
+ "__pycache__",
27
+ "doc",
28
+ "docs",
29
+ ".git",
30
+ "node_modules",
31
+ }
32
+
33
+
34
+ def _format_timestamp():
35
+ return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
36
+
37
+
38
+ def _multiline_string_lines(tokens):
39
+ """Line numbers that fall entirely inside a multi-line string literal."""
40
+ lines = set()
41
+
42
+ for token in tokens:
43
+ if token[0] == tokenize.STRING:
44
+ start_row, end_row = token[2][0], token[3][0]
45
+ if end_row > start_row:
46
+ lines.update(range(start_row + 1, end_row + 1))
47
+
48
+ return lines
49
+
50
+
51
+ def _collect_siren_lines(source):
52
+ lines = source.splitlines(keepends=True)
53
+ marked = set()
54
+
55
+ try:
56
+ tokens = list(tokenize.generate_tokens(io.StringIO(source).readline))
57
+ except tokenize.TokenError:
58
+ return marked
59
+
60
+ string_lines = _multiline_string_lines(tokens)
61
+
62
+ for index, token in enumerate(tokens):
63
+ toknum, tokval, start, _, _ = token
64
+
65
+ if toknum == tokenize.NAME and tokval == TOKEN_NAME:
66
+ # Skip definitions like `def siren(...)` / `def siren.trace(...)` -
67
+ # only actual calls should be stripped, not the definition itself.
68
+ prev = index - 1
69
+ while prev >= 0 and tokens[prev][0] in (
70
+ tokenize.NL,
71
+ tokenize.INDENT,
72
+ tokenize.DEDENT,
73
+ tokenize.COMMENT,
74
+ ):
75
+ prev -= 1
76
+
77
+ if prev >= 0 and tokens[prev][0] == tokenize.NAME and tokens[prev][1] == "def":
78
+ continue
79
+
80
+ j = index + 1
81
+ while j < len(tokens) and tokens[j][0] in (
82
+ tokenize.NL,
83
+ tokenize.NEWLINE,
84
+ tokenize.INDENT,
85
+ tokenize.DEDENT,
86
+ tokenize.ENDMARKER,
87
+ ):
88
+ j += 1
89
+
90
+ if j < len(tokens) and tokens[j][0] == tokenize.OP and tokens[j][1] == "(":
91
+ depth = 0
92
+ k = j
93
+
94
+ while k < len(tokens):
95
+ toknum2, tokval2, start2, _, _ = tokens[k]
96
+ marked.add(start2[0])
97
+
98
+ if toknum2 == tokenize.OP:
99
+ if tokval2 == "(":
100
+ depth += 1
101
+ elif tokval2 == ")":
102
+ depth -= 1
103
+ if depth == 0:
104
+ break
105
+
106
+ k += 1
107
+
108
+ for lineno, line in enumerate(lines, 1):
109
+ if lineno in string_lines:
110
+ continue
111
+ if IMPORT_RE.match(line):
112
+ marked.add(lineno)
113
+
114
+ return marked
115
+
116
+
117
+ def clean_file(path, dry_run=False):
118
+ # io.open decodes/encodes explicitly on both Python 2 and 3, unlike the
119
+ # builtin open() which doesn't accept encoding= on Python 2.
120
+ with io.open(path, "r", encoding="utf-8") as f:
121
+ source = f.read()
122
+
123
+ lines = source.splitlines(keepends=True)
124
+
125
+ try:
126
+ marked = _collect_siren_lines(source)
127
+ except Exception as e:
128
+ raise type(e)("{}\nArquivo: {}".format(e, path))
129
+
130
+ if not marked:
131
+ return 0
132
+
133
+ if dry_run:
134
+ safe_print("✓ {} ({} linhas)".format(path, len(marked)))
135
+ return len(marked)
136
+
137
+ new_lines = [line for lineno, line in enumerate(lines, start=1) if lineno not in marked]
138
+ new_source = "".join(new_lines)
139
+
140
+ try:
141
+ compile(new_source, path, "exec")
142
+ except SyntaxError:
143
+ # Removing these lines would leave invalid Python (e.g. an emptied
144
+ # block). Leave the file untouched rather than corrupting it.
145
+ return 0
146
+
147
+ with io.open(path, "w", encoding="utf-8") as f:
148
+ f.write(new_source)
149
+
150
+ return len(marked)
151
+
152
+
153
+ def clean_directory(root):
154
+ total_removed = 0
155
+
156
+ for base, dirs, files in os.walk(root):
157
+ # Impede o os.walk de entrar nesses diretórios
158
+ dirs[:] = [d for d in dirs if d.lower() not in IGNORE_DIRS]
159
+
160
+ for name in files:
161
+ if name.endswith(".py"):
162
+ path = os.path.join(base, name)
163
+ total_removed += clean_file(path)
164
+
165
+ return total_removed
166
+
167
+
168
+ def main():
169
+ try:
170
+ target = sys.argv[1] if len(sys.argv) > 1 else "."
171
+ removed = clean_directory(target)
172
+ safe_print(
173
+ "{}[{} SIREN CLEAN {}] {}{} linhas removidas{}".format(
174
+ COLOR,
175
+ EMOJI,
176
+ _format_timestamp(),
177
+ COLOR,
178
+ removed,
179
+ RESET,
180
+ )
181
+ )
182
+ except Exception:
183
+ traceback.print_exc()
184
+ sys.exit(1)
185
+
186
+
187
+ if __name__ == "__main__":
188
+ main()
siren/core.py ADDED
@@ -0,0 +1,589 @@
1
+ # -*- coding: utf-8 -*-
2
+ from __future__ import print_function
3
+
4
+ import io
5
+ import pprint
6
+ import inspect
7
+ import linecache
8
+ import re
9
+ import sys
10
+ import time
11
+ import os
12
+ import tokenize
13
+ from datetime import datetime
14
+
15
+ from ._output import safe_print
16
+
17
+ try:
18
+ text_type = unicode # Python 2
19
+ except NameError:
20
+ text_type = str # Python 3
21
+
22
+ try:
23
+ _read_input = raw_input # Python 2
24
+ except NameError:
25
+ _read_input = input # Python 3
26
+
27
+ # time.perf_counter() doesn't exist on Python 2; time.time() is lower
28
+ # resolution but good enough for a debug timer.
29
+ _perf_counter = getattr(time, "perf_counter", time.time)
30
+
31
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
32
+ COLOR = "\033[38;2;255;105;180m"
33
+ RESET = "\033[0m"
34
+ EMOJI = '🧜‍' #"🔱"
35
+
36
+ PROJECT_MARKERS = [
37
+ ".git",
38
+ "pyproject.toml",
39
+ "setup.py",
40
+ "requirements.txt",
41
+ "manage.py",
42
+ ]
43
+
44
+ # Global configuration
45
+ _SIREN_CONFIG = {
46
+ "quiet": False,
47
+ "logfile": None,
48
+ "enabled": True,
49
+ }
50
+
51
+ def find_project_root(start_path):
52
+ path = os.path.abspath(start_path)
53
+
54
+ while True:
55
+ for marker in PROJECT_MARKERS:
56
+ if os.path.exists(os.path.join(path, marker)):
57
+ return path
58
+
59
+ parent = os.path.dirname(path)
60
+
61
+ if parent == path:
62
+ return None
63
+
64
+ path = parent
65
+
66
+
67
+ def _is_simple(value):
68
+ return isinstance(value, (int, float, str, bool, type(None)))
69
+
70
+
71
+ def _format_timestamp():
72
+ return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
73
+
74
+
75
+ def _print_output(text):
76
+ """Handle output according to configuration (stdout or logfile)."""
77
+ if _SIREN_CONFIG["quiet"]:
78
+ return
79
+
80
+ safe_print(text)
81
+
82
+ if _SIREN_CONFIG["logfile"]:
83
+ try:
84
+ payload = text + "\n"
85
+ if not isinstance(payload, text_type):
86
+ payload = payload.decode("utf-8")
87
+ with io.open(_SIREN_CONFIG["logfile"], "a", encoding="utf-8") as f:
88
+ f.write(payload)
89
+ except Exception:
90
+ pass # Silently fail on log write errors
91
+
92
+
93
+ def _ensure_text(value):
94
+ """Decode byte strings to text so tokenize/io.StringIO work on Python 2,
95
+ where source read from disk isn't decoded automatically."""
96
+ if isinstance(value, text_type):
97
+ return value
98
+ try:
99
+ return value.decode("utf-8")
100
+ except UnicodeDecodeError:
101
+ return value.decode("utf-8", errors="replace")
102
+
103
+
104
+ def _get_call_source(frame):
105
+ filename = frame.f_code.co_filename
106
+ lineno = frame.f_lineno
107
+
108
+ line = linecache.getline(filename, lineno).strip()
109
+
110
+ return line
111
+
112
+
113
+ def get_rel_path(frame):
114
+ caminho = frame.f_code.co_filename
115
+
116
+ root = find_project_root(caminho)
117
+
118
+ if root:
119
+ return os.path.relpath(caminho, root)
120
+
121
+ return caminho
122
+
123
+
124
+ def _extract_args(frame):
125
+ filename = frame.f_code.co_filename
126
+ lineno = frame.f_lineno
127
+
128
+ try:
129
+ lines = linecache.getlines(filename)
130
+ except Exception:
131
+ return []
132
+
133
+ if not lines or lineno > len(lines):
134
+ return []
135
+
136
+ start = lineno - 1
137
+ while start >= 0 and "siren" not in lines[start]:
138
+ start -= 1
139
+
140
+ if start < 0:
141
+ return []
142
+
143
+ source_lines = []
144
+ paren_depth = 0
145
+ saw_call = False
146
+
147
+ for line in lines[start:]:
148
+ source_lines.append(line)
149
+
150
+ for char in line:
151
+ if char == "(":
152
+ paren_depth += 1
153
+ saw_call = saw_call or "siren" in line
154
+ elif char == ")":
155
+ paren_depth -= 1
156
+
157
+ if saw_call and paren_depth <= 0:
158
+ break
159
+
160
+ source = _ensure_text("".join(source_lines))
161
+
162
+ try:
163
+ tokens = list(tokenize.generate_tokens(io.StringIO(source).readline))
164
+ except tokenize.TokenError:
165
+ return []
166
+
167
+ for index, token in enumerate(tokens):
168
+ toknum, tokval, _, _, _ = token
169
+ if toknum == tokenize.NAME and tokval == "siren":
170
+ j = index + 1
171
+ while j < len(tokens) and tokens[j][0] in (
172
+ tokenize.NL,
173
+ tokenize.NEWLINE,
174
+ tokenize.INDENT,
175
+ tokenize.DEDENT,
176
+ ):
177
+ j += 1
178
+
179
+ if j >= len(tokens) or tokens[j][0] != tokenize.OP or tokens[j][1] != "(":
180
+ continue
181
+
182
+ depth = 0
183
+ current = []
184
+ parts = []
185
+ k = j + 1
186
+
187
+ while k < len(tokens):
188
+ tn, tv, _, _, _ = tokens[k]
189
+
190
+ if tn == tokenize.OP and tv == "(":
191
+ depth += 1
192
+ current.append(tv)
193
+ elif tn == tokenize.OP and tv == ")":
194
+ if depth == 0:
195
+ break
196
+ depth -= 1
197
+ current.append(tv)
198
+ elif tn == tokenize.OP and tv == "," and depth == 0:
199
+ parts.append("".join(current).strip())
200
+ current = []
201
+ else:
202
+ if tn in (tokenize.NEWLINE, tokenize.NL):
203
+ current.append(" ")
204
+ elif tn in (tokenize.INDENT, tokenize.DEDENT):
205
+ pass
206
+ elif tn == tokenize.COMMENT:
207
+ pass
208
+ else:
209
+ current.append(tv)
210
+ k += 1
211
+
212
+ if current:
213
+ parts.append("".join(current).strip())
214
+
215
+ return [p for p in parts if p]
216
+
217
+ return []
218
+
219
+
220
+ def _prefix(frame, label=None):
221
+
222
+ lineno = frame.f_lineno
223
+
224
+ base = "{}[{} SIREN {} {}:{}]{}".format(
225
+ COLOR,
226
+ EMOJI,
227
+ _format_timestamp(),
228
+ get_rel_path(frame),
229
+ lineno,
230
+ RESET,
231
+ )
232
+
233
+ if label:
234
+ base += " {}{}{}".format(COLOR, label, RESET)
235
+
236
+ return base
237
+
238
+
239
+ def siren(*values, **kwargs):
240
+ """
241
+ siren(x)
242
+ siren(x, y)
243
+ siren(x, label="BEFORE LOOP")
244
+ siren(x, timeit=True)
245
+ siren(x, quiet=True)
246
+ siren(x, if_equals=5)
247
+ siren(x, if_len_gt=100)
248
+ """
249
+
250
+ if not _SIREN_CONFIG["enabled"]:
251
+ if len(values) == 1:
252
+ return values[0]
253
+ return values
254
+
255
+ # Check conditional filters
256
+ if_equals = kwargs.get("if_equals")
257
+ if_len_gt = kwargs.get("if_len_gt")
258
+ if_len_lt = kwargs.get("if_len_lt")
259
+ if_true = kwargs.get("if_true")
260
+ if_false = kwargs.get("if_false")
261
+
262
+ # Evaluate conditions
263
+ if if_equals is not None:
264
+ if len(values) != 1 or values[0] != if_equals:
265
+ if len(values) == 1:
266
+ return values[0]
267
+ return values
268
+
269
+ if if_len_gt is not None:
270
+ if len(values) != 1 or not hasattr(values[0], "__len__") or len(values[0]) <= if_len_gt:
271
+ if len(values) == 1:
272
+ return values[0]
273
+ return values
274
+
275
+ if if_len_lt is not None:
276
+ if len(values) != 1 or not hasattr(values[0], "__len__") or len(values[0]) >= if_len_lt:
277
+ if len(values) == 1:
278
+ return values[0]
279
+ return values
280
+
281
+ if if_true is not None:
282
+ if not values[0]:
283
+ if len(values) == 1:
284
+ return values[0]
285
+ return values
286
+
287
+ if if_false is not None:
288
+ if values[0]:
289
+ if len(values) == 1:
290
+ return values[0]
291
+ return values
292
+
293
+ frame = inspect.currentframe().f_back
294
+
295
+ label = kwargs.get("label")
296
+ timeit = kwargs.get("timeit", False)
297
+ quiet = kwargs.get("quiet", False)
298
+
299
+ start = None
300
+
301
+ if timeit:
302
+ start = time.time()
303
+
304
+ args = _extract_args(frame)
305
+
306
+ prefix = _prefix(frame, label)
307
+
308
+ # Temporarily override quiet setting if specified
309
+ original_quiet = _SIREN_CONFIG["quiet"]
310
+ if quiet:
311
+ _SIREN_CONFIG["quiet"] = True
312
+
313
+ try:
314
+ for i, v in enumerate(values):
315
+
316
+ name = args[i] if i < len(args) else "?"
317
+
318
+ if _is_simple(v):
319
+ output = "{} {}{} = {}{}".format(prefix, COLOR, name, v, RESET)
320
+ else:
321
+ text = pprint.pformat(v)
322
+ output = "{} {}{} = {}{}".format(prefix, COLOR, name, text, RESET)
323
+
324
+ _print_output(output)
325
+
326
+ if timeit:
327
+ elapsed = time.time() - start
328
+ timer_output = "{}[{} SIREN TIME {}] {}{:.6f}s{}".format(
329
+ COLOR,
330
+ EMOJI,
331
+ _format_timestamp(),
332
+ COLOR,
333
+ elapsed,
334
+ RESET,
335
+ )
336
+ _print_output(timer_output)
337
+
338
+ if len(values) == 1:
339
+ return values[0]
340
+
341
+ return values
342
+ finally:
343
+ _SIREN_CONFIG["quiet"] = original_quiet
344
+
345
+ def _truncate_str(text, max_len=80):
346
+ """Truncate long strings for readable trace output."""
347
+ if len(text) <= max_len:
348
+ return text
349
+ return text[:max_len-3] + "..."
350
+
351
+
352
+ def _format_value_for_trace(value, max_len=80):
353
+ """Format a value for trace output with truncation."""
354
+ if _is_simple(value):
355
+ return str(value)
356
+ text = pprint.pformat(value)
357
+ return _truncate_str(text, max_len)
358
+
359
+
360
+ def _format_trace_args(func, args, kwargs):
361
+ try:
362
+ if hasattr(inspect, "signature"):
363
+ signature = inspect.signature(func)
364
+ bound = signature.bind_partial(*args, **kwargs)
365
+ bound.apply_defaults()
366
+ arguments = bound.arguments.items()
367
+ else:
368
+ # Python 2 has no inspect.signature; getcallargs is the closest
369
+ # equivalent for binding args/kwargs to parameter names.
370
+ arguments = inspect.getcallargs(func, *args, **kwargs).items()
371
+
372
+ parts = []
373
+
374
+ for name, value in arguments:
375
+ formatted = _format_value_for_trace(value, max_len=60)
376
+ parts.append("{}={}".format(name, formatted))
377
+
378
+ return ", ".join(parts)
379
+ except (TypeError, ValueError):
380
+ parts = [repr(value) for value in args]
381
+ parts += ["{}={}".format(name, repr(value)) for name, value in kwargs.items()]
382
+ return ", ".join(parts)
383
+
384
+
385
+ def trace(func=None, **options):
386
+ """
387
+ Decorator to display function entry and exit automatically.
388
+
389
+ Usage:
390
+ @siren.trace
391
+ def foo(...):
392
+ ...
393
+
394
+ Optional configuration:
395
+ @siren.trace(timeit=True, show_args=False, show_type=True)
396
+ """
397
+ timeit = options.get("timeit", True)
398
+ show_args = options.get("show_args", True)
399
+ show_return = options.get("show_return", True)
400
+ show_type = options.get("show_type", True)
401
+
402
+ def decorator(target):
403
+ def wrapper(*args, **kwargs):
404
+ arg_text = _format_trace_args(target, args, kwargs) if show_args else ""
405
+ call_label = "Calling {}".format(target.__name__)
406
+ if arg_text:
407
+ call_label += "({})".format(arg_text)
408
+ else:
409
+ call_label += "()"
410
+
411
+ start = _perf_counter() if timeit else None
412
+ siren.info(call_label)
413
+
414
+ result = None
415
+ exception_occurred = False
416
+ try:
417
+ result = target(*args, **kwargs)
418
+ except Exception as e:
419
+ exception_occurred = True
420
+ exc_type = type(e).__name__
421
+ exc_msg = str(e)
422
+ siren.info("Exception in {} -> {}: {}".format(target.__name__, exc_type, exc_msg))
423
+ raise
424
+ finally:
425
+ if not exception_occurred:
426
+ elapsed_text = ""
427
+ if timeit:
428
+ elapsed = _perf_counter() - start
429
+ elapsed_text = " ({:.6f}s)".format(elapsed)
430
+
431
+ if show_return and result is not None:
432
+ result_text = _format_value_for_trace(result, max_len=100)
433
+ type_text = " [{}]".format(type(result).__name__) if show_type else ""
434
+ siren.info(
435
+ "Returned from {} -> {}{}{}".format(
436
+ target.__name__, result_text, type_text, elapsed_text
437
+ )
438
+ )
439
+ elif show_return and result is None and timeit:
440
+ siren.info(
441
+ "Completed {} (None){}".format(
442
+ target.__name__, elapsed_text
443
+ )
444
+ )
445
+
446
+ return result
447
+
448
+ return wrapper
449
+
450
+ if func is None:
451
+ return decorator
452
+
453
+ return decorator(func)
454
+
455
+
456
+ def info(*args, **kwargs):
457
+ """
458
+ Siren info message
459
+ """
460
+ siren(*args, **kwargs)
461
+
462
+
463
+ def set_quiet(enabled=True):
464
+ """Enable or disable output (quiet mode)."""
465
+ _SIREN_CONFIG["quiet"] = enabled
466
+
467
+
468
+ def set_logfile(filepath):
469
+ """Set a file to log all siren output to."""
470
+ _SIREN_CONFIG["logfile"] = filepath
471
+
472
+
473
+ def set_enabled(enabled=True):
474
+ """Enable or disable siren completely."""
475
+ _SIREN_CONFIG["enabled"] = enabled
476
+
477
+
478
+ def get_config():
479
+ """Get current siren configuration."""
480
+ return _SIREN_CONFIG.copy()
481
+
482
+
483
+ def diff(obj1, obj2, label="DIFF"):
484
+ """
485
+ Compare two objects and display differences.
486
+
487
+ Usage:
488
+ before = {"name": "Alice", "age": 30}
489
+ after = {"name": "Alice", "age": 31, "city": "NYC"}
490
+ siren.diff(before, after)
491
+ """
492
+ frame = inspect.currentframe().f_back
493
+ prefix = _prefix(frame, label)
494
+
495
+ if type(obj1) != type(obj2):
496
+ _print_output("{} {}Type mismatch: {} vs {}{}".format(
497
+ prefix, COLOR, type(obj1).__name__, type(obj2).__name__, RESET
498
+ ))
499
+ return
500
+
501
+ if isinstance(obj1, dict) and isinstance(obj2, dict):
502
+ all_keys = set(obj1.keys()) | set(obj2.keys())
503
+
504
+ for key in sorted(all_keys):
505
+ if key not in obj1:
506
+ _print_output("{} {}[+] {}: {} (new){}".format(
507
+ prefix, COLOR, key, pprint.pformat(obj2[key]), RESET
508
+ ))
509
+ elif key not in obj2:
510
+ _print_output("{} {}[-] {}: {} (removed){}".format(
511
+ prefix, COLOR, key, pprint.pformat(obj1[key]), RESET
512
+ ))
513
+ elif obj1[key] != obj2[key]:
514
+ _print_output(
515
+ "{} {}[~] {}: {} → {} (changed){}".format(
516
+ prefix, COLOR, key, pprint.pformat(obj1[key]), pprint.pformat(obj2[key]), RESET
517
+ )
518
+ )
519
+
520
+ elif isinstance(obj1, (list, tuple)) and isinstance(obj2, (list, tuple)):
521
+ max_len = max(len(obj1), len(obj2))
522
+
523
+ for i in range(max_len):
524
+ if i >= len(obj1):
525
+ _print_output("{} {}[+] [{}]: {} (new){}".format(
526
+ prefix, COLOR, i, pprint.pformat(obj2[i]), RESET
527
+ ))
528
+ elif i >= len(obj2):
529
+ _print_output("{} {}[-] [{}]: {} (removed){}".format(
530
+ prefix, COLOR, i, pprint.pformat(obj1[i]), RESET
531
+ ))
532
+ elif obj1[i] != obj2[i]:
533
+ _print_output(
534
+ "{} {}[~] [{}]: {} → {} (changed){}".format(
535
+ prefix, COLOR, i, pprint.pformat(obj1[i]), pprint.pformat(obj2[i]), RESET
536
+ )
537
+ )
538
+
539
+ else:
540
+ if obj1 == obj2:
541
+ _print_output("{} {}No differences{}".format(prefix, COLOR, RESET))
542
+ else:
543
+ _print_output("{} {}Before: {}{}".format(prefix, COLOR, pprint.pformat(obj1), RESET))
544
+ _print_output("{} {}After: {}{}".format(prefix, COLOR, pprint.pformat(obj2), RESET))
545
+
546
+
547
+ def breakpoint_debug():
548
+ """
549
+ Interactive debugger with siren context.
550
+ Pauses execution and allows inspection.
551
+
552
+ Usage:
553
+ x = 42
554
+ siren.breakpoint() # Pauses here
555
+ """
556
+ frame = inspect.currentframe().f_back
557
+ prefix = _prefix(frame, "BREAKPOINT")
558
+
559
+ local_vars = frame.f_locals
560
+ _print_output("{} {}=== BREAKPOINT ==={}".format(prefix, COLOR, RESET))
561
+ _print_output("{} {}Locals:{}".format(prefix, COLOR, RESET))
562
+
563
+ for name, value in sorted(local_vars.items()):
564
+ if not name.startswith("_"):
565
+ formatted = _format_value_for_trace(value, max_len=80)
566
+ _print_output("{} {} {} = {}{}".format(prefix, COLOR, name, formatted, RESET))
567
+
568
+ _print_output("{} {}Press Ctrl+C to continue or 'd' for debugger...{}".format(prefix, COLOR, RESET))
569
+
570
+ try:
571
+ if sys.stdin.isatty():
572
+ response = _read_input(">>> ").strip()
573
+ if response.lower() == "d":
574
+ import pdb
575
+ pdb.set_trace()
576
+ except (EOFError, KeyboardInterrupt):
577
+ pass
578
+
579
+ _print_output("{} {}Continuing execution...{}".format(prefix, COLOR, RESET))
580
+
581
+
582
+ siren.trace = trace
583
+ siren.set_quiet = set_quiet
584
+ siren.set_logfile = set_logfile
585
+ siren.set_enabled = set_enabled
586
+ siren.get_config = get_config
587
+ siren.diff = diff
588
+ siren.breakpoint = breakpoint_debug
589
+ siren.info = info
@@ -0,0 +1,333 @@
1
+ Metadata-Version: 2.4
2
+ Name: siren-debug
3
+ Version: 0.5.0
4
+ Summary: Minimalist debug tool for Python with automatic cleaner
5
+ Author: Alexandra Bona Abreu
6
+ License: MIT
7
+ Classifier: Programming Language :: Python :: 2
8
+ Classifier: Programming Language :: Python :: 2.7
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.6
11
+ Classifier: Programming Language :: Python :: 3.7
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7
17
+ Description-Content-Type: text/markdown
18
+
19
+ # Siren
20
+
21
+ Minimal Python debug helper with automatic cleanup.
22
+
23
+ > A tiny debugging utility for Python that prints variables with file/line context, traces function calls, measures execution time, and safely removes debug calls from your code.
24
+
25
+ [![PyPI - Version](https://img.shields.io/pypi/v/siren-debug?label=PyPI&color=blue)](https://pypi.org/project/siren-debug/)
26
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/siren-debug?label=Python)](https://pypi.org/project/siren-debug/)
27
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
28
+
29
+ ---
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pip install siren-debug
35
+ ```
36
+
37
+ The package also installs two commands: `siren-clean` (remove debug calls) and `siren-autoload` (use `siren` without importing it).
38
+
39
+ ---
40
+
41
+ ## Quick Start
42
+
43
+ ```python
44
+ from siren import siren
45
+
46
+ x = 10
47
+ user = {"name": "Alex", "items": [1, 2, 3]}
48
+
49
+ siren(x)
50
+ siren(user)
51
+ ```
52
+
53
+ ```text
54
+ [🧜‍ SIREN core.py:10] x = 10
55
+ [🧜‍ SIREN core.py:11] user = {'name': 'Alex', 'items': [1, 2, 3]}
56
+ ```
57
+
58
+ Siren automatically uses `pprint` for complex objects, and picks up the file/line it was called from.
59
+
60
+ ---
61
+
62
+ ## Features
63
+
64
+ - Works with Python 2.7 and 3.6+
65
+ - Zero external dependencies
66
+ - Prints values with file and line number
67
+ - Uses `pprint` automatically for complex data
68
+ - Function tracing with `@siren.trace`, object diffing with `siren.diff`, and an interactive `siren.breakpoint()`
69
+ - Quiet mode, conditional logging, and file logging
70
+ - Removes `siren(...)` calls automatically with `siren-clean`
71
+ - Use `siren` anywhere without importing it via `siren-autoload`
72
+ - Works in scripts, CLI tools, Django, Flask, FastAPI, and more
73
+ - Colored output with emoji for easy visual scanning
74
+
75
+ ---
76
+
77
+ ## Usage
78
+
79
+ Call `siren(...)` with one or more values. It returns them unchanged, so it can be inlined:
80
+
81
+ ```python
82
+ from siren import siren
83
+
84
+ siren(x, data, user)
85
+ result = siren(compute()) # still returns compute()'s value
86
+ ```
87
+
88
+ **Label** — tag a call for easier scanning:
89
+
90
+ ```python
91
+ siren(value, label="BEFORE SAVE")
92
+ ```
93
+
94
+ **Timer** — measure execution time for a call:
95
+
96
+ ```python
97
+ siren(x, timeit=True)
98
+ # [🧜‍ SIREN core.py:10] x = 10
99
+ # [🧜‍ SIREN TIME] 0.000123s
100
+ ```
101
+
102
+ **Quiet mode** — suppress output without removing the call:
103
+
104
+ ```python
105
+ siren(x, quiet=True) # this call only, still returns x
106
+ siren.set_quiet(True) # every call, until set_quiet(False)
107
+ ```
108
+
109
+ **Conditional logging** — only print when a condition holds:
110
+
111
+ ```python
112
+ siren(x, if_equals=5) # only if x == 5
113
+ siren(items, if_len_gt=100) # only if len(items) > 100
114
+ siren(items, if_len_lt=5) # only if len(items) < 5
115
+ siren(result, if_true=True) # only if result is truthy
116
+ siren(error, if_false=True) # only if error is falsy
117
+ ```
118
+
119
+ **Logging to file** — mirror output to a file:
120
+
121
+ ```python
122
+ siren.set_logfile("debug.log")
123
+ siren(x) # prints to stdout AND writes to debug.log
124
+ ```
125
+
126
+ **Inspect configuration**:
127
+
128
+ ```python
129
+ config = siren.get_config()
130
+ print(config) # {"quiet": False, "logfile": None, "enabled": True}
131
+ ```
132
+
133
+ ---
134
+
135
+ ## Function tracing
136
+
137
+ `@siren.trace` logs a function's calls, arguments, return value, execution time, and exceptions automatically:
138
+
139
+ ```python
140
+ from siren import trace
141
+
142
+ @siren.trace
143
+ def add(a, b):
144
+ return a + b
145
+
146
+ add(2, 3)
147
+ ```
148
+
149
+ ```text
150
+ [🧜‍ SIREN core.py:10] Calling add(a=2, b=3)
151
+ [🧜‍ SIREN core.py:11] Returned from add -> 5 [int] (0.000123s)
152
+ ```
153
+
154
+ Configuration options (all default to `True`):
155
+
156
+ | Option | Effect |
157
+ |---|---|
158
+ | `timeit` | Show execution time |
159
+ | `show_args` | Show function arguments |
160
+ | `show_return` | Show return value |
161
+ | `show_type` | Show return type in brackets |
162
+
163
+ ```python
164
+ @siren.trace(timeit=True, show_args=False, show_type=False)
165
+ def multiply(a, b):
166
+ return a * b
167
+ ```
168
+
169
+ Exceptions are logged before being re-raised, so `@siren.trace` never swallows an error:
170
+
171
+ ```python
172
+ @siren.trace
173
+ def divide(a, b):
174
+ return a / b
175
+
176
+ divide(5, 0) # Logs exception before raising
177
+ ```
178
+
179
+ ---
180
+
181
+ ## Diff and breakpoint
182
+
183
+ **`siren.diff`** compares two dicts, lists, tuples, or any comparable objects:
184
+
185
+ ```python
186
+ before = {"name": "Alice", "age": 30}
187
+ after = {"name": "Alice", "age": 31, "city": "NYC"}
188
+
189
+ siren.diff(before, after)
190
+ ```
191
+
192
+ ```text
193
+ [🧜‍ SIREN test.py:10] DIFF
194
+ [🧜‍ SIREN test.py:11] [~] age: 30 → 31 (changed)
195
+ [🧜‍ SIREN test.py:12] [+] city: NYC (new)
196
+ ```
197
+
198
+ **`siren.breakpoint()`** pauses execution and prints local variables:
199
+
200
+ ```python
201
+ x = 42
202
+ data = {"items": [1, 2, 3]}
203
+
204
+ siren.breakpoint() # Pauses and displays all locals
205
+ # Press Ctrl+C to continue, or type 'd' to drop into pdb
206
+ ```
207
+
208
+ ---
209
+
210
+ ## Cleaning debug calls
211
+
212
+ Run `siren-clean` in a project folder to remove all `siren(...)` calls and their import lines — comments and string literals are left untouched:
213
+
214
+ ```bash
215
+ siren-clean
216
+ ```
217
+
218
+ Before:
219
+
220
+ ```python
221
+ from siren import siren
222
+ siren(x)
223
+ print("hello")
224
+ siren(data)
225
+ ```
226
+
227
+ After:
228
+
229
+ ```python
230
+ print("hello")
231
+ ```
232
+
233
+ ---
234
+
235
+ ## Autoload (no per-file imports)
236
+
237
+ By default you still need `from siren import siren` in every file that uses it. If you'd rather call `siren(x)` anywhere in a project without importing it each time, enable autoload once per environment (virtualenv, Docker image, CI job, etc.):
238
+
239
+ ```bash
240
+ siren-autoload on
241
+ siren-autoload status # check whether it's enabled
242
+ siren-autoload off # disable again
243
+ ```
244
+
245
+ This writes a `.pth` file into the current environment's `site-packages`, injecting `siren` into Python's builtins as soon as any interpreter starts in that environment — no import needed anywhere, including in Django apps, Flask views, scripts, or the shell. It's opt-in per environment, so it won't silently affect environments where you didn't run `on`.
246
+
247
+ ---
248
+
249
+ ## Framework examples
250
+
251
+ <details>
252
+ <summary>Django</summary>
253
+
254
+ ```python
255
+ from django.http import JsonResponse
256
+ from siren import siren
257
+
258
+ def my_view(request):
259
+ user_data = request.GET.dict()
260
+ siren(user_data, label="REQUEST_PARAMS")
261
+
262
+ result = process_data(user_data)
263
+ siren(result)
264
+
265
+ return JsonResponse(result)
266
+ ```
267
+ </details>
268
+
269
+ <details>
270
+ <summary>Flask</summary>
271
+
272
+ ```python
273
+ from flask import Flask, request
274
+ from siren import siren, trace
275
+
276
+ app = Flask(__name__)
277
+
278
+ @app.route("/api/users")
279
+ def get_users():
280
+ query = request.args.get("q")
281
+ siren(query, label="SEARCH_QUERY")
282
+
283
+ users = search_users(query)
284
+ return {"users": users}
285
+
286
+ @siren.trace
287
+ def search_users(query):
288
+ # Function entry/exit will be logged automatically
289
+ return [{"id": 1, "name": "Alice"}]
290
+ ```
291
+ </details>
292
+
293
+ <details>
294
+ <summary>FastAPI</summary>
295
+
296
+ ```python
297
+ from fastapi import FastAPI
298
+ from siren import siren, trace
299
+
300
+ app = FastAPI()
301
+
302
+ @app.get("/items/{item_id}")
303
+ async def get_item(item_id: int, q: str = None):
304
+ siren({"item_id": item_id, "q": q}, label="QUERY_PARAMS")
305
+
306
+ item = await fetch_item(item_id)
307
+ return item
308
+
309
+ @siren.trace(timeit=True)
310
+ async def fetch_item(item_id: int):
311
+ # Execution time and arguments will be logged
312
+ return {"id": item_id, "name": "Item"}
313
+ ```
314
+ </details>
315
+
316
+ ---
317
+
318
+ ## Why use Siren?
319
+
320
+ Debug prints are easy to add, but hard to remove later. Siren gives you a fast debug workflow and a safe cleanup step so your temporary debug code does not stay in production.
321
+
322
+ ---
323
+
324
+ ## Project
325
+
326
+ - Package name: `siren-debug`
327
+ - Python versions: `2.7`, `3.6+`
328
+ - License: MIT
329
+ - PyPI: https://pypi.org/project/siren-debug/
330
+
331
+ ## License
332
+
333
+ MIT
@@ -0,0 +1,10 @@
1
+ siren/__init__.py,sha256=vuXREwdjPxpco1D2AaStAeLrO4bD3YC5xenGpRcpPQ4,564
2
+ siren/_output.py,sha256=Y36TtCSa7u0BEpVNPWBuK5aGm6JbbE5nRZC63ehGSK8,277
3
+ siren/autoload.py,sha256=suDpdqJu1wNSnJpiwLFT2v8yD9CwY84XMwRz6h6B-5o,1801
4
+ siren/clean.py,sha256=ki9wdutmkyOGF13Z_9L1P63UVjnlUZR0OJPkwbnSAdE,5003
5
+ siren/core.py,sha256=EGwh4oWXN_vufnjvsPtvk1gdwwqrtEOY-V5pNo-sy4c,16706
6
+ siren_debug-0.5.0.dist-info/METADATA,sha256=IaJFn-qbh2H05g6GBA-u7Ha_l_d_dbxpea1wlovVwi0,8025
7
+ siren_debug-0.5.0.dist-info/WHEEL,sha256=4YBfCYNH4wlLpv3pzq1hbEuIlXA4WJabKLFurZ7eTL0,109
8
+ siren_debug-0.5.0.dist-info/entry_points.txt,sha256=XOCO_18z7SHeGfIk3OnDpkeqftqrvoqVuyz0fmEnMCk,86
9
+ siren_debug-0.5.0.dist-info/top_level.txt,sha256=VZWFA13ISiEdxbjPFWQnhSO2GH9ngjRjxTui7VTMXOE,6
10
+ siren_debug-0.5.0.dist-info/RECORD,,
@@ -0,0 +1,6 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py2-none-any
5
+ Tag: py3-none-any
6
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ siren-autoload = siren.autoload:main
3
+ siren-clean = siren.clean:main
@@ -0,0 +1 @@
1
+ siren