@@ -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 ]:
@@ -183,7 +185,76 @@ def _import_category(module: str, level: int, known_first_party: frozenset[str])
183185def _has_inline_comment (lines : list [str ], node : ast .AST ) -> bool :
184186 lineno = getattr (node , "lineno" , 1 )
185187 end_lineno = getattr (node , "end_lineno" , None ) or lineno
186- return any ("#" in line for line in lines [lineno - 1 : end_lineno ])
188+ return any (_has_comment_token (line ) for line in lines [lineno - 1 : end_lineno ])
189+
190+
191+ def _has_comment_token (line : str ) -> bool :
192+ try :
193+ tokens = tokenize .generate_tokens (StringIO (line ).readline )
194+ return any (token .type == tokenize .COMMENT for token in tokens )
195+ except tokenize .TokenError :
196+ return "#" in line
197+
198+
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
187258
188259
189260def _format_import_node_without_reordering (
@@ -1031,7 +1102,7 @@ def _format_generated_class_statement(
10311102 for keyword in config_dict [1 ].keywords
10321103 )
10331104 )
1034- if (len (line ) <= line_length and not config_dict_needs_formatting ) or "#" in line :
1105+ if (len (line ) <= line_length and not config_dict_needs_formatting ) or _has_comment_token ( line ) :
10351106 return None
10361107
10371108 if isinstance (statement , ast .FunctionDef ):
@@ -1289,7 +1360,7 @@ def _format_generated_class_definition(
12891360 line_length : int ,
12901361 source : str ,
12911362) -> str | None :
1292- if len (line ) <= line_length or "#" in line :
1363+ if len (line ) <= line_length or _has_comment_token ( line ) :
12931364 return None
12941365
12951366 indent = _line_indent (line )
@@ -1321,7 +1392,7 @@ def _format_generated_class_definition(
13211392def _format_generated_module_statement ( # noqa: PLR0911
13221393 statement : ast .stmt , line : str , line_length : int , source : str
13231394) -> str | None :
1324- if "#" in line :
1395+ if _has_comment_token ( line ) :
13251396 return None
13261397 if (
13271398 isinstance (statement , ast .AnnAssign )
@@ -1386,6 +1457,43 @@ def _format_type_checking_block(
13861457_LineReplacement = tuple [int , int , list [str ]]
13871458
13881459
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+
13891497def _collect_builtin_replacements ( # noqa: PLR0912, PLR0913
13901498 tree : ast .Module ,
13911499 lines : list [str ],
@@ -1615,9 +1723,8 @@ def apply_builtin_formatter( # noqa: PLR0913
16151723 if not code :
16161724 return ""
16171725
1618- try :
1619- tree = ast .parse (code , feature_version = python_version .version_key if python_version is not None else None )
1620- except SyntaxError :
1726+ tree = _parse_builtin_code (code , python_version )
1727+ if tree is None :
16211728 return f"{ code } \n "
16221729
16231730 replacements = _collect_builtin_replacements (
@@ -1628,6 +1735,8 @@ def apply_builtin_formatter( # noqa: PLR0913
16281735 known_first_party ,
16291736 wrap_string_literal = wrap_string_literal ,
16301737 )
1738+ if _needs_pep695_type_alias_placeholders (python_version ):
1739+ replacements .extend (_collect_pep695_type_alias_replacements (lines , line_length ))
16311740 import_nodes = _iter_module_import_nodes (tree )
16321741
16331742 if not import_nodes :
0 commit comments