Coverage for tdom/source.py: 98%

42 statements  

« 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 

4 

5from .template_utils import PartPosition, TemplateRef, TemplateSpan 

6 

7 

8@dataclass(slots=True, frozen=True) 

9class LinePosition: 

10 """An immutable position in a block of source code.""" 

11 

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.""" 

16 

17 

18def template_repr_iter(template: Template) -> t.Generator[str]: 

19 """ 

20 Yield a string representation of each part of a given template. 

21 

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) 

30 

31 

32def template_repr(template: Template) -> str: 

33 """ 

34 Create a string representation of the given template. 

35 """ 

36 return "".join(template_repr_iter(template)) 

37 

38 

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}}}" 

47 

48 

49@dataclass 

50class SourceReader: 

51 """Format report-like strings from template source for error reporting.""" 

52 

53 template: Template 

54 

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) 

61 

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) 

68 

69 def span_to_template(self, span: TemplateSpan) -> Template: 

70 return span.extract(self.template) 

71 

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}" 

78 

79 def make_interpolation_repr(self, i_index: int) -> str: 

80 return interpolation_repr(self.template.interpolations[i_index]) 

81 

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]))