Merge lp:~evfool/software-properties/miscfixes2 into lp:software-properties

Proposed by Robert Roth
Status: Needs review
Proposed branch: lp:~evfool/software-properties/miscfixes2
Merge into: lp:software-properties
Diff against target: 455 lines (+143/-105)
7 files modified
add-apt-repository (+67/-59)
data/gtkbuilder/dialog-add.ui (+11/-36)
softwareproperties/SoftwareProperties.py (+5/-3)
softwareproperties/gtk/SoftwarePropertiesGtk.py (+2/-0)
softwareproperties/ppa.py (+18/-6)
tests/test_aptaddrepo.py (+36/-0)
tests/test_lp.py (+4/-1)
To merge this branch: bzr merge lp:~evfool/software-properties/miscfixes2
Reviewer Review Type Date Requested Status
Michael Vogt (community) Needs Information
Review via email: mp+92611@code.launchpad.net

Description of the change

This branch fixes the following bugs:
- Bug #496879 - returns non-0 result code from add-apt-repository whenever anything goes wrong, PPA can not be found, or signing key can not be found
- Bug #779287 - API url changed to the URL of the pubic API
- Bug #489488 - partially fixes this by adding the PPA display name as deb-line comment, thus the displayname will be shown in software-properties instead of the long and confusing PPA url
- Bug #930624 - add a title (Add) to the add-dialog (instead of the default software-properties-gtk) and sets the image of the Add Source button to respect the buttons-have-icons setting
- Bug #599801 - sort software sources from the other software tab alphabetically by their description, this way we get a more deterministic ordering than the default, thus users will be able to find the source they are looking for faster.

I have also made the following changes:
- removed a duplicated utf8 method(the same method has been declared twice in apt-add-repo),
- added another testcase in testlp to test invalid repo is handled correctly (no PPA info is returned)
- updated the error handling (try .. except HTTPError, URLError statements have been removed) to work with new curl based PPA info fetching (as curl does not throw an exception, but the result code can be queried separately) by only accepting HTTP 200 OK for now, other "partially successful" HTTP response codes should be added as required
- added new testaptaddrepo tests for apt-add-repository sanity checks, to see if adding invalid repo returns non-0 result code and working commands return 0 result codes

To post a comment you must log in.
749. By Robert Roth

