Skip to content

Commit 33b9347

Browse files
authored
Skip from __future__ import annotations for Python 3.14+ targets (PEP 649) (#2658)
* Skip future annotations import for Python 3.14 and add version checks for native deferred annotations * Skip future annotations for Python 3.13 and 3.14 in tests with version checks
1 parent 232d9c6 commit 33b9347

7 files changed

Lines changed: 206 additions & 1 deletion

File tree

src/datamodel_code_generator/format.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,16 @@ def _is_py_311_or_later(self) -> bool: # pragma: no cover
7575
def _is_py_312_or_later(self) -> bool: # pragma: no cover
7676
return self.value not in {self.PY_39.value, self.PY_310.value, self.PY_311.value}
7777

78+
@cached_property
79+
def _is_py_314_or_later(self) -> bool:
80+
return self.value not in {
81+
self.PY_39.value,
82+
self.PY_310.value,
83+
self.PY_311.value,
84+
self.PY_312.value,
85+
self.PY_313.value,
86+
}
87+
7888
@property
7989
def has_union_operator(self) -> bool: # pragma: no cover
8090
"""Check if Python version supports the union operator (|)."""
@@ -100,6 +110,11 @@ def has_type_statement(self) -> bool:
100110
"""Check if Python version supports type statements."""
101111
return self._is_py_312_or_later
102112

113+
@property
114+
def has_native_deferred_annotations(self) -> bool:
115+
"""Check if Python version has native deferred annotations (Python 3.14+)."""
116+
return self._is_py_314_or_later
117+
103118
@property
104119
def has_strenum(self) -> bool:
105120
"""Check if Python version supports StrEnum."""

src/datamodel_code_generator/parser/base.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2146,7 +2146,11 @@ def parse( # noqa: PLR0912, PLR0913, PLR0914, PLR0915, PLR0917
21462146
"""Parse schema and generate code, returning single file or module dict."""
21472147
self.parse_raw()
21482148

2149-
if with_import and not disable_future_imports:
2149+
if (
2150+
with_import
2151+
and not disable_future_imports
2152+
and not self.target_python_version.has_native_deferred_annotations
2153+
):
21502154
self.imports.append(IMPORT_ANNOTATIONS)
21512155

21522156
if format_:
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# generated by datamodel-codegen:
2+
# filename: api.yaml
3+
# timestamp: 1985-10-26T08:21:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import List, Optional
8+
9+
from pydantic import AnyUrl, BaseModel, Field
10+
11+
12+
class Pet(BaseModel):
13+
id: int
14+
name: str
15+
tag: Optional[str] = None
16+
17+
18+
class Pets(BaseModel):
19+
__root__: List[Pet]
20+
21+
22+
class User(BaseModel):
23+
id: int
24+
name: str
25+
tag: Optional[str] = None
26+
27+
28+
class Users(BaseModel):
29+
__root__: List[User]
30+
31+
32+
class Id(BaseModel):
33+
__root__: str
34+
35+
36+
class Rules(BaseModel):
37+
__root__: List[str]
38+
39+
40+
class Error(BaseModel):
41+
code: int
42+
message: str
43+
44+
45+
class Api(BaseModel):
46+
apiKey: Optional[str] = Field(
47+
None, description='To be used as a dataset parameter value'
48+
)
49+
apiVersionNumber: Optional[str] = Field(
50+
None, description='To be used as a version parameter value'
51+
)
52+
apiUrl: Optional[AnyUrl] = Field(
53+
None, description="The URL describing the dataset's fields"
54+
)
55+
apiDocumentationUrl: Optional[AnyUrl] = Field(
56+
None, description='A URL to the API console for each API'
57+
)
58+
59+
60+
class Apis(BaseModel):
61+
__root__: List[Api]
62+
63+
64+
class Event(BaseModel):
65+
name: Optional[str] = None
66+
67+
68+
class Result(BaseModel):
69+
event: Optional[Event] = None
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# generated by datamodel-codegen:
2+
# filename: api.yaml
3+
# timestamp: 1985-10-26T08:21:00+00:00
4+
5+
from typing import List, Optional
6+
7+
from pydantic import AnyUrl, BaseModel, Field
8+
9+
10+
class Pet(BaseModel):
11+
id: int
12+
name: str
13+
tag: Optional[str] = None
14+
15+
16+
class Pets(BaseModel):
17+
__root__: List[Pet]
18+
19+
20+
class User(BaseModel):
21+
id: int
22+
name: str
23+
tag: Optional[str] = None
24+
25+
26+
class Users(BaseModel):
27+
__root__: List[User]
28+
29+
30+
class Id(BaseModel):
31+
__root__: str
32+
33+
34+
class Rules(BaseModel):
35+
__root__: List[str]
36+
37+
38+
class Error(BaseModel):
39+
code: int
40+
message: str
41+
42+
43+
class Api(BaseModel):
44+
apiKey: Optional[str] = Field(
45+
None, description='To be used as a dataset parameter value'
46+
)
47+
apiVersionNumber: Optional[str] = Field(
48+
None, description='To be used as a version parameter value'
49+
)
50+
apiUrl: Optional[AnyUrl] = Field(
51+
None, description="The URL describing the dataset's fields"
52+
)
53+
apiDocumentationUrl: Optional[AnyUrl] = Field(
54+
None, description='A URL to the API console for each API'
55+
)
56+
57+
58+
class Apis(BaseModel):
59+
__root__: List[Api]
60+
61+
62+
class Event(BaseModel):
63+
name: Optional[str] = None
64+
65+
66+
class Result(BaseModel):
67+
event: Optional[Event] = None

tests/main/conftest.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,18 @@
3030
reason="Type annotation formatting differs with black < 24",
3131
)
3232

33+
from datamodel_code_generator.format import PythonVersion, is_supported_in_black # noqa: E402
34+
35+
BLACK_PY313_SKIP = pytest.mark.skipif(
36+
not is_supported_in_black(PythonVersion.PY_313),
37+
reason=f"Installed black ({black.__version__}) doesn't support Python 3.13",
38+
)
39+
40+
BLACK_PY314_SKIP = pytest.mark.skipif(
41+
not is_supported_in_black(PythonVersion.PY_314),
42+
reason=f"Installed black ({black.__version__}) doesn't support Python 3.14",
43+
)
44+
3345
DATA_PATH: Path = Path(__file__).parent.parent / "data"
3446
EXPECTED_MAIN_PATH: Path = DATA_PATH / "expected" / "main"
3547

tests/main/openapi/test_main_openapi.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
from datamodel_code_generator.__main__ import Exit
3131
from tests.conftest import assert_directory_content, freeze_time
3232
from tests.main.conftest import (
33+
BLACK_PY313_SKIP,
34+
BLACK_PY314_SKIP,
3335
DATA_PATH,
3436
LEGACY_BLACK_SKIP,
3537
MSGSPEC_LEGACY_BLACK_SKIP,
@@ -333,6 +335,32 @@ def test_target_python_version(output_file: Path) -> None:
333335
)
334336

335337

338+
@BLACK_PY313_SKIP
339+
def test_target_python_version_313_has_future_annotations(output_file: Path) -> None:
340+
"""Test that Python 3.13 target includes future annotations import."""
341+
with freeze_time(TIMESTAMP):
342+
run_main_and_assert(
343+
input_path=OPEN_API_DATA_PATH / "api.yaml",
344+
output_path=output_file,
345+
input_file_type=None,
346+
assert_func=assert_file_content,
347+
extra_args=["--target-python-version", "3.13"],
348+
)
349+
350+
351+
@BLACK_PY314_SKIP
352+
def test_target_python_version_314_no_future_annotations(output_file: Path) -> None:
353+
"""Test that Python 3.14 target omits future annotations import (PEP 649)."""
354+
with freeze_time(TIMESTAMP):
355+
run_main_and_assert(
356+
input_path=OPEN_API_DATA_PATH / "api.yaml",
357+
output_path=output_file,
358+
input_file_type=None,
359+
assert_func=assert_file_content,
360+
extra_args=["--target-python-version", "3.14"],
361+
)
362+
363+
336364
@pytest.mark.benchmark
337365
def test_main_modular(output_dir: Path) -> None:
338366
"""Test main function on modular file."""

tests/test_format.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ def test_python_version() -> None:
2424
_ = PythonVersion("{}.{}".format(*sys.version_info[:2]))
2525

2626

27+
def test_python_version_has_native_deferred_annotations() -> None:
28+
"""Test that has_native_deferred_annotations returns correct values for each Python version."""
29+
assert not PythonVersion.PY_39.has_native_deferred_annotations
30+
assert not PythonVersion.PY_310.has_native_deferred_annotations
31+
assert not PythonVersion.PY_311.has_native_deferred_annotations
32+
assert not PythonVersion.PY_312.has_native_deferred_annotations
33+
assert not PythonVersion.PY_313.has_native_deferred_annotations
34+
assert PythonVersion.PY_314.has_native_deferred_annotations
35+
36+
2737
@pytest.mark.parametrize(
2838
("skip_string_normalization", "expected_output"),
2939
[

0 commit comments

Comments
 (0)