Merge lp:~abentley/bzr-builder/usage into lp:bzr-builder
- usage
- Merge into trunk
Proposed by
Aaron Bentley
Status: | Merged |
---|---|
Merged at revision: | 110 |
Proposed branch: | lp:~abentley/bzr-builder/usage |
Merge into: | lp:bzr-builder |
Diff against target: |
386 lines (+125/-64) 2 files modified
recipe.py (+58/-25) tests/test_recipe.py (+67/-39) |
To merge this branch: | bzr merge lp:~abentley/bzr-builder/usage |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
James Westby | Approve | ||
Martin Pool (community) | Approve | ||
Review via email: mp+41230@code.launchpad.net |
Commit message
Provide USAGE in parse errors.
Description of the change
This branch adds usage info to bzr-builder's instruction parsing errors
to make it easier to fix incorrect syntax.
To post a comment you must log in.
Revision history for this message
James Westby (james-w) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'recipe.py' |
2 | --- recipe.py 2010-10-27 18:22:03 +0000 |
3 | +++ recipe.py 2010-11-18 21:01:47 +0000 |
4 | @@ -16,6 +16,7 @@ |
5 | import os |
6 | import signal |
7 | import subprocess |
8 | +from textwrap import dedent |
9 | |
10 | from bzrlib import ( |
11 | branch, |
12 | @@ -55,6 +56,10 @@ |
13 | NEST_PART_INSTRUCTION = "nest-part" |
14 | NEST_INSTRUCTION = "nest" |
15 | RUN_INSTRUCTION = "run" |
16 | +USAGE = { |
17 | + 'merge': 'merge NAME BRANCH [REVISION]', |
18 | + 'nest': 'nest NAME BRANCH TARGET-DIR [REVISION]', |
19 | + 'nest-part': 'NAME BRANCH SUBDIR [TARGET-DIR [REVISION]]',} |
20 | |
21 | SAFE_INSTRUCTIONS = [ |
22 | MERGE_INSTRUCTION, NEST_PART_INSTRUCTION, NEST_INSTRUCTION] |
23 | @@ -767,6 +772,14 @@ |
24 | problem=problem) |
25 | |
26 | |
27 | +class InstructionParseError(RecipeParseError): |
28 | + _fmt = RecipeParseError._fmt + "\nUsage: %(usage)s" |
29 | + |
30 | + def __init__(self, filename, line, char, problem, instruction): |
31 | + RecipeParseError.__init__(self, filename, line, char, problem) |
32 | + self.usage = USAGE[instruction] |
33 | + |
34 | + |
35 | class ForbiddenInstructionError(RecipeParseError): |
36 | |
37 | def __init__(self, filename, line, char, problem, instruction_name=None): |
38 | @@ -858,12 +871,12 @@ |
39 | self.new_line() |
40 | active_branches[-1].run_command(command) |
41 | else: |
42 | - branch_id = self.parse_branch_id() |
43 | - url = self.parse_branch_url() |
44 | + branch_id = self.parse_branch_id(instruction) |
45 | + url = self.parse_branch_url(instruction) |
46 | if instruction == NEST_INSTRUCTION: |
47 | - location = self.parse_branch_location() |
48 | + location = self.parse_branch_location(instruction) |
49 | if instruction == NEST_PART_INSTRUCTION: |
50 | - path = self.parse_subpath() |
51 | + path = self.parse_subpath(instruction) |
52 | target_subdir = self.parse_optional_path() |
53 | if target_subdir == '': |
54 | target_subdir = None |
55 | @@ -872,7 +885,7 @@ |
56 | revspec = self.parse_optional_revspec() |
57 | else: |
58 | revspec = self.parse_optional_revspec() |
59 | - self.new_line() |
60 | + self.new_line(instruction) |
61 | last_branch = RecipeBranch(branch_id, url, revspec=revspec) |
62 | if instruction == NEST_INSTRUCTION: |
63 | active_branches[-1].nest_branch(location, last_branch) |
64 | @@ -927,12 +940,13 @@ |
65 | self.throw_parse_error("Expecting %s, got '%s'" |
66 | % (options_str, instruction)) |
67 | |
68 | - def parse_branch_id(self): |
69 | - self.parse_whitespace("the branch id") |
70 | + def parse_branch_id(self, instruction): |
71 | + self.parse_whitespace("the branch id", instruction=instruction) |
72 | branch_id = self.peek_to_whitespace() |
73 | if branch_id is None: |
74 | self.throw_parse_error("End of line while looking for the " |
75 | - "branch id") |
76 | + "branch id", cls=InstructionParseError, |
77 | + instruction=instruction) |
78 | if branch_id in self.seen_nicks: |
79 | self.throw_parse_error("'%s' was already used to identify " |
80 | "a branch." % branch_id) |
81 | @@ -940,36 +954,39 @@ |
82 | self.seen_nicks.add(branch_id) |
83 | return branch_id |
84 | |
85 | - def parse_branch_url(self): |
86 | - self.parse_whitespace("the branch url") |
87 | - branch_url = self.take_to_whitespace("the branch url") |
88 | + def parse_branch_url(self, instruction): |
89 | + self.parse_whitespace("the branch url", instruction=instruction) |
90 | + branch_url = self.take_to_whitespace("the branch url", instruction) |
91 | return branch_url |
92 | |
93 | - def parse_branch_location(self): |
94 | + def parse_branch_location(self, instruction): |
95 | # FIXME: Needs a better term |
96 | self.parse_whitespace("the location to nest") |
97 | location = self.peek_to_whitespace() |
98 | if location is None: |
99 | self.throw_parse_error("End of line while looking for the " |
100 | - "location to nest") |
101 | + "location to nest", cls=InstructionParseError, |
102 | + instruction=instruction) |
103 | norm_location = os.path.normpath(location) |
104 | if norm_location in self.seen_paths: |
105 | self.throw_parse_error("The path '%s' is a duplicate of " |
106 | "the one used on line %d." % (location, |
107 | - self.seen_paths[norm_location])) |
108 | + self.seen_paths[norm_location]), |
109 | + InstructionParseError, instruction=instruction) |
110 | if os.path.isabs(norm_location): |
111 | self.throw_parse_error("Absolute paths are not allowed: %s" |
112 | - % location) |
113 | + % location, InstructionParseError, instruction=instruction) |
114 | if norm_location.startswith(".."): |
115 | self.throw_parse_error("Paths outside the current directory " |
116 | - "are not allowed: %s" % location) |
117 | + "are not allowed: %s" % location, |
118 | + cls=InstructionParseError, instruction=instruction) |
119 | self.take_chars(len(location)) |
120 | self.seen_paths[norm_location] = self.line_index + 1 |
121 | return location |
122 | |
123 | - def parse_subpath(self): |
124 | - self.parse_whitespace("the subpath to merge") |
125 | - location = self.take_to_whitespace("the subpath to merge") |
126 | + def parse_subpath(self, instruction): |
127 | + self.parse_whitespace("the subpath to merge", instruction=instruction) |
128 | + location = self.take_to_whitespace("the subpath to merge", instruction) |
129 | return location |
130 | |
131 | def parse_revspec(self): |
132 | @@ -1004,13 +1021,18 @@ |
133 | def throw_eol(self, expected): |
134 | self.throw_parse_error("End of line while looking for '%s'" % expected) |
135 | |
136 | - def new_line(self): |
137 | + def new_line(self, instruction=None): |
138 | # Jump over any whitespace |
139 | self.parse_whitespace(None, require=False) |
140 | remaining = self.peek_to_whitespace() |
141 | if remaining != None: |
142 | + kwargs = {} |
143 | + if instruction is not None: |
144 | + kwargs = { |
145 | + 'cls': InstructionParseError, |
146 | + 'instruction': instruction} |
147 | self.throw_parse_error("Expecting the end of the line, got '%s'" |
148 | - % remaining) |
149 | + % remaining, **kwargs) |
150 | self.index = 0 |
151 | self.line_index += 1 |
152 | if self.line_index >= len(self.lines): |
153 | @@ -1076,12 +1098,18 @@ |
154 | return old_indent_level |
155 | return None |
156 | |
157 | - def parse_whitespace(self, looking_for, require=True): |
158 | + def parse_whitespace(self, looking_for, require=True, instruction=None): |
159 | if require: |
160 | actual = self.peek_char() |
161 | if actual is None: |
162 | + kwargs = {} |
163 | + if instruction is not None: |
164 | + kwargs = { |
165 | + 'cls': InstructionParseError, |
166 | + 'instruction': instruction, |
167 | + } |
168 | self.throw_parse_error("End of line while looking for " |
169 | - "%s" % looking_for) |
170 | + "%s" % looking_for, **kwargs) |
171 | if actual not in self.whitespace_chars: |
172 | self.throw_parse_error("Expecting whitespace before %s, " |
173 | "got '%s'." % (looking_for, actual)) |
174 | @@ -1128,11 +1156,16 @@ |
175 | char = self.peek_char(skip=count) |
176 | return ret |
177 | |
178 | - def take_to_whitespace(self, looking_for): |
179 | + def take_to_whitespace(self, looking_for, instruction=None): |
180 | text = self.peek_to_whitespace() |
181 | if text is None: |
182 | + kwargs = {} |
183 | + if instruction is not None: |
184 | + kwargs = { |
185 | + 'cls': InstructionParseError, |
186 | + 'instruction': instruction} |
187 | self.throw_parse_error("End of line while looking for %s" |
188 | - % looking_for) |
189 | + % looking_for, **kwargs) |
190 | self.take_chars(len(text)) |
191 | return text |
192 | |
193 | |
194 | === modified file 'tests/test_recipe.py' |
195 | --- tests/test_recipe.py 2010-10-27 18:22:03 +0000 |
196 | +++ tests/test_recipe.py 2010-11-18 21:01:47 +0000 |
197 | @@ -33,9 +33,11 @@ |
198 | pull_or_branch, |
199 | RecipeParser, |
200 | RecipeBranch, |
201 | + InstructionParseError, |
202 | RecipeParseError, |
203 | resolve_revisions, |
204 | SAFE_INSTRUCTIONS, |
205 | + USAGE, |
206 | ) |
207 | |
208 | |
209 | @@ -59,6 +61,13 @@ |
210 | self.assertEqual("recipe", exc.filename) |
211 | return exc |
212 | |
213 | + def assertInstructionParseError(self, line, char, problem, usage, |
214 | + callable, *args, **kwargs): |
215 | + exc = self.assertParseError(line, char, problem, callable, *args, |
216 | + **kwargs) |
217 | + self.assertIsInstance(exc, InstructionParseError), |
218 | + self.assertEqual(usage, exc.usage) |
219 | + |
220 | def check_recipe_branch(self, branch, name, url, revspec=None, |
221 | num_child_branches=0, revid=None): |
222 | self.assertEqual(name, branch.name) |
223 | @@ -138,53 +147,66 @@ |
224 | self.basic_header + "http://foo.org/\n" + "cat") |
225 | |
226 | def test_rejects_merge_no_name(self): |
227 | - self.assertParseError(3, 7, "End of line while looking for " |
228 | - "the branch id", self.get_recipe, |
229 | - self.basic_header_and_branch + "merge ") |
230 | + self.assertInstructionParseError(3, 7, "End of line while looking for " |
231 | + "the branch id", "merge NAME BRANCH [REVISION]", |
232 | + self.get_recipe, self.basic_header_and_branch + "merge ") |
233 | + |
234 | + def test_rejects_merge_no_name_no_space(self): |
235 | + self.assertInstructionParseError(3, 6, "End of line while looking for " |
236 | + "the branch id", "merge NAME BRANCH [REVISION]", |
237 | + self.get_recipe, self.basic_header_and_branch + "merge") |
238 | |
239 | def test_rejects_merge_no_url(self): |
240 | - self.assertParseError(3, 11, "End of line while looking for " |
241 | - "the branch url", self.get_recipe, |
242 | - self.basic_header_and_branch + "merge foo ") |
243 | + self.assertInstructionParseError(3, 11, "End of line while looking for" |
244 | + " the branch url", "merge NAME BRANCH [REVISION]", |
245 | + self.get_recipe, self.basic_header_and_branch + "merge foo ") |
246 | |
247 | def test_rejects_text_at_end_of_merge_line(self): |
248 | - self.assertParseError(3, 17, "Expecting the end of the line, " |
249 | - "got 'bar'", self.get_recipe, |
250 | + self.assertInstructionParseError(3, 17, |
251 | + "Expecting the end of the line, got 'bar'", |
252 | + 'merge NAME BRANCH [REVISION]', self.get_recipe, |
253 | self.basic_header_and_branch + "merge foo url 2 bar") |
254 | |
255 | def test_rejects_nest_part_no_name(self): |
256 | - self.assertParseError(3, 11, "End of line while looking for " |
257 | - "the branch id", self.get_recipe, |
258 | + self.assertInstructionParseError(3, 11, "End of line while looking for" |
259 | + " the branch id", USAGE['nest-part'], self.get_recipe, |
260 | self.basic_header_and_branch + "nest-part ") |
261 | |
262 | def test_rejects_nest_part_no_url(self): |
263 | - self.assertParseError(3, 15, "End of line while looking for " |
264 | - "the branch url", self.get_recipe, |
265 | + self.assertInstructionParseError(3, 15, "End of line while looking for" |
266 | + " the branch url", USAGE['nest-part'], self.get_recipe, |
267 | self.basic_header_and_branch + "nest-part foo ") |
268 | |
269 | def test_rejects_nest_part_no_subpath(self): |
270 | - self.assertParseError(3, 22, "End of line while looking for " |
271 | - "the subpath to merge", self.get_recipe, |
272 | + self.assertInstructionParseError(3, 22, "End of line while looking for" |
273 | + " the subpath to merge", USAGE['nest-part'], self.get_recipe, |
274 | self.basic_header_and_branch + "nest-part foo url:// ") |
275 | |
276 | + def test_rejects_nest_part_no_subpath_no_space(self): |
277 | + self.assertInstructionParseError(3, 21, "End of line while looking for" |
278 | + " the subpath to merge", USAGE['nest-part'], self.get_recipe, |
279 | + self.basic_header_and_branch + "nest-part foo url://") |
280 | + |
281 | def test_rejects_nest_no_name(self): |
282 | - self.assertParseError(3, 6, "End of line while looking for " |
283 | - "the branch id", self.get_recipe, |
284 | + self.assertInstructionParseError(3, 6, "End of line while looking for" |
285 | + " the branch id", USAGE['nest'], self.get_recipe, |
286 | self.basic_header_and_branch + "nest ") |
287 | |
288 | def test_rejects_nest_no_url(self): |
289 | - self.assertParseError(3, 10, "End of line while looking for " |
290 | - "the branch url", self.get_recipe, |
291 | - self.basic_header_and_branch + "nest foo ") |
292 | + self.assertInstructionParseError(3, 10, |
293 | + "End of line while looking for the branch url", USAGE['nest'], |
294 | + self.get_recipe, self.basic_header_and_branch + "nest foo ") |
295 | |
296 | def test_rejects_nest_no_location(self): |
297 | - self.assertParseError(3, 14, "End of line while looking for " |
298 | - "the location to nest", self.get_recipe, |
299 | - self.basic_header_and_branch + "nest foo url ") |
300 | + self.assertInstructionParseError(3, 14, |
301 | + "End of line while looking for the location to nest", |
302 | + USAGE['nest'], self.get_recipe, self.basic_header_and_branch + |
303 | + "nest foo url ") |
304 | |
305 | def test_rejects_text_at_end_of_nest_line(self): |
306 | - self.assertParseError(3, 20, "Expecting the end of the line, " |
307 | - "got 'baz'", self.get_recipe, |
308 | + self.assertInstructionParseError(3, 20, |
309 | + "Expecting the end of the line, got 'baz'", USAGE['nest'], |
310 | + self.get_recipe, |
311 | self.basic_header_and_branch + "nest foo url bar 2 baz") |
312 | |
313 | def test_rejects_indent_after_first_branch(self): |
314 | @@ -443,34 +465,40 @@ |
315 | self.assertEqual("run", exc.instruction_name) |
316 | |
317 | def test_error_on_duplicate_path(self): |
318 | - exc = self.assertParseError(3, 15, "The path '.' is a duplicate " |
319 | - "of the one used on line 1.", self.get_recipe, |
320 | + exc = self.assertInstructionParseError(3, 15, |
321 | + "The path '.' is a duplicate of the one used on line 1.", |
322 | + USAGE['nest'], self.get_recipe, |
323 | self.basic_header_and_branch + "nest nest url .\n") |
324 | |
325 | def test_error_on_duplicate_path_with_another_nest(self): |
326 | - exc = self.assertParseError(4, 16, "The path 'foo' is a duplicate " |
327 | - "of the one used on line 3.", self.get_recipe, |
328 | + exc = self.assertInstructionParseError(4, 16, |
329 | + "The path 'foo' is a duplicate of the one used on line 3.", |
330 | + USAGE['nest'], self.get_recipe, |
331 | self.basic_header_and_branch + "nest nest url foo\n" |
332 | + "nest nest2 url foo\n") |
333 | |
334 | def test_duplicate_path_check_uses_normpath(self): |
335 | - exc = self.assertParseError(3, 15, "The path 'foo/..' is a duplicate " |
336 | - "of the one used on line 1.", self.get_recipe, |
337 | + exc = self.assertInstructionParseError(3, 15, |
338 | + "The path 'foo/..' is a duplicate of the one used on line 1.", |
339 | + USAGE['nest'], self.get_recipe, |
340 | self.basic_header_and_branch + "nest nest url foo/..\n") |
341 | |
342 | def test_error_absolute_path(self): |
343 | - exc = self.assertParseError(3, 15, "Absolute paths are not allowed: " |
344 | - "/etc/passwd", self.get_recipe, self.basic_header_and_branch |
345 | + exc = self.assertInstructionParseError(3, 15, |
346 | + "Absolute paths are not allowed: /etc/passwd", USAGE['nest'], |
347 | + self.get_recipe, self.basic_header_and_branch |
348 | + "nest nest url /etc/passwd\n") |
349 | |
350 | def test_error_simple_parent_dir(self): |
351 | - exc = self.assertParseError(3, 15, "Paths outside the current " |
352 | - "directory are not allowed: ../foo", self.get_recipe, |
353 | + exc = self.assertInstructionParseError(3, 15, |
354 | + "Paths outside the current directory are not allowed: ../foo", |
355 | + USAGE['nest'], self.get_recipe, |
356 | self.basic_header_and_branch + "nest nest url ../foo\n") |
357 | |
358 | def test_error_complex_parent_dir(self): |
359 | - exc = self.assertParseError(3, 15, "Paths outside the current " |
360 | - "directory are not allowed: ./foo/../..", self.get_recipe, |
361 | + exc = self.assertInstructionParseError(3, 15, |
362 | + "Paths outside the current directory are not allowed:" |
363 | + " ./foo/../..", USAGE['nest'], self.get_recipe, |
364 | self.basic_header_and_branch + "nest nest url ./foo/../..\n") |
365 | |
366 | |
367 | @@ -1064,8 +1092,8 @@ |
368 | nested_branch1 = RecipeBranch("nested1", "nested1_url", |
369 | revspec="tag:foo") |
370 | base_branch.nest_branch(".", nested_branch1) |
371 | - self.assertRaises(RecipeParseError, base_branch.get_recipe_text, |
372 | - validate=True) |
373 | + self.assertRaises(InstructionParseError, |
374 | + base_branch.get_recipe_text, validate=True) |
375 | |
376 | def test_str_validates(self): |
377 | base_branch = BaseRecipeBranch("base_url", "1", 0.1) |
378 | @@ -1073,7 +1101,7 @@ |
379 | nested_branch1 = RecipeBranch("nested1", "nested1_url", |
380 | revspec="tag:foo") |
381 | base_branch.nest_branch(".", nested_branch1) |
382 | - self.assertRaises(RecipeParseError, str, base_branch) |
383 | + self.assertRaises(InstructionParseError, str, base_branch) |
384 | |
385 | def test_with_nest_part(self): |
386 | base_branch = BaseRecipeBranch("base_url", "1", 0.1) |
That looks reasonable to me, though James might like to review it too.
Needing to pass around the 'instruction' quite so much suggests perhaps splitting out a class that knows the instruction it's parsing?