Merge lp:~abentley/bzr-builder/usage into lp:bzr-builder

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
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
Martin Pool (mbp) wrote :

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?

review: Approve
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)

Subscribers

People subscribed via source and target branches