2626import marshal
2727import re
2828
29+ from collections import Counter
2930from enum import StrEnum , _simple_enum
3031from functools import cmp_to_key
3132from dataclasses import dataclass
33+ from os .path import join
34+ from pathlib import Path
3235
3336__all__ = ["Stats" , "SortKey" , "FunctionProfile" , "StatsProfile" ]
3437
@@ -276,16 +279,19 @@ def reverse_order(self):
276279 return self
277280
278281 def strip_dirs (self ):
282+ return self ._strip_directory_data (lambda fullpath_linenum_funcname : func_strip_path (fullpath_linenum_funcname ))
283+
284+ def _strip_directory_data (self , strip_function ):
279285 oldstats = self .stats
280286 self .stats = newstats = {}
281287 max_name_len = 0
282288 for func , (cc , nc , tt , ct , callers ) in oldstats .items ():
283- newfunc = func_strip_path (func )
289+ newfunc = strip_function (func )
284290 if len (func_std_string (newfunc )) > max_name_len :
285291 max_name_len = len (func_std_string (newfunc ))
286292 newcallers = {}
287293 for func2 , caller in callers .items ():
288- newcallers [func_strip_path (func2 )] = caller
294+ newcallers [strip_function (func2 )] = caller
289295
290296 if newfunc in newstats :
291297 newstats [newfunc ] = add_func_stats (
@@ -296,14 +302,30 @@ def strip_dirs(self):
296302 old_top = self .top_level
297303 self .top_level = new_top = set ()
298304 for func in old_top :
299- new_top .add (func_strip_path (func ))
305+ new_top .add (strip_function (func ))
300306
301307 self .max_name_len = max_name_len
302308
303309 self .fcn_list = None
304310 self .all_callees = None
305311 return self
306312
313+ def strip_non_unique_dirs (self ):
314+ full_paths = set ()
315+
316+ for (full_path , _ , _ ), (_ , _ , _ , _ , callers ) in self .stats .items ():
317+ full_paths .add (full_path )
318+ for (full_path_caller , _ , _ ), _ in callers .items ():
319+ full_paths .add (full_path_caller )
320+
321+ minimal_path_by_full_path = _build_minimal_path_by_full_path (full_paths )
322+
323+ def strip_function (fullpath_linenum_funcname ):
324+ fullpath , linenum , funcname = fullpath_linenum_funcname
325+ return minimal_path_by_full_path [fullpath ], linenum , funcname
326+
327+ return self ._strip_directory_data (strip_function )
328+
307329 def calc_callees (self ):
308330 if self .all_callees :
309331 return
@@ -774,4 +796,41 @@ def postcmd(self, stop, line):
774796 except KeyboardInterrupt :
775797 pass
776798
799+
800+ def _build_minimal_path_by_full_path (paths ):
801+ if not paths :
802+ return paths
803+
804+ completed = {
805+ full_path : None
806+ for full_path in paths
807+ }
808+ split_path_by_full = {full_path : Path (full_path ).parts for full_path in paths }
809+ max_step = max (len (x ) for x in split_path_by_full .values ())
810+ step = 1
811+ while step <= max_step :
812+ short_path_by_full = {
813+ full_path : split_path_by_full [full_path ][- step :]
814+ for full_path , value in completed .items ()
815+ if value is None
816+ }
817+ full_path_by_short = {v : k for k , v in short_path_by_full .items ()}
818+
819+ for short_path , count in Counter (short_path_by_full .values ()).most_common ():
820+ if count == 1 :
821+ joined_short_path = join (* short_path )
822+ # __init__.py is handled specially because it's a very common
823+ # file name which gives no clue what file is meant
824+ if joined_short_path == '__init__.py' :
825+ continue
826+ completed [full_path_by_short [short_path ]] = joined_short_path
827+
828+ step += 1
829+
830+ return {
831+ full_path : short_path if short_path is not None else full_path
832+ for full_path , short_path in completed .items ()
833+ }
834+
835+
777836# That's all, folks.
0 commit comments