Fix add source dialog consistency issues (LP: #930624)

750. By Robert Roth

Sort software sources alphabetically by their description (LP: #599801)

Revision history for this message
Michael Vogt (mvo) wrote :

Thanks! This looks great. I tweaked some minor issues and added a FIXME for the new tests/test_aptaddrepo.py so that it can be run as non-root. Do you think you would have time to fix this small issue? If not I will give it a go later :)

review: Needs Information
751. By Robert Roth

Merged from trunk

Revision history for this message
Robert Roth (evfool) wrote :

Hi mvo, I have missed the status change on this proposal. I have merged from the current trunk now, and I can't see your FIXMEs, what should I fix here?

752. By Robert Roth

Merged from mvo's branch

753. By Robert Roth

Added apt-root dir CLI parameter for apt-add-repository

Revision history for this message
Robert Roth (evfool) wrote :

I have found your branch, merged from it, made a few adjustments, and added an apt-rootdir CLI parameter for apt-add-repository and tried to make it work in the test, however somehow I couldn't get the tests correctly working. Could you please take a look at the updated branch?

Unmerged revisions

753. By Robert Roth

Added apt-root dir CLI parameter for apt-add-repository

752. By Robert Roth

Merged from mvo's branch

751. By Robert Roth

Merged from trunk

750. By Robert Roth

Sort software sources alphabetically by their description (LP: #599801)

749. By Robert Roth

Fix add source dialog consistency issues (LP: #930624)

748. By Robert Roth

Also return non-null result code if PPA key validation fails (LP: #496879)

747. By Robert Roth

Add basic unit test for apt-add-repository

746. By Robert Roth

Add the display name of a PPA as deb entry comment (LP: #489488)

745. By Robert Roth

Use public Launchpad API (LP:#779287)

744. By Robert Roth

Fix error handling in new launchpad info retrieval (LP: #496879)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'add-apt-repository'
2--- add-apt-repository 2012-03-07 06:43:56 +0000
3+++ add-apt-repository 2012-05-11 09:20:22 +0000
4@@ -4,6 +4,7 @@
5 import sys
6 import gettext
7 import locale
8+import pycurl
9
10 from softwareproperties.SoftwareProperties import SoftwareProperties
11 from softwareproperties.ppa import DEFAULT_KEYSERVER, expand_ppa_line
12@@ -24,7 +25,7 @@
13 return s.encode("utf-8", "ignore")
14 return unicode(s, "utf8", "ignore").encode("utf8")
15
16-def _maybe_suggest_ppa_name_based_on_user(user):
17+def _maybe_suggest_ppa_name_based_on_user(user, ppa_name):
18 try:
19 from launchpadlib.launchpad import Launchpad
20 lp = Launchpad.login_anonymously(lp_application_name, "production")
21@@ -32,31 +33,61 @@
22 user_inst = lp.people[user]
23 entity_name = "team" if user_inst.is_team else "user"
24 if len(user_inst.ppas) > 0:
25- print _("The %s named '%s' has no PPA named '%s'"
26- %(entity_name, user, ppa_name))
27+ print _("The %s named '%s' has no PPA named '%s'" ) % (entity_name, user, ppa_name)
28 print _("Please choose from the following available PPAs:")
29 for ppa in user_inst.ppas:
30 print _(" * '%s': %s" %(ppa.name, ppa.displayname))
31 else:
32- print _("The %s named '%s' does not have any PPA"
33- %(entity_name, user))
34+ print _("The %s named '%s' does not have any PPA repositories.") % (entity_name, user)
35 except KeyError:
36 pass
37 except ImportError:
38 print _("Please check that the PPA name or format is correct.")
39
40-
41-
42-def utf8(s):
43- """
44- Takes a string or unicode object and returns a utf-8 encoded
45- string, errors are ignored
46- """
47- if s is None:
48- return None
49- if isinstance(s, unicode):
50- return s.encode("utf-8", "ignore")
51- return unicode(s, "utf8", "ignore").encode("utf8")
52+def ask_user_about_ppa(line, is_removal):
53+
54+ from softwareproperties.ppa import get_ppa_info_from_lp, LAUNCHPAD_PPA_API
55+ user, sep, ppa_name = line.split(":")[1].partition("/")
56+ ppa_name = ppa_name or "ppa"
57+ try:
58+ ppa_info = get_ppa_info_from_lp(user, ppa_name)
59+ except pycurl.error:
60+ print _("Cannot access PPA (%s) to get PPA information, "
61+ "please check your internet connection.") % \
62+ (LAUNCHPAD_PPA_API % (user, ppa_name))
63+ return (1, None)
64+ # ppa not found
65+ if not ppa_info:
66+ print _("The specified PPA (%s) could not be found. ") % line
67+ if user.startswith("~"):
68+ print _("Did you mean 'ppa:%s/%s' ?" %(user[1:], ppa_name))
69+ return (1, None) # Exit because the user cannot be correct
70+ # If the PPA does not exist, then try to find if the user/team
71+ # exists. If it exists, list down the PPAs
72+ _maybe_suggest_ppa_name_based_on_user(user, ppa_name)
73+ return (1, None)
74+ # private PPAs are not supported
75+ if "private" in ppa_info and ppa_info["private"]:
76+ print _("Adding private PPAs is not supported currently")
77+ return (1, None)
78+
79+ if is_removal:
80+ print _("You are about to remove the following PPA from your system:")
81+ else:
82+ print _("You are about to add the following PPA to your system:")
83+ print " %s" % utf8(ppa_info["description"] or "")
84+ print _(" More info: %s") % ppa_info["web_link"]
85+ if (sys.stdin.isatty() and
86+ not "FORCE_ADD_APT_REPOSITORY" in os.environ):
87+ if is_removal:
88+ print _("Press [ENTER] to continue or ctrl-c to cancel removing it")
89+ else:
90+ print _("Press [ENTER] to continue or ctrl-c to cancel adding it")
91+ try:
92+ sys.stdin.readline()
93+ except KeyboardInterrupt:
94+ return (2, None)
95+ return (0, ppa_info["displayname"])
96
97 if __name__ == "__main__":
98 try:
99@@ -100,9 +131,16 @@
100 parser.add_option("-y", "--yes", action="store_true",
101 dest="assume_yes", default=False,
102 help="Assume yes to all queries")
103+ parser.add_option("-a", "--aptrootdir",
104+ action="store", dest="aptrootdir", type="string", default=None,
105+ help="Use the given directory as apt root (for testing purposes)")
106 (options, args) = parser.parse_args()
107
108- if os.geteuid() != 0:
109+ if options.aptrootdir:
110+ if not os.access(options.aptrootdir, os.W_OK):
111+ print _("Error: you don't have write access to the sources.list file %s, try running as root")%options.aptrootdir
112+ sys.exit(1)
113+ elif os.geteuid() != 0:
114 print _("Error: must run as root")
115 sys.exit(1)
116
117@@ -118,46 +156,16 @@
118
119 # display PPA info (if needed)
120 if line.startswith("ppa:") and not options.assume_yes:
121- from softwareproperties.ppa import get_ppa_info_from_lp, LAUNCHPAD_PPA_API
122- user, sep, ppa_name = line.split(":")[1].partition("/")
123- ppa_name = ppa_name or "ppa"
124- try:
125- ppa_info = get_ppa_info_from_lp(user, ppa_name)
126- except HTTPError:
127- print _("Cannot add PPA: '%s'.") % line
128- if user.startswith("~"):
129- print _("Did you mean 'ppa:%s/%s' ?" %(user[1:], ppa_name))
130- sys.exit(1) # Exit because the user cannot be correct
131- # If the PPA does not exist, then try to find if the user/team
132- # exists. If it exists, list down the PPAs
133- _maybe_suggest_ppa_name_based_on_user(user)
134- sys.exit(1)
135- except (ValueError, URLError):
136- print _("Cannot access PPA (%s) to get PPA information, "
137- "please check your internet connection.") % \
138- (LAUNCHPAD_PPA_API % (user, ppa_name))
139- sys.exit(1)
140- # private PPAs are not supported
141- if "private" in ppa_info and ppa_info["private"]:
142- print _("Adding private PPAs is not supported currently")
143- sys.exit(1)
144-
145- if options.remove:
146- print _("You are about to remove the following PPA from your system:")
147- else:
148- print _("You are about to add the following PPA to your system:")
149- print " %s" % utf8(ppa_info["description"] or "")
150- print _(" More info: %s") % ppa_info["web_link"]
151- if (sys.stdin.isatty() and
152- not "FORCE_ADD_APT_REPOSITORY" in os.environ):
153- if options.remove:
154- print _("Press [ENTER] to continue or ctrl-c to cancel removing it")
155- else:
156- print _("Press [ENTER] to continue or ctrl-c to cancel adding it")
157- sys.stdin.readline()
158+ (error_code, display_name) = ask_user_about_ppa(line, options.remove)
159+ if error_code != 0:
160+ sys.exit(error_code)
161+ line += "#" + display_name
162
163 # add it
164- sp = SoftwareProperties(options=options)
165+ if options.aptrootdir:
166+ sp = SoftwareProperties(options=options, rootdir=options.aptrootdir)
167+ else:
168+ sp = SoftwareProperties(options=options)
169 if options.remove:
170 (line, file) = expand_ppa_line(line.strip(), sp.distro.codename)
171 deb_line = sp.expand_http_line(line)
172@@ -167,14 +175,14 @@
173 try:
174 sp.remove_source(deb_entry)
175 except ValueError:
176- print _("Error: '%s' doesn't exist in a sourcelist file" % deb_line)
177+ print _("Error: '%s' doesn't exist in a sourcelist file") % deb_line
178 try:
179 sp.remove_source(debsrc_entry)
180 except ValueError:
181- print _("Error: '%s' doesn't exist in a sourcelist file" % debsrc_line)
182+ print _("Error: '%s' doesn't exist in a sourcelist file") % debsrc_line
183
184 else:
185 if not sp.add_source_from_line(line):
186- print _("Error: '%s' invalid" % line)
187+ print _("Error: could not add '%s'") % line
188 sys.exit(1)
189 sp.sourceslist.save()
190
191=== modified file 'data/gtkbuilder/dialog-add.ui'
192--- data/gtkbuilder/dialog-add.ui 2011-06-22 15:03:09 +0000
193+++ data/gtkbuilder/dialog-add.ui 2012-05-11 09:20:22 +0000
194@@ -2,11 +2,17 @@
195 <interface>
196 <!-- interface-requires gtk+ 2.12 -->
197 <!-- interface-naming-policy toplevel-contextual -->
198+ <object class="GtkImage" id="add_image">
199+ <property name="visible">True</property>
200+ <property name="can_focus">False</property>
201+ <property name="stock">gtk-add</property>
202+ </object>
203 <object class="GtkDialog" id="dialog_add_custom">
204 <property name="border_width">6</property>
205 <property name="resizable">False</property>
206 <property name="modal">True</property>
207 <property name="type_hint">dialog</property>
208+ <property name="title" translatable="yes">Add</property>
209 <property name="skip_taskbar_hint">True</property>
210 <child internal-child="vbox">
211 <object class="GtkVBox" id="dialog-vbox2">
212@@ -126,6 +132,8 @@
213 </child>
214 <child>
215 <object class="GtkButton" id="button_add_source">
216+ <property name="label" translatable="yes">_Add Source</property>
217+ <property name="use_action_appearance">False</property>
218 <property name="visible">True</property>
219 <property name="sensitive">False</property>
220 <property name="can_focus">True</property>
221@@ -133,42 +141,9 @@
222 <property name="can_default">True</property>
223 <property name="has_default">True</property>
224 <property name="receives_default">False</property>
225- <child>
226- <object class="GtkAlignment" id="alignment1">
227- <property name="visible">True</property>
228- <property name="xscale">0</property>
229- <property name="yscale">0</property>
230- <child>
231- <object class="GtkHBox" id="hbox10">
232- <property name="visible">True</property>
233- <property name="spacing">2</property>
234- <child>
235- <object class="GtkImage" id="image2">
236- <property name="visible">True</property>
237- <property name="stock">gtk-add</property>
238- </object>
239- <packing>
240- <property name="expand">False</property>
241- <property name="fill">False</property>
242- <property name="position">0</property>
243- </packing>
244- </child>
245- <child>
246- <object class="GtkLabel" id="label35">
247- <property name="visible">True</property>
248- <property name="label" translatable="yes">_Add Source</property>
249- <property name="use_underline">True</property>
250- </object>
251- <packing>
252- <property name="expand">False</property>
253- <property name="fill">False</property>
254- <property name="position">1</property>
255- </packing>
256- </child>
257- </object>
258- </child>
259- </object>
260- </child>
261+ <property name="use_action_appearance">False</property>
262+ <property name="image">add_image</property>
263+ <property name="use_underline">True</property>
264 </object>
265 <packing>
266 <property name="expand">False</property>
267
268=== modified file 'softwareproperties/SoftwareProperties.py'
269--- softwareproperties/SoftwareProperties.py 2012-03-07 08:42:03 +0000
270+++ softwareproperties/SoftwareProperties.py 2012-05-11 09:20:22 +0000
271@@ -491,7 +491,7 @@
272 for c in source.comps:
273 contents += " %s" % c
274 if source.type in ("deb-src", "rpm-src"):
275- contents += " %s" % _("(Source Code)")
276+ contents += " (%s)" % _("Source Code")
277 return contents
278 else:
279 # try to make use of a corresponding template
280@@ -697,6 +697,8 @@
281 deb_line = self.expand_http_line(deb_line)
282 debsrc_line = 'deb-src' + deb_line[3:]
283 new_deb_entry = SourceEntry(deb_line, file)
284+ if new_deb_entry.comment:
285+ debsrc_line += " (%s)" % _("Source Code")
286 new_debsrc_entry = SourceEntry(debsrc_line, file)
287 if new_deb_entry.invalid or new_debsrc_entry.invalid:
288 return False
289@@ -719,8 +721,8 @@
290 if worker:
291 # wait for GPG key to be downloaded
292 worker.join(30)
293- return True
294-
295+ return worker.success
296+
297 def remove_source(self, source):
298 """Remove the given source"""
299 # first find the source object if we got a string
300
301=== modified file 'softwareproperties/gtk/SoftwarePropertiesGtk.py'
302--- softwareproperties/gtk/SoftwarePropertiesGtk.py 2012-03-07 09:13:48 +0000
303+++ softwareproperties/gtk/SoftwarePropertiesGtk.py 2012-05-11 09:20:22 +0000
304@@ -572,6 +572,7 @@
305 GObject.TYPE_PYOBJECT,
306 GObject.TYPE_BOOLEAN,
307 GObject.TYPE_BOOLEAN)
308+ self.source_store.set_sort_column_id(STORE_DESCRIPTION, Gtk.SortType.ASCENDING)
309 self.treeview_sources.set_model(self.source_store)
310 self.treeview_sources.set_row_separator_func(self.is_separator,
311 STORE_SEPARATOR)
312@@ -582,6 +583,7 @@
313 col_desc = Gtk.TreeViewColumn(_("Software Sources"), cell_desc,
314 markup=COLUMN_DESC)
315 col_desc.set_max_width(1000)
316+ col_desc.set_sort_column_id(STORE_DESCRIPTION)
317
318 cell_toggle = Gtk.CellRendererToggle()
319 cell_toggle.set_property("xpad", 2)
320
321=== modified file 'softwareproperties/ppa.py'
322--- softwareproperties/ppa.py 2012-01-31 15:51:15 +0000
323+++ softwareproperties/ppa.py 2012-05-11 09:20:22 +0000
324@@ -25,10 +25,11 @@
325 import subprocess
326 from threading import Thread
327 import pycurl
328+from httplib import OK as HTTP_200_OK
329
330 DEFAULT_KEYSERVER = "hkp://keyserver.ubuntu.com:80/"
331 # maintained until 2015
332-LAUNCHPAD_PPA_API = 'https://launchpad.net/api/1.0/~%s/+archive/%s'
333+LAUNCHPAD_PPA_API = 'https://api.launchpad.net/1.0/~%s/+archive/%s'
334 # None means use pycurl default
335 LAUNCHPAD_PPA_CERT = None
336
337@@ -45,13 +46,18 @@
338 # via some sort of API, see LP #385129)
339 abrev = abrev.split(":")[1]
340 ppa_owner = abrev.split("/")[0]
341+ comment = None
342+ if abrev.find("#") != -1:
343+ comment = abrev.split("#")[1]
344 try:
345- ppa_name = abrev.split("/")[1]
346+ ppa_name = abrev.split("/")[1].split("#")[0]
347 except IndexError, e:
348 ppa_name = "ppa"
349 sourceslistd = apt_pkg.config.find_dir("Dir::Etc::sourceparts")
350 line = "deb http://ppa.launchpad.net/%s/%s/ubuntu %s main" % (
351 ppa_owner, ppa_name, distro_codename)
352+ if comment:
353+ line += "#" + comment
354 filename = "%s/%s-%s-%s.list" % (
355 sourceslistd, encode(ppa_owner), encode(ppa_name), distro_codename)
356 return (line, filename)
357@@ -78,9 +84,12 @@
358 curl.setopt(pycurl.URL, str(lp_url))
359 curl.setopt(pycurl.HTTPHEADER, ["Accept: application/json"])
360 curl.perform()
361+ return_code = curl.getinfo(pycurl.HTTP_CODE);
362 curl.close()
363- lp_page = callback.contents
364- return json.loads(lp_page)
365+ if return_code == HTTP_200_OK:
366+ lp_page = callback.contents
367+ return json.loads(lp_page)
368+ return None
369
370 class AddPPASigningKeyThread(Thread):
371 " thread class for adding the signing key in the background "
372@@ -88,11 +97,12 @@
373 def __init__(self, ppa_path, keyserver=None):
374 Thread.__init__(self)
375 self.ppa_path = ppa_path
376+ self.success = None
377 self.keyserver = (keyserver if keyserver is not None
378 else DEFAULT_KEYSERVER)
379
380 def run(self):
381- self.add_ppa_signing_key(self.ppa_path)
382+ self.success = self.add_ppa_signing_key(self.ppa_path)
383
384 def add_ppa_signing_key(self, ppa_path):
385 """Query and add the corresponding PPA signing key.
386@@ -112,9 +122,11 @@
387 print "Error: can't find signing_key_fingerprint at %s" % lp_url
388 return False
389 res = subprocess.call(
390- ["apt-key", "adv",
391+ ["apt-key", "adv",
392 "--keyserver", self.keyserver,
393 "--recv", signing_key_fingerprint])
394+ if res != 0:
395+ print "Error: can't retrieve key file for the PPA's signing_key_fingerprint"
396 return (res == 0)
397
398
399
400=== added file 'tests/test_aptaddrepo.py'
401--- tests/test_aptaddrepo.py 1970-01-01 00:00:00 +0000
402+++ tests/test_aptaddrepo.py 2012-05-11 09:20:22 +0000
403@@ -0,0 +1,36 @@
404+#!/usr/bin/python
405+
406+import pycurl
407+import unittest
408+import sys
409+from subprocess import call
410+sys.path.insert(0, "..")
411+
412+class TestAptAddRepo(unittest.TestCase):
413+
414+ # FIXME: we need to add a --datadir argument for add-apt-repository
415+ # so that we can run this test as non-root
416+
417+ def test_adding_invalid_ppa(self):
418+ resultcode = call(["../add-apt-repository",
419+ "-a" , "./aptroot",
420+ "ppa:mvo/inexistent-testing-ppa"])
421+ self.assertNotEquals(resultcode, 0)
422+
423+ def test_ppa_add(self):
424+ resultcode = call(["../add-apt-repository",
425+ "-y",
426+ "-a" , "./aptroot",
427+ "ppa:mvo/ppa"])
428+ self.assertEquals(resultcode, 0)
429+
430+ def test_ppa_remove(self):
431+ resultcode = call(["../add-apt-repository",
432+ "-y", "-r",
433+ "-a" , "./aptroot",
434+ "ppa:mvo/ppa"])
435+ self.assertEquals(resultcode, 0)
436+
437+
438+if __name__ == "__main__":
439+ unittest.main()
440
441=== modified file 'tests/test_lp.py'
442--- tests/test_lp.py 2012-01-31 15:51:15 +0000
443+++ tests/test_lp.py 2012-05-11 09:20:22 +0000
444@@ -19,7 +19,10 @@
445 self.assertRaises(
446 pycurl.error, softwareproperties.ppa.get_ppa_info_from_lp, "mvo", "ppa")
447
448-
449+ def test_invalid_ppa(self):
450+ # retrieve info for an inexistent PPA which will return anything else than HTTP 200 OK
451+ info = softwareproperties.ppa.get_ppa_info_from_lp("evfool", "inexistent-testing-ppa")
452+ self.assertEquals(info, None)
453
454
455 if __name__ == "__main__":

Subscribers

People subscribed via source and target branches

to status/vote changes: