2
2
Utilities for conversion to writer-agnostic Excel representation.
3
3
"""
4
4
5
- from __future__ import annotations
5
+ from _future_ import annotations
6
6
7
7
from collections .abc import (
8
8
Callable ,
11
11
Mapping ,
12
12
Sequence ,
13
13
)
14
+ import pandas as pd
14
15
import functools
15
16
import itertools
16
17
import re
63
64
64
65
65
66
class ExcelCell :
66
- __fields__ = ("row" , "col" , "val" , "style" , "mergestart" , "mergeend" )
67
- __slots__ = __fields__
67
+ _fields_ = ("row" , "col" , "val" , "style" , "mergestart" , "mergeend" )
68
+ _slots_ = _fields_
68
69
69
- def __init__ (
70
+ def _init_ (
70
71
self ,
71
72
row : int ,
72
73
col : int ,
@@ -84,7 +85,7 @@ def __init__(
84
85
85
86
86
87
class CssExcelCell (ExcelCell ):
87
- def __init__ (
88
+ def _init_ (
88
89
self ,
89
90
row : int ,
90
91
col : int ,
@@ -105,7 +106,7 @@ def __init__(
105
106
unique_declarations = frozenset (declaration_dict .items ())
106
107
style = css_converter (unique_declarations )
107
108
108
- super ().__init__ (row = row , col = col , val = val , style = style , ** kwargs )
109
+ super ()._init_ (row = row , col = col , val = val , style = style , ** kwargs )
109
110
110
111
111
112
class CSSToExcelConverter :
@@ -116,14 +117,14 @@ class CSSToExcelConverter:
116
117
focusing on font styling, backgrounds, borders and alignment.
117
118
118
119
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 ).
121
122
122
123
Parameters
123
124
----------
124
125
inherited : str, optional
125
126
CSS declarations understood to be the containing scope for the
126
- CSS processed by :meth:` __call__` .
127
+ CSS processed by :meth: __call__ .
127
128
"""
128
129
129
130
NAMED_COLORS = CSS4_COLORS
@@ -183,25 +184,25 @@ class CSSToExcelConverter:
183
184
]
184
185
}
185
186
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
188
189
# instancemethods so that users can easily experiment with extensions
189
190
# without monkey-patching.
190
191
inherited : dict [str , str ] | None
191
192
192
- def __init__ (self , inherited : str | None = None ) -> None :
193
+ def _init_ (self , inherited : str | None = None ) -> None :
193
194
if inherited is not None :
194
195
self .inherited = self .compute_css (inherited )
195
196
else :
196
197
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
199
200
# garbage collection no longer deletes the instance.
200
201
self ._call_cached = functools .cache (self ._call_uncached )
201
202
202
203
compute_css = CSSResolver ()
203
204
204
- def __call__ (
205
+ def _call_ (
205
206
self , declarations : str | frozenset [tuple [str , str ]]
206
207
) -> dict [str , dict [str , str ]]:
207
208
"""
@@ -517,27 +518,27 @@ class ExcelFormatter:
517
518
output row names (index)
518
519
index_label : str or sequence, default None
519
520
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
521
522
sequence should be given if the DataFrame uses MultiIndex.
522
523
merge_cells : bool or 'columns', default False
523
524
Format MultiIndex column headers and Hierarchical Rows as merged cells
524
525
if True. Merge MultiIndex column headers only if 'columns'.
525
526
.. versionchanged:: 3.0.0
526
527
Added the 'columns' option.
527
- inf_rep : str, default ` 'inf'`
528
+ inf_rep : str, default 'inf'
528
529
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.
530
531
style_converter : callable, optional
531
532
This translates Styler styles (CSS) into ExcelWriter styles.
532
- Defaults to `` CSSToExcelConverter()` `.
533
+ Defaults to ` CSSToExcelConverter() `.
533
534
It should have signature css_declarations string -> excel style.
534
535
This is only called for body cells.
535
536
"""
536
537
537
538
max_rows = 2 ** 20
538
539
max_cols = 2 ** 14
539
540
540
- def __init__ (
541
+ def _init_ (
541
542
self ,
542
543
df ,
543
544
na_rep : str = "" ,
@@ -594,7 +595,11 @@ def _format_value(self, val):
594
595
elif missing .isneginf_scalar (val ):
595
596
val = f"-{ self .inf_rep } "
596
597
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
+
598
603
if getattr (val , "tzinfo" , None ) is not None :
599
604
raise ValueError (
600
605
"Excel does not support datetimes with "
@@ -616,7 +621,20 @@ def _format_header_mi(self) -> Iterable[ExcelCell]:
616
621
617
622
columns = self .columns
618
623
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 )
620
638
level_lengths = get_level_lengths (level_strs )
621
639
coloffset = 0
622
640
lnum = 0
@@ -625,17 +643,24 @@ def _format_header_mi(self) -> Iterable[ExcelCell]:
625
643
coloffset = self .df .index .nlevels - 1
626
644
627
645
for lnum , name in enumerate (columns .names ):
646
+ val = NBSP if pd .isna (name ) else str (name )
628
647
yield ExcelCell (
629
648
row = lnum ,
630
649
col = coloffset ,
631
- val = name ,
650
+ val = val ,
632
651
style = None ,
633
652
)
634
653
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 )
637
660
):
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
+
639
664
for i , span_val in spans .items ():
640
665
mergestart , mergeend = None , None
641
666
if merge_columns and span_val > 1 :
@@ -652,6 +677,7 @@ def _format_header_mi(self) -> Iterable[ExcelCell]:
652
677
mergestart = mergestart ,
653
678
mergeend = mergeend ,
654
679
)
680
+
655
681
self .rowcounter = lnum
656
682
657
683
def _format_header_regular (self ) -> Iterable [ExcelCell ]:
@@ -673,6 +699,12 @@ def _format_header_regular(self) -> Iterable[ExcelCell]:
673
699
)
674
700
colnames = self .header
675
701
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
+
676
708
for colindex , colname in enumerate (colnames ):
677
709
yield CssExcelCell (
678
710
row = self .rowcounter ,
@@ -896,8 +928,8 @@ def write(
896
928
is to be frozen
897
929
engine : string, default None
898
930
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 `.
901
933
902
934
{storage_options}
903
935
@@ -939,4 +971,4 @@ def write(
939
971
finally :
940
972
# make sure to close opened file handles
941
973
if need_save :
942
- writer .close ()
974
+ writer .close ()
0 commit comments