11"""Adapted from tabulate (https://github.com/astanin/python-tabulate) written by Sergey Astanin and contributors (MIT License)."""
22
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+
39"""Pretty-print tabular data."""
410# ruff: noqa
511
@@ -650,128 +656,116 @@ def tabulate(
650656 rowalign = None ,
651657 maxheadercolwidths = None ,
652658):
659+ # Shortcuts & locals
653660 if tabular_data is None :
654661 tabular_data = []
655662
663+ # 1. Normalize tabular data once
656664 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
658666
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
664669 if tablefmt == "pretty" :
665- min_padding = 0
666670 disable_numparse = True
667671 numalign = "center" if numalign == _DEFAULT_ALIGN else numalign
668672 stralign = "center" if stralign == _DEFAULT_ALIGN else stralign
669673 else :
670674 numalign = "decimal" if numalign == _DEFAULT_ALIGN else numalign
671675 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.
676676 if tablefmt == "colon_grid" :
677677 colglobalalign = "left"
678678 headersglobalalign = "left"
679679
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 )
695688 has_invisible = _ansi_codes .search (plain_text ) is not None
696-
697689 enable_widechars = wcwidth is not None and WIDE_CHARS_MODE
690+ is_multiline = False
698691 if not isinstance (tablefmt , TableFormat ) and tablefmt in multiline_formats and _is_multiline (plain_text ):
699692 tablefmt = multiline_formats .get (tablefmt , tablefmt )
700693 is_multiline = True
701- else :
702- is_multiline = False
703694 width_fn = _choose_width_fn (has_invisible , enable_widechars , is_multiline )
704695
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
723730 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
737731 aligns = [numalign if ct in {int , float } else stralign for ct in coltypes ]
738- # then specific alignments
739732 if colalign is not None :
740- assert isinstance (colalign , Iterable )
741733 if isinstance (colalign , str ):
742734 warnings .warn (
743735 f"As a string, `colalign` is interpreted as { [c for c in colalign ]} . "
744736 f'Did you mean `colglobalalign = "{ colalign } "` or `colalign = ("{ colalign } ",)`?' ,
745737 stacklevel = 2 ,
746738 )
747739 for idx , align in enumerate (colalign ):
748- if not idx < len (aligns ):
740+ if idx >= len (aligns ):
749741 break
750742 if align != "global" :
751743 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- ]
762744
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
764762 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 :
771767 aligns_headers = aligns or [stralign ] * len (headers )
772- # then specific header alignments
773768 if headersalign is not None :
774- assert isinstance (headersalign , Iterable )
775769 if isinstance (headersalign , str ):
776770 warnings .warn (
777771 f"As a string, `headersalign` is interpreted as { [c for c in headersalign ]} . "
@@ -781,28 +775,47 @@ def tabulate(
781775 )
782776 for idx , align in enumerate (headersalign ):
783777 hidx = headers_pad + idx
784- if not hidx < len (aligns_headers ):
778+ if hidx >= len (aligns_headers ):
785779 break
786- if align == "same" and hidx < len (aligns ): # same as column align
780+ if align == "same" and hidx < len (aligns ):
787781 aligns_headers [hidx ] = aligns [hidx ]
788782 elif align != "global" :
789783 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
791789 headers = [
792790 _align_header (h , a , minw , width_fn (h ), is_multiline , width_fn )
793791 for h , a , minw in zip (headers , aligns_headers , minwidths )
794792 ]
795- rows = list (zip (* cols ))
793+ # Transpose aligned_cols for rows
794+ rows = list (zip (* aligned_cols ))
796795 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 ))
799801
802+ # Get TableFormat up front
800803 if not isinstance (tablefmt , TableFormat ):
801804 tablefmt = _table_formats .get (tablefmt , _table_formats ["simple" ])
802805
803806 ra_default = rowalign if isinstance (rowalign , str ) else None
804807 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+ )
806819
807820
808821def _expand_numparse (disable_numparse , column_count ):
0 commit comments