Coverage for tdom/source.py: 98%
42 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-19 21:37 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-19 21:37 +0000
1import typing as t
2from dataclasses import dataclass
3from string.templatelib import Interpolation, Template
5from .template_utils import PartPosition, TemplateRef, TemplateSpan
8@dataclass(slots=True, frozen=True)
9class LinePosition:
10 """An immutable position in a block of source code."""
12 line: int = 1
13 """Line of code, starts at 1."""
14 offset: int = 0
15 """Offset from the start of the line, starts at 0."""
18def template_repr_iter(template: Template) -> t.Generator[str]:
19 """
20 Yield a string representation of each part of a given template.
22 @NOTE: This will not yield empty strings because it uses the underlying
23 template iterator which does not.
24 """
25 for part in template:
26 if isinstance(part, str):
27 yield part
28 else:
29 yield interpolation_repr(part)
32def template_repr(template: Template) -> str:
33 """
34 Create a string representation of the given template.
35 """
36 return "".join(template_repr_iter(template))
39def interpolation_repr(ip: Interpolation) -> str:
40 """
41 Create a string representation of the given interpolation.
42 """
43 expr_str = ip.expression
44 conversion_str = f"!{ip.conversion}" if ip.conversion is not None else ""
45 format_spec_str = f":{ip.format_spec}" if ip.format_spec else ""
46 return f"{{{expr_str}{conversion_str}{format_spec_str}}}"
49@dataclass
50class SourceReader:
51 """Format report-like strings from template source for error reporting."""
53 template: Template
55 def ref_to_repr(self, ref: TemplateRef) -> str:
56 """
57 Convert tref to string representation of the underlying template.
58 """
59 filled_template = ref.bind(self.template.interpolations)
60 return template_repr(filled_template)
62 def span_to_repr(self, span: TemplateSpan) -> str:
63 """
64 Extract template span and convert to string representation.
65 """
66 filled_template = span.extract(self.template)
67 return template_repr(filled_template)
69 def span_to_template(self, span: TemplateSpan) -> Template:
70 return span.extract(self.template)
72 def make_template_pos_msg(self, source_pos: PartPosition) -> str:
73 """
74 Make a message to display the line number and offset number.
75 """
76 template_pos = self.to_template_pos(source_pos)
77 return f"line {template_pos.line} offset {template_pos.offset}"
79 def make_interpolation_repr(self, i_index: int) -> str:
80 return interpolation_repr(self.template.interpolations[i_index])
82 def to_template_pos(self, source_pos: PartPosition) -> LinePosition:
83 """
84 Convert a (template) part position into a line position based on the
85 string representation of the template.
86 """
87 span_up_to_pos = TemplateSpan(start=PartPosition(0, 0), stop=source_pos)
88 repr_up_to_pos = template_repr(span_up_to_pos.extract(self.template))
89 lines_up_to_pos = repr_up_to_pos.split("\n")
90 return LinePosition(line=len(lines_up_to_pos), offset=len(lines_up_to_pos[-1]))