Skip to main content

rich/
pretty.rs

1//! Port of upstream rich/pretty.py.
2//!
3//! Upstream pretty-prints a *Python* object: `traverse` walks it into a tree
4//! of [`Node`]s, and [`Node::render`] lays the tree out to fit a width,
5//! expanding containers onto their own lines only where they do not fit.
6//! Rust has no runtime reflection, so the object is described by a
7//! [`PrettyValue`] (Python's `None`, `bool`, `int`, `float`, `str`, `bytes`,
8//! `list`, `tuple`, `set`, `frozenset`, `dict`, and objects with a
9//! dataclass, namedtuple or `__rich_repr__` repr). From there, `traverse`,
10//! `Node`, `_Line` and [`pretty_repr`] follow upstream line for line, and
11//! [`Pretty::from_value`] renders byte for byte what upstream's `Pretty`
12//! renders for the same object (golden `parity_docs.tsv`).
13//!
14//! [`Pretty::new`] and [`Pretty::compact`] are the older Rust-native form: they
15//! format any value with its [`Debug`] implementation (`{:#?}` / `{:?}`) and
16//! colour that with the [`ReprHighlighter`]. Debug spellings differ from
17//! Python's repr (`true`, `"a"`), so that form is not parity (DIVERGENCES
18//! §19).
19
20use std::fmt::Debug;
21
22use crate::cells::cell_len;
23use crate::console::{Console, ConsoleOptions, Justify, Overflow};
24use crate::highlighter::ReprHighlighter;
25use crate::measure::Measurement;
26use crate::protocol::{Highlighter, Renderable};
27use crate::pyformat::float_repr;
28use crate::segment::Segment;
29use crate::text::Text;
30
31/// A Python value, as `traverse` sees it. Mirrors the types upstream's
32/// `pretty.py` knows how to expand (`_BRACES`, dataclasses, namedtuples and
33/// `__rich_repr__`); anything else is an atom with its own repr
34/// ([`PrettyValue::Repr`]).
35#[derive(Clone, Debug, PartialEq)]
36pub enum PrettyValue {
37    /// `None`.
38    None,
39    /// `True` / `False`.
40    Bool(bool),
41    /// An `int`. Larger integers can be given as [`PrettyValue::Repr`].
42    Int(i128),
43    /// A `float`, written as Python's `repr` writes it.
44    Float(f64),
45    /// A `str`, written as Python's `repr` writes it (and cut by `max_string`).
46    Str(String),
47    /// A `bytes`, written as Python's `repr` writes it (and cut by `max_string`).
48    Bytes(Vec<u8>),
49    /// Any other atom: the text is its repr, used as it is.
50    Repr(String),
51    /// A `list`.
52    List(Vec<PrettyValue>),
53    /// A `tuple` (a tuple of one is written `(x,)`).
54    Tuple(Vec<PrettyValue>),
55    /// A `set`, its items in iteration order.
56    Set(Vec<PrettyValue>),
57    /// A `frozenset`, its items in iteration order.
58    FrozenSet(Vec<PrettyValue>),
59    /// A `dict`, its items in insertion order.
60    Dict(Vec<(PrettyValue, PrettyValue)>),
61    /// An object with a structured repr: a dataclass, a namedtuple or a
62    /// `__rich_repr__`.
63    Object(PrettyObject),
64}
65
66/// Which of upstream's structured reprs an object has.
67#[derive(Clone, Copy, Debug, PartialEq, Eq)]
68pub enum ObjectRepr {
69    /// `__rich_repr__`: `Name(a, b=1)`, or `<Name a b=1>` when `angular`.
70    RichRepr { angular: bool },
71    /// A dataclass with its generated repr: `Name(a=1)`.
72    Dataclass,
73    /// A namedtuple with its default repr: `Name(a=1)`.
74    NamedTuple,
75}
76
77/// An object with a structured repr: a class name and its fields, each
78/// positional (`None` key) or named.
79#[derive(Clone, Debug, PartialEq)]
80pub struct PrettyObject {
81    pub name: String,
82    pub fields: Vec<(Option<String>, PrettyValue)>,
83    pub kind: ObjectRepr,
84}
85
86impl PrettyObject {
87    /// An object with the given class name, structured repr and fields.
88    pub fn new(
89        name: impl Into<String>,
90        kind: ObjectRepr,
91        fields: Vec<(Option<String>, PrettyValue)>,
92    ) -> Self {
93        PrettyObject {
94            name: name.into(),
95            fields,
96            kind,
97        }
98    }
99}
100
101impl From<&serde_json::Value> for PrettyValue {
102    /// The value `json.loads` makes of a JSON document: `null` is `None`,
103    /// objects are `dict`s in document order, arrays are `list`s, integers
104    /// are `int`s and other numbers `float`s.
105    fn from(value: &serde_json::Value) -> Self {
106        use serde_json::Value;
107        match value {
108            Value::Null => PrettyValue::None,
109            Value::Bool(b) => PrettyValue::Bool(*b),
110            Value::Number(n) => {
111                if let Some(i) = n.as_i64() {
112                    PrettyValue::Int(i128::from(i))
113                } else if let Some(u) = n.as_u64() {
114                    PrettyValue::Int(i128::from(u))
115                } else {
116                    PrettyValue::Float(n.as_f64().unwrap_or(f64::NAN))
117                }
118            }
119            Value::String(s) => PrettyValue::Str(s.clone()),
120            Value::Array(items) => PrettyValue::List(items.iter().map(Into::into).collect()),
121            Value::Object(map) => PrettyValue::Dict(
122                map.iter()
123                    .map(|(k, v)| (PrettyValue::Str(k.clone()), v.into()))
124                    .collect(),
125            ),
126        }
127    }
128}
129
130/// Python's `str.isprintable` for one character, as `repr` uses it.
131///
132/// Covers the control (Cc), format (Cf), separator (Zs other than the space,
133/// Zl, Zp) and private-use (Co) categories. Unassigned code points (Cn)
134/// count as printable here; Python escapes those too.
135fn is_printable(ch: char) -> bool {
136    let c = ch as u32;
137    !matches!(c,
138        0x00..=0x1F | 0x7F..=0xA0 | 0xAD
139        | 0x600..=0x605 | 0x61C | 0x6DD | 0x70F | 0x890..=0x891 | 0x8E2
140        | 0x1680 | 0x180E
141        | 0x2000..=0x200F | 0x2028..=0x202F | 0x205F..=0x2064 | 0x2066..=0x206F
142        | 0x3000 | 0xE000..=0xF8FF | 0xFEFF | 0xFFF9..=0xFFFB
143        | 0x110BD | 0x110CD | 0x13430..=0x1343F | 0x1BCA0..=0x1BCA3
144        | 0x1D173..=0x1D17A | 0xE0001 | 0xE0020..=0xE007F
145        | 0xF0000..=0xFFFFD | 0x100000..=0x10FFFD)
146}
147
148/// Python's `repr()` of a `str`.
149pub fn str_repr(value: &str) -> String {
150    let quote = if value.contains('\'') && !value.contains('"') {
151        '"'
152    } else {
153        '\''
154    };
155    let mut out = String::with_capacity(value.len() + 2);
156    out.push(quote);
157    for ch in value.chars() {
158        match ch {
159            '\\' => out.push_str("\\\\"),
160            '\n' => out.push_str("\\n"),
161            '\r' => out.push_str("\\r"),
162            '\t' => out.push_str("\\t"),
163            ch if ch == quote => {
164                out.push('\\');
165                out.push(ch);
166            }
167            ch if !is_printable(ch) => {
168                let c = ch as u32;
169                if c < 0x100 {
170                    out.push_str(&format!("\\x{c:02x}"));
171                } else if c < 0x10000 {
172                    out.push_str(&format!("\\u{c:04x}"));
173                } else {
174                    out.push_str(&format!("\\U{c:08x}"));
175                }
176            }
177            ch => out.push(ch),
178        }
179    }
180    out.push(quote);
181    out
182}
183
184/// Python's `repr()` of a `bytes`.
185pub fn bytes_repr(value: &[u8]) -> String {
186    let quote = if value.contains(&b'\'') && !value.contains(&b'"') {
187        b'"'
188    } else {
189        b'\''
190    };
191    let mut out = String::with_capacity(value.len() + 3);
192    out.push('b');
193    out.push(quote as char);
194    for &byte in value {
195        match byte {
196            b'\\' => out.push_str("\\\\"),
197            b'\n' => out.push_str("\\n"),
198            b'\r' => out.push_str("\\r"),
199            b'\t' => out.push_str("\\t"),
200            b if b == quote => {
201                out.push('\\');
202                out.push(b as char);
203            }
204            0x20..=0x7E => out.push(byte as char),
205            _ => out.push_str(&format!("\\x{byte:02x}")),
206        }
207    }
208    out.push(quote as char);
209    out
210}
211
212/// A node in a repr tree. May be atomic or a container. Port of
213/// `rich.pretty.Node`.
214#[derive(Clone, Debug, PartialEq)]
215pub struct Node {
216    pub key_repr: String,
217    pub value_repr: String,
218    pub open_brace: String,
219    pub close_brace: String,
220    pub empty: String,
221    pub last: bool,
222    pub is_tuple: bool,
223    pub is_namedtuple: bool,
224    pub children: Option<Vec<Node>>,
225    pub key_separator: String,
226    pub separator: String,
227}
228
229impl Default for Node {
230    fn default() -> Self {
231        Node {
232            key_repr: String::new(),
233            value_repr: String::new(),
234            open_brace: String::new(),
235            close_brace: String::new(),
236            empty: String::new(),
237            last: false,
238            is_tuple: false,
239            is_namedtuple: false,
240            children: None,
241            key_separator: ": ".to_string(),
242            separator: ", ".to_string(),
243        }
244    }
245}
246
247impl Node {
248    fn atom(value_repr: impl Into<String>) -> Node {
249        Node {
250            value_repr: value_repr.into(),
251            ..Node::default()
252        }
253    }
254
255    /// Visit the tokens of this node in order; stops early, returning
256    /// `false`, when `visit` does. Port of `Node.iter_tokens`.
257    fn visit_tokens(&self, visit: &mut dyn FnMut(&str) -> bool) -> bool {
258        if !self.key_repr.is_empty() && !(visit(&self.key_repr) && visit(&self.key_separator)) {
259            return false;
260        }
261        if !self.value_repr.is_empty() {
262            return visit(&self.value_repr);
263        }
264        if let Some(children) = &self.children {
265            if children.is_empty() {
266                return visit(&self.empty);
267            }
268            if !visit(&self.open_brace) {
269                return false;
270            }
271            if self.is_tuple && !self.is_namedtuple && children.len() == 1 {
272                if !(children[0].visit_tokens(visit) && visit(",")) {
273                    return false;
274                }
275            } else {
276                for child in children {
277                    if !child.visit_tokens(visit) {
278                        return false;
279                    }
280                    if !child.last && !visit(&self.separator) {
281                        return false;
282                    }
283                }
284            }
285            return visit(&self.close_brace);
286        }
287        true
288    }
289
290    /// Whether the node fits in `max_length` cells after `start_length`.
291    /// Port of `Node.check_length`.
292    pub fn check_length(&self, start_length: usize, max_length: usize) -> bool {
293        let mut total_length = start_length;
294        self.visit_tokens(&mut |token| {
295            total_length += cell_len(token);
296            total_length <= max_length
297        })
298    }
299
300    /// Render the node to a pretty repr, expanding containers that do not
301    /// fit in `max_width` (or all of them with `expand_all`). Port of
302    /// `Node.render`.
303    pub fn render(&self, max_width: usize, indent_size: usize, expand_all: bool) -> String {
304        let mut lines = vec![Line {
305            node: Some(self),
306            ..Line::default()
307        }];
308        let mut line_no = 0;
309        while line_no < lines.len() {
310            let line = &lines[line_no];
311            if line.expandable() && (expand_all || !line.check_length(max_width)) {
312                let expanded = line.expand(indent_size);
313                lines.splice(line_no..=line_no, expanded);
314            }
315            line_no += 1;
316        }
317        lines
318            .iter()
319            .map(Line::to_string)
320            .collect::<Vec<_>>()
321            .join("\n")
322    }
323}
324
325impl std::fmt::Display for Node {
326    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
327        let mut result = Ok(());
328        self.visit_tokens(&mut |token| {
329            result = f.write_str(token);
330            result.is_ok()
331        });
332        result
333    }
334}
335
336/// A line in repr output. Port of `rich.pretty._Line`.
337#[derive(Default)]
338struct Line<'a> {
339    node: Option<&'a Node>,
340    text: String,
341    suffix: String,
342    whitespace: String,
343    last: bool,
344}
345
346impl<'a> Line<'a> {
347    fn expandable(&self) -> bool {
348        self.node
349            .and_then(|node| node.children.as_ref())
350            .is_some_and(|children| !children.is_empty())
351    }
352
353    fn check_length(&self, max_length: usize) -> bool {
354        let start_length = self.whitespace.len() + cell_len(&self.text) + cell_len(&self.suffix);
355        self.node
356            .expect("an expandable line has a node")
357            .check_length(start_length, max_length)
358    }
359
360    fn expand(&self, indent_size: usize) -> Vec<Line<'a>> {
361        let node = self.node.expect("an expandable line has a node");
362        let children = node.children.as_ref().expect("expandable");
363        let whitespace = self.whitespace.clone();
364        let text = if node.key_repr.is_empty() {
365            node.open_brace.clone()
366        } else {
367            format!("{}{}{}", node.key_repr, node.key_separator, node.open_brace)
368        };
369        let mut lines = vec![Line {
370            text,
371            whitespace: whitespace.clone(),
372            ..Line::default()
373        }];
374        let child_whitespace = format!("{whitespace}{}", " ".repeat(indent_size));
375        let tuple_of_one = node.is_tuple && children.len() == 1;
376        let count = children.len();
377        for (index, child) in children.iter().enumerate() {
378            let last = index + 1 == count;
379            lines.push(Line {
380                node: Some(child),
381                whitespace: child_whitespace.clone(),
382                suffix: if tuple_of_one {
383                    ",".to_string()
384                } else {
385                    node.separator.clone()
386                },
387                last: last && !tuple_of_one,
388                ..Line::default()
389            });
390        }
391        lines.push(Line {
392            text: node.close_brace.clone(),
393            whitespace,
394            suffix: self.suffix.clone(),
395            last: self.last,
396            ..Line::default()
397        });
398        lines
399    }
400}
401
402impl std::fmt::Display for Line<'_> {
403    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
404        write!(f, "{}{}", self.whitespace, self.text)?;
405        if let Some(node) = self.node {
406            write!(f, "{node}")?;
407        }
408        if !self.last {
409            f.write_str(self.suffix.trim_end())?;
410        }
411        Ok(())
412    }
413}
414
415/// Limits for [`traverse`] and [`pretty_repr`]; `None` disables each.
416#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
417pub struct ReprLimits {
418    /// Maximum length of containers before abbreviating (`... +N`).
419    pub max_length: Option<usize>,
420    /// Maximum length of strings and bytes before truncating (`'ab'+3`).
421    pub max_string: Option<usize>,
422    /// Maximum depth of nested data structures (`[...]`).
423    pub max_depth: Option<usize>,
424}
425
426/// Python's `repr()` of a value (no limits, on one line).
427pub fn py_repr(value: &PrettyValue) -> String {
428    traverse(value, ReprLimits::default()).to_string()
429}
430
431/// `to_repr`: the repr of an atom, cut to `max_string` for `str` and `bytes`.
432fn to_repr(value: &PrettyValue, max_string: Option<usize>) -> String {
433    match value {
434        PrettyValue::Str(s) => match max_string {
435            Some(max) if s.chars().count() > max => {
436                let truncated = s.chars().count() - max;
437                let head: String = s.chars().take(max).collect();
438                format!("{}+{truncated}", str_repr(&head))
439            }
440            _ => str_repr(s),
441        },
442        PrettyValue::Bytes(b) => match max_string {
443            Some(max) if b.len() > max => {
444                format!("{}+{}", bytes_repr(&b[..max]), b.len() - max)
445            }
446            _ => bytes_repr(b),
447        },
448        PrettyValue::None => "None".to_string(),
449        PrettyValue::Bool(true) => "True".to_string(),
450        PrettyValue::Bool(false) => "False".to_string(),
451        PrettyValue::Int(i) => i.to_string(),
452        PrettyValue::Float(f) => float_repr(*f),
453        PrettyValue::Repr(r) => r.clone(),
454        other => py_repr(other),
455    }
456}
457
458/// Walk a value into a tree of [`Node`]s. Port of `rich.pretty.traverse`.
459pub fn traverse(value: &PrettyValue, limits: ReprLimits) -> Node {
460    traverse_node(value, true, 0, limits)
461}
462
463fn traverse_node(value: &PrettyValue, root: bool, depth: usize, limits: ReprLimits) -> Node {
464    let reached_max_depth = limits.max_depth.is_some_and(|max| depth >= max);
465    let mut node = match value {
466        PrettyValue::Object(object) => traverse_object(object, root, depth, limits),
467        PrettyValue::List(items)
468        | PrettyValue::Tuple(items)
469        | PrettyValue::Set(items)
470        | PrettyValue::FrozenSet(items) => {
471            let (open_brace, close_brace, empty) = match value {
472                PrettyValue::List(_) => ("[", "]", "[]"),
473                PrettyValue::Tuple(_) => ("(", ")", "()"),
474                PrettyValue::Set(_) => ("{", "}", "set()"),
475                _ => ("frozenset({", "})", "frozenset()"),
476            };
477            if reached_max_depth {
478                Node::atom(format!("{open_brace}...{close_brace}"))
479            } else if items.is_empty() {
480                Node {
481                    empty: empty.to_string(),
482                    children: Some(Vec::new()),
483                    last: root,
484                    ..Node::default()
485                }
486            } else {
487                let last_item_index = items.len() - 1;
488                let shown = limits.max_length.unwrap_or(usize::MAX);
489                let mut children: Vec<Node> = items
490                    .iter()
491                    .take(shown)
492                    .enumerate()
493                    .map(|(index, child)| {
494                        let mut child_node = traverse_node(child, false, depth + 1, limits);
495                        child_node.last = index == last_item_index;
496                        child_node
497                    })
498                    .collect();
499                push_more(&mut children, items.len(), limits.max_length);
500                Node {
501                    open_brace: open_brace.to_string(),
502                    close_brace: close_brace.to_string(),
503                    children: Some(children),
504                    last: root,
505                    ..Node::default()
506                }
507            }
508        }
509        PrettyValue::Dict(items) => {
510            if reached_max_depth {
511                Node::atom("{...}")
512            } else if items.is_empty() {
513                Node {
514                    empty: "{}".to_string(),
515                    children: Some(Vec::new()),
516                    last: root,
517                    ..Node::default()
518                }
519            } else {
520                let last_item_index = items.len() - 1;
521                let shown = limits.max_length.unwrap_or(usize::MAX);
522                let mut children: Vec<Node> = items
523                    .iter()
524                    .take(shown)
525                    .enumerate()
526                    .map(|(index, (key, child))| {
527                        let mut child_node = traverse_node(child, false, depth + 1, limits);
528                        child_node.key_repr = to_repr(key, limits.max_string);
529                        child_node.last = index == last_item_index;
530                        child_node
531                    })
532                    .collect();
533                push_more(&mut children, items.len(), limits.max_length);
534                Node {
535                    open_brace: "{".to_string(),
536                    close_brace: "}".to_string(),
537                    children: Some(children),
538                    last: root,
539                    ..Node::default()
540                }
541            }
542        }
543        atom => Node {
544            value_repr: to_repr(atom, limits.max_string),
545            last: root,
546            ..Node::default()
547        },
548    };
549    node.is_tuple = matches!(value, PrettyValue::Tuple(_));
550    node.is_namedtuple = matches!(
551        value,
552        PrettyValue::Object(PrettyObject {
553            kind: ObjectRepr::NamedTuple,
554            ..
555        })
556    );
557    node
558}
559
560/// The `... +N` node after a container cut to `max_length`.
561fn push_more(children: &mut Vec<Node>, num_items: usize, max_length: Option<usize>) {
562    if let Some(max_length) = max_length {
563        if num_items > max_length {
564            children.push(Node {
565                value_repr: format!("... +{}", num_items - max_length),
566                last: true,
567                ..Node::default()
568            });
569        }
570    }
571}
572
573fn traverse_object(object: &PrettyObject, root: bool, depth: usize, limits: ReprLimits) -> Node {
574    let reached_max_depth = limits.max_depth.is_some_and(|max| depth >= max);
575    let name = &object.name;
576    let children = || -> Vec<Node> {
577        let count = object.fields.len();
578        object
579            .fields
580            .iter()
581            .enumerate()
582            .map(|(index, (key, child))| {
583                let mut child_node = traverse_node(child, false, depth + 1, limits);
584                child_node.last = index + 1 == count;
585                if let Some(key) = key {
586                    child_node.key_repr = key.clone();
587                    child_node.key_separator = "=".to_string();
588                }
589                child_node
590            })
591            .collect()
592    };
593    match object.kind {
594        ObjectRepr::RichRepr { angular } => {
595            if object.fields.is_empty() {
596                Node {
597                    value_repr: if angular {
598                        format!("<{name}>")
599                    } else {
600                        format!("{name}()")
601                    },
602                    children: Some(Vec::new()),
603                    last: root,
604                    ..Node::default()
605                }
606            } else if reached_max_depth {
607                Node::atom(if angular {
608                    format!("<{name}...>")
609                } else {
610                    format!("{name}(...)")
611                })
612            } else if angular {
613                Node {
614                    open_brace: format!("<{name} "),
615                    close_brace: ">".to_string(),
616                    children: Some(children()),
617                    last: root,
618                    separator: " ".to_string(),
619                    ..Node::default()
620                }
621            } else {
622                Node {
623                    open_brace: format!("{name}("),
624                    close_brace: ")".to_string(),
625                    children: Some(children()),
626                    last: root,
627                    ..Node::default()
628                }
629            }
630        }
631        ObjectRepr::Dataclass | ObjectRepr::NamedTuple => {
632            if reached_max_depth {
633                Node::atom(format!("{name}(...)"))
634            } else {
635                Node {
636                    open_brace: format!("{name}("),
637                    close_brace: ")".to_string(),
638                    children: Some(children()),
639                    // Upstream's namedtuple node leaves `last` unset.
640                    last: root && object.kind == ObjectRepr::Dataclass,
641                    empty: format!("{name}()"),
642                    ..Node::default()
643                }
644            }
645        }
646    }
647}
648
649/// A repr of `value`, expanded onto several lines to fit `max_width`. Port
650/// of `rich.pretty.pretty_repr`.
651pub fn pretty_repr(
652    value: &PrettyValue,
653    max_width: usize,
654    indent_size: usize,
655    limits: ReprLimits,
656    expand_all: bool,
657) -> String {
658    traverse(value, limits).render(max_width, indent_size, expand_all)
659}
660
661enum Source {
662    /// A Rust value's `Debug` output (the Rust-native form).
663    Debug(Text),
664    /// A Python value, laid out as upstream does.
665    Value(PrettyValue),
666}
667
668/// A renderable that pretty prints a value. Mirrors `rich.pretty.Pretty`.
669pub struct Pretty {
670    source: Source,
671    highlighter: Box<dyn Highlighter + Send + Sync>,
672    indent_size: usize,
673    justify: Option<Justify>,
674    overflow: Option<Overflow>,
675    no_wrap: Option<bool>,
676    indent_guides: bool,
677    limits: ReprLimits,
678    expand_all: bool,
679    margin: usize,
680    insert_line: bool,
681}
682
683impl Pretty {
684    /// Pretty-print `value` (`{:#?}`, multi-line and indented) and highlight
685    /// it. Not upstream's layout; see [`Pretty::from_value`].
686    pub fn new(value: &impl Debug) -> Self {
687        Pretty::from_debug_string(format!("{value:#?}"))
688    }
689
690    /// Format `value` compactly on one line (`{:?}`) and highlight it.
691    pub fn compact(value: &impl Debug) -> Self {
692        Pretty::from_debug_string(format!("{value:?}"))
693    }
694
695    fn from_debug_string(rendered: String) -> Self {
696        let mut text = Text::new(rendered);
697        ReprHighlighter::new().highlight(&mut text);
698        Pretty::with_source(Source::Debug(text))
699    }
700
701    /// Pretty-print a Python value as upstream's `Pretty(obj)` does.
702    pub fn from_value(value: PrettyValue) -> Self {
703        Pretty::with_source(Source::Value(value))
704    }
705
706    fn with_source(source: Source) -> Self {
707        Pretty {
708            source,
709            highlighter: Box::new(ReprHighlighter::new()),
710            indent_size: 4,
711            justify: None,
712            overflow: None,
713            no_wrap: Some(false),
714            indent_guides: false,
715            limits: ReprLimits::default(),
716            expand_all: false,
717            margin: 0,
718            insert_line: false,
719        }
720    }
721
722    /// The highlighter applied to the repr (upstream `highlighter`, default
723    /// [`ReprHighlighter`]).
724    pub fn highlighter(mut self, highlighter: impl Highlighter + Send + Sync + 'static) -> Self {
725        self.highlighter = Box::new(highlighter);
726        self
727    }
728
729    /// Spaces per indent level (upstream `indent_size`, default 4).
730    pub fn indent_size(mut self, indent_size: usize) -> Self {
731        self.indent_size = indent_size;
732        self
733    }
734
735    /// Justify method, or `None` for the console's (upstream `justify`).
736    pub fn justify(mut self, justify: Option<Justify>) -> Self {
737        self.justify = justify;
738        self
739    }
740
741    /// Overflow method, or `None` for the console's (upstream `overflow`).
742    pub fn overflow(mut self, overflow: Option<Overflow>) -> Self {
743        self.overflow = overflow;
744        self
745    }
746
747    /// Disable wrapping, or `None` for the console's (upstream `no_wrap`,
748    /// default `False`).
749    pub fn no_wrap(mut self, no_wrap: Option<bool>) -> Self {
750        self.no_wrap = no_wrap;
751        self
752    }
753
754    /// Draw indent guides (upstream `indent_guides`).
755    pub fn indent_guides(mut self, indent_guides: bool) -> Self {
756        self.indent_guides = indent_guides;
757        self
758    }
759
760    /// Abbreviate containers longer than this (upstream `max_length`).
761    pub fn max_length(mut self, max_length: Option<usize>) -> Self {
762        self.limits.max_length = max_length;
763        self
764    }
765
766    /// Truncate strings longer than this (upstream `max_string`).
767    pub fn max_string(mut self, max_string: Option<usize>) -> Self {
768        self.limits.max_string = max_string;
769        self
770    }
771
772    /// Stop at this depth of nesting (upstream `max_depth`).
773    pub fn max_depth(mut self, max_depth: Option<usize>) -> Self {
774        self.limits.max_depth = max_depth;
775        self
776    }
777
778    /// Expand every container (upstream `expand_all`).
779    pub fn expand_all(mut self, expand_all: bool) -> Self {
780        self.expand_all = expand_all;
781        self
782    }
783
784    /// Cells taken off the width, so containers expand sooner (upstream
785    /// `margin`).
786    pub fn margin(mut self, margin: usize) -> Self {
787        self.margin = margin;
788        self
789    }
790
791    /// Start with a blank line when the output has several lines (upstream
792    /// `insert_line`).
793    pub fn insert_line(mut self, insert_line: bool) -> Self {
794        self.insert_line = insert_line;
795        self
796    }
797
798    fn repr_at(&self, value: &PrettyValue, max_width: usize) -> String {
799        pretty_repr(
800            value,
801            max_width,
802            self.indent_size,
803            self.limits,
804            self.expand_all,
805        )
806    }
807}
808
809impl Renderable for Pretty {
810    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
811        let value = match &self.source {
812            Source::Debug(text) => return text.rich_render(console, options),
813            Source::Value(value) => value,
814        };
815        let pretty_str = self.repr_at(value, options.max_width.saturating_sub(self.margin));
816        let mut pretty_text = Text::from_ansi(&pretty_str, "pretty");
817        pretty_text.set_justify(self.justify.unwrap_or(options.justify));
818        pretty_text.set_overflow(self.overflow.or(options.overflow));
819        pretty_text.set_no_wrap(Some(self.no_wrap.or(options.no_wrap).unwrap_or(false)));
820        if pretty_text.is_empty() {
821            // Upstream names the object's type here, which a `PrettyValue`
822            // does not carry.
823            pretty_text = Text::styled("__repr__ returned empty string", "dim italic");
824        } else {
825            self.highlighter.highlight(&mut pretty_text);
826        }
827        if self.indent_guides && !options.ascii_only() {
828            pretty_text =
829                pretty_text.with_indent_guides(Some(self.indent_size), "│", "repr.indent");
830        }
831        let mut segments = Vec::new();
832        if self.insert_line && pretty_text.plain().contains('\n') {
833            segments.push(Segment::line());
834        }
835        segments.extend(pretty_text.rich_render(console, options));
836        segments
837    }
838
839    fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
840        let value = match &self.source {
841            Source::Debug(text) => return text.measure(console, options),
842            Source::Value(value) => value,
843        };
844        let pretty_str = self.repr_at(value, options.max_width);
845        let text_width = pretty_str.lines().map(cell_len).max().unwrap_or(0);
846        Measurement {
847            minimum: text_width,
848            maximum: text_width,
849        }
850    }
851}
852
853#[cfg(test)]
854mod tests {
855    use super::*;
856    use crate::color::ColorSystem;
857
858    fn render(pretty: &Pretty) -> String {
859        Console::builder()
860            .force_terminal(true)
861            .color_system(Some(ColorSystem::Truecolor))
862            .width(80)
863            .no_color(false)
864            .build()
865            .render_to_string(pretty)
866    }
867
868    #[test]
869    fn highlights_numbers_in_debug_output() {
870        // Numbers get the repr.number style (bold cyan → 1;36).
871        let out = render(&Pretty::compact(&vec![1, 2, 3]));
872        assert!(out.contains("\x1b[1;36m1\x1b[0m"), "got {out:?}");
873        assert!(out.contains('3'));
874    }
875
876    #[test]
877    fn highlights_string_in_debug_output() {
878        // A quoted string gets the repr.str style (green → 32).
879        let out = render(&Pretty::compact(&"hello"));
880        assert!(out.contains("\x1b[32m\"hello\"\x1b[0m"), "got {out:?}");
881    }
882
883    #[test]
884    fn pretty_multiline_preserves_structure() {
885        // `{:#?}` on a nested collection spans multiple indented lines.
886        let out = render(&Pretty::new(&vec![vec![1, 2], vec![3, 4]]));
887        assert!(out.contains('\n'), "expected pretty multi-line layout");
888        assert!(out.contains("\x1b[1;36m4\x1b[0m"), "numbers highlighted");
889    }
890
891    #[test]
892    fn python_reprs() {
893        use PrettyValue as V;
894        assert_eq!(str_repr("it's"), "\"it's\"");
895        assert_eq!(
896            str_repr("a'\"b\n\x01é\u{2028}"),
897            "'a\\'\"b\\n\\x01é\\u2028'"
898        );
899        assert_eq!(bytes_repr(b"a\x00'"), "b\"a\\x00'\"");
900        let value = V::List(vec![
901            V::Tuple(vec![V::Int(1)]),
902            V::Set(vec![]),
903            V::Dict(vec![(V::Str("k".into()), V::Float(1e16))]),
904            V::None,
905            V::Bool(true),
906        ]);
907        assert_eq!(py_repr(&value), "[(1,), set(), {'k': 1e+16}, None, True]");
908    }
909
910    #[test]
911    fn expands_only_what_does_not_fit() {
912        use PrettyValue as V;
913        let value = V::Dict(vec![
914            (V::Str("a".into()), V::List((0..3).map(V::Int).collect())),
915            (V::Str("b".into()), V::Tuple(vec![V::Str("x".repeat(30))])),
916        ]);
917        let out = pretty_repr(&value, 20, 4, ReprLimits::default(), false);
918        assert_eq!(
919            out,
920            "{\n    'a': [0, 1, 2],\n    'b': (\n        'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',\n    )\n}"
921        );
922    }
923}