11799.4.1
by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful. |
1 |
#!/usr/bin/env python2.6
|
11799.4.3
by Gavin Panella
Allow specifying the project. |
2 |
# -*- mode: python -*-
|
11799.4.1
by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful. |
3 |
|
4 |
from base64 import standard_b64encode |
|
11799.4.5
by Gavin Panella
Add an option parser and accept some nickname-related options. |
5 |
from optparse import OptionParser |
11799.4.6
by Gavin Panella
Move the __main__ content into a main() function. |
6 |
import sys |
11799.4.7
by Gavin Panella
Create better reporting functions. |
7 |
|
11799.4.1
by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful. |
8 |
from lxml import etree |
9 |
||
10 |
||
11 |
NS = "https://launchpad.net/xmlns/2006/bugs" |
|
12 |
||
13 |
||
14 |
def norm_text(elem): |
|
15 |
if elem is not None: |
|
16 |
if elem.text is None: |
|
17 |
elem.text = u"" |
|
18 |
else: |
|
19 |
elem.text = elem.text.strip() |
|
20 |
||
21 |
||
22 |
def truncate(text, message=None): |
|
23 |
lines = text.splitlines() |
|
24 |
if len(lines) >= 30: |
|
25 |
if message is None: |
|
26 |
message = "[Truncated]" |
|
27 |
else: |
|
28 |
message = "[Truncated; %s]" % message |
|
29 |
return u"%s...\n\n%s" % ( |
|
30 |
"\n".join(lines[:30]).strip(), message) |
|
31 |
else: |
|
32 |
return text |
|
33 |
||
34 |
||
11799.4.7
by Gavin Panella
Create better reporting functions. |
35 |
def problem(message): |
36 |
sys.stderr.write("{0}\n".format(message)) |
|
37 |
||
38 |
||
39 |
def problem_detail(message): |
|
40 |
sys.stderr.write(" {0}\n".format(message)) |
|
41 |
||
42 |
||
43 |
def problem_resolution(message): |
|
44 |
sys.stderr.write(" --> {0}\n".format(message)) |
|
45 |
||
46 |
||
47 |
def problem_resolved(): |
|
48 |
sys.stderr.write("\n") |
|
49 |
||
50 |
||
11799.4.9
by Gavin Panella
Rename munge() to massage(). |
51 |
def massage(root, project_name, fix_nickname, tag_nickname): |
11799.4.4
by Gavin Panella
Move the munging code into a function. |
52 |
"""Fix problems in the bug import XML tree.
|
53 |
||
11799.4.5
by Gavin Panella
Add an option parser and accept some nickname-related options. |
54 |
This includes:
|
11799.4.4
by Gavin Panella
Move the munging code into a function. |
55 |
|
56 |
- Adding a tags element if one does not exist,
|
|
57 |
||
58 |
- Fixing up the bug nickname, adding the existing nickname as a tag,
|
|
59 |
||
11799.4.5
by Gavin Panella
Add an option parser and accept some nickname-related options. |
60 |
- Fixing up the description, including truncating it if it's too long,
|
11799.4.4
by Gavin Panella
Move the munging code into a function. |
61 |
|
11799.4.5
by Gavin Panella
Add an option parser and accept some nickname-related options. |
62 |
- Fixing up the first comment, including truncating it if it's too long,
|
11799.4.4
by Gavin Panella
Move the munging code into a function. |
63 |
|
64 |
- Normalizing whitespace.
|
|
65 |
||
66 |
"""
|
|
11799.4.1
by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful. |
67 |
# Scan the tree, fixing up issues.
|
68 |
for bug in root.findall('{%s}bug' % NS): |
|
69 |
# Get or create the tags element.
|
|
70 |
tags = bug.find('{%s}tags' % NS) |
|
71 |
if tags is None: |
|
72 |
tags = etree.SubElement(bug, '{%s}tags' % NS) |
|
73 |
||
74 |
nickname = bug.find('{%s}nickname' % NS) |
|
75 |
if nickname is None: |
|
11799.4.5
by Gavin Panella
Add an option parser and accept some nickname-related options. |
76 |
# Add an empty nickname to be filled in later.
|
11799.4.1
by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful. |
77 |
nickname = etree.SubElement(bug, '{%s}nickname' % NS) |
11799.4.5
by Gavin Panella
Add an option parser and accept some nickname-related options. |
78 |
elif tag_nickname: |
79 |
# Add the original nickname as a tag.
|
|
11799.4.1
by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful. |
80 |
etree.SubElement(tags, '{%s}tag' % NS).text = nickname.text |
81 |
||
82 |
# Change the nickname.
|
|
11799.4.5
by Gavin Panella
Add an option parser and accept some nickname-related options. |
83 |
if nickname.text is None or fix_nickname: |
84 |
nickname.text = u"%s-%s" % (project_name, bug.get('id')) |
|
11799.4.1
by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful. |
85 |
|
86 |
# Get the first comment and its text. We'll need these later.
|
|
87 |
first_comment = bug.find('{%s}comment' % NS) |
|
88 |
first_comment_text = first_comment.find('{%s}text' % NS) |
|
89 |
norm_text(first_comment_text) |
|
90 |
||
91 |
# Check the description.
|
|
92 |
description = bug.find('{%s}description' % NS) |
|
93 |
norm_text(description) |
|
94 |
if len(description.text) == 0: |
|
11799.4.7
by Gavin Panella
Create better reporting functions. |
95 |
problem("Bug %s has no description." % bug.get('id')) |
11799.4.1
by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful. |
96 |
# Try and get the description from the first comment.
|
97 |
if first_comment_text is None: |
|
11799.4.7
by Gavin Panella
Create better reporting functions. |
98 |
problem_detail("No comments!") |
99 |
problem_resolution("Setting description to '-'.") |
|
11799.4.1
by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful. |
100 |
description.text = u'-' |
101 |
elif len(first_comment_text.text) == 0: |
|
11799.4.7
by Gavin Panella
Create better reporting functions. |
102 |
problem_detail("First comment has no text!") |
103 |
problem_resolution("Setting description to '-'.") |
|
11799.4.1
by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful. |
104 |
description.text = u'-' |
105 |
else: |
|
11799.4.7
by Gavin Panella
Create better reporting functions. |
106 |
problem_detail("First comment has text.") |
107 |
problem_resolution("Removing description.") |
|
11799.4.10
by Gavin Panella
Fix comment. |
108 |
# The spec says that the description is not optional, but the
|
109 |
# importer treats it as optional.
|
|
11799.4.1
by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful. |
110 |
bug.remove(description) |
11799.4.7
by Gavin Panella
Create better reporting functions. |
111 |
problem_resolved() |
11799.4.1
by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful. |
112 |
elif len(description.text) > 50000: |
11799.4.7
by Gavin Panella
Create better reporting functions. |
113 |
problem( |
114 |
"Bug %s's description is too long (%d chars)." % ( |
|
11799.4.1
by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful. |
115 |
bug.get('id'), len(description.text),)) |
116 |
# Compare the description to the first comment. If it's
|
|
117 |
# the same, we don't need the description.
|
|
118 |
if first_comment_text is None: |
|
11799.4.7
by Gavin Panella
Create better reporting functions. |
119 |
problem_detail("No comments!") |
120 |
problem_resolution("Adding comment.") |
|
11799.4.1
by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful. |
121 |
raise NotImplementedError("Add a comment.") |
122 |
elif description.text == first_comment_text.text: |
|
11799.4.7
by Gavin Panella
Create better reporting functions. |
123 |
problem_detail('Description is same as first comment.') |
124 |
problem_resolution('Trimming description.') |
|
11799.4.1
by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful. |
125 |
# It's safe to point the user to an attachment here,
|
126 |
# even though it has not yet been created. It will be
|
|
127 |
# created later because the first comment is also too
|
|
128 |
# long.
|
|
129 |
description.text = truncate( |
|
130 |
description.text, 'see "Full description" attachment') |
|
131 |
else: |
|
11799.4.7
by Gavin Panella
Create better reporting functions. |
132 |
problem_resolution("Truncating description.") |
11799.4.1
by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful. |
133 |
raise NotImplementedError("Fix overlong description.") |
11799.4.7
by Gavin Panella
Create better reporting functions. |
134 |
problem_resolved() |
11799.4.1
by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful. |
135 |
|
136 |
# Check first comment text.
|
|
137 |
if first_comment_text is not None: |
|
138 |
if len(first_comment_text.text) == 0: |
|
11799.4.7
by Gavin Panella
Create better reporting functions. |
139 |
problem( |
140 |
"Bug %s's first comment has no text." % bug.get('id')) |
|
141 |
problem_resolution("Setting comment text to '-'.") |
|
11799.4.1
by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful. |
142 |
first_comment_text.text = u'-' |
11799.4.7
by Gavin Panella
Create better reporting functions. |
143 |
problem_resolved() |
11799.4.1
by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful. |
144 |
elif len(first_comment_text.text) > 50000: |
11799.4.7
by Gavin Panella
Create better reporting functions. |
145 |
problem( |
146 |
"Bug %s's first comment is too long (%d chars)." % ( |
|
11799.4.1
by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful. |
147 |
bug.get('id'), len(first_comment_text.text))) |
148 |
# Save the original text as an attachment.
|
|
11799.4.7
by Gavin Panella
Create better reporting functions. |
149 |
problem_resolution('Adding attachment.') |
11799.4.1
by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful. |
150 |
attachment = etree.SubElement( |
151 |
first_comment, '{%s}attachment' % NS) |
|
152 |
etree.SubElement(attachment, '{%s}filename' % NS).text = ( |
|
11799.4.3
by Gavin Panella
Allow specifying the project. |
153 |
u"%s-bug-%s-full-description.txt" % ( |
154 |
project_name, bug.get('id'))) |
|
11799.4.1
by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful. |
155 |
etree.SubElement(attachment, '{%s}title' % NS).text = ( |
156 |
u"Full description (text/plain, utf-8)") |
|
157 |
etree.SubElement(attachment, '{%s}mimetype' % NS).text = ( |
|
158 |
u"text/plain") |
|
159 |
etree.SubElement(attachment, '{%s}contents' % NS).text = ( |
|
160 |
standard_b64encode( |
|
161 |
first_comment_text.text.encode('utf-8'))) |
|
162 |
# Trim the comment text.
|
|
11799.4.7
by Gavin Panella
Create better reporting functions. |
163 |
problem_resolution('Trimming comment text.') |
11799.4.1
by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful. |
164 |
first_comment_text.text = truncate( |
165 |
first_comment_text.text, |
|
166 |
'see "Full description" attachment') |
|
11799.4.7
by Gavin Panella
Create better reporting functions. |
167 |
problem_resolved() |
11799.4.1
by Gavin Panella
Script to alter/repair bug import XML from the Openfiler project. May be generally useful. |
168 |
|
11799.4.4
by Gavin Panella
Move the munging code into a function. |
169 |
|
11799.4.6
by Gavin Panella
Move the __main__ content into a main() function. |
170 |
def main(arguments): |
11799.4.5
by Gavin Panella
Add an option parser and accept some nickname-related options. |
171 |
# optparse.OptionParser uses lower-case for usage and help text by
|
172 |
# default. This is distressing, so it is corrected for below.
|
|
173 |
usage = "Usage: %prog [options]" |
|
174 |
description = """ |
|
175 |
This acts as a filter: pipe bug import XML into stdin and capture
|
|
176 |
stdout. By default it will ensure that bug descriptions and the first
|
|
177 |
comment are correct. If either exceeds 50,000 characters it is
|
|
178 |
truncated and an attachment is created to hold the original.
|
|
179 |
"""
|
|
180 |
parser = OptionParser( |
|
181 |
usage=usage, |
|
182 |
description=description.strip(), |
|
183 |
add_help_option=False) |
|
184 |
parser.add_option( |
|
185 |
"-p", "--project", dest="project_name", metavar="NAME", |
|
186 |
help="The project to which this import data refers.") |
|
187 |
parser.add_option( |
|
188 |
"--fix-nickname", action="store_true", dest="fix_nickname", |
|
189 |
help="Normalize the nickname to ${project_name}-${bug-id}.") |
|
190 |
parser.add_option( |
|
191 |
"--tag-nickname", action="store_true", dest="tag_nickname", |
|
192 |
help="Add the original bug nickname as a tag.") |
|
193 |
parser.add_option( |
|
194 |
"-h", "--help", action="help", |
|
195 |
help="Show this help message and exit.") |
|
196 |
parser.set_defaults( |
|
197 |
project_name=None, |
|
198 |
fix_nickname=False, |
|
199 |
tag_nickname=False) |
|
200 |
||
11799.4.6
by Gavin Panella
Move the __main__ content into a main() function. |
201 |
options, args = parser.parse_args(arguments) |
11799.4.5
by Gavin Panella
Add an option parser and accept some nickname-related options. |
202 |
if len(args) != 0: |
203 |
parser.error("Positional arguments are not recognized.") |
|
204 |
if options.project_name is None: |
|
205 |
parser.error("A project name must be specified.") |
|
206 |
||
11799.4.7
by Gavin Panella
Create better reporting functions. |
207 |
tree = etree.parse(sys.stdin) |
11799.4.9
by Gavin Panella
Rename munge() to massage(). |
208 |
massage( |
11799.4.5
by Gavin Panella
Add an option parser and accept some nickname-related options. |
209 |
root=tree.getroot(), |
210 |
project_name=options.project_name, |
|
211 |
fix_nickname=options.fix_nickname, |
|
212 |
tag_nickname=options.tag_nickname) |
|
213 |
tree.write( |
|
11799.4.7
by Gavin Panella
Create better reporting functions. |
214 |
sys.stdout, encoding='utf-8', |
11799.4.5
by Gavin Panella
Add an option parser and accept some nickname-related options. |
215 |
pretty_print=True, xml_declaration=True) |
11799.4.6
by Gavin Panella
Move the __main__ content into a main() function. |
216 |
|
217 |
return 0 |
|
218 |
||
219 |
||
220 |
if __name__ == '__main__': |
|
221 |
sys.exit(main(sys.argv[1:])) |