Repository: huggingface/huggingface_hub

Issue:
Consider implementing Rich Repr Protocol for classes in hf_api 
**Is your feature request related to a problem? Please describe.**

Currently, the `__repr__`'s for classes in the `hf_api` module are nice and compact but can require a lot of side-scrolling to view all of the information. For example, the `__repr__` for `ModelInfo` looks like:

 ```
ModelInfo: {
	modelId: flyswot/convnext-tiny-224_flyswot
	sha: c6d4b2138e10efeafef8f5305ce16270ca583618
	lastModified: 2022-04-05T16:08:35.000Z
	tags: ['pytorch', 'convnext', 'image-classification', 'dataset:image_folder', 'transformers', 'generated_from_trainer', 'model-index']
	pipeline_tag: image-classification
	siblings: [ModelFile(rfilename='.gitattributes'), ModelFile(rfilename='.gitignore'), ModelFile(rfilename='README.md'), ModelFile(rfilename='config.json'), ModelFile(rfilename='preprocessor_config.json'), ModelFile(rfilename='pytorch_model.bin'), ModelFile(rfilename='training_args.bin')]
	config: {'architectures': ['ConvNextForImageClassification'], 'model_type': 'convnext'}
	id: flyswot/convnext-tiny-224_flyswot
	author: flyswot
	private: False
	downloads: 999
	library_name: transformers
	model-index: [{'name': 'convnext-tiny-224_flyswot', 'results': [{'task': {'name': 'Image Classification', 'type': 'image-classification'}, 'dataset': {'name': 'image_folder', 'type': 'image_folder', 'args': 'default'}, 'metrics': [{'name': 'F1', 'type': 'f1', 'value': 0.9756290792360154}]}]}]
	cardData: {'tags': ['generated_from_trainer'], 'datasets': ['image_folder'], 'metrics': ['f1'], 'model-index': [{'name': 'convnext-tiny-224_flyswot', 'results': [{'task': {'name': 'Image Classification', 'type': 'image-classification'}, 'dataset': {'name': 'image_folder', 'type': 'image_folder', 'args': 'default'}, 'metrics': [{'name': 'F1', 'type': 'f1', 'value': 0.9756290792360154}]}]}]}
}
```

**Describe the solution you'd like**

