22Utilities for conversion to writer-agnostic Excel representation.
33"""
44
5- from __future__ import annotations
5+ from _future_ import annotations
66
77from collections .abc import (
88 Callable ,
1111 Mapping ,
1212 Sequence ,
1313)
14+ import pandas as pd
1415import functools
1516import itertools
1617import re
6364
6465
6566class ExcelCell :
66- __fields__ = ("row" , "col" , "val" , "style" , "mergestart" , "mergeend" )
67- __slots__ = __fields__
67+ _fields_ = ("row" , "col" , "val" , "style" , "mergestart" , "mergeend" )
68+ _slots_ = _fields_
6869
69- def __init__ (
70+ def _init_ (
7071 self ,
7172 row : int ,
7273 col : int ,
@@ -84,7 +85,7 @@ def __init__(
8485
8586
8687class CssExcelCell (ExcelCell ):
87- def __init__ (
88+ def _init_ (
8889 self ,
8990 row : int ,
9091 col : int ,
@@ -105,7 +106,7 @@ def __init__(
105106 unique_declarations = frozenset (declaration_dict .items ())
106107 style = css_converter (unique_declarations )
107108
108- super ().__init__ (row = row , col = col , val = val , style = style , ** kwargs )
109+ super ()._init_ (row = row , col = col , val = val , style = style , ** kwargs )
109110
110111
111112class CSSToExcelConverter :
@@ -116,14 +117,14 @@ class CSSToExcelConverter:
116117 focusing on font styling, backgrounds, borders and alignment.
117118
118119 Operates by first computing CSS styles in a fairly generic
119- way (see :meth:` compute_css` ) then determining Excel style
120- properties from CSS properties (see :meth:` build_xlstyle` ).
120+ way (see :meth: compute_css ) then determining Excel style
121+ properties from CSS properties (see :meth: build_xlstyle ).
121122
122123 Parameters
123124 ----------
124125 inherited : str, optional
125126 CSS declarations understood to be the containing scope for the
126- CSS processed by :meth:` __call__` .
127+ CSS processed by :meth: __call__ .
127128 """
128129
129130 NAMED_COLORS = CSS4_COLORS
@@ -183,25 +184,25 @@ class CSSToExcelConverter:
183184 ]
184185 }
185186
186- # NB: Most of the methods here could be classmethods, as only __init__
187- # and __call__ make use of instance attributes. We leave them as
187+ # NB: Most of the methods here could be classmethods, as only _init_
188+ # and _call_ make use of instance attributes. We leave them as
188189 # instancemethods so that users can easily experiment with extensions
189190 # without monkey-patching.
190191 inherited : dict [str , str ] | None
191192
192- def __init__ (self , inherited : str | None = None ) -> None :
193+ def _init_ (self , inherited : str | None = None ) -> None :
193194 if inherited is not None :
194195 self .inherited = self .compute_css (inherited )
195196 else :
196197 self .inherited = None
197- # We should avoid cache on the __call__ method.
198- # Otherwise once the method __call__ has been called
198+ # We should avoid cache on the _call_ method.
199+ # Otherwise once the method _call_ has been called
199200 # garbage collection no longer deletes the instance.
200201 self ._call_cached = functools .cache (self ._call_uncached )
201202
202203 compute_css = CSSResolver ()
203204
204- def __call__ (
205+ def _call_ (
205206 self , declarations : str | frozenset [tuple [str , str ]]
206207 ) -> dict [str , dict [str , str ]]:
207208 """
@@ -517,27 +518,27 @@ class ExcelFormatter:
517518 output row names (index)
518519 index_label : str or sequence, default None
519520 Column label for index column(s) if desired. If None is given, and
520- ` header` and ` index` are True, then the index names are used. A
521+ header and index are True, then the index names are used. A
521522 sequence should be given if the DataFrame uses MultiIndex.
522523 merge_cells : bool or 'columns', default False
523524 Format MultiIndex column headers and Hierarchical Rows as merged cells
524525 if True. Merge MultiIndex column headers only if 'columns'.
525526 .. versionchanged:: 3.0.0
526527 Added the 'columns' option.
527- inf_rep : str, default ` 'inf'`
528+ inf_rep : str, default 'inf'
528529 representation for np.inf values (which aren't representable in Excel)
529- A ` '-'` sign will be added in front of -inf.
530+ A '-' sign will be added in front of -inf.
530531 style_converter : callable, optional
531532 This translates Styler styles (CSS) into ExcelWriter styles.
532- Defaults to `` CSSToExcelConverter()` `.
533+ Defaults to ` CSSToExcelConverter() `.
533534 It should have signature css_declarations string -> excel style.
534535 This is only called for body cells.
535536 """
536537
537538 max_rows = 2 ** 20
538539 max_cols = 2 ** 14
539540
540- def __init__ (
541+ def _init_ (
541542 self ,
542543 df ,
543544 na_rep : str = "" ,
@@ -594,7 +595,11 @@ def _format_value(self, val):
594595 elif missing .isneginf_scalar (val ):
595596 val = f"-{ self .inf_rep } "
596597 elif self .float_format is not None :
597- val = float (self .float_format % val )
598+ val = self .float_format % val
599+ else :
600+ # respecter l'affichage par défaut de pandas (console)
601+ val = repr (val )
602+
598603 if getattr (val , "tzinfo" , None ) is not None :
599604 raise ValueError (
600605 "Excel does not support datetimes with "
@@ -616,7 +621,20 @@ def _format_header_mi(self) -> Iterable[ExcelCell]:
616621
617622 columns = self .columns
618623 merge_columns = self .merge_cells in {True , "columns" }
619- level_strs = columns ._format_multi (sparsify = merge_columns , include_names = False )
624+
625+ # Replace NaN column header values with a non-breaking space so
626+ # Excel output matches console display (see user's _fix_headers).
627+ NBSP = "\u00A0 "
628+ if isinstance (columns , MultiIndex ):
629+ fixed_levels = []
630+ for lvl in range (columns .nlevels ):
631+ vals = columns .get_level_values (lvl )
632+ fixed_levels .append ([NBSP if pd .isna (v ) else str (v ) for v in vals ])
633+ fixed_columns = MultiIndex .from_arrays (fixed_levels , names = columns .names )
634+ else :
635+ fixed_columns = Index ([NBSP if pd .isna (v ) else str (v ) for v in columns ], name = columns .name )
636+
637+ level_strs = fixed_columns ._format_multi (sparsify = merge_columns , include_names = False )
620638 level_lengths = get_level_lengths (level_strs )
621639 coloffset = 0
622640 lnum = 0
@@ -625,17 +643,24 @@ def _format_header_mi(self) -> Iterable[ExcelCell]:
625643 coloffset = self .df .index .nlevels - 1
626644
627645 for lnum , name in enumerate (columns .names ):
646+ val = NBSP if pd .isna (name ) else str (name )
628647 yield ExcelCell (
629648 row = lnum ,
630649 col = coloffset ,
631- val = name ,
650+ val = val ,
632651 style = None ,
633652 )
634653
635- for lnum , (spans , levels , level_codes ) in enumerate (
636- zip (level_lengths , columns .levels , columns .codes )
654+
655+
656+ # Iterate the fixed_columns levels/codes so values already have
657+ # NaNs replaced by NBSP (and are strings).
658+ for lnum , (spans , level , codes ) in enumerate (
659+ zip (level_lengths , fixed_columns .levels , fixed_columns .codes )
637660 ):
638- values = levels .take (level_codes )
661+ # level.take(codes) on fixed_columns.levels yields string values
662+ values = level .take (codes ).to_numpy ()
663+
639664 for i , span_val in spans .items ():
640665 mergestart , mergeend = None , None
641666 if merge_columns and span_val > 1 :
@@ -652,6 +677,7 @@ def _format_header_mi(self) -> Iterable[ExcelCell]:
652677 mergestart = mergestart ,
653678 mergeend = mergeend ,
654679 )
680+
655681 self .rowcounter = lnum
656682
657683 def _format_header_regular (self ) -> Iterable [ExcelCell ]:
@@ -673,6 +699,12 @@ def _format_header_regular(self) -> Iterable[ExcelCell]:
673699 )
674700 colnames = self .header
675701
702+ # Normalize NaN column labels to a non-breaking space so Excel
703+ # header output matches console display (same behavior as
704+ # applied to MultiIndex headers in _format_header_mi).
705+ NBSP = "\u00A0 "
706+ colnames = [NBSP if pd .isna (v ) else str (v ) for v in colnames ]
707+
676708 for colindex , colname in enumerate (colnames ):
677709 yield CssExcelCell (
678710 row = self .rowcounter ,
@@ -896,8 +928,8 @@ def write(
896928 is to be frozen
897929 engine : string, default None
898930 write engine to use if writer is a path - you can also set this
899- via the options `` io.excel.xlsx.writer` `,
900- or `` io.excel.xlsm.writer` `.
931+ via the options ` io.excel.xlsx.writer `,
932+ or ` io.excel.xlsm.writer `.
901933
902934 {storage_options}
903935
@@ -939,4 +971,4 @@ def write(
939971 finally :
940972 # make sure to close opened file handles
941973 if need_save :
942- writer .close ()
974+ writer .close ()
0 commit comments