2626import marshal
2727import re
2828
29+ from collections import Counter
2930from enum import Enum
3031from functools import cmp_to_key
3132from dataclasses import dataclass
3233from typing import Dict
34+ from os .path import join
35+ from pathlib import Path
3336
3437__all__ = ["Stats" , "SortKey" , "FunctionProfile" , "StatsProfile" ]
3538
@@ -278,16 +281,19 @@ def reverse_order(self):
278281 return self
279282
280283 def strip_dirs (self ):
284+ return self ._strip_directory_data (lambda fullpath_linenum_funcname : func_strip_path (fullpath_linenum_funcname ))
285+
286+ def _strip_directory_data (self , strip_function ):
281287 oldstats = self .stats
282288 self .stats = newstats = {}
283289 max_name_len = 0
284290 for func , (cc , nc , tt , ct , callers ) in oldstats .items ():
285- newfunc = func_strip_path (func )
291+ newfunc = strip_function (func )
286292 if len (func_std_string (newfunc )) > max_name_len :
287293 max_name_len = len (func_std_string (newfunc ))
288294 newcallers = {}
289295 for func2 , caller in callers .items ():
290- newcallers [func_strip_path (func2 )] = caller
296+ newcallers [strip_function (func2 )] = caller
291297
292298 if newfunc in newstats :
293299 newstats [newfunc ] = add_func_stats (
@@ -298,14 +304,30 @@ def strip_dirs(self):
298304 old_top = self .top_level
299305 self .top_level = new_top = set ()
300306 for func in old_top :
301- new_top .add (func_strip_path (func ))
307+ new_top .add (strip_function (func ))
302308
303309 self .max_name_len = max_name_len
304310
305311 self .fcn_list = None
306312 self .all_callees = None
307313 return self
308314
315+ def strip_non_unique_dirs (self ):
316+ full_paths = set ()
317+
318+ for (full_path , _ , _ ), (_ , _ , _ , _ , callers ) in self .stats .items ():
319+ full_paths .add (full_path )
320+ for (full_path_caller , _ , _ ), _ in callers .items ():
321+ full_paths .add (full_path_caller )
322+
323+ minimal_path_by_full_path = _build_minimal_path_by_full_path (full_paths )
324+
325+ def strip_function (fullpath_linenum_funcname ):
326+ fullpath , linenum , funcname = fullpath_linenum_funcname
327+ return minimal_path_by_full_path [fullpath ], linenum , funcname
328+
329+ return self ._strip_directory_data (strip_function )
330+
309331 def calc_callees (self ):
310332 if self .all_callees :
311333 return
@@ -776,4 +798,41 @@ def postcmd(self, stop, line):
776798 except KeyboardInterrupt :
777799 pass
778800
801+
802+ def _build_minimal_path_by_full_path (paths ):
803+ if not paths :
804+ return paths
805+
806+ completed = {
807+ full_path : None
808+ for full_path in paths
809+ }
810+ split_path_by_full = {full_path : Path (full_path ).parts for full_path in paths }
811+ max_step = max (len (x ) for x in split_path_by_full .values ())
812+ step = 1
813+ while step <= max_step :
814+ short_path_by_full = {
815+ full_path : split_path_by_full [full_path ][- step :]
816+ for full_path , value in completed .items ()
817+ if value is None
818+ }
819+ full_path_by_short = {v : k for k , v in short_path_by_full .items ()}
820+
821+ for short_path , count in Counter (short_path_by_full .values ()).most_common ():
822+ if count == 1 :
823+ joined_short_path = join (* short_path )
824+ # __init__.py is handled specially because it's a very common
825+ # file name which gives no clue what file is meant
826+ if joined_short_path == '__init__.py' :
827+ continue
828+ completed [full_path_by_short [short_path ]] = joined_short_path
829+
830+ step += 1
831+
832+ return {
833+ full_path : short_path if short_path is not None else full_path
834+ for full_path , short_path in completed .items ()
835+ }
836+
837+
779838# That's all, folks.
0 commit comments