dbg_cli/session_db/canonicalizer/
python.rs1use std::path::Path;
15use std::sync::OnceLock;
16
17use regex::Regex;
18
19use super::{CanonicalSymbol, Canonicalizer};
20
21pub struct PythonCanonicalizer;
22
23impl Canonicalizer for PythonCanonicalizer {
24 fn lang(&self) -> &'static str {
25 "python"
26 }
27
28 fn canonicalize(&self, raw: &str) -> CanonicalSymbol {
29 let parsed = parse(raw);
30 CanonicalSymbol {
31 lang: "python",
32 fqn: parsed.fqn,
33 file: parsed.file,
34 line: parsed.line,
35 demangled: None,
36 raw: raw.to_string(),
37 is_synthetic: parsed.synthetic,
38 }
39 }
40}
41
42struct Parsed {
43 fqn: String,
44 file: Option<String>,
45 line: Option<u32>,
46 synthetic: bool,
47}
48
49fn parse(raw: &str) -> Parsed {
50 if let Some(inner) = raw
52 .strip_prefix("<built-in method ")
53 .and_then(|s| s.strip_suffix('>'))
54 {
55 return Parsed {
56 fqn: inner.to_string(),
57 file: None,
58 line: None,
59 synthetic: false,
60 };
61 }
62 static METHOD_OF: OnceLock<Regex> = OnceLock::new();
64 let re_method_of = METHOD_OF.get_or_init(|| {
65 Regex::new(r"^<method '(?P<m>[^']+)' of '(?P<t>[^']+)' objects>$").unwrap()
66 });
67 if let Some(c) = re_method_of.captures(raw) {
68 return Parsed {
69 fqn: format!("{}.{}", &c["t"], &c["m"]),
70 file: None,
71 line: None,
72 synthetic: false,
73 };
74 }
75
76 static PSTATS: OnceLock<Regex> = OnceLock::new();
78 let re_pstats = PSTATS.get_or_init(|| {
79 Regex::new(r"^(?P<file>[^\s\(]+):(?P<line>\d+)\((?P<func>[^)]+)\)$").unwrap()
80 });
81 if let Some(c) = re_pstats.captures(raw) {
82 let file = c["file"].to_string();
83 let line: u32 = c["line"].parse().ok().unwrap_or(0);
84 let func = c["func"].to_string();
85 let module = module_from_file(&file);
86 let fqn = if module.is_empty() {
87 func.clone()
88 } else {
89 format!("{module}.{func}")
90 };
91 let synthetic = is_synthetic_func(&func);
92 return Parsed {
93 fqn,
94 file: Some(file),
95 line: Some(line),
96 synthetic,
97 };
98 }
99
100 static PYSPY: OnceLock<Regex> = OnceLock::new();
102 let re_pyspy = PYSPY.get_or_init(|| {
103 Regex::new(r"^(?P<func>[A-Za-z_<][\w<>]*)\s+\((?P<file>[^:)]+)(?::(?P<line>\d+))?\)$")
104 .unwrap()
105 });
106 if let Some(c) = re_pyspy.captures(raw) {
107 let func = c["func"].to_string();
108 let file = c["file"].to_string();
109 let line: Option<u32> = c.name("line").and_then(|m| m.as_str().parse().ok());
110 let module = module_from_file(&file);
111 let fqn = if module.is_empty() {
112 func.clone()
113 } else {
114 format!("{module}.{func}")
115 };
116 let synthetic = is_synthetic_func(&func);
117 return Parsed {
118 fqn,
119 file: Some(file),
120 line,
121 synthetic,
122 };
123 }
124
125 let synthetic = is_synthetic_func(raw);
127 Parsed {
128 fqn: raw.to_string(),
129 file: None,
130 line: None,
131 synthetic,
132 }
133}
134
135fn module_from_file(file: &str) -> String {
136 Path::new(file)
139 .file_stem()
140 .and_then(|s| s.to_str())
141 .unwrap_or("")
142 .to_string()
143}
144
145fn is_synthetic_func(f: &str) -> bool {
146 matches!(
147 f,
148 "<lambda>" | "<listcomp>" | "<dictcomp>" | "<setcomp>" | "<genexpr>" | "<module>"
149 )
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 fn py() -> PythonCanonicalizer {
157 PythonCanonicalizer
158 }
159
160 #[test]
161 fn pstats_form_parsed() {
162 let s = py().canonicalize("/opt/myapp/api.py:42(handle_request)");
163 assert_eq!(s.fqn, "api.handle_request");
164 assert_eq!(s.file.as_deref(), Some("/opt/myapp/api.py"));
165 assert_eq!(s.line, Some(42));
166 assert!(!s.is_synthetic);
167 }
168
169 #[test]
170 fn pstats_lambda_is_synthetic() {
171 let s = py().canonicalize("/opt/myapp/api.py:42(<lambda>)");
172 assert!(s.is_synthetic);
173 assert_eq!(s.fqn, "api.<lambda>");
174 }
175
176 #[test]
177 fn pyspy_form_with_line_parsed() {
178 let s = py().canonicalize("handle_request (api.py:42)");
179 assert_eq!(s.fqn, "api.handle_request");
180 assert_eq!(s.file.as_deref(), Some("api.py"));
181 assert_eq!(s.line, Some(42));
182 }
183
184 #[test]
185 fn pyspy_form_without_line_parsed() {
186 let s = py().canonicalize("handle_request (api.py)");
187 assert_eq!(s.fqn, "api.handle_request");
188 assert_eq!(s.line, None);
189 }
190
191 #[test]
192 fn bare_dotted_is_passed_through() {
193 let s = py().canonicalize("myapp.services.users.login");
194 assert_eq!(s.fqn, "myapp.services.users.login");
195 assert_eq!(s.file, None);
196 }
197
198 #[test]
199 fn builtin_method_form() {
200 let s = py().canonicalize("<built-in method builtins.print>");
201 assert_eq!(s.fqn, "builtins.print");
202 }
203
204 #[test]
205 fn method_of_form() {
206 let s = py().canonicalize("<method 'write' of 'BufferedWriter' objects>");
207 assert_eq!(s.fqn, "BufferedWriter.write");
208 }
209
210 #[test]
211 fn listcomp_synthetic() {
212 let s = py().canonicalize("/app/main.py:10(<listcomp>)");
213 assert!(s.is_synthetic);
214 }
215
216 #[test]
217 fn module_level_synthetic() {
218 let s = py().canonicalize("/app/main.py:1(<module>)");
219 assert!(s.is_synthetic);
220 }
221
222 #[test]
223 fn structured_default_joins_with_dot() {
224 let s = py().canonicalize_structured("", "UserService", "login", "");
225 assert_eq!(s.fqn, "UserService.login");
226 }
227
228 #[test]
229 fn key_is_lang_plus_fqn() {
230 let s = py().canonicalize("app.main");
231 assert_eq!(s.key(), ("python", "app.main"));
232 }
233}