@@ -45,6 +45,8 @@ class _ImportCategory(IntEnum):
4545LONG_TARGET_PREFIX_LENGTH = 30
4646TYPE_ALIAS_INLINE_ARGUMENT_COUNT = 2
4747STRING_PREFIX_PATTERN = re .compile (r"(?i)^([rubf]*)(\"\"\"|'''|\"|')" )
48+ PEP695_TYPE_ALIAS_START_PATTERN = re .compile (r"^(?P<indent>\s*)type\s+(?P<target>[A-Za-z_]\w*(?:\[.*?\])?)\s*=" )
49+ PEP695_TYPE_ALIAS_PLACEHOLDER = "__datamodel_codegen_builtin_type_alias__"
4850
4951
5052def _is_valid_builtin_line_length (line_length : Any ) -> TypeGuard [int ]:
@@ -194,6 +196,67 @@ def _has_comment_token(line: str) -> bool:
194196 return "#" in line
195197
196198
199+ def _needs_pep695_type_alias_placeholders (python_version : PythonVersion | None ) -> bool :
200+ return (
201+ python_version is not None
202+ and python_version .version_key >= (3 , 12 )
203+ and (3 , 10 ) <= sys .version_info [:2 ] < (3 , 12 )
204+ )
205+
206+
207+ def _pep695_type_alias_statement_end (lines : list [str ], start_index : int ) -> int :
208+ bracket_depth = 0
209+ try :
210+ tokens = tokenize .generate_tokens (StringIO ("\n " .join (lines [start_index :])).readline )
211+ for token in tokens :
212+ if token .type == tokenize .OP :
213+ if token .string in {"(" , "[" , "{" }:
214+ bracket_depth += 1
215+ elif token .string in {")" , "]" , "}" }:
216+ bracket_depth -= 1
217+ if token .type == tokenize .NEWLINE and bracket_depth <= 0 :
218+ return start_index + token .end [0 ] - 1
219+ except tokenize .TokenError :
220+ return start_index
221+ return start_index
222+
223+
224+ def _replace_pep695_type_aliases_with_placeholders (code : str ) -> str :
225+ lines = code .splitlines ()
226+ placeholder_lines = list (lines )
227+ line_index = 0
228+ while line_index < len (lines ):
229+ line = lines [line_index ]
230+ if not PEP695_TYPE_ALIAS_START_PATTERN .match (line ):
231+ line_index += 1
232+ continue
233+
234+ indent = _line_indent (line )
235+ end_index = _pep695_type_alias_statement_end (lines , line_index )
236+ placeholder_lines [line_index ] = f"{ indent } { PEP695_TYPE_ALIAS_PLACEHOLDER } = None"
237+ for continuation_index in range (line_index + 1 , end_index + 1 ):
238+ placeholder_lines [continuation_index ] = ""
239+ line_index = end_index + 1
240+ return "\n " .join (placeholder_lines )
241+
242+
243+ def _parse_builtin_code (code : str , python_version : PythonVersion | None ) -> ast .Module | None :
244+ feature_version = python_version .version_key if python_version is not None else None
245+ try :
246+ return ast .parse (code , feature_version = feature_version )
247+ except SyntaxError :
248+ if not _needs_pep695_type_alias_placeholders (python_version ):
249+ return None
250+
251+ placeholder_code = _replace_pep695_type_aliases_with_placeholders (code )
252+ if placeholder_code == code :
253+ return None
254+ try :
255+ return ast .parse (placeholder_code , feature_version = feature_version )
256+ except SyntaxError :
257+ return None
258+
259+
197260def _format_import_node_without_reordering (
198261 node : ast .Import | ast .ImportFrom ,
199262 lines : list [str ],
@@ -1297,7 +1360,7 @@ def _format_generated_class_definition(
12971360 line_length : int ,
12981361 source : str ,
12991362) -> str | None :
1300- if len (line ) <= line_length or "#" in line :
1363+ if len (line ) <= line_length or _has_comment_token ( line ) :
13011364 return None
13021365
13031366 indent = _line_indent (line )
@@ -1329,7 +1392,7 @@ def _format_generated_class_definition(
13291392def _format_generated_module_statement ( # noqa: PLR0911
13301393 statement : ast .stmt , line : str , line_length : int , source : str
13311394) -> str | None :
1332- if "#" in line :
1395+ if _has_comment_token ( line ) :
13331396 return None
13341397 if (
13351398 isinstance (statement , ast .AnnAssign )
@@ -1394,6 +1457,43 @@ def _format_type_checking_block(
13941457_LineReplacement = tuple [int , int , list [str ]]
13951458
13961459
1460+ def _pep695_type_alias_replacement (lines : list [str ], start_index : int , line_length : int ) -> _LineReplacement | None :
1461+ line = lines [start_index ]
1462+ if (match := PEP695_TYPE_ALIAS_START_PATTERN .match (line )) is None :
1463+ return None
1464+
1465+ end_index = _pep695_type_alias_statement_end (lines , start_index )
1466+ statement_lines = lines [start_index : end_index + 1 ]
1467+ if any (_has_comment_token (statement_line ) for statement_line in statement_lines ):
1468+ return None
1469+
1470+ rhs_first_line = line [match .end () :].lstrip ()
1471+ rhs_source = "\n " .join ([rhs_first_line , * statement_lines [1 :]])
1472+ try :
1473+ value = ast .parse (rhs_source , mode = "eval" ).body
1474+ except SyntaxError :
1475+ return None
1476+ if not _is_annotated (value ) or len (line ) <= line_length :
1477+ return None
1478+
1479+ indent = match .group ("indent" )
1480+ target = match .group ("target" )
1481+ formatted_value = _format_annotated (value , indent , line_length , rhs_source )
1482+ return start_index + 1 , end_index + 1 , f"{ indent } type { target } = { formatted_value } " .splitlines ()
1483+
1484+
1485+ def _collect_pep695_type_alias_replacements (lines : list [str ], line_length : int ) -> list [_LineReplacement ]:
1486+ replacements : list [_LineReplacement ] = []
1487+ line_index = 0
1488+ while line_index < len (lines ):
1489+ if (replacement := _pep695_type_alias_replacement (lines , line_index , line_length )) is None :
1490+ line_index += 1
1491+ continue
1492+ replacements .append (replacement )
1493+ line_index = replacement [1 ]
1494+ return replacements
1495+
1496+
13971497def _collect_builtin_replacements ( # noqa: PLR0912, PLR0913
13981498 tree : ast .Module ,
13991499 lines : list [str ],
@@ -1623,9 +1723,8 @@ def apply_builtin_formatter( # noqa: PLR0913
16231723 if not code :
16241724 return ""
16251725
1626- try :
1627- tree = ast .parse (code , feature_version = python_version .version_key if python_version is not None else None )
1628- except SyntaxError :
1726+ tree = _parse_builtin_code (code , python_version )
1727+ if tree is None :
16291728 return f"{ code } \n "
16301729
16311730 replacements = _collect_builtin_replacements (
@@ -1636,6 +1735,8 @@ def apply_builtin_formatter( # noqa: PLR0913
16361735 known_first_party ,
16371736 wrap_string_literal = wrap_string_literal ,
16381737 )
1738+ if _needs_pep695_type_alias_placeholders (python_version ):
1739+ replacements .extend (_collect_pep695_type_alias_replacements (lines , line_length ))
16391740 import_nodes = _iter_module_import_nodes (tree )
16401741
16411742 if not import_nodes :
0 commit comments