Repository: samuelcolvin/pydantic (python)

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.

Pull request:
fix: ignore `__doc__` as valid private attribute
## Change Summary
`__doc__` was not ignored when setting private attributes

## Related issue number
closes #2090

## Checklist

* [ ] Unit tests for the changes exist
* [ ] Tests pass on CI and coverage remains at 100%
* [ ] Documentation reflects the changes where applicable
* [ ] `changes/<pull request or issue id>-<github username>.md` file added describing change
  (see [changes/README.md](https://github.com/samuelcolvin/pydantic/blob/master/changes/README.md) for details)


Patch:
diff --git a/changes/2090-PrettyWood.md b/changes/2090-PrettyWood.md
new file mode 100644
index 000000000..2e2e56f55
--- /dev/null
+++ b/changes/2090-PrettyWood.md
@@ -0,0 +1,1 @@
+Ignore `__doc__` as private attribute when `Config.underscore_attrs_are_private` is set
\ No newline at end of file
diff --git a/pydantic/utils.py b/pydantic/utils.py
index 4592223d1..e3e613ca3 100644
--- 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
index 48be872e2..21f52090b 100644
--- a/tests/test_private_attributes.py
+++ b/tests/test_private_attributes.py
@@ -55,6 +55,8 @@ def test_private_attribute_factory():
 
 def test_private_attribute_annotation():
     class Model(BaseModel):
+        """The best model"""
+
         __foo__: str
 
         class Config:
@@ -63,6 +65,7 @@ def test_private_attribute_annotation():
     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):