Repository: pydantic/pydantic

Issue:
The underscore_attrs_are_private config flag turns __doc__ attribute into a ModelPrivateAttr
### Checks

* [x] I added a descriptive title to this issue
* [x] I have searched (google, github) for similar issues and couldn't find anything
* [x] I have read and followed [the docs](https://pydantic-docs.helpmanual.io/) and still think this is a bug

<!-- Sorry to sound so draconian, but every second saved replying to issues is time spend improving pydantic :-) -->

# Bug

Output of `python -c "import pydantic.utils; print(pydantic.utils.version_info())"`:
```
             pydantic version: 1.7.2
            pydantic compiled: False
                 install path: D:\dev\Miniconda3\envs\geoenv\Lib\site-packages\pydantic
               python version: 3.8.6 | packaged by conda-forge | (default, Oct  7 2020, 18:22:52) [MSC v.1916 64 bit (AMD64)]
                     platform: Windows-10-10.0.19041-SP0
     optional deps. installed: ['typing-extensions']
```
<!-- or if you're using pydantic prior to v1.3, manually include: OS, python version and pydantic version -->

<!-- Please read the [docs](https://pydantic-docs.helpmanual.io/) and search through issues to
confirm your bug hasn't already been reported. -->

<!-- Where possible please include a self-contained code snippet describing your bug: -->

Pydantic v1.7 introduced the private attributes. But when the config flag `underscore_attrs_are_private` is set to `True`, the model's `__doc__` attribute also becomes a private attribute. Example:

```py
>>> from pydantic import BaseModel
>>> class MyModel(BaseModel):
...     """MyModel docsting."""
...     class Config:
...         underscore_attrs_are_private = True
...
>>> MyModel.__doc__
<member '__doc__' of 'MyModel' objects>
```

My current workaround is to define a new metaclass to handle the issue:
```py
>>> from pydantic import BaseModel
>>> from pydantic.main import ModelMetaclass
>>> from pydantic.fields import ModelPrivateAttr
>>> class DocPreservingModelMetaclass(ModelMetaclass):
...     def __new__(mcs, name, bases, namespace, **kwargs):
...         cls = super().__new__(mcs, name, bases, namespace, **kwargs)
...         if cls.__config__.underscore_attrs_are_private:
...             doc = cls.__private_attributes__.pop('__doc__', None)
...             if isinstance(doc, ModelPrivateAttr):
...                 cls.__doc__ = doc.default
...         return cls
...
>>> class MyModel(BaseModel, metaclass=DocPreservingModelMetaclass):
...     """MyModel docsting."""
...     class Config:
...         underscore_attrs_are_private = True
...
>>> MyModel.__doc__
'MyModel docsting.'
```

But maybe it would be best if `__doc__` was excluded via `pydantic.utils.is_valid_private_name`, much like the other standard properties are excluded through this method. However, I am not sure if the latter will have other implication in the current codebase.


Discussion:

Resolving patch:
diff --git a/pydantic/utils.py b/pydantic/utils.py
--- a/pydantic/utils.py
+++ b/pydantic/utils.py
@@ -631,4 +631,10 @@ def is_valid_field(name: str) -> bool:
 
 
 def is_valid_private_name(name: str) -> bool:
-    return not is_valid_field(name) and name not in {'__annotations__', '__classcell__', '__module__', '__qualname__'}
+    return not is_valid_field(name) and name not in {
+        '__annotations__',
+        '__classcell__',
+        '__doc__',
+        '__module__',
+        '__qualname__',
+    }


Test patch:
diff --git a/tests/test_private_attributes.py b/tests/test_private_attributes.py
--- a/tests/test_private_attributes.py
+++ b/tests/test_private_attributes.py
@@ -55,6 +55,8 @@ class Model(BaseModel):
 
 def test_private_attribute_annotation():
     class Model(BaseModel):
+        """The best model"""
+
         __foo__: str
 
         class Config:
@@ -63,6 +65,7 @@ class Config:
     assert Model.__slots__ == {'__foo__'}
     assert repr(Model.__foo__) == "<member '__foo__' of 'Model' objects>"
     assert Model.__private_attributes__ == {'__foo__': PrivateAttr(Undefined)}
+    assert repr(Model.__doc__) == "'The best model'"
 
     m = Model()
     with pytest.raises(AttributeError):