Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit9452e13

Browse files
authored
Add initial support for Python 3.14 (#11991)
Adds basic support for Python 3.14. Deferred annotations work for simple cases, but will need to be improved in the future.
1 parentdac3c43 commit9452e13

17 files changed

+469
-285
lines changed

‎.github/workflows/ci.yml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
strategy:
2121
fail-fast:false
2222
matrix:
23-
python-version:['3.9', '3.10', '3.11', '3.12', '3.13']
23+
python-version:['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
2424
steps:
2525
-uses:actions/checkout@v4
2626

@@ -87,7 +87,7 @@ jobs:
8787
fail-fast:false
8888
matrix:
8989
os:[ubuntu-latest, macos-13, macos-latest, windows-latest]
90-
python-version:['3.9', '3.10', '3.11', '3.12', '3.13', '3.13t']
90+
python-version:['3.9', '3.10', '3.11', '3.12', '3.13', '3.14', '3.14t']
9191
include:
9292
# no pydantic-core binaries for pypy on windows, so tests take absolute ages
9393
# macos tests with pypy take ages (>10mins) since pypy is very slow
@@ -140,6 +140,11 @@ jobs:
140140
COVERAGE_FILE:coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}-without-deps
141141
CONTEXT:${{ runner.os }}-py${{ matrix.python-version }}-without-deps
142142

143+
# TODO remove when memray supports 3.14:
144+
-name:Install memray system dependencies
145+
if:${{ matrix.python-version == '3.14' && matrix.os == 'ubuntu-latest' }}
146+
run:sudo apt-get install libunwind-dev libdebuginfod-dev
147+
143148
-name:Install extra dependencies
144149
# Skip free threaded, we can't install memray
145150
if:${{ !endsWith(matrix.python-version, 't') }}
@@ -299,7 +304,7 @@ jobs:
299304
strategy:
300305
fail-fast:false
301306
matrix:
302-
python-version:['3.9', '3.10', '3.11', '3.12', '3.13']
307+
python-version:['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
303308
steps:
304309
-uses:actions/checkout@v4
305310

‎.github/workflows/integration.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
runs-on:ubuntu-latest
1212
strategy:
1313
matrix:
14-
python-version:['3.9', '3.10', '3.11', '3.12', '3.13']
14+
python-version:['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
1515
steps:
1616
-uses:actions/checkout@v4
1717

@@ -27,7 +27,7 @@ jobs:
2727
runs-on:ubuntu-latest
2828
strategy:
2929
matrix:
30-
python-version:['3.9', '3.10', '3.11', '3.12', '3.13']
30+
python-version:['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
3131
steps:
3232
-uses:actions/checkout@v4
3333

‎docs/migration.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ to help ease migration, but calling them will emit `DeprecationWarning`s.
188188
If you'd still like to use said arguments, you can use[this workaround](https://github.com/pydantic/pydantic/issues/8825#issuecomment-1946206415).
189189
* JSON serialization of non-string key values is generally done with`str(key)`, leading to some changes in behavior such as the following:
190190

191-
```python
191+
```python {test="skip"}
192192
from typingimport Optional
193193

194194
from pydanticimport BaseModelas V2BaseModel
@@ -218,7 +218,7 @@ print(v2_model.model_dump_json())
218218
*`model_dump_json()` results are compacted in order to save space, and don't always exactly match that of`json.dumps()` output.
219219
That being said, you can easily modify the separators used in`json.dumps()` results in order to align the two outputs:
220220

221-
```python
221+
```python {test="skip"}
222222
import json
223223

224224
from pydanticimport BaseModelas V2BaseModel

‎pydantic/_internal/_config.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,13 @@ def __init__(self, config: ConfigDict | dict[str, Any] | type[Any] | None, *, ch
9898
self.config_dict=cast(ConfigDict,config)
9999

100100
@classmethod
101-
deffor_model(cls,bases:tuple[type[Any], ...],namespace:dict[str,Any],kwargs:dict[str,Any])->Self:
101+
deffor_model(
102+
cls,
103+
bases:tuple[type[Any], ...],
104+
namespace:dict[str,Any],
105+
raw_annotations:dict[str,Any],
106+
kwargs:dict[str,Any],
107+
)->Self:
102108
"""Build a new `ConfigWrapper` instance for a `BaseModel`.
103109
104110
The config wrapper built based on (in descending order of priority):
@@ -109,6 +115,7 @@ def for_model(cls, bases: tuple[type[Any], ...], namespace: dict[str, Any], kwar
109115
Args:
110116
bases: A tuple of base classes.
111117
namespace: The namespace of the class being created.
118+
raw_annotations: The (non-evaluated) annotations of the model.
112119
kwargs: The kwargs passed to the class being created.
113120
114121
Returns:
@@ -123,7 +130,6 @@ def for_model(cls, bases: tuple[type[Any], ...], namespace: dict[str, Any], kwar
123130
config_class_from_namespace=namespace.get('Config')
124131
config_dict_from_namespace=namespace.get('model_config')
125132

126-
raw_annotations=namespace.get('__annotations__', {})
127133
ifraw_annotations.get('model_config')andconfig_dict_from_namespaceisNone:
128134
raisePydanticUserError(
129135
'`model_config` cannot be used as a model field name. Use `model_config` for model configuration.',

‎pydantic/_internal/_fields.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,8 @@ def collect_model_fields( # noqa: C901
259259

260260
# https://docs.python.org/3/howto/annotations.html#accessing-the-annotations-dict-of-an-object-in-python-3-9-and-older
261261
# annotations is only used for finding fields in parent classes
262-
annotations=cls.__dict__.get('__annotations__', {})
262+
annotations=_typing_extra.safe_get_annotations(cls)
263+
263264
fields:dict[str,FieldInfo]= {}
264265

265266
class_vars:set[str]=set()
@@ -508,7 +509,9 @@ def collect_dataclass_fields(
508509

509510
withns_resolver.push(base):
510511
forann_name,dataclass_fieldindataclass_fields.items():
511-
ifann_namenotinbase.__dict__.get('__annotations__', {}):
512+
base_anns=_typing_extra.safe_get_annotations(base)
513+
514+
ifann_namenotinbase_anns:
512515
# `__dataclass_fields__`contains every field, even the ones from base classes.
513516
# Only collect the ones defined on `base`.
514517
continue

‎pydantic/_internal/_generics.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
from __future__importannotations
22

3+
importoperator
34
importsys
45
importtypes
56
importtyping
67
fromcollectionsimportChainMap
78
fromcollections.abcimportIterator,Mapping
89
fromcontextlibimportcontextmanager
910
fromcontextvarsimportContextVar
11+
fromfunctoolsimportreduce
1012
fromitertoolsimportzip_longest
1113
fromtypesimportprepare_class
1214
fromtypingimportTYPE_CHECKING,Annotated,Any,TypeVar
@@ -21,9 +23,6 @@
2123
from ._forward_refimportPydanticRecursiveRef
2224
from ._utilsimportall_identical,is_model_class
2325

24-
ifsys.version_info>= (3,10):
25-
fromtypingimport_UnionGenericAlias# type: ignore[attr-defined]
26-
2726
ifTYPE_CHECKING:
2827
from ..mainimportBaseModel
2928

@@ -311,7 +310,7 @@ def replace_types(type_: Any, type_map: Mapping[TypeVar, Any] | None) -> Any:
311310
# PEP-604 syntax (Ex.: list | str) is represented with a types.UnionType object that does not have __getitem__.
312311
# We also cannot use isinstance() since we have to compare types.
313312
ifsys.version_info>= (3,10)andorigin_typeistypes.UnionType:
314-
return_UnionGenericAlias(origin_type,resolved_type_args)
313+
returnreduce(operator.or_,resolved_type_args)
315314
# NotRequired[T] and Required[T] don't support tuple type resolved_type_args, hence the condition below
316315
returnorigin_type[resolved_type_args[0]iflen(resolved_type_args)==1elseresolved_type_args]
317316

‎pydantic/_internal/_model_construction.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,29 @@ def __new__(
105105
# that `BaseModel` itself won't have any bases, but any subclass of it will, to determine whether the `__new__`
106106
# call we're in the middle of is for the `BaseModel` class.
107107
ifbases:
108+
raw_annotations:dict[str,Any]
109+
ifsys.version_info>= (3,14):
110+
if (
111+
'__annotations__'innamespace
112+
):# `from __future__ import annotations` was used in the model's module
113+
raw_annotations=namespace['__annotations__']
114+
else:
115+
# See https://docs.python.org/3.14/library/annotationlib.html#using-annotations-in-a-metaclass:
116+
fromannotationlibimportFormat,call_annotate_function,get_annotate_from_class_namespace
117+
118+
ifannotate:=get_annotate_from_class_namespace(namespace):
119+
raw_annotations=call_annotate_function(annotate,format=Format.FORWARDREF)
120+
else:
121+
raw_annotations= {}
122+
else:
123+
raw_annotations=namespace.get('__annotations__', {})
124+
108125
base_field_names,class_vars,base_private_attributes=mcs._collect_bases_data(bases)
109126

110-
config_wrapper=ConfigWrapper.for_model(bases,namespace,kwargs)
127+
config_wrapper=ConfigWrapper.for_model(bases,namespace,raw_annotations,kwargs)
111128
namespace['model_config']=config_wrapper.config_dict
112129
private_attributes=inspect_namespace(
113-
namespace,config_wrapper.ignored_types,class_vars,base_field_names
130+
namespace,raw_annotations,config_wrapper.ignored_types,class_vars,base_field_names
114131
)
115132
ifprivate_attributesorbase_private_attributes:
116133
original_model_post_init=get_model_post_init(namespace,bases)
@@ -363,6 +380,7 @@ def get_model_post_init(namespace: dict[str, Any], bases: tuple[type[Any], ...])
363380

364381
definspect_namespace(# noqa C901
365382
namespace:dict[str,Any],
383+
raw_annotations:dict[str,Any],
366384
ignored_types:tuple[type[Any], ...],
367385
base_class_vars:set[str],
368386
base_class_fields:set[str],
@@ -373,6 +391,7 @@ def inspect_namespace( # noqa C901
373391
374392
Args:
375393
namespace: The attribute dictionary of the class to be created.
394+
raw_annotations: The (non-evaluated) annotations of the model.
376395
ignored_types: A tuple of ignore types.
377396
base_class_vars: A set of base class class variables.
378397
base_class_fields: A set of base class fields.
@@ -394,7 +413,6 @@ def inspect_namespace( # noqa C901
394413
all_ignored_types=ignored_types+default_ignored_types()
395414

396415
private_attributes:dict[str,ModelPrivateAttr]= {}
397-
raw_annotations=namespace.get('__annotations__', {})
398416

399417
if'__root__'inraw_annotationsor'__root__'innamespace:
400418
raiseTypeError("To define root models, use `pydantic.RootModel` rather than a field called '__root__'")

‎pydantic/_internal/_typing_extra.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
fromtypesimportEllipsisTypeasEllipsisType
2727
fromtypesimportNoneTypeasNoneType
2828

29+
ifsys.version_info>= (3,14):
30+
importannotationlib
31+
2932
ifTYPE_CHECKING:
3033
frompydanticimportBaseModel
3134

@@ -289,6 +292,19 @@ def _type_convert(arg: Any) -> Any:
289292
returnarg
290293

291294

295+
defsafe_get_annotations(cls:type[Any])->dict[str,Any]:
296+
"""Get the annotations for the provided class, accounting for potential deferred forward references.
297+
298+
Starting with Python 3.14, accessing the `__annotations__` attribute might raise a `NameError` if
299+
a referenced symbol isn't defined yet. In this case, we return the annotation in the *forward ref*
300+
format.
301+
"""
302+
ifsys.version_info>= (3,14):
303+
returnannotationlib.get_annotations(cls,format=annotationlib.Format.FORWARDREF)
304+
else:
305+
returncls.__dict__.get('__annotations__', {})
306+
307+
292308
defget_model_type_hints(
293309
obj:type[BaseModel],
294310
*,
@@ -309,9 +325,14 @@ def get_model_type_hints(
309325
ns_resolver=ns_resolverorNsResolver()
310326

311327
forbaseinreversed(obj.__mro__):
312-
ann:dict[str,Any]|None=base.__dict__.get('__annotations__')
313-
ifnotannorisinstance(ann,types.GetSetDescriptorType):
328+
# For Python 3.14, we could also use `Format.VALUE` and pass the globals/locals
329+
# from the ns_resolver, but we want to be able to know which specific field failed
330+
# to evaluate:
331+
ann=safe_get_annotations(base)
332+
333+
ifnotann:
314334
continue
335+
315336
withns_resolver.push(base):
316337
globalns,localns=ns_resolver.types_namespace
317338
forname,valueinann.items():
@@ -341,13 +362,18 @@ def get_cls_type_hints(
341362
obj: The class to inspect.
342363
ns_resolver: A namespace resolver instance to use. Defaults to an empty instance.
343364
"""
344-
hints:dict[str,Any]|dict[str,tuple[Any,bool]]= {}
365+
hints:dict[str,Any]= {}
345366
ns_resolver=ns_resolverorNsResolver()
346367

347368
forbaseinreversed(obj.__mro__):
348-
ann:dict[str,Any]|None=base.__dict__.get('__annotations__')
349-
ifnotannorisinstance(ann,types.GetSetDescriptorType):
369+
# For Python 3.14, we could also use `Format.VALUE` and pass the globals/locals
370+
# from the ns_resolver, but we want to be able to know which specific field failed
371+
# to evaluate:
372+
ann=safe_get_annotations(base)
373+
374+
ifnotann:
350375
continue
376+
351377
withns_resolver.push(base):
352378
globalns,localns=ns_resolver.types_namespace
353379
forname,valueinann.items():

‎pydantic/dataclasses.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,12 @@ def make_pydantic_fields_compatible(cls: type[Any]) -> None:
158158
`x: int = dataclasses.field(default=pydantic.Field(..., kw_only=True), kw_only=True)`
159159
"""
160160
forannotation_clsincls.__mro__:
161-
annotations:dict[str,Any]=getattr(annotation_cls,'__annotations__', {})
161+
ifsys.version_info>= (3,14):
162+
fromannotationlibimportFormat,get_annotations
163+
164+
annotations=get_annotations(annotation_cls,format=Format.FORWARDREF)
165+
else:
166+
annotations:dict[str,Any]=getattr(annotation_cls,'__annotations__', {})
162167
forfield_nameinannotations:
163168
field_value=getattr(cls,field_name,None)
164169
# Process only if this is an instance of `FieldInfo`.
@@ -177,9 +182,9 @@ def make_pydantic_fields_compatible(cls: type[Any]) -> None:
177182
field_args['repr']=field_value.repr
178183

179184
setattr(cls,field_name,dataclasses.field(**field_args))
180-
# In Python 3.9, when subclassing, information is pulled fromcls.__dict__['__annotations__']
181-
# for annotations, so we must make sure it's initialized before we add to it.
182-
ifcls.__dict__.get('__annotations__')isNone:
185+
ifsys.version_info< (3,10)andcls.__dict__.get('__annotations__')isNone:
186+
# In Python 3.9, when a class doesn't have any annotations, accessing `__annotations__`
187+
# raises an `AttributeError`.
183188
cls.__annotations__= {}
184189
cls.__annotations__[field_name]=annotations[field_name]
185190

‎pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ classifiers = [
3434
'Programming Language :: Python :: 3.11',
3535
'Programming Language :: Python :: 3.12',
3636
'Programming Language :: Python :: 3.13',
37+
'Programming Language :: Python :: 3.14',
3738
'Intended Audience :: Developers',
3839
'Intended Audience :: Information Technology',
3940
'Operating System :: OS Independent',
@@ -223,6 +224,8 @@ pydocstyle = { convention = 'google' }
223224
'docs/*' = ['D']
224225
'pydantic/__init__.py' = ['F405','F403','D']
225226
'tests/test_forward_ref.py' = ['F821']
227+
# We can't configure a specific Python version per file (this one only supports 3.14+):
228+
'tests/test_deferred_annotations.py' = ['F821','F841']
226229
'tests/test_main.py' = ['PIE807']
227230
'tests/*' = ['D','B','C4']
228231
'pydantic/_internal/_known_annotated_metadata.py' = ['PIE800']

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp