1
1
"""Adapted from tabulate (https://github.com/astanin/python-tabulate) written by Sergey Astanin and contributors (MIT License)."""
2
2
3
+ from __future__ import annotations
4
+ import warnings
5
+ import wcwidth
6
+ from itertools import chain , zip_longest as izip_longest
7
+ from collections .abc import Iterable
8
+
3
9
"""Pretty-print tabular data."""
4
10
# ruff: noqa
5
11
@@ -650,128 +656,116 @@ def tabulate(
650
656
rowalign = None ,
651
657
maxheadercolwidths = None ,
652
658
):
659
+ # Shortcuts & locals
653
660
if tabular_data is None :
654
661
tabular_data = []
655
662
663
+ # 1. Normalize tabular data once
656
664
list_of_lists , headers , headers_pad = _normalize_tabular_data (tabular_data , headers , showindex = showindex )
657
- list_of_lists , separating_lines = _remove_separating_lines (list_of_lists )
665
+ list_of_lists , _ = _remove_separating_lines (list_of_lists ) # separating_lines not used
658
666
659
- # PrettyTable formatting does not use any extra padding.
660
- # Numbers are not parsed and are treated the same as strings for alignment.
661
- # Check if pretty is the format being used and override the defaults so it
662
- # does not impact other formats.
663
- min_padding = MIN_PADDING
667
+ # 2. Pre-calculate format switches (reduce repeated logic)
668
+ min_padding = 0 if tablefmt == "pretty" else MIN_PADDING
664
669
if tablefmt == "pretty" :
665
- min_padding = 0
666
670
disable_numparse = True
667
671
numalign = "center" if numalign == _DEFAULT_ALIGN else numalign
668
672
stralign = "center" if stralign == _DEFAULT_ALIGN else stralign
669
673
else :
670
674
numalign = "decimal" if numalign == _DEFAULT_ALIGN else numalign
671
675
stralign = "left" if stralign == _DEFAULT_ALIGN else stralign
672
-
673
- # 'colon_grid' uses colons in the line beneath the header to represent a column's
674
- # alignment instead of literally aligning the text differently. Hence,
675
- # left alignment of the data in the text output is enforced.
676
676
if tablefmt == "colon_grid" :
677
677
colglobalalign = "left"
678
678
headersglobalalign = "left"
679
679
680
- # optimization: look for ANSI control codes once,
681
- # enable smart width functions only if a control code is found
682
- #
683
- # convert the headers and rows into a single, tab-delimited string ensuring
684
- # that any bytestrings are decoded safely (i.e. errors ignored)
685
- plain_text = "\t " .join (
686
- chain (
687
- # headers
688
- map (_to_str , headers ),
689
- # rows: chain the rows together into a single iterable after mapping
690
- # the bytestring conversino to each cell value
691
- chain .from_iterable (map (_to_str , row ) for row in list_of_lists ),
692
- )
693
- )
694
-
680
+ # 3. Prepare plain_text for features detection
681
+ # Flatten quite efficiently
682
+ # (The main cost here is table flattening for detection. Avoid generator object cost with a one-liner.)
683
+ if headers :
684
+ iters = chain (map (_to_str , headers ), (cell for row in list_of_lists for cell in map (_to_str , row )))
685
+ else :
686
+ iters = (cell for row in list_of_lists for cell in map (_to_str , row ))
687
+ plain_text = "\t " .join (iters )
695
688
has_invisible = _ansi_codes .search (plain_text ) is not None
696
-
697
689
enable_widechars = wcwidth is not None and WIDE_CHARS_MODE
690
+ is_multiline = False
698
691
if not isinstance (tablefmt , TableFormat ) and tablefmt in multiline_formats and _is_multiline (plain_text ):
699
692
tablefmt = multiline_formats .get (tablefmt , tablefmt )
700
693
is_multiline = True
701
- else :
702
- is_multiline = False
703
694
width_fn = _choose_width_fn (has_invisible , enable_widechars , is_multiline )
704
695
705
- # format rows and columns, convert numeric values to strings
706
- cols = list (izip_longest (* list_of_lists ))
707
- numparses = _expand_numparse (disable_numparse , len (cols ))
708
- coltypes = [_column_type (col , numparse = np ) for col , np in zip (cols , numparses )]
709
- if isinstance (floatfmt , str ): # old version
710
- float_formats = len (cols ) * [floatfmt ] # just duplicate the string to use in each column
711
- else : # if floatfmt is list, tuple etc we have one per column
712
- float_formats = list (floatfmt )
713
- if len (float_formats ) < len (cols ):
714
- float_formats .extend ((len (cols ) - len (float_formats )) * [_DEFAULT_FLOATFMT ])
715
- if isinstance (intfmt , str ): # old version
716
- int_formats = len (cols ) * [intfmt ] # just duplicate the string to use in each column
717
- else : # if intfmt is list, tuple etc we have one per column
718
- int_formats = list (intfmt )
719
- if len (int_formats ) < len (cols ):
720
- int_formats .extend ((len (cols ) - len (int_formats )) * [_DEFAULT_INTFMT ])
721
- if isinstance (missingval , str ):
722
- missing_vals = len (cols ) * [missingval ]
696
+ # 4. Transpose data only once, for column-oriented transforms
697
+ # Avoid expensive list + zip + star unpacking overhead by storing list_of_lists directly
698
+ data_rows = list_of_lists
699
+ ncols = len (data_rows [0 ]) if data_rows else len (headers )
700
+ cols = [list (col ) for col in izip_longest (* data_rows , fillvalue = "" )]
701
+
702
+ # 5. Pre-compute per-column formatting parameters (avoid loop in loop)
703
+ numparses = _expand_numparse (disable_numparse , ncols )
704
+ coltypes = []
705
+ append_coltype = coltypes .append
706
+ for col , np in zip (cols , numparses ):
707
+ append_coltype (_column_type (col , numparse = np ))
708
+ float_formats = (
709
+ [floatfmt ] * ncols
710
+ if isinstance (floatfmt , str )
711
+ else list (floatfmt ) + [_DEFAULT_FLOATFMT ] * (ncols - len (floatfmt ))
712
+ )
713
+ int_formats = (
714
+ [intfmt ] * ncols if isinstance (intfmt , str ) else list (intfmt ) + [_DEFAULT_INTFMT ] * (ncols - len (intfmt ))
715
+ )
716
+ missing_vals = (
717
+ [missingval ] * ncols
718
+ if isinstance (missingval , str )
719
+ else list (missingval ) + [_DEFAULT_MISSINGVAL ] * (ncols - len (missingval ))
720
+ )
721
+
722
+ # 6. Pre-format all columns (avoid repeated conversion/type detection)
723
+ formatted_cols = []
724
+ for c , ct , fl_fmt , int_fmt , miss_v in zip (cols , coltypes , float_formats , int_formats , missing_vals ):
725
+ formatted_cols .append ([_format (v , ct , fl_fmt , int_fmt , miss_v , has_invisible ) for v in c ])
726
+
727
+ # 7. Alignment selection (avoid dict/set lookups per-column by building list-style)
728
+ if colglobalalign is not None :
729
+ aligns = [colglobalalign ] * ncols
723
730
else :
724
- missing_vals = list (missingval )
725
- if len (missing_vals ) < len (cols ):
726
- missing_vals .extend ((len (cols ) - len (missing_vals )) * [_DEFAULT_MISSINGVAL ])
727
- cols = [
728
- [_format (v , ct , fl_fmt , int_fmt , miss_v , has_invisible ) for v in c ]
729
- for c , ct , fl_fmt , int_fmt , miss_v in zip (cols , coltypes , float_formats , int_formats , missing_vals )
730
- ]
731
-
732
- # align columns
733
- # first set global alignment
734
- if colglobalalign is not None : # if global alignment provided
735
- aligns = [colglobalalign ] * len (cols )
736
- else : # default
737
731
aligns = [numalign if ct in {int , float } else stralign for ct in coltypes ]
738
- # then specific alignments
739
732
if colalign is not None :
740
- assert isinstance (colalign , Iterable )
741
733
if isinstance (colalign , str ):
742
734
warnings .warn (
743
735
f"As a string, `colalign` is interpreted as { [c for c in colalign ]} . "
744
736
f'Did you mean `colglobalalign = "{ colalign } "` or `colalign = ("{ colalign } ",)`?' ,
745
737
stacklevel = 2 ,
746
738
)
747
739
for idx , align in enumerate (colalign ):
748
- if not idx < len (aligns ):
740
+ if idx >= len (aligns ):
749
741
break
750
742
if align != "global" :
751
743
aligns [idx ] = align
752
- minwidths = [width_fn (h ) + min_padding for h in headers ] if headers else [0 ] * len (cols )
753
- aligns_copy = aligns .copy ()
754
- # Reset alignments in copy of alignments list to "left" for 'colon_grid' format,
755
- # which enforces left alignment in the text output of the data.
756
- if tablefmt == "colon_grid" :
757
- aligns_copy = ["left" ] * len (cols )
758
- cols = [
759
- _align_column (c , a , minw , has_invisible , enable_widechars , is_multiline , preserve_whitespace )
760
- for c , a , minw in zip (cols , aligns_copy , minwidths )
761
- ]
762
744
763
- aligns_headers = None
745
+ # 8. Compute minimum widths in a branch to avoid repeated expression evaluation
746
+ if headers :
747
+ # Precompute column min widths (includes header + padding)
748
+ minwidths = [width_fn (h ) + min_padding for h in headers ]
749
+ else :
750
+ minwidths = [0 ] * ncols
751
+
752
+ aligns_copy = aligns if tablefmt != "colon_grid" else ["left" ] * ncols
753
+
754
+ # 9. Align all columns (single allocation per column)
755
+ aligned_cols = []
756
+ for c , a , minw in zip (formatted_cols , aligns_copy , minwidths ):
757
+ aligned_cols .append (
758
+ _align_column (c , a , minw , has_invisible , enable_widechars , is_multiline , preserve_whitespace )
759
+ )
760
+
761
+ # 10. Handle header alignment and formatting
764
762
if headers :
765
- # align headers and add headers
766
- t_cols = cols or [["" ]] * len (headers )
767
- # first set global alignment
768
- if headersglobalalign is not None : # if global alignment provided
769
- aligns_headers = [headersglobalalign ] * len (t_cols )
770
- else : # default
763
+ t_cols = aligned_cols or [["" ]] * ncols
764
+ if headersglobalalign is not None :
765
+ aligns_headers = [headersglobalalign ] * ncols
766
+ else :
771
767
aligns_headers = aligns or [stralign ] * len (headers )
772
- # then specific header alignments
773
768
if headersalign is not None :
774
- assert isinstance (headersalign , Iterable )
775
769
if isinstance (headersalign , str ):
776
770
warnings .warn (
777
771
f"As a string, `headersalign` is interpreted as { [c for c in headersalign ]} . "
@@ -781,28 +775,47 @@ def tabulate(
781
775
)
782
776
for idx , align in enumerate (headersalign ):
783
777
hidx = headers_pad + idx
784
- if not hidx < len (aligns_headers ):
778
+ if hidx >= len (aligns_headers ):
785
779
break
786
- if align == "same" and hidx < len (aligns ): # same as column align
780
+ if align == "same" and hidx < len (aligns ):
787
781
aligns_headers [hidx ] = aligns [hidx ]
788
782
elif align != "global" :
789
783
aligns_headers [hidx ] = align
790
- minwidths = [max (minw , max (width_fn (cl ) for cl in c )) for minw , c in zip (minwidths , t_cols )]
784
+ # 1. Optimize minwidths by combining two loops into one, avoid repeated width_fn calls
785
+ for i in range (ncols ):
786
+ if t_cols [i ]:
787
+ minwidths [i ] = max (minwidths [i ], max (width_fn (x ) for x in t_cols [i ]))
788
+ # 2. Optimize headers alignment: single pass, in-place
791
789
headers = [
792
790
_align_header (h , a , minw , width_fn (h ), is_multiline , width_fn )
793
791
for h , a , minw in zip (headers , aligns_headers , minwidths )
794
792
]
795
- rows = list (zip (* cols ))
793
+ # Transpose aligned_cols for rows
794
+ rows = list (zip (* aligned_cols ))
796
795
else :
797
- minwidths = [max (width_fn (cl ) for cl in c ) for c in cols ]
798
- rows = list (zip (* cols ))
796
+ # No headers: just use widest cell for minwidth
797
+ for i in range (ncols ):
798
+ if aligned_cols [i ]:
799
+ minwidths [i ] = max (width_fn (x ) for x in aligned_cols [i ])
800
+ rows = list (zip (* aligned_cols ))
799
801
802
+ # Get TableFormat up front
800
803
if not isinstance (tablefmt , TableFormat ):
801
804
tablefmt = _table_formats .get (tablefmt , _table_formats ["simple" ])
802
805
803
806
ra_default = rowalign if isinstance (rowalign , str ) else None
804
807
rowaligns = _expand_iterable (rowalign , len (rows ), ra_default )
805
- return _format_table (tablefmt , headers , aligns_headers , rows , minwidths , aligns , is_multiline , rowaligns = rowaligns )
808
+ # 11. Table rendering (as per original logic)
809
+ return _format_table (
810
+ tablefmt ,
811
+ headers ,
812
+ aligns_headers if headers else None ,
813
+ rows ,
814
+ minwidths ,
815
+ aligns ,
816
+ is_multiline ,
817
+ rowaligns = rowaligns ,
818
+ )
806
819
807
820
808
821
def _expand_numparse (disable_numparse , column_count ):
0 commit comments