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
=== modified file 'recipe.py'
--- recipe.py 2010-10-27 18:22:03 +0000
+++ recipe.py 2010-11-18 21:01:47 +0000
@@ -16,6 +16,7 @@
16import os16import os
17import signal17import signal
18import subprocess18import subprocess
19from textwrap import dedent
1920
20from bzrlib import (21from bzrlib import (
21 branch,22 branch,
@@ -55,6 +56,10 @@
55NEST_PART_INSTRUCTION = "nest-part"56NEST_PART_INSTRUCTION = "nest-part"
56NEST_INSTRUCTION = "nest"57NEST_INSTRUCTION = "nest"
57RUN_INSTRUCTION = "run"58RUN_INSTRUCTION = "run"
59USAGE = {
60 'merge': 'merge NAME BRANCH [REVISION]',
61 'nest': 'nest NAME BRANCH TARGET-DIR [REVISION]',
62 'nest-part': 'NAME BRANCH SUBDIR [TARGET-DIR [REVISION]]',}
5863
59SAFE_INSTRUCTIONS = [64SAFE_INSTRUCTIONS = [
60 MERGE_INSTRUCTION, NEST_PART_INSTRUCTION, NEST_INSTRUCTION]65 MERGE_INSTRUCTION, NEST_PART_INSTRUCTION, NEST_INSTRUCTION]
@@ -767,6 +772,14 @@
767 problem=problem)772 problem=problem)
768773
769774
775class InstructionParseError(RecipeParseError):
776 _fmt = RecipeParseError._fmt + "\nUsage: %(usage)s"
777
778 def __init__(self, filename, line, char, problem, instruction):
779 RecipeParseError.__init__(self, filename, line, char, problem)
780 self.usage = USAGE[instruction]
781
782
770class ForbiddenInstructionError(RecipeParseError):783class ForbiddenInstructionError(RecipeParseError):
771784
772 def __init__(self, filename, line, char, problem, instruction_name=None):785 def __init__(self, filename, line, char, problem, instruction_name=None):
@@ -858,12 +871,12 @@
858 self.new_line()871 self.new_line()
859 active_branches[-1].run_command(command)872 active_branches[-1].run_command(command)
860 else:873 else:
861 branch_id = self.parse_branch_id()874 branch_id = self.parse_branch_id(instruction)
862 url = self.parse_branch_url()875 url = self.parse_branch_url(instruction)
863 if instruction == NEST_INSTRUCTION:876 if instruction == NEST_INSTRUCTION:
864 location = self.parse_branch_location()877 location = self.parse_branch_location(instruction)
865 if instruction == NEST_PART_INSTRUCTION:878 if instruction == NEST_PART_INSTRUCTION:
866 path = self.parse_subpath()879 path = self.parse_subpath(instruction)
867 target_subdir = self.parse_optional_path()880 target_subdir = self.parse_optional_path()
868 if target_subdir == '':881 if target_subdir == '':
869 target_subdir = None882 target_subdir = None
@@ -872,7 +885,7 @@
872 revspec = self.parse_optional_revspec()885 revspec = self.parse_optional_revspec()
873 else:886 else:
874 revspec = self.parse_optional_revspec()887 revspec = self.parse_optional_revspec()
875 self.new_line()888 self.new_line(instruction)
876 last_branch = RecipeBranch(branch_id, url, revspec=revspec)889 last_branch = RecipeBranch(branch_id, url, revspec=revspec)
877 if instruction == NEST_INSTRUCTION:890 if instruction == NEST_INSTRUCTION:
878 active_branches[-1].nest_branch(location, last_branch)891 active_branches[-1].nest_branch(location, last_branch)
@@ -927,12 +940,13 @@
927 self.throw_parse_error("Expecting %s, got '%s'"940 self.throw_parse_error("Expecting %s, got '%s'"
928 % (options_str, instruction))941 % (options_str, instruction))
929942
930 def parse_branch_id(self):943 def parse_branch_id(self, instruction):
931 self.parse_whitespace("the branch id")944 self.parse_whitespace("the branch id", instruction=instruction)
932 branch_id = self.peek_to_whitespace()945 branch_id = self.peek_to_whitespace()
933 if branch_id is None:946 if branch_id is None:
934 self.throw_parse_error("End of line while looking for the "947 self.throw_parse_error("End of line while looking for the "
935 "branch id")948 "branch id", cls=InstructionParseError,
949 instruction=instruction)
936 if branch_id in self.seen_nicks:950 if branch_id in self.seen_nicks:
937 self.throw_parse_error("'%s' was already used to identify "951 self.throw_parse_error("'%s' was already used to identify "
938 "a branch." % branch_id)952 "a branch." % branch_id)
@@ -940,36 +954,39 @@
940 self.seen_nicks.add(branch_id)954 self.seen_nicks.add(branch_id)
941 return branch_id955 return branch_id
942956
943 def parse_branch_url(self):957 def parse_branch_url(self, instruction):
944 self.parse_whitespace("the branch url")958 self.parse_whitespace("the branch url", instruction=instruction)
945 branch_url = self.take_to_whitespace("the branch url")959 branch_url = self.take_to_whitespace("the branch url", instruction)
946 return branch_url960 return branch_url
947961
948 def parse_branch_location(self):962 def parse_branch_location(self, instruction):
949 # FIXME: Needs a better term963 # FIXME: Needs a better term
950 self.parse_whitespace("the location to nest")964 self.parse_whitespace("the location to nest")
951 location = self.peek_to_whitespace()965 location = self.peek_to_whitespace()
952 if location is None:966 if location is None:
953 self.throw_parse_error("End of line while looking for the "967 self.throw_parse_error("End of line while looking for the "
954 "location to nest")968 "location to nest", cls=InstructionParseError,
969 instruction=instruction)
955 norm_location = os.path.normpath(location)970 norm_location = os.path.normpath(location)
956 if norm_location in self.seen_paths:971 if norm_location in self.seen_paths:
957 self.throw_parse_error("The path '%s' is a duplicate of "972 self.throw_parse_error("The path '%s' is a duplicate of "
958 "the one used on line %d." % (location,973 "the one used on line %d." % (location,
959 self.seen_paths[norm_location]))974 self.seen_paths[norm_location]),
975 InstructionParseError, instruction=instruction)
960 if os.path.isabs(norm_location):976 if os.path.isabs(norm_location):
961 self.throw_parse_error("Absolute paths are not allowed: %s"977 self.throw_parse_error("Absolute paths are not allowed: %s"
962 % location)978 % location, InstructionParseError, instruction=instruction)
963 if norm_location.startswith(".."):979 if norm_location.startswith(".."):
964 self.throw_parse_error("Paths outside the current directory "980 self.throw_parse_error("Paths outside the current directory "
965 "are not allowed: %s" % location)981 "are not allowed: %s" % location,
982 cls=InstructionParseError, instruction=instruction)
966 self.take_chars(len(location))983 self.take_chars(len(location))
967 self.seen_paths[norm_location] = self.line_index + 1984 self.seen_paths[norm_location] = self.line_index + 1
968 return location985 return location
969986
970 def parse_subpath(self):987 def parse_subpath(self, instruction):
971 self.parse_whitespace("the subpath to merge")988 self.parse_whitespace("the subpath to merge", instruction=instruction)
972 location = self.take_to_whitespace("the subpath to merge")989 location = self.take_to_whitespace("the subpath to merge", instruction)
973 return location990 return location
974991
975 def parse_revspec(self):992 def parse_revspec(self):
@@ -1004,13 +1021,18 @@
1004 def throw_eol(self, expected):1021 def throw_eol(self, expected):
1005 self.throw_parse_error("End of line while looking for '%s'" % expected)1022 self.throw_parse_error("End of line while looking for '%s'" % expected)
10061023
1007 def new_line(self):1024 def new_line(self, instruction=None):
1008 # Jump over any whitespace1025 # Jump over any whitespace
1009 self.parse_whitespace(None, require=False)1026 self.parse_whitespace(None, require=False)
1010 remaining = self.peek_to_whitespace()1027 remaining = self.peek_to_whitespace()
1011 if remaining != None:1028 if remaining != None:
1029 kwargs = {}
1030 if instruction is not None:
1031 kwargs = {
1032 'cls': InstructionParseError,
1033 'instruction': instruction}
1012 self.throw_parse_error("Expecting the end of the line, got '%s'"1034 self.throw_parse_error("Expecting the end of the line, got '%s'"
1013 % remaining)1035 % remaining, **kwargs)
1014 self.index = 01036 self.index = 0
1015 self.line_index += 11037 self.line_index += 1
1016 if self.line_index >= len(self.lines):1038 if self.line_index >= len(self.lines):
@@ -1076,12 +1098,18 @@
1076 return old_indent_level1098 return old_indent_level
1077 return None1099 return None
10781100
1079 def parse_whitespace(self, looking_for, require=True):1101 def parse_whitespace(self, looking_for, require=True, instruction=None):
1080 if require:1102 if require:
1081 actual = self.peek_char()1103 actual = self.peek_char()
1082 if actual is None:1104 if actual is None:
1105 kwargs = {}
1106 if instruction is not None:
1107 kwargs = {
1108 'cls': InstructionParseError,
1109 'instruction': instruction,
1110 }
1083 self.throw_parse_error("End of line while looking for "1111 self.throw_parse_error("End of line while looking for "
1084 "%s" % looking_for)1112 "%s" % looking_for, **kwargs)
1085 if actual not in self.whitespace_chars:1113 if actual not in self.whitespace_chars:
1086 self.throw_parse_error("Expecting whitespace before %s, "1114 self.throw_parse_error("Expecting whitespace before %s, "
1087 "got '%s'." % (looking_for, actual))1115 "got '%s'." % (looking_for, actual))
@@ -1128,11 +1156,16 @@
1128 char = self.peek_char(skip=count)1156 char = self.peek_char(skip=count)
1129 return ret1157 return ret
11301158
1131 def take_to_whitespace(self, looking_for):1159 def take_to_whitespace(self, looking_for, instruction=None):
1132 text = self.peek_to_whitespace()1160 text = self.peek_to_whitespace()
1133 if text is None:1161 if text is None:
1162 kwargs = {}
1163 if instruction is not None:
1164 kwargs = {
1165 'cls': InstructionParseError,
1166 'instruction': instruction}
1134 self.throw_parse_error("End of line while looking for %s"1167 self.throw_parse_error("End of line while looking for %s"
1135 % looking_for)1168 % looking_for, **kwargs)
1136 self.take_chars(len(text))1169 self.take_chars(len(text))
1137 return text1170 return text
11381171
11391172
=== modified file 'tests/test_recipe.py'
--- tests/test_recipe.py 2010-10-27 18:22:03 +0000
+++ tests/test_recipe.py 2010-11-18 21:01:47 +0000
@@ -33,9 +33,11 @@
33 pull_or_branch,33 pull_or_branch,
34 RecipeParser,34 RecipeParser,
35 RecipeBranch,35 RecipeBranch,
36 InstructionParseError,
36 RecipeParseError,37 RecipeParseError,
37 resolve_revisions,38 resolve_revisions,
38 SAFE_INSTRUCTIONS,39 SAFE_INSTRUCTIONS,
40 USAGE,
39 )41 )
4042
4143
@@ -59,6 +61,13 @@
59 self.assertEqual("recipe", exc.filename)61 self.assertEqual("recipe", exc.filename)
60 return exc62 return exc
6163
64 def assertInstructionParseError(self, line, char, problem, usage,
65 callable, *args, **kwargs):
66 exc = self.assertParseError(line, char, problem, callable, *args,
67 **kwargs)
68 self.assertIsInstance(exc, InstructionParseError),
69 self.assertEqual(usage, exc.usage)
70
62 def check_recipe_branch(self, branch, name, url, revspec=None,71 def check_recipe_branch(self, branch, name, url, revspec=None,
63 num_child_branches=0, revid=None):72 num_child_branches=0, revid=None):
64 self.assertEqual(name, branch.name)73 self.assertEqual(name, branch.name)
@@ -138,53 +147,66 @@
138 self.basic_header + "http://foo.org/\n" + "cat")147 self.basic_header + "http://foo.org/\n" + "cat")
139148
140 def test_rejects_merge_no_name(self):149 def test_rejects_merge_no_name(self):
141 self.assertParseError(3, 7, "End of line while looking for "150 self.assertInstructionParseError(3, 7, "End of line while looking for "
142 "the branch id", self.get_recipe,151 "the branch id", "merge NAME BRANCH [REVISION]",
143 self.basic_header_and_branch + "merge ")152 self.get_recipe, self.basic_header_and_branch + "merge ")
153
154 def test_rejects_merge_no_name_no_space(self):
155 self.assertInstructionParseError(3, 6, "End of line while looking for "
156 "the branch id", "merge NAME BRANCH [REVISION]",
157 self.get_recipe, self.basic_header_and_branch + "merge")
144158
145 def test_rejects_merge_no_url(self):159 def test_rejects_merge_no_url(self):
146 self.assertParseError(3, 11, "End of line while looking for "160 self.assertInstructionParseError(3, 11, "End of line while looking for"
147 "the branch url", self.get_recipe,161 " the branch url", "merge NAME BRANCH [REVISION]",
148 self.basic_header_and_branch + "merge foo ")162 self.get_recipe, self.basic_header_and_branch + "merge foo ")
149163
150 def test_rejects_text_at_end_of_merge_line(self):164 def test_rejects_text_at_end_of_merge_line(self):
151 self.assertParseError(3, 17, "Expecting the end of the line, "165 self.assertInstructionParseError(3, 17,
152 "got 'bar'", self.get_recipe,166 "Expecting the end of the line, got 'bar'",
167 'merge NAME BRANCH [REVISION]', self.get_recipe,
153 self.basic_header_and_branch + "merge foo url 2 bar")168 self.basic_header_and_branch + "merge foo url 2 bar")
154169
155 def test_rejects_nest_part_no_name(self):170 def test_rejects_nest_part_no_name(self):
156 self.assertParseError(3, 11, "End of line while looking for "171 self.assertInstructionParseError(3, 11, "End of line while looking for"
157 "the branch id", self.get_recipe,172 " the branch id", USAGE['nest-part'], self.get_recipe,
158 self.basic_header_and_branch + "nest-part ")173 self.basic_header_and_branch + "nest-part ")
159174
160 def test_rejects_nest_part_no_url(self):175 def test_rejects_nest_part_no_url(self):
161 self.assertParseError(3, 15, "End of line while looking for "176 self.assertInstructionParseError(3, 15, "End of line while looking for"
162 "the branch url", self.get_recipe,177 " the branch url", USAGE['nest-part'], self.get_recipe,
163 self.basic_header_and_branch + "nest-part foo ")178 self.basic_header_and_branch + "nest-part foo ")
164179
165 def test_rejects_nest_part_no_subpath(self):180 def test_rejects_nest_part_no_subpath(self):
166 self.assertParseError(3, 22, "End of line while looking for "181 self.assertInstructionParseError(3, 22, "End of line while looking for"
167 "the subpath to merge", self.get_recipe,182 " the subpath to merge", USAGE['nest-part'], self.get_recipe,
168 self.basic_header_and_branch + "nest-part foo url:// ")183 self.basic_header_and_branch + "nest-part foo url:// ")
169184
185 def test_rejects_nest_part_no_subpath_no_space(self):
186 self.assertInstructionParseError(3, 21, "End of line while looking for"
187 " the subpath to merge", USAGE['nest-part'], self.get_recipe,
188 self.basic_header_and_branch + "nest-part foo url://")
189
170 def test_rejects_nest_no_name(self):190 def test_rejects_nest_no_name(self):
171 self.assertParseError(3, 6, "End of line while looking for "191 self.assertInstructionParseError(3, 6, "End of line while looking for"
172 "the branch id", self.get_recipe,192 " the branch id", USAGE['nest'], self.get_recipe,
173 self.basic_header_and_branch + "nest ")193 self.basic_header_and_branch + "nest ")
174194
175 def test_rejects_nest_no_url(self):195 def test_rejects_nest_no_url(self):
176 self.assertParseError(3, 10, "End of line while looking for "196 self.assertInstructionParseError(3, 10,
177 "the branch url", self.get_recipe,197 "End of line while looking for the branch url", USAGE['nest'],
178 self.basic_header_and_branch + "nest foo ")198 self.get_recipe, self.basic_header_and_branch + "nest foo ")
179199
180 def test_rejects_nest_no_location(self):200 def test_rejects_nest_no_location(self):
181 self.assertParseError(3, 14, "End of line while looking for "201 self.assertInstructionParseError(3, 14,
182 "the location to nest", self.get_recipe,202 "End of line while looking for the location to nest",
183 self.basic_header_and_branch + "nest foo url ")203 USAGE['nest'], self.get_recipe, self.basic_header_and_branch +
204 "nest foo url ")
184205
185 def test_rejects_text_at_end_of_nest_line(self):206 def test_rejects_text_at_end_of_nest_line(self):
186 self.assertParseError(3, 20, "Expecting the end of the line, "207 self.assertInstructionParseError(3, 20,
187 "got 'baz'", self.get_recipe,208 "Expecting the end of the line, got 'baz'", USAGE['nest'],
209 self.get_recipe,
188 self.basic_header_and_branch + "nest foo url bar 2 baz")210 self.basic_header_and_branch + "nest foo url bar 2 baz")
189211
190 def test_rejects_indent_after_first_branch(self):212 def test_rejects_indent_after_first_branch(self):
@@ -443,34 +465,40 @@
443 self.assertEqual("run", exc.instruction_name)465 self.assertEqual("run", exc.instruction_name)
444466
445 def test_error_on_duplicate_path(self):467 def test_error_on_duplicate_path(self):
446 exc = self.assertParseError(3, 15, "The path '.' is a duplicate "468 exc = self.assertInstructionParseError(3, 15,
447 "of the one used on line 1.", self.get_recipe,469 "The path '.' is a duplicate of the one used on line 1.",
470 USAGE['nest'], self.get_recipe,
448 self.basic_header_and_branch + "nest nest url .\n")471 self.basic_header_and_branch + "nest nest url .\n")
449472
450 def test_error_on_duplicate_path_with_another_nest(self):473 def test_error_on_duplicate_path_with_another_nest(self):
451 exc = self.assertParseError(4, 16, "The path 'foo' is a duplicate "474 exc = self.assertInstructionParseError(4, 16,
452 "of the one used on line 3.", self.get_recipe,475 "The path 'foo' is a duplicate of the one used on line 3.",
476 USAGE['nest'], self.get_recipe,
453 self.basic_header_and_branch + "nest nest url foo\n"477 self.basic_header_and_branch + "nest nest url foo\n"
454 + "nest nest2 url foo\n")478 + "nest nest2 url foo\n")
455479
456 def test_duplicate_path_check_uses_normpath(self):480 def test_duplicate_path_check_uses_normpath(self):
457 exc = self.assertParseError(3, 15, "The path 'foo/..' is a duplicate "481 exc = self.assertInstructionParseError(3, 15,
458 "of the one used on line 1.", self.get_recipe,482 "The path 'foo/..' is a duplicate of the one used on line 1.",
483 USAGE['nest'], self.get_recipe,
459 self.basic_header_and_branch + "nest nest url foo/..\n")484 self.basic_header_and_branch + "nest nest url foo/..\n")
460485
461 def test_error_absolute_path(self):486 def test_error_absolute_path(self):
462 exc = self.assertParseError(3, 15, "Absolute paths are not allowed: "487 exc = self.assertInstructionParseError(3, 15,
463 "/etc/passwd", self.get_recipe, self.basic_header_and_branch488 "Absolute paths are not allowed: /etc/passwd", USAGE['nest'],
489 self.get_recipe, self.basic_header_and_branch
464 + "nest nest url /etc/passwd\n")490 + "nest nest url /etc/passwd\n")
465491
466 def test_error_simple_parent_dir(self):492 def test_error_simple_parent_dir(self):
467 exc = self.assertParseError(3, 15, "Paths outside the current "493 exc = self.assertInstructionParseError(3, 15,
468 "directory are not allowed: ../foo", self.get_recipe,494 "Paths outside the current directory are not allowed: ../foo",
495 USAGE['nest'], self.get_recipe,
469 self.basic_header_and_branch + "nest nest url ../foo\n")496 self.basic_header_and_branch + "nest nest url ../foo\n")
470497
471 def test_error_complex_parent_dir(self):498 def test_error_complex_parent_dir(self):
472 exc = self.assertParseError(3, 15, "Paths outside the current "499 exc = self.assertInstructionParseError(3, 15,
473 "directory are not allowed: ./foo/../..", self.get_recipe,500 "Paths outside the current directory are not allowed:"
501 " ./foo/../..", USAGE['nest'], self.get_recipe,
474 self.basic_header_and_branch + "nest nest url ./foo/../..\n")502 self.basic_header_and_branch + "nest nest url ./foo/../..\n")
475503
476504
@@ -1064,8 +1092,8 @@
1064 nested_branch1 = RecipeBranch("nested1", "nested1_url",1092 nested_branch1 = RecipeBranch("nested1", "nested1_url",
1065 revspec="tag:foo")1093 revspec="tag:foo")
1066 base_branch.nest_branch(".", nested_branch1)1094 base_branch.nest_branch(".", nested_branch1)
1067 self.assertRaises(RecipeParseError, base_branch.get_recipe_text,1095 self.assertRaises(InstructionParseError,
1068 validate=True)1096 base_branch.get_recipe_text, validate=True)
10691097
1070 def test_str_validates(self):1098 def test_str_validates(self):
1071 base_branch = BaseRecipeBranch("base_url", "1", 0.1)1099 base_branch = BaseRecipeBranch("base_url", "1", 0.1)
@@ -1073,7 +1101,7 @@
1073 nested_branch1 = RecipeBranch("nested1", "nested1_url",1101 nested_branch1 = RecipeBranch("nested1", "nested1_url",
1074 revspec="tag:foo")1102 revspec="tag:foo")
1075 base_branch.nest_branch(".", nested_branch1)1103 base_branch.nest_branch(".", nested_branch1)
1076 self.assertRaises(RecipeParseError, str, base_branch)1104 self.assertRaises(InstructionParseError, str, base_branch)
10771105
1078 def test_with_nest_part(self):1106 def test_with_nest_part(self):
1079 base_branch = BaseRecipeBranch("base_url", "1", 0.1)1107 base_branch = BaseRecipeBranch("base_url", "1", 0.1)

Subscribers

People subscribed via source and target branches