1"""
2Printing tools.
3"""
4
5from __future__ import annotations
6
7from collections.abc import (
8 Callable,
9 Iterable,
10 Mapping,
11 Sequence,
12)
13import sys
14from typing import (
15 TYPE_CHECKING,
16 Any,
17 TypeAlias,
18 TypeVar,
19)
20from unicodedata import east_asian_width
21
22from pandas._config import get_option
23
24from pandas.core.dtypes.inference import is_sequence
25
26from pandas.io.formats.console import get_console_size
27
28if TYPE_CHECKING:
29 from pandas._typing import ListLike
30EscapeChars: TypeAlias = Mapping[str, str] | Iterable[str]
31_KT = TypeVar("_KT")
32_VT = TypeVar("_VT")
33
34
35def adjoin(space: int, *lists: list[str], **kwargs: Any) -> str:
36 """
37 Glues together two sets of strings using the amount of space requested.
38 The idea is to prettify.
39
40 ----------
41 space : int
42 number of spaces for padding
43 lists : str
44 list of str which being joined
45 strlen : callable
46 function used to calculate the length of each str. Needed for unicode
47 handling.
48 justfunc : callable
49 function used to justify str. Needed for unicode handling.
50 """
51 strlen = kwargs.pop("strlen", len)
52 justfunc = kwargs.pop("justfunc", _adj_justify)
53
54 newLists = []
55 lengths = [max(map(strlen, x)) + space for x in lists[:-1]]
56 # not the last one
57 lengths.append(max(map(len, lists[-1])))
58 maxLen = max(map(len, lists))
59 for i, lst in enumerate(lists):
60 nl = justfunc(lst, lengths[i], mode="left")
61 nl = ([" " * lengths[i]] * (maxLen - len(lst))) + nl
62 newLists.append(nl)
63 toJoin = zip(*newLists, strict=True)
64 return "\n".join("".join(lines) for lines in toJoin)
65
66
67def _adj_justify(texts: Iterable[str], max_len: int, mode: str = "right") -> list[str]:
68 """
69 Perform ljust, center, rjust against string or list-like
70 """
71 if mode == "left":
72 return [x.ljust(max_len) for x in texts]
73 elif mode == "center":
74 return [x.center(max_len) for x in texts]
75 else:
76 return [x.rjust(max_len) for x in texts]
77
78
79# Unicode consolidation
80# ---------------------
81#
82# pprinting utility functions for generating Unicode text or
83# bytes(3.x)/str(2.x) representations of objects.
84# Try to use these as much as possible rather than rolling your own.
85#
86# When to use
87# -----------
88#
89# 1) If you're writing code internal to pandas (no I/O directly involved),
90# use pprint_thing().
91#
92# It will always return unicode text which can handled by other
93# parts of the package without breakage.
94#
95# 2) if you need to write something out to file, use
96# pprint_thing_encoded(encoding).
97#
98# If no encoding is specified, it defaults to utf-8. Since encoding pure
99# ascii with utf-8 is a no-op you can safely use the default utf-8 if you're
100# working with straight ascii.
101
102
103def _pprint_seq(
104 seq: ListLike, _nest_lvl: int = 0, max_seq_items: int | None = None, **kwds: Any
105) -> str:
106 """
107 internal. pprinter for iterables. you should probably use pprint_thing()
108 rather than calling this directly.
109
110 bounds length of printed sequence, depending on options
111 """
112 if isinstance(seq, set):
113 fmt = "{{{body}}}"
114 elif isinstance(seq, frozenset):
115 fmt = "frozenset({{{body}}})"
116 else:
117 fmt = "[{body}]" if hasattr(seq, "__setitem__") else "({body})"
118
119 if max_seq_items is False:
120 max_items = None
121 else:
122 max_items = max_seq_items or get_option("max_seq_items") or len(seq)
123
124 s = iter(seq)
125 # handle sets, no slicing
126 r = []
127 max_items_reached = False
128 for i, item in enumerate(s):
129 if (max_items is not None) and (i >= max_items):
130 max_items_reached = True
131 break
132 r.append(pprint_thing(item, _nest_lvl + 1, max_seq_items=max_seq_items, **kwds))
133 body = ", ".join(r)
134
135 if max_items_reached:
136 body += ", ..."
137 elif isinstance(seq, tuple) and len(seq) == 1:
138 body += ","
139
140 return fmt.format(body=body)
141
142
143def _pprint_dict(
144 seq: Mapping, _nest_lvl: int = 0, max_seq_items: int | None = None, **kwds: Any
145) -> str:
146 """
147 internal. pprinter for iterables. you should probably use pprint_thing()
148 rather than calling this directly.
149 """
150 fmt = "{{{things}}}"
151 pairs = []
152
153 pfmt = "{key}: {val}"
154
155 if max_seq_items is False:
156 nitems = len(seq)
157 else:
158 nitems = max_seq_items or get_option("max_seq_items") or len(seq)
159
160 for k, v in list(seq.items())[:nitems]:
161 pairs.append(
162 pfmt.format(
163 key=pprint_thing(k, _nest_lvl + 1, max_seq_items=max_seq_items, **kwds),
164 val=pprint_thing(v, _nest_lvl + 1, max_seq_items=max_seq_items, **kwds),
165 )
166 )
167
168 if nitems < len(seq):
169 return fmt.format(things=", ".join(pairs) + ", ...")
170 else:
171 return fmt.format(things=", ".join(pairs))
172
173
174def pprint_thing(
175 thing: object,
176 _nest_lvl: int = 0,
177 escape_chars: EscapeChars | None = None,
178 default_escapes: bool = False,
179 quote_strings: bool = False,
180 max_seq_items: int | None = None,
181) -> str:
182 """
183 This function is the sanctioned way of converting objects
184 to a string representation and properly handles nested sequences.
185
186 Parameters
187 ----------
188 thing : anything to be formatted
189 _nest_lvl : internal use only. pprint_thing() is mutually-recursive
190 with pprint_sequence, this argument is used to keep track of the
191 current nesting level, and limit it.
192 escape_chars : list[str] or Mapping[str, str], optional
193 Characters to escape. If a Mapping is passed the values are the
194 replacements
195 default_escapes : bool, default False
196 Whether the input escape characters replaces or adds to the defaults
197 max_seq_items : int or None, default None
198 Pass through to other pretty printers to limit sequence printing
199
200 Returns
201 -------
202 str
203 """
204
205 def as_escaped_string(
206 thing: Any, escape_chars: EscapeChars | None = escape_chars
207 ) -> str:
208 translate = {"\t": r"\t", "\n": r"\n", "\r": r"\r", "'": r"\'"}
209 if isinstance(escape_chars, Mapping):
210 if default_escapes:
211 translate.update(escape_chars)
212 else:
213 translate = escape_chars # type: ignore[assignment]
214 escape_chars = list(escape_chars.keys())
215 else:
216 escape_chars = escape_chars or ()
217
218 result = str(thing)
219 for c in escape_chars:
220 result = result.replace(c, translate[c])
221 return result
222
223 if hasattr(thing, "__next__"):
224 return str(thing)
225 elif isinstance(thing, Mapping) and _nest_lvl < get_option(
226 "display.pprint_nest_depth"
227 ):
228 result = _pprint_dict(
229 thing, _nest_lvl, quote_strings=True, max_seq_items=max_seq_items
230 )
231 elif is_sequence(thing) and _nest_lvl < get_option("display.pprint_nest_depth"):
232 result = _pprint_seq(
233 # error: Argument 1 to "_pprint_seq" has incompatible type "object";
234 # expected "ExtensionArray | ndarray[Any, Any] | Index | Series |
235 # SequenceNotStr[Any] | range"
236 thing, # type: ignore[arg-type]
237 _nest_lvl,
238 escape_chars=escape_chars,
239 quote_strings=quote_strings,
240 max_seq_items=max_seq_items,
241 )
242 elif isinstance(thing, str) and quote_strings:
243 result = f"'{as_escaped_string(thing)}'"
244 else:
245 result = as_escaped_string(thing)
246
247 return result
248
249
250def pprint_thing_encoded(
251 object: object, encoding: str = "utf-8", errors: str = "replace"
252) -> bytes:
253 value = pprint_thing(object) # get unicode representation of object
254 return value.encode(encoding, errors)
255
256
257def enable_data_resource_formatter(enable: bool) -> None:
258 if "IPython" not in sys.modules:
259 # definitely not in IPython
260 return
261 from IPython import get_ipython
262
263 # error: Call to untyped function "get_ipython" in typed context
264 ip = get_ipython() # type: ignore[no-untyped-call]
265 if ip is None:
266 # still not in IPython
267 return
268
269 formatters = ip.display_formatter.formatters
270 mimetype = "application/vnd.dataresource+json"
271
272 if enable:
273 if mimetype not in formatters:
274 # define tableschema formatter
275 from IPython.core.formatters import BaseFormatter
276 from traitlets import ObjectName
277
278 class TableSchemaFormatter(BaseFormatter):
279 print_method = ObjectName("_repr_data_resource_")
280 _return_type = (dict,)
281
282 # register it:
283 formatters[mimetype] = TableSchemaFormatter()
284 # enable it if it's been disabled:
285 formatters[mimetype].enabled = True
286 # unregister tableschema mime-type
287 elif mimetype in formatters:
288 formatters[mimetype].enabled = False
289
290
291def default_pprint(thing: Any, max_seq_items: int | None = None) -> str:
292 return pprint_thing(
293 thing,
294 escape_chars=("\t", "\r", "\n"),
295 quote_strings=True,
296 max_seq_items=max_seq_items,
297 )
298
299
300def format_object_summary(
301 obj: ListLike,
302 formatter: Callable,
303 is_justify: bool = True,
304 name: str | None = None,
305 indent_for_name: bool = True,
306 line_break_each_value: bool = False,
307) -> str:
308 """
309 Return the formatted obj as a unicode string
310
311 Parameters
312 ----------
313 obj : object
314 must be iterable and support __getitem__
315 formatter : callable
316 string formatter for an element
317 is_justify : bool
318 should justify the display
319 name : name, optional
320 defaults to the class name of the obj
321 indent_for_name : bool, default True
322 Whether subsequent lines should be indented to
323 align with the name.
324 line_break_each_value : bool, default False
325 If True, inserts a line break for each value of ``obj``.
326 If False, only break lines when the a line of values gets wider
327 than the display width.
328
329 Returns
330 -------
331 summary string
332 """
333 display_width, _ = get_console_size()
334 if display_width is None:
335 display_width = get_option("display.width") or 80
336 if name is None:
337 name = type(obj).__name__
338
339 if indent_for_name:
340 name_len = len(name)
341 space1 = f"\n{(' ' * (name_len + 1))}"
342 space2 = f"\n{(' ' * (name_len + 2))}"
343 else:
344 space1 = "\n"
345 space2 = "\n " # space for the opening '['
346
347 n = len(obj)
348 if line_break_each_value:
349 # If we want to vertically align on each value of obj, we need to
350 # separate values by a line break and indent the values
351 sep = ",\n " + " " * len(name)
352 else:
353 sep = ","
354 max_seq_items = get_option("display.max_seq_items") or n
355
356 # are we a truncated display
357 is_truncated = n > max_seq_items
358
359 # adj can optionally handle unicode eastern asian width
360 adj = get_adjustment()
361
362 def _extend_line(
363 s: str, line: str, value: str, display_width: int, next_line_prefix: str
364 ) -> tuple[str, str]:
365 if adj.len(line.rstrip()) + adj.len(value.rstrip()) >= display_width:
366 s += line.rstrip()
367 line = next_line_prefix
368 line += value
369 return s, line
370
371 def best_len(values: list[str]) -> int:
372 if values:
373 return max(adj.len(x) for x in values)
374 else:
375 return 0
376
377 close = ", "
378
379 if n == 0:
380 summary = f"[]{close}"
381 elif n == 1 and not line_break_each_value:
382 first = formatter(obj[0])
383 summary = f"[{first}]{close}"
384 elif n == 2 and not line_break_each_value:
385 first = formatter(obj[0])
386 last = formatter(obj[-1])
387 summary = f"[{first}, {last}]{close}"
388 else:
389 if max_seq_items == 1:
390 # If max_seq_items=1 show only last element
391 head = []
392 tail = [formatter(x) for x in obj[-1:]]
393 elif n > max_seq_items:
394 n = min(max_seq_items // 2, 10)
395 head = [formatter(x) for x in obj[:n]]
396 tail = [formatter(x) for x in obj[-n:]]
397 else:
398 head = []
399 tail = [formatter(x) for x in obj]
400
401 # adjust all values to max length if needed
402 if is_justify:
403 if line_break_each_value:
404 # Justify each string in the values of head and tail, so the
405 # strings will right align when head and tail are stacked
406 # vertically.
407 head, tail = _justify(head, tail)
408 elif is_truncated or not (
409 len(", ".join(head)) < display_width
410 and len(", ".join(tail)) < display_width
411 ):
412 # Each string in head and tail should align with each other
413 max_length = max(best_len(head), best_len(tail))
414 head = [x.rjust(max_length) for x in head]
415 tail = [x.rjust(max_length) for x in tail]
416 # If we are not truncated and we are only a single
417 # line, then don't justify
418
419 if line_break_each_value:
420 # Now head and tail are of type List[Tuple[str]]. Below we
421 # convert them into List[str], so there will be one string per
422 # value. Also truncate items horizontally if wider than
423 # max_space
424 max_space = display_width - len(space2)
425 value = tail[0]
426 max_items = 1
427 for num_items in reversed(range(1, len(value) + 1)):
428 pprinted_seq = _pprint_seq(value, max_seq_items=num_items)
429 if len(pprinted_seq) < max_space:
430 max_items = num_items
431 break
432 head = [_pprint_seq(x, max_seq_items=max_items) for x in head]
433 tail = [_pprint_seq(x, max_seq_items=max_items) for x in tail]
434
435 summary = ""
436 line = space2
437
438 for head_value in head:
439 word = head_value + sep + " "
440 summary, line = _extend_line(summary, line, word, display_width, space2)
441
442 if is_truncated:
443 # remove trailing space of last line
444 summary += line.rstrip() + space2 + "..."
445 line = space2
446
447 for tail_item in tail[:-1]:
448 word = tail_item + sep + " "
449 summary, line = _extend_line(summary, line, word, display_width, space2)
450
451 # last value: no sep added + 1 space of width used for trailing ','
452 summary, line = _extend_line(summary, line, tail[-1], display_width - 2, space2)
453 summary += line
454
455 # right now close is either '' or ', '
456 # Now we want to include the ']', but not the maybe space.
457 close = "]" + close.rstrip(" ")
458 summary += close
459
460 if len(summary) > (display_width) or line_break_each_value:
461 summary += space1
462 else: # one row
463 summary += " "
464
465 # remove initial space
466 summary = "[" + summary[len(space2) :]
467
468 return summary
469
470
471def _justify(
472 head: list[Sequence[str]], tail: list[Sequence[str]]
473) -> tuple[list[tuple[str, ...]], list[tuple[str, ...]]]:
474 """
475 Justify items in head and tail, so they are right-aligned when stacked.
476
477 Parameters
478 ----------
479 head : list-like of list-likes of strings
480 tail : list-like of list-likes of strings
481
482 Returns
483 -------
484 tuple of list of tuples of strings
485 Same as head and tail, but items are right aligned when stacked
486 vertically.
487
488 Examples
489 --------
490 >>> _justify([["a", "b"]], [["abc", "abcd"]])
491 ([(' a', ' b')], [('abc', 'abcd')])
492 """
493 combined = head + tail
494
495 # For each position for the sequences in ``combined``,
496 # find the length of the largest string.
497 max_length = [0] * len(combined[0])
498 for inner_seq in combined:
499 length = [len(item) for item in inner_seq]
500 max_length = [max(x, y) for x, y in zip(max_length, length, strict=True)]
501
502 # justify each item in each list-like in head and tail using max_length
503 head_tuples = [
504 tuple(x.rjust(max_len) for x, max_len in zip(seq, max_length, strict=True))
505 for seq in head
506 ]
507 tail_tuples = [
508 tuple(x.rjust(max_len) for x, max_len in zip(seq, max_length, strict=True))
509 for seq in tail
510 ]
511 return head_tuples, tail_tuples
512
513
514class PrettyDict(dict[_KT, _VT]):
515 """Dict extension to support abbreviated __repr__"""
516
517 def __repr__(self) -> str:
518 return pprint_thing(self)
519
520
521class _TextAdjustment:
522 def __init__(self) -> None:
523 self.encoding = get_option("display.encoding")
524
525 def len(self, text: str) -> int:
526 return len(text)
527
528 def justify(self, texts: Any, max_len: int, mode: str = "right") -> list[str]:
529 """
530 Perform ljust, center, rjust against string or list-like
531 """
532 if mode == "left":
533 return [x.ljust(max_len) for x in texts]
534 elif mode == "center":
535 return [x.center(max_len) for x in texts]
536 else:
537 return [x.rjust(max_len) for x in texts]
538
539 def adjoin(self, space: int, *lists: Any, **kwargs: Any) -> str:
540 return adjoin(space, *lists, strlen=self.len, justfunc=self.justify, **kwargs)
541
542
543class _EastAsianTextAdjustment(_TextAdjustment):
544 def __init__(self) -> None:
545 super().__init__()
546 if get_option("display.unicode.ambiguous_as_wide"):
547 self.ambiguous_width = 2
548 else:
549 self.ambiguous_width = 1
550
551 # Definition of East Asian Width
552 # https://unicode.org/reports/tr11/
553 # Ambiguous width can be changed by option
554 self._EAW_MAP = {"Na": 1, "N": 1, "W": 2, "F": 2, "H": 1}
555
556 def len(self, text: str) -> int:
557 """
558 Calculate display width considering unicode East Asian Width
559 """
560 if not isinstance(text, str):
561 return len(text)
562
563 return sum(
564 self._EAW_MAP.get(east_asian_width(c), self.ambiguous_width) for c in text
565 )
566
567 def justify(
568 self, texts: Iterable[str], max_len: int, mode: str = "right"
569 ) -> list[str]:
570 # re-calculate padding space per str considering East Asian Width
571 def _get_pad(t: str) -> int:
572 return max_len - self.len(t) + len(t)
573
574 if mode == "left":
575 return [x.ljust(_get_pad(x)) for x in texts]
576 elif mode == "center":
577 return [x.center(_get_pad(x)) for x in texts]
578 else:
579 return [x.rjust(_get_pad(x)) for x in texts]
580
581
582def get_adjustment() -> _TextAdjustment:
583 use_east_asian_width = get_option("display.unicode.east_asian_width")
584 if use_east_asian_width:
585 return _EastAsianTextAdjustment()
586 else:
587 return _TextAdjustment()