Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pandas/io/formats/string.py: 23%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

144 statements  

1""" 

2Module for formatting output data in console (to string). 

3""" 

4 

5from __future__ import annotations 

6 

7from shutil import get_terminal_size 

8from typing import TYPE_CHECKING 

9 

10import numpy as np 

11 

12from pandas.io.formats.printing import pprint_thing 

13 

14if TYPE_CHECKING: 

15 from collections.abc import Iterable 

16 

17 from pandas.io.formats.format import DataFrameFormatter 

18 

19 

20class StringFormatter: 

21 """Formatter for string representation of a dataframe.""" 

22 

23 def __init__(self, fmt: DataFrameFormatter, line_width: int | None = None) -> None: 

24 self.fmt = fmt 

25 self.adj = fmt.adj 

26 self.frame = fmt.frame 

27 self.line_width = line_width 

28 

29 def to_string(self) -> str: 

30 text = self._get_string_representation() 

31 if self.fmt.should_show_dimensions: 

32 text = f"{text}{self.fmt.dimensions_info}" 

33 return text 

34 

35 def _get_strcols(self) -> list[list[str]]: 

36 strcols = self.fmt.get_strcols() 

37 if self.fmt.is_truncated: 

38 strcols = self._insert_dot_separators(strcols) 

39 return strcols 

40 

41 def _get_string_representation(self) -> str: 

42 if self.fmt.frame.empty: 

43 return self._empty_info_line 

44 

45 strcols = self._get_strcols() 

46 

47 if self.line_width is None: 

48 # no need to wrap around just print the whole frame 

49 return self.adj.adjoin(1, *strcols) 

50 

51 if self._need_to_wrap_around: 

52 return self._join_multiline(strcols) 

53 

54 return self._fit_strcols_to_terminal_width(strcols) 

55 

56 @property 

57 def _empty_info_line(self) -> str: 

58 return ( 

59 f"Empty {type(self.frame).__name__}\n" 

60 f"Columns: {pprint_thing(self.frame.columns)}\n" 

61 f"Index: {pprint_thing(self.frame.index)}" 

62 ) 

63 

64 @property 

65 def _need_to_wrap_around(self) -> bool: 

66 return bool(self.fmt.max_cols is None or self.fmt.max_cols > 0) 

67 

68 def _insert_dot_separators(self, strcols: list[list[str]]) -> list[list[str]]: 

69 index_length = len(strcols[0]) 

70 

71 if self.fmt.is_truncated_horizontally: 

72 strcols = self._insert_dot_separator_horizontal(strcols, index_length) 

73 

74 if self.fmt.is_truncated_vertically: 

75 strcols = self._insert_dot_separator_vertical(strcols, index_length) 

76 

77 return strcols 

78 

79 @property 

80 def _adjusted_tr_col_num(self) -> int: 

81 return self.fmt.tr_col_num + 1 if self.fmt.index else self.fmt.tr_col_num 

82 

83 def _insert_dot_separator_horizontal( 

84 self, strcols: list[list[str]], index_length: int 

85 ) -> list[list[str]]: 

86 strcols.insert(self._adjusted_tr_col_num, [" ..."] * index_length) 

87 return strcols 

88 

89 def _insert_dot_separator_vertical( 

90 self, strcols: list[list[str]], index_length: int 

91 ) -> list[list[str]]: 

92 n_header_rows = index_length - len(self.fmt.tr_frame) 

93 row_num = self.fmt.tr_row_num 

94 for ix, col in enumerate(strcols): 

95 cwidth = self.adj.len(col[row_num]) 

96 

97 if self.fmt.is_truncated_horizontally: 

98 is_dot_col = ix == self._adjusted_tr_col_num 

99 else: 

100 is_dot_col = False 

101 

102 if cwidth > 3 or is_dot_col: 

103 dots = "..." 

104 else: 

105 dots = ".." 

106 

107 if ix == 0 and self.fmt.index: 

108 dot_mode = "left" 

109 elif is_dot_col: 

110 cwidth = 4 

111 dot_mode = "right" 

112 else: 

113 dot_mode = "right" 

114 

115 dot_str = self.adj.justify([dots], cwidth, mode=dot_mode)[0] 

116 col.insert(row_num + n_header_rows, dot_str) 

117 return strcols 

118 

119 def _join_multiline(self, strcols_input: Iterable[list[str]]) -> str: 

120 lwidth = self.line_width 

