Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for deprecated fields #8237

Merged
merged 44 commits into from Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
5a8944e
Add support for deprecated fields
Viicos Nov 27, 2023
4c9e717
Add `Deprecated`
Viicos Nov 28, 2023
772b244
Update to latest typing-extensions, add test
Viicos Dec 2, 2023
8f818aa
Add support for computed fields
Viicos Dec 2, 2023
16bda91
Add description to deprecated argument
Viicos Dec 2, 2023
8af3c99
Update to latest typing-extensions
Viicos Dec 10, 2023
e50fa19
Some initial PR feedback
Viicos Dec 12, 2023
2c4cbea
deprecated is now str
Viicos Dec 28, 2023
6d58b09
lock
Viicos Dec 28, 2023
bbf3aa0
relock
Viicos Dec 28, 2023
28aafe9
lint
Viicos Dec 28, 2023
0506476
Emit runtime warning
Viicos Dec 29, 2023
5214ffc
Fix computed_fields handling
Viicos Dec 29, 2023
c894937
Handle `TypeError` as well
Viicos Dec 29, 2023
1084b3b
Last fix
Viicos Dec 29, 2023
3292732
fix import for 3.8
Viicos Dec 29, 2023
768605d
Merge branch 'main' into deprecated
Viicos Jan 12, 2024
537ba83
Use `cls.model_computed_fields`
Viicos Jan 12, 2024
61802e3
Merge branch 'main' into deprecated
Viicos Jan 29, 2024
bc28d13
Allow instances of `deprecated`
Viicos Jan 29, 2024
089bb5e
fixes
Viicos Jan 29, 2024
efb9b4b
add docs
Viicos Jan 29, 2024
06817c0
Merge branch 'main' into deprecated
Viicos Jan 30, 2024
2986654
lint docs
Viicos Jan 30, 2024
9a63496
Rename `__pydantic_deprecated_messages__` to `__pydantic_deprecated_f…
Viicos Jan 30, 2024
92fd53f
lint
Viicos Jan 30, 2024
cf06a73
PoC with deprecated object
Viicos Feb 9, 2024
4a5ff92
Revert "PoC with deprecated object"
Viicos Feb 20, 2024
1fc7ec8
Use a descriptor for deprecated fields
Viicos Feb 20, 2024
8110b79
Merge branch 'main' into deprecated
Viicos Feb 20, 2024
82b9bfb
Use `NoReturn`
Viicos Feb 21, 2024
69b83dc
Use `packaging` as a dev dependency to parse the `typing_exensions` v…
Viicos Feb 22, 2024
4db38c2
Remove `referencing`
Viicos Feb 26, 2024
2bf597a
Merge branch 'main' into deprecated
Viicos Feb 26, 2024
501abd4
Reorganize `pyproject.toml` sections
Viicos Feb 26, 2024
c1f92e1
Add `packaging` as a dev dep.
Viicos Feb 26, 2024
9579d4e
Revert unrelated changes
Viicos Feb 27, 2024
90fb70b
Work around `from_deprecated_decorator`, extra tests
Viicos Feb 27, 2024
6cf695c
Catch warning in test
Viicos Feb 29, 2024
b67698c
Update docs
Viicos Feb 29, 2024
3c8da2c
Temp. disable FastAPI tests
Viicos Feb 29, 2024
fc73851
adding bool support
sydney-runkle Feb 29, 2024
fb2a119
remove comment
sydney-runkle Feb 29, 2024
4acb83a
docs updates
sydney-runkle Feb 29, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
59 changes: 59 additions & 0 deletions docs/concepts/fields.md
Expand Up @@ -692,6 +692,46 @@ print(user.model_dump()) # (1)!

See the [Serialization] section for more details.

## Deprecated fields

The `deprecated` parameter can be used to mark a field as being deprecated. Doing so will result in:

* a runtime deprecation warning emitted when accessing the field.
* `"deprecated": true` being set in the generated JSON schema.

```py
from typing_extensions import Annotated

from pydantic import BaseModel, Field


class Model(BaseModel):
deprecated_field: Annotated[int, Field(deprecated='This is deprecated')]


