Repository: pydantic/pydantic
Pull request: Add migration error messages to V2
<!-- Thank you for your contribution! -->
<!-- Unless your change is trivial, please create an issue to discuss the change before creating a PR -->
<!-- We're currently in the process of rewriting pydantic in preparation for V2, see https://docs.pydantic.dev/blog/pydantic-v2/. -->
<!-- **Note:** if you're making a pull request to fix pydantic v1.10, please make it against the `1.10.X-fixes` branch. -->

## Change Summary

This PR does two things:
1. Add deprecation warnings to the `deprecated` namespace.
2. Tries to improve the user experience on imports that were changed/removed from v1 to v2.

For 2, I've used the scripts on https://gist.github.com/Kludex/9087acb5af2a940bd3fcefce96799412 as a base line.

## Deprecation Warning on IDEs

The `deprecated` from [PEP702](https://peps.python.org/pep-0702/) is used for VSCode, and the `warnings.warn` is used for PyCharm. As a note, PyCharm doesn't "work" properly when using variables on warnings i.e. `warn(_MY_DESCRIPTION, DeprecationWarning)` doesn't work.

On VSCode:
![image](https://user-images.githubusercontent.com/7353520/232453508-dfb13f25-304a-4637-9335-c6f8758d03a7.png)

On Pycharm:
![image](https://user-images.githubusercontent.com/7353520/232453620-f5dc98ae-5383-454c-8bbc-0409869c42be.png)

## Related issue number

- Closes [#5016](https://github.com/pydantic/pydantic/issues/5016)

## Questions

1. Should I test that `__deprecated__` field is used on the objects that are using the deprecated decorator?

## Missing

- [ ] Tests for `DeprecationWarning`s introduced.

## Checklist

* [X] Unit tests for the changes exist
* [X] Tests pass on CI and coverage remains at 100%
* [X] 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: @adriangb

Problem statement:
Add nice error message when attempting to import removed modules/functions in v2
If a user attempts to import removed functions, from removed modules, or both, we should give a better error message than a plain ImportError. Ideally the error message will include some information about the migration path, or possibly a request to create an issue if the changes break something in their codebase in a way that is difficult for them to resolve.

As an example, it would be preferable to get such an error message when attempting to run `from pydantic.generics import replace_types`.

Probably anything that could be construed as part of the "public" api of v1.X (i.e., doesn't use leading underscores in the name or import path) should be handled neatly.

Hints from issue discussion:
Keen to work on this! Will require a fair bit of discussion on how we wish to inform the user, how long this will be persisted for etc.

I have an initial idea (see PR https://github.com/pydantic/pydantic/pull/5113) 


Using this [PEP-562](https://github.com/orgs/pydantic/projects/1/views/3?pane=issue&itemId=19807370) standard, we could add a `__getattr__` method to all files (and the old file names - noting this will be all that is in them) 

Let me know your thoughts :) 

We could also monkeypatch internals, this seems like a nice way to do it, but potentially risky... 

```shell
Traceback (most recent call last):
  File "/projects/pydantic/caller.py", line 1, in <module>
    from pydantic import Required
  File "/projects/pydantic/import_helper.py", line 30, in _handle_fromlist_override
    raise exception
pydantic.import_helper.V2MigrationRemoved: pydantic.Required has been removed in favour of ...
```

This way, any deleted files can remain deleted as it will be intercepted before the `__getattr__` call
@samuelcolvin @dmontagu thoughts?

Commit message: Apply suggestions from code review

Co-authored-by: David Montague <35119617+dmontagu@users.noreply.github.com>

Patch to review:
diff --git a/docs/usage/errors.md b/docs/usage/errors.md
index 01f55d9c6b5..cf3080a4748 100644
--- a/docs/usage/errors.md
+++ b/docs/usage/errors.md
@@ -69,6 +69,10 @@ TODO
 
 TODO
 
+## Import error {#import-error}
+
+This error is raised when you try to import an object that was available in V1 but has been removed in V2.
+
 ## create_model field definitions {#create-model-field-definitions}
 
 TODO
diff --git a/pydantic/__init__.py b/pydantic/__init__.py
index 1fbefc51598..0bc56391e67 100644
--- a/pydantic/__init__.py
+++ b/pydantic/__init__.py
@@ -9,6 +9,7 @@
 )
 
 from . import dataclasses
+from ._migration import getattr_migration
 from .analyzed_type import AnalyzedType
 from .config import ConfigDict, Extra
 from .decorators import field_serializer, field_validator, model_serializer, root_validator, validator
@@ -52,6 +53,7 @@
     'ValidationError',
     'PydanticUserError',
     'PydanticSchemaGenerationError',
+    'PydanticImportError',
     'PydanticUndefinedAnnotation',
     # fields
     'AliasPath',
@@ -131,3 +133,6 @@
     # version
     'VERSION',
 ]
+
+
+__getattr__ = getattr_migration(__name__)
diff --git a/pydantic/_migration.py b/pydantic/_migration.py
new file mode 100644
index 00000000000..1afd4f0c693
--- /dev/null
+++ b/pydantic/_migration.py
@@ -0,0 +1,268 @@
+import warnings
+from typing import Any, Callable
+
+from ._internal._validators import import_string
+
+MOVED_IN_V2 = {
+    'pydantic.utils.version_info': 'pydantic.version.version_info',
+    'pydantic.error_wrappers.ValidationError': 'pydantic.ValidationError',
+}
+
+DEPRECATED_MOVED_IN_V2 = {
+    'pydantic.tools.schema_of': 'pydantic.deprecated.tools.schema_of',
+    'pydantic.tools.parse_obj_as': 'pydantic.deprecated.tools.parse_obj_as',
+    'pydantic.tools.schema_json_of': 'pydantic.deprecated.tools.schema_json_of',
+    'pydantic.json.pydantic_encoder': 'pydantic.deprecated.json.pydantic_encoder',
+    'pydantic.validate_arguments': 'pydantic.deprecated.decorator.validate_arguments',
+    'pydantic.json.custom_pydantic_encoder': 'pydantic.deprecated.json.custom_pydantic_encoder',
+    'pydantic.json.timedelta_isoformat': 'pydantic.deprecated.json.timedelta_isoformat',
+    'pydantic.decorator.validate_arguments': 'pydantic.deprecated.decorator.validate_arguments',
+}
+
+REMOVED_IN_V2 = {
+    'pydantic.BaseSettings',
+    'pydantic.ConstrainedBytes',
+    'pydantic.ConstrainedDate',
+    'pydantic.ConstrainedDecimal',
+    'pydantic.ConstrainedFloat',
+    'pydantic.ConstrainedFrozenSet',
+    'pydantic.ConstrainedInt',
+    'pydantic.ConstrainedList',
+    'pydantic.ConstrainedSet',
+    'pydantic.ConstrainedStr',
+    'pydantic.JsonWrapper',
+    'pydantic.NoneBytes',
+    'pydantic.NoneStr',
+    'pydantic.NoneStrBytes',
+    'pydantic.Protocol',
+    'pydantic.PyObject',
+    'pydantic.Required',
+    'pydantic.StrBytes',
+    'pydantic.compiled',
+    'pydantic.config.get_config',
+    'pydantic.config.inherit_config',
+    'pydantic.config.prepare_config',
+    'pydantic.create_model_from_namedtuple',
+    'pydantic.create_model_from_typeddict',
+    'pydantic.dataclasses.create_pydantic_model_from_dataclass',
+    'pydantic.dataclasses.make_dataclass_validator',
+    'pydantic.dataclasses.set_validation',
+    'pydantic.error_wrappers.ErrorWrapper',
+    'pydantic.errors.AnyStrMaxLengthError',
+    'pydantic.errors.AnyStrMinLengthError',
+    'pydantic.errors.ArbitraryTypeError',
+    'pydantic.errors.BoolError',
+    'pydantic.errors.BytesError',
+    'pydantic.errors.CallableError',
+    'pydantic.errors.ClassError',
+    'pydantic.errors.ColorError',
+    'pydantic.errors.ConfigError',
+    'pydantic.errors.DataclassTypeError',
+    'pydantic.errors.DateError',
+    'pydantic.errors.DateNotInTheFutureError',
+    'pydantic.errors.DateNotInThePastError',
+    'pydantic.errors.DateTimeError',
+    'pydantic.errors.DecimalError',
+    'pydantic.errors.DecimalIsNotFiniteError',
+    'pydantic.errors.DecimalMaxDigitsError',
+    'pydantic.errors.DecimalMaxPlacesError',
+    'pydantic.errors.DecimalWholeDigitsError',
+    'pydantic.errors.DictError',
+    'pydantic.errors.DurationError',
+    'pydantic.errors.EmailError',
+    'pydantic.errors.EnumError',
+    'pydantic.errors.EnumMemberError',
+    'pydantic.errors.ExtraError',
+    'pydantic.errors.FloatError',
+    'pydantic.errors.FrozenSetError',
+    'pydantic.errors.FrozenSetMaxLengthError',
+    'pydantic.errors.FrozenSetMinLengthError',
+    'pydantic.errors.HashableError',
+    'pydantic.errors.IPv4AddressError',
+    'pydantic.errors.IPv4InterfaceError',
+    'pydantic.errors.IPv4NetworkError',
+    'pydantic.errors.IPv6AddressError',
+    'pydantic.errors.IPv6InterfaceError',
+    'pydantic.errors.IPv6NetworkError',
+    'pydantic.errors.IPvAnyAddressError',
+    'pydantic.errors.IPvAnyInterfaceError',
+    'pydantic.errors.IPvAnyNetworkError',
+    'pydantic.errors.IntEnumError',
+    'pydantic.errors.IntegerError',
+    'pydantic.errors.InvalidByteSize',
+    'pydantic.errors.InvalidByteSizeUnit',
+    'pydantic.errors.InvalidDiscriminator',
+    'pydantic.errors.InvalidLengthForBrand',
+    'pydantic.errors.JsonError',
+    'pydantic.errors.JsonTypeError',
+    'pydantic.errors.ListError',
+    'pydantic.errors.ListMaxLengthError',
+    'pydantic.errors.ListMinLengthError',
+    'pydantic.errors.ListUniqueItemsError',
+    'pydantic.errors.LuhnValidationError',
+    'pydantic.errors.MissingDiscriminator',
+    'pydantic.errors.MissingError',
+    'pydantic.errors.NoneIsAllowedError',
+    'pydantic.errors.NoneIsNotAllowedError',
+    'pydantic.errors.NotDigitError',
+    'pydantic.errors.NotNoneError',
+    'pydantic.errors.NumberNotGeError',
+    'pydantic.errors.NumberNotGtError',
+    'pydantic.errors.NumberNotLeError',
+    'pydantic.errors.NumberNotLtError',
+    'pydantic.errors.NumberNotMultipleError',
+    'pydantic.errors.PathError',
+    'pydantic.errors.PathNotADirectoryError',
+    'pydantic.errors.PathNotAFileError',
+    'pydantic.errors.PathNotExistsError',
+    'pydantic.errors.PatternError',
+    'pydantic.errors.PyObjectError',
+    'pydantic.errors.PydanticTypeError',
+    'pydantic.errors.PydanticValueError',
+    'pydantic.errors.SequenceError',
+    'pydantic.errors.SetError',
+    'pydantic.errors.SetMaxLengthError',
+    'pydantic.errors.SetMinLengthError',
+    'pydantic.errors.StrError',
+    'pydantic.errors.StrRegexError',
+    'pydantic.errors.StrictBoolError',
+    'pydantic.errors.SubclassError',
+    'pydantic.errors.TimeError',
+    'pydantic.errors.TupleError',
+    'pydantic.errors.TupleLengthError',
+    'pydantic.errors.UUIDError',
+    'pydantic.errors.UUIDVersionError',
+    'pydantic.errors.UrlError',
+    'pydantic.errors.UrlExtraError',
+    'pydantic.errors.UrlHostError',
+    'pydantic.errors.UrlHostTldError',
+    'pydantic.errors.UrlPortError',
+    'pydantic.errors.UrlSchemeError',
+    'pydantic.errors.UrlSchemePermittedError',
+    'pydantic.errors.UrlUserInfoError',
+    'pydantic.errors.WrongConstantError',
+    'pydantic.main.validate_model',
+    'pydantic.networks.stricturl',
+    'pydantic.parse_file_as',
+    'pydantic.parse_raw_as',
+    'pydantic.stricturl',
+    'pydantic.tools.parse_file_as',
+    'pydantic.tools.parse_raw_as',
+    'pydantic.types.ConstrainedBytes',
+    'pydantic.types.ConstrainedDate',
+    'pydantic.types.ConstrainedDecimal',
+    'pydantic.types.ConstrainedFloat',
+    'pydantic.types.ConstrainedFrozenSet',
+    'pydantic.types.ConstrainedInt',
+    'pydantic.types.ConstrainedList',
+    'pydantic.types.ConstrainedSet',
+    'pydantic.types.ConstrainedStr',
+    'pydantic.types.JsonWrapper',
+    'pydantic.types.NoneBytes',
+    'pydantic.types.NoneStr',
+    'pydantic.types.NoneStrBytes',
+    'pydantic.types.PyObject',
+    'pydantic.types.StrBytes',
+    'pydantic.typing.AbstractSetIntStr',
+    'pydantic.typing.AnyCallable',
+    'pydantic.typing.AnyClassMethod',
+    'pydantic.typing.CallableGenerator',
+    'pydantic.typing.DictAny',
+    'pydantic.typing.DictIntStrAny',
+    'pydantic.typing.DictStrAny',
+    'pydantic.typing.IntStr',
+    'pydantic.typing.ListStr',
+    'pydantic.typing.MappingIntStrAny',
+    'pydantic.typing.NoArgAnyCallable',
+    'pydantic.typing.NoneType',
+    'pydantic.typing.ReprArgs',
+    'pydantic.typing.SetStr',
+    'pydantic.typing.StrPath',
+    'pydantic.typing.TupleGenerator',
+    'pydantic.typing.WithArgsTypes',
+    'pydantic.typing.all_literal_values',
+    'pydantic.typing.display_as_type',
+    'pydantic.typing.get_all_type_hints',
+    'pydantic.typing.get_args',
+    'pydantic.typing.get_origin',
+    'pydantic.typing.get_sub_types',
+    'pydantic.typing.is_callable_type',
+    'pydantic.typing.is_classvar',
+    'pydantic.typing.is_finalvar',
+    'pydantic.typing.is_literal_type',
+    'pydantic.typing.is_namedtuple',
+    'pydantic.typing.is_new_type',
+    'pydantic.typing.is_none_type',
+    'pydantic.typing.is_typeddict',
+    'pydantic.typing.is_typeddict_special',
+    'pydantic.typing.is_union',
+    'pydantic.typing.new_type_supertype',
+    'pydantic.typing.resolve_annotations',
+    'pydantic.typing.typing_base',
+    'pydantic.typing.update_field_forward_refs',
+    'pydantic.typing.update_model_forward_refs',
+    'pydantic.utils.ClassAttribute',
+    'pydantic.utils.DUNDER_ATTRIBUTES',
+    'pydantic.utils.GetterDict',
+    'pydantic.utils.PyObjectStr',
+    'pydantic.utils.ROOT_KEY',
+    'pydantic.utils.Representation',
+    'pydantic.utils.ValueItems',
+    'pydantic.utils.almost_equal_floats',
+    'pydantic.utils.deep_update',
+    'pydantic.utils.get_discriminator_alias_and_values',
+    'pydantic.utils.get_model',
+    'pydantic.utils.get_unique_discriminator_alias',
+    'pydantic.utils.import_string',
+    'pydantic.utils.in_ipython',
+    'pydantic.utils.is_valid_field',
+    'pydantic.utils.is_valid_identifier',
+    'pydantic.utils.lenient_isinstance',
+    'pydantic.utils.lenient_issubclass',
+    'pydantic.utils.path_type',
+    'pydantic.utils.sequence_like',
+    'pydantic.utils.smart_deepcopy',
+    'pydantic.utils.to_camel',
+    'pydantic.utils.update_not_none',
+    'pydantic.utils.validate_field_name',
+    'pydantic.validate_model',
+}
+
+
+def getattr_migration(module: str) -> Callable[[str], Any]:
+    """Implement PEP 562 for objects that were either moved or removed on the migration
+    to V2.
+
+    Args:
+        module: The module name.
+
+    Returns:
+        A callable that will raise an error if the object is not found.
+    """
+    # This avoids circular import with errors.py.
+    from .errors import PydanticImportError
+
+    def wrapper(name: str) -> object:
+        """Raise an error if the object is not found, or warn if it was moved.
+
+        In case it was moved, it still returns the object.
+
+        Args:
+            name: The object name.
+
+        Returns:
+            The object.
+        """
+        import_path = f'{module}.{name}'
+        if import_path in MOVED_IN_V2.keys():
+            new_location = MOVED_IN_V2[import_path]
+            warnings.warn(f'`{import_path}` has been moved to `{new_location}`.')
+            return import_string(MOVED_IN_V2[import_path])
+        if import_path in DEPRECATED_MOVED_IN_V2.keys():
+            # skip the warning here because a deprecation warning will be raised elsewhere
+            return import_string(DEPRECATED_MOVED_IN_V2[import_path])
+        if import_path in REMOVED_IN_V2:
+            raise PydanticImportError(f'`{import_path}` has been removed in V2.')
+        raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
+
+    return wrapper
diff --git a/pydantic/class_validators.py b/pydantic/class_validators.py
new file mode 100644
index 00000000000..d2239407ccc
--- /dev/null
+++ b/pydantic/class_validators.py
@@ -0,0 +1,3 @@
+from ._migration import getattr_migration
+
+__getattr__ = getattr_migration(__name__)
diff --git a/pydantic/config.py b/pydantic/config.py
index d10b6a27236..8a4106530f8 100644
--- a/pydantic/config.py
+++ b/pydantic/config.py
@@ -6,6 +6,7 @@
 
 from typing_extensions import Literal, TypedDict
 
+from ._migration import getattr_migration
 from .deprecated.config import BaseConfig
 
 __all__ = 'BaseConfig', 'ConfigDict', 'Extra'
@@ -20,6 +21,7 @@ def __getattribute__(self, __name: str) -> Any:
         warn(
             '`pydantic.config.Extra` is deprecated, use literal values instead' " (e.g. `extra='allow'`)",
             DeprecationWarning,
+            stacklevel=2,
         )
         return super().__getattribute__(__name)
 
@@ -91,3 +93,6 @@ class ConfigDict(TypedDict, total=False):
     validate_default: bool
     # whether to validate the return value from call validator
     validate_return: bool
+
+
+__getattr__ = getattr_migration(__name__)
diff --git a/pydantic/dataclasses.py b/pydantic/dataclasses.py
index eacff024a82..b65f6e1862e 100644
--- a/pydantic/dataclasses.py
+++ b/pydantic/dataclasses.py
@@ -13,6 +13,7 @@
 from ._internal import _config, _decorators
 from ._internal import _dataclasses as _pydantic_dataclasses
 from ._internal._dataclasses import is_builtin_dataclass
+from ._migration import getattr_migration
 from .config import ConfigDict
 from .fields import Field
 
@@ -200,3 +201,6 @@ def create_dataclass(cls: type[Any]) -> type[PydanticDataclass]:
         return create_dataclass
 
     return create_dataclass(_cls)
+
+
+__getattr__ = getattr_migration(__name__)
diff --git a/pydantic/decorator.py b/pydantic/decorator.py
new file mode 100644
index 00000000000..d2239407ccc
--- /dev/null
+++ b/pydantic/decorator.py
@@ -0,0 +1,3 @@
+from ._migration import getattr_migration
+
+__getattr__ = getattr_migration(__name__)
diff --git a/pydantic/deprecated/config.py b/pydantic/deprecated/config.py
index f601b2f3235..051208949f1 100644
--- a/pydantic/deprecated/config.py
+++ b/pydantic/deprecated/config.py
@@ -3,6 +3,8 @@
 import warnings
 from typing import Any
 
+from typing_extensions import deprecated
+
 from .._internal import _config
 
 __all__ = ('BaseConfig',)
@@ -18,6 +20,7 @@ def __getattr__(self, item: str) -> Any:
             raise AttributeError(f"type object '{self.__name__}' has no attribute {exc}") from exc
 
 
+@deprecated('BaseConfig is deprecated. Use the `pydantic.ConfigDict` instead.')
 class BaseConfig(metaclass=_ConfigMetaclass):
     """
     This class is only retained for backwards compatibility.
diff --git a/pydantic/deprecated/decorator.py b/pydantic/deprecated/decorator.py
index 9526c0bcc21..6be54d4d239 100644
--- a/pydantic/deprecated/decorator.py
+++ b/pydantic/deprecated/decorator.py
@@ -2,6 +2,8 @@
 from functools import wraps
 from typing import TYPE_CHECKING, Any, Callable, Dict, List, Mapping, Optional, Tuple, Type, TypeVar, Union, overload
 
+from typing_extensions import deprecated
+
 from .._internal import _config, _typing_extra, _utils
 from ..decorators import field_validator
 from ..errors import PydanticUserError
@@ -17,11 +19,13 @@
 
 
 @overload
+@deprecated('The `validate_arguments` method is deprecated; use `validate_call` instead.')
 def validate_arguments(func: None = None, *, config: 'ConfigType' = None) -> Callable[['AnyCallableT'], 'AnyCallableT']:
     ...
 
 
 @overload
+@deprecated('The `validate_arguments` method is deprecated; use `validate_call` instead.')
 def validate_arguments(func: 'AnyCallableT') -> 'AnyCallableT':
     ...
 
@@ -30,7 +34,9 @@ def validate_arguments(func: Optional['AnyCallableT'] = None, *, config: 'Config
     """
     Decorator to validate the arguments passed to a function.
     """
-    warnings.warn('The `validate_arguments` method is deprecated; use `validate_call` instead.', DeprecationWarning)
+    warnings.warn(
+        'The `validate_arguments` method is deprecated; use `validate_call` instead.', DeprecationWarning, stacklevel=2
+    )
 
     def validate(_func: 'AnyCallable') -> 'AnyCallable':
         vd = ValidatedFunction(_func, config)
diff --git a/pydantic/deprecated/json.py b/pydantic/deprecated/json.py
index 839fed42c2d..f8e01a233a9 100644
--- a/pydantic/deprecated/json.py
+++ b/pydantic/deprecated/json.py
@@ -1,4 +1,5 @@
 import datetime
+import warnings
 from collections import deque
 from decimal import Decimal
 from enum import Enum
@@ -9,6 +10,8 @@
 from typing import Any, Callable, Dict, Type, Union
 from uuid import UUID
 
+from typing_extensions import deprecated
+
 from ..color import Color
 from ..networks import NameEmail
 from ..types import SecretBytes, SecretStr
@@ -70,11 +73,13 @@ def decimal_encoder(dec_value: Decimal) -> Union[int, float]:
 }
 
 
+@deprecated('pydantic_encoder is deprecated, use pydantic_core.to_jsonable_python instead.')
 def pydantic_encoder(obj: Any) -> Any:
     from dataclasses import asdict, is_dataclass
 
     from ..main import BaseModel
 
+    warnings.warn('pydantic_encoder is deprecated, use BaseModel.model_dump instead.', DeprecationWarning, stacklevel=2)
     if isinstance(obj, BaseModel):
         return obj.model_dump()
     elif is_dataclass(obj):
@@ -91,8 +96,12 @@ def pydantic_encoder(obj: Any) -> Any:
         raise TypeError(f"Object of type '{obj.__class__.__name__}' is not JSON serializable")
 
 
+@deprecated('custom_pydantic_encoder is deprecated, use BaseModel.model_dump instead.')
 def custom_pydantic_encoder(type_encoders: Dict[Any, Callable[[Type[Any]], Any]], obj: Any) -> Any:
     # Check the class type and its superclasses for a matching encoder
+    warnings.warn(
+        'custom_pydantic_encoder is deprecated, use BaseModel.model_dump instead.', DeprecationWarning, stacklevel=2
+    )
     for base in obj.__class__.__mro__[:-1]:
         try:
             encoder = type_encoders[base]
@@ -104,10 +113,12 @@ def custom_pydantic_encoder(type_encoders: Dict[Any, Callable[[Type[Any]], Any]]
         return pydantic_encoder(obj)
 
 
+@deprecated('timedelta_isoformat is deprecated.')
 def timedelta_isoformat(td: datetime.timedelta) -> str:
     """
     ISO 8601 encoding for Python timedelta object.
     """
+    warnings.warn('timedelta_isoformat is deprecated.', DeprecationWarning, stacklevel=2)
     minutes, seconds = divmod(td.seconds, 60)
     hours, minutes = divmod(minutes, 60)
     return f'{"-" if td.days < 0 else ""}P{abs(td.days)}DT{hours:d}H{minutes:d}M{seconds:d}.{td.microseconds:06d}S'
diff --git a/pydantic/deprecated/parse.py b/pydantic/deprecated/parse.py
index a2576658d8f..b5d3c1ed589 100644
--- a/pydantic/deprecated/parse.py
+++ b/pydantic/deprecated/parse.py
@@ -2,16 +2,20 @@
 
 import json
 import pickle
+import warnings
 from enum import Enum
 from pathlib import Path
 from typing import Any, Callable
 
+from typing_extensions import deprecated
+
 
 class Protocol(str, Enum):
     json = 'json'
     pickle = 'pickle'
 
 
+@deprecated('load_str_bytes is deprecated.')
 def load_str_bytes(
     b: str | bytes,
     *,
@@ -21,6 +25,7 @@ def load_str_bytes(
     allow_pickle: bool = False,
     json_loads: Callable[[str], Any] = json.loads,
 ) -> Any:
+    warnings.warn('load_str_bytes is deprecated.', DeprecationWarning, stacklevel=2)
     if proto is None and content_type:
         if content_type.endswith(('json', 'javascript')):
             pass
@@ -44,6 +49,7 @@ def load_str_bytes(
         raise TypeError(f'Unknown protocol: {proto}')
 
 
+@deprecated('load_file is deprecated.')
 def load_file(
     path: str | Path,
     *,
@@ -53,6 +59,7 @@ def load_file(
     allow_pickle: bool = False,
     json_loads: Callable[[str], Any] = json.loads,
 ) -> Any:
+    warnings.warn('load_file is deprecated.', DeprecationWarning, stacklevel=2)
     path = Path(path)
     b = path.read_bytes()
     if content_type is None:
diff --git a/pydantic/deprecated/tools.py b/pydantic/deprecated/tools.py
index ddcbe8f44b9..c6aa95f5109 100644
--- a/pydantic/deprecated/tools.py
+++ b/pydantic/deprecated/tools.py
@@ -4,20 +4,26 @@
 import warnings
 from typing import Any, Callable, Type, TypeVar, Union
 
-from .. import AnalyzedType
-
-__all__ = 'parse_obj_as', 'schema_of', 'schema_json_of'
+from typing_extensions import deprecated
 
+from ..analyzed_type import AnalyzedType
 from ..json_schema import DEFAULT_REF_TEMPLATE, GenerateJsonSchema
 
+__all__ = 'parse_obj_as', 'schema_of', 'schema_json_of'
+
 NameFactory = Union[str, Callable[[Type[Any]], str]]
 
 
 T = TypeVar('T')
 
 
+@deprecated('parse_obj_as is deprecated. Use pydantic.AnalyzedType.validate_python instead.')
 def parse_obj_as(type_: type[T], obj: Any, type_name: NameFactory | None = None) -> T:
-    # TODO: add deprecation warning of some sort
+    warnings.warn(
+        'parse_obj_as is deprecated. Use pydantic.AnalyzedType.validate_python instead.',
+        DeprecationWarning,
+        stacklevel=2,
+    )
     if type_name is not None:  # pragma: no cover
         warnings.warn(
             'The type_name parameter is deprecated. parse_obj_as no longer creates temporary models',
@@ -27,6 +33,7 @@ def parse_obj_as(type_: type[T], obj: Any, type_name: NameFactory | None = None)
     return AnalyzedType(type_).validate_python(obj)
 
 
+@deprecated('schema_of is deprecated. Use pydantic.AnalyzedType.json_schema instead.')
 def schema_of(
     type_: Any,
     *,
@@ -36,6 +43,9 @@ def schema_of(
     schema_generator: type[GenerateJsonSchema] = GenerateJsonSchema,
 ) -> dict[str, Any]:
     """Generate a JSON schema (as dict) for the passed model or dynamically generated one"""
+    warnings.warn(
+        'schema_of is deprecated. Use pydantic.AnalyzedType.json_schema instead.', DeprecationWarning, stacklevel=2
+    )
     res = AnalyzedType(type_).json_schema(
         by_alias=by_alias,
         schema_generator=schema_generator,
@@ -54,6 +64,7 @@ def schema_of(
     return res
 
 
+@deprecated('schema_json_of is deprecated. Use pydantic.AnalyzedType.json_schema instead.')
 def schema_json_of(
     type_: Any,
     *,
@@ -64,6 +75,9 @@ def schema_json_of(
     **dumps_kwargs: Any,
 ) -> str:
     """Generate a JSON schema (as JSON) for the passed model or dynamically generated one"""
+    warnings.warn(
+        'schema_json_of is deprecated. Use pydantic.AnalyzedType.json_schema instead.', DeprecationWarning, stacklevel=2
+    )
     return json.dumps(
         schema_of(type_, title=title, by_alias=by_alias, ref_template=ref_template, schema_generator=schema_generator),
         **dumps_kwargs,
diff --git a/pydantic/env_settings.py b/pydantic/env_settings.py
new file mode 100644
index 00000000000..d2239407ccc
--- /dev/null
+++ b/pydantic/env_settings.py
@@ -0,0 +1,3 @@
+from ._migration import getattr_migration
+
+__getattr__ = getattr_migration(__name__)
diff --git a/pydantic/error_wrappers.py b/pydantic/error_wrappers.py
new file mode 100644
index 00000000000..d2239407ccc
--- /dev/null
+++ b/pydantic/error_wrappers.py
@@ -0,0 +1,3 @@
+from ._migration import getattr_migration
+
+__getattr__ = getattr_migration(__name__)
diff --git a/pydantic/errors.py b/pydantic/errors.py
index 3b1a151a1f4..83e56b3b4d5 100644
--- a/pydantic/errors.py
+++ b/pydantic/errors.py
@@ -7,11 +7,13 @@
 
 from typing_extensions import Literal
 
+from ._migration import getattr_migration
 from .version import VERSION
 
 __all__ = (
     'PydanticUserError',
     'PydanticUndefinedAnnotation',
+    'PydanticImportError',
     'PydanticSchemaGenerationError',
     'PydanticInvalidForJsonSchema',
 )
@@ -39,6 +41,7 @@
     'base-model-instantiated',
     'undefined-annotation',
     'schema-for-unknown-type',
+    'import-error',
     'create-model-field-definitions',
     'create-model-config-base',
     'validator-no-fields',
@@ -80,8 +83,6 @@ class PydanticUserError(PydanticErrorMixin, TypeError):
     Error raised due to incorrect use of Pydantic.
     """
 
-    pass
-
 
 class PydanticUndefinedAnnotation(PydanticErrorMixin, NameError):
     """A subclass of `NameError` raised when handling undefined annotations during `CoreSchema` generation.
@@ -113,6 +114,13 @@ def from_name_error(cls, name_error: NameError) -> PydanticUndefinedAnnotation:
         return cls(name=name, message=str(name_error))
 
 
+class PydanticImportError(PydanticErrorMixin, ImportError):
+    """Error occurs when an import fails due to module changes between V1 and V2."""
+
+    def __init__(self, message: str) -> None:
+        super().__init__(message, code='import-error')
+
+
 class PydanticSchemaGenerationError(PydanticUserError):
     """
     Error raised during failures to generate a `CoreSchema` for some type.
@@ -135,3 +143,6 @@ class PydanticInvalidForJsonSchema(PydanticUserError):
 
     def __init__(self, message: str) -> None:
         super().__init__(message, code='invalid-for-json-schema')
+
+
+__getattr__ = getattr_migration(__name__)
diff --git a/pydantic/generics.py b/pydantic/generics.py
new file mode 100644
index 00000000000..d2239407ccc
--- /dev/null
+++ b/pydantic/generics.py
@@ -0,0 +1,3 @@
+from ._migration import getattr_migration
+
+__getattr__ = getattr_migration(__name__)
diff --git a/pydantic/json.py b/pydantic/json.py
new file mode 100644
index 00000000000..d2239407ccc
--- /dev/null
+++ b/pydantic/json.py
@@ -0,0 +1,3 @@
+from ._migration import getattr_migration
+
+__getattr__ = getattr_migration(__name__)
diff --git a/pydantic/main.py b/pydantic/main.py
index 5b416e28439..6a68603ec8d 100644
--- a/pydantic/main.py
+++ b/pydantic/main.py
@@ -26,6 +26,7 @@
     _utils,
 )
 from ._internal._fields import Undefined
+from ._migration import getattr_migration
 from .config import ConfigDict
 from .deprecated import copy_internals as _deprecated_copy_internals
 from .deprecated import parse as _deprecated_parse
@@ -1020,3 +1021,6 @@ def _collect_bases_data(bases: tuple[type[Any], ...]) -> tuple[set[str], set[str
             class_vars.update(base.__class_vars__)
             private_attributes.update(base.__private_attributes__)
     return field_names, class_vars, private_attributes
+
+
+__getattr__ = getattr_migration(__name__)
diff --git a/pydantic/networks.py b/pydantic/networks.py
index 16938467a2b..c1dc4de5600 100644
--- a/pydantic/networks.py
+++ b/pydantic/networks.py
@@ -9,6 +9,7 @@
 from typing_extensions import Annotated, TypeAlias
 
 from ._internal import _fields, _repr
+from ._migration import getattr_migration
 
 if TYPE_CHECKING:
     import email_validator
@@ -308,3 +309,6 @@ def validate_email(value: str) -> tuple[str, str]:
         ) from e
 
     return name or parts['local'], parts['email']
+
+
+__getattr__ = getattr_migration(__name__)
diff --git a/pydantic/parse.py b/pydantic/parse.py
new file mode 100644
index 00000000000..d2239407ccc
--- /dev/null
+++ b/pydantic/parse.py
@@ -0,0 +1,3 @@
+from ._migration import getattr_migration
+
+__getattr__ = getattr_migration(__name__)
diff --git a/pydantic/schema.py b/pydantic/schema.py
new file mode 100644
index 00000000000..d2239407ccc
--- /dev/null
+++ b/pydantic/schema.py
@@ -0,0 +1,3 @@
+from ._migration import getattr_migration
+
+__getattr__ = getattr_migration(__name__)
diff --git a/pydantic/tools.py b/pydantic/tools.py
new file mode 100644
index 00000000000..d2239407ccc
--- /dev/null
+++ b/pydantic/tools.py
@@ -0,0 +1,3 @@
+from ._migration import getattr_migration
+
+__getattr__ = getattr_migration(__name__)
diff --git a/pydantic/types.py b/pydantic/types.py
index fdc4b80d6ca..8f44de06ed4 100644
--- a/pydantic/types.py
+++ b/pydantic/types.py
@@ -27,6 +27,7 @@
 from typing_extensions import Annotated, Literal
 
 from ._internal import _fields, _validators
+from ._migration import getattr_migration
 
 __all__ = [
     'Strict',
@@ -853,3 +854,6 @@ def __get_pydantic_core_schema__(
 
         def __repr__(self) -> str:
             return 'NaiveDatetime'
+
+
+__getattr__ = getattr_migration(__name__)
diff --git a/pydantic/typing.py b/pydantic/typing.py
new file mode 100644
index 00000000000..d2239407ccc
--- /dev/null
+++ b/pydantic/typing.py
@@ -0,0 +1,3 @@
+from ._migration import getattr_migration
+
+__getattr__ = getattr_migration(__name__)
diff --git a/pydantic/utils.py b/pydantic/utils.py
new file mode 100644
index 00000000000..d2239407ccc
--- /dev/null
+++ b/pydantic/utils.py
@@ -0,0 +1,3 @@
+from ._migration import getattr_migration
+
+__getattr__ = getattr_migration(__name__)
diff --git a/pydantic/validators.py b/pydantic/validators.py
new file mode 100644
index 00000000000..d2239407ccc
--- /dev/null
+++ b/pydantic/validators.py
@@ -0,0 +1,3 @@
+from ._migration import getattr_migration
+
+__getattr__ = getattr_migration(__name__)
diff --git a/tests/mypy/test_mypy.py b/tests/mypy/test_mypy.py
index 7f26e5b9086..aaf7b4038f6 100644
--- a/tests/mypy/test_mypy.py
+++ b/tests/mypy/test_mypy.py
@@ -201,6 +201,7 @@ def test_bad_toml_config() -> None:
 
 
 @pytest.mark.parametrize('module', sorted(executable_modules))
+@pytest.mark.filterwarnings('ignore:.*is deprecated.*:DeprecationWarning')
 def test_success_cases_run(module: str) -> None:
     """
     Ensure the "success" files can actually be executed
diff --git a/tests/test_deprecated.py b/tests/test_deprecated.py
index 985beaf6672..38197ffa8e4 100644
--- a/tests/test_deprecated.py
+++ b/tests/test_deprecated.py
@@ -1,5 +1,8 @@
 import platform
 import re
+import sys
+from datetime import timedelta
+from pathlib import Path
 from types import SimpleNamespace
 from typing import Any, Dict, List, Type
 
@@ -8,6 +11,15 @@
 
 from pydantic import BaseModel, ConfigDict, Field, PydanticUserError, ValidationError, model_serializer, root_validator
 from pydantic.config import Extra
+from pydantic.deprecated.decorator import validate_arguments
+from pydantic.deprecated.json import custom_pydantic_encoder, pydantic_encoder, timedelta_isoformat
+from pydantic.deprecated.parse import load_file, load_str_bytes
+from pydantic.deprecated.tools import parse_obj_as, schema_json_of, schema_of
+
+if sys.version_info < (3, 11):
+    from typing_extensions import get_overloads
+else:
+    from typing import get_overloads
 
 
 def deprecated_from_orm(model_type: Type[BaseModel], obj: Any) -> Any:
@@ -612,3 +624,60 @@ def test_get_value():
             exclude_none=False,
         )
     assert v == [1, 2, 3]
+
+
+def test_deprecated_module(tmp_path: Path) -> None:
+    class Model(BaseModel):
+        x: int
+
+    assert hasattr(parse_obj_as, '__deprecated__')
+    with pytest.warns(
+        DeprecationWarning, match='parse_obj_as is deprecated. Use pydantic.AnalyzedType.validate_python instead.'
+    ):
+        parse_obj_as(Model, {'x': 1})
+
+    assert hasattr(schema_json_of, '__deprecated__')
+    with pytest.warns(
+        DeprecationWarning, match='schema_json_of is deprecated. Use pydantic.AnalyzedType.json_schema instead.'
+    ):
+        schema_json_of(Model)
+
+    assert hasattr(schema_of, '__deprecated__')
+    with pytest.warns(
+        DeprecationWarning, match='schema_of is deprecated. Use pydantic.AnalyzedType.json_schema instead.'
+    ):
+        schema_of(Model)
+
+    assert hasattr(load_str_bytes, '__deprecated__')
+    with pytest.warns(DeprecationWarning, match='load_str_bytes is deprecated.'):
+        load_str_bytes('{"x": 1}')
+
+    assert hasattr(load_file, '__deprecated__')
+    file = tmp_path / 'main.py'
+    file.write_text('{"x": 1}')
+    with pytest.warns(DeprecationWarning, match='load_file is deprecated.'):
+        load_file(file)
+
+    assert hasattr(pydantic_encoder, '__deprecated__')
+    with pytest.warns(DeprecationWarning, match='pydantic_encoder is deprecated, use BaseModel.model_dump instead.'):
+        pydantic_encoder(Model(x=1))
+
+    assert hasattr(custom_pydantic_encoder, '__deprecated__')
+    with pytest.warns(
+        DeprecationWarning, match='custom_pydantic_encoder is deprecated, use BaseModel.model_dump instead.'
+    ):
+        custom_pydantic_encoder({int: lambda x: str(x)}, Model(x=1))
+
+    assert hasattr(timedelta_isoformat, '__deprecated__')
+    with pytest.warns(DeprecationWarning, match='timedelta_isoformat is deprecated.'):
+        timedelta_isoformat(timedelta(seconds=1))
+
+    assert all(hasattr(func, '__deprecated__') for func in get_overloads(validate_arguments))
+    with pytest.warns(
+        DeprecationWarning, match='The `validate_arguments` method is deprecated; use `validate_call` instead.'
+    ):
+
+        def test(a: int, b: int):
+            pass
+
+        validate_arguments()(test)
diff --git a/tests/test_docs.py b/tests/test_docs.py
index c805ed051b2..96d857ce40a 100644
--- a/tests/test_docs.py
+++ b/tests/test_docs.py
@@ -75,6 +75,7 @@ def now(cls, *args, **kwargs):
 skip_reason = skip_docs_tests()
 
 
+@pytest.mark.filterwarnings('ignore:(parse_obj_as|schema_json_of|schema_of) is deprecated.*:DeprecationWarning')
 @pytest.mark.skipif(bool(skip_reason), reason=skip_reason or 'not skipping')
 @pytest.mark.parametrize('example', find_examples(str(DOCS_ROOT), skip=sys.platform == 'win32'), ids=str)
 def test_docs_examples(example: CodeExample, eval_example: EvalExample, tmp_path: Path, mocker):  # noqa: C901
diff --git a/tests/test_json.py b/tests/test_json.py
index ec64e9c49c3..1fec217c91a 100644
--- a/tests/test_json.py
+++ b/tests/test_json.py
@@ -26,6 +26,9 @@
     email_validator = None
 
 
+pytestmark = pytest.mark.filterwarnings('ignore::DeprecationWarning')
+
+
 class MyEnum(Enum):
     foo = 'bar'
     snap = 'crackle'
diff --git a/tests/test_migration.py b/tests/test_migration.py
new file mode 100644
index 00000000000..b35d14960d7
--- /dev/null
+++ b/tests/test_migration.py
@@ -0,0 +1,30 @@
+import importlib
+
+import pytest
+
+from pydantic._migration import DEPRECATED_MOVED_IN_V2, MOVED_IN_V2, REMOVED_IN_V2
+from pydantic.errors import PydanticImportError
+
+
+def import_from(dotted_path: str):
+    module, obj_name = dotted_path.rsplit('.', 1)
+    module = importlib.import_module(module)
+    return getattr(module, obj_name)
+
+
+@pytest.mark.filterwarnings('ignore::UserWarning')
+@pytest.mark.parametrize('module', MOVED_IN_V2.keys())
+def test_moved_on_v2(module: str):
+    import_from(module)
+
+
+@pytest.mark.parametrize('module', DEPRECATED_MOVED_IN_V2.keys())
+def test_moved_but_not_warn_on_v2(module: str):
+    import_from(module)
+
+
+@pytest.mark.parametrize('module', REMOVED_IN_V2)
+def test_removed_on_v2(module: str):
+    with pytest.raises(PydanticImportError, match=f'`{module}` has been removed in V2.'):
+        import_from(module)
+        assert False, f'{module} should not be importable'
diff --git a/tests/test_parse.py b/tests/test_parse.py
index 61f014b0a8e..5df22b3c588 100644
--- a/tests/test_parse.py
+++ b/tests/test_parse.py
@@ -150,6 +150,7 @@ class MyModel(BaseModel):
     assert isinstance(m.nested.root[0], NestedData)
 
 
+@pytest.mark.filterwarnings('ignore:parse_obj_as is deprecated.*:DeprecationWarning')
 def test_parse_nested_root_tuple():
     class NestedData(BaseModel):
         id: str
diff --git a/tests/test_tools.py b/tests/test_tools.py
index 91b22bcfa6f..4ff3e331567 100644
--- a/tests/test_tools.py
+++ b/tests/test_tools.py
@@ -6,6 +6,8 @@
 from pydantic.dataclasses import dataclass
 from pydantic.deprecated.tools import parse_obj_as, schema_json_of, schema_of
 
+pytestmark = pytest.mark.filterwarnings('ignore::DeprecationWarning')
+
 
 @pytest.mark.parametrize('obj,type_,parsed', [('1', int, 1), (['1'], List[int], [1])])
 def test_parse_obj(obj, type_, parsed):


Code review comments:
diff_hunk: @@ -91,8 +96,12 @@ def pydantic_encoder(obj: Any) -> Any:
         raise TypeError(f"Object of type '{obj.__class__.__name__}' is not JSON serializable")
 
 
+@deprecated('custom_pydantic_encoder is deprecated, use BaseModel.model_dump instead.')
path: pydantic/deprecated/json.py
text: @author:
I'll wait for @user1 's input here. :pray: 

@user3:
I see the `fallback` kwarg lets you handle the encoding of objects that would otherwise be non-serializable, but I don't see a way to override the encoding for specific types. @user1 it seems like it may be worth adding support for this in pydantic-core, which might also help resolve the xfailing `tests.test_forward_ref.test_json_encoder_str` (though would require support for something similar in `SchemaSerializer`).

In the meantime, my suggestion would be to, in addition to the suggestion above, add a TODO note like this:
```suggestion
# TODO: Add a suggested migration path once there is a way to use custom encoders
@user2('custom_pydantic_encoder is deprecated.')
```
(You can ignore the suggestion in the earlier comment, this one would replace it.)

Happy to get Samuel's thoughts though.