55import html
66import importlib .resources
77import json
8+ import locale
89import math
910import os
1011import platform
1516from typing import Dict , List , Tuple
1617
1718from ._css_utils import get_combined_css
19+ from ._format_utils import fmt
1820from .collector import normalize_location , extract_lineno
1921from .stack_collector import StackTraceCollector
2022
@@ -343,7 +345,7 @@ def render_hierarchical_html(self, trees: Dict[str, TreeNode]) -> str:
343345 <div class="type-header" onclick="toggleTypeSection(this)">
344346 <span class="type-icon">{ icon } </span>
345347 <span class="type-title">{ type_names [module_type ]} </span>
346- <span class="type-stats">({ tree .count } { file_word } , { tree .samples :, } { sample_word } )</span>
348+ <span class="type-stats">({ tree .count } { file_word } , { tree .samples :n } { sample_word } )</span>
347349 </div>
348350 <div class="type-content"{ content_style } >
349351'''
@@ -390,7 +392,7 @@ def _render_folder(self, node: TreeNode, name: str, level: int = 1) -> str:
390392 parts .append (f'{ indent } <span class="folder-icon">▶</span>' )
391393 parts .append (f'{ indent } <span class="folder-name">📁 { html .escape (name )} </span>' )
392394 parts .append (f'{ indent } <span class="folder-stats">'
393- f'({ node .count } { file_word } , { node .samples :, } { sample_word } )</span>' )
395+ f'({ node .count } { file_word } , { node .samples :n } { sample_word } )</span>' )
394396 parts .append (f'{ indent } </div>' )
395397 parts .append (f'{ indent } <div class="folder-content" style="display: none;">' )
396398
@@ -431,10 +433,11 @@ def _render_file_item(self, stat: FileStats, indent: str = '') -> str:
431433 bar_width = min (stat .percentage , 100 )
432434
433435 html_file = self .file_index [stat .filename ]
436+ s = "" if stat .total_samples == 1 else "s"
434437
435438 return (f'{ indent } <div class="file-item">\n '
436439 f'{ indent } <a href="{ html_file } " class="file-link" title="{ full_path } ">📄 { module_name } </a>\n '
437- f'{ indent } <span class="file-samples">{ stat .total_samples :, } samples </span>\n '
440+ f'{ indent } <span class="file-samples">{ stat .total_samples :n } sample { s } </span>\n '
438441 f'{ indent } <div class="heatmap-bar-container"><div class="heatmap-bar" style="width: { bar_width } px; height: { self .heatmap_bar_height } px;" data-intensity="{ intensity :.3f} "></div></div>\n '
439442 f'{ indent } </div>\n ' )
440443
@@ -761,7 +764,8 @@ def _print_export_summary(self, output_dir, file_stats: List[FileStats]):
761764 """Print summary of exported heatmap."""
762765 print (f"Heatmap output written to { output_dir } /" )
763766 print (f" - Index: { output_dir / 'index.html' } " )
764- print (f" - { len (file_stats )} source file(s) analyzed" )
767+ s = "" if len (file_stats ) == 1 else "s"
768+ print (f" - { len (file_stats )} source file{ s } analyzed" )
765769
766770 def _calculate_file_stats (self ) -> List [FileStats ]:
767771 """Calculate statistics for each file.
@@ -824,7 +828,7 @@ def _generate_index_html(self, index_path: Path, file_stats: List[FileStats]):
824828 # Format error rate and missed samples with bar classes
825829 error_rate = self .stats .get ('error_rate' )
826830 if error_rate is not None :
827- error_rate_str = f"{ error_rate :.1f } %"
831+ error_rate_str = f"{ fmt ( error_rate ) } %"
828832 error_rate_width = min (error_rate , 100 )
829833 # Determine bar color class based on rate
830834 if error_rate < 5 :
@@ -840,7 +844,7 @@ def _generate_index_html(self, index_path: Path, file_stats: List[FileStats]):
840844
841845 missed_samples = self .stats .get ('missed_samples' )
842846 if missed_samples is not None :
843- missed_samples_str = f"{ missed_samples :.1f } %"
847+ missed_samples_str = f"{ fmt ( missed_samples ) } %"
844848 missed_samples_width = min (missed_samples , 100 )
845849 if missed_samples < 5 :
846850 missed_samples_class = "good"
@@ -859,10 +863,10 @@ def _generate_index_html(self, index_path: Path, file_stats: List[FileStats]):
859863 "<!-- INLINE_JS -->" : f"<script>\n { self ._template_loader .index_js } \n </script>" ,
860864 "<!-- PYTHON_LOGO -->" : self ._template_loader .logo_html ,
861865 "<!-- PYTHON_VERSION -->" : f"{ sys .version_info .major } .{ sys .version_info .minor } " ,
862- "<!-- NUM_FILES -->" : str ( len (file_stats )) ,
863- "<!-- TOTAL_SAMPLES -->" : f"{ self ._total_samples :, } " ,
864- "<!-- DURATION -->" : f" { self .stats .get ('duration_sec' , 0 ):.1f } s" ,
865- "<!-- SAMPLE_RATE -->" : f" { self .stats .get ('sample_rate' , 0 ):.1f } " ,
866+ "<!-- NUM_FILES -->" : f" { len (file_stats ):n } " ,
867+ "<!-- TOTAL_SAMPLES -->" : f"{ self ._total_samples :n } " ,
868+ "<!-- DURATION -->" : fmt ( self .stats .get ('duration_sec' , 0 )) ,
869+ "<!-- SAMPLE_RATE -->" : fmt ( self .stats .get ('sample_rate' , 0 )) ,
866870 "<!-- ERROR_RATE -->" : error_rate_str ,
867871 "<!-- ERROR_RATE_WIDTH -->" : str (error_rate_width ),
868872 "<!-- ERROR_RATE_CLASS -->" : error_rate_class ,
@@ -906,12 +910,12 @@ def _generate_file_html(self, output_path: Path, filename: str,
906910 # Populate template
907911 replacements = {
908912 "<!-- FILENAME -->" : html .escape (filename ),
909- "<!-- TOTAL_SAMPLES -->" : f"{ file_stat .total_samples :, } " ,
910- "<!-- TOTAL_SELF_SAMPLES -->" : f"{ file_stat .total_self_samples :, } " ,
911- "<!-- NUM_LINES -->" : str ( file_stat .num_lines ) ,
912- "<!-- PERCENTAGE -->" : f" { file_stat .percentage :.2f } " ,
913- "<!-- MAX_SAMPLES -->" : str ( file_stat .max_samples ) ,
914- "<!-- MAX_SELF_SAMPLES -->" : str ( file_stat .max_self_samples ) ,
913+ "<!-- TOTAL_SAMPLES -->" : f"{ file_stat .total_samples :n } " ,
914+ "<!-- TOTAL_SELF_SAMPLES -->" : f"{ file_stat .total_self_samples :n } " ,
915+ "<!-- NUM_LINES -->" : f" { file_stat .num_lines :n } " ,
916+ "<!-- PERCENTAGE -->" : fmt ( file_stat .percentage , 2 ) ,
917+ "<!-- MAX_SAMPLES -->" : f" { file_stat .max_samples :n } " ,
918+ "<!-- MAX_SELF_SAMPLES -->" : f" { file_stat .max_self_samples :n } " ,
915919 "<!-- CODE_LINES -->" : '' .join (code_lines_html ),
916920 "<!-- INLINE_CSS -->" : f"<style>\n { self ._template_loader .file_css } \n </style>" ,
917921 "<!-- INLINE_JS -->" : f"<script>\n { self ._template_loader .file_js } \n </script>" ,
@@ -948,9 +952,9 @@ def _build_line_html(self, line_num: int, line_content: str,
948952 else :
949953 self_intensity = 0
950954
951- self_display = f"{ self_samples :, } " if self_samples > 0 else ""
952- cumulative_display = f"{ cumulative_samples :, } "
953- tooltip = f"Self: { self_samples :, } , Total: { cumulative_samples :, } "
955+ self_display = f"{ self_samples :n } " if self_samples > 0 else ""
956+ cumulative_display = f"{ cumulative_samples :n } "
957+ tooltip = f"Self: { self_samples :n } , Total: { cumulative_samples :n } "
954958 else :
955959 cumulative_intensity = 0
956960 self_intensity = 0
@@ -1205,7 +1209,7 @@ def _create_navigation_button(self, items_with_counts: List[Tuple[str, int, str,
12051209 file , line , func , count = valid_items [0 ]
12061210 target_html = self .file_index [file ]
12071211 nav_data = json .dumps ({'link' : f"{ target_html } #line-{ line } " , 'func' : func })
1208- title = f"Go to { btn_class } : { html .escape (func )} ({ count :, } samples)"
1212+ title = f"Go to { btn_class } : { html .escape (func )} ({ count :n } samples)"
12091213 return f'<button class="nav-btn { btn_class } " data-nav=\' { html .escape (nav_data )} \' title="{ title } ">{ arrow } </button>'
12101214
12111215 # Multiple items - create menu
@@ -1220,5 +1224,5 @@ def _create_navigation_button(self, items_with_counts: List[Tuple[str, int, str,
12201224 for file , line , func , count in valid_items
12211225 ]
12221226 items_json = html .escape (json .dumps (items_data ))
1223- title = f"{ len (items_data )} { btn_class } s ({ total_samples :, } samples)"
1227+ title = f"{ len (items_data )} { btn_class } s ({ total_samples :n } samples)"
12241228 return f'<button class="nav-btn { btn_class } " data-nav-multi=\' { items_json } \' title="{ title } ">{ arrow } </button>'
0 commit comments