44//! and caches per-code-object resolutions so the hot tracing callbacks only pay a fast lookup.
55
66use crate :: code_object:: CodeObjectWrapper ;
7- use crate :: module_identity:: { is_valid_module_name, normalise_to_posix , ModuleIdentityResolver } ;
7+ use crate :: module_identity:: { is_valid_module_name, module_from_relative , normalise_to_posix } ;
88use crate :: trace_filter:: config:: {
99 ExecDirective , FilterSource , FilterSummary , ScopeRule , TraceFilterConfig , ValueAction ,
1010 ValuePattern ,
@@ -178,20 +178,6 @@ impl ScopeResolution {
178178 pub fn matched_rule_reason ( & self ) -> Option < & str > {
179179 self . matched_rule_reason . as_deref ( )
180180 }
181-
182- pub ( crate ) fn clone_with_module_name ( & self , module_name : Option < String > ) -> ScopeResolution {
183- ScopeResolution {
184- exec : self . exec ,
185- value_policy : Arc :: clone ( & self . value_policy ) ,
186- module_name,
187- object_name : self . object_name . clone ( ) ,
188- relative_path : self . relative_path . clone ( ) ,
189- absolute_path : self . absolute_path . clone ( ) ,
190- matched_rule_index : self . matched_rule_index ,
191- matched_rule_source : self . matched_rule_source ,
192- matched_rule_reason : self . matched_rule_reason . clone ( ) ,
193- }
194- }
195181}
196182
197183/// Runtime engine wrapping a compiled filter configuration.
@@ -202,7 +188,6 @@ pub struct TraceFilterEngine {
202188 default_value_source : usize ,
203189 rules : Arc < [ CompiledScopeRule ] > ,
204190 cache : DashMap < usize , Arc < ScopeResolution > > ,
205- module_resolver : ModuleIdentityResolver ,
206191}
207192
208193impl TraceFilterEngine {
@@ -212,7 +197,6 @@ impl TraceFilterEngine {
212197 let default_value_action = config. default_value_action ( ) ;
213198 let default_value_source = config. default_value_source ( ) ;
214199 let rules = compile_rules ( config. rules ( ) ) ;
215- let module_resolver = ModuleIdentityResolver :: new ( ) ;
216200
217201 TraceFilterEngine {
218202 config : Arc :: new ( config) ,
@@ -221,7 +205,6 @@ impl TraceFilterEngine {
221205 default_value_source,
222206 rules,
223207 cache : DashMap :: new ( ) ,
224- module_resolver,
225208 }
226209 }
227210
@@ -230,12 +213,13 @@ impl TraceFilterEngine {
230213 & self ,
231214 py : Python < ' py > ,
232215 code : & CodeObjectWrapper ,
216+ module_hint : Option < & str > ,
233217 ) -> RecorderResult < Arc < ScopeResolution > > {
234218 if let Some ( entry) = self . cache . get ( & code. id ( ) ) {
235219 return Ok ( entry. clone ( ) ) ;
236220 }
237221
238- let resolution = Arc :: new ( self . resolve_uncached ( py, code) ?) ;
222+ let resolution = Arc :: new ( self . resolve_uncached ( py, code, module_hint ) ?) ;
239223 let entry = self
240224 . cache
241225 . entry ( code. id ( ) )
@@ -247,6 +231,7 @@ impl TraceFilterEngine {
247231 & self ,
248232 py : Python < ' _ > ,
249233 code : & CodeObjectWrapper ,
234+ module_hint : Option < & str > ,
250235 ) -> RecorderResult < ScopeResolution > {
251236 let filename = code
252237 . filename ( py)
@@ -257,23 +242,31 @@ impl TraceFilterEngine {
257242
258243 let mut context = ScopeContext :: derive ( filename, self . config . sources ( ) ) ;
259244 context. refresh_object_name ( qualname) ;
260- if let Some ( absolute) = context. absolute_path . clone ( ) {
261- if let Some ( module) = self . resolve_module_name ( py, & absolute) {
262- let replace = match context. module_name . as_deref ( ) {
263- None => true ,
264- Some ( existing) => {
265- !is_valid_module_name ( existing)
266- || ( module == "__main__" && !existing. contains ( '.' ) )
267- }
268- } ;
269- if replace {
270- context. update_module_name ( module, qualname) ;
245+
246+ if let Some ( hint) = module_hint {
247+ if is_valid_module_name ( hint) {
248+ context. module_name = Some ( hint. to_string ( ) ) ;
249+ context. refresh_object_name ( qualname) ;
250+ }
251+ }
252+
253+ if context
254+ . module_name
255+ . as_ref ( )
256+ . map ( |name| !is_valid_module_name ( name) )
257+ . unwrap_or ( true )
258+ && context. module_name . is_none ( )
259+ {
260+ if let Some ( absolute) = context. absolute_path . as_deref ( ) {
261+ if let Some ( module) = module_name_from_packages ( Path :: new ( absolute) ) {
262+ context. module_name = Some ( module) ;
263+ context. refresh_object_name ( qualname) ;
264+ } else {
265+ log:: debug!(
266+ "[TraceFilter] unable to derive module name for '{}'; package selectors may not match" ,
267+ absolute
268+ ) ;
271269 }
272- } else if context. module_name . is_none ( ) {
273- log:: debug!(
274- "[TraceFilter] unable to derive module name for '{}'; package selectors may not match" ,
275- absolute
276- ) ;
277270 }
278271 }
279272
@@ -338,10 +331,6 @@ impl TraceFilterEngine {
338331 pub fn summary ( & self ) -> FilterSummary {
339332 self . config . summary ( )
340333 }
341-
342- fn resolve_module_name ( & self , py : Python < ' _ > , absolute : & str ) -> Option < String > {
343- self . module_resolver . resolve_absolute ( py, absolute)
344- }
345334}
346335
347336#[ derive( Debug , Clone ) ]
@@ -463,7 +452,8 @@ impl ScopeContext {
463452
464453 let module_name = relative_path
465454 . as_deref ( )
466- . and_then ( |rel| crate :: module_identity:: module_from_relative ( rel) ) ;
455+ . and_then ( |rel| module_from_relative ( rel) )
456+ . filter ( |name| is_valid_module_name ( name) ) ;
467457
468458 ScopeContext {
469459 module_name,
@@ -482,11 +472,6 @@ impl ScopeContext {
482472 ( None , true ) => None ,
483473 } ;
484474 }
485-
486- fn update_module_name ( & mut self , module : String , qualname : & str ) {
487- self . module_name = Some ( module) ;
488- self . refresh_object_name ( qualname) ;
489- }
490475}
491476
492477fn normalise_relative ( relative : PathBuf ) -> String {
@@ -506,6 +491,39 @@ fn normalise_relative(relative: PathBuf) -> String {
506491 components. join ( "/" )
507492}
508493
494+ fn module_name_from_packages ( path : & Path ) -> Option < String > {
495+ let mut segments: Vec < String > = Vec :: new ( ) ;
496+ let mut current = path. parent ( ) ;
497+
498+ while let Some ( dir) = current {
499+ let init = dir. join ( "__init__.py" ) ;
500+ if init. exists ( ) {
501+ if let Some ( name) = dir. file_name ( ) . and_then ( |s| s. to_str ( ) ) {
502+ if is_valid_module_name ( name) {
503+ segments. push ( name. to_string ( ) ) ;
504+ current = dir. parent ( ) ;
505+ continue ;
506+ }
507+ }
508+ }
509+ break ;
510+ }
511+
512+ segments. reverse ( ) ;
513+
514+ if let Some ( stem) = path. file_stem ( ) . and_then ( |s| s. to_str ( ) ) {
515+ if stem != "__init__" && is_valid_module_name ( stem) {
516+ segments. push ( stem. to_string ( ) ) ;
517+ }
518+ }
519+
520+ if segments. is_empty ( ) {
521+ return None ;
522+ }
523+
524+ Some ( segments. join ( "." ) )
525+ }
526+
509527fn py_attr_error ( attr : & str , err : PyErr ) -> recorder_errors:: RecorderError {
510528 target ! (
511529 ErrorCode :: FrameIntrospectionFailed ,
@@ -518,9 +536,8 @@ fn py_attr_error(attr: &str, err: PyErr) -> recorder_errors::RecorderError {
518536#[ cfg( test) ]
519537mod tests {
520538 use super :: * ;
521- use crate :: module_identity:: module_name_from_roots;
522539 use crate :: trace_filter:: config:: TraceFilterConfig ;
523- use pyo3:: types:: { PyAny , PyCode , PyDict , PyList , PyModule } ;
540+ use pyo3:: types:: { PyAny , PyCode , PyList , PyModule } ;
524541 use std:: ffi:: CString ;
525542 use std:: fs;
526543 use std:: io:: Write ;
@@ -564,7 +581,7 @@ mod tests {
564581 let wrapper = CodeObjectWrapper :: new ( py, & code_obj) ;
565582 let engine = TraceFilterEngine :: new ( config. clone ( ) ) ;
566583
567- let first = engine. resolve ( py, & wrapper) ?;
584+ let first = engine. resolve ( py, & wrapper, None ) ?;
568585 assert_eq ! ( first. exec( ) , ExecDecision :: Trace ) ;
569586 assert_eq ! ( first. matched_rule_index( ) , Some ( 0 ) ) ;
570587 assert_eq ! ( first. module_name( ) , Some ( "app.foo" ) ) ;
@@ -583,7 +600,7 @@ mod tests {
583600 ValueAction :: Allow
584601 ) ;
585602
586- let second = engine. resolve ( py, & wrapper) ?;
603+ let second = engine. resolve ( py, & wrapper, None ) ?;
587604 assert ! ( Arc :: ptr_eq( & first, & second) ) ;
588605 Ok ( ( ) )
589606 } )
@@ -619,7 +636,7 @@ mod tests {
619636 let wrapper = CodeObjectWrapper :: new ( py, & code_obj) ;
620637
621638 let engine = TraceFilterEngine :: new ( config) ;
622- let resolution = engine. resolve ( py, & wrapper) ?;
639+ let resolution = engine. resolve ( py, & wrapper, None ) ?;
623640
624641 assert_eq ! ( resolution. exec( ) , ExecDecision :: Trace ) ;
625642 assert_eq ! ( resolution. matched_rule_index( ) , Some ( 1 ) ) ;
@@ -672,13 +689,6 @@ mod tests {
672689 normalise_to_posix ( Path :: new ( file_path. to_string_lossy ( ) . as_ref ( ) ) ) . unwrap ( ) ;
673690
674691 let module = py. import ( "app.foo" ) . expect ( "import app.foo" ) ;
675- let modules_any = sys. getattr ( "modules" ) . expect ( "sys.modules" ) ;
676- let modules: Bound < ' _ , PyDict > =
677- modules_any. downcast_into :: < PyDict > ( ) . expect ( "modules dict" ) ;
678- modules
679- . set_item ( "app.foo" , module. as_any ( ) )
680- . expect ( "register module" ) ;
681-
682692 let func: Bound < ' _ , PyAny > = module. getattr ( "foo" ) . expect ( "get foo" ) ;
683693 let code_obj = func
684694 . getattr ( "__code__" )
@@ -690,18 +700,11 @@ mod tests {
690700 assert_eq ! ( recorded_filename, file_path. to_string_lossy( ) ) ;
691701
692702 let engine = TraceFilterEngine :: new ( config. clone ( ) ) ;
693- let expected_root =
694- normalise_to_posix ( Path :: new ( project_root. to_string_lossy ( ) . as_ref ( ) ) ) . unwrap ( ) ;
695- let resolver_roots = engine. module_resolver . module_roots ( ) ;
696- assert ! ( resolver_roots. iter( ) . any( |root| root == & expected_root) ) ;
697- let derived_from_roots = module_name_from_roots ( resolver_roots, absolute_path. as_str ( ) ) ;
698- assert_eq ! ( derived_from_roots, Some ( "app.foo" . to_string( ) ) ) ;
699- let resolution = engine. resolve ( py, & wrapper) ?;
703+ let resolution = engine. resolve ( py, & wrapper, None ) ?;
700704 assert_eq ! ( resolution. absolute_path( ) , Some ( absolute_path. as_str( ) ) ) ;
701705 assert_eq ! ( resolution. module_name( ) , Some ( "app.foo" ) ) ;
702706 assert_eq ! ( resolution. exec( ) , ExecDecision :: Skip ) ;
703707
704- modules. del_item ( "app.foo" ) . expect ( "remove module" ) ;
705708 sys_path. del_item ( 0 ) . expect ( "restore sys.path" ) ;
706709 Ok ( ( ) )
707710 } )
@@ -727,7 +730,7 @@ mod tests {
727730 let wrapper = CodeObjectWrapper :: new ( py, & code_obj) ;
728731
729732 let engine = TraceFilterEngine :: new ( config) ;
730- let resolution = engine. resolve ( py, & wrapper) ?;
733+ let resolution = engine. resolve ( py, & wrapper, None ) ?;
731734
732735 assert_eq ! ( resolution. exec( ) , ExecDecision :: Skip ) ;
733736 assert_eq ! ( resolution. relative_path( ) , Some ( "app/foo.py" ) ) ;
0 commit comments