print(Model.model_json_schema()['properties']['deprecated_field'])
#> {'deprecated': True, 'title': 'Deprecated Field', 'type': 'integer'}
```

Alternatively, the `warnings.deprecated` decorator (and the `typing_extensions` backport) can be used:

```py test="skip"
from typing_extensions import Annotated, deprecated

from pydantic import BaseModel, Field


class Model(BaseModel):
deprecated_field: Annotated[int, deprecated('This is deprecated')]

# Or explicitly using `Field`:
alt_form: Annotated[int, Field(deprecated=deprecated('This is deprecated'))]
```

!!! note "Support for `category` and `stacklevel`"
The current implementation of this feature does not take into account the `category` and `stacklevel`
arguments to the `deprecated` decorator. This might land in a future version of Pydantic.

## Customizing JSON Schema

Some field parameters are used exclusively to customize the generated JSON schema. The parameters in question are:
Expand Down Expand Up @@ -733,6 +773,25 @@ print(b.model_dump())
#> {'width': 1.0, 'height': 2.0, 'depth': 3.0, 'volume': 6.0}
```

As with regular fields, computed fields can be marked as being deprecated:

```py
from typing_extensions import deprecated

from pydantic import BaseModel, computed_field


class Box(BaseModel):
width: float
height: float
depth: float

@computed_field
@deprecated("'volume' is deprecated")
def volume(self) -> float:
return self.width * self.height * self.depth
Viicos marked this conversation as resolved.
Show resolved Hide resolved
```


[JSON Schema Draft 2020-12]: https://json-schema.org/understanding-json-schema/reference/numeric.html#numeric-types
[Discriminated Unions]: ../concepts/unions.md#discriminated-unions
Expand Down
7 changes: 3 additions & 4 deletions pdm.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion pydantic/_internal/_generate_schema.py
Expand Up @@ -80,7 +80,7 @@
from ._schema_generation_shared import (
CallbackGetCoreSchemaHandler,
)
from ._typing_extra import is_finalvar
from ._typing_extra import DEPRECATED_TYPES, is_finalvar
from ._utils import lenient_issubclass

