Repository: Textualize/rich (python)

Issue:
[REQUEST] Allow delimiters when using `pprint(..., max_depth=...)`
**How would you improve Rich?**

Currently, when pretty printing a nested object, we can set the max depth to simplify the object display:
```python
>>> from rich.pretty import pprint
>>> pprint({"a": {"b": {"c": "d"}}, "e": ["f", "g"]}, max_depth=1)
{'a': ..., 'e': ...}
```

However, by using ellipsis like this, we obscure the information about the data type of "a". In contrast, Python's built-in `pprint` module adds delimiters around the ellipsis, to differentiate between a list and a dict:
```python
>>> import pprint
>>> pprint.pprint({"a": {"b": {"c": "d"}}, "e": ["f", "g"]}, depth=1)
{'a': {...}, 'e': [...]}
```

Notice how, with only 4 extra characters, we have significantly more information about the eluded structure.

I would like to propose that we add a new argument to `pprint()` such as `max_depth_delims: bool`, which when True, would enable the behaviour above.

**What problem does it solve for you?**

When I use `max_depth` in tutorial or example, it's because I don't want unnecessary detail taking up a large amount of screen real estate. However I still want my users to understand that there *is* additional detail available, and to give them a hint as to what kind of data is nested further in the tree, and how to access it. Adding these delimiters nicely enable this compromise.

Pull request description:
Add delimiters to elided pretty prints
## Type of changes

- [ ] Bug fix
- [x] New feature
- [ ] Documentation / docstrings
- [ ] Tests
- [ ] Other

## Checklist

- [x] I've run the latest [black](https://github.com/psf/black) with default args on new code.
- [x] I've updated CHANGELOG.md and CONTRIBUTORS.md where appropriate.
- [x] I've added tests for new code.
- [x] I accept that @willmcgugan may be pedantic in the code review.

## Description

Closes #2485

One thing not discussed in the issue was how to deal with more complex types like dataclasses and namedtuples. So far I'm treating them the same as dicts and lists. In other words, the delimiters stay the same as if we weren't abridging anything, but the children are replaced with `...`. For example: `"{'unit': StockKeepingUnit(...)}"`

Patch:
diff --git a/rich/pretty.py b/rich/pretty.py
index 002508f7..6b45b5b3 100644
--- a/rich/pretty.py
+++ b/rich/pretty.py
@@ -676,7 +676,10 @@ def traverse(
                 append = children.append
 
                 if reached_max_depth:
-                    node = Node(value_repr=f"...")
+                    if angular:
+                        node = Node(value_repr=f"<{class_name}...>")
+                    else:
+                        node = Node(value_repr=f"{class_name}(...)")
                 else:
                     if angular:
                         node = Node(
@@ -718,7 +721,7 @@ def traverse(
             attr_fields = _get_attr_fields(obj)
             if attr_fields:
                 if reached_max_depth:
-                    node = Node(value_repr=f"...")
+                    node = Node(value_repr=f"{obj.__class__.__name__}(...)")
                 else:
                     node = Node(
                         open_brace=f"{obj.__class__.__name__}(",
@@ -774,7 +777,7 @@ def traverse(
             children = []
             append = children.append
             if reached_max_depth:
-                node = Node(value_repr=f"...")
+                node = Node(value_repr=f"{obj.__class__.__name__}(...)")
             else:
                 node = Node(
                     open_brace=f"{obj.__class__.__name__}(",
@@ -794,18 +797,21 @@ def traverse(
 
                 pop_visited(obj_id)
         elif _is_namedtuple(obj) and _has_default_namedtuple_repr(obj):
+            class_name = obj.__class__.__name__
             if reached_max_depth:
-                node = Node(value_repr="...")
+                # If we've reached the max depth, we still show the class name, but not its contents
+                node = Node(
+                    value_repr=f"{class_name}(...)",
+                )
             else:
                 children = []
-                class_name = obj.__class__.__name__
+                append = children.append
                 node = Node(
                     open_brace=f"{class_name}(",
                     close_brace=")",
                     children=children,
                     empty=f"{class_name}()",
                 )
-                append = children.append
                 for last, (key, value) in loop_last(obj._asdict().items()):
                     child_node = _traverse(value, depth=depth + 1)
                     child_node.key_repr = key
@@ -827,7 +833,7 @@ def traverse(
             open_brace, close_brace, empty = _BRACES[obj_type](obj)
 
             if reached_max_depth:
-                node = Node(value_repr=f"...", last=root)
+                node = Node(value_repr=f"{open_brace}...{close_brace}")
             elif obj_type.__repr__ != type(obj).__repr__:
                 node = Node(value_repr=to_repr(obj), last=root)
             elif obj:


Test patch:
diff --git a/tests/test_pretty.py b/tests/test_pretty.py
index 9346b390..0551d248 100644
--- a/tests/test_pretty.py
+++ b/tests/test_pretty.py
@@ -256,7 +256,7 @@ def test_pretty_namedtuple_fields_invalid_type():
 def test_pretty_namedtuple_max_depth():
     instance = {"unit": StockKeepingUnit("a", "b", 1.0, "c", ["d", "e"])}
     result = pretty_repr(instance, max_depth=1)
-    assert result == "{'unit': ...}"
+    assert result == "{'unit': StockKeepingUnit(...)}"
 
 
 def test_small_width():
@@ -318,13 +318,13 @@ def test_max_depth():
     d = {}
     d["foo"] = {"fob": {"a": [1, 2, 3], "b": {"z": "x", "y": ["a", "b", "c"]}}}
 
-    assert pretty_repr(d, max_depth=0) == "..."
-    assert pretty_repr(d, max_depth=1) == "{'foo': ...}"
-    assert pretty_repr(d, max_depth=2) == "{'foo': {'fob': ...}}"
-    assert pretty_repr(d, max_depth=3) == "{'foo': {'fob': {'a': ..., 'b': ...}}}"
+    assert pretty_repr(d, max_depth=0) == "{...}"
+    assert pretty_repr(d, max_depth=1) == "{'foo': {...}}"
+    assert pretty_repr(d, max_depth=2) == "{'foo': {'fob': {...}}}"
+    assert pretty_repr(d, max_depth=3) == "{'foo': {'fob': {'a': [...], 'b': {...}}}}"
     assert (
         pretty_repr(d, max_width=100, max_depth=4)
-        == "{'foo': {'fob': {'a': [1, 2, 3], 'b': {'z': 'x', 'y': ...}}}}"
+        == "{'foo': {'fob': {'a': [1, 2, 3], 'b': {'z': 'x', 'y': [...]}}}}"
     )
     assert (
         pretty_repr(d, max_width=100, max_depth=5)
@@ -353,7 +353,7 @@ def test_max_depth_rich_repr():
 
     assert (
         pretty_repr(Foo(foo=Bar(bar=Foo(foo=[]))), max_depth=2)
-        == "Foo(foo=Bar(bar=...))"
+        == "Foo(foo=Bar(bar=Foo(...)))"
     )
 
 
@@ -368,7 +368,7 @@ def test_max_depth_attrs():
 
     assert (
         pretty_repr(Foo(foo=Bar(bar=Foo(foo=[]))), max_depth=2)
-        == "Foo(foo=Bar(bar=...))"
+        == "Foo(foo=Bar(bar=Foo(...)))"
     )
 
 
@@ -383,7 +383,7 @@ def test_max_depth_dataclass():
 
     assert (
         pretty_repr(Foo(foo=Bar(bar=Foo(foo=[]))), max_depth=2)
-        == "Foo(foo=Bar(bar=...))"
+        == "Foo(foo=Bar(bar=Foo(...)))"
     )