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