121 adjoin_width = 1 

122 strcols = list(strcols_input) 

123 

124 if self.fmt.index: 

125 idx = strcols.pop(0) 

126 lwidth -= np.array([self.adj.len(x) for x in idx]).max() + adjoin_width 

127 

128 col_widths = [ 

129 np.array([self.adj.len(x) for x in col]).max() if len(col) > 0 else 0 

130 for col in strcols 

131 ] 

132 

133 assert lwidth is not None 

134 col_bins = _binify(col_widths, lwidth) 

135 nbins = len(col_bins) 

136 

137 str_lst = [] 

138 start = 0 

139 for i, end in enumerate(col_bins): 

140 row = strcols[start:end] 

141 if self.fmt.index: 

142 row.insert(0, idx) 

143 if nbins > 1: 

144 nrows = len(row[-1]) 

145 if end <= len(strcols) and i < nbins - 1: 

146 row.append([" \\"] + [" "] * (nrows - 1)) 

147 else: 

148 row.append([" "] * nrows) 

149 str_lst.append(self.adj.adjoin(adjoin_width, *row)) 

150 start = end 

151 return "\n\n".join(str_lst) 

152 

153 def _fit_strcols_to_terminal_width(self, strcols: list[list[str]]) -> str: 

154 col_lens = [max(self.adj.len(x) for x in col) if col else 0 for col in strcols] 

155 n_cols = len(col_lens) 

156 # total width = sum of column widths + adjoin spacing (1 per gap) 

157 total_width = sum(col_lens) + n_cols - 1 

158 

159 width, _ = get_terminal_size() 

160 dif = total_width - width 

161 # '+ 1' to avoid too wide repr (GH PR #17023) 

162 adj_dif = dif + 1 

163 

164 if adj_dif <= 0: 

165 # All columns fit; no truncation needed and max_cols_fitted 

166 # stays at the value set by _calc_max_cols_fitted (0), so 

167 # is_truncated_horizontally remains False. 

168 return self.adj.adjoin(1, *strcols) 

169 

170 # Account for the " ..." separator column that will be inserted 

171 # after horizontal truncation (GH#32461) 

172 adj_dif += 4 + 1 # 4 chars for " ..." + 1 adjoin spacing 

173 

174 while adj_dif > 0 and n_cols > 1: 

175 mid = round(n_cols / 2) 

176 # adjoin adds one 

177 adj_dif -= col_lens.pop(mid) + 1 

178 n_cols -= 1 

179 

180 # subtract index column 

181 max_cols_fitted = n_cols - self.fmt.index 

182 # GH-21180. Ensure that we print at least two. 

183 max_cols_fitted = max(max_cols_fitted, 2) 

184 self.fmt.max_cols_fitted = max_cols_fitted 

185 

186 # Reuse already-formatted columns instead of re-truncating and 

187 # re-formatting the entire DataFrame. 

188 num_data_cols = len(strcols) - self.fmt.index 

189 if max_cols_fitted >= num_data_cols: 

190 # All data columns fit (can happen due to the max(..., 2) above) 

191 return self.adj.adjoin(1, *strcols) 

192 

193 col_num = max_cols_fitted // 2 

194 has_index = self.fmt.index 

195 if has_index: 

196 # strcols[0] is the index 

197 kept = strcols[: col_num + 1] + strcols[-col_num:] 

198 insert_pos = col_num + 1 

199 else: 

200 kept = strcols[:col_num] + strcols[-col_num:] 

201 insert_pos = col_num 

202 

203 # Insert horizontal "..." separator column 

204 row_count = len(kept[0]) if kept else 0 

205 kept.insert(insert_pos, [" ..."] * row_count) 

206 

207 return self.adj.adjoin(1, *kept) 

208 

209 

210def _binify(cols: list[int], line_width: int) -> list[int]: 

211 adjoin_width = 1 

212 bins = [] 

213 curr_width = 0 

214 i_last_column = len(cols) - 1 

215 for i, w in enumerate(cols): 

216 w_adjoined = w + adjoin_width 

217 curr_width += w_adjoined 

218 if i_last_column == i: 

219 wrap = curr_width + 1 > line_width and i > 0 

220 else: 

221 wrap = curr_width + 2 > line_width and i > 0 

222 if wrap: 

223 bins.append(i) 

224 curr_width = w_adjoined 

225 

226 bins.append(len(cols)) 

227 return bins