99from collections import defaultdict
1010from enum import IntEnum
1111from io import StringIO
12- from typing import TYPE_CHECKING , Any , TypeGuard
12+ from typing import TYPE_CHECKING , Any , TypeGuard , cast
1313
1414from datamodel_code_generator .util import load_toml
1515
@@ -23,6 +23,12 @@ class PythonVersion(Protocol):
2323 def version_key (self ) -> tuple [int , int ]:
2424 raise NotImplementedError
2525
26+ class _SourceLocationNode (Protocol ):
27+ lineno : int
28+ end_lineno : int | None
29+ col_offset : int
30+ end_col_offset : int | None
31+
2632
2733class _AliasSortCategory (IntEnum ):
2834 CONSTANT = 0
@@ -47,6 +53,7 @@ class _ImportCategory(IntEnum):
4753STRING_PREFIX_PATTERN = re .compile (r"(?i)^([rubf]*)(\"\"\"|'''|\"|')" )
4854PEP695_TYPE_ALIAS_START_PATTERN = re .compile (r"^(?P<indent>\s*)type\s+(?P<target>[A-Za-z_]\w*(?:\[.*?\])?)\s*=" )
4955PEP695_TYPE_ALIAS_PLACEHOLDER = "__datamodel_codegen_builtin_type_alias__"
56+ _SOURCE_LINES_CACHE : list [tuple [str , list [str ]]] = []
5057
5158
5259def _is_valid_builtin_line_length (line_length : Any ) -> TypeGuard [int ]:
@@ -417,8 +424,70 @@ def _is_type_checking_if(node: ast.AST) -> TypeGuard[ast.If]:
417424 return isinstance (node , ast .If ) and _is_name_or_attr (node .test , "TYPE_CHECKING" )
418425
419426
427+ def _splitlines_no_ff (source : str ) -> list [str ]:
428+ """Split source lines like the Python parser, without form-feed splitting."""
429+ index = 0
430+ lines : list [str ] = []
431+ next_line = ""
432+ while index < len (source ):
433+ character = source [index ]
434+ next_line += character
435+ index += 1
436+
437+ if character == "\r " and index < len (source ) and source [index ] == "\n " :
438+ next_line += "\n "
439+ index += 1
440+ if character in "\r \n " :
441+ lines .append (next_line )
442+ next_line = ""
443+
444+ if next_line :
445+ lines .append (next_line )
446+ return lines
447+
448+
449+ def _source_lines (source : str ) -> list [str ]:
450+ if _SOURCE_LINES_CACHE :
451+ cached_source , cached_lines = _SOURCE_LINES_CACHE [0 ]
452+ if source is cached_source or source == cached_source :
453+ return cached_lines
454+
455+ lines = _splitlines_no_ff (source )
456+ _SOURCE_LINES_CACHE [:] = [(source , lines )]
457+ return lines
458+
459+
460+ def _source_segment_from_cached_lines (source : str , node : ast .AST ) -> str | None :
461+ if not (
462+ hasattr (node , "lineno" )
463+ and hasattr (node , "end_lineno" )
464+ and hasattr (node , "col_offset" )
465+ and hasattr (node , "end_col_offset" )
466+ ):
467+ return None
468+
469+ location_node = cast ("_SourceLocationNode" , node )
470+ lineno = location_node .lineno
471+ end_lineno = location_node .end_lineno
472+ col_offset = location_node .col_offset
473+ end_col_offset = location_node .end_col_offset
474+ if end_lineno is None or end_col_offset is None :
475+ return None
476+
477+ lineno -= 1
478+ end_lineno -= 1
479+
480+ lines = _source_lines (source )
481+ if end_lineno == lineno :
482+ return lines [lineno ].encode ()[col_offset :end_col_offset ].decode ()
483+
484+ first = lines [lineno ].encode ()[col_offset :].decode ()
485+ last = lines [end_lineno ].encode ()[:end_col_offset ].decode ()
486+ return "" .join ([first , * lines [lineno + 1 : end_lineno ], last ])
487+
488+
420489def _source_segment (source : str , node : ast .AST ) -> str :
421- return ast . get_source_segment (source , node ) or ast .unparse (node )
490+ return _source_segment_from_cached_lines (source , node ) or ast .unparse (node )
422491
423492
424493def _inline_source_segment (source : str , node : ast .AST ) -> str :
0 commit comments