Merge lp:~barry/computer-janitor/uicleanup into lp:computer-janitor

Proposed by Barry Warsaw on 2010-03-10
Status: Merged
Merged at revision: not available
Proposed branch: lp:~barry/computer-janitor/uicleanup
Merge into: lp:computer-janitor
Diff against target: 5609 lines (+3346/-1851)
45 files modified
.bzrignore (+5/-0)
computer-janitor (+12/-20)
computer-janitor-gtk (+18/-9)
computerjanitorapp/__init__.py (+20/-24)
computerjanitorapp/app.py (+0/-192)
computerjanitorapp/app_tests.py (+0/-178)
computerjanitorapp/areyousure.py (+76/-0)
computerjanitorapp/maincli.py (+290/-163)
computerjanitorapp/maingtk.py (+32/-0)
computerjanitorapp/state.py (+0/-56)
computerjanitorapp/state_tests.py (+0/-77)
computerjanitorapp/store.py (+147/-0)
computerjanitorapp/terminalsize.py (+24/-29)
computerjanitorapp/tests/test_all.py (+29/-0)
computerjanitorapp/tests/test_terminalsize.py (+24/-9)
computerjanitorapp/tests/test_utilities.py (+71/-0)
computerjanitorapp/ui.py (+0/-39)
computerjanitorapp/ui_cli_tests.py (+0/-246)
computerjanitorapp/ui_gtk.py (+0/-714)
computerjanitorapp/ui_tests.py (+0/-36)
computerjanitorapp/uigtk.py (+515/-0)
computerjanitorapp/utilities.py (+51/-0)
computerjanitord/application.py (+111/-0)
computerjanitord/authenticator.py (+85/-0)
computerjanitord/collector.py (+149/-0)
computerjanitord/data/com.ubuntu.ComputerJanitor.conf (+15/-0)
computerjanitord/data/com.ubuntu.computerjanitor.policy (+19/-0)
computerjanitord/errors.py (+57/-0)
computerjanitord/main.py (+97/-0)
computerjanitord/service.py (+260/-0)
computerjanitord/state.py (+92/-0)
computerjanitord/tests/data/etc/apt/sources.list (+1/-0)
computerjanitord/tests/data/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_intrepid_restricted_binary-i386_Packages (+54/-0)
computerjanitord/tests/data/var/lib/dpkg/status (+28/-0)
computerjanitord/tests/test_all.py (+41/-0)
computerjanitord/tests/test_application.py (+106/-0)
computerjanitord/tests/test_authenticator.py (+97/-0)
computerjanitord/tests/test_collector.py (+187/-0)
computerjanitord/tests/test_state.py (+141/-0)
computerjanitord/tests/test_whitelist.py (+119/-0)
computerjanitord/whitelist.py (+86/-0)
data/ComputerJanitor.ui (+10/-56)
janitord (+40/-0)
po/computerjanitor.pot (+233/-0)
setup.py (+4/-3)
To merge this branch: bzr merge lp:~barry/computer-janitor/uicleanup
Reviewer Review Type Date Requested Status
Ubuntu Release Team 2010-03-10 Pending
computer-janitor-hackers 2010-03-10 Pending
Review via email: mp+21086@code.launchpad.net

Description of the change

This removes the "Recommended" column, since it is never filled. It also does a few minor other ui tweaks such as move the progress bar and shrink the default size of the window.

To post a comment you must log in.
Barry Warsaw (barry) wrote :