[`rich`](https://github.com/Textualize/rich/) is a:

> Python library for rich text and beautiful formatting in the terminal.

One of the features `rich` offers its own `print`. An example of this 

<img width="770" alt="Screenshot 2022-04-28 at 14 17 21" src="https://user-images.githubusercontent.com/8995957/165760899-3dab2f26-fac5-448a-96cd-f3e6d2e24546.png">

However: 
> Rich is able to syntax highlight any output, but the formatting is restricted to built-in containers, dataclasses, and other objects Rich knows about, such as objects generated by the [attrs](https://www.attrs.org/en/stable/) library. To add Rich formatting capabilities to custom objects, you can implement the rich repr protocol. -[source](https://rich.readthedocs.io/en/latest/pretty.html#rich-repr-protocol)

It might be nice to add this for the container classes under `hf_api`. This requires implementing `__rich_rep__`. 

As an example, I have created a simple version of this for `ModelInfo`:

```python
def __rich_repr__(self:ModelInfo):
    for key, val in self.__dict__.items():
        yield key, val
```

This results in the following when using `rich`'s version of `print`:

```python
ModelInfo(
    modelId='flyswot/convnext-tiny-224_flyswot',
    sha='c6d4b2138e10efeafef8f5305ce16270ca583618',
    lastModified='2022-04-05T16:08:35.000Z',
    tags=[
        'pytorch',
        'convnext',
        'image-classification',
        'dataset:image_folder',
        'transformers',
        'generated_from_trainer',
        'model-index'
    ],
    pipeline_tag='image-classification',
    siblings=[
        ModelFile(rfilename='.gitattributes'),
        ModelFile(rfilename='.gitignore'),
        ModelFile(rfilename='README.md'),
        ModelFile(rfilename='config.json'),
        ModelFile(rfilename='preprocessor_config.json'),
        ModelFile(rfilename='pytorch_model.bin'),
        ModelFile(rfilename='training_args.bin')
    ],
    config={'architectures': ['ConvNextForImageClassification'], 'model_type': 'convnext'},
    id='flyswot/convnext-tiny-224_flyswot',
    author='flyswot',
    private=False,
    downloads=999,
    library_name='transformers',
    model-index=[
        {
            'name': 'convnext-tiny-224_flyswot',
            'results': [
                {
                    'task': {
                        'name': 'Image Classification',
                        'type': 'image-classification'
                    },
                    'dataset': {
                        'name': 'image_folder',
                        'type': 'image_folder',
                        'args': 'default'
                    },
                    'metrics': [{'name': 'F1', 'type': 'f1', 'value': 0.9756290792360154}]
                }
            ]
        }
    ],
    cardData={
        'tags': ['generated_from_trainer'],
        'datasets': ['image_folder'],
        'metrics': ['f1'],
        'model-index': [
            {
                'name': 'convnext-tiny-224_flyswot',
                'results': [
                    {
                        'task': {
                            'name': 'Image Classification',
                            'type': 'image-classification'
                        },
                        'dataset': {
                            'name': 'image_folder',
                            'type': 'image_folder',
                            'args': 'default'
                        },
                        'metrics': [
                            {'name': 'F1', 'type': 'f1', 'value': 0.9756290792360154}
                        ]
                    }
                ]
            }
        ]
    }
)
```

It also comes with nice colour highlighting:

<img width="762" alt="Screenshot 2022-04-28 at 14 23 01" src="https://user-images.githubusercontent.com/8995957/165761987-432289c4-6d26-48da-9a75-0751ad3058f1.png">

There is scope for making this nicer, but even though this version is much longer, it does allow everything to be visible, even on narrow displays. 

One of the reasons I think this could be a candidate for inclusion is that:

> you can add __rich_repr__ methods to third-party libraries without including Rich as a dependency. If Rich is not installed, then nothing will break. Hopefully more third-party libraries will adopt Rich repr methods in the future.

This means that implementing a `__rich__` repr doesn't require this library or end-users, to have `rich` installed. If they like the features of `rich`, they get the benefits, but it's not forced on anyone. 

**Describe alternatives you've considered**

Not doing this! Although I find even the naive `__rich_repr__` quite nice, this is a matter of personal preference. There is also a potential scope creep with implementing `rich_repr__` across more and more code. However, I think the container classes in `hf_api` are particularly good candidates because it contains information you might want to inspect on a terminal/print out in a notebook etc. 

**Additional context**

If this is something that is deemed worth exploring more I would be happy to make an initial pull request to begin implementing this. 





Discussion:
Hey there, thanks for all the info @davanstrien - really helpful. I've also been annoyed by the formatting here in the terminal/when printing, so this sounds like an interesting addition.

One question about this quote:

> you can add rich_repr methods to third-party libraries without including Rich as a dependency. If Rich is not installed, then nothing will break. Hopefully more third-party libraries will adopt Rich repr methods in the future.

For users who _have rich installed_, would they have to use `rich.print` to get the benefits?
Sorry, that was a bit confusing on my part. Usually, it is possible to use rich as an end user in a few ways that don't rely on print.  

In the [REPL](https://rich.readthedocs.io/en/stable/introduction.html#rich-in-the-repl) you can do

```python
>>> from rich import pretty
>>> pretty.install()
```
and you will get rich formatting for Python objects (and things which a `__rich_repr__`. 

In iPython/notebooks you can install do the following:

```console
pip install rich[jupyter]
```
and then at the top of a notebook do 
```python 
%load_ext rich
```
Example [Colab notebook](https://colab.research.google.com/drive/1N-kW9ZXYeLYefPbeEqwUUlYKdK8VLUXV?usp=sharing). I have started putting this at the top of most notebooks now since you also get nice tracebacks too. 
Hey @davanstrien, that's a very interesting proposal, thank you!

I think what we'd ideally reach for here is for the `__repr__` to be updated to output similar information to what is displayed by the `__rich_repr__` you mention here, without necessarily implementing syntax highlighting.

`rich` looks like a cool package, but as we aim for `huggingface_hub` to be a very central package to be used across many different libraries, we aim for it to be as lean as possible. This means that we want it lean in terms of dependencies, of course (not impacted by this as `rich` is optional), but also lean in terms of code readability, class attributes, etc.
My personal feeling is that having an optional method that only benefits users that have a certain library installed and that adds features that could be implemented so that all users benefit from it without leveraging that optional dependency goes against that principle. 

What do you think?


> Hey @davanstrien, that's a very interesting proposal, thank you!
> 
> I think what we'd ideally reach for here is for the `__repr__` to be updated to output similar information to what is displayed by the `__rich_repr__` you mention here, without necessarily implementing syntax highlighting.

This sounds great!
> `rich` looks like a cool package, but as we aim for `huggingface_hub` to be a very central package to be used across many different libraries, we aim for it to be as lean as possible. This means that we want it lean in terms of dependencies, of course (not impacted by this as `rich` is optional), but also lean in terms of code readability, class attributes, etc. My personal feeling is that having an optional method that only benefits users that have a certain library installed and that adds features that could be implemented so that all users benefit from it without leveraging that optional dependency goes against that principle.
> 
> What do you think?

This makes a lot of sense and I can see the issues with adding extra debt by adding `__rich_repr__` especially as this isn't primarily a 'visual' package. I think getting the `__repr__`'s closer to the rich version would be a great solution. 

@davanstrien do you want to give it a shot? This would be a nice contribution 😄 
> @davanstrien do you want to give it a shot? This would be a nice contribution 😄

I'd love to give it a go! The only thing is I'm afk until the start of June but if you're okay to wait until then, I'm happy to make a start on this.

I think we can wait till you come back @davanstrien , enjoy your time off!

Resolving patch:
diff --git a/src/huggingface_hub/hf_api.py b/src/huggingface_hub/hf_api.py
--- a/src/huggingface_hub/hf_api.py
+++ b/src/huggingface_hub/hf_api.py
@@ -14,7 +14,9 @@
 # limitations under the License.
 import json
 import os
+import pprint
 import re
+import textwrap
 import warnings
 from dataclasses import dataclass, field
 from datetime import datetime
@@ -92,6 +94,16 @@
 logger = logging.get_logger(__name__)
 
 
+class ReprMixin:
+    """Mixin to create the __repr__ for a class"""
+
+    def __repr__(self):
+        s = f"{self.__class__.__name__}: {{ \n"
+        s += textwrap.indent(pprint.pformat(self.__dict__, width=88, sort_dicts=False), "        ")
+        s += "\n\t}"
+        return s
+
+
 def repo_type_and_id_from_hf_id(hf_id: str, hub_url: Optional[str] = None) -> Tuple[Optional[str], Optional[str], str]:
     """
     Returns the repo type and ID from a huggingface.co URL linking to a
@@ -339,12 +351,8 @@ def __init__(
         for k, v in kwargs.items():
             setattr(self, k, v)
 
-    def __repr__(self):
-        items = (f"{k}='{v}'" for k, v in self.__dict__.items())
-        return f"{self.__class__.__name__}({', '.join(items)})"
-
 
-class ModelInfo:
+class ModelInfo(ReprMixin):
     """
     Info about a model accessible from huggingface.co
 
@@ -402,12 +410,6 @@ def __init__(
         for k, v in kwargs.items():
             setattr(self, k, v)
 
-    def __repr__(self):
-        s = f"{self.__class__.__name__}:" + " {"
-        for key, val in self.__dict__.items():
-            s += f"\n\t{key}: {val}"
-        return s + "\n}"
-
     def __str__(self):
         r = f"Model Name: {self.modelId}, Tags: {self.tags}"
         if self.pipeline_tag:
@@ -415,7 +417,7 @@ def __str__(self):
         return r
 
 
-class DatasetInfo:
+class DatasetInfo(ReprMixin):
     """
     Info about a dataset accessible from huggingface.co
 
@@ -476,18 +478,12 @@ def __init__(
         for k, v in kwargs.items():
             setattr(self, k, v)
 
-    def __repr__(self):
-        s = f"{self.__class__.__name__}:" + " {"
-        for key, val in self.__dict__.items():
-            s += f"\n\t{key}: {val}"
-        return s + "\n}"
-
     def __str__(self):
         r = f"Dataset Name: {self.id}, Tags: {self.tags}"
         return r
 
 
-class SpaceInfo:
+class SpaceInfo(ReprMixin):
     """
     Info about a Space accessible from huggingface.co
 
@@ -531,14 +527,8 @@ def __init__(
         for k, v in kwargs.items():
             setattr(self, k, v)
 
-    def __repr__(self):
-        s = f"{self.__class__.__name__}:" + " {"
-        for key, val in self.__dict__.items():
-            s += f"\n\t{key}: {val}"
-        return s + "\n}"
 
-
-class MetricInfo:
+class MetricInfo(ReprMixin):
     """
     Info about a public metric accessible from huggingface.co
     """
@@ -561,12 +551,6 @@ def __init__(
         for k, v in kwargs.items():
             setattr(self, k, v)
 
-    def __repr__(self):
-        s = f"{self.__class__.__name__}:" + " {"
-        for key, val in self.__dict__.items():
-            s += f"\n\t{key}: {val}"
-        return s + "\n}"
-
     def __str__(self):
         r = f"Metric Name: {self.id}"
         return r


Test patch: