Repository: pydantic/pydantic
Pull request: 💥 Use custom `PydanticDeprecationWarning` warning instead of the generic one
## Change Summary

Implement custom `PydanticDeprecationWarning`.
- Provides a single class to ignore or test for.
- Provides info on Pydantic version the deprecation was introduced in.
- Provides a link to the V2 migration guide for things deprecated in v2.0. 

## Related issue number

Fixes #5571

## Checklist

* [x] Unit tests for the changes exist
* [x] 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)
* [x] My PR is ready to review, **please add a comment including the phrase "please review" to assign reviewers**


Selected Reviewer: @Kludex

Problem statement:
Add migration documentation for filterwarnings/DeprecationWarning
### Initial Checks

- [X] I confirm that I'm using Pydantic V2 installed directly from the `main` branch, or equivalent

### Description

Having a list of all DeprecationWarning to use in pyproject/[tool.pytest.ini_options]/filterwarnings would allow taking care of things which actually break tests when moving to v2 and take care of the warnings once the real problems are resolved.


### Example Code

```Python
[tool.pytest.ini_options]
filterwarnings = [
# …
    # pydantic2
    'ignore:Support for "config" as "type" is deprecated and will be removed in a future version:DeprecationWarning',
    'ignore:Valid config keys have changed in V2:UserWarning',
    'ignore:The `update_forward_refs` method is deprecated; use `model_rebuild` instead.:DeprecationWarning',
    'ignore:The `parse_obj` method is deprecated; use `model_validate` instead.:DeprecationWarning',
    'ignore:The `construct` method is deprecated; use `model_construct` instead.:DeprecationWarning',
    'ignore:The `dict` method is deprecated; use `model_dump` instead.:DeprecationWarning',
    'ignore:`pydantic.config.Extra` is deprecated, use literal values instead .*$:DeprecationWarning',
    'ignore:Support for class-based `config` is deprecated, use ConfigDict instead.:DeprecationWarning',
    'ignore:The `__fields_set__` attribute is deprecated, use `model_fields_set` instead.:DeprecationWarning',
]
```


### Python, Pydantic & OS Version

```Text
pydantic version: 2.0a2
        pydantic-core version: 0.23.1 release build profile
                 install path: ~/venv/openapi3/lib/python3.10/site-packages/pydantic
               python version: 3.10.6 (main, Mar 10 2023, 10:55:28) [GCC 11.3.0]
                     platform: Linux-5.19.0-40-generic-x86_64-with-glibc2.35
     optional deps. installed: ['email-validator', 'typing-extensions']
```

Hints from issue discussion:
PR welcome if it's not too intrusive; would be best if there was a way to add a test for this that ensured we kept using it.

One possible thorn is that pycharm will show deprecation warning info in the hover popups _only_ if the first line of the deprecated function emits the warning with a string literal as the reason. So it will lightly degrade user experience to assign the error message to a variable and re-use it in the test etc.
Using sourcegraph I found there is tests for this already:
https://sourcegraph.com/search?q=context:global+repo:%5Egithub%5C.com/pydantic/pydantic%24+DeprecationWarning&patternType=standard&sm=1&groupBy=path

https://github.com/pydantic/pydantic/blob/main/tests/test_deprecated.py
Would it be possible to create a custom PydanticDeprecationWarning subclass and have all of the warnings raise that instead of a plain DeprecationWarning? Projects that still need to support v1 and can't address all of the deprecations could just add something like `ignore::pydantic.PydanticDeprecationWarning`  and be on their way.
I think this would be a great idea, it would also allow us to customize printing to include a link to docs, like errors.

Commit message: 🚑 Implement workaround for PyCharm unable displaying DeprecationWarning subclasses

Patch to review:
diff --git a/pydantic/__init__.py b/pydantic/__init__.py
index b71d47159cf..e18a78a62b1 100644
--- a/pydantic/__init__.py
+++ b/pydantic/__init__.py
@@ -42,6 +42,7 @@
 from .types import *
 from .validate_call import validate_call
 from .version import VERSION
+from .warnings import *
 
 __version__ = VERSION
 
@@ -183,6 +184,9 @@
     'TypeAdapter',
     # version
     'VERSION',
+    # warnings
+    'PydanticDeprecatedSince20',
+    'PydanticDeprecationWarning',
     # annotated handlers
     'GetCoreSchemaHandler',
     'GetJsonSchemaHandler',
diff --git a/pydantic/_internal/_config.py b/pydantic/_internal/_config.py
index 89aa72859b0..ecbf6bf4226 100644
--- a/pydantic/_internal/_config.py
+++ b/pydantic/_internal/_config.py
@@ -8,6 +8,11 @@
 
 from ..config import ConfigDict, ExtraValues, JsonSchemaExtraCallable
 from ..errors import PydanticUserError
+from ..warnings import PydanticDeprecatedSince20
+
+if not TYPE_CHECKING:
+    # See PyCharm issues PY-21915 and PY-51428
+    DeprecationWarning = PydanticDeprecatedSince20
 
 DEPRECATION_MESSAGE = 'Support for class-based `config` is deprecated, use ConfigDict instead.'
 
diff --git a/pydantic/_internal/_dataclasses.py b/pydantic/_internal/_dataclasses.py
index ed97f78c2aa..ea93749bbc0 100644
--- a/pydantic/_internal/_dataclasses.py
+++ b/pydantic/_internal/_dataclasses.py
@@ -12,6 +12,7 @@
 
 from ..errors import PydanticUndefinedAnnotation
 from ..fields import FieldInfo
+from ..warnings import PydanticDeprecatedSince20
 from . import _config, _decorators, _typing_extra
 from ._core_utils import collect_invalid_schemas, flatten_schema_defs, inline_schema_defs
 from ._fields import collect_dataclass_fields
@@ -41,6 +42,10 @@ class PydanticDataclass(StandardDataclass, typing.Protocol):
         __pydantic_config__: typing.ClassVar[ConfigDict]
         __pydantic_complete__: typing.ClassVar[bool]
 
+else:
+    # See PyCharm issues PY-21915 and PY-51428
+    DeprecationWarning = PydanticDeprecatedSince20
+
 
 def set_dataclass_fields(cls: type[StandardDataclass], types_namespace: dict[str, Any] | None = None) -> None:
     """Collect and set `cls.__pydantic_fields__`.
diff --git a/pydantic/_internal/_model_construction.py b/pydantic/_internal/_model_construction.py
index 1296e3e3f7a..f1f433fbddd 100644
--- a/pydantic/_internal/_model_construction.py
+++ b/pydantic/_internal/_model_construction.py
@@ -13,6 +13,7 @@
 
 from ..errors import PydanticUndefinedAnnotation, PydanticUserError
 from ..fields import Field, FieldInfo, ModelPrivateAttr, PrivateAttr
+from ..warnings import PydanticDeprecatedSince20
 from ._config import ConfigWrapper
 from ._core_utils import collect_invalid_schemas, flatten_schema_defs, inline_schema_defs
 from ._decorators import ComputedFieldInfo, DecoratorInfos, PydanticDescriptorProxy
@@ -28,6 +29,9 @@
     from inspect import Signature
 
     from ..main import BaseModel
+else:
+    # See PyCharm issues PY-21915 and PY-51428
+    DeprecationWarning = PydanticDeprecatedSince20
 
 
 IGNORED_TYPES: tuple[Any, ...] = (
diff --git a/pydantic/config.py b/pydantic/config.py
index de16fc16b9c..319127ece82 100644
--- a/pydantic/config.py
+++ b/pydantic/config.py
@@ -1,13 +1,18 @@
 """Configuration for Pydantic models."""
 from __future__ import annotations as _annotations
 
-from typing import Any, Callable, overload
+from typing import TYPE_CHECKING, Any, Callable, overload
 from warnings import warn
 
 from typing_extensions import Literal, Protocol, TypedDict
 
 from ._migration import getattr_migration
 from .deprecated.config import BaseConfig
+from .warnings import PydanticDeprecatedSince20
+
+if not TYPE_CHECKING:
+    # See PyCharm issues PY-21915 and PY-51428
+    DeprecationWarning = PydanticDeprecatedSince20
 
 __all__ = 'BaseConfig', 'ConfigDict', 'Extra'
 
diff --git a/pydantic/deprecated/class_validators.py b/pydantic/deprecated/class_validators.py
index b6a2a248e4d..e8c9cddd3ff 100644
--- a/pydantic/deprecated/class_validators.py
+++ b/pydantic/deprecated/class_validators.py
@@ -11,6 +11,7 @@
 
 from .._internal import _decorators, _decorators_v1
 from ..errors import PydanticUserError
+from ..warnings import PydanticDeprecatedSince20
 
 _ALLOW_REUSE_WARNING_MESSAGE = '`allow_reuse` is deprecated and will be ignored; it should no longer be necessary'
 
@@ -72,6 +73,9 @@ def __call__(
         _V1RootValidatorClsMethod,
         _PartialClsOrStaticMethod,
     )
+else:
+    # See PyCharm issues PY-21915 and PY-51428
+    DeprecationWarning = PydanticDeprecatedSince20
 
 
 def validator(
diff --git a/pydantic/deprecated/config.py b/pydantic/deprecated/config.py
index cafa6979f34..a6e23f4369a 100644
--- a/pydantic/deprecated/config.py
+++ b/pydantic/deprecated/config.py
@@ -1,11 +1,16 @@
 from __future__ import annotations as _annotations
 
 import warnings
-from typing import Any
+from typing import TYPE_CHECKING, Any
 
 from typing_extensions import deprecated
 
 from .._internal import _config
+from ..warnings import PydanticDeprecatedSince20
+
+if not TYPE_CHECKING:
+    # See PyCharm issues PY-21915 and PY-51428
+    DeprecationWarning = PydanticDeprecatedSince20
 
 __all__ = ('BaseConfig',)
 
diff --git a/pydantic/deprecated/decorator.py b/pydantic/deprecated/decorator.py
index 153b01f4642..9034380260f 100644
--- a/pydantic/deprecated/decorator.py
+++ b/pydantic/deprecated/decorator.py
@@ -9,6 +9,11 @@
 from ..errors import PydanticUserError
 from ..functional_validators import field_validator
 from ..main import BaseModel, create_model
+from ..warnings import PydanticDeprecatedSince20
+
+if not TYPE_CHECKING:
+    # See PyCharm issues PY-21915 and PY-51428
+    DeprecationWarning = PydanticDeprecatedSince20
 
 __all__ = ('validate_arguments',)
 
diff --git a/pydantic/deprecated/json.py b/pydantic/deprecated/json.py
index c3b0d1daefd..b2450648e9d 100644
--- a/pydantic/deprecated/json.py
+++ b/pydantic/deprecated/json.py
@@ -7,7 +7,7 @@
 from pathlib import Path
 from re import Pattern
 from types import GeneratorType
-from typing import Any, Callable, Dict, Type, Union
+from typing import TYPE_CHECKING, Any, Callable, Dict, Type, Union
 from uuid import UUID
 
 from typing_extensions import deprecated
@@ -15,6 +15,11 @@
 from ..color import Color
 from ..networks import NameEmail
 from ..types import SecretBytes, SecretStr
+from ..warnings import PydanticDeprecatedSince20
+
+if not TYPE_CHECKING:
+    # See PyCharm issues PY-21915 and PY-51428
+    DeprecationWarning = PydanticDeprecatedSince20
 
 __all__ = 'pydantic_encoder', 'custom_pydantic_encoder', 'timedelta_isoformat'
 
diff --git a/pydantic/deprecated/parse.py b/pydantic/deprecated/parse.py
index b5d3c1ed589..5fe9f46eaa6 100644
--- a/pydantic/deprecated/parse.py
+++ b/pydantic/deprecated/parse.py
@@ -5,10 +5,16 @@
 import warnings
 from enum import Enum
 from pathlib import Path
-from typing import Any, Callable
+from typing import TYPE_CHECKING, Any, Callable
 
 from typing_extensions import deprecated
 
+from ..warnings import PydanticDeprecatedSince20
+
+if not TYPE_CHECKING:
+    # See PyCharm issues PY-21915 and PY-51428
+    DeprecationWarning = PydanticDeprecatedSince20
+
 
 class Protocol(str, Enum):
     json = 'json'
diff --git a/pydantic/deprecated/tools.py b/pydantic/deprecated/tools.py
index b91b20a4133..3eed48b299a 100644
--- a/pydantic/deprecated/tools.py
+++ b/pydantic/deprecated/tools.py
@@ -2,12 +2,17 @@
 
 import json
 import warnings
-from typing import Any, Callable, Type, TypeVar, Union
+from typing import TYPE_CHECKING, Any, Callable, Type, TypeVar, Union
 
 from typing_extensions import deprecated
 
 from ..json_schema import DEFAULT_REF_TEMPLATE, GenerateJsonSchema
 from ..type_adapter import TypeAdapter
+from ..warnings import PydanticDeprecatedSince20
+
+if not TYPE_CHECKING:
+    # See PyCharm issues PY-21915 and PY-51428
+    DeprecationWarning = PydanticDeprecatedSince20
 
 __all__ = 'parse_obj_as', 'schema_of', 'schema_json_of'
 
diff --git a/pydantic/fields.py b/pydantic/fields.py
index 83e4b657ae0..be435b3bc2d 100644
--- a/pydantic/fields.py
+++ b/pydantic/fields.py
@@ -18,9 +18,13 @@
 from . import types
 from ._internal import _decorators, _fields, _generics, _internal_dataclass, _repr, _typing_extra, _utils
 from .errors import PydanticUserError
+from .warnings import PydanticDeprecatedSince20
 
 if typing.TYPE_CHECKING:
     from ._internal._repr import ReprArgs
+else:
+    # See PyCharm issues PY-21915 and PY-51428
+    DeprecationWarning = PydanticDeprecatedSince20
 
 
 class _FromFieldInfoInputs(typing_extensions.TypedDict, total=False):
diff --git a/pydantic/main.py b/pydantic/main.py
index 917461ffb05..3ec1a0981bc 100644
--- a/pydantic/main.py
+++ b/pydantic/main.py
@@ -31,6 +31,7 @@
 from .errors import PydanticUndefinedAnnotation, PydanticUserError
 from .fields import ComputedFieldInfo, FieldInfo, ModelPrivateAttr
 from .json_schema import DEFAULT_REF_TEMPLATE, GenerateJsonSchema, JsonSchemaMode, JsonSchemaValue, model_json_schema
+from .warnings import PydanticDeprecatedSince20
 
 if typing.TYPE_CHECKING:
     from inspect import Signature
@@ -47,6 +48,9 @@
     Model = typing.TypeVar('Model', bound='BaseModel')
     # should be `set[int] | set[str] | dict[int, IncEx] | dict[str, IncEx] | None`, but mypy can't cope
     IncEx: typing_extensions.TypeAlias = 'set[int] | set[str] | dict[int, Any] | dict[str, Any] | None'
+else:
+    # See PyCharm issues PY-21915 and PY-51428
+    DeprecationWarning = PydanticDeprecatedSince20
 
 __all__ = 'BaseModel', 'create_model'
 
diff --git a/pydantic/warnings.py b/pydantic/warnings.py
new file mode 100644
index 00000000000..fdcffac417f
--- /dev/null
+++ b/pydantic/warnings.py
@@ -0,0 +1,45 @@
+"""Pydantic-specific warnings."""
+from __future__ import annotations as _annotations
+
+__all__ = 'PydanticDeprecatedSince20', 'PydanticDeprecationWarning'
+
+
+class PydanticDeprecationWarning(DeprecationWarning):
+    """A Pydantic specific deprecation warning.
+
+    This warning is raised when using deprecated functionality in Pydantic. It provides information on when the
+    deprecation was introduced and the expected version in which the corresponding functionality will be removed.
+
+    Attributes:
+        message: Description of the warning.
+        since: Pydantic version in what the deprecation was introduced.
+        expected_removal: Pydantic version in what the corresponding functionality expected to be removed.
+    """
+
+    message: str
+    since: tuple[int, int]
+    expected_removal: tuple[int, int]
+
+    def __init__(
+        self, message: str, *args: object, since: tuple[int, int], expected_removal: tuple[int, int] | None = None
+    ) -> None:
+        super().__init__(message, *args)
+        self.message = message.rstrip('.')
+        self.since = since
+        self.expected_removal = expected_removal if expected_removal is not None else (since[0] + 1, 0)
+
+    def __str__(self) -> str:
+        message = (
+            f'{self.message}. Deprecated in Pydantic V{self.since[0]}.{self.since[1]}'
+            f' to be removed in V{self.expected_removal[0]}.{self.expected_removal[1]}.'
+        )
+        if self.since == (2, 0):
+            message += ' See Pydantic V2 Migration Guide at https://errors.pydantic.dev/v2.0/migration/'
+        return message
+
+
+class PydanticDeprecatedSince20(PydanticDeprecationWarning):
+    """A specific `PydanticDeprecationWarning` subclass defining functionality deprecated since Pydantic 2.0."""
+
+    def __init__(self, message: str, *args: object) -> None:
+        super().__init__(message, *args, since=(2, 0))
diff --git a/tests/test_warnings.py b/tests/test_warnings.py
new file mode 100644
index 00000000000..28c3c5898a7
--- /dev/null
+++ b/tests/test_warnings.py
@@ -0,0 +1,40 @@
+from pydantic import PydanticDeprecatedSince20, PydanticDeprecationWarning
+
+
+def test_pydantic_deprecation_warning():
+    warning = PydanticDeprecationWarning('Warning message', 'Arbitrary argument', since=(2, 1), expected_removal=(4, 0))
+
+    assert str(warning) == 'Warning message. Deprecated in Pydantic V2.1 to be removed in V4.0.'
+    assert warning.args[0] == 'Warning message'
+    assert warning.args[1] == 'Arbitrary argument'
+
+
+def test_pydantic_deprecation_warning_tailing_dot_in_message():
+    warning = PydanticDeprecationWarning('Warning message.', since=(2, 1), expected_removal=(4, 0))
+
+    assert str(warning) == 'Warning message. Deprecated in Pydantic V2.1 to be removed in V4.0.'
+    assert warning.args[0] == 'Warning message.'
+
+
+def test_pydantic_deprecation_warning_calculated_expected_removal():
+    warning = PydanticDeprecationWarning('Warning message', since=(2, 1))
+
+    assert str(warning) == 'Warning message. Deprecated in Pydantic V2.1 to be removed in V3.0.'
+
+
+def test_pydantic_deprecation_warning_2_0_migration_guide_link():
+    warning = PydanticDeprecationWarning('Warning message', since=(2, 0))
+
+    assert (
+        str(warning)
+        == 'Warning message. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/v2.0/migration/'
+    )
+
+
+def test_pydantic_deprecated_since_2_0_warning():
+    warning = PydanticDeprecatedSince20('Warning message')
+
+    assert isinstance(warning, PydanticDeprecationWarning)
+    assert warning.message == 'Warning message'
+    assert warning.since == (2, 0)
+    assert warning.expected_removal == (3, 0)


Code review comments:
diff_hunk: @@ -8,6 +8,11 @@
 
 from ..config import ConfigDict, ExtraValues, JsonSchemaExtraCallable
 from ..errors import PydanticUserError
+from ..warnings import PydanticDeprecatedSince20
+
+if not TYPE_CHECKING:
+    # See PyCharm issues PY-21915 and PY-51428
+    DeprecationWarning = PydanticDeprecatedSince20
path: pydantic/_internal/_config.py
text: @author:
This makes it work in PyCharm. My concern is that we won't be able using more than one `DeprecationWarning` subclass in a single module file.

@user1:
🤷, shame but no other way. Technically we could use multiple subclasses, we'd just need to assign different classes to `DeprecationWarning` through the file.

Ugly I know, but not the end of the world

diff_hunk: @@ -0,0 +1,45 @@
+"""Pydantic-specific warnings."""
+from __future__ import annotations as _annotations
+
+__all__ = 'PydanticDeprecatedSince20', 'PydanticDeprecationWarning'
+
+
+class PydanticDeprecationWarning(DeprecationWarning):
+    """A Pydantic specific deprecation warning.
+
+    This warning is raised when using deprecated functionality in Pydantic. It provides information on when the
+    deprecation was introduced and the expected version in which the corresponding functionality will be removed.
+
+    Attributes:
+        message: Description of the warning.
+        since: Pydantic version in what the deprecation was introduced.
+        expected_removal: Pydantic version in what the corresponding functionality expected to be removed.
+    """
+
+    message: str
+    since: tuple[int, int]
+    expected_removal: tuple[int, int]
+
+    def __init__(
+        self, message: str, *args: object, since: tuple[int, int], expected_removal: tuple[int, int] | None = None
+    ) -> None:
+        super().__init__(message, *args)
+        self.message = message.rstrip('.')
+        self.since = since
+        self.expected_removal = expected_removal if expected_removal is not None else (since[0] + 1, 0)
+
+    def __str__(self) -> str:
+        message = (
+            f'{self.message}. Deprecated in Pydantic V{self.since[0]}.{self.since[1]}'
+            f' to be removed in V{self.expected_removal[0]}.{self.expected_removal[1]}.'
+        )
+        if self.since == (2, 0):
+            message += ' See Pydantic V2 Migration Guide at https://errors.pydantic.dev/v2.0/migration/'
+        return message
+
+
+class PydanticDeprecatedSince20(PydanticDeprecationWarning):
+    """A specific `PydanticDeprecationWarning` subclass defining functionality deprecated since Pydantic 2.0."""
+
+    def __init__(self, message: str, *args: object) -> None:
+        super().__init__(message, *args, since=(2, 0))
path: pydantic/warnings.py
text: @user1:
```suggestion
class PydanticDeprecatedV2(PydanticDeprecationWarning):
    ...
```

diff_hunk: @@ -0,0 +1,45 @@
+"""Pydantic-specific warnings."""
+from __future__ import annotations as _annotations
+
+__all__ = 'PydanticDeprecatedSince20', 'PydanticDeprecationWarning'
+
+
+class PydanticDeprecationWarning(DeprecationWarning):
+    """A Pydantic specific deprecation warning.
+
+    This warning is raised when using deprecated functionality in Pydantic. It provides information on when the
+    deprecation was introduced and the expected version in which the corresponding functionality will be removed.
+
+    Attributes:
+        message: Description of the warning.
+        since: Pydantic version in what the deprecation was introduced.
+        expected_removal: Pydantic version in what the corresponding functionality expected to be removed.
+    """
+
+    message: str
+    since: tuple[int, int]
+    expected_removal: tuple[int, int]
+
+    def __init__(
+        self, message: str, *args: object, since: tuple[int, int], expected_removal: tuple[int, int] | None = None
+    ) -> None:
+        super().__init__(message, *args)
+        self.message = message.rstrip('.')
+        self.since = since
+        self.expected_removal = expected_removal if expected_removal is not None else (since[0] + 1, 0)
+
+    def __str__(self) -> str:
+        message = (
+            f'{self.message}. Deprecated in Pydantic V{self.since[0]}.{self.since[1]}'
+            f' to be removed in V{self.expected_removal[0]}.{self.expected_removal[1]}.'
+        )
+        if self.since == (2, 0):
+            message += ' See Pydantic V2 Migration Guide at https://errors.pydantic.dev/v2.0/migration/'
path: pydantic/warnings.py
text: @user1:
We should use the actual version from `version.py` here, not `v2.0`.