if TYPE_CHECKING:
Expand Down Expand Up @@ -1135,6 +1135,7 @@ def set_discriminator(schema: CoreSchema) -> CoreSchema:
json_schema_updates = {
'title': field_info.title,
'description': field_info.description,
'deprecated': isinstance(field_info.deprecated, (DEPRECATED_TYPES, str)) or None,
'examples': to_jsonable_python(field_info.examples),
}
json_schema_updates = {k: v for k, v in json_schema_updates.items() if v is not None}
Expand Down Expand Up @@ -1731,6 +1732,9 @@ def set_computed_field_metadata(schema: CoreSchemaOrField, handler: GetJsonSchem
if description is not None:
json_schema['description'] = description

if isinstance(d.info.deprecated, (DEPRECATED_TYPES, str)):
json_schema['deprecated'] = True

examples = d.info.examples
if examples is not None:
json_schema['examples'] = to_jsonable_python(examples)
Expand Down
58 changes: 57 additions & 1 deletion pydantic/_internal/_model_construction.py
@@ -1,14 +1,15 @@
"""Private logic for creating models."""
from __future__ import annotations as _annotations

import builtins
import operator
import typing
import warnings
import weakref
from abc import ABCMeta
from functools import partial
from types import FunctionType
from typing import Any, Callable, Generic
from typing import Any, Callable, Generic, NoReturn

import typing_extensions
from pydantic_core import PydanticUndefined, SchemaSerializer
Expand Down Expand Up @@ -211,6 +212,8 @@ def wrapped_model_post_init(self: BaseModel, __context: Any) -> None:
# the generic computed fields return type is set to PydanticUndefined
cls.model_computed_fields = {k: v.info for k, v in cls.__pydantic_decorators__.computed_fields.items()}

set_deprecated_descriptors(cls)

# using super(cls, cls) on the next line ensures we only call the parent class's __pydantic_init_subclass__
# I believe the `type: ignore` is only necessary because mypy doesn't realize that this code branch is
# only hit for _proper_ subclasses of BaseModel
Expand Down Expand Up @@ -571,6 +574,59 @@ def complete_model_class(
return True


def set_deprecated_descriptors(cls: type[BaseModel]) -> None:
"""Set data descriptors on the class for deprecated fields."""
for field, field_info in cls.model_fields.items():
if (msg := field_info.deprecation_message) is not None:
sydney-runkle marked this conversation as resolved.
Show resolved Hide resolved
desc = _DeprecatedFieldDescriptor(msg)
desc.__set_name__(cls, field)
setattr(cls, field, desc)

for field, computed_field_info in cls.model_computed_fields.items():
if (
not computed_field_info.from_deprecated_decorator # Avoid having two warnings emitted
sydney-runkle marked this conversation as resolved.
Show resolved Hide resolved
and (msg := computed_field_info.deprecation_message) is not None
):
desc = _DeprecatedFieldDescriptor(msg, computed_field_info.wrapped_property)
desc.__set_name__(cls, field)
setattr(cls, field, desc)


class _DeprecatedFieldDescriptor:
sydney-runkle marked this conversation as resolved.
Show resolved Hide resolved
"""Data descriptor used to emit a runtime deprecation warning before accessing a deprecated field.

Attributes:
msg: The deprecation message to be emitted.
wrapped_property: The property instance if the deprecated field is a computed field, or `None`.
field_name: The name of the field being deprecated.
"""

field_name: str

def __init__(self, msg: str, wrapped_property: property | None = None) -> None:
self.msg = msg
self.wrapped_property = wrapped_property

def __set_name__(self, cls: type[BaseModel], name: str) -> None:
self.field_name = name

def __get__(self, obj: BaseModel | None, obj_type: type[BaseModel] | None = None) -> Any:
if obj is None:
raise AttributeError(self.field_name)

warnings.warn(self.msg, builtins.DeprecationWarning, stacklevel=2)

if self.wrapped_property is not None:
return self.wrapped_property.__get__(obj, obj_type)
return obj.__dict__[self.field_name]

# Defined to take precedence over the instance's dictionary
# Note that it will not be called when setting a value on a model instance
# as `BaseModel.__setattr__` is defined and takes priority.
def __set__(self, obj: Any, value: Any) -> NoReturn:
raise AttributeError(self.field_name)


class _PydanticWeakRef:
"""Wrapper for `weakref.ref` that enables `pickle` serialization.

Expand Down
12 changes: 11 additions & 1 deletion pydantic/_internal/_typing_extra.py
Expand Up @@ -5,12 +5,13 @@
import sys
import types
import typing
import warnings
from collections.abc import Callable
from functools import partial
from types import GetSetDescriptorType
from typing import TYPE_CHECKING, Any, Final

from typing_extensions import Annotated, Literal, TypeAliasType, TypeGuard, get_args, get_origin
from typing_extensions import Annotated, Literal, TypeAliasType, TypeGuard, deprecated, get_args, get_origin

if TYPE_CHECKING:
from ._dataclasses import StandardDataclass
Expand Down Expand Up @@ -62,6 +63,11 @@ def origin_is_union(tp: type[Any] | None) -> bool:
if hasattr(typing, 'Literal'):
LITERAL_TYPES.add(typing.Literal) # type: ignore

# Check if `deprecated` is a type to prevent errors when using typing_extensions < 4.9.0
DEPRECATED_TYPES: tuple[Any, ...] = (deprecated,) if isinstance(deprecated, type) else ()
if hasattr(warnings, 'deprecated'):
DEPRECATED_TYPES = (*DEPRECATED_TYPES, warnings.deprecated) # type: ignore

NONE_TYPES: tuple[Any, ...] = (None, NoneType, *(tp[None] for tp in LITERAL_TYPES))


Expand All @@ -80,6 +86,10 @@ def is_literal_type(type_: type[Any]) -> bool:
return Literal is not None and get_origin(type_) in LITERAL_TYPES


def is_deprecated_instance(instance: Any) -> TypeGuard[deprecated]:
return isinstance(instance, DEPRECATED_TYPES)


def literal_values(type_: type[Any]) -> tuple[Any, ...]:
return get_args(type_)

Expand Down