Coverage for tdom/parser_utils_test.py: 100%

109 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-19 21:37 +0000

1from string.templatelib import Template 

2 

3import pytest 

4 

5from .parser_utils import ( 

6 ParserPositionTranslator, 

7 make_parser_pos_translator, 

8) 

9from .placeholders import PlaceholderConfig, make_placeholder_config 

10from .source import LinePosition 

11from .template_utils import PartPosition, TemplateSpan 

12 

13 

14@pytest.fixture(scope="module") 

15def ph_config(): 

16 return make_placeholder_config() 

17 

18 

19def make_ppt(template: Template, config: PlaceholderConfig) -> ParserPositionTranslator: 

20 """Just a shorthand function.""" 

21 return make_parser_pos_translator(template=template, config=config) 

22 

23 

24class TestParserPositionTranslator: 

25 def test_line_pos_to_abs_pos(self, ph_config): 

26 ppt = make_ppt(t"ab\ncd\n", ph_config) 

27 

28 assert ppt.line_pos_to_abs_pos(LinePosition(1, 0)) == 0 

29 assert ppt.line_pos_to_abs_pos(LinePosition(1, 2)) == 2 

30 assert ppt.line_pos_to_abs_pos(LinePosition(2, 0)) == 3 

31 assert ppt.line_pos_to_abs_pos(LinePosition(2, 2)) == 5 

32 assert ppt.line_pos_to_abs_pos(LinePosition(3, 0)) == 6 

33 

34 def test_translate_abs_pos_to_part_pos(self, ph_config): 

35 ppt = make_ppt(t"a{0}b", ph_config) 

36 placeholder_length = len(ph_config.make_placeholder(0)) 

37 

38 assert ppt.abs_pos_to_part_pos(0) == PartPosition(0, 0) 

39 assert ppt.abs_pos_to_part_pos(1) == PartPosition(0, 1) 

40 assert ppt.abs_pos_to_part_pos(1 + placeholder_length) == PartPosition(1, 0) 

41 assert ppt.abs_pos_to_part_pos(2 + placeholder_length) == PartPosition(1, 1) 

42 

43 with pytest.raises(ValueError, match="Absolute position falls outside"): 

44 ppt.abs_pos_to_part_pos(-1) 

45 with pytest.raises(ValueError, match="Absolute position falls outside"): 

46 ppt.abs_pos_to_part_pos(3 + placeholder_length) 

47 

48 def test_translate_span(self, ph_config): 

49 ppt = make_ppt(t"before\n <{0}\n x>", ph_config) 

50 placeholder_length = len(ph_config.make_placeholder(0)) 

51 

52 assert ppt.translate_span( 

53 LinePosition(line=2, offset=2), 

54 len("<") + placeholder_length + len("\n x>"), 

55 ) == TemplateSpan( 

56 start=PartPosition(0, len("before\n ")), 

57 stop=PartPosition(1, len("\n x>")), 

58 ) 

59 

60 with pytest.raises(ValueError, match="Parser span length must be positive"): 

61 ppt.translate_span(LinePosition(), -1) 

62 

63 def test_case_nontailing_string_ends_with_newline(self, ph_config): 

64 ppt = make_ppt(t"a\n{0}b", ph_config) 

65 assert ppt.translate(LinePosition(line=2, offset=0)) == PartPosition( 

66 s_index=0, offset=len("a\n") 

67 ), "The placeholder starts at the end of its preceding string." 

68 assert ppt.translate( 

69 LinePosition(line=2, offset=len(ph_config.make_placeholder(0))) 

70 ) == PartPosition( 

71 s_index=1, offset=0 

72 ), """This must be the start of the following string because we can't 

73 know the offset of the actual interpolation content.""" 

74 

75 def test_case_tailing_string_starts_with_newline(self, ph_config): 

76 ppt = make_ppt(t"a{0}\nb", ph_config) 

77 assert ppt.translate(LinePosition(line=2, offset=0)) == PartPosition( 

78 s_index=1, offset=1 

79 ), "line 2 should be inside the tailing string" 

80 assert ppt.translate( 

81 LinePosition(line=1, offset=1 + len(ph_config.make_placeholder(0))) 

82 ) == PartPosition(s_index=1, offset=0), ( 

83 "end of line 1 should be inside the tailing string?" 

84 ) 

85 

86 def test_case_interpolation_without_lines(self, ph_config): 

87 ppt = make_ppt(t"a{0}b", ph_config) 

88 assert ppt.translate(LinePosition(line=1, offset=1)) == PartPosition( 

89 s_index=0, offset=1 

90 ), "the interpolation starts at the end of the preceding string" 

91 assert ppt.translate( 

92 LinePosition(line=1, offset=1 + len(ph_config.make_placeholder(0))) 

93 ) == PartPosition(s_index=1, offset=0), ( 

94 "the end of the interpolation is the start of the tailing string" 

95 ) 

96 assert ppt.translate( 

97 LinePosition(line=1, offset=1 + len(ph_config.make_placeholder(0)) + 1) 

98 ) == PartPosition(s_index=1, offset=1), ( 

99 "the end of the tailing string remains the end." 

100 ) 

101 

102 def test_case_consecutive_interpolations(self, ph_config): 

103 ppt = make_ppt(t"{0}{1}", ph_config) 

104 placeholder_length = len(ph_config.make_placeholder(0)) 

105 

106 assert ppt.translate(LinePosition(1, 0)) == PartPosition(0, 0) 

107 assert ppt.translate(LinePosition(1, placeholder_length)) == PartPosition(1, 0) 

108 assert ppt.translate(LinePosition(1, 2 * placeholder_length)) == PartPosition( 

109 2, 0 

110 ) 