This will need a ui exception to make it into Lucid beta 1, but I think it's worth it.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file '.bzrignore'
2--- .bzrignore 1970-01-01 00:00:00 +0000
3+++ .bzrignore 2010-03-10 21:40:30 +0000
4@@ -0,0 +1,5 @@
5+build
6+computer_janitor.egg-info
7+computerjanitord/tests/data/var/cache
8+computerjanitord/tests/data/var/lib/dpkg
9+computerjanitord/tests/data/var/lib/apt/lists/partial
10
11=== modified file 'computer-janitor'
12--- computer-janitor 2009-08-18 13:48:50 +0000
13+++ computer-janitor 2010-03-10 21:40:30 +0000
14@@ -1,7 +1,6 @@
15 #!/usr/bin/python
16 #
17-# computer-janitor - clean up a Unix-like operating system
18-# Copyright (C) 2008, 2009 Canonical, Ltd.
19+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
20 #
21 # This program is free software: you can redistribute it and/or modify
22 # it under the terms of the GNU General Public License as published by
23@@ -15,21 +14,14 @@
24 # You should have received a copy of the GNU General Public License
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26
27-
28-import logging
29-import os
30-import sys
31-import traceback
32-
33-import computerjanitor
34-import computerjanitorapp
35-
36-
37-try:
38- app = computerjanitorapp.Application()
39- app.run(computerjanitorapp.CommandLineUserInterface)
40-except computerjanitor.Exception, e:
41- logging.debug(unicode(traceback.format_exc()))
42- logging.error(e._str)
43- sys.exit(1)
44-
45+"""Start the command-line computer janitor."""
46+
47+from __future__ import absolute_import, unicode_literals
48+
49+__metaclass__ = type
50+__all__ = [
51+ ]
52+
53+
54+from computerjanitorapp.maincli import main
55+main()
56
57=== modified file 'computer-janitor-gtk'
58--- computer-janitor-gtk 2009-02-11 17:21:24 +0000
59+++ computer-janitor-gtk 2010-03-10 21:40:30 +0000
60@@ -1,7 +1,7 @@
61 #!/usr/bin/python
62 #
63 # computer-janitor-gtk - clean up a Unix-like operating system (GTK version)
64-# Copyright (C) 2008, 2009 Canonical, Ltd.
65+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
66 #
67 # This program is free software: you can redistribute it and/or modify
68 # it under the terms of the GNU General Public License as published by
69@@ -15,20 +15,29 @@
70 # You should have received a copy of the GNU General Public License
71 # along with this program. If not, see <http://www.gnu.org/licenses/>.
72
73-
74-import logging
75+"""Start the gtk computer janitor."""
76+
77+from __future__ import absolute_import, unicode_literals
78+
79+__metaclass__ = type
80+__all__ = [
81+ ]
82+
83+
84 import os
85 import sys
86+import logging
87 import traceback
88-
89 import computerjanitor
90-import computerjanitorapp
91+
92+from computerjanitorapp.maingtk import main
93
94
95 try:
96- app = computerjanitorapp.Application()
97- app.run(computerjanitorapp.GtkUserInterface)
98-except computerjanitor.Exception, e:
99+ main()
100+except KeyboardInterrupt:
101+ pass
102+except computerjanitor.Exception as error:
103 logging.debug(unicode(traceback.format_exc()))
104- logging.error(unicode(e))
105+ logging.error(unicode(error))
106 sys.exit(1)
107
108=== modified file 'computerjanitorapp/__init__.py'
109--- computerjanitorapp/__init__.py 2009-09-09 14:36:30 +0000
110+++ computerjanitorapp/__init__.py 2010-03-10 21:40:30 +0000
111@@ -1,5 +1,4 @@
112-# __init__.py for computerjanitorapp
113-# Copyright (C) 2008, 2009 Canonical, Ltd.
114+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
115 #
116 # This program is free software: you can redistribute it and/or modify
117 # it under the terms of the GNU General Public License as published by
118@@ -13,24 +12,29 @@
119 # You should have received a copy of the GNU General Public License
120 # along with this program. If not, see <http://www.gnu.org/licenses/>.
121
122-
123-VERSION = "1.13.3"
124-
125-
126-# Set up gettext. This needs to be before the import statements below
127-# so that if any modules call it right after importing, they find
128-# setup_gettext.
129-
130+"""computerjanitorapp module."""
131+
132+from __future__ import absolute_import, unicode_literals
133+
134+__metaclass__ = type
135+__all__ = [
136+ '__version__',
137+ 'setup_gettext',
138+ ]
139+
140+
141+__version__ = '2.0'
142+
143+
144+# Set up gettext.
145 def setup_gettext():
146 """Set up gettext for a module.
147
148- Return a method to be used for looking up translations. Usage:
149-
150- import computerjanitorapp
151- _ = computerjanitorapp.setup_gettext()
152-
153+ Return a method to be used for looking up translations. Usage:
154+
155+ >>> import computerjanitorapp
156+ >>> _ = computerjanitorapp.setup_gettext()
157 """
158-
159 import gettext
160 import os
161
162@@ -38,11 +42,3 @@
163 localedir = os.environ.get('LOCPATH', None)
164 t = gettext.translation(domain, localedir=localedir, fallback=True)
165 return t.ugettext
166-
167-
168-from app import Application
169-from ui import UserInterface
170-from ui_cli import CommandLineUserInterface
171-from ui_gtk import GtkUserInterface
172-from state import State
173-from terminalsize import get_terminal_size
174
175=== removed file 'computerjanitorapp/app.py'
176--- computerjanitorapp/app.py 2009-11-10 20:01:34 +0000
177+++ computerjanitorapp/app.py 1970-01-01 00:00:00 +0000
178@@ -1,192 +0,0 @@
179-# app.py - the application main program of Computer Janitor
180-# Copyright (C) 2008, 2009 Canonical, Ltd.
181-#
182-# This program is free software: you can redistribute it and/or modify
183-# it under the terms of the GNU General Public License as published by
184-# the Free Software Foundation, version 3 of the License.
185-#
186-# This program is distributed in the hope that it will be useful,
187-# but WITHOUT ANY WARRANTY; without even the implied warranty of
188-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
189-# GNU General Public License for more details.
190-#
191-# You should have received a copy of the GNU General Public License
192-# along with this program. If not, see <http://www.gnu.org/licenses/>.
193-
194-
195-import ConfigParser
196-import logging
197-import logging.handlers
198-import optparse
199-import os
200-import sys
201-
202-import computerjanitor
203-import computerjanitorapp
204-_ = computerjanitorapp.setup_gettext()
205-
206-
207-DEFAULT_PLUGINS_DIRS = "/usr/share/computerjanitor/plugins"
208-DEFAULT_STATE_FILE = "/var/lib/computer-janitor/state.dat"
209-
210-
211-class SourcesListProblem(computerjanitor.Exception):
212-
213- def __init__(self, missing_package):
214- self._str = _("Essential package %s is missing. There may be "
215- "problems with apt sources.list or Packages files may "
216- "be missing?") % missing_package
217-
218-
219-class AsciiFormatter(logging.Formatter):
220-
221- def format(self, record): # pragma: no cover
222- msg = logging.Formatter.format(self, record)
223- return msg.encode('ascii', 'replace')
224-
225-
226-def setup_logging(): # pragma: no cover
227- if os.environ.get("COMPUTER_JANITOR_DEBUG", None) == "yes":
228- level = logging.DEBUG
229- elif os.environ.get("COMPUTER_JANITOR_UNITTEST", None) == "yes":
230- level = logging.CRITICAL
231- else:
232- level = logging.INFO
233- logging.basicConfig(level=level, format="%(levelname)s: %(message)s")
234-
235- try:
236- formatter = AsciiFormatter(
237- "computer-janitor %(levelname)s: %(message)s")
238- handler = logging.handlers.SysLogHandler("/dev/log")
239- handler.setLevel(level)
240- handler.setFormatter(formatter)
241- logging.getLogger().addHandler(handler)
242- except:
243- # We're OK with something going wrong.
244- logging.debug(_("Logging to syslog cannot be set up."))
245-
246-
247-class Application(object):
248-
249- """The main class for the program."""
250-
251- whitelist_dirs = ["/etc/computer-janitor.d"]
252-
253- def __init__(self, apt=None):
254- self.state = computerjanitorapp.State()
255- self.parser = self.create_option_parser()
256- self.apt = computerjanitor.apt
257- if apt is not None:
258- self.apt = apt
259- self.refresh_apt_cache()
260-
261- # The Plugin API requires that we have a config that is a
262- # ConfigParser. We don't use it for anything, for now.
263- self.config = ConfigParser.ConfigParser()
264-
265- def run(self, ui_class,
266- plugin_manager_class=computerjanitor.PluginManager):
267- setup_logging()
268- logging.debug(_("Running application, with:"))
269- logging.debug(" plugin_manager_class=%s" % plugin_manager_class)
270-
271- options, args = self.parse_options()
272-
273- pluginpath = os.environ.get("COMPUTER_JANITOR_PLUGINS",
274- DEFAULT_PLUGINS_DIRS)
275- plugindirs = pluginpath.split(":")
276- pm = plugin_manager_class(self, plugindirs)
277-
278- ui = ui_class(self, pm)
279- ui.run(options, args)
280-
281- def create_option_parser(self): # pragma: no cover
282- parser = optparse.OptionParser(version="%%prog %s" %
283- computerjanitorapp.VERSION)
284-
285- parser.usage = _("""
286-%prog [options] find
287-%prog [options] cleanup [CRUFT]...
288-%prog [options] ignore [CRUFT]...
289-%prog [options] unignore [CRUFT]...
290-
291-%prog finds and removes cruft from your system.
292-
293-Cruft is anything that shouldn't be on the system, but is. Stretching
294-the definition, it is also things that should be on the system, but
295-aren't.""")
296-
297- parser.add_option("--all", action="store_true",
298- help=_("Make the 'cleanup' command remove all "
299- "packages, if none are given on the "
300- "command line."))
301-
302- parser.add_option("--state-file", metavar="FILE",
303- default=DEFAULT_STATE_FILE,
304- help=_("Store state of each piece of cruft in "
305- "FILE. (Default is %default)."))
306-
307- parser.add_option("--no-act", action="store_true",
308- help=_("Don't actually remove anything, just "
309- "pretend to do so. This is useful for "
310- "testing stuff."))
311-
312- parser.add_option("--verbose", action="store_true",
313- help=_("Verbose operation: make find show an "
314- "explanation for each piece of cruft "
315- "found."))
316-
317- return parser
318-
319- def parse_options(self, args=None):
320- return self.parser.parse_args(args=args)
321-
322- def refresh_apt_cache(self):
323- self.apt_cache = self.apt.Cache()
324- self.apt_cache._depcache.ReadPinFile("/var/lib/synaptic/preferences")
325-
326- def verify_apt_cache(self):
327- for name in ["dash", "gzip"]:
328- if name not in self.apt_cache:
329- raise SourcesListProblem(name)
330- if not any(v.downloadable for v in self.apt_cache[name].versions):
331- raise SourcesListProblem(name)
332-
333- def whitelist_files(self, dirnames):
334- """Find files with whitelists in a list of directories.
335-
336- The directory is scanned for files ending in ".whitelist".
337- Subdirectories are not scanned.
338-
339- """
340-
341- list = []
342- for dirname in dirnames:
343- try:
344- basenames = os.listdir(dirname)
345- whitelists = [x for x in basenames if x.endswith(".whitelist")]
346- list += [os.path.join(dirname, x) for x in whitelists]
347- except os.error:
348- pass
349-
350- return list
351-
352- def whitelisted_cruft(self, dirnames=None):
353- """Return list of cruft that is whitelisted."""
354-
355- dirnames = dirnames or self.whitelist_dirs
356-
357- list = []
358- for filename in self.whitelist_files(dirnames):
359- f = file(filename, "r")
360- for line in f.readlines():
361- line = line.strip()
362- if line and not line.startswith("#"):
363- list.append(line)
364- f.close()
365-
366- return list
367-
368- def remove_whitelisted(self, crufts, dirnames=None):
369- whitelisted = self.whitelisted_cruft(dirnames=dirnames)
370- return [c for c in crufts if c.get_name() not in whitelisted]
371
372=== removed file 'computerjanitorapp/app_tests.py'
373--- computerjanitorapp/app_tests.py 2009-08-14 12:37:25 +0000
374+++ computerjanitorapp/app_tests.py 1970-01-01 00:00:00 +0000
375@@ -1,178 +0,0 @@
376-# app_tests.py - unit tests for app.py
377-# Copyright (C) 2008, 2009 Canonical, Ltd.
378-#
379-# This program is free software: you can redistribute it and/or modify
380-# it under the terms of the GNU General Public License as published by
381-# the Free Software Foundation, version 3 of the License.
382-#
383-# This program is distributed in the hope that it will be useful,
384-# but WITHOUT ANY WARRANTY; without even the implied warranty of
385-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
386-# GNU General Public License for more details.
387-#
388-# You should have received a copy of the GNU General Public License
389-# along with this program. If not, see <http://www.gnu.org/licenses/>.
390-
391-
392-import os
393-import shutil
394-import tempfile
395-import unittest
396-
397-import computerjanitor
398-import computerjanitorapp
399-
400-
401-class MockUI(object):
402-
403- def __init__(self, testcase, app, pm):
404- self.testcase = testcase
405-
406- def run(self, options, args):
407- self.testcase.ui_ran = True
408-
409-
410-class MockPackage(object):
411-
412- def __init__(self, downloadable=False):
413- self.downloadable = downloadable
414- self.candidate = self
415-
416-
417-class MockApt(dict):
418-
419- def __init__(self):
420- self._depcache = self
421-
422- def ReadPinFile(self, filename):
423- pass
424-
425- def Cache(self):
426- return self
427-
428-
429-class MockCruft(object):
430-
431- def __init__(self, name):
432- self.name = name
433-
434- def get_name(self):
435- return self.name
436-
437-
438-class ApplicationTests(unittest.TestCase):
439-
440- def setUp(self):
441- self.mock_apt = MockApt()
442- self.app = computerjanitorapp.Application(apt=self.mock_apt)
443- self.app.apt_cache["dash"] = MockPackage(downloadable=True)
444- self.app.apt_cache["gzip"] = MockPackage(downloadable=True)
445-
446- def testSetsUpAptAttributeCorrectly(self):
447- self.assertEqual(self.app.apt, self.mock_apt)
448-
449- def testSetsUpAndReturnsState(self):
450- self.assert_(self.app.state)
451-
452- def testSetsUpAptCacheWhenRequested(self):
453- self.assertNotEqual(self.app.apt_cache, None)
454-
455- def testSetsOptionDefaultsCorrectly(self):
456- options, args = self.app.parse_options(args=[])
457- self.assertEqual(args, [])
458- self.assertEqual(options.all, None)
459- self.assertEqual(options.state_file,
460- "/var/lib/computer-janitor/state.dat")
461- self.assertEqual(options.no_act, None)
462-
463- def testAcceptsDashDashAllOption(self):
464- options, args = self.app.parse_options(args=["--all"])
465- self.assertEqual(options.all, True)
466-
467- def testAcceptsDashDashStateFileOption(self):
468- options, args = self.app.parse_options(args=["--state-file=foo"])
469- self.assertEqual(options.state_file, "foo")
470-
471- def testAcceptsDashDashNoActOption(self):
472- options, args = self.app.parse_options(args=["--no-act"])
473- self.assertEqual(options.no_act, True)
474-
475- def testRunsUserInterface(self):
476-
477- def pm_class(app, plugin_dirs):
478- self.pm_ran = True
479-
480- def ui_class(app, pm):
481- return MockUI(self, app, pm)
482-
483- self.pm_ran = False
484- self.ui_ran = False
485- self.app.run(ui_class=ui_class, plugin_manager_class=pm_class)
486- self.assert_(self.ui_ran)
487-
488- def testAcceptsAptCacheWhenEssentialPackagesAreThere(self):
489- self.assertEqual(self.app.verify_apt_cache(), None)
490-
491- def testRejectsAptCacheWhenDashIsMissing(self):
492- del self.app.apt_cache["dash"]
493- self.assertRaises(computerjanitor.Exception,
494- self.app.verify_apt_cache)
495-
496- def testRejectsAptCacheWhenGzipIsMissing(self):
497- del self.app.apt_cache["gzip"]
498- self.assertRaises(computerjanitor.Exception,
499- self.app.verify_apt_cache)
500-
501- def testRejectsAptCacheWhenDashIsNotDownloadable(self):
502- self.app.apt_cache["dash"].candidate.downloadable = False
503- self.assertRaises(computerjanitor.Exception,
504- self.app.verify_apt_cache)
505-
506- def testRejectsAptCacheWhenGzipIsNotDownloadable(self):
507- self.app.apt_cache["dash"].candidate.downloadable = False
508- self.assertRaises(computerjanitor.Exception,
509- self.app.verify_apt_cache)
510-
511- def testSetsDefaultListOfWhitelistDirectoriesCorrectly(self):
512- self.assert_("/etc/computer-janitor.d" in self.app.whitelist_dirs)
513-
514- def testReturnsEmptyWhitelistByDefault(self):
515- dirname = tempfile.mkdtemp()
516- whitelist = self.app.whitelisted_cruft(dirnames=[dirname])
517- shutil.rmtree(dirname)
518- self.assertEqual(whitelist, [])
519-
520- def testDoesNotMindNonExistentWhitelistDirectory(self):
521- dirname = tempfile.mkdtemp()
522- subdir = os.path.join(dirname, "foo")
523- whitelist = self.app.whitelisted_cruft(dirnames=[subdir])
524- shutil.rmtree(dirname)
525- self.assertEqual(whitelist, [])
526-
527- def testReadsWhitelistFilesCorrectly(self):
528- dirname = tempfile.mkdtemp()
529- temp1 = os.path.join(dirname, "foo.whitelist")
530- temp2 = os.path.join(dirname, "foo.whitelist~")
531-
532- file(temp1, "w").write("deb:foo\n")
533- file(temp2, "w").write("deb:bar\n")
534-
535- whitelist = self.app.whitelisted_cruft(dirnames=[dirname])
536- shutil.rmtree(dirname)
537- self.assertEqual(whitelist, ["deb:foo"])
538-
539- def testFindsCorrectWhitelistFilesInDotDDirectory(self):
540- dirname = tempfile.mkdtemp()
541- file(os.path.join(dirname, "foo.whitelist"), "w").write("deb:foo\n")
542- file(os.path.join(dirname, "foo.whitelist~"), "w").write("deb:bar\n")
543- list = self.app.whitelist_files([dirname])
544- shutil.rmtree(dirname)
545- self.assertEqual(list, [os.path.join(dirname, "foo.whitelist")])
546-
547- def testRemovesWhitelistedCruftCorrectly(self):
548- crufts = [MockCruft("deb:foo"), MockCruft("deb:bar")]
549- dirname = tempfile.mkdtemp()
550- file(os.path.join(dirname, "foo.whitelist"), "w").write("deb:foo\n")
551- crufts2 = self.app.remove_whitelisted(crufts, dirnames=[dirname])
552- shutil.rmtree(dirname)
553- self.assertEqual(crufts2, crufts[1:])
554
555=== added file 'computerjanitorapp/areyousure.py'
556--- computerjanitorapp/areyousure.py 1970-01-01 00:00:00 +0000
557+++ computerjanitorapp/areyousure.py 2010-03-10 21:40:30 +0000
558@@ -0,0 +1,76 @@
559+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
560+#
561+# This program is free software: you can redistribute it and/or modify
562+# it under the terms of the GNU General Public License as published by
563+# the Free Software Foundation, version 3 of the License.
564+#
565+# This program is distributed in the hope that it will be useful,
566+# but WITHOUT ANY WARRANTY; without even the implied warranty of
567+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
568+# GNU General Public License for more details.
569+#
570+# You should have received a copy of the GNU General Public License
571+# along with this program. If not, see <http://www.gnu.org/licenses/>.
572+
573+"""Confirm some actions."""
574+
575+from __future__ import absolute_import, unicode_literals
576+
577+__metaclass__ = type
578+__all__ = [
579+ 'AreYouSure',
580+ ]
581+
582+
583+import gtk
584+
585+from computerjanitorapp import setup_gettext
586+from computerjanitorapp.store import ListStoreColumns
587+
588+_ = setup_gettext()
589+NL = '\n'
590+
591+
592+class AreYouSure:
593+ """Confirmation of destructive actions."""
594+
595+ def __init__(self, ui):
596+ self._ui = ui
597+
598+ def verify(self):
599+ """Confirm package removal."""
600+ # Start by getting all the active, non-ignored cruft. These are the
601+ # candidates for removal.
602+ cleanable_cruft = self._ui.get_cleanable_cruft()
603+ # It would be nice if we could produce better messages than these, but
604+ # that would require a richer interface to the dbus service, and
605+ # probably to the cruft plugin architecture underneath that.
606+ message = _('Are you sure you want to clean your system?')
607+ dialog = gtk.MessageDialog(
608+ parent=self._ui.widgets['window'],
609+ type=gtk.MESSAGE_WARNING,
610+ buttons=gtk.BUTTONS_NONE,
611+ message_format=message)
612+ dialog.set_title(_('Clean up'))
613+ package_cruft_count = sum(1 for cruft_name, is_pkg in cleanable_cruft
614+ if is_pkg)
615+ other_cruft_count = len(cleanable_cruft) - package_cruft_count
616+ messages = []
617+ if package_cruft_count > 0:
618+ messages = [_('<b>Software packages to remove: {packages}</b>.'),
619+ _('\nRemoving packages that are still in use can '
620+ 'cause errors.')]
621+ ok_button = _('Remove packages')
622+ if other_cruft_count > 0:
623+ messages.insert(1, _('Non-package items to remove: {others}.'))
624+ ok_button = _('Clean up')
625+ message = NL.join(messages).format(packages=package_cruft_count,
626+ others=other_cruft_count)
627+ dialog.format_secondary_markup(message)
628+ dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CLOSE)
629+ dialog.add_button(ok_button, gtk.RESPONSE_YES)
630+ # Show the dialog and get the user's response.
631+ dialog.show_all()
632+ response = dialog.run()
633+ dialog.hide()
634+ return response == gtk.RESPONSE_YES
635
636=== renamed file 'computerjanitorapp/ui_cli.py' => 'computerjanitorapp/maincli.py'
637--- computerjanitorapp/ui_cli.py 2009-08-18 14:10:42 +0000
638+++ computerjanitorapp/maincli.py 2010-03-10 21:40:30 +0000
639@@ -1,5 +1,4 @@
640-# ui_cli.py - command line user interface
641-# Copyright (C) 2008, 2009 Canonical, Ltd.
642+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
643 #
644 # This program is free software: you can redistribute it and/or modify
645 # it under the terms of the GNU General Public License as published by
646@@ -13,168 +12,296 @@
647 # You should have received a copy of the GNU General Public License
648 # along with this program. If not, see <http://www.gnu.org/licenses/>.
649
650-
651-import os
652-import logging
653+"""Command line user interface."""
654+
655+from __future__ import absolute_import, unicode_literals
656+
657+__metaclass__ = type
658+__all__ = [
659+ 'main',
660+ ]
661+
662+
663 import sys
664+import dbus
665+import gobject
666+import argparse
667 import textwrap
668-import traceback
669-
670-import computerjanitor
671-import computerjanitorapp
672-_ = computerjanitorapp.setup_gettext()
673-
674-
675-class UnknownCommand(computerjanitor.Exception):
676-
677- def __init__(self, name):
678- self._str = _("Unknown command: %s") % name
679-
680-
681-class UnknownCruft(computerjanitor.Exception):
682-
683- def __init__(self, name):
684- self._str = _("Unknown cruft: %s") % name
685-
686-
687-class MustBeRoot(computerjanitor.Exception):
688+
689+from dbus.mainloop.glib import DBusGMainLoop
690+
691+from computerjanitorapp import __version__, setup_gettext
692+from computerjanitorapp.terminalsize import get_terminal_size
693+from computerjanitorapp.utilities import format_size
694+from computerjanitord.service import DBUS_INTERFACE_NAME
695+_ = setup_gettext()
696+
697+
698+class Options:
699+ """Command line option parser."""
700+ def __init__(self, runner):
701+ """Parse the command line options.
702+
703+ :param runner: The class implementing the sub-commands.
704+ :type runner: `Runner`
705+ """
706+ self.runner = runner
707+ # Parse the arguments.
708+ self.parser = argparse.ArgumentParser(
709+ description=_("""\
710+ Find and remove cruft from your system.
711+
712+ Cruft is anything that shouldn't be on your system, but is.
713+ Stretching the definition, cruft is also things that should be on
714+ your system but aren't."""),
715+ version=__version__)
716+ subparser = self.parser.add_subparsers(title='Commands')
717+ # The 'find' subcommand.
718+ command = subparser.add_parser(
719+ 'find', help=_('Find and display all cruft found on your system.'))
720+ command.add_argument(
721+ '-v', '--verbose', action='store_true',
722+ help=_("""\
723+ Display a detailed explanation for each piece of cruft found."""))
724+ command.add_argument(
725+ '-i', '--ignored', action='store_true',
726+ help=_('Find and display only the ignored cruft.'))
727+ command.add_argument(
728+ '-r', '--removable', action='store_true',
729+ help=_('Find and display only the removable cruft.'))
730+ command.add_argument(
731+ '-s', '--short', action='store_true',
732+ help=_('Display only the package names. Do not use with -v.'))
733+ command.set_defaults(func=self.runner.find)
734+ # The 'ignore' subcommand.
735+ command = subparser.add_parser(
736+ 'ignore', help=_("""\
737+ Ignore a piece of cruft so that it is not cleaned up."""))
738+ command.add_argument(
739+ 'cruft', nargs=1,
740+ help=_('The name of the cruft to ignore.'))
741+ command.set_defaults(func=self.runner.ignore)
742+ # The 'unignore' subcommand.
743+ command = subparser.add_parser(
744+ 'unignore', help=_("""\
745+ Unignore a piece of cruft so that it will be cleaned up."""))
746+ command.add_argument(
747+ 'cruft', nargs=1,
748+ help=_('The name of the cruft to unignore.'))
749+ command.set_defaults(func=self.runner.unignore)
750+ # The 'clean' subcommand.
751+ command = subparser.add_parser(
752+ 'clean',
753+ help=_('Remove the selected cruft from the system.'))
754+ command.add_argument(
755+ '-a', '--all', action='store_true',
756+ help=_('Clean up all unignored cruft.'))
757+ command.add_argument(
758+ '-v', '--verbose', action='store_true',
759+ help=_("Provide more details on what's being cleaned."))
760+ command.add_argument(
761+ 'cruft', nargs='?',
762+ help=_("""\
763+ The name of the cruft to clean up. Do not use if specifying
764+ --all."""))
765+ command.set_defaults(func=self.runner.clean)
766+ # Parse the arguments and execute the subcommand.
767+ self.arguments = self.parser.parse_args()
768+
769+
770+class Runner:
771+ """Implementations of subcommands."""
772
773 def __init__(self):
774- self._str = _("computer-janitor must be run as root, sorry.")
775-
776-
777-class CommandLineUserInterface(computerjanitorapp.UserInterface):
778-
779- def run(self, options, args):
780- if self.mustberoot and os.getuid() != 0:
781- raise MustBeRoot()
782-
783- self.app.verify_apt_cache()
784-
785- dict = {
786- "find": self.show_cruft,
787- "cleanup": self.cleanup,
788- "ignore": self.ignore,
789- "unignore": self.unignore,
790- "help": self.help,
791- }
792-
793- if args:
794- cmd = args[0]
795- args = args[1:]
796- else:
797- cmd = "help"
798- args = []
799-
800- if cmd in dict:
801- app = self.app
802- app.state.load(options.state_file)
803- try:
804- dict[cmd](options, args)
805- except Exception, e: # pragma: no cover
806- logging.debug(unicode(traceback.format_exc()))
807- logging.critical(unicode(e))
808- sys.exit(1)
809- else:
810- raise UnknownCommand(cmd)
811-
812- def find_cruft(self):
813- list = []
814- for plugin in self.pm.get_plugins():
815- for cruft in plugin.get_cruft():
816- list.append(cruft)
817- return self.app.remove_whitelisted(list)
818-
819- def show_one_cruft(self, name, desc, s, width): #pragma: no cover
820- if width is None:
821- max = len(name) + len(desc or '')
822- else:
823- max = width
824- max -= 9 # state column
825- max -= 2 # two spaces
826- max -= 1 # avoid the last column, some terminals word wrap there
827-
828- msg = ["%-9s %.*s" % (s, max, name)]
829- if desc:
830- paras = desc.split("\n\n")
831- for para in paras:
832- for line in textwrap.wrap(para, max):
833- msg.append("%9s %s" % ("", line))
834- msg.append("")
835- print ("\n".join(msg)).encode('utf-8') # FIXME: horrible kludge
836- # the above makes it possible to write out stuff even when
837- # it's going to somewhere like a pipe, because we explicitly
838- # encode it as utf-8 first. This ignores the user's desired
839- # charset, which is bad, bad, bad, but I can't figure out a
840- # way to make Python behave in a sensible way. *sigh*
841- # --liw, 2009-08-18
842-
843- def show_cruft(self, options, args):
844- list = []
845- maxname = ""
846- state = self.app.state
847- for cruft in self.find_cruft():
848- name = cruft.get_name()
849- if options.verbose:
850- desc = cruft.get_description() # pragma: no cover
851- else:
852- desc = None
853- if state.is_enabled(name):
854- s = _("removable")
855- else:
856- s = _("ignored")
857- list.append((name, desc, s))
858- if len(name) > len(maxname):
859- maxname = name
860-
861- rows, cols = computerjanitorapp.get_terminal_size()
862- for name, desc, s in sorted(list):
863- self.show_one_cruft(name, desc, s, cols)
864-
865- def cleanup(self, options, args):
866- crufts = {}
867- for cruft in self.find_cruft():
868- crufts[cruft.get_name()] = cruft
869-
870- if args:
871- for arg in args:
872- if arg not in crufts:
873- raise UnknownCruft(arg)
874- elif options.all:
875- state = self.app.state
876- args = []
877- for name in crufts.keys():
878- if state.is_enabled(name):
879- args.append(name)
880- else:
881- logging.info(_("Ignored: %s") % name)
882-
883- for arg in args:
884- if options.no_act:
885- logging.info(_("Pretending to remove cruft: %s") % arg)
886- else:
887- logging.info(_("Removing cruft: %s") % arg)
888- crufts[arg].cleanup()
889- for plugin in self.pm.get_plugins():
890- if options.no_act:
891- logging.info(_("Pretending to post-cleanup: %s") % plugin)
892- else:
893- logging.info(_("Post-cleanup: %s") % plugin)
894- plugin.post_cleanup()
895-
896- def ignore(self, options, cruft_names):
897- state = self.app.state
898- for cruft_name in cruft_names:
899- state.disable(cruft_name)
900- if not options.no_act:
901- state.save(options.state_file)
902-
903- def unignore(self, options, cruft_names):
904- state = self.app.state
905- for cruft_name in cruft_names:
906- state.enable(cruft_name)
907- if not options.no_act:
908- state.save(options.state_file)
909-
910- def help(self, options, args):
911- self.app.parser.print_help()
912+ # Connect to the dbus service.
913+ system_bus = dbus.SystemBus()
914+ proxy = system_bus.get_object(DBUS_INTERFACE_NAME, '/')
915+ self.janitord = dbus.Interface(
916+ proxy, dbus_interface=DBUS_INTERFACE_NAME)
917+ # Connect to the signal the server will emit when cleaning up.
918+ self.janitord.connect_to_signal('cleanup_status', self._clean_working)
919+ # This will get backpatched by __main__. We need it to produce error
920+ # messages from the argparser.
921+ self.options = None
922+ # The main loop for asynchronous calls and signal reception.
923+ self.loop = gobject.MainLoop()
924+
925+ def _error(self, message):
926+ """Generate a parser error and exit.
927+
928+ :param message: The error message.
929+ :type message: string
930+ """
931+ self.options.parser.error(message)
932+ # No return.
933+
934+ def find(self, arguments):
935+ """Find and display all cruft.
936+
937+ :param arguments: Command line options.
938+ """
939+ # Cruft will be prefixed by 'removable' if it is not being ignored.
940+ ignored = set(self.janitord.ignored())
941+ cruft_names = set(self.janitord.find())
942+ # Filter names based on option flags.
943+ if arguments.ignored:
944+ cruft = sorted(cruft_names & ignored)
945+ elif arguments.removable:
946+ cruft = sorted(cruft_names - ignored)
947+ else:
948+ cruft = sorted(cruft_names)
949+ # The prefix will either be 'ignored' or 'removable' however this
950+ # string will be translated, so calculate the prefix size in the
951+ # native language, then add two columns of separator, followed by the
952+ # cruft name.
953+ prefixi = _('ignored')
954+ prefixr = _('removable')
955+ prefix_width = max(len(prefixi), len(prefixr))
956+ # Long, short, shorter display.
957+ if arguments.verbose and arguments.short:
958+ self._error('Use either -s or -v but not both.')
959+ if arguments.short:
960+ # This is the shorter output.
961+ for name in cruft:
962+ print name
963+ elif not arguments.verbose:
964+ # This is the short output.
965+ for name in cruft:
966+ prefix = (prefixi if name in ignored else prefixr)
967+ # 10 spaces for the prefix
968+ print '{0:{1}} {2}'.format(prefix, prefix_width, name)
969+ else:
970+ # This is the verbose output. Start by getting the terminal's
971+ # size, though all we care about is the width.
972+ rows, columns = get_terminal_size()
973+ width = ((80 if columns in (None, 0) else columns)
974+ - prefix_width # space for prefix
975+ - 2 # separator
976+ - 1 # avoid last column, some terminals wrap
977+ )
978+ margin = ' ' * (prefix_width + 2)
979+ for name in cruft:
980+ prefix = (prefixi if name in ignored else prefixr)
981+ # 10 spaces for the prefix
982+ print '{0:{1}} {2}'.format(prefix, prefix_width, name)
983+ # Print some details about the cruft.
984+ cruft_type, disk_usage = self.janitord.get_details(name)
985+ print '{0}{1} of type {2}'.format(
986+ margin, format_size(disk_usage), cruft_type)
987+ # Get the description, wrap it to the available columns, and
988+ # display it line-by-line with the proper amount of leading
989+ # spaces (prefix_width + 2).
990+ description = self.janitord.get_description(name)
991+ if len(description) == 0:
992+ print
993+ continue
994+ paragraphs = description.split('\n\n')
995+ for paragraph in paragraphs:
996+ for line in textwrap.wrap(paragraph, width):
997+ # 2010-02-09 barry: The original code forced the
998+ # output to utf-8, claiming that was necessary to
999+ # "write out stuff even when it's going to somewhere
1000+ # like a pipe [...] ignor[ing] the user's desired
1001+ # charset, which is bad, bad, bad...". This makes me
1002+ # pretty uncomfortable, so I'd like to see a bug
1003+ # report before I copy that from the previous
1004+ # implementation.
1005+ print '{0}{1}'.format(margin, line)
1006+ # Paragraph separator.
1007+ print
1008+
1009+ def ignore(self, arguments):
1010+ """Ignore some cruft.
1011+
1012+ :param arguments: Command line options.
1013+ """
1014+ assert len(arguments.cruft) == 1, 'Unexpected arguments'
1015+ self.janitord.ignore(arguments.cruft[0])
1016+ self.janitord.save()
1017+
1018+ def unignore(self, arguments):
1019+ """Unignore some cruft.
1020+
1021+ :param arguments: Command line options.
1022+ """
1023+ assert len(arguments.cruft) == 1, 'Unexpected arguments'
1024+ self.janitord.unignore(arguments.cruft[0])
1025+ self.janitord.save()
1026+
1027+ def _clean_reply(self):
1028+ """The 'clean' operation has completed successfully."""
1029+ self.loop.quit()
1030+ print 'done.'
1031+
1032+ def _clean_error(self, exception):
1033+ """The 'clean' operation has failed."""
1034+ self.loop.quit()
1035+ print 'dbus service error:', exception
1036+
1037+ def _clean_working(self, cruft):
1038+ """The 'clean' operation is in progress.
1039+
1040+ :param cruft: The cruft that is being cleaned up.
1041+ :type cruft: string
1042+ """
1043+ verbose = self.options.arguments.verbose
1044+ if cruft == '':
1045+ # We're done.
1046+ if not verbose:
1047+ sys.stdout.write(' ')
1048+ sys.stdout.flush()
1049+ else:
1050+ if verbose:
1051+ print 'Working on', cruft
1052+ else:
1053+ sys.stdout.write('.')
1054+ sys.stdout.flush()
1055+
1056+ def clean(self, arguments):
1057+ """Clean up the cruft.
1058+
1059+ :param arguments: Command line options.
1060+ """
1061+ if arguments.all:
1062+ # You can't specify both --all and a cruft name.
1063+ if arguments.cruft is not None:
1064+ self._error('Specify a cruft name or --all, but not both')
1065+ # No return.
1066+ all_cruft = set(self.janitord.find())
1067+ ignored = set(self.janitord.ignored())
1068+ cleanable_cruft = all_cruft - ignored
1069+ else:
1070+ if arguments.cruft is None:
1071+ self._error('You must specify a cruft name, or use --all')
1072+ # No return.
1073+ cleanable_cruft = (arguments.cruft,)
1074+ # Make the asynchronous call because this can take a long time. We'll
1075+ # get status updates periodically. Note however that even though this
1076+ # is asynchronous, dbus still expects a response within a certain
1077+ # amount of time. We have no idea how long it will take to clean up
1078+ # the cruft though, so just crank the timeout up to some insanely huge
1079+ # number (of seconds).
1080+ self.janitord.clean(cleanable_cruft,
1081+ reply_handler=self._clean_reply,
1082+ error_handler=self._clean_error,
1083+ # If it takes longer than an hour, we're screwed.
1084+ timeout=3600)
1085+ # Start the main loop. This will exit when the remote operation is
1086+ # complete.
1087+ if not arguments.verbose:
1088+ sys.stdout.write('processing')
1089+ sys.stdout.flush()
1090+ self.loop.run()
1091+
1092+
1093+def main():
1094+ # We'll need a main loop to receive status signals from the dbus service.
1095+ # Don't start the main loop yet though, since we only need it for 'clean'
1096+ # commands.
1097+ DBusGMainLoop(set_as_default=True)
1098+ runner = Runner()
1099+ options = Options(runner)
1100+ # Backpatch runner because of circular references.
1101+ runner.options = options
1102+ # Execute the subcommand.
1103+ options.arguments.func(options.arguments)
1104
1105=== added file 'computerjanitorapp/maingtk.py'
1106--- computerjanitorapp/maingtk.py 1970-01-01 00:00:00 +0000
1107+++ computerjanitorapp/maingtk.py 2010-03-10 21:40:30 +0000
1108@@ -0,0 +1,32 @@
1109+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
1110+#
1111+# This program is free software: you can redistribute it and/or modify
1112+# it under the terms of the GNU General Public License as published by
1113+# the Free Software Foundation, version 3 of the License.
1114+#
1115+# This program is distributed in the hope that it will be useful,
1116+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1117+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1118+# GNU General Public License for more details.
1119+#
1120+# You should have received a copy of the GNU General Public License
1121+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1122+
1123+"""Command line user interface."""
1124+
1125+from __future__ import absolute_import, unicode_literals
1126+
1127+__metaclass__ = type
1128+__all__ = [
1129+ 'main',
1130+ ]
1131+
1132+
1133+from computerjanitorapp.uigtk import UserInterface
1134+from dbus.mainloop.glib import DBusGMainLoop
1135+
1136+
1137+def main():
1138+ DBusGMainLoop(set_as_default=True)
1139+ ui = UserInterface()
1140+ ui.run()
1141
1142=== removed file 'computerjanitorapp/state.py'
1143--- computerjanitorapp/state.py 2009-07-13 09:48:06 +0000
1144+++ computerjanitorapp/state.py 1970-01-01 00:00:00 +0000
1145@@ -1,56 +0,0 @@
1146-# state.py - store persistent state of crufts
1147-# Copyright (C) 2008, 2009 Canonical, Ltd.
1148-#
1149-# This program is free software: you can redistribute it and/or modify
1150-# it under the terms of the GNU General Public License as published by
1151-# the Free Software Foundation, version 3 of the License.
1152-#
1153-# This program is distributed in the hope that it will be useful,
1154-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1155-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1156-# GNU General Public License for more details.
1157-#
1158-# You should have received a copy of the GNU General Public License
1159-# along with this program. If not, see <http://www.gnu.org/licenses/>.
1160-
1161-
1162-import ConfigParser
1163-
1164-
1165-class State(object):
1166-
1167- def __init__(self):
1168- self._cp = ConfigParser.ConfigParser()
1169- self._previously_ignored = set()
1170-
1171- def load(self, filename):
1172- self._cp.read(filename)
1173- self._previously_ignored = set()
1174- for cruft_name in self._cp.sections():
1175- if not self.is_enabled(cruft_name):
1176- self._previously_ignored.add(cruft_name)
1177-
1178- def save(self, filename):
1179- f = file(filename, "w")
1180- self._cp.write(f)
1181- f.close()
1182-
1183- def is_enabled(self, cruft_name):
1184- if self._cp.has_section(cruft_name):
1185- return self._cp.getboolean(cruft_name, "enabled")
1186- else:
1187- return True
1188-
1189- def enable(self, cruft_name):
1190- if not self._cp.has_section(cruft_name):
1191- self._cp.add_section(cruft_name)
1192- self._cp.set(cruft_name, "enabled", "true")
1193-
1194- def disable(self, cruft_name):
1195- if not self._cp.has_section(cruft_name):
1196- self._cp.add_section(cruft_name)
1197- self._cp.set(cruft_name, "enabled", "false")
1198-
1199- def was_previously_ignored(self, cruft_name):
1200- return cruft_name in self._previously_ignored
1201-
1202
1203=== removed file 'computerjanitorapp/state_tests.py'
1204--- computerjanitorapp/state_tests.py 2009-07-13 09:48:06 +0000
1205+++ computerjanitorapp/state_tests.py 1970-01-01 00:00:00 +0000
1206@@ -1,77 +0,0 @@
1207-# state_tests.py - unit tests for store.py
1208-# Copyright (C) 2008, 2009 Canonical, Ltd.
1209-#
1210-# This program is free software: you can redistribute it and/or modify
1211-# it under the terms of the GNU General Public License as published by
1212-# the Free Software Foundation, version 3 of the License.
1213-#
1214-# This program is distributed in the hope that it will be useful,
1215-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1216-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1217-# GNU General Public License for more details.
1218-#
1219-# You should have received a copy of the GNU General Public License
1220-# along with this program. If not, see <http://www.gnu.org/licenses/>.
1221-
1222-
1223-import os
1224-import tempfile
1225-import unittest
1226-
1227-import computerjanitorapp
1228-
1229-
1230-class StateTests(unittest.TestCase):
1231-
1232- def setUp(self):
1233- self.state = computerjanitorapp.State()
1234-
1235- def testInitiallyEverythingIsEnabled(self):
1236- self.assert_(self.state.is_enabled("foo"))
1237-
1238- def testDisablesWhenAsked(self):
1239- self.state.disable("foo")
1240- self.assertFalse(self.state.is_enabled("foo"))
1241-
1242- def testEnablesDisabledCruft(self):
1243- self.state.disable("foo")
1244- self.state.enable("foo")
1245- self.assert_(self.state.is_enabled("foo"))
1246-
1247- def testEnablesEnabledCruft(self):
1248- self.state.enable("foo")
1249- self.assert_(self.state.is_enabled("foo"))
1250-
1251- def testSavesAndLoadsFiles(self):
1252- fd, filename = tempfile.mkstemp()
1253- os.close(fd)
1254- self.state.enable("foo")
1255- self.state.disable("bar")
1256- self.state.save(filename)
1257- self.state.disable("foo")
1258- self.state.enable("bar")
1259- self.state.load(filename)
1260- self.assert_(self.state.is_enabled("foo"))
1261- self.assertFalse(self.state.is_enabled("bar"))
1262- os.remove(filename)
1263-
1264- def testInitallyNothingIsPreviouslyIgnored(self):
1265- self.assertFalse(self.state.was_previously_ignored("foo"))
1266-
1267- def testRemembersWhatWasPreviouslyIgnored(self):
1268- fd, filename = tempfile.mkstemp()
1269- os.close(fd)
1270-
1271- # Set a state to disabled.
1272- state1 = computerjanitorapp.State()
1273- state1.disable("foo")
1274- state1.save(filename)
1275-
1276- # Load new state.
1277- state2 = computerjanitorapp.State()
1278- state2.load(filename)
1279-
1280- self.assert_(state2.was_previously_ignored("foo"))
1281-
1282- os.remove(filename)
1283-
1284
1285=== added file 'computerjanitorapp/store.py'
1286--- computerjanitorapp/store.py 1970-01-01 00:00:00 +0000
1287+++ computerjanitorapp/store.py 2010-03-10 21:40:30 +0000
1288@@ -0,0 +1,147 @@
1289+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
1290+#
1291+# This program is free software: you can redistribute it and/or modify
1292+# it under the terms of the GNU General Public License as published by
1293+# the Free Software Foundation, version 3 of the License.
1294+#
1295+# This program is distributed in the hope that it will be useful,
1296+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1297+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1298+# GNU General Public License for more details.
1299+#
1300+# You should have received a copy of the GNU General Public License
1301+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1302+
1303+"""Gtk ListStore model for backing the TreeViews of cruft."""
1304+
1305+from __future__ import absolute_import, unicode_literals
1306+
1307+__metaclass__ = type
1308+__all__ = [
1309+ 'Store',
1310+ 'optimize',
1311+ 'unused',
1312+ ]
1313+
1314+
1315+import gtk
1316+import gobject
1317+
1318+
1319+# XXX 2010-03-04 barry: Use a munepy enum.
1320+class ListStoreColumns:
1321+ # The cruft name as known by the dbus service.
1322+ name = 0
1323+ # The short name of the cruft.
1324+ short_name = 1
1325+ # The text displayed in the cell.
1326+ text = 2
1327+ # Should the cruft be shown?
1328+ show = 3
1329+ # Is the cruft active locally (i.e. toggle button is set)?
1330+ active = 4
1331+ # Is the cruft being ignored in the dbus service?
1332+ server_ignored = 5
1333+ # Is the cruft's description expanded?
1334+ expanded = 6
1335+ # Is this cruft package cruft or some other kind of cruft?
1336+ is_package_cruft = 7
1337+
1338+
1339+class Store:
1340+ """The higher level wrapper around the gtk.ListStore."""
1341+
1342+ def __init__(self, janitord):
1343+ """Create the Store.
1344+
1345+ :param janitord: The `dbus.Interface` for talking to the Computer
1346+ Janitor dbus service.
1347+ """
1348+ self.janitord = janitord
1349+ # This ListStore is the backing data structure for the TreeViews in
1350+ # the main window. It holds information about the individual pieces
1351+ # of cruft. See ListStoreColumns for details.
1352+ #
1353+ # XXX 2010-03-04 barry: pygtk does not like 'unicode'.
1354+ self.store = gtk.ListStore(str, str, str, bool, bool, bool, bool, bool)
1355+
1356+ def find_cruft(self):
1357+ """Find cruft and populate the backing store."""
1358+ ignored_cruft = set(self.janitord.ignored())
1359+ all_cruft_names = set(self.janitord.find())
1360+ pkg_cruft_names = set(
1361+ cruft_name for cruft_name in all_cruft_names
1362+ if self.janitord.get_details(cruft_name)[0].lower()
1363+ == 'packagecruft')
1364+ self.store.clear()
1365+ # Cruft always starts out in the active, not-expanded state. Package
1366+ # cruft goes in the 'unused' column while non-package cruft goes in
1367+ # the 'optimize' column.
1368+ for cruft_name in all_cruft_names:
1369+ cruft_shortname = self.janitord.get_shortname(cruft_name)
1370+ self.store.append((
1371+ cruft_name, cruft_shortname,
1372+ # What is being displayed in the cell.
1373+ gobject.markup_escape_text(cruft_shortname),
1374+ # Show the cruft be shown?
1375+ cruft_name not in ignored_cruft,
1376+ # Is the cruft active locally?
1377+ cruft_name not in ignored_cruft,
1378+ # Is the cruft ignored in the dbus service?
1379+ cruft_name in ignored_cruft,
1380+ # Is the cruft's description expanded?
1381+ False,
1382+ # Is this package cruft?
1383+ cruft_name in pkg_cruft_names,
1384+ ))
1385+
1386+ # Forwards to the underlying store, for convenience. Really, these should
1387+ # use generic delegates, or clients should just use self.store.store.
1388+
1389+ def get_value(self, *args, **kws):
1390+ return self.store.get_value(*args, **kws)
1391+
1392+ def set_value(self, *args, **kws):
1393+ return self.store.set_value(*args, **kws)
1394+
1395+ def filter_new(self, *args, **kws):
1396+ return self.store.filter_new(*args, **kws)
1397+
1398+ def foreach(self, *args, **kws):
1399+ return self.store.foreach(*args, **kws)
1400+
1401+ def reorder(self, *args, **kws):
1402+ return self.store.reorder(*args, **kws)
1403+
1404+ def get_iter_first(self, *args, **kws):
1405+ return self.store.get_iter_first(*args, **kws)
1406+
1407+ def iter_next(self, *args, **kws):
1408+ return self.store.iter_next(*args, **kws)
1409+
1410+ def clear(self, *args, **kws):
1411+ return self.store.clear(*args, **kws)
1412+
1413+
1414+# Filter functions for use with TreeView column display.
1415+
1416+def unused(store, iter):
1417+ """True if the cruft is not being ignored and is package cruft.
1418+
1419+ :param store: The ListStore instance.
1420+ :param iter: The TreeIter instance.
1421+ """
1422+ show = store.get_value(iter, ListStoreColumns.show)
1423+ is_package_cruft = store.get_value(iter, ListStoreColumns.is_package_cruft)
1424+ return show and is_package_cruft
1425+
1426+
1427+def optimize(store, iter):
1428+ """True if the cruft is not package cruft.
1429+
1430+ :param store: The ListStore instance.
1431+ :param iter: The TreeIter instance.
1432+ """
1433+ show = store.get_value(iter, ListStoreColumns.show)
1434+ is_package_cruft = store.get_value(iter, ListStoreColumns.is_package_cruft)
1435+ return show and not is_package_cruft
1436
1437=== modified file 'computerjanitorapp/terminalsize.py'
1438--- computerjanitorapp/terminalsize.py 2008-09-22 21:39:57 +0000
1439+++ computerjanitorapp/terminalsize.py 2010-03-10 21:40:30 +0000
1440@@ -26,44 +26,39 @@
1441
1442 def get_terminal_size(fd=1):
1443 """Return size of terminal attached to the standard output.
1444-
1445- Use ioctl(2) to query a terminal for its size, given a file
1446- descriptor attached to the terminal. Return (None, None) if this
1447- fails, otherwise a tuple (columns, rows).
1448-
1449- (The optional 'fd' argument can be set to whatever file
1450- descriptor you want to use. This is useful for unit tests.)
1451-
1452+
1453+ Use ioctl(2) to query a terminal for its size, given a file descriptor
1454+ attached to the terminal.
1455+
1456+ :param fd: Use the given file descriptor.
1457+ :type fd: int
1458+ :return: The columns and rows representing the size of the terminal. If
1459+ this cannot be determined, None is returned for both values.
1460+ :rtype: 2-tuple
1461 """
1462
1463 try:
1464- # Do the ioctl call. termios.TIOCGWINSZ is the code to query
1465- # terminal size (see tty_ioctl(4), at least on Linux). We need
1466- # to give it a string of suitable size to use as the input
1467- # buffer for ioctl. Ioctl modifies the buffer and returns the
1468- # modified buffer as its return value.
1469+ # Do the ioctl call. termios.TIOCGWINSZ is the code to query terminal
1470+ # size (see tty_ioctl(4), at least on Linux). We need to give it a
1471+ # string of suitable size to use as the input buffer for ioctl.
1472+ # ioctl() modifies the buffer and returns the modified buffer as its
1473+ # return value.
1474 #
1475 # The manual page specifies a struct winsize to be used, which
1476- # consists of four unsigned shorts. We use struct.calcsize to
1477+ # consists of four unsigned shorts. We use struct.calcsize() to
1478 # compute the size of that.
1479- #
1480- # Note that Blake's original code assumes only the first two
1481- # shorts in the struct are used, and that two shorts fit into
1482- # four bytes, which is probably true for all the relevant
1483- # platforms, but is cramped enough that it makes me feel icky.
1484- # Thus, I assume less. This will still break if the contents
1485- # of the struct change, but since that would change the system
1486- # call API, that's unlikely.
1487-
1488+ #
1489+ # Note that Blake's original code assumes only the first two shorts in
1490+ # the struct are used, and that two shorts fit into four bytes, which
1491+ # is probably true for all the relevant platforms, but is cramped
1492+ # enough that it makes me feel icky. Thus, I assume less. This will
1493+ # still break if the contents of the struct change, but since that
1494+ # would change the system call API, that's unlikely.
1495 buflen = struct.calcsize('hhhh')
1496 buf = fcntl.ioctl(fd, termios.TIOCGWINSZ, '\0' * buflen)
1497-
1498 # ioctl returns a binary buffer that represents the struct
1499 # at the C level. We unpack it with struct.unpack.
1500-
1501- tuple = struct.unpack('hhhh', buf)
1502- except:
1503+ return tuple(struct.unpack('hhhh', buf)[:2])
1504+ except Exception:
1505 # If anything went wrong, we give up and claim we don't know.
1506 return None, None
1507-
1508- return tuple[0], tuple[1]
1509
1510=== added directory 'computerjanitorapp/tests'
1511=== added file 'computerjanitorapp/tests/__init__.py'
1512=== added file 'computerjanitorapp/tests/test_all.py'
1513--- computerjanitorapp/tests/test_all.py 1970-01-01 00:00:00 +0000
1514+++ computerjanitorapp/tests/test_all.py 2010-03-10 21:40:30 +0000
1515@@ -0,0 +1,29 @@
1516+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
1517+#
1518+# This program is free software: you can redistribute it and/or modify
1519+# it under the terms of the GNU General Public License as published by
1520+# the Free Software Foundation, version 3 of the License.
1521+#
1522+# This program is distributed in the hope that it will be useful,
1523+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1524+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1525+# GNU General Public License for more details.
1526+#
1527+# You should have received a copy of the GNU General Public License
1528+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1529+
1530+"""Test suite for Computer Janitor."""
1531+
1532+import unittest
1533+
1534+from computerjanitorapp.tests import test_terminalsize
1535+from computerjanitorapp.tests import test_utilities
1536+from computerjanitord.tests.test_all import test_suite as cjd_suite
1537+
1538+
1539+def test_suite():
1540+ suite = unittest.TestSuite()
1541+ suite.addTests(test_terminalsize.test_suite())
1542+ suite.addTests(test_utilities.test_suite())
1543+ suite.addTests(cjd_suite())
1544+ return suite
1545
1546=== renamed file 'computerjanitorapp/terminalsize_tests.py' => 'computerjanitorapp/tests/test_terminalsize.py'
1547--- computerjanitorapp/terminalsize_tests.py 2008-09-22 21:39:57 +0000
1548+++ computerjanitorapp/tests/test_terminalsize.py 2010-03-10 21:40:30 +0000
1549@@ -1,5 +1,4 @@
1550-# terminalsize_tests.py - unit tests for terminalsize.py
1551-# Copyright (C) 2008 Canonical, Ltd.
1552+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
1553 #
1554 # This program is free software: you can redistribute it and/or modify
1555 # it under the terms of the GNU General Public License as published by
1556@@ -13,23 +12,33 @@
1557 # You should have received a copy of the GNU General Public License
1558 # along with this program. If not, see <http://www.gnu.org/licenses/>.
1559
1560+"""Test calculation of terminal sizes."""
1561+
1562+from __future__ import absolute_import, unicode_literals
1563+
1564+__metaclass__ = type
1565+__all__ = [
1566+ 'test_suite',
1567+ ]
1568+
1569
1570 import os
1571 import unittest
1572
1573-import terminalsize
1574-
1575-
1576-class GetTerminalSizeTests(unittest.TestCase):
1577-
1578- def testReturnsUnknownWhenQueryingDevNull(self):
1579+from computerjanitorapp import terminalsize
1580+
1581+
1582+class TestTerminalSize(unittest.TestCase):
1583+ """Test calculation of terminal sizes."""
1584+
1585+ def test_returns_unknown_when_querying_dev_null(self):
1586 fd = os.open("/dev/null", os.O_RDONLY)
1587 rows, cols = terminalsize.get_terminal_size(fd)
1588 os.close(fd)
1589 self.assertEqual(rows, None)
1590 self.assertEqual(cols, None)
1591
1592- def testReturnsTwoIntegersWhenStdoutIsATerminal(self):
1593+ def test_returns_two_integers_when_stdout_is_a_terminal(self):
1594 # We only run this check if stdout is a terminal.
1595 # Unfortunately, there is no sensible way of checking the values.
1596 # But that's OK, they're lumberjacks.
1597@@ -37,3 +46,9 @@
1598 rows, cols = terminalsize.get_terminal_size(1)
1599 self.assertEqual(type(rows), int)
1600 self.assertEqual(type(cols), int)
1601+
1602+
1603+def test_suite():
1604+ suite = unittest.TestSuite()
1605+ suite.addTests(unittest.makeSuite(TestTerminalSize))
1606+ return suite
1607
1608=== added file 'computerjanitorapp/tests/test_utilities.py'
1609--- computerjanitorapp/tests/test_utilities.py 1970-01-01 00:00:00 +0000
1610+++ computerjanitorapp/tests/test_utilities.py 2010-03-10 21:40:30 +0000
1611@@ -0,0 +1,71 @@
1612+# Copyright (C) 2010 Canonical, Ltd.
1613+#
1614+# This program is free software: you can redistribute it and/or modify
1615+# it under the terms of the GNU General Public License as published by
1616+# the Free Software Foundation, version 3 of the License.
1617+#
1618+# This program is distributed in the hope that it will be useful,
1619+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1620+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1621+# GNU General Public License for more details.
1622+#
1623+# You should have received a copy of the GNU General Public License
1624+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1625+
1626+"""Test common utilities."""
1627+
1628+from __future__ import absolute_import, unicode_literals
1629+
1630+__metaclass__ = type
1631+__all__ = [
1632+ 'test_suite',
1633+ ]
1634+
1635+
1636+import os
1637+import unittest
1638+
1639+from computerjanitorapp.utilities import format_size
1640+
1641+
1642+class TestUtilities(unittest.TestCase):
1643+ """Test common utilities."""
1644+
1645+ def test_format_negative(self):
1646+ self.assertRaises(AssertionError, format_size, -1)
1647+
1648+ def test_format_zero(self):
1649+ self.assertEqual(format_size(0), '0B')
1650+
1651+ def test_format_small(self):
1652+ self.assertEqual(format_size(500), '500B')
1653+
1654+ def test_format_1k(self):
1655+ self.assertEqual(format_size(1000), '1kB')
1656+
1657+ def test_format_smallish(self):
1658+ self.assertEqual(format_size(500000), '500kB')
1659+
1660+ def test_format_1M(self):
1661+ self.assertEqual(format_size(1000000), '1MB')
1662+
1663+ def test_format_mediumish(self):
1664+ self.assertEqual(format_size(500000000), '500MB')
1665+
1666+ def test_format_1G(self):
1667+ self.assertEqual(format_size(1000000000), '1GB')
1668+
1669+ def test_format_bigish(self):
1670+ self.assertEqual(format_size(500000000000), '500GB')
1671+
1672+ def test_format_1T(self):
1673+ self.assertEqual(format_size(1000000000000), '1TB')
1674+
1675+ def test_format_hugish(self):
1676+ self.assertEqual(format_size(500000000000000), '>1TB')
1677+
1678+
1679+def test_suite():
1680+ suite = unittest.TestSuite()
1681+ suite.addTests(unittest.makeSuite(TestUtilities))
1682+ return suite
1683
1684=== removed file 'computerjanitorapp/ui.py'
1685--- computerjanitorapp/ui.py 2009-02-11 16:25:03 +0000
1686+++ computerjanitorapp/ui.py 1970-01-01 00:00:00 +0000
1687@@ -1,39 +0,0 @@
1688-# ui.py - user interface interface class
1689-# Copyright (C) 2008, 2009 Canonical, Ltd.
1690-#
1691-# This program is free software: you can redistribute it and/or modify
1692-# it under the terms of the GNU General Public License as published by
1693-# the Free Software Foundation, version 3 of the License.
1694-#
1695-# This program is distributed in the hope that it will be useful,
1696-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1697-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1698-# GNU General Public License for more details.
1699-#
1700-# You should have received a copy of the GNU General Public License
1701-# along with this program. If not, see <http://www.gnu.org/licenses/>.
1702-
1703-
1704-import computerjanitor
1705-
1706-
1707-class UserInterface(object):
1708-
1709- """This is the base class for user interfaces.
1710-
1711- The user interface is in charge of obeying command line arguments,
1712- and interacting with the user.
1713-
1714- The app and pm given to the constructor are stored in the app and
1715- pm attributes.
1716-
1717- """
1718-
1719- def __init__(self, app, pm, mustberoot=True):
1720- self.pm = pm
1721- self.app = app
1722- self.mustberoot = mustberoot
1723-
1724- def run(self, options, args):
1725- """Obey command line arguments in ARGS, and options in OPTIONS."""
1726- raise computerjanitor.UnimplementedMethod(self.run)
1727
1728=== removed file 'computerjanitorapp/ui_cli_tests.py'
1729--- computerjanitorapp/ui_cli_tests.py 2009-02-11 16:25:03 +0000
1730+++ computerjanitorapp/ui_cli_tests.py 1970-01-01 00:00:00 +0000
1731@@ -1,246 +0,0 @@
1732-# ui_cli_tests.py - unit tests for ui_cli.py
1733-# Copyright (C) 2008, 2009 Canonical, Ltd.
1734-#
1735-# This program is free software: you can redistribute it and/or modify
1736-# it under the terms of the GNU General Public License as published by
1737-# the Free Software Foundation, version 3 of the License.
1738-#
1739-# This program is distributed in the hope that it will be useful,
1740-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1741-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1742-# GNU General Public License for more details.
1743-#
1744-# You should have received a copy of the GNU General Public License
1745-# along with this program. If not, see <http://www.gnu.org/licenses/>.
1746-
1747-
1748-import unittest
1749-
1750-import computerjanitor
1751-import computerjanitorapp
1752-
1753-
1754-class MockCruft(object):
1755-
1756- def __init__(self, name):
1757- self._name = name
1758- self.cleaned = False
1759- self.post_cleaned = False
1760-
1761- def get_name(self):
1762- return self._name
1763-
1764- def cleanup(self):
1765- self.cleaned = True
1766-
1767-
1768-class MockPlugin(object):
1769-
1770- def __init__(self, crufts):
1771- self._crufts = crufts
1772- self.post_cleaned = False
1773-
1774- def get_cruft(self):
1775- for cruft in self._crufts:
1776- yield cruft
1777-
1778- def post_cleanup(self):
1779- self.post_cleaned = True
1780-
1781-
1782-class MockPluginManager(object):
1783-
1784- def __init__(self, plugins):
1785- self._plugins = plugins
1786-
1787- def get_plugins(self):
1788- return self._plugins
1789-
1790-
1791-class MockState(object):
1792-
1793- def __init__(self, enabled):
1794- self._enabled = set(enabled)
1795-
1796- def is_enabled(self, name):
1797- return name in self._enabled
1798-
1799- def enable(self, name):
1800- if name not in self._enabled:
1801- self._enabled.add(name)
1802-
1803- def disable(self, name):
1804- if name in self._enabled:
1805- self._enabled.remove(name)
1806-
1807- def load(self, filename):
1808- self.load_filename = filename
1809-
1810- def save(self, filename):
1811- self.save_filename = filename
1812-
1813-
1814-class MockOptionParser(object):
1815-
1816- def __init__(self):
1817- self.help_printed = False
1818-
1819- def print_help(self):
1820- self.help_printed = True
1821-
1822-
1823-class MockApplication(object):
1824-
1825- def __init__(self, enabled):
1826- self.state = MockState(enabled)
1827- self.parser = MockOptionParser()
1828-
1829- def verify_apt_cache(self):
1830- pass
1831-
1832- def remove_whitelisted(self, crufts):
1833- return crufts
1834-
1835-
1836-class MockOptions(object):
1837-
1838- def __init__(self):
1839- self.all = None
1840- self.state_file = "foo"
1841- self.no_act = None
1842- self.verbose = None
1843-
1844-
1845-class CommandLineUserInterfaceTests(unittest.TestCase):
1846-
1847- def setUp(self):
1848- self.app = MockApplication(["foo"])
1849- self.cruft_names = ["foo", "bar"]
1850- self.crufts = [MockCruft(name) for name in self.cruft_names]
1851- self.cruftdict = dict((c.get_name(), c) for c in self.crufts)
1852- self.plugin = MockPlugin(self.crufts)
1853- self.pm = MockPluginManager([self.plugin])
1854- self.ui = computerjanitorapp.CommandLineUserInterface(self.app,
1855- self.pm, mustberoot=False)
1856-
1857- self.options = MockOptions()
1858-
1859- def testInsistsOnBeingRoot(self):
1860- self.ui.mustberoot = True
1861- self.assertRaises(computerjanitor.Exception, self.ui.run, None,
1862- None)
1863-
1864- def testFindsTheRightCruft(self):
1865- self.assertEqual(self.ui.find_cruft(), self.crufts)
1866-
1867- def testShowsTheRightCruftTheRightWay(self):
1868-
1869- def mock_find_cruft():
1870- return self.crufts
1871-
1872- def mock_show_one_cruft(name, desc, state, width):
1873- output.append((name, state))
1874-
1875- output = []
1876- self.ui.find_cruft = mock_find_cruft
1877- self.ui.show_one_cruft = mock_show_one_cruft
1878- self.ui.show_cruft(MockOptions(), None)
1879- self.assertEqual(output,
1880- sorted([("foo", "removable"), ("bar", "ignored")]))
1881-
1882- def testIgnoresCruftCorrectly(self):
1883- self.ui.ignore(self.options, ["foo"])
1884- self.assertFalse(self.app.state.is_enabled("foo"))
1885-
1886- def testUnignoresCruftCorrectly(self):
1887- self.ui.unignore(self.options, ["bar"])
1888- self.assert_(self.app.state.is_enabled("bar"))
1889-
1890- def testCleansUpEnabledCruftWithDashDashAll(self):
1891- self.options.all = True
1892- self.ui.cleanup(self.options, [])
1893- self.assert_(self.cruftdict["foo"].cleaned)
1894-
1895- def testDoesNotCleanUpEnabledCruftWithDashDashAllWhenNoActIsSet(self):
1896- self.options.all = True
1897- self.options.no_act = True
1898- self.ui.cleanup(self.options, [])
1899- self.assertFalse(self.cruftdict["foo"].cleaned)
1900-
1901- def testCleansUpRequestedEnabledCruft(self):
1902- self.ui.cleanup(self.options, ["foo"])
1903- self.assert_(self.cruftdict["foo"].cleaned)
1904-
1905- def testDoesNotCleanUpDisaabledCruftWithDashDashAll(self):
1906- self.options.all = True
1907- self.ui.cleanup(self.options, [])
1908- self.assertFalse(self.cruftdict["bar"].cleaned)
1909-
1910- def testCleansUpRequestedDisabledCruft(self):
1911- self.ui.cleanup(self.options, ["bar"])
1912- self.assert_(self.cruftdict["bar"].cleaned)
1913-
1914- def testRunsPostCleanup(self):
1915- self.ui.cleanup(self.options, [])
1916- self.assert_(self.plugin.post_cleaned)
1917-
1918- def testDoesNotRunPostCleanupWhenNoActIsSet(self):
1919- self.options.no_act = True
1920- self.ui.cleanup(self.options, [])
1921- self.assertFalse(self.plugin.post_cleaned)
1922-
1923- def testRaisesExceptionForUnknownCruft(self):
1924- self.assertRaises(computerjanitor.Exception, self.ui.cleanup,
1925- self.options, ["unknown"])
1926-
1927- def testHelpCallsParserPrintHelp(self):
1928- self.ui.help(None, None)
1929- self.assert_(self.app.parser.help_printed)
1930-
1931- def setup_run(self):
1932- names = ["show_cruft", "cleanup", "ignore", "unignore", "help"]
1933- for name in names:
1934- method = getattr(self.ui, name)
1935- wrapper = lambda options, args, name=name: \
1936- setattr(self, "operation", name)
1937- setattr(self.ui, name, wrapper)
1938-
1939- def testRunCallsShowCruft(self):
1940- self.setup_run()
1941- self.ui.run(self.options, ["find"])
1942- self.assertEqual(self.operation, "show_cruft")
1943-
1944- def testRunCallsCleanup(self):
1945- self.setup_run()
1946- self.ui.run(self.options, ["cleanup"])
1947- self.assertEqual(self.operation, "cleanup")
1948-
1949- def testRunCallsIgnore(self):
1950- self.setup_run()
1951- self.ui.run(self.options, ["ignore"])
1952- self.assertEqual(self.operation, "ignore")
1953-
1954- def testRunCallsUnignore(self):
1955- self.setup_run()
1956- self.ui.run(self.options, ["unignore"])
1957- self.assertEqual(self.operation, "unignore")
1958-
1959- def testRunCallsHelp(self):
1960- self.setup_run()
1961- self.ui.run(self.options, ["help"])
1962- self.assertEqual(self.operation, "help")
1963-
1964- def testRunCallsHelpWhenThereAreNoArguments(self):
1965- self.setup_run()
1966- self.ui.run(self.options, [])
1967- self.assertEqual(self.operation, "help")
1968-
1969- def testRaisesExceptionForUnknownCommand(self):
1970- self.assertRaises(computerjanitor.Exception, self.ui.run,
1971- self.options, ["yikes"])
1972-
1973- def testRunLoadsStateFromRightFile(self):
1974- self.setup_run()
1975- self.ui.run(self.options, ["ignore"])
1976- self.assertEqual(self.app.state.load_filename,
1977- self.options.state_file)
1978
1979=== removed file 'computerjanitorapp/ui_gtk.py'
1980--- computerjanitorapp/ui_gtk.py 2010-01-21 13:54:34 +0000
1981+++ computerjanitorapp/ui_gtk.py 1970-01-01 00:00:00 +0000
1982@@ -1,714 +0,0 @@
1983-# ui_gtk.py - graphical user interface implemented using GTK+
1984-# Copyright (C) 2008, 2009 Canonical, Ltd.
1985-#
1986-# This program is free software: you can redistribute it and/or modify
1987-# it under the terms of the GNU General Public License as published by
1988-# the Free Software Foundation, version 3 of the License.
1989-#
1990-# This program is distributed in the hope that it will be useful,
1991-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1992-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1993-# GNU General Public License for more details.
1994-#
1995-# You should have received a copy of the GNU General Public License
1996-# along with this program. If not, see <http://www.gnu.org/licenses/>.
1997-
1998-
1999-import logging
2000-import os
2001-import sys
2002-import threading
2003-import time
2004-import traceback
2005-
2006-import computerjanitor
2007-import computerjanitorapp
2008-_ = computerjanitorapp.setup_gettext()
2009-
2010-# We can't import gtk here, since that would mean it gets imported
2011-# even when we run in command line mode. Thus, we do the imports in
2012-# the class, when we know we need them.
2013-
2014-
2015-import os
2016-GLADE = os.environ.get('COMPUTER_JANITOR_GLADE',
2017- '/usr/share/computer-janitor/ComputerJanitor.ui')
2018-
2019-
2020-STATE_COL = 0
2021-NAME_COL = 1
2022-CRUFT_COL = 2
2023-EXPANDED_COL = 3
2024-SHOW_COL = 4
2025-
2026-
2027-def ui(method):
2028- """Decorator function for UI methods.
2029-
2030- Use @ui to decorate all methods in the controller class that
2031- call GTK stuff, to ensure correct locking.
2032-
2033- """
2034-
2035- def new(self, *args, **kwargs):
2036- self.gtk.gdk.threads_enter()
2037- ret = method(self, *args, **kwargs)
2038- self.gtk.gdk.threads_leave()
2039- return ret
2040-
2041- return new
2042-
2043-
2044-class ListUpdater(threading.Thread):
2045-
2046- """Find cruft in the background, update user interface when done."""
2047-
2048- def __init__(self, ui):
2049- threading.Thread.__init__(self)
2050- self.ui = ui
2051-
2052- def run(self):
2053- self.ui.finding = True
2054- self.ui.set_sensitive()
2055- status = self.ui.widgets['statusbar']
2056- context_id = status.get_context_id('ListUpdater')
2057- status.push(context_id, _('Analyzing system...'))
2058- for plugin in self.ui.pm.get_plugins():
2059- for cruft in self.ui.app.remove_whitelisted(plugin.get_cruft()):
2060- self.ui.add_cruft(cruft)
2061- status.pop(context_id)
2062- self.ui.finding = False
2063- self.ui.set_sensitive()
2064- self.ui.done_updating_list()
2065-
2066-
2067-class Cleaner(threading.Thread):
2068-
2069- """Actually clean up the cruft."""
2070-
2071- def __init__(self, ui):
2072- threading.Thread.__init__(self)
2073- self.ui = ui
2074- self.gtk = self.ui.gtk
2075- self.crufts = ui.get_crufts()
2076- self.plugins = ui.pm.get_plugins()
2077- # progress reporting dialog/bar
2078- # FIXME: the progress bar is a bit silly, it takes
2079- # items like fix-fstab (0.1s) and deb_package (5min)
2080- # and assigns them the same slice - use a pulsing one
2081- # instead?
2082- self.dialog = ui.widgets['cleanup_dialog']
2083- self.pbar = ui.widgets['cleanup_progressbar']
2084- # add status bar context
2085- self.status = self.ui.widgets['statusbar']
2086- self.context_id = self.status.get_context_id('ListUpdater')
2087- self.status.push(self.context_id, _('Cleaning up...'))
2088- # cancel button
2089- button = ui.widgets['cleanup_cancel']
2090- button.connect('clicked', self.cancel)
2091- self.cancel_event = threading.Event()
2092-
2093- def cancel(self, *args):
2094- self.cancel_event.set()
2095-
2096- @ui
2097- def inc_done(self):
2098- self.done_work += 1.0
2099- self.pbar.set_fraction(self.done_work / self.total_work)
2100-
2101- @ui
2102- def start_reporting(self):
2103- self.total_work = len(self.crufts) + len(self.plugins)
2104- self.done_work = 0.0
2105- self.pbar.set_fraction(0.0)
2106-
2107- self.dialog.show()
2108-
2109- @ui
2110- def end_reporting(self):
2111- self.pbar.set_fraction(1.0)
2112- self.dialog.hide()
2113- self.status.pop(self.context_id)
2114-
2115- def run(self):
2116- self.ui.cleaning = True
2117- self.ui.set_sensitive()
2118-
2119- self.start_reporting()
2120-
2121- for cruft in self.crufts:
2122- if self.cancel_event.isSet():
2123- break
2124- name = cruft.get_name()
2125- if self.ui.app.state.is_enabled(name):
2126- if self.ui.options.no_act:
2127- time.sleep(0.1)
2128- logging.info(_("Pretending to remove cruft: %s") % name)
2129- else:
2130- logging.info(_("Removing cruft: %s") % name)
2131- cruft.cleanup()
2132- self.inc_done()
2133-
2134- for plugin in self.plugins:
2135- if self.cancel_event.isSet():
2136- break
2137- if self.ui.options.no_act:
2138- logging.info(_("Pretending to post-cleanup: %s") % plugin)
2139- else:
2140- logging.info(_("Post-cleanup: %s") % plugin)
2141- error = None
2142- try:
2143- plugin.post_cleanup()
2144- except Exception, e:
2145- logging.debug(unicode(traceback.format_exc()))
2146- self.ui.show_error(_("Could not clean up properly"),
2147- unicode(e))
2148- break
2149- self.inc_done()
2150-
2151- # do not run it directly (thread deadlocks), register for running
2152- # in the gtk main thread instead
2153- self.ui.glib.timeout_add(100, self.ui.show_cruft)
2154- self.ui.cleaning = False
2155-
2156- self.end_reporting()
2157-
2158-
2159-class GtkUserInterface(computerjanitorapp.UserInterface):
2160-
2161- """The GTK+ user interface of Computer Janitor."""
2162-
2163- # This acts as the controller in MVC. To simplify binding of callbacks
2164- # to GTK signals, we follow a strict convention: any method named
2165- # on_WIDGETNAME_SIGNALNAME is a callback and will be bound automatically.
2166- # This way, we don't need do add each binding by hand, either in the
2167- # code or in the .glade file. See the find_and_bind_widgets method
2168- # for details.
2169-
2170- def run(self, options, args):
2171- # This is where UI execution starts.
2172-
2173- import gtk
2174- self.gtk = gtk
2175-
2176- import gobject
2177- self.gobject = gobject
2178-
2179- import glib
2180- self.glib = glib
2181-
2182- self.options = options
2183- self.app.state.load(options.state_file)
2184-
2185- builder = gtk.Builder()
2186- builder.set_translation_domain('computerjanitor')
2187- builder.add_from_file(GLADE)
2188- self.find_and_bind_widgets(builder)
2189-
2190- self.store = self.create_cruft_store()
2191- self.name_cols = set()
2192- self.popup_menus = dict()
2193- self.create_column('unused_treeview', self.unused_filter)
2194- self.create_column('recommended_treeview', self.recommended_filter)
2195- self.create_column('optimize_treeview', self.optimize_filter)
2196-
2197- self.show_previously_ignored = False
2198-
2199- self.first_map = True
2200-
2201- self.set_default_window_size()
2202- self.widgets['window'].show()
2203-
2204- self.sort_crufts_by_current_order = self.sort_crufts_by_name
2205-
2206- self.finding = False
2207- self.cleaning = False
2208-
2209- # set thread switches interval to make it more UI friendly
2210- sys.setcheckinterval(0)
2211- gtk.gdk.threads_init()
2212- gtk.main()
2213-
2214- def find_and_bind_widgets(self, builder):
2215- """Bind widgets and callbacks."""
2216- import gtk
2217- self.widgets = {}
2218- for o in builder.get_objects():
2219- if issubclass(type(o), gtk.Buildable):
2220- name = gtk.Buildable.get_name(o)
2221- self.widgets[name] = o
2222- for attr in dir(self):
2223- prefix = 'on_%s_' % name
2224- if attr.startswith(prefix):
2225- signal_name = attr[len(prefix):]
2226- method = getattr(self, attr)
2227- o.connect(signal_name, method)
2228-
2229-
2230- def set_default_window_size(self):
2231- w = self.widgets['window']
2232- width = 900
2233- height = 700
2234- self.widgets['window'].set_default_size(width, height)
2235-
2236- def create_cruft_store(self):
2237- """Create a gtk.ListStore for holding all the cruft."""
2238- pairs = ((NAME_COL, self.gobject.TYPE_STRING),
2239- (STATE_COL, self.gobject.TYPE_BOOLEAN),
2240- (CRUFT_COL, self.gobject.TYPE_PYOBJECT),
2241- (EXPANDED_COL, self.gobject.TYPE_BOOLEAN),
2242- (SHOW_COL, self.gobject.TYPE_BOOLEAN))
2243- column_types = [pair[1] for pair in sorted(pairs)]
2244- store = self.gtk.ListStore(*column_types)
2245-
2246- return store
2247-
2248- def get_crufts(self):
2249- def get(model, path, iter, crufts):
2250- cruft = model.get_value(iter, CRUFT_COL)
2251- crufts.append(cruft)
2252- crufts = []
2253- self.store.foreach(get, crufts)
2254- return crufts
2255-
2256- def sort_crufts(self, get_key):
2257- crufts = self.get_crufts()
2258- crufts = [(get_key(c), i, c) for i, c in enumerate(crufts)]
2259- crufts.sort()
2260- crufts = [i for key, i, c in crufts]
2261- self.store.reorder(crufts)
2262-
2263- def sort_crufts_by_name(self):
2264- def get_key(cruft):
2265- return cruft.get_name()
2266- self.sort_crufts(get_key)
2267-
2268- def sort_crufts_by_size(self):
2269- def get_key(cruft):
2270- return -cruft.get_disk_usage()
2271- self.sort_crufts(get_key)
2272-
2273- def create_column(self, widget_name, filterfunc):
2274- """Add gtk.TreeViewColumn to the desired widget."""
2275- treeview = self.widgets[widget_name]
2276- treeview.set_rules_hint(True)
2277-
2278- toggle_cr = self.gtk.CellRendererToggle()
2279- toggle_cr.connect('toggled', self.toggled, treeview)
2280- toggle_cr.set_property("yalign", 0)
2281- toggle_col = self.gtk.TreeViewColumn()
2282- toggle_col.pack_start(toggle_cr)
2283- toggle_col.add_attribute(toggle_cr, 'active', STATE_COL)
2284- treeview.append_column(toggle_col)
2285-
2286- name_cr = self.gtk.CellRendererText()
2287- name_cr.set_property("yalign", 0)
2288- import pango
2289- name_cr.set_property("wrap-mode", pango.WRAP_WORD)
2290- name_col = self.gtk.TreeViewColumn()
2291- name_col.pack_start(name_cr)
2292- name_col.add_attribute(name_cr, 'markup', NAME_COL)
2293- treeview.append_column(name_col)
2294- self.name_cols.add(name_col)
2295-
2296- filter_store = self.store.filter_new()
2297- filter_store.set_visible_func(filterfunc)
2298- treeview.set_model(filter_store)
2299-
2300- self.create_popup_menu_for_treeview(treeview)
2301-
2302- def create_popup_menu_for_treeview(self, treeview):
2303- select_all = self.gtk.MenuItem(label='Select all')
2304- select_all.connect('activate', self.popup_menu_select_all, treeview)
2305-
2306- unselect_all = self.gtk.MenuItem(label='Unselect all')
2307- unselect_all.connect('activate', self.popup_menu_unselect_all, treeview)
2308-
2309- menu = self.gtk.Menu()
2310- menu.append(select_all)
2311- menu.append(unselect_all)
2312- menu.show_all()
2313-
2314- self.popup_menus[treeview] = menu
2315-
2316- def unused_filter(self, store, iter):
2317- cruft = store.get_value(iter, CRUFT_COL)
2318- shown = store.get_value(iter, SHOW_COL)
2319- return shown and isinstance(cruft, computerjanitor.PackageCruft)
2320-
2321- def recommended_filter(self, store, iter):
2322- return False
2323-
2324- def optimize_filter(self, store, iter):
2325- shown = store.get_value(iter, SHOW_COL)
2326- return (shown and
2327- not self.unused_filter(store, iter) and
2328- not self.recommended_filter(store, iter))
2329-
2330- def error_dialog(self, msg, secondary_msg=None):
2331- dialog = self.gtk.MessageDialog(parent=self.widgets["window"],
2332- type=self.gtk.MESSAGE_ERROR,
2333- buttons=self.gtk.BUTTONS_OK,
2334- message_format=msg)
2335- if secondary_msg:
2336- dialog.format_secondary_text(secondary_msg)
2337-
2338- return dialog
2339-
2340- @ui
2341- def show_error(self, msg, secondary_msg=None):
2342- dialog = self.error_dialog(msg, secondary_msg)
2343- dialog.show()
2344- dialog.run()
2345- dialog.hide()
2346-
2347- def require_root(self):
2348- if os.getuid() != 0:
2349- dialog = self.error_dialog(_("Root access required."),
2350- _("You must run computer-janitor-gtk "
2351- "as root. Sorry."))
2352- dialog.show()
2353- dialog.run()
2354- sys.exit(1)
2355-
2356- def require_working_apt_cache(self):
2357- """ensure that the apt cache is in good state and error/exit
2358- otherwise
2359- """
2360- try:
2361- self.app.verify_apt_cache()
2362- except computerjanitor.Exception, e:
2363- logging.error(unicode(traceback.format_exc()))
2364- dialog = self.error_dialog(str(e))
2365- dialog.show()
2366- dialog.run()
2367- sys.exit(1)
2368-
2369- def pulse(self):
2370- """pulse callback that shows a progress pulse until finding is False"""
2371- progress = self.widgets['progressbar_status']
2372- if self.finding or self.cleaning:
2373- progress.show()
2374- progress.pulse()
2375- return True
2376- else:
2377- progress.hide()
2378- return False
2379-
2380- def show_cruft(self):
2381- """clear the cruft store and update it again via a thread """
2382- self.store.clear()
2383- # run as "daemon" thread to ensure that the main app exist
2384- # if the user presses "quit" before the ListUpdater thread
2385- # has finished
2386- t = ListUpdater(self)
2387- t.daemon = True
2388- t.start()
2389- # run a glib handler to shows a pulse progress
2390- self.glib.timeout_add(150, self.pulse)
2391-
2392- @ui
2393- def add_cruft(self, cruft):
2394- state = self.app.state.is_enabled(cruft.get_name())
2395- shown = (self.show_previously_ignored or
2396- not self.app.state.was_previously_ignored(cruft.get_name()))
2397- sort_index = 0
2398- values = ((CRUFT_COL, cruft),
2399- (NAME_COL, cruft.get_shortname()),
2400- (STATE_COL, state),
2401- (EXPANDED_COL, False),
2402- (SHOW_COL, shown))
2403- values = [pair[1] for pair in sorted(values)]
2404- self.store.append(values)
2405- self.sort_crufts_by_current_order()
2406-
2407- @ui
2408- def done_updating_list(self):
2409- if not self.find_visible_cruft():
2410- dialog = self.widgets['borednow_messagedialog']
2411- dialog.show()
2412- dialog.run()
2413- dialog.hide()
2414-
2415- def foreach_set_state(self, treeview, enabled):
2416- def set_state(model, path, iter, user_data):
2417- iter2 = model.convert_iter_to_child_iter(iter)
2418- cruft = self.store.get_value(iter2, CRUFT_COL)
2419- cruft_name = cruft.get_name()
2420- if enabled:
2421- self.app.state.enable(cruft_name)
2422- else:
2423- self.app.state.disable(cruft_name)
2424- self.store.set_value(iter2, STATE_COL, enabled)
2425- treeview.get_model().foreach(set_state, None)
2426- self.app.state.save(self.options.state_file)
2427- self.set_sensitive_unlocked()
2428-
2429- def format_name(self, cruft):
2430- return self.gobject.markup_escape_text(cruft.get_shortname())
2431-
2432- def format_size(self, bytes):
2433- table = ((1000**3, "GB"),
2434- (1000**2, "MB"),
2435- (1000**1, "kB"),
2436- ( 1, "B"))
2437- for factor, unit in table:
2438- if bytes >= factor or factor == 1:
2439- return '%d %s' % (bytes / factor, unit)
2440-
2441- def format_description(self, cruft):
2442- esc = self.gobject.markup_escape_text
2443-
2444- lines = [esc(cruft.get_shortname())]
2445-
2446- # FIXME: The action verbs should come from the crufts themselves.
2447- action_verbs = {
2448- computerjanitor.PackageCruft: 'uninstall',
2449- computerjanitor.FileCruft: 'remove',
2450- computerjanitor.MissingPackageCruft: 'install',
2451- }
2452- action_descriptions = {
2453- 'uninstall': _('Package will be <b>removed</b>.'),
2454- 'install': _('Package will be <b>installed</b>.'),
2455- 'remove': _('File will be <b>removed</b>.'),
2456- }
2457- action_verb = action_verbs.get(type(cruft))
2458- if action_verb:
2459- lines += [action_descriptions[action_verb]]
2460-
2461- size = cruft.get_disk_usage()
2462- if size is not None:
2463- lines += [_('Size: %s.') % self.format_size(size)]
2464-
2465- desc = cruft.get_description()
2466- if desc:
2467- lines += ['', esc(desc)]
2468-
2469- return '\n'.join(lines)
2470-
2471- def toggle_long_description(self, treeview):
2472- """Toggle the showing of the long description of some cruft."""
2473-
2474- selection = treeview.get_selection()
2475- filtermodel, selected = selection.get_selected()
2476- if not selected:
2477- return
2478- model = filtermodel.get_model()
2479- iter = filtermodel.convert_iter_to_child_iter(selected)
2480- cruft = model.get_value(iter, CRUFT_COL)
2481- expanded = model.get_value(iter, EXPANDED_COL)
2482- expanded = not expanded
2483- model.set_value(iter, EXPANDED_COL, expanded)
2484- if expanded:
2485- value = self.format_description(cruft)
2486- else:
2487- value = self.format_name(cruft)
2488- model.set_value(iter, NAME_COL, value)
2489-
2490- @ui
2491- def set_sensitive(self):
2492- self.set_sensitive_unlocked()
2493-
2494- def set_sensitive_unlocked(self):
2495- do = self.widgets['do_button']
2496-
2497- names = ['unused_treeview', 'recommended_treeview',
2498- 'optimize_treeview']
2499- cleanable_cruft = self.find_visible_cruft()
2500- do.set_sensitive(not self.finding and
2501- not self.cleaning and
2502- len(cleanable_cruft) > 0)
2503-
2504- def find_visible_cruft(self):
2505- names = ['unused_treeview', 'recommended_treeview',
2506- 'optimize_treeview']
2507- cleanable_cruft = []
2508- for name in names:
2509- w = self.widgets[name]
2510- model = w.get_model()
2511- it = model.get_iter_first()
2512- while it is not None:
2513- cruft = model.get_value(it, CRUFT_COL)
2514- if self.app.state.is_enabled(cruft.get_name()):
2515- cleanable_cruft.append(cruft)
2516- it = model.iter_next(it)
2517- return cleanable_cruft
2518-
2519- def really_cleanup(self):
2520- """Ask user if they really mean to clean up.
2521-
2522- Be especially insistent (in wording) if they are removing
2523- packages.
2524-
2525- """
2526-
2527- crufts = self.get_crufts()
2528- crufts = [c
2529- for c in crufts
2530- if self.app.state.is_enabled(c.get_name())]
2531- packages = [c
2532- for c in crufts
2533- if isinstance(c, computerjanitor.PackageCruft)]
2534- others = [c for c in crufts if c not in packages]
2535-
2536- # The following messages are a bit vague, since we need to handle
2537- # cases where we remove packages, and don't remove packages, and
2538- # so on, given that "clean up cruft" is such a general concept.
2539- # My apologies to anyone who thinks this is confusing. Please
2540- # provide a patch that a) works b) is not specific to removing
2541- # packages.
2542-
2543- msg = _('Are you sure you want to clean up?')
2544- dialog = self.gtk.MessageDialog(parent=self.widgets['window'],
2545- type=self.gtk.MESSAGE_WARNING,
2546- buttons=self.gtk.BUTTONS_NONE,
2547- message_format=msg)
2548- dialog.set_title(_('Clean up'))
2549-
2550- if packages:
2551- msg = (_('You have chosen to <b>remove %d software packages.</b> '
2552- 'Removing packages that are still needed can cause '
2553- 'errors.') %
2554- len(packages))
2555- else:
2556- msg = _('Do you want to continue?')
2557- dialog.format_secondary_markup(msg)
2558-
2559- dialog.add_button(self.gtk.STOCK_CANCEL, self.gtk.RESPONSE_CLOSE)
2560- if others:
2561- dialog.add_button(_('Clean up'), self.gtk.RESPONSE_YES)
2562- else:
2563- dialog.add_button(_('Remove packages'), self.gtk.RESPONSE_YES)
2564-
2565- dialog.show_all()
2566- response = dialog.run()
2567- dialog.hide()
2568-
2569- return response == self.gtk.RESPONSE_YES
2570-
2571- # The rest of this class is callbacks for GTK signals.
2572-
2573- def on_about_menuitem_activate(self, *args):
2574- w = self.widgets['about_dialog']
2575- w.set_name(_('Computer Janitor'))
2576- w.set_version(computerjanitorapp.VERSION)
2577- w.show()
2578- w.run()
2579- w.hide()
2580-
2581- def on_do_button_clicked(self, *args):
2582- if self.really_cleanup():
2583- Cleaner(self).start()
2584- self.glib.timeout_add(150, self.pulse)
2585-
2586- def on_show_previously_ignored_toggled(self, menuitem):
2587- self.show_previously_ignored = menuitem.get_active()
2588- iter = self.store.get_iter_first()
2589- while iter:
2590- cruft = self.store.get_value(iter, CRUFT_COL)
2591- name = cruft.get_name()
2592- shown = (self.show_previously_ignored or
2593- not self.app.state.was_previously_ignored(name))
2594- self.store.set_value(iter, SHOW_COL, shown)
2595- iter = self.store.iter_next(iter)
2596-
2597- def treeview_size_allocate(self, treeview, *args):
2598- column = treeview.get_column(NAME_COL)
2599- name_cr = column.get_cell_renderers()[0]
2600- x, y, width, height = name_cr.get_size(treeview, None)
2601- width = column.get_width()
2602- name_cr.set_property("wrap-width", width)
2603-
2604- on_unused_treeview_size_allocate = treeview_size_allocate
2605- on_recommended_treeview_size_allocate = treeview_size_allocate
2606- on_optimize_treeview_size_allocate = treeview_size_allocate
2607-
2608- def on_quit_menuitem_activate(self, *args):
2609- self.gtk.main_quit()
2610-
2611- on_window_delete_event = on_quit_menuitem_activate
2612-
2613- def on_window_map_event(self, *args):
2614- if self.first_map:
2615- self.first_map = False
2616- self.require_root()
2617- self.require_working_apt_cache()
2618- self.show_cruft()
2619-
2620- def treeview_button_press_event(self, treeview, event):
2621- # We handle mouse button presses ourselves so that we can either
2622- # toggle the long description (button 1, typically left) or
2623- # pop up a menu (button 3, typically right).
2624- #
2625- # This is slightly tricky and probably a source of bugs.
2626- # Oh well.
2627-
2628- if event.button == 1:
2629- # Select row being clicked on. Also show/hide its long
2630- # description. But only if click is on the name
2631- # portion of the column, not the toggle button.
2632- x = int(event.x)
2633- y = int(event.y)
2634- time = event.time
2635- pathinfo = treeview.get_path_at_pos(x, y)
2636- if pathinfo:
2637- path, col, cellx, celly = pathinfo
2638- if col in self.name_cols:
2639- treeview.set_cursor(path, col, False)
2640- self.toggle_long_description(treeview)
2641- else:
2642- return False
2643- return True
2644- if event.button == 3:
2645- # Popup a menu
2646- x = int(event.x)
2647- y = int(event.y)
2648- time = event.time
2649- pathinfo = treeview.get_path_at_pos(x, y)
2650- if pathinfo:
2651- path, col, cellx, celly = pathinfo
2652- treeview.grab_focus()
2653- treeview.set_cursor(path, col, False)
2654- menu = self.popup_menus[treeview]
2655- menu.popup(None, None, None, event.button, time)
2656- return True
2657-
2658- on_unused_treeview_button_press_event = treeview_button_press_event
2659- on_recommended_treeview_button_press_event = treeview_button_press_event
2660- on_optimize_treeview_button_press_event = treeview_button_press_event
2661-
2662- def popup_menu_select_all(self, menuitem, treeview):
2663- self.foreach_set_state(treeview, True)
2664-
2665- def popup_menu_unselect_all(self, menuitem, treeview):
2666- self.foreach_set_state(treeview, False)
2667-
2668- def toggled(self, cr, path, treeview):
2669- model = treeview.get_model()
2670- filter_iter = model.get_iter(path)
2671- iter = model.convert_iter_to_child_iter(filter_iter)
2672- cruft = self.store.get_value(iter, CRUFT_COL)
2673- cruft_name = cruft.get_name()
2674- enabled = self.app.state.is_enabled(cruft.get_name())
2675- enabled = not enabled
2676- if enabled:
2677- self.app.state.enable(cruft_name)
2678- else:
2679- self.app.state.disable(cruft_name)
2680- self.app.state.save(self.options.state_file)
2681- self.store.set_value(iter, STATE_COL, enabled)
2682- self.set_sensitive_unlocked()
2683-
2684- def on_sort_by_name_toggled(self, menuitem):
2685- if menuitem.get_active():
2686- self.sort_crufts_by_current_order = self.sort_crufts_by_name
2687- else:
2688- self.sort_crufts_by_current_order = self.sort_crufts_by_size
2689- self.sort_crufts_by_current_order()
2690-
2691- def on_borednow_messagedialog_close(self, dialog):
2692- dialog.hide()
2693-
2694- def on_borednow_messagedialog_response(self, dialog, response):
2695- dialog.hide()
2696-
2697
2698=== removed file 'computerjanitorapp/ui_tests.py'
2699--- computerjanitorapp/ui_tests.py 2009-02-11 16:25:03 +0000
2700+++ computerjanitorapp/ui_tests.py 1970-01-01 00:00:00 +0000
2701@@ -1,36 +0,0 @@
2702-# ui_tests.py - unit tests for ui.py
2703-# Copyright (C) 2008, 2009 Canonical, Ltd.
2704-#
2705-# This program is free software: you can redistribute it and/or modify
2706-# it under the terms of the GNU General Public License as published by
2707-# the Free Software Foundation, version 3 of the License.
2708-#
2709-# This program is distributed in the hope that it will be useful,
2710-# but WITHOUT ANY WARRANTY; without even the implied warranty of
2711-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2712-# GNU General Public License for more details.
2713-#
2714-# You should have received a copy of the GNU General Public License
2715-# along with this program. If not, see <http://www.gnu.org/licenses/>.
2716-
2717-
2718-import unittest
2719-
2720-import computerjanitor
2721-import computerjanitorapp
2722-
2723-
2724-class UserInterfaceTests(unittest.TestCase):
2725-
2726- def setUp(self):
2727- self.ui = computerjanitorapp.UserInterface("app", "pm")
2728-
2729- def testReturnsCorrectApp(self):
2730- self.assertEqual(self.ui.app, "app")
2731-
2732- def testReturnsCorrectPluginManager(self):
2733- self.assertEqual(self.ui.pm, "pm")
2734-
2735- def testRunRaisesUnimplemented(self):
2736- self.assertRaises(computerjanitor.UnimplementedMethod,
2737- self.ui.run, None, None)
2738
2739=== added file 'computerjanitorapp/uigtk.py'
2740--- computerjanitorapp/uigtk.py 1970-01-01 00:00:00 +0000
2741+++ computerjanitorapp/uigtk.py 2010-03-10 21:40:30 +0000
2742@@ -0,0 +1,515 @@
2743+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
2744+#
2745+# This program is free software: you can redistribute it and/or modify
2746+# it under the terms of the GNU General Public License as published by
2747+# the Free Software Foundation, version 3 of the License.
2748+#
2749+# This program is distributed in the hope that it will be useful,
2750+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2751+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2752+# GNU General Public License for more details.
2753+#
2754+# You should have received a copy of the GNU General Public License
2755+# along with this program. If not, see <http://www.gnu.org/licenses/>.
2756+
2757+"""Gtk user interface for computer janitor."""
2758+
2759+from __future__ import absolute_import, unicode_literals
2760+
2761+__metaclass__ = type
2762+__all__ = [
2763+ 'UserInterface',
2764+ ]
2765+
2766+
2767+import os
2768+import gtk
2769+import dbus
2770+import glib
2771+import pango
2772+import gobject
2773+
2774+from computerjanitorapp import __version__, setup_gettext
2775+from computerjanitorapp.areyousure import AreYouSure
2776+from computerjanitorapp.store import (
2777+ ListStoreColumns, Store, optimize, unused)
2778+from computerjanitorapp.utilities import format_size
2779+from computerjanitord.service import DBUS_INTERFACE_NAME
2780+
2781+_ = setup_gettext()
2782+
2783+GLADE = '/usr/share/computer-janitor/ComputerJanitor.ui'
2784+ROOT_WIDTH = 900
2785+ROOT_HEIGHT = 500
2786+NL = '\n'
2787+
2788+# Keys are lower-cased cruft types, i.e. class name of cruft instances.
2789+ACTIONS = dict(
2790+ packagecruft=_('Package will be <b>removed</b>.'),
2791+ filecruft=_('Package will be <b>installed</b>.'),
2792+ missingpackagecruft=_('File will be <b>removed</b>.'),
2793+ )
2794+
2795+SENSITIVE_WIDGETS = (
2796+ 'do_button',
2797+ 'optimize_treeview',
2798+ 'quit_menuitem',
2799+ 'show_previously_ignored',
2800+ 'sort_by_name',
2801+ 'sort_by_size',
2802+ 'unused_treeview',
2803+ )
2804+
2805+
2806+class UserInterface:
2807+ """Implementation of the Gtk user interface."""
2808+
2809+ def __init__(self):
2810+ # Connect to the dbus service.
2811+ system_bus = dbus.SystemBus()
2812+ proxy = system_bus.get_object(DBUS_INTERFACE_NAME, '/')
2813+ self.janitord = dbus.Interface(
2814+ proxy, dbus_interface=DBUS_INTERFACE_NAME)
2815+ # Create the model on which the TreeViews will be based.
2816+ self.store = Store(self.janitord)
2817+ self.popup_menus = {}
2818+ self.cruft_name_columns = set()
2819+ # Sort by name by default.
2820+ self._sort_key = self._by_name
2821+ # Work is happening asynchronously on the dbus service.
2822+ self.working = False
2823+ # Connect to the signal the server will emit when cleaning up.
2824+ self.janitord.connect_to_signal('cleanup_status', self._clean_working)
2825+
2826+ def run(self):
2827+ """Set up the widgets and run the main loop."""
2828+ builder = gtk.Builder()
2829+ builder.set_translation_domain('computerjanitor')
2830+ # Load the glade ui file, which can be overridden from the environment
2831+ # for testing purposes.
2832+ glade_file = os.environ.get('COMPUTER_JANITOR_GLADE', GLADE)
2833+ builder.add_from_file(glade_file)
2834+ # Bind widgets to callbacks.
2835+ self.find_and_bind_widgets(builder)
2836+ # Do the initial search for cruft and set up the TreeView model.
2837+ self.store.find_cruft()
2838+ self.sort_cruft()
2839+ # Now hook the TreeViews up to there view of the model.
2840+ self.create_column('unused_treeview', unused)
2841+ self.create_column('optimize_treeview', optimize)
2842+ # Set the dimensions of the root window.
2843+ root = self.widgets['window']
2844+ root.set_default_size(ROOT_WIDTH, ROOT_HEIGHT)
2845+ # Map the root window and go!
2846+ root.show()
2847+ gtk.main()
2848+
2849+ def find_and_bind_widgets(self, builder):
2850+ """Bind widgets and callbacks."""
2851+ # Start by extracting all the bindable widgets from the ui builder,
2852+ # keeping track of them as mapped to their name.
2853+ self.widgets = {}
2854+ for ui_object in builder.get_objects():
2855+ if issubclass(type(ui_object), gtk.Buildable):
2856+ widget_name = gtk.Buildable.get_name(ui_object)
2857+ self.widgets[widget_name] = ui_object
2858+ # Search through the attributes of this instance looking for
2859+ # callbacks for this widget. We use the naming convention
2860+ # 'on_<widget>_<signal>' for such methods. Both the widget
2861+ # name and signal (or event) can contain underscores. Connect
2862+ # the widget to the callback.
2863+ prefix = 'on_{0}_'.format(widget_name)
2864+ for method_name in dir(self):
2865+ if method_name.startswith(prefix):
2866+ signal_name = method_name[len(prefix):]
2867+ ui_object.connect(
2868+ signal_name, getattr(self, method_name))
2869+
2870+ def create_column(self, widget_name, filter_func):
2871+ """Set up a column in the named TreeView."""
2872+ treeview = self.widgets[widget_name]
2873+ # XXX 2010-03-04 barry: This is a bit of an abuse because it's
2874+ # supposed to specify whether users are to read across the rows. As a
2875+ # side effect it renders the columns with alternating row colors, but
2876+ # that's not it's primary function.
2877+ treeview.set_rules_hint(True)
2878+ # Each TreeView contains two columns. The leftmost one is a toggle
2879+ # that when select tells c-j to act on that cruft. Deselecting the
2880+ # toggle ignores the package for next time.
2881+ toggle_cr = gtk.CellRendererToggle()
2882+ toggle_cr.connect('toggled', self._toggled, treeview)
2883+ toggle_cr.set_property('yalign', 0)
2884+ toggle_col = gtk.TreeViewColumn()
2885+ toggle_col.pack_start(toggle_cr)
2886+ toggle_col.add_attribute(toggle_cr, 'active', ListStoreColumns.active)
2887+ treeview.append_column(toggle_col)
2888+ # The rightmost column contains the details of the cruft. It will
2889+ # always contain the cruft name and can be expanded to display cruft
2890+ # details. Tell the column to get its toggle's active state from the
2891+ # model.
2892+ name_cr = gtk.CellRendererText()
2893+ name_cr.set_property('yalign', 0)
2894+ name_cr.set_property('wrap-mode', pango.WRAP_WORD)
2895+ name_col = gtk.TreeViewColumn()
2896+ name_col.pack_start(name_cr)
2897+ name_col.add_attribute(name_cr, 'markup', ListStoreColumns.text)
2898+ treeview.append_column(name_col)
2899+ self.cruft_name_columns.add(name_col)
2900+ # The individual crufts may or may not be visible in this TreeView.
2901+ # It's the filter function that controls this, so set that now.
2902+ filter_store = self.store.filter_new()
2903+ filter_store.set_visible_func(filter_func)
2904+ treeview.set_model(filter_store)
2905+ # Each TreeView has a popup menu for select or deselecting all visible
2906+ # cruft.
2907+ self.create_popup_menu_for_treeview(treeview)
2908+
2909+ def create_popup_menu_for_treeview(self, treeview):
2910+ """The tree views have a popup menu to select/deselect everything.
2911+
2912+ :param treeview: The `TreeView` to attach the menu to.
2913+ """
2914+ select_all = gtk.MenuItem(label='Select all')
2915+ select_all.connect('activate', self.popup_menu_select_all, treeview)
2916+ unselect_all = gtk.MenuItem(label='Unselect all')
2917+ unselect_all.connect('activate',
2918+ self.popup_menu_unselect_all, treeview)
2919+ menu = gtk.Menu()
2920+ menu.append(select_all)
2921+ menu.append(unselect_all)
2922+ menu.show_all()
2923+ self.popup_menus[treeview] = menu
2924+
2925+ def _by_name(self, cruft_name):
2926+ """Sort by cruft name."""
2927+ return cruft_name
2928+
2929+ def _by_size(self, cruft_name):
2930+ """Sort by cruft size, from largest to smallest."""
2931+ # Return negative size to sort from largest to smallest.
2932+ return -self.janitord.get_details(cruft_name)[1]
2933+
2934+ def sort_cruft(self):
2935+ """Sort the cruft displays, either by name or size."""
2936+ # The way reordering (not technically 'sorting') works in gtk is that
2937+ # you give a list of integer indexes to the the ListStore. These
2938+ # indexes are in sorted order, and refer to the pre-sort indexes of
2939+ # the items in the store. IOW, the ListStore knows that if the first
2940+ # integer in the list is 7, it will move the 7th item to the top.
2941+ #
2942+ # Start by getting the indexes and names of the currenly sorted cruft.
2943+ # We'll fill this list with 2-tuples of the format:
2944+ # (sort-key, current-index).
2945+ cruft_data = []
2946+ def get(model, path, iter, crufts):
2947+ cruft_name = model.get_value(iter, ListStoreColumns.name)
2948+ crufts.append((self._sort_key(cruft_name), len(crufts)))
2949+ # Continue iterating.
2950+ return False
2951+ self.store.foreach(get, cruft_data)
2952+ cruft_data.sort()
2953+ cruft_indexes = [index for key, index in cruft_data]
2954+ self.store.reorder(cruft_indexes)
2955+
2956+ def get_cleanable_cruft(self):
2957+ """Return the list of cleanable cruft candidates.
2958+
2959+ :return: List of cleanable cruft.
2960+ :rtype: list of 2-tuples of (cruft_name, is_package_cruft)
2961+ """
2962+ cleanable_cruft = []
2963+ def collect(model, path, iter, crufts):
2964+ # Only clean up active cruft, i.e. those that are specifically
2965+ # checked as ready for cleaning.
2966+ cruft_active = model.get_value(iter, ListStoreColumns.active)
2967+ if cruft_active:
2968+ cruft_name = model.get_value(iter, ListStoreColumns.name)
2969+ cruft_is_package_cruft = model.get_value(
2970+ iter, ListStoreColumns.is_package_cruft)
2971+ crufts.append((cruft_name, cruft_is_package_cruft))
2972+ # Continue iterating.
2973+ return False
2974+ self.store.foreach(collect, cleanable_cruft)
2975+ return cleanable_cruft
2976+
2977+ def toggle_long_description(self, treeview):
2978+ """Toggle the currently selected cruft's long description.
2979+
2980+ :param treeview: The TreeView
2981+ """
2982+ selection = treeview.get_selection()
2983+ filter_model, selected = selection.get_selected()
2984+ if not selected:
2985+ return
2986+ model = filter_model.get_model()
2987+ iter = filter_model.convert_iter_to_child_iter(selected)
2988+ cruft_name = model.get_value(iter, ListStoreColumns.name)
2989+ expanded = model.get_value(iter, ListStoreColumns.expanded)
2990+ shortname = model.get_value(iter, ListStoreColumns.short_name)
2991+ if expanded:
2992+ # Collapse it.
2993+ value = gobject.markup_escape_text(shortname)
2994+ else:
2995+ cruft_type, size = self.janitord.get_details(cruft_name)
2996+ lines = [gobject.markup_escape_text(shortname)]
2997+ action = ACTIONS.get(cruft_type.lower())
2998+ if action is not None:
2999+ lines.append(action)
3000+ lines.append('Size: {0}'.format(format_size(size)))
3001+ lines.append('')
3002+ description = self.janitord.get_description(cruft_name)
3003+ lines.append(gobject.markup_escape_text(description))
3004+ value = NL.join(lines)
3005+ model.set_value(iter, ListStoreColumns.text, value)
3006+ model.set_value(iter, ListStoreColumns.expanded, not expanded)
3007+
3008+ def desensitize(self):
3009+ """Make certain ui elements insensitive during work."""
3010+ for widget in SENSITIVE_WIDGETS:
3011+ self.widgets[widget].set_sensitive(False)
3012+
3013+ def sensitize(self):
3014+ """Make certain ui elements sensitive after work."""
3015+ for widget in SENSITIVE_WIDGETS:
3016+ self.widgets[widget].set_sensitive(True)
3017+
3018+ # Popup menu support.
3019+
3020+ def popup_menu_foreach_set_state(self, treeview, enabled):
3021+ """Set the state of the cruft 'active' flag for all cruft.
3022+
3023+ :param treeview: The `TreeView` to set cruft state on.
3024+ :param enabled: The new state flag for all cruft. True means enabled.
3025+ :type enabled: bool
3026+ """
3027+ def set_state(model, path, iter, user_data):
3028+ # Set the state on an individual piece of cruft. Start by
3029+ # changing the state of the cruft on the dbus service.
3030+ child_iter = model.convert_iter_to_child_iter(iter)
3031+ cruft_name = self.store.get_value(
3032+ child_iter, ListStoreColumns.name)
3033+ if enabled:
3034+ self.janitord.unignore(cruft_name)
3035+ else:
3036+ self.janitord.ignore(cruft_name)
3037+ # Now set the active state in the model.
3038+ self.store.set_value(child_iter, ListStoreColumns.active, enabled)
3039+ treeview.get_model().foreach(set_state, None)
3040+ # Save the updated state on the dbus service.
3041+ self.janitord.save()
3042+
3043+ def popup_menu_select_all(self, menuitem, treeview):
3044+ self.popup_menu_foreach_set_state(treeview, True)
3045+
3046+ def popup_menu_unselect_all(self, menuitem, treeview):
3047+ self.popup_menu_foreach_set_state(treeview, False)
3048+
3049+ # Progress bar
3050+
3051+ def pulse(self):
3052+ """Progress bar callback, showing that something is happening."""
3053+ progress = self.widgets['progressbar_status']
3054+ if self.working:
3055+ progress.show()
3056+ progress.pulse()
3057+ return True
3058+ else:
3059+ # All done. Hide the progress bar, make the ui elements sensitive
3060+ # again, update the store, and kill the timer.
3061+ progress.hide()
3062+ self.store.clear()
3063+ self.store.find_cruft()
3064+ self.sensitize()
3065+ return False
3066+
3067+ def _clean_working(self, cruft):
3068+ """dbus signal handler; the 'clean' operation is in progress.
3069+
3070+ :param done: The cruft that is being cleaned up.
3071+ :type done: string
3072+ """
3073+ # Just mark the status here. The progress bar pulsar will handle
3074+ # doing the actual work.
3075+ self.working = (cruft != '')
3076+ if self.working:
3077+ self.widgets['progressbar_status'].set_text(
3078+ _('Processing {0}').format(cruft))
3079+
3080+ # Callbacks
3081+
3082+ def _toggled(self, widget, path, treeview):
3083+ """Handle the toggle button in a TreeView cell.
3084+
3085+ :param widget: The CellRendererToggle
3086+ :param path: The cell's path.
3087+ :param treeview: The TreeView
3088+ """
3089+ # Find out which cruft's toggle was clicked.
3090+ model = treeview.get_model()
3091+ filter_iter = model.get_iter(path)
3092+ child_iter = model.convert_iter_to_child_iter(filter_iter)
3093+ cruft_name = self.store.get_value(child_iter, ListStoreColumns.name)
3094+ state = self.store.get_value(child_iter, ListStoreColumns.active)
3095+ # Toggle the current state.
3096+ new_state = not state
3097+ if new_state:
3098+ self.janitord.unignore(cruft_name)
3099+ else:
3100+ self.janitord.ignore(cruft_name)
3101+ self.store.set_value(child_iter, ListStoreColumns.active, new_state)
3102+ self.store.set_value(
3103+ child_iter, ListStoreColumns.server_ignored, not new_state)
3104+ # Save the new ignored state on the dbus service.
3105+ self.janitord.save()
3106+
3107+ # Signal and event handlers.
3108+
3109+ def on_quit_menuitem_activate(self, *args):
3110+ """Signal and event handlers for quitting.
3111+
3112+ Since we just want things to go away, we don't really care about the
3113+ arguments. Just tell the main loop to exit.
3114+ """
3115+ # Don't quit while we're working.
3116+ if self.working:
3117+ return True
3118+ gtk.main_quit()
3119+
3120+ on_window_delete_event = on_quit_menuitem_activate
3121+
3122+ def treeview_button_press_event(self, treeview, event):
3123+ """Handle mouse button press events on the TreeView ourselves.
3124+
3125+ We handle mouse button presses ourselves so that we can either
3126+ toggle the long description (button 1, typically left) or
3127+ pop up a menu (button 3, typically right).
3128+ """
3129+ # Original comment: This is slightly tricky and probably a source of
3130+ # bugs. Oh well.
3131+ if event.button == 1:
3132+ # Left button event. Select the row being clicked on. If the
3133+ # click is on the cruft name, show or hide its long description.
3134+ # If the click the click is elsewhere do not handle it. This
3135+ # allows the toggle button event to be handled separately.
3136+ x = int(event.x)
3137+ y = int(event.y)
3138+ time = event.time
3139+ pathinfo = treeview.get_path_at_pos(x, y)
3140+ if pathinfo is None:
3141+ # The click was not in a cell, but we've handled it anyway.
3142+ return True
3143+ path, column, cell_x, cell_y = pathinfo
3144+ if column in self.cruft_name_columns:
3145+ treeview.set_cursor(path, column, False)
3146+ self.toggle_long_description(treeview)
3147+ return True
3148+ else:
3149+ # We are not handling this event so that the toggle button
3150+ # handling can occur.
3151+ return False
3152+ elif event.button == 3:
3153+ # Right button event. Pop up the select/deselect all menu.
3154+ treeview.grab_focus()
3155+ x = int(event.x)
3156+ y = int(event.y)
3157+ time = event.time
3158+ pathinfo = treeview.get_path_at_pos(x, y)
3159+ if pathinfo is not None:
3160+ path, column, cell_x, cell_y = pathinfo
3161+ treeview.set_cursor(path, column, False)
3162+ menu = self.popup_menus[treeview]
3163+ menu.popup(None, None, None, event.button, time)
3164+ return True
3165+ else:
3166+ # No other events are handled by us.
3167+ return False
3168+
3169+ # The actual event handler is totally generic. Alias it to names
3170+ # recognized by the automatic event binding scheme.
3171+ on_unused_treeview_button_press_event = treeview_button_press_event
3172+ on_optimize_treeview_button_press_event = treeview_button_press_event
3173+
3174+ def treeview_size_allocate(self, treeview, *args):
3175+ """Allocate space for the tree view and set wrap width.
3176+
3177+ :param treeview: The TreeView
3178+ :param args: Additional ignored positional arguments
3179+ """
3180+ # Get the rightmost of the two columns in the TreeView, i.e. the one
3181+ # containing the text.
3182+ column = treeview.get_column(1)
3183+ name_cr = column.get_cell_renderers()[0]
3184+ # Wrap to the entire width of the column.
3185+ width = column.get_width()
3186+ name_cr.set_property('wrap-width', width)
3187+
3188+ on_unused_treeview_size_allocate = treeview_size_allocate
3189+ on_optimize_treeview_size_allocate = treeview_size_allocate
3190+
3191+ def on_sort_by_name_toggled(self, menuitem):
3192+ """Reorder the crufts to be sorted by name or size."""
3193+ if menuitem.get_active():
3194+ self._sort_key = self._by_name
3195+ else:
3196+ self._sort_key = self._by_size
3197+ self.sort_cruft()
3198+
3199+ def on_about_menuitem_activate(self, *args):
3200+ dialog = self.widgets['about_dialog']
3201+ dialog.set_name(_('Computer Janitor'))
3202+ dialog.set_version(__version__)
3203+ dialog.show()
3204+ dialog.run()
3205+ dialog.hide()
3206+
3207+ def on_show_previously_ignored_toggled(self, menuitem):
3208+ """Show all cruft, even those being ignored.
3209+
3210+ Normally, we only show cruft that wasn't explicitly ignored. By
3211+ toggling this menu item, the janitor can also display cruft that is
3212+ marked as ignored on the dbus service.
3213+ """
3214+ show_ignored_cruft = menuitem.get_active()
3215+ iter = self.store.get_iter_first()
3216+ while iter:
3217+ cruft_name = self.store.get_value(iter, ListStoreColumns.name)
3218+ server_ignored = self.store.get_value(
3219+ iter, ListStoreColumns.server_ignored)
3220+ show = (show_ignored_cruft or not server_ignored)
3221+ self.store.set_value(iter, ListStoreColumns.show, show)
3222+ iter = self.store.iter_next(iter)
3223+
3224+ def on_do_button_clicked(self, *args):
3225+ """JFDI, well almost."""
3226+ self.count = 0
3227+ response = AreYouSure(self).verify()
3228+ if not response:
3229+ return
3230+ # This can take a long time. Make an asynchronous call to the dbus
3231+ # service and arrange for it to occasionally provide us with status.
3232+ # This isn't great ui, but OTOH, the package cruft cleaners themselves
3233+ # don't provide much granularity, so there's little we can do anyway
3234+ # without a major rewrite of the plugin architecture.
3235+ self.working = True
3236+ glib.timeout_add(150, self.pulse)
3237+ # Make various ui elements insensitive.
3238+ self.desensitize()
3239+ cleanable = [cruft for cruft, ispkg in self.get_cleanable_cruft()]
3240+ # XXX 2010-03-08 barry: Do better than this.
3241+ def error(exception):
3242+ print exception
3243+ self.working = False
3244+ def reply():
3245+ pass
3246+ # Make the asynchronous call because this can take a long time. We'll
3247+ # get status updates periodically. Note however that even though this
3248+ # is asynchronous, dbus still expects a response within a certain
3249+ # amount of time. We have no idea how long it will take to clean up
3250+ # the cruft though, so just crank the timeout up to some insanely huge
3251+ # number (of seconds).
3252+ self.widgets['progressbar_status'].set_text('Authenticating...')
3253+ self.janitord.clean(cleanable,
3254+ reply_handler=reply,
3255+ error_handler=error,
3256+ # If it takes longer than an hour, we're screwed.
3257+ timeout=3600)
3258
3259=== added file 'computerjanitorapp/utilities.py'
3260--- computerjanitorapp/utilities.py 1970-01-01 00:00:00 +0000
3261+++ computerjanitorapp/utilities.py 2010-03-10 21:40:30 +0000
3262@@ -0,0 +1,51 @@
3263+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
3264+#
3265+# This program is free software: you can redistribute it and/or modify
3266+# it under the terms of the GNU General Public License as published by
3267+# the Free Software Foundation, version 3 of the License.
3268+#
3269+# This program is distributed in the hope that it will be useful,
3270+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3271+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3272+# GNU General Public License for more details.
3273+#
3274+# You should have received a copy of the GNU General Public License
3275+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3276+
3277+"""Common utilities."""
3278+
3279+from __future__ import absolute_import, unicode_literals
3280+
3281+__metaclass__ = type
3282+__all__ = [
3283+ 'format_size',
3284+ ]
3285+
3286+
3287+from math import log10
3288+
3289+
3290+TABLE = (
3291+ 'B',
3292+ 'kB',
3293+ 'MB',
3294+ 'GB',
3295+ 'TB',
3296+ )
3297+
3298+
3299+def format_size(bytes):
3300+ """Format size in bytes.
3301+
3302+ :param bytes: Integer size in bytes.
3303+ :type bytes: integer
3304+ :return: Formatted size
3305+ :rtype: string
3306+ """
3307+ assert bytes >= 0, 'Cannot have negative sizes'
3308+ if bytes == 0:
3309+ return '0B'
3310+ if bytes > 10**12:
3311+ return '>1TB'
3312+ key = divmod(int(log10(bytes)), 3)[0]
3313+ return '{0}{1}'.format(bytes // 10**(key * 3), TABLE[key])
3314
3315=== added directory 'computerjanitord'
3316=== added file 'computerjanitord/__init__.py'
3317=== added file 'computerjanitord/application.py'
3318--- computerjanitord/application.py 1970-01-01 00:00:00 +0000
3319+++ computerjanitord/application.py 2010-03-10 21:40:30 +0000
3320@@ -0,0 +1,111 @@
3321+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
3322+#
3323+# This program is free software: you can redistribute it and/or modify
3324+# it under the terms of the GNU General Public License as published by
3325+# the Free Software Foundation, version 3 of the License.
3326+#
3327+# This program is distributed in the hope that it will be useful,
3328+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3329+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3330+# GNU General Public License for more details.
3331+#
3332+# You should have received a copy of the GNU General Public License
3333+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3334+
3335+"""Application interface for use by plugins.
3336+
3337+This primarily makes certain apt functionality available to the plugin.
3338+"""
3339+
3340+from __future__ import absolute_import, unicode_literals
3341+
3342+__metaclass__ = type
3343+__all__ = [
3344+ 'Application',
3345+ 'SourcesListError',
3346+ ]
3347+
3348+
3349+import apt
3350+
3351+import computerjanitor
3352+import computerjanitorapp
3353+
3354+_ = computerjanitorapp.setup_gettext()
3355+
3356+SYNTAPTIC_PREFERENCES_FILE = '/var/lib/synaptic/preferences'
3357+# There really isn't anything special about these packages. These really just
3358+# represent landmarks in the package namespace that we look for to try to
3359+# judge the sanity of the apt cache. If these are missing, things are really
3360+# messed up and we can't actually figure out how to continue.
3361+LANDMARK_PACKAGES = [
3362+ 'dash',
3363+ 'gzip',
3364+ ]
3365+
3366+
3367+class LandmarkPackageError(computerjanitor.Exception):
3368+ """Base class for problems with the landmark packages."""
3369+
3370+ _errmsg = None
3371+
3372+ def __init__(self, package):
3373+ self.package = package
3374+
3375+ def __str__(self):
3376+ # gettext translation needs to be called at run time.
3377+ return _(self._errmsg).format(self)
3378+
3379+
3380+class MissingLandmarkError(LandmarkPackageError):
3381+ """A landmark package could not be found."""
3382+
3383+ _errmsg = _('Landmark package {0.package} is missing')
3384+
3385+
3386+class NonDownloadableError(LandmarkPackageError):
3387+ """A landmark package is not downloadable."""
3388+
3389+ _errmsg = _('Landmark package {0.package} is not downloadable')
3390+
3391+
3392+class Application:
3393+ """Interface for plugins requesting apt actions."""
3394+
3395+ def __init__(self, apt_cache=None):
3396+ """Create the application interface.
3397+
3398+ :param apt_cache: Alternative apt cache for testing purposes. When
3399+ `None` use the default apt cache.
3400+ """
3401+ if apt_cache is None:
3402+ # Use the real apt cache.
3403+ self.apt_cache = apt.Cache()
3404+ else:
3405+ self.apt_cache = apt_cache
3406+ self.refresh_apt_cache()
3407+
3408+ def refresh_apt_cache(self):
3409+ """Refresh the apt cache.
3410+
3411+ This API is used by plugins.
3412+ """
3413+ self.apt_cache.open()
3414+ # For historical purposes, Synaptic has a different way of pinning
3415+ # packages than apt, so we have to load its preferences file in order
3416+ # to know what it's pinning.
3417+ self.apt_cache._depcache.ReadPinFile(SYNTAPTIC_PREFERENCES_FILE)
3418+
3419+ def verify_apt_cache(self):
3420+ """Verify that essential packages are available in the apt cache.
3421+
3422+ This API is used by plugins.
3423+
3424+ :raises SourcesListProblem: when an essential package is not
3425+ available.
3426+ """
3427+ for name in LANDMARK_PACKAGES:
3428+ if name not in self.apt_cache:
3429+ raise MissingLandmarkError(name)
3430+ if not any(v.downloadable for v in self.apt_cache[name].versions):
3431+ raise NonDownloadableError(name)
3432
3433=== added file 'computerjanitord/authenticator.py'
3434--- computerjanitord/authenticator.py 1970-01-01 00:00:00 +0000
3435+++ computerjanitord/authenticator.py 2010-03-10 21:40:30 +0000
3436@@ -0,0 +1,85 @@
3437+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
3438+#
3439+# This program is free software: you can redistribute it and/or modify
3440+# it under the terms of the GNU General Public License as published by
3441+# the Free Software Foundation, version 3 of the License.
3442+#
3443+# This program is distributed in the hope that it will be useful,
3444+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3445+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3446+# GNU General Public License for more details.
3447+#
3448+# You should have received a copy of the GNU General Public License
3449+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3450+
3451+"""Authentication for Computer Janitor backend services."""
3452+
3453+from __future__ import absolute_import, unicode_literals
3454+
3455+__metaclass__ = type
3456+__all__ = [
3457+ 'Authenticator',
3458+ ]
3459+
3460+
3461+import dbus
3462+
3463+
3464+PK_AUTHORITY_BUS_NAME = 'org.freedesktop.PolicyKit1'
3465+PK_AUTHORITY_OBJECT_PATH = '/org/freedesktop/PolicyKit1/Authority'
3466+PK_AUTHORITY_INTERFACE = 'org.freedesktop.PolicyKit1.Authority'
3467+# From the PolicyKit API.
3468+# http://hal.freedesktop.org/docs/polkit/
3469+# eggdbus-interface-org.freedesktop.PolicyKit1.Authority.html
3470+AllowUserInteraction = 0x00000001
3471+
3472+
3473+class Authenticator:
3474+ """PolicyKit authenticator."""
3475+
3476+ def authenticate(self, sender, connection, privilege):
3477+ """Authenticate with PolicyKit.
3478+
3479+ :param sender: The initiator of the action.
3480+ :param connection: The dbus connection that initiated the action.
3481+ :param privilege: The privilege being requested.
3482+ :return: Whether the subject is authorized or not.
3483+ :rtype: bool
3484+ """
3485+ policykit = self._get_policykit_proxy()
3486+ sender_pid = self._get_sender_pid(connection, sender)
3487+ # This is the CheckAuthorization() 'subject' structure.
3488+ subject = (
3489+ 'unix-process', {
3490+ 'pid': sender_pid,
3491+ 'start-time': 0,
3492+ })
3493+ # No details or cancellation_id needed.
3494+ details = {'': ''}
3495+ cancellation_id = ''
3496+ flags = AllowUserInteraction
3497+ # CheckAuthorization returns an AuthorizationResult structure, modeled
3498+ # as a 3-tuple. The only thing we care about though is the boolean
3499+ # describing whether we got authorized or not.
3500+ is_authorized, is_challenge, details = policykit.CheckAuthorization(
3501+ subject, privilege, details, flags, cancellation_id)
3502+ return is_authorized
3503+
3504+ def _get_policykit_proxy(self):
3505+ """Contact the system bus to get a PolicyKit proxy."""
3506+ system_bus = dbus.SystemBus()
3507+ pk_proxy = system_bus.get_object(
3508+ PK_AUTHORITY_BUS_NAME, PK_AUTHORITY_OBJECT_PATH)
3509+ return dbus.Interface(pk_proxy, PK_AUTHORITY_INTERFACE)
3510+
3511+ def _get_sender_pid(self, connection, sender):
3512+ """Contact the system bus to get the sender connection PID."""
3513+ # Since we're going to authorize a Unix process, we need to get the
3514+ # sender's process id. This is available on the dbus. The
3515+ # CheckAuthorization() method also requires us to have a start-time,
3516+ # but it's not clear what the semantics are for that, so we'll just
3517+ # put a zero there.
3518+ db_proxy = connection.get_object(
3519+ dbus.BUS_DAEMON_NAME, dbus.BUS_DAEMON_PATH, introspect=False)
3520+ info = dbus.Interface(db_proxy, dbus.BUS_DAEMON_IFACE)
3521+ return info.GetConnectionUnixProcessID(sender)
3522
3523=== added file 'computerjanitord/collector.py'
3524--- computerjanitord/collector.py 1970-01-01 00:00:00 +0000
3525+++ computerjanitord/collector.py 2010-03-10 21:40:30 +0000
3526@@ -0,0 +1,149 @@
3527+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
3528+#
3529+# This program is free software: you can redistribute it and/or modify
3530+# it under the terms of the GNU General Public License as published by
3531+# the Free Software Foundation, version 3 of the License.
3532+#
3533+# This program is distributed in the hope that it will be useful,
3534+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3535+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3536+# GNU General Public License for more details.
3537+#
3538+# You should have received a copy of the GNU General Public License
3539+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3540+
3541+"""A cruft collector."""
3542+
3543+from __future__ import absolute_import, unicode_literals
3544+
3545+__metaclass__ = type
3546+__all__ = [
3547+ 'Collector',
3548+ ]
3549+
3550+
3551+import os
3552+import time
3553+import logging
3554+
3555+from computerjanitor import PluginManager
3556+from computerjanitord.errors import DuplicateCruftError
3557+from computerjanitord.whitelist import Whitelist
3558+
3559+
3560+log = logging.getLogger('computerjanitor')
3561+MISSING = object()
3562+DEFAULT_PLUGINS_DIRS = "/usr/share/computerjanitor/plugins"
3563+
3564+# For testing purposes.
3565+SLEEPY_TIME = float(os.environ.get('COMPUTER_JANITOR_SLEEPY_TIME', '1.0'))
3566+
3567+
3568+class Collector:
3569+ """A cruft collector."""
3570+
3571+ def __init__(self, application, plugin_manager_class=None,
3572+ whitelist_dirs=None, service=None):
3573+ """Create a cruft collector.
3574+
3575+ :param application: The `Application` class.
3576+ :type application: This object is used by plugins to access the apt
3577+ database. It must have an attribute named `apt_cache` and a
3578+ method named `refresh_apt_cache()`.
3579+ :param plugin_manager_class: The plugin manager class. If None (the
3580+ default), then `computerjanitor.PluginManager` is used.
3581+ :type plugin_manager_class: callable accepting a single argument,
3582+ which is a sequence of plugin directories.
3583+ :param whitelist_dirs: Sequence of directories to search for
3584+ '.whitelist' files. Passed directly to
3585+ `computerjanitord.whitelist.Whitelist`.
3586+ :param service: The dbus service; when doing plugin post-cleanup, this
3587+ will be used to emit a progress signal.
3588+ """
3589+ self.application = application
3590+ self.service = service
3591+ self.whitelist = Whitelist(whitelist_dirs)
3592+ # Keep track of cruft and map between the cruft's name and its Cruft
3593+ # instance. We'll use the latter when cruft cleanup is requested
3594+ # through the dbus API.
3595+ self.cruft = None
3596+ self.cruft_by_name = None
3597+ # Set up the plugin manager.
3598+ plugin_path = os.environ.get('COMPUTER_JANITOR_PLUGINS',
3599+ DEFAULT_PLUGINS_DIRS)
3600+ plugin_dirs = plugin_path.split(':')
3601+ if plugin_manager_class is None:
3602+ plugin_manager_class = PluginManager
3603+ self.plugin_manager = plugin_manager_class(application, plugin_dirs)
3604+ self.load()
3605+
3606+ def load(self):
3607+ """Reload all cruft."""
3608+ self.cruft = []
3609+ self.cruft_by_name = {}
3610+ # Ask all the plugins to find their cruft, filtering out whitelisted
3611+ # cruft.
3612+ for plugin in self.plugin_manager.get_plugins():
3613+ for cruft in plugin.get_cruft():
3614+ if not self.whitelist.is_whitelisted(cruft):
3615+ # Different plugins can give us duplicate cruft names,
3616+ # however the Cruft class better be the same, otherwise we
3617+ # won't actually know how to map the name back to a cruft
3618+ # instance for proper cleanup.
3619+ if cruft.get_name() in self.cruft_by_name:
3620+ my_cruft = self.cruft_by_name[cruft.get_name()]
3621+ if cruft.__class__ is my_cruft.__class__:
3622+ # We only need one instance of this cruft.
3623+ continue
3624+ else:
3625+ raise DuplicateCruftError(cruft.get_name())
3626+ #print ' ', cruft.get_name()
3627+ self.cruft.append(cruft)
3628+ self.cruft_by_name[cruft.get_name()] = cruft
3629+
3630+ def clean(self, names, dry_run=False):
3631+ """Clean up the named cruft.
3632+
3633+ :param names: The names of the cruft to clean up.
3634+ :type names: list of strings
3635+ :param dry_run: Flag indicating whether to do permanent changes.
3636+ :type dry_run: bool
3637+ """
3638+ # Ensure that all named cruft is known.
3639+ for name in names:
3640+ cruft = self.cruft_by_name.get(name, MISSING)
3641+ if cruft is MISSING:
3642+ log.error('No such cruft: {0}'.format(name))
3643+ raise NoSuchCruftError(name)
3644+ log.info('cleaning cruft: {0}'.format(cruft.get_name()))
3645+ if not dry_run:
3646+ cruft.cleanup()
3647+ # Do plugin-specific post-cleanup.
3648+ for plugin in self.plugin_manager.get_plugins():
3649+ logging.info('post-cleanup: {0}'.format(plugin))
3650+ if self.service is not None:
3651+ # Notify the client that we're not done yet.
3652+ #
3653+ # 2010-02-09 barry: this actually kind of sucks because the
3654+ # granularity is too coarse. Some plugins will post_cleanup()
3655+ # very quickly, others will take a long time. Unfortunately,
3656+ # the computerjanitor.Plugin API doesn't support a more
3657+ # granular feedback. Plugin.get_plugin(..., callback=foo)
3658+ # doesn't really cut it because that only gets called during
3659+ # get_plugins().
3660+ self.service.cleanup_status(plugin.__class__.__name__)
3661+ if dry_run:
3662+ # For testing purposes.
3663+ time.sleep(SLEEPY_TIME)
3664+ else:
3665+ try:
3666+ plugin.post_cleanup()
3667+ except Exception:
3668+ logging.exception('plugin: {0}'.format(plugin))
3669+ # Keep going.
3670+ # Now we're done.
3671+ if self.service is not None:
3672+ self.service.cleanup_status('')
3673+ # Reload list of crufts.
3674+ self.application.refresh_apt_cache()
3675+ self.load()
3676
3677=== added directory 'computerjanitord/data'
3678=== added file 'computerjanitord/data/com.ubuntu.ComputerJanitor.conf'
3679--- computerjanitord/data/com.ubuntu.ComputerJanitor.conf 1970-01-01 00:00:00 +0000
3680+++ computerjanitord/data/com.ubuntu.ComputerJanitor.conf 2010-03-10 21:40:30 +0000
3681@@ -0,0 +1,15 @@
3682+<!DOCTYPE busconfig PUBLIC
3683+ "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
3684+ "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
3685+<busconfig>
3686+ <policy user="root">
3687+ <allow own="com.ubuntu.ComputerJanitor"/>
3688+ </policy>
3689+
3690+ <policy context="default">
3691+ <allow send_interface="com.ubuntu.ComputerJanitor"/>
3692+ <allow receive_interface="com.ubuntu.ComputerJanitor"
3693+ receive_sender="com.ubuntu.ComputerJanitor"/>
3694+ </policy>
3695+
3696+</busconfig>
3697
3698=== added file 'computerjanitord/data/com.ubuntu.computerjanitor.policy'
3699--- computerjanitord/data/com.ubuntu.computerjanitor.policy 1970-01-01 00:00:00 +0000
3700+++ computerjanitord/data/com.ubuntu.computerjanitor.policy 2010-03-10 21:40:30 +0000
3701@@ -0,0 +1,19 @@
3702+<?xml version="1.0" encoding="UTF-8"?>
3703+<!DOCTYPE policyconfig PUBLIC
3704+ "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
3705+ "http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">
3706+<policyconfig>
3707+
3708+ <vendor>ComputerJanitor</vendor>
3709+ <vendor_url>https://launchpad.net/computer-janitor</vendor_url>
3710+
3711+ <action id="com.ubuntu.computerjanitor.updatesystem">
3712+ <description>Clean up packages that are no longer necessary</description>
3713+ <message>Removing unused packages requires authentication</message>
3714+ <defaults>
3715+ <allow_inactive>no</allow_inactive>
3716+ <allow_active>auth_admin_keep</allow_active>
3717+ </defaults>
3718+ </action>
3719+
3720+</policyconfig>
3721
3722=== added file 'computerjanitord/errors.py'
3723--- computerjanitord/errors.py 1970-01-01 00:00:00 +0000
3724+++ computerjanitord/errors.py 2010-03-10 21:40:30 +0000
3725@@ -0,0 +1,57 @@
3726+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
3727+#
3728+# This program is free software: you can redistribute it and/or modify
3729+# it under the terms of the GNU General Public License as published by
3730+# the Free Software Foundation, version 3 of the License.
3731+#
3732+# This program is distributed in the hope that it will be useful,
3733+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3734+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3735+# GNU General Public License for more details.
3736+#
3737+# You should have received a copy of the GNU General Public License
3738+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3739+
3740+"""Exceptions for the Computer Janitor daemon."""
3741+
3742+
3743+from __future__ import absolute_import, unicode_literals
3744+
3745+__metaclass__ = type
3746+__all__ = [
3747+ 'DuplicateCruftError',
3748+ 'NoSuchCruftError',
3749+ 'PermissionDeniedError',
3750+ ]
3751+
3752+
3753+import dbus
3754+
3755+from computerjanitorapp import setup_gettext
3756+_ = setup_gettext()
3757+
3758+
3759+class PermissionDeniedError(dbus.DBusException):
3760+ """Permission denied by policy"""
3761+
3762+
3763+class CruftError(dbus.DBusException):
3764+ MSG = None
3765+
3766+ def __init__(self, cruft_name):
3767+ self.cruft_name = cruft_name
3768+
3769+ def __str__(self):
3770+ return _(self.MSG).format(self)
3771+
3772+
3773+class DuplicateCruftError(CruftError):
3774+ """Duplicate cruft name with different cleanup."""
3775+
3776+ MSG = _('Duplicate cruft with different cleanup: {0.cruft_name}')
3777+
3778+
3779+class NoSuchCruftError(CruftError):
3780+ """There is no cruft by the given name."""
3781+
3782+ MSG = _('No such cruft: {0.cruft_name}')
3783
3784=== added file 'computerjanitord/main.py'
3785--- computerjanitord/main.py 1970-01-01 00:00:00 +0000
3786+++ computerjanitord/main.py 2010-03-10 21:40:30 +0000
3787@@ -0,0 +1,97 @@
3788+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
3789+#
3790+# This program is free software: you can redistribute it and/or modify
3791+# it under the terms of the GNU General Public License as published by
3792+# the Free Software Foundation, version 3 of the License.
3793+#
3794+# This program is distributed in the hope that it will be useful,
3795+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3796+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3797+# GNU General Public License for more details.
3798+#
3799+# You should have received a copy of the GNU General Public License
3800+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3801+
3802+"""Main entry point for Computer Janitor dbus daemon."""
3803+
3804+from __future__ import absolute_import, unicode_literals
3805+
3806+__metaclass__ = type
3807+__all__ = [
3808+ 'main',
3809+ ]
3810+
3811+
3812+import os
3813+import gobject
3814+import logging
3815+import argparse
3816+import warnings
3817+import dbus.mainloop.glib
3818+import logging.handlers
3819+
3820+from computerjanitorapp import __version__, setup_gettext
3821+from computerjanitord.service import Service
3822+
3823+_ = setup_gettext()
3824+
3825+
3826+# 2010-02-09 barry: computerjanitor.package_cruft has a DeprecationWarning,
3827+# but that really needs to be fixed in that package, which in turn needs to be
3828+# ripped out of update-manager.
3829+warnings.filterwarnings('ignore', category=DeprecationWarning,
3830+ module='computerjanitor.package_cruft')
3831+
3832+
3833+class Options:
3834+ """Command line options."""
3835+
3836+ def __init__(self):
3837+ self.parser = argparse.ArgumentParser(
3838+ description=_('Computer janitor dbus daemon'),
3839+ version=__version__)
3840+ self.parser.add_argument(
3841+ '-n', '--dry-run', action='store_true',
3842+ help=_("""\
3843+ Only pretend to do anything permanent. This is useful for testing
3844+ and debugging."""))
3845+ self.parser.add_argument(
3846+ '-f', '--state-file', metavar='FILE',
3847+ help=_('Store ignored state in FILE instead of the default.'))
3848+ self.arguments = self.parser.parse_args()
3849+
3850+
3851+class ASCIIFormatter(logging.Formatter):
3852+ """Force the log messages to ASCII."""
3853+ def format(self, record):
3854+ message = logging.Formatter.format(self, record)
3855+ return message.encode('ascii', 'replace')
3856+
3857+
3858+def main():
3859+ """Main entry point."""
3860+ # Set up logging.
3861+ if os.environ.get('COMPUTER_JANITOR_DEBUG') is not None:
3862+ level = logging.DEBUG
3863+ else:
3864+ level = logging.INFO
3865+ logging.basicConfig(level=level, format='%(levelname)s: %(message)s')
3866+ log = logging.getLogger('computerjanitor')
3867+ # SysLogHandler does not recognize unicode arguments.
3868+ syslog = logging.handlers.SysLogHandler(str('/dev/log'))
3869+ # syslog will provide a timestamp.
3870+ formatter = ASCIIFormatter('computerjanitord:%(levelname)s: %(message)s')
3871+ syslog.setFormatter(formatter)
3872+ syslog.setLevel(level)
3873+ log.addHandler(syslog)
3874+ options = Options()
3875+ dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
3876+ server = Service(options)
3877+ try:
3878+ gobject.MainLoop().run()
3879+ except KeyboardInterrupt:
3880+ pass
3881+
3882+
3883+if __name__ == '__main__':
3884+ main()
3885
3886=== added file 'computerjanitord/service.py'
3887--- computerjanitord/service.py 1970-01-01 00:00:00 +0000
3888+++ computerjanitord/service.py 2010-03-10 21:40:30 +0000
3889@@ -0,0 +1,260 @@
3890+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
3891+#
3892+# This program is free software: you can redistribute it and/or modify
3893+# it under the terms of the GNU General Public License as published by
3894+# the Free Software Foundation, version 3 of the License.
3895+#
3896+# This program is distributed in the hope that it will be useful,
3897+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3898+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3899+# GNU General Public License for more details.
3900+#
3901+# You should have received a copy of the GNU General Public License
3902+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3903+
3904+"""dbus service for cleaning up crufty packages that are no longer needed."""
3905+
3906+
3907+from __future__ import absolute_import, unicode_literals
3908+
3909+__metaclass__ = type
3910+__all__ = [
3911+ 'Service',
3912+ ]
3913+
3914+
3915+import atexit
3916+import logging
3917+
3918+import dbus.service
3919+
3920+from computerjanitor import PackageCruft
3921+from computerjanitord.application import Application
3922+from computerjanitord.authenticator import Authenticator
3923+from computerjanitord.collector import Collector
3924+from computerjanitord.errors import NoSuchCruftError, PermissionDeniedError
3925+from computerjanitord.state import State, DEFAULT_STATE_FILE
3926+
3927+
3928+log = logging.getLogger('computerjanitor')
3929+MISSING = object()
3930+
3931+DBUS_INTERFACE_NAME = 'com.ubuntu.ComputerJanitor'
3932+PRIVILEGE = 'com.ubuntu.computerjanitor.updatesystem'
3933+
3934+
3935+class Service(dbus.service.Object):
3936+ """Backend dbus service that handles removing crufty packages."""
3937+
3938+ def __init__(self, options):
3939+ """Create the dbus service.
3940+
3941+ :param options: The command line options class.
3942+ :type options: `Options`
3943+ """
3944+ self.dry_run = options.arguments.dry_run
3945+ self.state_file = (DEFAULT_STATE_FILE
3946+ if options.arguments.state_file is None
3947+ else options.arguments.state_file)
3948+ self.application = Application()
3949+ self.state = State()
3950+ self.state.load(self.state_file)
3951+ self.collector = Collector(self.application, service=self)
3952+ self.authenticator = Authenticator()
3953+ bus_name = dbus.service.BusName(
3954+ DBUS_INTERFACE_NAME, bus=dbus.SystemBus())
3955+ dbus.service.Object.__init__(self, bus_name, '/')
3956+ # We can't use the decorator because that doesn't work with methods;
3957+ # self doesn't get passed to the handler.
3958+ atexit.register(self._exit_handler)
3959+
3960+ def _exit_handler(self):
3961+ """Ensure that the state file is saved at exit."""
3962+ if not self.dry_run:
3963+ self.state.save(self.state_file)
3964+
3965+ def _authenticate(self, sender, connection):
3966+ """Authenticate via PolicyKit.
3967+
3968+ :param sender: The dbus client sender.
3969+ :param connection: The dbus client connection.
3970+ :raises PermissionDeniedError: when the authentication fails.
3971+ """
3972+ if not self.authenticator.authenticate(sender, connection, PRIVILEGE):
3973+ log.error('Permission denied: {0} for {1} on {2}'.format(
3974+ PRIVILEGE, sender, connection))
3975+ raise PermissionDeniedError(PRIVILEGE)
3976+ log.debug('Permission granted: {0} for {1} on {2}'.format(
3977+ PRIVILEGE, sender, connection))
3978+
3979+ @dbus.service.method(DBUS_INTERFACE_NAME,
3980+ out_signature='as')
3981+ def find(self):
3982+ """Find all the non-whitelisted cruft on the system.
3983+
3984+ Because this is a read-only interface it does not need authorization
3985+ to be called.
3986+
3987+ :return: A list of matching cruft names.
3988+ """
3989+ return list(cruft.get_name() for cruft in self.collector.cruft)
3990+
3991+ @dbus.service.method(DBUS_INTERFACE_NAME,
3992+ out_signature='as',
3993+ # Must wrap these in str() because Python < 2.6.5
3994+ # does not like unicode keyword arguments.
3995+ sender_keyword=str('sender'),
3996+ connection_keyword=str('connection'))
3997+ def load(self, sender=None, connection=None):
3998+ """Load the state file."""
3999+ self._authenticate(sender, connection)
4000+ self.state.load(self.state_file)
4001+ return list(self.state.ignore)
4002+
4003+ @dbus.service.method(DBUS_INTERFACE_NAME,
4004+ # Must wrap these in str() because Python < 2.6.5
4005+ # does not like unicode keyword arguments.
4006+ sender_keyword=str('sender'),
4007+ connection_keyword=str('connection'))
4008+ def save(self, sender=None, connection=None):
4009+ """Save the state file."""
4010+ self._authenticate(sender, connection)
4011+ if not self.dry_run:
4012+ self.state.save(self.state_file)
4013+
4014+ @dbus.service.method(DBUS_INTERFACE_NAME,
4015+ in_signature='s',
4016+ # Must wrap these in str() because Python < 2.6.5
4017+ # does not like unicode keyword arguments.
4018+ sender_keyword=str('sender'),
4019+ connection_keyword=str('connection'))
4020+ def ignore(self, name, sender=None, connection=None):
4021+ """Ignore the named cruft.
4022+
4023+ :param name: The name of the cruft to ignore.
4024+ :type filename: string
4025+ """
4026+ # Make sure this is known cruft first.
4027+ cruft = self.collector.cruft_by_name.get(name, MISSING)
4028+ if cruft is MISSING:
4029+ log.error('ignore(): No such cruft: {0}'.format(name))
4030+ raise NoSuchCruftError(name)
4031+ self._authenticate(sender, connection)
4032+ if not self.dry_run:
4033+ self.state.ignore.add(name)
4034+
4035+ @dbus.service.method(DBUS_INTERFACE_NAME,
4036+ in_signature='s',
4037+ # Must wrap these in str() because Python < 2.6.5
4038+ # does not like unicode keyword arguments.
4039+ sender_keyword=str('sender'),
4040+ connection_keyword=str('connection'))
4041+ def unignore(self, name, sender=None, connection=None):
4042+ """Unignore the named cruft.
4043+
4044+ :param name: The name of the cruft to unignore.
4045+ :type filename: string
4046+ """
4047+ cruft = self.collector.cruft_by_name.get(name, MISSING)
4048+ if cruft is MISSING:
4049+ log.error('ignore(): No such cruft: {0}'.format(name))
4050+ raise NoSuchCruftError(name)
4051+ self._authenticate(sender, connection)
4052+ if not self.dry_run:
4053+ # Don't worry if we're already not ignoring the cruft (i.e. don't
4054+ # raise a KeyError here if 'name' is not in the set).
4055+ self.state.ignore.discard(name)
4056+
4057+ @dbus.service.method(DBUS_INTERFACE_NAME,
4058+ out_signature='as')
4059+ def ignored(self):
4060+ """Return the list of ignored cruft.
4061+
4062+ :return: The names of the ignored cruft.
4063+ :rtype: list of strings
4064+ """
4065+ return list(self.state.ignore)
4066+
4067+ @dbus.service.method(DBUS_INTERFACE_NAME,
4068+ in_signature='s',
4069+ out_signature='s')
4070+ def get_description(self, name):
4071+ """Return the description of the named cruft.
4072+
4073+ :param name: The cruft name.
4074+ :type name: string
4075+ :return: The description of the cruft.
4076+ :rtype: string
4077+ """
4078+ cruft = self.collector.cruft_by_name.get(name, MISSING)
4079+ if cruft is MISSING:
4080+ log.error('get_description(): No such cruft: {0}'.format(name))
4081+ raise NoSuchCruftError(name)
4082+ return cruft.get_description()
4083+
4084+ @dbus.service.method(DBUS_INTERFACE_NAME,
4085+ in_signature='s',
4086+ out_signature='s')
4087+ def get_shortname(self, name):
4088+ """Return the short name of the named cruft.
4089+
4090+ :param name: The cruft name.
4091+ :type name: string
4092+ :return: The short nameof the cruft.
4093+ :rtype: string
4094+ """
4095+ cruft = self.collector.cruft_by_name.get(name, MISSING)
4096+ if cruft is MISSING:
4097+ log.error('get_shortname(): No such cruft: {0}'.format(name))
4098+ raise NoSuchCruftError(name)
4099+ return cruft.get_shortname()
4100+
4101+ @dbus.service.method(DBUS_INTERFACE_NAME,
4102+ in_signature='s',
4103+ out_signature='st')
4104+ def get_details(self, name):
4105+ """Return some extra details about the named cruft.
4106+
4107+ :param name: The cruft name.
4108+ :type name: string
4109+ :return: Some extra details about the named cruft, specifically its
4110+ 'type' and the amount of disk space it consumes. The type is
4111+ simply the name of the cruft instance's class.
4112+ :rtype: string, uint64
4113+ """
4114+ cruft = self.collector.cruft_by_name.get(name, MISSING)
4115+ if cruft is MISSING:
4116+ log.error('get_shortname(): No such cruft: {0}'.format(name))
4117+ raise NoSuchCruftError(name)
4118+ return cruft.__class__.__name__, cruft.get_disk_usage()
4119+
4120+ @dbus.service.method(DBUS_INTERFACE_NAME,
4121+ in_signature='as', # array of strings
4122+ # Must wrap these in str() because Python < 2.6.5
4123+ # does not like unicode keyword arguments.
4124+ sender_keyword=str('sender'),
4125+ connection_keyword=str('connection'))
4126+ def clean(self, names, sender=None, connection=None):
4127+ """Clean the named crufts.
4128+
4129+ :param names: The names of the cruft to clean.
4130+ :type names: list of strings
4131+ """
4132+ self._authenticate(sender, connection)
4133+ self.collector.clean(names, self.dry_run)
4134+
4135+ @dbus.service.signal(DBUS_INTERFACE_NAME,
4136+ signature='s')
4137+ def cleanup_status(self, cruft):
4138+ """Signal cleanup status.
4139+
4140+ This signal is used to incrementally inform clients that some cleanup
4141+ work is being done. It is called at the beginning of the cleanup
4142+ process and after each plugin has completed its `post_cleanup()`
4143+ method.
4144+
4145+ :param done: The name of the next piece of cruft to be cleaned up, or
4146+ the empty string when there's nothing left to do.
4147+ :type done: string
4148+ """
4149+ log.debug('cleanup_status: {0}'.format(cruft))
4150
4151=== added file 'computerjanitord/state.py'
4152--- computerjanitord/state.py 1970-01-01 00:00:00 +0000
4153+++ computerjanitord/state.py 2010-03-10 21:40:30 +0000
4154@@ -0,0 +1,92 @@
4155+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
4156+#
4157+# This program is free software: you can redistribute it and/or modify
4158+# it under the terms of the GNU General Public License as published by
4159+# the Free Software Foundation, version 3 of the License.
4160+#
4161+# This program is distributed in the hope that it will be useful,
4162+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4163+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4164+# GNU General Public License for more details.
4165+#
4166+# You should have received a copy of the GNU General Public License
4167+# along with this program. If not, see <http://www.gnu.org/licenses/>.
4168+
4169+"""Maintaining package ignored state."""
4170+
4171+from __future__ import absolute_import, unicode_literals
4172+
4173+__metaclass__ = type
4174+__all__ = [
4175+ 'DEFAULT_STATE_FILE',
4176+ 'State',
4177+ ]
4178+
4179+
4180+import ConfigParser
4181+import textwrap
4182+
4183+
4184+DEFAULT_STATE_FILE = '/var/lib/computer-janitor/state.dat'
4185+
4186+
4187+class State:
4188+ """Maintain the state of cruft which should be ignored.
4189+
4190+ The file's format is a `ConfigParser` style .ini file. Each section is a
4191+ cruft's `.get_name()` value and contains a single boolean setting
4192+ `ignore`. If true, then the cruft is ignored.
4193+
4194+ For backward compatibility purposes, the setting `enabled` is also
4195+ recognized (on read only). If `enabled` is false then the cruft is
4196+ ignored.
4197+ """
4198+
4199+ def __init__(self):
4200+ self.ignore = set()
4201+
4202+ def load(self, filename):
4203+ """Load ignored state from a file.
4204+
4205+ This reset any previously determined state and re-initializes it with
4206+ the state stored in the file.
4207+
4208+ :param filename: The file to load.
4209+ :type filename: string
4210+ """
4211+ parser = ConfigParser.ConfigParser()
4212+ parser.read(filename)
4213+ # Reset the set of ignored packages.
4214+ self.ignore = set()
4215+ for cruft_name in parser.sections():
4216+ # For backwards compatibility, recognize both the 'ignore' setting
4217+ # and the 'enabled' setting. We only write the former.
4218+ try:
4219+ ignore = parser.getboolean(cruft_name, 'ignore')
4220+ except ConfigParser.NoOptionError:
4221+ try:
4222+ ignore = not parser.getboolean(cruft_name, 'enabled')
4223+ except ConfigParser.NoOptionError:
4224+ # No other settings are recognized.
4225+ ignore = False
4226+ if ignore:
4227+ self.ignore.add(cruft_name)
4228+
4229+ def save(self, filename):
4230+ """Save the ignored state to a file.
4231+
4232+ Only the packages being ignored are stored to the file, and writing
4233+ overwrites the previous contents of the file.
4234+
4235+ :param filename: The file to load.
4236+ :type filename: string
4237+ """
4238+ # It's easier just to write the .ini file directly instead of using
4239+ # the ConfigParser interface. This way we can guarantee sort order
4240+ # and can automatically cull unignored packages from the file.
4241+ with open(filename, 'w') as fp:
4242+ for cruft_name in self.ignore:
4243+ print >> fp, textwrap.dedent("""\
4244+ [{0}]
4245+ ignore: true
4246+ """.format(cruft_name))
4247
4248=== added directory 'computerjanitord/tests'
4249=== added file 'computerjanitord/tests/__init__.py'
4250=== added directory 'computerjanitord/tests/data'
4251=== added file 'computerjanitord/tests/data/empty'
4252=== added directory 'computerjanitord/tests/data/etc'
4253=== added directory 'computerjanitord/tests/data/etc/apt'
4254=== added file 'computerjanitord/tests/data/etc/apt/sources.list'
4255--- computerjanitord/tests/data/etc/apt/sources.list 1970-01-01 00:00:00 +0000
4256+++ computerjanitord/tests/data/etc/apt/sources.list 2010-03-10 21:40:30 +0000
4257@@ -0,0 +1,1 @@
4258+deb http://archive.ubuntu.com/ubuntu intrepid main restricted
4259
4260=== added directory 'computerjanitord/tests/data/var'
4261=== added directory 'computerjanitord/tests/data/var/cache'
4262=== added directory 'computerjanitord/tests/data/var/cache/apt'
4263=== added directory 'computerjanitord/tests/data/var/cache/apt/archives'
4264=== added directory 'computerjanitord/tests/data/var/cache/apt/archives/partial'
4265=== added directory 'computerjanitord/tests/data/var/lib'
4266=== added directory 'computerjanitord/tests/data/var/lib/apt'
4267=== added directory 'computerjanitord/tests/data/var/lib/apt/lists'
4268=== added file 'computerjanitord/tests/data/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_intrepid_restricted_binary-i386_Packages'
4269--- computerjanitord/tests/data/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_intrepid_restricted_binary-i386_Packages 1970-01-01 00:00:00 +0000
4270+++ computerjanitord/tests/data/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_intrepid_restricted_binary-i386_Packages 2010-03-10 21:40:30 +0000
4271@@ -0,0 +1,54 @@
4272+Package: dash
4273+Priority: required
4274+Section: shells
4275+Installed-Size: 236
4276+Maintainer: Ubuntu Core Developers <ubuntu-devel-discuss@lists.ubuntu.com>
4277+Original-Maintainer: Gerrit Pape <pape@smarden.org>
4278+Architecture: all
4279+Version: 0.5.5.1-3ubuntu1
4280+Depends: debianutils (>= 2.15), dpkg (>= 1.15.0)
4281+Pre-Depends: libc6 (>= 2.11~20100104-0ubuntu2)
4282+Filename: pool/main/d/dash/dash_0.5.5.1-3ubuntu1_all.deb
4283+Size: 104190
4284+MD5sum: a7f08fe3ee941d06c0d98e5e99c02190
4285+SHA1: d2dda78f9a6f82c58c01d34f18b94aebfcb33f19
4286+SHA256: 69709747f854ac1bd671dff37b47c18e589b095ecb8f3116beaed7fc0eeb657e
4287+Description: POSIX-compliant shell
4288+ The Debian Almquist Shell (dash) is a POSIX-compliant shell derived
4289+ from ash.
4290+ .
4291+ Since it executes scripts faster than bash, and has fewer library
4292+ dependencies (making it more robust against software or hardware
4293+ failures), it is used as the default system shell on Debian systems.
4294+Homepage: http://gondor.apana.org.au/~herbert/dash/
4295+Bugs: https://bugs.launchpad.net/ubuntu/+filebug
4296+Origin: Ubuntu
4297+Supported: 5y
4298+Task: minimal
4299+
4300+Package: gzip
4301+Essential: yes
4302+Priority: required
4303+Section: utils
4304+Installed-Size: 284
4305+Maintainer: Ubuntu Core Developers <ubuntu-devel-discuss@lists.ubuntu.com>
4306+Original-Maintainer: Bdale Garbee <bdale@gag.com>
4307+Architecture: all
4308+Version: 1.3.12-9ubuntu1
4309+Pre-Depends: libc6 (>= 2.4)
4310+Suggests: less
4311+Filename: pool/main/g/gzip/gzip_1.3.12-9ubuntu1_all.deb
4312+Size: 107030
4313+MD5sum: f64beb93d2d1a3348cfc47f1fd176ee1
4314+SHA1: 3dd3e56f551fb85ba2ad385df463adeff1fff2d9
4315+SHA256: 2545f0a28514535006adf9ee8576ca3be2aa6da3d890047f92eeda18c0e3aa57
4316+Description: GNU compression utilities
4317+ This package provides the standard GNU file compression utilities, which
4318+ are also the default compression tools for Debian. They typically operate
4319+ on files with names ending in '.gz', but can also decompress files ending
4320+ in '.Z' created with 'compress'.
4321+Bugs: https://bugs.launchpad.net/ubuntu/+filebug
4322+Origin: Ubuntu
4323+Supported: 5y
4324+Task: minimal
4325+
4326
4327=== added directory 'computerjanitord/tests/data/var/lib/apt/lists/partial'
4328=== added directory 'computerjanitord/tests/data/var/lib/dpkg'
4329=== added file 'computerjanitord/tests/data/var/lib/dpkg/status'
4330--- computerjanitord/tests/data/var/lib/dpkg/status 1970-01-01 00:00:00 +0000
4331+++ computerjanitord/tests/data/var/lib/dpkg/status 2010-03-10 21:40:30 +0000
4332@@ -0,0 +1,28 @@
4333+Package: dash-nodownload
4334+Priority: required
4335+Section: shells
4336+Installed-Size: 236
4337+Maintainer: Ubuntu Core Developers <ubuntu-devel-discuss@lists.ubuntu.com>
4338+Original-Maintainer: Gerrit Pape <pape@smarden.org>
4339+Architecture: all
4340+Version: 0.5.5.1-3ubuntu1
4341+Depends: debianutils (>= 2.15), dpkg (>= 1.15.0)
4342+Pre-Depends: libc6 (>= 2.11~20100104-0ubuntu2)
4343+Filename: pool/main/d/dash/dash_0.5.5.1-3ubuntu1_all.deb
4344+Size: 104190
4345+MD5sum: a7f08fe3ee941d06c0d98e5e99c02190
4346+SHA1: d2dda78f9a6f82c58c01d34f18b94aebfcb33f19
4347+SHA256: 69709747f854ac1bd671dff37b47c18e589b095ecb8f3116beaed7fc0eeb657e
4348+Description: POSIX-compliant shell
4349+ The Debian Almquist Shell (dash) is a POSIX-compliant shell derived
4350+ from ash.
4351+ .
4352+ Since it executes scripts faster than bash, and has fewer library
4353+ dependencies (making it more robust against software or hardware
4354+ failures), it is used as the default system shell on Debian systems.
4355+Homepage: http://gondor.apana.org.au/~herbert/dash/
4356+Bugs: https://bugs.launchpad.net/ubuntu/+filebug
4357+Origin: Ubuntu
4358+Supported: 5y
4359+Task: minimal
4360+
4361
4362=== added file 'computerjanitord/tests/test_all.py'
4363--- computerjanitord/tests/test_all.py 1970-01-01 00:00:00 +0000
4364+++ computerjanitord/tests/test_all.py 2010-03-10 21:40:30 +0000
4365@@ -0,0 +1,41 @@
4366+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
4367+#
4368+# This program is free software: you can redistribute it and/or modify
4369+# it under the terms of the GNU General Public License as published by
4370+# the Free Software Foundation, version 3 of the License.
4371+#
4372+# This program is distributed in the hope that it will be useful,
4373+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4374+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4375+# GNU General Public License for more details.
4376+#
4377+# You should have received a copy of the GNU General Public License
4378+# along with this program. If not, see <http://www.gnu.org/licenses/>.
4379+
4380+"""Test suite for Computer Janitor daemon (dbus backend)."""
4381+
4382+from __future__ import absolute_import, unicode_literals
4383+
4384+__metaclass__ = type
4385+__all__ = [
4386+ 'test_suite',
4387+ ]
4388+
4389+
4390+import unittest
4391+
4392+from computerjanitord.tests import test_application
4393+from computerjanitord.tests import test_authenticator
4394+from computerjanitord.tests import test_collector
4395+from computerjanitord.tests import test_state
4396+from computerjanitord.tests import test_whitelist
4397+
4398+
4399+def test_suite():
4400+ suite = unittest.TestSuite()
4401+ suite.addTests(test_application.test_suite())
4402+ suite.addTests(test_authenticator.test_suite())
4403+ suite.addTests(test_collector.test_suite())
4404+ suite.addTests(test_state.test_suite())
4405+ suite.addTests(test_whitelist.test_suite())
4406+ return suite
4407
4408=== added file 'computerjanitord/tests/test_application.py'
4409--- computerjanitord/tests/test_application.py 1970-01-01 00:00:00 +0000
4410+++ computerjanitord/tests/test_application.py 2010-03-10 21:40:30 +0000
4411@@ -0,0 +1,106 @@
4412+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
4413+#
4414+# This program is free software: you can redistribute it and/or modify
4415+# it under the terms of the GNU General Public License as published by
4416+# the Free Software Foundation, version 3 of the License.
4417+#
4418+# This program is distributed in the hope that it will be useful,
4419+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4420+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4421+# GNU General Public License for more details.
4422+#
4423+# You should have received a copy of the GNU General Public License
4424+# along with this program. If not, see <http://www.gnu.org/licenses/>.
4425+
4426+"""Test the plugin application interfaces."""
4427+
4428+from __future__ import absolute_import, unicode_literals
4429+
4430+__metaclass__ = type
4431+__all__ = [
4432+ 'ApplicationTestSetupMixin',
4433+ 'test_suite',
4434+ ]
4435+
4436+
4437+import os
4438+import apt
4439+import apt_pkg
4440+import unittest
4441+import warnings
4442+import pkg_resources
4443+
4444+from contextlib import contextmanager
4445+
4446+import computerjanitor
4447+import computerjanitord.application
4448+
4449+from computerjanitord.application import (
4450+ Application, MissingLandmarkError, NonDownloadableError)
4451+
4452+
4453+@contextmanager
4454+def landmarks(*packages):
4455+ # Hack the module global list of known landmark packages.
4456+ old_landmarks = computerjanitord.application.LANDMARK_PACKAGES[:]
4457+ computerjanitord.application.LANDMARK_PACKAGES[:] = packages
4458+ yield
4459+ computerjanitord.application.LANDMARK_PACKAGES[:] = old_landmarks
4460+
4461+
4462+class MockCruft:
4463+ def __init__(self, name):
4464+ self.name = name
4465+
4466+ def get_name(self):
4467+ warnings.warn('.get_name() is deprecated; use .name',
4468+ DeprecationWarning)
4469+ return self.name
4470+
4471+
4472+class ApplicationTestSetupMixin:
4473+ """Set up an `Application` instance with test data in its apt_cache."""
4474+
4475+ def setUp(self):
4476+ self.data_dir = os.path.abspath(
4477+ pkg_resources.resource_filename('computerjanitord.tests', 'data'))
4478+ # Make the test insensitive to the platform's architecture.
4479+ apt_pkg.Config.Set('APT::Architecture', 'i386')
4480+ self.cache = apt.Cache(rootdir=self.data_dir)
4481+ self.app = Application(self.cache)
4482+
4483+ def tearDown(self):
4484+ # Clear the cache.
4485+ cache_dir = os.path.join(self.data_dir, 'var', 'cache', 'apt')
4486+ for filename in os.listdir(cache_dir):
4487+ if filename.endswith('.bin'):
4488+ os.remove(os.path.join(cache_dir, filename))
4489+
4490+
4491+class TestApplication(unittest.TestCase, ApplicationTestSetupMixin):
4492+ """Test the `Application` interface."""
4493+
4494+ def setUp(self):
4495+ ApplicationTestSetupMixin.setUp(self)
4496+
4497+ def tearDown(self):
4498+ ApplicationTestSetupMixin.tearDown(self)
4499+
4500+ def test_verify_apt_cache_good_path(self):
4501+ # All essential packages are in the cache by default.
4502+ self.assertEqual(self.app.verify_apt_cache(), None)
4503+
4504+ def test_verify_apt_cache_with_nondownloadable_landmark(self):
4505+ # Test that a missing landmark file causes an exception.
4506+ with landmarks('gzip', 'dash-nodownload'):
4507+ self.assertRaises(NonDownloadableError, self.app.verify_apt_cache)
4508+
4509+ def test_verify_apt_cache_with_missing_landmark(self):
4510+ with landmarks('gzip', 'dash', 'i-am-not-here'):
4511+ self.assertRaises(MissingLandmarkError, self.app.verify_apt_cache)
4512+
4513+
4514+def test_suite():
4515+ suite = unittest.TestSuite()
4516+ suite.addTests(unittest.makeSuite(TestApplication))
4517+ return suite
4518
4519=== added file 'computerjanitord/tests/test_authenticator.py'
4520--- computerjanitord/tests/test_authenticator.py 1970-01-01 00:00:00 +0000
4521+++ computerjanitord/tests/test_authenticator.py 2010-03-10 21:40:30 +0000
4522@@ -0,0 +1,97 @@
4523+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
4524+#
4525+# This program is free software: you can redistribute it and/or modify
4526+# it under the terms of the GNU General Public License as published by
4527+# the Free Software Foundation, version 3 of the License.
4528+#
4529+# This program is distributed in the hope that it will be useful,
4530+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4531+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4532+# GNU General Public License for more details.
4533+#
4534+# You should have received a copy of the GNU General Public License
4535+# along with this program. If not, see <http://www.gnu.org/licenses/>.
4536+
4537+"""Test the authenticator."""
4538+
4539+import unittest
4540+
4541+from computerjanitord.authenticator import Authenticator
4542+
4543+SUCCESS_PID = 801
4544+FAILURE_PID = 999
4545+
4546+AUTHENTICATED_USER = 'desktop-user'
4547+IMPOSTER_USER = 'imposter'
4548+EXPECTED_PRIVILEGE = 'com.ubuntu.computerjanitor.cleanpackages'
4549+BOGUS_PRIVILEGE = 'com.example.evil-corp.killsystem'
4550+
4551+
4552+class MockPolicyKit(object):
4553+ """Mock the PolicyKit's CheckAuthorization() method."""
4554+
4555+ def CheckAuthorization(self, subject, privilege, details, flags,
4556+ cancellation_id):
4557+ """See `policykit.CheckAuthorization()`.
4558+
4559+ :return: (is_authorized, is_challenge, details)
4560+ """
4561+ if privilege == BOGUS_PRIVILEGE:
4562+ return False, False, ''
4563+ assert isinstance(subject, tuple) and len(subject) == 2, (
4564+ 'subject is not a 2-tuple')
4565+ assert subject[0] == 'unix-process', 'Badly formed subject'
4566+ assert isinstance(subject[1], dict), 'Badly formed subject details'
4567+ assert subject[1]['start-time'] == 0, 'subject missing start-time'
4568+ if subject[1]['pid'] == SUCCESS_PID:
4569+ return True, False, ''
4570+ else:
4571+ return False, False, ''
4572+
4573+
4574+class TestableAuthenticator(Authenticator):
4575+ """See `Authenticator`."""
4576+
4577+ def _get_policykit_proxy(self):
4578+ """See `Authenticator`."""
4579+ return MockPolicyKit()
4580+
4581+ def _get_sender_pid(self, connection, sender):
4582+ """See `Authenticator`."""
4583+ if sender == AUTHENTICATED_USER:
4584+ return SUCCESS_PID
4585+ else:
4586+ return FAILURE_PID
4587+
4588+
4589+class TestAuthenticator(unittest.TestCase):
4590+ """Tests of the PolicyKit authenticator."""
4591+
4592+ def setUp(self):
4593+ """See `unittest.TestCase`."""
4594+ self.authenticator = TestableAuthenticator()
4595+ self.connection = object()
4596+
4597+ def tearDown(self):
4598+ """See `unittest.TestCase`."""
4599+
4600+ def test_good_path(self):
4601+ # Test for successful authentication.
4602+ self.assertTrue(self.authenticator.authenticate(
4603+ AUTHENTICATED_USER, self.connection, EXPECTED_PRIVILEGE))
4604+
4605+ def test_bogus_privilege(self):
4606+ # Test for bogus privilege fails.
4607+ self.assertFalse(self.authenticator.authenticate(
4608+ AUTHENTICATED_USER, self.connection, BOGUS_PRIVILEGE))
4609+
4610+ def test_unauthorized(self):
4611+ # Test for some imposter not being able to authenticate.
4612+ self.assertFalse(self.authenticator.authenticate(
4613+ IMPOSTER_USER, self.connection, EXPECTED_PRIVILEGE))
4614+
4615+
4616+def test_suite():
4617+ suite = unittest.TestSuite()
4618+ suite.addTests(unittest.makeSuite(TestAuthenticator))
4619+ return suite
4620
4621=== added file 'computerjanitord/tests/test_collector.py'
4622--- computerjanitord/tests/test_collector.py 1970-01-01 00:00:00 +0000
4623+++ computerjanitord/tests/test_collector.py 2010-03-10 21:40:30 +0000
4624@@ -0,0 +1,187 @@
4625+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
4626+#
4627+# This program is free software: you can redistribute it and/or modify
4628+# it under the terms of the GNU General Public License as published by
4629+# the Free Software Foundation, version 3 of the License.
4630+#
4631+# This program is distributed in the hope that it will be useful,
4632+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4633+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4634+# GNU General Public License for more details.
4635+#
4636+# You should have received a copy of the GNU General Public License
4637+# along with this program. If not, see <http://www.gnu.org/licenses/>.
4638+
4639+"""Test the cruft collector."""
4640+
4641+from __future__ import absolute_import, unicode_literals
4642+
4643+__metaclass__ = type
4644+__all__ = [
4645+ 'test_suite',
4646+ ]
4647+
4648+
4649+import os
4650+import shutil
4651+import tempfile
4652+import unittest
4653+
4654+from computerjanitor.plugin import Plugin
4655+from computerjanitord.collector import Collector
4656+from computerjanitord.errors import DuplicateCruftError
4657+from computerjanitord.tests.test_application import ApplicationTestSetupMixin
4658+
4659+
4660+class MockCruft:
4661+ """Mock cruft that supports the required `get_name()` interface."""
4662+
4663+ def __init__(self, name):
4664+ self.name = name
4665+
4666+ def get_name(self):
4667+ return self.name
4668+
4669+
4670+class MockPlugin(Plugin):
4671+ cruft_class = MockCruft
4672+
4673+ def __init__(self, prefix, shortnames):
4674+ super(MockPlugin, self).__init__()
4675+ self.prefix = prefix
4676+ self.shortnames = shortnames
4677+
4678+ def get_cruft(self):
4679+ for shortname in self.shortnames:
4680+ yield self.cruft_class('{0}:{1}'.format(self.prefix, shortname))
4681+
4682+
4683+class SawAppPlugin(Plugin):
4684+ """All this plugin does is set a marker attribute on the `Application`.
4685+
4686+ This proves that access to the application through the plugin works.
4687+ """
4688+ def get_cruft(self):
4689+ self.app.saw_app_plugin = True
4690+ return []
4691+
4692+
4693+class MockPluginManager:
4694+ def __init__(self, app, plugin_dirs):
4695+ self.app = app
4696+ # Ignore plugin_dirs
4697+
4698+ def get_plugins(self):
4699+ shortnames = ('one', 'two', 'three')
4700+ for prefix in ('foo', 'bar', 'baz'):
4701+ plugin = MockPlugin(prefix, shortnames)
4702+ plugin.set_application(self.app)
4703+ yield plugin
4704+ plugin = SawAppPlugin()
4705+ plugin.set_application(self.app)
4706+ yield plugin
4707+
4708+
4709+class MockCruftExtra(MockCruft):
4710+ """Cruft with a different class."""
4711+
4712+
4713+class MockPluginExtra(MockPlugin):
4714+ """A mock plugin that returns cruft with a different class."""
4715+
4716+ cruft_class = MockCruftExtra
4717+
4718+
4719+class IgnoredDuplicateCruftPluginManager(MockPluginManager):
4720+ """Add an additional piece of ignorable duplication cruft."""
4721+
4722+ def __init__(self, app, plugin_dirs):
4723+ self.app = app
4724+ # Ignore plugin_dirs
4725+
4726+ def get_plugins(self):
4727+ yield MockPlugin('one', ('foo', 'bar'))
4728+ yield MockPlugin('one', ('baz', 'foo'))
4729+
4730+
4731+class BadDuplicateCruftPluginManager(MockPluginManager):
4732+ """Add an additional piece of bad duplicate cruft."""
4733+ def __init__(self, app, plugin_dirs):
4734+ self.app = app
4735+ # Ignore plugin_dirs
4736+
4737+ def get_plugins(self):
4738+ yield MockPlugin('one', ('foo', 'bar'))
4739+ yield MockPluginExtra('one', ('baz', 'foo'))
4740+
4741+
4742+class TestCollector(unittest.TestCase, ApplicationTestSetupMixin):
4743+ """Test the cruft collector."""
4744+
4745+ def setUp(self):
4746+ # Set up the test data Application.
4747+ ApplicationTestSetupMixin.setUp(self)
4748+ self.tempdir = tempfile.mkdtemp()
4749+ whitelist_dirs = (self.tempdir,)
4750+ with open(os.path.join(self.tempdir, 'one.whitelist'), 'w') as fp:
4751+ print >> fp, 'foo:two'
4752+ print >> fp, 'bar:one'
4753+ print >> fp, 'baz:three'
4754+ self.collector = Collector(self.app, MockPluginManager, whitelist_dirs)
4755+
4756+ def tearDown(self):
4757+ shutil.rmtree(self.tempdir)
4758+ ApplicationTestSetupMixin.tearDown(self)
4759+
4760+ def test_cruft_collector(self):
4761+ cruft_names = set(cruft.get_name() for cruft in self.collector.cruft)
4762+ self.assertEqual(cruft_names, set(('foo:one', 'foo:three',
4763+ 'bar:two', 'bar:three',
4764+ 'baz:one', 'baz:two')))
4765+
4766+ def test_plugin_needs_application(self):
4767+ # SawAppPlugin returned by the MockPluginManager sets this attribute
4768+ # on the Application.
4769+ self.assertTrue(self.app.saw_app_plugin)
4770+
4771+ def test_collector_name_mapping(self):
4772+ cruft_keys = set(self.collector.cruft_by_name)
4773+ cruft_names = set(cruft.get_name() for cruft in self.collector.cruft)
4774+ self.assertEqual(cruft_keys, cruft_names)
4775+
4776+
4777+class TestDuplicateCruftCollector(
4778+ unittest.TestCase, ApplicationTestSetupMixin):
4779+
4780+ def setUp(self):
4781+ # Set up the test data Application.
4782+ ApplicationTestSetupMixin.setUp(self)
4783+
4784+ def tearDown(self):
4785+ ApplicationTestSetupMixin.tearDown(self)
4786+
4787+ def test_duplicate_cruft_error(self):
4788+ self.assertRaises(DuplicateCruftError, Collector,
4789+ self.app, BadDuplicateCruftPluginManager, [])
4790+
4791+ def test_duplicate_cruft_error_message(self):
4792+ try:
4793+ Collector(self.app, BadDuplicateCruftPluginManager, [])
4794+ except DuplicateCruftError as error:
4795+ self.assertEqual(
4796+ str(error),
4797+ 'Duplicate cruft with different cleanup: one:foo')
4798+ else:
4799+ raise AssertionError('DuplicateCruftError expected')
4800+
4801+ def test_ignored_duplicate_cruft(self):
4802+ collector = Collector(self.app, IgnoredDuplicateCruftPluginManager, [])
4803+ self.assertEqual(list(cruft.get_name() for cruft in collector.cruft),
4804+ ['one:foo', 'one:bar', 'one:baz'])
4805+
4806+
4807+def test_suite():
4808+ suite = unittest.TestSuite()
4809+ suite.addTests(unittest.makeSuite(TestCollector))
4810+ suite.addTests(unittest.makeSuite(TestDuplicateCruftCollector))
4811+ return suite
4812
4813=== added file 'computerjanitord/tests/test_state.py'
4814--- computerjanitord/tests/test_state.py 1970-01-01 00:00:00 +0000
4815+++ computerjanitord/tests/test_state.py 2010-03-10 21:40:30 +0000
4816@@ -0,0 +1,141 @@
4817+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
4818+#
4819+# This program is free software: you can redistribute it and/or modify
4820+# it under the terms of the GNU General Public License as published by
4821+# the Free Software Foundation, version 3 of the License.
4822+#
4823+# This program is distributed in the hope that it will be useful,
4824+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4825+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4826+# GNU General Public License for more details.
4827+#
4828+# You should have received a copy of the GNU General Public License
4829+# along with this program. If not, see <http://www.gnu.org/licenses/>.
4830+
4831+"""Test the package state."""
4832+
4833+from __future__ import absolute_import, unicode_literals
4834+
4835+__metaclass__ = type
4836+__all__ = [
4837+ 'test_suite',
4838+ ]
4839+
4840+
4841+import os
4842+import difflib
4843+import tempfile
4844+import textwrap
4845+import unittest
4846+
4847+from computerjanitord.state import State
4848+
4849+NL = '\n'
4850+
4851+
4852+class TestState(unittest.TestCase):
4853+ """Test the `State` class."""
4854+
4855+ def setUp(self):
4856+ self.state = State()
4857+ # Create a temporary file with some enabled and disabled packages.
4858+ fd, self._state_file = tempfile.mkstemp()
4859+ os.close(fd)
4860+ with open(self._state_file, 'w') as fp:
4861+ print >> fp, textwrap.dedent("""\
4862+ [deb:foo]
4863+ ignore: false
4864+ [deb:bar]
4865+ ignore: true
4866+ [deb:baz]
4867+ ignore: true
4868+ """)
4869+ fd, self._state_file_old = tempfile.mkstemp()
4870+ os.close(fd)
4871+ with open(self._state_file_old, 'w') as fp:
4872+ print >> fp, textwrap.dedent("""\
4873+ [deb:qux]
4874+ enabled: false
4875+ [deb:fno]
4876+ enabled: true
4877+ [deb:bla]
4878+ enabled: true
4879+ """)
4880+ fd, self._write_file = tempfile.mkstemp()
4881+ os.close(fd)
4882+
4883+ def tearDown(self):
4884+ os.remove(self._state_file)
4885+ os.remove(self._state_file_old)
4886+ os.remove(self._write_file)
4887+
4888+ def assertEqualNdiff(self, expected, got):
4889+ expected_lines = expected.splitlines()
4890+ got_lines = got.splitlines()
4891+ self.assertEqual(
4892+ expected, got,
4893+ '\n' + NL.join(difflib.ndiff(expected_lines, got_lines)))
4894+
4895+ def test_initially_no_previously_ignored(self):
4896+ self.assertEqual(self.state.ignore, set())
4897+
4898+ def test_load_state(self):
4899+ self.state.load(self._state_file)
4900+ self.assertEqual(self.state.ignore, set(('deb:bar', 'deb:baz')))
4901+
4902+ def test_backward_compatibility_file_format(self):
4903+ # Here, enabled:false means to ignore the package.
4904+ self.state.load(self._state_file_old)
4905+ self.assertEqual(self.state.ignore, set(('deb:qux',)))
4906+
4907+ def test_ignore(self):
4908+ self.state.ignore.add('deb:buz')
4909+ self.state.ignore.add('deb:baz')
4910+ self.assertEqual(self.state.ignore, set(('deb:buz', 'deb:baz')))
4911+
4912+ def test_more_ignores(self):
4913+ self.state.load(self._state_file)
4914+ self.state.ignore.add('deb:buz')
4915+ self.state.ignore.add('deb:baz')
4916+ self.assertEqual(self.state.ignore,
4917+ set(('deb:bar', 'deb:buz', 'deb:baz')))
4918+
4919+ def test_unignore(self):
4920+ self.state.load(self._state_file)
4921+ self.state.ignore.remove('deb:bar')
4922+ self.assertEqual(self.state.ignore, set(('deb:baz',)))
4923+
4924+ def test_write(self):
4925+ self.state.load(self._state_file)
4926+ self.state.ignore.add('deb:buz')
4927+ self.state.ignore.remove('deb:baz')
4928+ self.state.save(self._write_file)
4929+ with open(self._write_file) as fp:
4930+ got = fp.read()
4931+ expected = textwrap.dedent("""\
4932+ [deb:bar]
4933+ ignore: true
4934+
4935+ [deb:buz]
4936+ ignore: true
4937+
4938+ """)
4939+ self.assertEqualNdiff(expected, got)
4940+
4941+ def test_write_new_format(self):
4942+ self.state.load(self._state_file_old)
4943+ self.state.save(self._write_file)
4944+ with open(self._write_file) as fp:
4945+ got = fp.read()
4946+ expected = textwrap.dedent("""\
4947+ [deb:qux]
4948+ ignore: true
4949+
4950+ """)
4951+ self.assertEqualNdiff(expected, got)
4952+
4953+
4954+def test_suite():
4955+ suite = unittest.TestSuite()
4956+ suite.addTests(unittest.makeSuite(TestState))
4957+ return suite
4958
4959=== added file 'computerjanitord/tests/test_whitelist.py'
4960--- computerjanitord/tests/test_whitelist.py 1970-01-01 00:00:00 +0000
4961+++ computerjanitord/tests/test_whitelist.py 2010-03-10 21:40:30 +0000
4962@@ -0,0 +1,119 @@
4963+# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
4964+#
4965+# This program is free software: you can redistribute it and/or modify
4966+# it under the terms of the GNU General Public License as published by
4967+# the Free Software Foundation, version 3 of the License.
4968+#
4969+# This program is distributed in the hope that it will be useful,
4970+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4971+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4972+# GNU General Public License for more details.
4973+#
4974+# You should have received a copy of the GNU General Public License
4975+# along with this program. If not, see <http://www.gnu.org/licenses/>.
4976+
4977+"""Test the whitelister."""
4978+
4979+from __future__ import absolute_import, unicode_literals
4980+
4981+__metaclass__ = type
4982+__all__ = [
4983+ 'test_suite',
4984+ ]
4985+
4986+
4987+import os
4988+import shutil
4989+import tempfile
4990+import unittest
4991+
4992+from computerjanitord.whitelist import Whitelist
4993+
4994+
4995+class MockCruft:
4996+ """Mock cruft that supports the required `get_name()` interface."""
4997+
4998+ def __init__(self, shortname):
4999+ self.name = 'foo:' + shortname
5000+
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches