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 +19 -0
- siren/_output.py +10 -0
- siren/autoload.py +74 -0
- siren/clean.py +188 -0
- siren/core.py +589 -0
- siren_debug-0.5.0.dist-info/METADATA +333 -0
- siren_debug-0.5.0.dist-info/RECORD +10 -0
- siren_debug-0.5.0.dist-info/WHEEL +6 -0
- siren_debug-0.5.0.dist-info/entry_points.txt +3 -0
- siren_debug-0.5.0.dist-info/top_level.txt +1 -0
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
|
+
[](https://pypi.org/project/siren-debug/)
|
|
26
|
+
[](https://pypi.org/project/siren-debug/)
|
|
27
|
+
[](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 @@
|
|
|
1
|
+
siren
|