111 

112 def test_offset_without_line(self, ph_config): 

113 ppt = make_ppt(t"a*", ph_config) 

114 assert ppt.translate(LinePosition(line=1, offset=1)) == PartPosition( 

115 s_index=0, offset=1 

116 ), "the offset matches up without lines" 

117 assert ppt.translate(LinePosition(line=1, offset=2)) == PartPosition( 

118 s_index=0, offset=2 

119 ), "end of line is end of string" 

120 with pytest.raises(ValueError, match="Offset exceeds reachable"): 

121 # only 0, 1 and 2 are valid offsets for line 1 

122 _ = ppt.translate(LinePosition(line=1, offset=3)) == PartPosition( 

123 s_index=0, offset=3 

124 ) 

125 

126 def test_offset_with_line(self, ph_config): 

127 ppt = make_ppt(t"ab\n*", ph_config) 

128 assert ppt.translate(LinePosition(line=2, offset=0)) == PartPosition( 

129 s_index=0, offset=3 

130 ), "2nd line starts at offset in the head string" 

131 assert ppt.translate(LinePosition(line=1, offset=2)) == PartPosition( 

132 s_index=0, offset=2 

133 ), "end of 1st line is offset to NL" 

134 with pytest.raises(ValueError, match="Offset exceeds reachable"): 

135 # only 0, 1 and 2 are valid offsets for line 1 

136 _ = ppt.translate(LinePosition(line=1, offset=3)) 

137 

138 def test_offset_with_line_in_middle_part(self, ph_config): 

139 ppt = make_ppt(t"a\nb{0}cd\ne{1}\nfe", ph_config) 

140 assert ppt.translate( 

141 LinePosition(line=2, offset=1 + len(ph_config.make_placeholder(0)) + 2) 

142 ) == PartPosition(s_index=1, offset=2) 

143 

144 def test_empty_strings(self, ph_config): 

145 ppt = make_ppt(t"{0}", ph_config) 

146 assert ppt.translate(LinePosition(line=1, offset=0)) == PartPosition( 

147 s_index=0, offset=0 

148 ), "the interpolation starts at the end of the empty preceding string" 

149 assert ppt.translate( 

150 LinePosition(line=1, offset=len(ph_config.make_placeholder(0))) 

151 ) == PartPosition(s_index=1, offset=0), ( 

152 "end of interpolation is start of tail string" 

153 ) 

154 with pytest.raises(ValueError, match="Offset exceeds reachable"): 

155 # Cannot go past end of the template. 

156 _ = ppt.translate( 

157 LinePosition(line=1, offset=len(ph_config.make_placeholder(0)) + 1) 

158 ) 

159 

160 def test_empty_string(self, ph_config): 

161 ppt = make_ppt(t"", ph_config) 

162 assert ppt.translate(LinePosition(line=1, offset=0)) == PartPosition( 

163 s_index=0, offset=0 

164 ) 

165 with pytest.raises(ValueError, match="Offset exceeds reachable"): 

166 # Cannot go past end of the template. 

167 _ = ppt.translate(LinePosition(line=1, offset=1)) 

168 

169 def test_empty_line(self, ph_config): 

170 ppt = make_ppt(t"\n", ph_config) 

171 assert ppt.translate(LinePosition(line=1, offset=0)) == PartPosition( 

172 s_index=0, offset=0 

173 ) 

174 with pytest.raises(ValueError, match="Offset exceeds reachable"): 

175 # line 1 is empty, cannot offset anything 

176 _ = ppt.translate(LinePosition(line=1, offset=1)) 

177 assert ppt.translate(LinePosition(line=2, offset=0)) == PartPosition( 

178 s_index=0, offset=1 

179 ), "To skip over empty line just skip over newline" 

180 with pytest.raises(ValueError, match="Offset exceeds reachable"): 

181 # line 2 is empty, cannot offset anything 

182 _ = ppt.translate(LinePosition(line=2, offset=1)) 

183 

184 def test_bad_parser_pos_check_bounds(self, ph_config): 

185 ppt = make_ppt(t"abc\ndef", ph_config) 

186 

187 with pytest.raises(ValueError, match="Line does not exist"): 

188 _ = ppt.translate(LinePosition(line=3, offset=0)) 

189 with pytest.raises(ValueError, match="Unreachable line number"): 

190 _ = ppt.translate(LinePosition(line=0, offset=0)) 

191 with pytest.raises(ValueError, match="Unreachable offset"): 

192 _ = ppt.translate(LinePosition(line=1, offset=-1)) 

193 with pytest.raises(ValueError, match="Unreachable offset"): 

194 _ = ppt.translate(LinePosition(line=2, offset=-1)) 

195 with pytest.raises(ValueError, match="Offset exceeds reachable"): 

196 _ = ppt.translate(LinePosition(line=1, offset=100)) 

197 with pytest.raises(ValueError, match="Offset exceeds reachable"): 

198 _ = ppt.translate(LinePosition(line=2, offset=100)) 

199 

200 def test_bad_parser_pos_cannot_offset_interpolation(self, ph_config): 

201 ppt = make_ppt(t"abc\n{0}def", ph_config) 

202 

203 with pytest.raises( 

204 ValueError, 

205 match=r"Positions inside interpolation placeholders are undefined.", 

206 ): 

207 _ = ppt.translate(LinePosition(line=2, offset=1)) 

208 with pytest.raises( 

209 ValueError, 

210 match=r"Positions inside interpolation placeholders are undefined.", 

211 ): 

212 _ = ppt.translate( 

213 LinePosition(line=2, offset=len(ph_config.make_placeholder(0)) - 1) 

214 )