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

Proposed by Barry Warsaw
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 Pending
computer-janitor-hackers 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.
Revision history for this message
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
=== added file '.bzrignore'
--- .bzrignore 1970-01-01 00:00:00 +0000
+++ .bzrignore 2010-03-10 21:40:30 +0000
@@ -0,0 +1,5 @@
1build
2computer_janitor.egg-info
3computerjanitord/tests/data/var/cache
4computerjanitord/tests/data/var/lib/dpkg
5computerjanitord/tests/data/var/lib/apt/lists/partial
06
=== modified file 'computer-janitor'
--- computer-janitor 2009-08-18 13:48:50 +0000
+++ computer-janitor 2010-03-10 21:40:30 +0000
@@ -1,7 +1,6 @@
1#!/usr/bin/python1#!/usr/bin/python
2#2#
3# computer-janitor - clean up a Unix-like operating system3# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
4# Copyright (C) 2008, 2009 Canonical, Ltd.
5#4#
6# This program is free software: you can redistribute it and/or modify5# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by6# it under the terms of the GNU General Public License as published by
@@ -15,21 +14,14 @@
15# You should have received a copy of the GNU General Public License14# You should have received a copy of the GNU General Public License
16# along with this program. If not, see <http://www.gnu.org/licenses/>.15# along with this program. If not, see <http://www.gnu.org/licenses/>.
1716
1817"""Start the command-line computer janitor."""
19import logging18
20import os19from __future__ import absolute_import, unicode_literals
21import sys20
22import traceback21__metaclass__ = type
2322__all__ = [
24import computerjanitor23 ]
25import computerjanitorapp24
2625
2726from computerjanitorapp.maincli import main
28try:27main()
29 app = computerjanitorapp.Application()
30 app.run(computerjanitorapp.CommandLineUserInterface)
31except computerjanitor.Exception, e:
32 logging.debug(unicode(traceback.format_exc()))
33 logging.error(e._str)
34 sys.exit(1)
35
3628
=== modified file 'computer-janitor-gtk'
--- computer-janitor-gtk 2009-02-11 17:21:24 +0000
+++ computer-janitor-gtk 2010-03-10 21:40:30 +0000
@@ -1,7 +1,7 @@
1#!/usr/bin/python1#!/usr/bin/python
2#2#
3# computer-janitor-gtk - clean up a Unix-like operating system (GTK version)3# computer-janitor-gtk - clean up a Unix-like operating system (GTK version)
4# Copyright (C) 2008, 2009 Canonical, Ltd.4# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
5#5#
6# This program is free software: you can redistribute it and/or modify6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by7# it under the terms of the GNU General Public License as published by
@@ -15,20 +15,29 @@
15# You should have received a copy of the GNU General Public License15# You should have received a copy of the GNU General Public License
16# along with this program. If not, see <http://www.gnu.org/licenses/>.16# along with this program. If not, see <http://www.gnu.org/licenses/>.
1717
1818"""Start the gtk computer janitor."""
19import logging19
20from __future__ import absolute_import, unicode_literals
21
22__metaclass__ = type
23__all__ = [
24 ]
25
26
20import os27import os
21import sys28import sys
29import logging
22import traceback30import traceback
23
24import computerjanitor31import computerjanitor
25import computerjanitorapp32
33from computerjanitorapp.maingtk import main
2634
2735
28try:36try:
29 app = computerjanitorapp.Application()37 main()
30 app.run(computerjanitorapp.GtkUserInterface)38except KeyboardInterrupt:
31except computerjanitor.Exception, e:39 pass
40except computerjanitor.Exception as error:
32 logging.debug(unicode(traceback.format_exc()))41 logging.debug(unicode(traceback.format_exc()))
33 logging.error(unicode(e))42 logging.error(unicode(error))
34 sys.exit(1)43 sys.exit(1)
3544
=== modified file 'computerjanitorapp/__init__.py'
--- computerjanitorapp/__init__.py 2009-09-09 14:36:30 +0000
+++ computerjanitorapp/__init__.py 2010-03-10 21:40:30 +0000
@@ -1,5 +1,4 @@
1# __init__.py for computerjanitorapp1# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
2# Copyright (C) 2008, 2009 Canonical, Ltd.
3#2#
4# This program is free software: you can redistribute it and/or modify3# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by4# it under the terms of the GNU General Public License as published by
@@ -13,24 +12,29 @@
13# You should have received a copy of the GNU General Public License12# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.13# along with this program. If not, see <http://www.gnu.org/licenses/>.
1514
1615"""computerjanitorapp module."""
17VERSION = "1.13.3"16
1817from __future__ import absolute_import, unicode_literals
1918
20# Set up gettext. This needs to be before the import statements below19__metaclass__ = type
21# so that if any modules call it right after importing, they find20__all__ = [
22# setup_gettext.21 '__version__',
2322 'setup_gettext',
23 ]
24
25
26__version__ = '2.0'
27
28
29# Set up gettext.
24def setup_gettext():30def setup_gettext():
25 """Set up gettext for a module.31 """Set up gettext for a module.
2632
27 Return a method to be used for looking up translations. Usage:33 Return a method to be used for looking up translations. Usage:
2834
29 import computerjanitorapp35 >>> import computerjanitorapp
30 _ = computerjanitorapp.setup_gettext()36 >>> _ = computerjanitorapp.setup_gettext()
31
32 """37 """
33
34 import gettext38 import gettext
35 import os39 import os
3640
@@ -38,11 +42,3 @@
38 localedir = os.environ.get('LOCPATH', None)42 localedir = os.environ.get('LOCPATH', None)
39 t = gettext.translation(domain, localedir=localedir, fallback=True)43 t = gettext.translation(domain, localedir=localedir, fallback=True)
40 return t.ugettext44 return t.ugettext
41
42
43from app import Application
44from ui import UserInterface
45from ui_cli import CommandLineUserInterface
46from ui_gtk import GtkUserInterface
47from state import State
48from terminalsize import get_terminal_size
4945
=== removed file 'computerjanitorapp/app.py'
--- computerjanitorapp/app.py 2009-11-10 20:01:34 +0000
+++ computerjanitorapp/app.py 1970-01-01 00:00:00 +0000
@@ -1,192 +0,0 @@
1# app.py - the application main program of Computer Janitor
2# Copyright (C) 2008, 2009 Canonical, Ltd.
3#
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation, version 3 of the License.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16
17import ConfigParser
18import logging
19import logging.handlers
20import optparse
21import os
22import sys
23
24import computerjanitor
25import computerjanitorapp
26_ = computerjanitorapp.setup_gettext()
27
28
29DEFAULT_PLUGINS_DIRS = "/usr/share/computerjanitor/plugins"
30DEFAULT_STATE_FILE = "/var/lib/computer-janitor/state.dat"
31
32
33class SourcesListProblem(computerjanitor.Exception):
34
35 def __init__(self, missing_package):
36 self._str = _("Essential package %s is missing. There may be "
37 "problems with apt sources.list or Packages files may "
38 "be missing?") % missing_package
39
40
41class AsciiFormatter(logging.Formatter):
42
43 def format(self, record): # pragma: no cover
44 msg = logging.Formatter.format(self, record)
45 return msg.encode('ascii', 'replace')
46
47
48def setup_logging(): # pragma: no cover
49 if os.environ.get("COMPUTER_JANITOR_DEBUG", None) == "yes":
50 level = logging.DEBUG
51 elif os.environ.get("COMPUTER_JANITOR_UNITTEST", None) == "yes":
52 level = logging.CRITICAL
53 else:
54 level = logging.INFO
55 logging.basicConfig(level=level, format="%(levelname)s: %(message)s")
56
57 try:
58 formatter = AsciiFormatter(
59 "computer-janitor %(levelname)s: %(message)s")
60 handler = logging.handlers.SysLogHandler("/dev/log")
61 handler.setLevel(level)
62 handler.setFormatter(formatter)
63 logging.getLogger().addHandler(handler)
64 except:
65 # We're OK with something going wrong.
66 logging.debug(_("Logging to syslog cannot be set up."))
67
68
69class Application(object):
70
71 """The main class for the program."""
72
73 whitelist_dirs = ["/etc/computer-janitor.d"]
74
75 def __init__(self, apt=None):
76 self.state = computerjanitorapp.State()
77 self.parser = self.create_option_parser()
78 self.apt = computerjanitor.apt
79 if apt is not None:
80 self.apt = apt
81 self.refresh_apt_cache()
82
83 # The Plugin API requires that we have a config that is a
84 # ConfigParser. We don't use it for anything, for now.
85 self.config = ConfigParser.ConfigParser()
86
87 def run(self, ui_class,
88 plugin_manager_class=computerjanitor.PluginManager):
89 setup_logging()
90 logging.debug(_("Running application, with:"))
91 logging.debug(" plugin_manager_class=%s" % plugin_manager_class)
92
93 options, args = self.parse_options()
94
95 pluginpath = os.environ.get("COMPUTER_JANITOR_PLUGINS",
96 DEFAULT_PLUGINS_DIRS)
97 plugindirs = pluginpath.split(":")
98 pm = plugin_manager_class(self, plugindirs)
99
100 ui = ui_class(self, pm)
101 ui.run(options, args)
102
103 def create_option_parser(self): # pragma: no cover
104 parser = optparse.OptionParser(version="%%prog %s" %
105 computerjanitorapp.VERSION)
106
107 parser.usage = _("""
108%prog [options] find
109%prog [options] cleanup [CRUFT]...
110%prog [options] ignore [CRUFT]...
111%prog [options] unignore [CRUFT]...
112
113%prog finds and removes cruft from your system.
114
115Cruft is anything that shouldn't be on the system, but is. Stretching
116the definition, it is also things that should be on the system, but
117aren't.""")
118
119 parser.add_option("--all", action="store_true",
120 help=_("Make the 'cleanup' command remove all "
121 "packages, if none are given on the "
122 "command line."))
123
124 parser.add_option("--state-file", metavar="FILE",
125 default=DEFAULT_STATE_FILE,
126 help=_("Store state of each piece of cruft in "
127 "FILE. (Default is %default)."))
128
129 parser.add_option("--no-act", action="store_true",
130 help=_("Don't actually remove anything, just "
131 "pretend to do so. This is useful for "
132 "testing stuff."))
133
134 parser.add_option("--verbose", action="store_true",
135 help=_("Verbose operation: make find show an "
136 "explanation for each piece of cruft "
137 "found."))
138
139 return parser
140
141 def parse_options(self, args=None):
142 return self.parser.parse_args(args=args)
143
144 def refresh_apt_cache(self):
145 self.apt_cache = self.apt.Cache()
146 self.apt_cache._depcache.ReadPinFile("/var/lib/synaptic/preferences")
147
148 def verify_apt_cache(self):
149 for name in ["dash", "gzip"]:
150 if name not in self.apt_cache:
151 raise SourcesListProblem(name)
152 if not any(v.downloadable for v in self.apt_cache[name].versions):
153 raise SourcesListProblem(name)
154
155 def whitelist_files(self, dirnames):
156 """Find files with whitelists in a list of directories.
157
158 The directory is scanned for files ending in ".whitelist".
159 Subdirectories are not scanned.
160
161 """
162
163 list = []
164 for dirname in dirnames:
165 try:
166 basenames = os.listdir(dirname)
167 whitelists = [x for x in basenames if x.endswith(".whitelist")]
168 list += [os.path.join(dirname, x) for x in whitelists]
169 except os.error:
170 pass
171
172 return list
173
174 def whitelisted_cruft(self, dirnames=None):
175 """Return list of cruft that is whitelisted."""
176
177 dirnames = dirnames or self.whitelist_dirs
178
179 list = []
180 for filename in self.whitelist_files(dirnames):
181 f = file(filename, "r")
182 for line in f.readlines():
183 line = line.strip()
184 if line and not line.startswith("#"):
185 list.append(line)
186 f.close()
187
188 return list
189
190 def remove_whitelisted(self, crufts, dirnames=None):
191 whitelisted = self.whitelisted_cruft(dirnames=dirnames)
192 return [c for c in crufts if c.get_name() not in whitelisted]
1930
=== removed file 'computerjanitorapp/app_tests.py'
--- computerjanitorapp/app_tests.py 2009-08-14 12:37:25 +0000
+++ computerjanitorapp/app_tests.py 1970-01-01 00:00:00 +0000
@@ -1,178 +0,0 @@
1# app_tests.py - unit tests for app.py
2# Copyright (C) 2008, 2009 Canonical, Ltd.
3#
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation, version 3 of the License.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16
17import os
18import shutil
19import tempfile
20import unittest
21
22import computerjanitor
23import computerjanitorapp
24
25
26class MockUI(object):
27
28 def __init__(self, testcase, app, pm):
29 self.testcase = testcase
30
31 def run(self, options, args):
32 self.testcase.ui_ran = True
33
34
35class MockPackage(object):
36
37 def __init__(self, downloadable=False):
38 self.downloadable = downloadable
39 self.candidate = self
40
41
42class MockApt(dict):
43
44 def __init__(self):
45 self._depcache = self
46
47 def ReadPinFile(self, filename):
48 pass
49
50 def Cache(self):
51 return self
52
53
54class MockCruft(object):
55
56 def __init__(self, name):
57 self.name = name
58
59 def get_name(self):
60 return self.name
61
62
63class ApplicationTests(unittest.TestCase):
64
65 def setUp(self):
66 self.mock_apt = MockApt()
67 self.app = computerjanitorapp.Application(apt=self.mock_apt)
68 self.app.apt_cache["dash"] = MockPackage(downloadable=True)
69 self.app.apt_cache["gzip"] = MockPackage(downloadable=True)
70
71 def testSetsUpAptAttributeCorrectly(self):
72 self.assertEqual(self.app.apt, self.mock_apt)
73
74 def testSetsUpAndReturnsState(self):
75 self.assert_(self.app.state)
76
77 def testSetsUpAptCacheWhenRequested(self):
78 self.assertNotEqual(self.app.apt_cache, None)
79
80 def testSetsOptionDefaultsCorrectly(self):
81 options, args = self.app.parse_options(args=[])
82 self.assertEqual(args, [])
83 self.assertEqual(options.all, None)
84 self.assertEqual(options.state_file,
85 "/var/lib/computer-janitor/state.dat")
86 self.assertEqual(options.no_act, None)
87
88 def testAcceptsDashDashAllOption(self):
89 options, args = self.app.parse_options(args=["--all"])
90 self.assertEqual(options.all, True)
91
92 def testAcceptsDashDashStateFileOption(self):
93 options, args = self.app.parse_options(args=["--state-file=foo"])
94 self.assertEqual(options.state_file, "foo")
95
96 def testAcceptsDashDashNoActOption(self):
97 options, args = self.app.parse_options(args=["--no-act"])
98 self.assertEqual(options.no_act, True)
99
100 def testRunsUserInterface(self):
101
102 def pm_class(app, plugin_dirs):
103 self.pm_ran = True
104
105 def ui_class(app, pm):
106 return MockUI(self, app, pm)
107
108 self.pm_ran = False
109 self.ui_ran = False
110 self.app.run(ui_class=ui_class, plugin_manager_class=pm_class)
111 self.assert_(self.ui_ran)
112
113 def testAcceptsAptCacheWhenEssentialPackagesAreThere(self):
114 self.assertEqual(self.app.verify_apt_cache(), None)
115
116 def testRejectsAptCacheWhenDashIsMissing(self):
117 del self.app.apt_cache["dash"]
118 self.assertRaises(computerjanitor.Exception,
119 self.app.verify_apt_cache)
120
121 def testRejectsAptCacheWhenGzipIsMissing(self):
122 del self.app.apt_cache["gzip"]
123 self.assertRaises(computerjanitor.Exception,
124 self.app.verify_apt_cache)
125
126 def testRejectsAptCacheWhenDashIsNotDownloadable(self):
127 self.app.apt_cache["dash"].candidate.downloadable = False
128 self.assertRaises(computerjanitor.Exception,
129 self.app.verify_apt_cache)
130
131 def testRejectsAptCacheWhenGzipIsNotDownloadable(self):
132 self.app.apt_cache["dash"].candidate.downloadable = False
133 self.assertRaises(computerjanitor.Exception,
134 self.app.verify_apt_cache)
135
136 def testSetsDefaultListOfWhitelistDirectoriesCorrectly(self):
137 self.assert_("/etc/computer-janitor.d" in self.app.whitelist_dirs)
138
139 def testReturnsEmptyWhitelistByDefault(self):
140 dirname = tempfile.mkdtemp()
141 whitelist = self.app.whitelisted_cruft(dirnames=[dirname])
142 shutil.rmtree(dirname)
143 self.assertEqual(whitelist, [])
144
145 def testDoesNotMindNonExistentWhitelistDirectory(self):
146 dirname = tempfile.mkdtemp()
147 subdir = os.path.join(dirname, "foo")
148 whitelist = self.app.whitelisted_cruft(dirnames=[subdir])
149 shutil.rmtree(dirname)
150 self.assertEqual(whitelist, [])
151
152 def testReadsWhitelistFilesCorrectly(self):
153 dirname = tempfile.mkdtemp()
154 temp1 = os.path.join(dirname, "foo.whitelist")
155 temp2 = os.path.join(dirname, "foo.whitelist~")
156
157 file(temp1, "w").write("deb:foo\n")
158 file(temp2, "w").write("deb:bar\n")
159
160 whitelist = self.app.whitelisted_cruft(dirnames=[dirname])
161 shutil.rmtree(dirname)
162 self.assertEqual(whitelist, ["deb:foo"])
163
164 def testFindsCorrectWhitelistFilesInDotDDirectory(self):
165 dirname = tempfile.mkdtemp()
166 file(os.path.join(dirname, "foo.whitelist"), "w").write("deb:foo\n")
167 file(os.path.join(dirname, "foo.whitelist~"), "w").write("deb:bar\n")
168 list = self.app.whitelist_files([dirname])
169 shutil.rmtree(dirname)
170 self.assertEqual(list, [os.path.join(dirname, "foo.whitelist")])
171
172 def testRemovesWhitelistedCruftCorrectly(self):
173 crufts = [MockCruft("deb:foo"), MockCruft("deb:bar")]
174 dirname = tempfile.mkdtemp()
175 file(os.path.join(dirname, "foo.whitelist"), "w").write("deb:foo\n")
176 crufts2 = self.app.remove_whitelisted(crufts, dirnames=[dirname])
177 shutil.rmtree(dirname)
178 self.assertEqual(crufts2, crufts[1:])
1790
=== added file 'computerjanitorapp/areyousure.py'
--- computerjanitorapp/areyousure.py 1970-01-01 00:00:00 +0000
+++ computerjanitorapp/areyousure.py 2010-03-10 21:40:30 +0000
@@ -0,0 +1,76 @@
1# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, version 3 of the License.
6#
7# This program is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License
13# along with this program. If not, see <http://www.gnu.org/licenses/>.
14
15"""Confirm some actions."""
16
17from __future__ import absolute_import, unicode_literals
18
19__metaclass__ = type
20__all__ = [
21 'AreYouSure',
22 ]
23
24
25import gtk
26
27from computerjanitorapp import setup_gettext
28from computerjanitorapp.store import ListStoreColumns
29
30_ = setup_gettext()
31NL = '\n'
32
33
34class AreYouSure:
35 """Confirmation of destructive actions."""
36
37 def __init__(self, ui):
38 self._ui = ui
39
40 def verify(self):
41 """Confirm package removal."""
42 # Start by getting all the active, non-ignored cruft. These are the
43 # candidates for removal.
44 cleanable_cruft = self._ui.get_cleanable_cruft()
45 # It would be nice if we could produce better messages than these, but
46 # that would require a richer interface to the dbus service, and
47 # probably to the cruft plugin architecture underneath that.
48 message = _('Are you sure you want to clean your system?')
49 dialog = gtk.MessageDialog(
50 parent=self._ui.widgets['window'],
51 type=gtk.MESSAGE_WARNING,
52 buttons=gtk.BUTTONS_NONE,
53 message_format=message)
54 dialog.set_title(_('Clean up'))
55 package_cruft_count = sum(1 for cruft_name, is_pkg in cleanable_cruft
56 if is_pkg)
57 other_cruft_count = len(cleanable_cruft) - package_cruft_count
58 messages = []
59 if package_cruft_count > 0:
60 messages = [_('<b>Software packages to remove: {packages}</b>.'),
61 _('\nRemoving packages that are still in use can '
62 'cause errors.')]
63 ok_button = _('Remove packages')
64 if other_cruft_count > 0:
65 messages.insert(1, _('Non-package items to remove: {others}.'))
66 ok_button = _('Clean up')
67 message = NL.join(messages).format(packages=package_cruft_count,
68 others=other_cruft_count)
69 dialog.format_secondary_markup(message)
70 dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CLOSE)
71 dialog.add_button(ok_button, gtk.RESPONSE_YES)
72 # Show the dialog and get the user's response.
73 dialog.show_all()
74 response = dialog.run()
75 dialog.hide()
76 return response == gtk.RESPONSE_YES
077
=== renamed file 'computerjanitorapp/ui_cli.py' => 'computerjanitorapp/maincli.py'
--- computerjanitorapp/ui_cli.py 2009-08-18 14:10:42 +0000
+++ computerjanitorapp/maincli.py 2010-03-10 21:40:30 +0000
@@ -1,5 +1,4 @@
1# ui_cli.py - command line user interface1# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
2# Copyright (C) 2008, 2009 Canonical, Ltd.
3#2#
4# This program is free software: you can redistribute it and/or modify3# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by4# it under the terms of the GNU General Public License as published by
@@ -13,168 +12,296 @@
13# You should have received a copy of the GNU General Public License12# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.13# along with this program. If not, see <http://www.gnu.org/licenses/>.
1514
1615"""Command line user interface."""
17import os16
18import logging17from __future__ import absolute_import, unicode_literals
18
19__metaclass__ = type
20__all__ = [
21 'main',
22 ]
23
24
19import sys25import sys
26import dbus
27import gobject
28import argparse
20import textwrap29import textwrap
21import traceback30
2231from dbus.mainloop.glib import DBusGMainLoop
23import computerjanitor32
24import computerjanitorapp33from computerjanitorapp import __version__, setup_gettext
25_ = computerjanitorapp.setup_gettext()34from computerjanitorapp.terminalsize import get_terminal_size
2635from computerjanitorapp.utilities import format_size
2736from computerjanitord.service import DBUS_INTERFACE_NAME
28class UnknownCommand(computerjanitor.Exception):37_ = setup_gettext()
2938
30 def __init__(self, name):39
31 self._str = _("Unknown command: %s") % name40class Options:
3241 """Command line option parser."""
3342 def __init__(self, runner):
34class UnknownCruft(computerjanitor.Exception):43 """Parse the command line options.
3544
36 def __init__(self, name):45 :param runner: The class implementing the sub-commands.
37 self._str = _("Unknown cruft: %s") % name46 :type runner: `Runner`
3847 """
3948 self.runner = runner
40class MustBeRoot(computerjanitor.Exception):49 # Parse the arguments.
50 self.parser = argparse.ArgumentParser(
51 description=_("""\
52 Find and remove cruft from your system.
53
54 Cruft is anything that shouldn't be on your system, but is.
55 Stretching the definition, cruft is also things that should be on
56 your system but aren't."""),
57 version=__version__)
58 subparser = self.parser.add_subparsers(title='Commands')
59 # The 'find' subcommand.
60 command = subparser.add_parser(
61 'find', help=_('Find and display all cruft found on your system.'))
62 command.add_argument(
63 '-v', '--verbose', action='store_true',
64 help=_("""\
65 Display a detailed explanation for each piece of cruft found."""))
66 command.add_argument(
67 '-i', '--ignored', action='store_true',
68 help=_('Find and display only the ignored cruft.'))
69 command.add_argument(
70 '-r', '--removable', action='store_true',
71 help=_('Find and display only the removable cruft.'))
72 command.add_argument(
73 '-s', '--short', action='store_true',
74 help=_('Display only the package names. Do not use with -v.'))
75 command.set_defaults(func=self.runner.find)
76 # The 'ignore' subcommand.
77 command = subparser.add_parser(
78 'ignore', help=_("""\
79 Ignore a piece of cruft so that it is not cleaned up."""))
80 command.add_argument(
81 'cruft', nargs=1,
82 help=_('The name of the cruft to ignore.'))
83 command.set_defaults(func=self.runner.ignore)
84 # The 'unignore' subcommand.
85 command = subparser.add_parser(
86 'unignore', help=_("""\
87 Unignore a piece of cruft so that it will be cleaned up."""))
88 command.add_argument(
89 'cruft', nargs=1,
90 help=_('The name of the cruft to unignore.'))
91 command.set_defaults(func=self.runner.unignore)
92 # The 'clean' subcommand.
93 command = subparser.add_parser(
94 'clean',
95 help=_('Remove the selected cruft from the system.'))
96 command.add_argument(
97 '-a', '--all', action='store_true',
98 help=_('Clean up all unignored cruft.'))
99 command.add_argument(
100 '-v', '--verbose', action='store_true',
101 help=_("Provide more details on what's being cleaned."))
102 command.add_argument(
103 'cruft', nargs='?',
104 help=_("""\
105 The name of the cruft to clean up. Do not use if specifying
106 --all."""))
107 command.set_defaults(func=self.runner.clean)
108 # Parse the arguments and execute the subcommand.
109 self.arguments = self.parser.parse_args()
110
111
112class Runner:
113 """Implementations of subcommands."""
41114
42 def __init__(self):115 def __init__(self):
43 self._str = _("computer-janitor must be run as root, sorry.")116 # Connect to the dbus service.
44117 system_bus = dbus.SystemBus()
45118 proxy = system_bus.get_object(DBUS_INTERFACE_NAME, '/')
46class CommandLineUserInterface(computerjanitorapp.UserInterface):119 self.janitord = dbus.Interface(
47120 proxy, dbus_interface=DBUS_INTERFACE_NAME)
48 def run(self, options, args):121 # Connect to the signal the server will emit when cleaning up.
49 if self.mustberoot and os.getuid() != 0:122 self.janitord.connect_to_signal('cleanup_status', self._clean_working)
50 raise MustBeRoot()123 # This will get backpatched by __main__. We need it to produce error
51124 # messages from the argparser.
52 self.app.verify_apt_cache()125 self.options = None
53 126 # The main loop for asynchronous calls and signal reception.
54 dict = {127 self.loop = gobject.MainLoop()
55 "find": self.show_cruft,128
56 "cleanup": self.cleanup,129 def _error(self, message):
57 "ignore": self.ignore,130 """Generate a parser error and exit.
58 "unignore": self.unignore,131
59 "help": self.help,132 :param message: The error message.
60 }133 :type message: string
61134 """
62 if args:135 self.options.parser.error(message)
63 cmd = args[0]136 # No return.
64 args = args[1:]137
65 else:138 def find(self, arguments):
66 cmd = "help"139 """Find and display all cruft.
67 args = []140
68 141 :param arguments: Command line options.
69 if cmd in dict:142 """
70 app = self.app143 # Cruft will be prefixed by 'removable' if it is not being ignored.
71 app.state.load(options.state_file)144 ignored = set(self.janitord.ignored())
72 try:145 cruft_names = set(self.janitord.find())
73 dict[cmd](options, args)146 # Filter names based on option flags.
74 except Exception, e: # pragma: no cover147 if arguments.ignored:
75 logging.debug(unicode(traceback.format_exc()))148 cruft = sorted(cruft_names & ignored)
76 logging.critical(unicode(e))149 elif arguments.removable:
77 sys.exit(1)150 cruft = sorted(cruft_names - ignored)
78 else:151 else:
79 raise UnknownCommand(cmd)152 cruft = sorted(cruft_names)
80 153 # The prefix will either be 'ignored' or 'removable' however this
81 def find_cruft(self):154 # string will be translated, so calculate the prefix size in the
82 list = []155 # native language, then add two columns of separator, followed by the
83 for plugin in self.pm.get_plugins():156 # cruft name.
84 for cruft in plugin.get_cruft():157 prefixi = _('ignored')
85 list.append(cruft)158 prefixr = _('removable')
86 return self.app.remove_whitelisted(list)159 prefix_width = max(len(prefixi), len(prefixr))
87 160 # Long, short, shorter display.
88 def show_one_cruft(self, name, desc, s, width): #pragma: no cover161 if arguments.verbose and arguments.short:
89 if width is None:162 self._error('Use either -s or -v but not both.')
90 max = len(name) + len(desc or '')163 if arguments.short:
91 else:164 # This is the shorter output.
92 max = width165 for name in cruft:
93 max -= 9 # state column166 print name
94 max -= 2 # two spaces167 elif not arguments.verbose:
95 max -= 1 # avoid the last column, some terminals word wrap there168 # This is the short output.
96169 for name in cruft:
97 msg = ["%-9s %.*s" % (s, max, name)]170 prefix = (prefixi if name in ignored else prefixr)
98 if desc:171 # 10 spaces for the prefix
99 paras = desc.split("\n\n")172 print '{0:{1}} {2}'.format(prefix, prefix_width, name)
100 for para in paras:173 else:
101 for line in textwrap.wrap(para, max):174 # This is the verbose output. Start by getting the terminal's
102 msg.append("%9s %s" % ("", line))175 # size, though all we care about is the width.
103 msg.append("")176 rows, columns = get_terminal_size()
104 print ("\n".join(msg)).encode('utf-8') # FIXME: horrible kludge177 width = ((80 if columns in (None, 0) else columns)
105 # the above makes it possible to write out stuff even when178 - prefix_width # space for prefix
106 # it's going to somewhere like a pipe, because we explicitly179 - 2 # separator
107 # encode it as utf-8 first. This ignores the user's desired180 - 1 # avoid last column, some terminals wrap
108 # charset, which is bad, bad, bad, but I can't figure out a181 )
109 # way to make Python behave in a sensible way. *sigh*182 margin = ' ' * (prefix_width + 2)
110 # --liw, 2009-08-18183 for name in cruft:
111 184 prefix = (prefixi if name in ignored else prefixr)
112 def show_cruft(self, options, args):185 # 10 spaces for the prefix
113 list = []186 print '{0:{1}} {2}'.format(prefix, prefix_width, name)
114 maxname = ""187 # Print some details about the cruft.
115 state = self.app.state188 cruft_type, disk_usage = self.janitord.get_details(name)
116 for cruft in self.find_cruft():189 print '{0}{1} of type {2}'.format(
117 name = cruft.get_name()190 margin, format_size(disk_usage), cruft_type)
118 if options.verbose:191 # Get the description, wrap it to the available columns, and
119 desc = cruft.get_description() # pragma: no cover192 # display it line-by-line with the proper amount of leading
120 else:193 # spaces (prefix_width + 2).
121 desc = None194 description = self.janitord.get_description(name)
122 if state.is_enabled(name):195 if len(description) == 0:
123 s = _("removable")196 print
124 else:197 continue
125 s = _("ignored")198 paragraphs = description.split('\n\n')
126 list.append((name, desc, s))199 for paragraph in paragraphs:
127 if len(name) > len(maxname):200 for line in textwrap.wrap(paragraph, width):
128 maxname = name201 # 2010-02-09 barry: The original code forced the
129202 # output to utf-8, claiming that was necessary to
130 rows, cols = computerjanitorapp.get_terminal_size()203 # "write out stuff even when it's going to somewhere
131 for name, desc, s in sorted(list):204 # like a pipe [...] ignor[ing] the user's desired
132 self.show_one_cruft(name, desc, s, cols)205 # charset, which is bad, bad, bad...". This makes me
133206 # pretty uncomfortable, so I'd like to see a bug
134 def cleanup(self, options, args):207 # report before I copy that from the previous
135 crufts = {}208 # implementation.
136 for cruft in self.find_cruft():209 print '{0}{1}'.format(margin, line)
137 crufts[cruft.get_name()] = cruft210 # Paragraph separator.
138 211 print
139 if args:212
140 for arg in args:213 def ignore(self, arguments):
141 if arg not in crufts:214 """Ignore some cruft.
142 raise UnknownCruft(arg)215
143 elif options.all:216 :param arguments: Command line options.
144 state = self.app.state217 """
145 args = []218 assert len(arguments.cruft) == 1, 'Unexpected arguments'
146 for name in crufts.keys():219 self.janitord.ignore(arguments.cruft[0])
147 if state.is_enabled(name):220 self.janitord.save()
148 args.append(name)221
149 else:222 def unignore(self, arguments):
150 logging.info(_("Ignored: %s") % name)223 """Unignore some cruft.
151 224
152 for arg in args:225 :param arguments: Command line options.
153 if options.no_act:226 """
154 logging.info(_("Pretending to remove cruft: %s") % arg)227 assert len(arguments.cruft) == 1, 'Unexpected arguments'
155 else:228 self.janitord.unignore(arguments.cruft[0])
156 logging.info(_("Removing cruft: %s") % arg)229 self.janitord.save()
157 crufts[arg].cleanup()230
158 for plugin in self.pm.get_plugins():231 def _clean_reply(self):
159 if options.no_act:232 """The 'clean' operation has completed successfully."""
160 logging.info(_("Pretending to post-cleanup: %s") % plugin)233 self.loop.quit()
161 else:234 print 'done.'
162 logging.info(_("Post-cleanup: %s") % plugin)235
163 plugin.post_cleanup()236 def _clean_error(self, exception):
164237 """The 'clean' operation has failed."""
165 def ignore(self, options, cruft_names):238 self.loop.quit()
166 state = self.app.state239 print 'dbus service error:', exception
167 for cruft_name in cruft_names:240
168 state.disable(cruft_name)241 def _clean_working(self, cruft):
169 if not options.no_act:242 """The 'clean' operation is in progress.
170 state.save(options.state_file)243
171244 :param cruft: The cruft that is being cleaned up.
172 def unignore(self, options, cruft_names):245 :type cruft: string
173 state = self.app.state246 """
174 for cruft_name in cruft_names:247 verbose = self.options.arguments.verbose
175 state.enable(cruft_name)248 if cruft == '':
176 if not options.no_act:249 # We're done.
177 state.save(options.state_file)250 if not verbose:
178251 sys.stdout.write(' ')
179 def help(self, options, args):252 sys.stdout.flush()
180 self.app.parser.print_help()253 else:
254 if verbose:
255 print 'Working on', cruft
256 else:
257 sys.stdout.write('.')
258 sys.stdout.flush()
259
260 def clean(self, arguments):
261 """Clean up the cruft.
262
263 :param arguments: Command line options.
264 """
265 if arguments.all:
266 # You can't specify both --all and a cruft name.
267 if arguments.cruft is not None:
268 self._error('Specify a cruft name or --all, but not both')
269 # No return.
270 all_cruft = set(self.janitord.find())
271 ignored = set(self.janitord.ignored())
272 cleanable_cruft = all_cruft - ignored
273 else:
274 if arguments.cruft is None:
275 self._error('You must specify a cruft name, or use --all')
276 # No return.
277 cleanable_cruft = (arguments.cruft,)
278 # Make the asynchronous call because this can take a long time. We'll
279 # get status updates periodically. Note however that even though this
280 # is asynchronous, dbus still expects a response within a certain
281 # amount of time. We have no idea how long it will take to clean up
282 # the cruft though, so just crank the timeout up to some insanely huge
283 # number (of seconds).
284 self.janitord.clean(cleanable_cruft,
285 reply_handler=self._clean_reply,
286 error_handler=self._clean_error,
287 # If it takes longer than an hour, we're screwed.
288 timeout=3600)
289 # Start the main loop. This will exit when the remote operation is
290 # complete.
291 if not arguments.verbose:
292 sys.stdout.write('processing')
293 sys.stdout.flush()
294 self.loop.run()
295
296
297def main():
298 # We'll need a main loop to receive status signals from the dbus service.
299 # Don't start the main loop yet though, since we only need it for 'clean'
300 # commands.
301 DBusGMainLoop(set_as_default=True)
302 runner = Runner()
303 options = Options(runner)
304 # Backpatch runner because of circular references.
305 runner.options = options
306 # Execute the subcommand.
307 options.arguments.func(options.arguments)
181308
=== added file 'computerjanitorapp/maingtk.py'
--- computerjanitorapp/maingtk.py 1970-01-01 00:00:00 +0000
+++ computerjanitorapp/maingtk.py 2010-03-10 21:40:30 +0000
@@ -0,0 +1,32 @@
1# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, version 3 of the License.
6#
7# This program is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License
13# along with this program. If not, see <http://www.gnu.org/licenses/>.
14
15"""Command line user interface."""
16
17from __future__ import absolute_import, unicode_literals
18
19__metaclass__ = type
20__all__ = [
21 'main',
22 ]
23
24
25from computerjanitorapp.uigtk import UserInterface
26from dbus.mainloop.glib import DBusGMainLoop
27
28
29def main():
30 DBusGMainLoop(set_as_default=True)
31 ui = UserInterface()
32 ui.run()
033
=== removed file 'computerjanitorapp/state.py'
--- computerjanitorapp/state.py 2009-07-13 09:48:06 +0000
+++ computerjanitorapp/state.py 1970-01-01 00:00:00 +0000
@@ -1,56 +0,0 @@
1# state.py - store persistent state of crufts
2# Copyright (C) 2008, 2009 Canonical, Ltd.
3#
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation, version 3 of the License.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16
17import ConfigParser
18
19
20class State(object):
21
22 def __init__(self):
23 self._cp = ConfigParser.ConfigParser()
24 self._previously_ignored = set()
25
26 def load(self, filename):
27 self._cp.read(filename)
28 self._previously_ignored = set()
29 for cruft_name in self._cp.sections():
30 if not self.is_enabled(cruft_name):
31 self._previously_ignored.add(cruft_name)
32
33 def save(self, filename):
34 f = file(filename, "w")
35 self._cp.write(f)
36 f.close()
37
38 def is_enabled(self, cruft_name):
39 if self._cp.has_section(cruft_name):
40 return self._cp.getboolean(cruft_name, "enabled")
41 else:
42 return True
43
44 def enable(self, cruft_name):
45 if not self._cp.has_section(cruft_name):
46 self._cp.add_section(cruft_name)
47 self._cp.set(cruft_name, "enabled", "true")
48
49 def disable(self, cruft_name):
50 if not self._cp.has_section(cruft_name):
51 self._cp.add_section(cruft_name)
52 self._cp.set(cruft_name, "enabled", "false")
53
54 def was_previously_ignored(self, cruft_name):
55 return cruft_name in self._previously_ignored
56
570
=== removed file 'computerjanitorapp/state_tests.py'
--- computerjanitorapp/state_tests.py 2009-07-13 09:48:06 +0000
+++ computerjanitorapp/state_tests.py 1970-01-01 00:00:00 +0000
@@ -1,77 +0,0 @@
1# state_tests.py - unit tests for store.py
2# Copyright (C) 2008, 2009 Canonical, Ltd.
3#
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation, version 3 of the License.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16
17import os
18import tempfile
19import unittest
20
21import computerjanitorapp
22
23
24class StateTests(unittest.TestCase):
25
26 def setUp(self):
27 self.state = computerjanitorapp.State()
28
29 def testInitiallyEverythingIsEnabled(self):
30 self.assert_(self.state.is_enabled("foo"))
31
32 def testDisablesWhenAsked(self):
33 self.state.disable("foo")
34 self.assertFalse(self.state.is_enabled("foo"))
35
36 def testEnablesDisabledCruft(self):
37 self.state.disable("foo")
38 self.state.enable("foo")
39 self.assert_(self.state.is_enabled("foo"))
40
41 def testEnablesEnabledCruft(self):
42 self.state.enable("foo")
43 self.assert_(self.state.is_enabled("foo"))
44
45 def testSavesAndLoadsFiles(self):
46 fd, filename = tempfile.mkstemp()
47 os.close(fd)
48 self.state.enable("foo")
49 self.state.disable("bar")
50 self.state.save(filename)
51 self.state.disable("foo")
52 self.state.enable("bar")
53 self.state.load(filename)
54 self.assert_(self.state.is_enabled("foo"))
55 self.assertFalse(self.state.is_enabled("bar"))
56 os.remove(filename)
57
58 def testInitallyNothingIsPreviouslyIgnored(self):
59 self.assertFalse(self.state.was_previously_ignored("foo"))
60
61 def testRemembersWhatWasPreviouslyIgnored(self):
62 fd, filename = tempfile.mkstemp()
63 os.close(fd)
64
65 # Set a state to disabled.
66 state1 = computerjanitorapp.State()
67 state1.disable("foo")
68 state1.save(filename)
69
70 # Load new state.
71 state2 = computerjanitorapp.State()
72 state2.load(filename)
73
74 self.assert_(state2.was_previously_ignored("foo"))
75
76 os.remove(filename)
77
780
=== added file 'computerjanitorapp/store.py'
--- computerjanitorapp/store.py 1970-01-01 00:00:00 +0000
+++ computerjanitorapp/store.py 2010-03-10 21:40:30 +0000
@@ -0,0 +1,147 @@
1# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, version 3 of the License.
6#
7# This program is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License
13# along with this program. If not, see <http://www.gnu.org/licenses/>.
14
15"""Gtk ListStore model for backing the TreeViews of cruft."""
16
17from __future__ import absolute_import, unicode_literals
18
19__metaclass__ = type
20__all__ = [
21 'Store',
22 'optimize',
23 'unused',
24 ]
25
26
27import gtk
28import gobject
29
30
31# XXX 2010-03-04 barry: Use a munepy enum.
32class ListStoreColumns:
33 # The cruft name as known by the dbus service.
34 name = 0
35 # The short name of the cruft.
36 short_name = 1
37 # The text displayed in the cell.
38 text = 2
39 # Should the cruft be shown?
40 show = 3
41 # Is the cruft active locally (i.e. toggle button is set)?
42 active = 4
43 # Is the cruft being ignored in the dbus service?
44 server_ignored = 5
45 # Is the cruft's description expanded?
46 expanded = 6
47 # Is this cruft package cruft or some other kind of cruft?
48 is_package_cruft = 7
49
50
51class Store:
52 """The higher level wrapper around the gtk.ListStore."""
53
54 def __init__(self, janitord):
55 """Create the Store.
56
57 :param janitord: The `dbus.Interface` for talking to the Computer
58 Janitor dbus service.
59 """
60 self.janitord = janitord
61 # This ListStore is the backing data structure for the TreeViews in
62 # the main window. It holds information about the individual pieces
63 # of cruft. See ListStoreColumns for details.
64 #
65 # XXX 2010-03-04 barry: pygtk does not like 'unicode'.
66 self.store = gtk.ListStore(str, str, str, bool, bool, bool, bool, bool)
67
68 def find_cruft(self):
69 """Find cruft and populate the backing store."""
70 ignored_cruft = set(self.janitord.ignored())
71 all_cruft_names = set(self.janitord.find())
72 pkg_cruft_names = set(
73 cruft_name for cruft_name in all_cruft_names
74 if self.janitord.get_details(cruft_name)[0].lower()
75 == 'packagecruft')
76 self.store.clear()
77 # Cruft always starts out in the active, not-expanded state. Package
78 # cruft goes in the 'unused' column while non-package cruft goes in
79 # the 'optimize' column.
80 for cruft_name in all_cruft_names:
81 cruft_shortname = self.janitord.get_shortname(cruft_name)
82 self.store.append((
83 cruft_name, cruft_shortname,
84 # What is being displayed in the cell.
85 gobject.markup_escape_text(cruft_shortname),
86 # Show the cruft be shown?
87 cruft_name not in ignored_cruft,
88 # Is the cruft active locally?
89 cruft_name not in ignored_cruft,
90 # Is the cruft ignored in the dbus service?
91 cruft_name in ignored_cruft,
92 # Is the cruft's description expanded?
93 False,
94 # Is this package cruft?
95 cruft_name in pkg_cruft_names,
96 ))
97
98 # Forwards to the underlying store, for convenience. Really, these should
99 # use generic delegates, or clients should just use self.store.store.
100
101 def get_value(self, *args, **kws):
102 return self.store.get_value(*args, **kws)
103
104 def set_value(self, *args, **kws):
105 return self.store.set_value(*args, **kws)
106
107 def filter_new(self, *args, **kws):
108 return self.store.filter_new(*args, **kws)
109
110 def foreach(self, *args, **kws):
111 return self.store.foreach(*args, **kws)
112
113 def reorder(self, *args, **kws):
114 return self.store.reorder(*args, **kws)
115
116 def get_iter_first(self, *args, **kws):
117 return self.store.get_iter_first(*args, **kws)
118
119 def iter_next(self, *args, **kws):
120 return self.store.iter_next(*args, **kws)
121
122 def clear(self, *args, **kws):
123 return self.store.clear(*args, **kws)
124
125
126# Filter functions for use with TreeView column display.
127
128def unused(store, iter):
129 """True if the cruft is not being ignored and is package cruft.
130
131 :param store: The ListStore instance.
132 :param iter: The TreeIter instance.
133 """
134 show = store.get_value(iter, ListStoreColumns.show)
135 is_package_cruft = store.get_value(iter, ListStoreColumns.is_package_cruft)
136 return show and is_package_cruft
137
138
139def optimize(store, iter):
140 """True if the cruft is not package cruft.
141
142 :param store: The ListStore instance.
143 :param iter: The TreeIter instance.
144 """
145 show = store.get_value(iter, ListStoreColumns.show)
146 is_package_cruft = store.get_value(iter, ListStoreColumns.is_package_cruft)
147 return show and not is_package_cruft
0148
=== modified file 'computerjanitorapp/terminalsize.py'
--- computerjanitorapp/terminalsize.py 2008-09-22 21:39:57 +0000
+++ computerjanitorapp/terminalsize.py 2010-03-10 21:40:30 +0000
@@ -26,44 +26,39 @@
2626
27def get_terminal_size(fd=1):27def get_terminal_size(fd=1):
28 """Return size of terminal attached to the standard output.28 """Return size of terminal attached to the standard output.
29 29
30 Use ioctl(2) to query a terminal for its size, given a file30 Use ioctl(2) to query a terminal for its size, given a file descriptor
31 descriptor attached to the terminal. Return (None, None) if this31 attached to the terminal.
32 fails, otherwise a tuple (columns, rows).32
33 33 :param fd: Use the given file descriptor.
34 (The optional 'fd' argument can be set to whatever file34 :type fd: int
35 descriptor you want to use. This is useful for unit tests.)35 :return: The columns and rows representing the size of the terminal. If
36 36 this cannot be determined, None is returned for both values.
37 :rtype: 2-tuple
37 """38 """
3839
39 try:40 try:
40 # Do the ioctl call. termios.TIOCGWINSZ is the code to query41 # Do the ioctl call. termios.TIOCGWINSZ is the code to query terminal
41 # terminal size (see tty_ioctl(4), at least on Linux). We need42 # size (see tty_ioctl(4), at least on Linux). We need to give it a
42 # to give it a string of suitable size to use as the input43 # string of suitable size to use as the input buffer for ioctl.
43 # buffer for ioctl. Ioctl modifies the buffer and returns the44 # ioctl() modifies the buffer and returns the modified buffer as its
44 # modified buffer as its return value.45 # return value.
45 #46 #
46 # The manual page specifies a struct winsize to be used, which47 # The manual page specifies a struct winsize to be used, which
47 # consists of four unsigned shorts. We use struct.calcsize to48 # consists of four unsigned shorts. We use struct.calcsize() to
48 # compute the size of that.49 # compute the size of that.
49 # 50 #
50 # Note that Blake's original code assumes only the first two 51 # Note that Blake's original code assumes only the first two shorts in
51 # shorts in the struct are used, and that two shorts fit into52 # the struct are used, and that two shorts fit into four bytes, which
52 # four bytes, which is probably true for all the relevant53 # is probably true for all the relevant platforms, but is cramped
53 # platforms, but is cramped enough that it makes me feel icky.54 # enough that it makes me feel icky. Thus, I assume less. This will
54 # Thus, I assume less. This will still break if the contents55 # still break if the contents of the struct change, but since that
55 # of the struct change, but since that would change the system56 # would change the system call API, that's unlikely.
56 # call API, that's unlikely.
57
58 buflen = struct.calcsize('hhhh')57 buflen = struct.calcsize('hhhh')
59 buf = fcntl.ioctl(fd, termios.TIOCGWINSZ, '\0' * buflen)58 buf = fcntl.ioctl(fd, termios.TIOCGWINSZ, '\0' * buflen)
60
61 # ioctl returns a binary buffer that represents the struct59 # ioctl returns a binary buffer that represents the struct
62 # at the C level. We unpack it with struct.unpack.60 # at the C level. We unpack it with struct.unpack.
63 61 return tuple(struct.unpack('hhhh', buf)[:2])
64 tuple = struct.unpack('hhhh', buf)62 except Exception:
65 except:
66 # If anything went wrong, we give up and claim we don't know.63 # If anything went wrong, we give up and claim we don't know.
67 return None, None64 return None, None
68
69 return tuple[0], tuple[1]
7065
=== added directory 'computerjanitorapp/tests'
=== added file 'computerjanitorapp/tests/__init__.py'
=== added file 'computerjanitorapp/tests/test_all.py'
--- computerjanitorapp/tests/test_all.py 1970-01-01 00:00:00 +0000
+++ computerjanitorapp/tests/test_all.py 2010-03-10 21:40:30 +0000
@@ -0,0 +1,29 @@
1# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, version 3 of the License.
6#
7# This program is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License
13# along with this program. If not, see <http://www.gnu.org/licenses/>.
14
15"""Test suite for Computer Janitor."""
16
17import unittest
18
19from computerjanitorapp.tests import test_terminalsize
20from computerjanitorapp.tests import test_utilities
21from computerjanitord.tests.test_all import test_suite as cjd_suite
22
23
24def test_suite():
25 suite = unittest.TestSuite()
26 suite.addTests(test_terminalsize.test_suite())
27 suite.addTests(test_utilities.test_suite())
28 suite.addTests(cjd_suite())
29 return suite
030
=== renamed file 'computerjanitorapp/terminalsize_tests.py' => 'computerjanitorapp/tests/test_terminalsize.py'
--- computerjanitorapp/terminalsize_tests.py 2008-09-22 21:39:57 +0000
+++ computerjanitorapp/tests/test_terminalsize.py 2010-03-10 21:40:30 +0000
@@ -1,5 +1,4 @@
1# terminalsize_tests.py - unit tests for terminalsize.py1# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
2# Copyright (C) 2008 Canonical, Ltd.
3#2#
4# This program is free software: you can redistribute it and/or modify3# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by4# it under the terms of the GNU General Public License as published by
@@ -13,23 +12,33 @@
13# You should have received a copy of the GNU General Public License12# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.13# along with this program. If not, see <http://www.gnu.org/licenses/>.
1514
15"""Test calculation of terminal sizes."""
16
17from __future__ import absolute_import, unicode_literals
18
19__metaclass__ = type
20__all__ = [
21 'test_suite',
22 ]
23
1624
17import os25import os
18import unittest26import unittest
1927
20import terminalsize28from computerjanitorapp import terminalsize
2129
2230
23class GetTerminalSizeTests(unittest.TestCase):31class TestTerminalSize(unittest.TestCase):
2432 """Test calculation of terminal sizes."""
25 def testReturnsUnknownWhenQueryingDevNull(self):33
34 def test_returns_unknown_when_querying_dev_null(self):
26 fd = os.open("/dev/null", os.O_RDONLY)35 fd = os.open("/dev/null", os.O_RDONLY)
27 rows, cols = terminalsize.get_terminal_size(fd)36 rows, cols = terminalsize.get_terminal_size(fd)
28 os.close(fd)37 os.close(fd)
29 self.assertEqual(rows, None)38 self.assertEqual(rows, None)
30 self.assertEqual(cols, None)39 self.assertEqual(cols, None)
3140
32 def testReturnsTwoIntegersWhenStdoutIsATerminal(self):41 def test_returns_two_integers_when_stdout_is_a_terminal(self):
33 # We only run this check if stdout is a terminal.42 # We only run this check if stdout is a terminal.
34 # Unfortunately, there is no sensible way of checking the values.43 # Unfortunately, there is no sensible way of checking the values.
35 # But that's OK, they're lumberjacks.44 # But that's OK, they're lumberjacks.
@@ -37,3 +46,9 @@
37 rows, cols = terminalsize.get_terminal_size(1)46 rows, cols = terminalsize.get_terminal_size(1)
38 self.assertEqual(type(rows), int)47 self.assertEqual(type(rows), int)
39 self.assertEqual(type(cols), int)48 self.assertEqual(type(cols), int)
49
50
51def test_suite():
52 suite = unittest.TestSuite()
53 suite.addTests(unittest.makeSuite(TestTerminalSize))
54 return suite
4055
=== added file 'computerjanitorapp/tests/test_utilities.py'
--- computerjanitorapp/tests/test_utilities.py 1970-01-01 00:00:00 +0000
+++ computerjanitorapp/tests/test_utilities.py 2010-03-10 21:40:30 +0000
@@ -0,0 +1,71 @@
1# Copyright (C) 2010 Canonical, Ltd.
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, version 3 of the License.
6#
7# This program is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License
13# along with this program. If not, see <http://www.gnu.org/licenses/>.
14
15"""Test common utilities."""
16
17from __future__ import absolute_import, unicode_literals
18
19__metaclass__ = type
20__all__ = [
21 'test_suite',
22 ]
23
24
25import os
26import unittest
27
28from computerjanitorapp.utilities import format_size
29
30
31class TestUtilities(unittest.TestCase):
32 """Test common utilities."""
33
34 def test_format_negative(self):
35 self.assertRaises(AssertionError, format_size, -1)
36
37 def test_format_zero(self):
38 self.assertEqual(format_size(0), '0B')
39
40 def test_format_small(self):
41 self.assertEqual(format_size(500), '500B')
42
43 def test_format_1k(self):
44 self.assertEqual(format_size(1000), '1kB')
45
46 def test_format_smallish(self):
47 self.assertEqual(format_size(500000), '500kB')
48
49 def test_format_1M(self):
50 self.assertEqual(format_size(1000000), '1MB')
51
52 def test_format_mediumish(self):
53 self.assertEqual(format_size(500000000), '500MB')
54
55 def test_format_1G(self):
56 self.assertEqual(format_size(1000000000), '1GB')
57
58 def test_format_bigish(self):
59 self.assertEqual(format_size(500000000000), '500GB')
60
61 def test_format_1T(self):
62 self.assertEqual(format_size(1000000000000), '1TB')
63
64 def test_format_hugish(self):
65 self.assertEqual(format_size(500000000000000), '>1TB')
66
67
68def test_suite():
69 suite = unittest.TestSuite()
70 suite.addTests(unittest.makeSuite(TestUtilities))
71 return suite
072
=== removed file 'computerjanitorapp/ui.py'
--- computerjanitorapp/ui.py 2009-02-11 16:25:03 +0000
+++ computerjanitorapp/ui.py 1970-01-01 00:00:00 +0000
@@ -1,39 +0,0 @@
1# ui.py - user interface interface class
2# Copyright (C) 2008, 2009 Canonical, Ltd.
3#
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation, version 3 of the License.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16
17import computerjanitor
18
19
20class UserInterface(object):
21
22 """This is the base class for user interfaces.
23
24 The user interface is in charge of obeying command line arguments,
25 and interacting with the user.
26
27 The app and pm given to the constructor are stored in the app and
28 pm attributes.
29
30 """
31
32 def __init__(self, app, pm, mustberoot=True):
33 self.pm = pm
34 self.app = app
35 self.mustberoot = mustberoot
36
37 def run(self, options, args):
38 """Obey command line arguments in ARGS, and options in OPTIONS."""
39 raise computerjanitor.UnimplementedMethod(self.run)
400
=== removed file 'computerjanitorapp/ui_cli_tests.py'
--- computerjanitorapp/ui_cli_tests.py 2009-02-11 16:25:03 +0000
+++ computerjanitorapp/ui_cli_tests.py 1970-01-01 00:00:00 +0000
@@ -1,246 +0,0 @@
1# ui_cli_tests.py - unit tests for ui_cli.py
2# Copyright (C) 2008, 2009 Canonical, Ltd.
3#
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation, version 3 of the License.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16
17import unittest
18
19import computerjanitor
20import computerjanitorapp
21
22
23class MockCruft(object):
24
25 def __init__(self, name):
26 self._name = name
27 self.cleaned = False
28 self.post_cleaned = False
29
30 def get_name(self):
31 return self._name
32
33 def cleanup(self):
34 self.cleaned = True
35
36
37class MockPlugin(object):
38
39 def __init__(self, crufts):
40 self._crufts = crufts
41 self.post_cleaned = False
42
43 def get_cruft(self):
44 for cruft in self._crufts:
45 yield cruft
46
47 def post_cleanup(self):
48 self.post_cleaned = True
49
50
51class MockPluginManager(object):
52
53 def __init__(self, plugins):
54 self._plugins = plugins
55
56 def get_plugins(self):
57 return self._plugins
58
59
60class MockState(object):
61
62 def __init__(self, enabled):
63 self._enabled = set(enabled)
64
65 def is_enabled(self, name):
66 return name in self._enabled
67
68 def enable(self, name):
69 if name not in self._enabled:
70 self._enabled.add(name)
71
72 def disable(self, name):
73 if name in self._enabled:
74 self._enabled.remove(name)
75
76 def load(self, filename):
77 self.load_filename = filename
78
79 def save(self, filename):
80 self.save_filename = filename
81
82
83class MockOptionParser(object):
84
85 def __init__(self):
86 self.help_printed = False
87
88 def print_help(self):
89 self.help_printed = True
90
91
92class MockApplication(object):
93
94 def __init__(self, enabled):
95 self.state = MockState(enabled)
96 self.parser = MockOptionParser()
97
98 def verify_apt_cache(self):
99 pass
100
101 def remove_whitelisted(self, crufts):
102 return crufts
103
104
105class MockOptions(object):
106
107 def __init__(self):
108 self.all = None
109 self.state_file = "foo"
110 self.no_act = None
111 self.verbose = None
112
113
114class CommandLineUserInterfaceTests(unittest.TestCase):
115
116 def setUp(self):
117 self.app = MockApplication(["foo"])
118 self.cruft_names = ["foo", "bar"]
119 self.crufts = [MockCruft(name) for name in self.cruft_names]
120 self.cruftdict = dict((c.get_name(), c) for c in self.crufts)
121 self.plugin = MockPlugin(self.crufts)
122 self.pm = MockPluginManager([self.plugin])
123 self.ui = computerjanitorapp.CommandLineUserInterface(self.app,
124 self.pm, mustberoot=False)
125
126 self.options = MockOptions()
127
128 def testInsistsOnBeingRoot(self):
129 self.ui.mustberoot = True
130 self.assertRaises(computerjanitor.Exception, self.ui.run, None,
131 None)
132
133 def testFindsTheRightCruft(self):
134 self.assertEqual(self.ui.find_cruft(), self.crufts)
135
136 def testShowsTheRightCruftTheRightWay(self):
137
138 def mock_find_cruft():
139 return self.crufts
140
141 def mock_show_one_cruft(name, desc, state, width):
142 output.append((name, state))
143
144 output = []
145 self.ui.find_cruft = mock_find_cruft
146 self.ui.show_one_cruft = mock_show_one_cruft
147 self.ui.show_cruft(MockOptions(), None)
148 self.assertEqual(output,
149 sorted([("foo", "removable"), ("bar", "ignored")]))
150
151 def testIgnoresCruftCorrectly(self):
152 self.ui.ignore(self.options, ["foo"])
153 self.assertFalse(self.app.state.is_enabled("foo"))
154
155 def testUnignoresCruftCorrectly(self):
156 self.ui.unignore(self.options, ["bar"])
157 self.assert_(self.app.state.is_enabled("bar"))
158
159 def testCleansUpEnabledCruftWithDashDashAll(self):
160 self.options.all = True
161 self.ui.cleanup(self.options, [])
162 self.assert_(self.cruftdict["foo"].cleaned)
163
164 def testDoesNotCleanUpEnabledCruftWithDashDashAllWhenNoActIsSet(self):
165 self.options.all = True
166 self.options.no_act = True
167 self.ui.cleanup(self.options, [])
168 self.assertFalse(self.cruftdict["foo"].cleaned)
169
170 def testCleansUpRequestedEnabledCruft(self):
171 self.ui.cleanup(self.options, ["foo"])
172 self.assert_(self.cruftdict["foo"].cleaned)
173
174 def testDoesNotCleanUpDisaabledCruftWithDashDashAll(self):
175 self.options.all = True
176 self.ui.cleanup(self.options, [])
177 self.assertFalse(self.cruftdict["bar"].cleaned)
178
179 def testCleansUpRequestedDisabledCruft(self):
180 self.ui.cleanup(self.options, ["bar"])
181 self.assert_(self.cruftdict["bar"].cleaned)
182
183 def testRunsPostCleanup(self):
184 self.ui.cleanup(self.options, [])
185 self.assert_(self.plugin.post_cleaned)
186
187 def testDoesNotRunPostCleanupWhenNoActIsSet(self):
188 self.options.no_act = True
189 self.ui.cleanup(self.options, [])
190 self.assertFalse(self.plugin.post_cleaned)
191
192 def testRaisesExceptionForUnknownCruft(self):
193 self.assertRaises(computerjanitor.Exception, self.ui.cleanup,
194 self.options, ["unknown"])
195
196 def testHelpCallsParserPrintHelp(self):
197 self.ui.help(None, None)
198 self.assert_(self.app.parser.help_printed)
199
200 def setup_run(self):
201 names = ["show_cruft", "cleanup", "ignore", "unignore", "help"]
202 for name in names:
203 method = getattr(self.ui, name)
204 wrapper = lambda options, args, name=name: \
205 setattr(self, "operation", name)
206 setattr(self.ui, name, wrapper)
207
208 def testRunCallsShowCruft(self):
209 self.setup_run()
210 self.ui.run(self.options, ["find"])
211 self.assertEqual(self.operation, "show_cruft")
212
213 def testRunCallsCleanup(self):
214 self.setup_run()
215 self.ui.run(self.options, ["cleanup"])
216 self.assertEqual(self.operation, "cleanup")
217
218 def testRunCallsIgnore(self):
219 self.setup_run()
220 self.ui.run(self.options, ["ignore"])
221 self.assertEqual(self.operation, "ignore")
222
223 def testRunCallsUnignore(self):
224 self.setup_run()
225 self.ui.run(self.options, ["unignore"])
226 self.assertEqual(self.operation, "unignore")
227
228 def testRunCallsHelp(self):
229 self.setup_run()
230 self.ui.run(self.options, ["help"])
231 self.assertEqual(self.operation, "help")
232
233 def testRunCallsHelpWhenThereAreNoArguments(self):
234 self.setup_run()
235 self.ui.run(self.options, [])
236 self.assertEqual(self.operation, "help")
237
238 def testRaisesExceptionForUnknownCommand(self):
239 self.assertRaises(computerjanitor.Exception, self.ui.run,
240 self.options, ["yikes"])
241
242 def testRunLoadsStateFromRightFile(self):
243 self.setup_run()
244 self.ui.run(self.options, ["ignore"])
245 self.assertEqual(self.app.state.load_filename,
246 self.options.state_file)
2470
=== removed file 'computerjanitorapp/ui_gtk.py'
--- computerjanitorapp/ui_gtk.py 2010-01-21 13:54:34 +0000
+++ computerjanitorapp/ui_gtk.py 1970-01-01 00:00:00 +0000
@@ -1,714 +0,0 @@
1# ui_gtk.py - graphical user interface implemented using GTK+
2# Copyright (C) 2008, 2009 Canonical, Ltd.
3#
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation, version 3 of the License.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16
17import logging
18import os
19import sys
20import threading
21import time
22import traceback
23
24import computerjanitor
25import computerjanitorapp
26_ = computerjanitorapp.setup_gettext()
27
28# We can't import gtk here, since that would mean it gets imported
29# even when we run in command line mode. Thus, we do the imports in
30# the class, when we know we need them.
31
32
33import os
34GLADE = os.environ.get('COMPUTER_JANITOR_GLADE',
35 '/usr/share/computer-janitor/ComputerJanitor.ui')
36
37
38STATE_COL = 0
39NAME_COL = 1
40CRUFT_COL = 2
41EXPANDED_COL = 3
42SHOW_COL = 4
43
44
45def ui(method):
46 """Decorator function for UI methods.
47
48 Use @ui to decorate all methods in the controller class that
49 call GTK stuff, to ensure correct locking.
50
51 """
52
53 def new(self, *args, **kwargs):
54 self.gtk.gdk.threads_enter()
55 ret = method(self, *args, **kwargs)
56 self.gtk.gdk.threads_leave()
57 return ret
58
59 return new
60
61
62class ListUpdater(threading.Thread):
63
64 """Find cruft in the background, update user interface when done."""
65
66 def __init__(self, ui):
67 threading.Thread.__init__(self)
68 self.ui = ui
69
70 def run(self):
71 self.ui.finding = True
72 self.ui.set_sensitive()
73 status = self.ui.widgets['statusbar']
74 context_id = status.get_context_id('ListUpdater')
75 status.push(context_id, _('Analyzing system...'))
76 for plugin in self.ui.pm.get_plugins():
77 for cruft in self.ui.app.remove_whitelisted(plugin.get_cruft()):
78 self.ui.add_cruft(cruft)
79 status.pop(context_id)
80 self.ui.finding = False
81 self.ui.set_sensitive()
82 self.ui.done_updating_list()
83
84
85class Cleaner(threading.Thread):
86
87 """Actually clean up the cruft."""
88
89 def __init__(self, ui):
90 threading.Thread.__init__(self)
91 self.ui = ui
92 self.gtk = self.ui.gtk
93 self.crufts = ui.get_crufts()
94 self.plugins = ui.pm.get_plugins()
95 # progress reporting dialog/bar
96 # FIXME: the progress bar is a bit silly, it takes
97 # items like fix-fstab (0.1s) and deb_package (5min)
98 # and assigns them the same slice - use a pulsing one
99 # instead?
100 self.dialog = ui.widgets['cleanup_dialog']
101 self.pbar = ui.widgets['cleanup_progressbar']
102 # add status bar context
103 self.status = self.ui.widgets['statusbar']
104 self.context_id = self.status.get_context_id('ListUpdater')
105 self.status.push(self.context_id, _('Cleaning up...'))
106 # cancel button
107 button = ui.widgets['cleanup_cancel']
108 button.connect('clicked', self.cancel)
109 self.cancel_event = threading.Event()
110
111 def cancel(self, *args):
112 self.cancel_event.set()
113
114 @ui
115 def inc_done(self):
116 self.done_work += 1.0
117 self.pbar.set_fraction(self.done_work / self.total_work)
118
119 @ui
120 def start_reporting(self):
121 self.total_work = len(self.crufts) + len(self.plugins)
122 self.done_work = 0.0
123 self.pbar.set_fraction(0.0)
124
125 self.dialog.show()
126
127 @ui
128 def end_reporting(self):
129 self.pbar.set_fraction(1.0)
130 self.dialog.hide()
131 self.status.pop(self.context_id)
132
133 def run(self):
134 self.ui.cleaning = True
135 self.ui.set_sensitive()
136
137 self.start_reporting()
138
139 for cruft in self.crufts:
140 if self.cancel_event.isSet():
141 break
142 name = cruft.get_name()
143 if self.ui.app.state.is_enabled(name):
144 if self.ui.options.no_act:
145 time.sleep(0.1)
146 logging.info(_("Pretending to remove cruft: %s") % name)
147 else:
148 logging.info(_("Removing cruft: %s") % name)
149 cruft.cleanup()
150 self.inc_done()
151
152 for plugin in self.plugins:
153 if self.cancel_event.isSet():
154 break
155 if self.ui.options.no_act:
156 logging.info(_("Pretending to post-cleanup: %s") % plugin)
157 else:
158 logging.info(_("Post-cleanup: %s") % plugin)
159 error = None
160 try:
161 plugin.post_cleanup()
162 except Exception, e:
163 logging.debug(unicode(traceback.format_exc()))
164 self.ui.show_error(_("Could not clean up properly"),
165 unicode(e))
166 break
167 self.inc_done()
168
169 # do not run it directly (thread deadlocks), register for running
170 # in the gtk main thread instead
171 self.ui.glib.timeout_add(100, self.ui.show_cruft)
172 self.ui.cleaning = False
173
174 self.end_reporting()
175
176
177class GtkUserInterface(computerjanitorapp.UserInterface):
178
179 """The GTK+ user interface of Computer Janitor."""
180
181 # This acts as the controller in MVC. To simplify binding of callbacks
182 # to GTK signals, we follow a strict convention: any method named
183 # on_WIDGETNAME_SIGNALNAME is a callback and will be bound automatically.
184 # This way, we don't need do add each binding by hand, either in the
185 # code or in the .glade file. See the find_and_bind_widgets method
186 # for details.
187
188 def run(self, options, args):
189 # This is where UI execution starts.
190
191 import gtk
192 self.gtk = gtk
193
194 import gobject
195 self.gobject = gobject
196
197 import glib
198 self.glib = glib
199
200 self.options = options
201 self.app.state.load(options.state_file)
202
203 builder = gtk.Builder()
204 builder.set_translation_domain('computerjanitor')
205 builder.add_from_file(GLADE)
206 self.find_and_bind_widgets(builder)
207
208 self.store = self.create_cruft_store()
209 self.name_cols = set()
210 self.popup_menus = dict()
211 self.create_column('unused_treeview', self.unused_filter)
212 self.create_column('recommended_treeview', self.recommended_filter)
213 self.create_column('optimize_treeview', self.optimize_filter)
214
215 self.show_previously_ignored = False
216
217 self.first_map = True
218
219 self.set_default_window_size()
220 self.widgets['window'].show()
221
222 self.sort_crufts_by_current_order = self.sort_crufts_by_name
223
224 self.finding = False
225 self.cleaning = False
226
227 # set thread switches interval to make it more UI friendly
228 sys.setcheckinterval(0)
229 gtk.gdk.threads_init()
230 gtk.main()
231
232 def find_and_bind_widgets(self, builder):
233 """Bind widgets and callbacks."""
234 import gtk
235 self.widgets = {}
236 for o in builder.get_objects():
237 if issubclass(type(o), gtk.Buildable):
238 name = gtk.Buildable.get_name(o)
239 self.widgets[name] = o
240 for attr in dir(self):
241 prefix = 'on_%s_' % name
242 if attr.startswith(prefix):
243 signal_name = attr[len(prefix):]
244 method = getattr(self, attr)
245 o.connect(signal_name, method)
246
247
248 def set_default_window_size(self):
249 w = self.widgets['window']
250 width = 900
251 height = 700
252 self.widgets['window'].set_default_size(width, height)
253
254 def create_cruft_store(self):
255 """Create a gtk.ListStore for holding all the cruft."""
256 pairs = ((NAME_COL, self.gobject.TYPE_STRING),
257 (STATE_COL, self.gobject.TYPE_BOOLEAN),
258 (CRUFT_COL, self.gobject.TYPE_PYOBJECT),
259 (EXPANDED_COL, self.gobject.TYPE_BOOLEAN),
260 (SHOW_COL, self.gobject.TYPE_BOOLEAN))
261 column_types = [pair[1] for pair in sorted(pairs)]
262 store = self.gtk.ListStore(*column_types)
263
264 return store
265
266 def get_crufts(self):
267 def get(model, path, iter, crufts):
268 cruft = model.get_value(iter, CRUFT_COL)
269 crufts.append(cruft)
270 crufts = []
271 self.store.foreach(get, crufts)
272 return crufts
273
274 def sort_crufts(self, get_key):
275 crufts = self.get_crufts()
276 crufts = [(get_key(c), i, c) for i, c in enumerate(crufts)]
277 crufts.sort()
278 crufts = [i for key, i, c in crufts]
279 self.store.reorder(crufts)
280
281 def sort_crufts_by_name(self):
282 def get_key(cruft):
283 return cruft.get_name()
284 self.sort_crufts(get_key)
285
286 def sort_crufts_by_size(self):
287 def get_key(cruft):
288 return -cruft.get_disk_usage()
289 self.sort_crufts(get_key)
290
291 def create_column(self, widget_name, filterfunc):
292 """Add gtk.TreeViewColumn to the desired widget."""
293 treeview = self.widgets[widget_name]
294 treeview.set_rules_hint(True)
295
296 toggle_cr = self.gtk.CellRendererToggle()
297 toggle_cr.connect('toggled', self.toggled, treeview)
298 toggle_cr.set_property("yalign", 0)
299 toggle_col = self.gtk.TreeViewColumn()
300 toggle_col.pack_start(toggle_cr)
301 toggle_col.add_attribute(toggle_cr, 'active', STATE_COL)
302 treeview.append_column(toggle_col)
303
304 name_cr = self.gtk.CellRendererText()
305 name_cr.set_property("yalign", 0)
306 import pango
307 name_cr.set_property("wrap-mode", pango.WRAP_WORD)
308 name_col = self.gtk.TreeViewColumn()
309 name_col.pack_start(name_cr)
310 name_col.add_attribute(name_cr, 'markup', NAME_COL)
311 treeview.append_column(name_col)
312 self.name_cols.add(name_col)
313
314 filter_store = self.store.filter_new()
315 filter_store.set_visible_func(filterfunc)
316 treeview.set_model(filter_store)
317
318 self.create_popup_menu_for_treeview(treeview)
319
320 def create_popup_menu_for_treeview(self, treeview):
321 select_all = self.gtk.MenuItem(label='Select all')
322 select_all.connect('activate', self.popup_menu_select_all, treeview)
323
324 unselect_all = self.gtk.MenuItem(label='Unselect all')
325 unselect_all.connect('activate', self.popup_menu_unselect_all, treeview)
326
327 menu = self.gtk.Menu()
328 menu.append(select_all)
329 menu.append(unselect_all)
330 menu.show_all()
331
332 self.popup_menus[treeview] = menu
333
334 def unused_filter(self, store, iter):
335 cruft = store.get_value(iter, CRUFT_COL)
336 shown = store.get_value(iter, SHOW_COL)
337 return shown and isinstance(cruft, computerjanitor.PackageCruft)
338
339 def recommended_filter(self, store, iter):
340 return False
341
342 def optimize_filter(self, store, iter):
343 shown = store.get_value(iter, SHOW_COL)
344 return (shown and
345 not self.unused_filter(store, iter) and
346 not self.recommended_filter(store, iter))
347
348 def error_dialog(self, msg, secondary_msg=None):
349 dialog = self.gtk.MessageDialog(parent=self.widgets["window"],
350 type=self.gtk.MESSAGE_ERROR,
351 buttons=self.gtk.BUTTONS_OK,
352 message_format=msg)
353 if secondary_msg:
354 dialog.format_secondary_text(secondary_msg)
355
356 return dialog
357
358 @ui
359 def show_error(self, msg, secondary_msg=None):
360 dialog = self.error_dialog(msg, secondary_msg)
361 dialog.show()
362 dialog.run()
363 dialog.hide()
364
365 def require_root(self):
366 if os.getuid() != 0:
367 dialog = self.error_dialog(_("Root access required."),
368 _("You must run computer-janitor-gtk "
369 "as root. Sorry."))
370 dialog.show()
371 dialog.run()
372 sys.exit(1)
373
374 def require_working_apt_cache(self):
375 """ensure that the apt cache is in good state and error/exit
376 otherwise
377 """
378 try:
379 self.app.verify_apt_cache()
380 except computerjanitor.Exception, e:
381 logging.error(unicode(traceback.format_exc()))
382 dialog = self.error_dialog(str(e))
383 dialog.show()
384 dialog.run()
385 sys.exit(1)
386
387 def pulse(self):
388 """pulse callback that shows a progress pulse until finding is False"""
389 progress = self.widgets['progressbar_status']
390 if self.finding or self.cleaning:
391 progress.show()
392 progress.pulse()
393 return True
394 else:
395 progress.hide()
396 return False
397
398 def show_cruft(self):
399 """clear the cruft store and update it again via a thread """
400 self.store.clear()
401 # run as "daemon" thread to ensure that the main app exist
402 # if the user presses "quit" before the ListUpdater thread
403 # has finished
404 t = ListUpdater(self)
405 t.daemon = True
406 t.start()
407 # run a glib handler to shows a pulse progress
408 self.glib.timeout_add(150, self.pulse)
409
410 @ui
411 def add_cruft(self, cruft):
412 state = self.app.state.is_enabled(cruft.get_name())
413 shown = (self.show_previously_ignored or
414 not self.app.state.was_previously_ignored(cruft.get_name()))
415 sort_index = 0
416 values = ((CRUFT_COL, cruft),
417 (NAME_COL, cruft.get_shortname()),
418 (STATE_COL, state),
419 (EXPANDED_COL, False),
420 (SHOW_COL, shown))
421 values = [pair[1] for pair in sorted(values)]
422 self.store.append(values)
423 self.sort_crufts_by_current_order()
424
425 @ui
426 def done_updating_list(self):
427 if not self.find_visible_cruft():
428 dialog = self.widgets['borednow_messagedialog']
429 dialog.show()
430 dialog.run()
431 dialog.hide()
432
433 def foreach_set_state(self, treeview, enabled):
434 def set_state(model, path, iter, user_data):
435 iter2 = model.convert_iter_to_child_iter(iter)
436 cruft = self.store.get_value(iter2, CRUFT_COL)
437 cruft_name = cruft.get_name()
438 if enabled:
439 self.app.state.enable(cruft_name)
440 else:
441 self.app.state.disable(cruft_name)
442 self.store.set_value(iter2, STATE_COL, enabled)
443 treeview.get_model().foreach(set_state, None)
444 self.app.state.save(self.options.state_file)
445 self.set_sensitive_unlocked()
446
447 def format_name(self, cruft):
448 return self.gobject.markup_escape_text(cruft.get_shortname())
449
450 def format_size(self, bytes):
451 table = ((1000**3, "GB"),
452 (1000**2, "MB"),
453 (1000**1, "kB"),
454 ( 1, "B"))
455 for factor, unit in table:
456 if bytes >= factor or factor == 1:
457 return '%d %s' % (bytes / factor, unit)
458
459 def format_description(self, cruft):
460 esc = self.gobject.markup_escape_text
461
462 lines = [esc(cruft.get_shortname())]
463
464 # FIXME: The action verbs should come from the crufts themselves.
465 action_verbs = {
466 computerjanitor.PackageCruft: 'uninstall',
467 computerjanitor.FileCruft: 'remove',
468 computerjanitor.MissingPackageCruft: 'install',
469 }
470 action_descriptions = {
471 'uninstall': _('Package will be <b>removed</b>.'),
472 'install': _('Package will be <b>installed</b>.'),
473 'remove': _('File will be <b>removed</b>.'),
474 }
475 action_verb = action_verbs.get(type(cruft))
476 if action_verb:
477 lines += [action_descriptions[action_verb]]
478
479 size = cruft.get_disk_usage()
480 if size is not None:
481 lines += [_('Size: %s.') % self.format_size(size)]
482
483 desc = cruft.get_description()
484 if desc:
485 lines += ['', esc(desc)]
486
487 return '\n'.join(lines)
488
489 def toggle_long_description(self, treeview):
490 """Toggle the showing of the long description of some cruft."""
491
492 selection = treeview.get_selection()
493 filtermodel, selected = selection.get_selected()
494 if not selected:
495 return
496 model = filtermodel.get_model()
497 iter = filtermodel.convert_iter_to_child_iter(selected)
498 cruft = model.get_value(iter, CRUFT_COL)
499 expanded = model.get_value(iter, EXPANDED_COL)
500 expanded = not expanded
501 model.set_value(iter, EXPANDED_COL, expanded)
502 if expanded:
503 value = self.format_description(cruft)
504 else:
505 value = self.format_name(cruft)
506 model.set_value(iter, NAME_COL, value)
507
508 @ui
509 def set_sensitive(self):
510 self.set_sensitive_unlocked()
511
512 def set_sensitive_unlocked(self):
513 do = self.widgets['do_button']
514
515 names = ['unused_treeview', 'recommended_treeview',
516 'optimize_treeview']
517 cleanable_cruft = self.find_visible_cruft()
518 do.set_sensitive(not self.finding and
519 not self.cleaning and
520 len(cleanable_cruft) > 0)
521
522 def find_visible_cruft(self):
523 names = ['unused_treeview', 'recommended_treeview',
524 'optimize_treeview']
525 cleanable_cruft = []
526 for name in names:
527 w = self.widgets[name]
528 model = w.get_model()
529 it = model.get_iter_first()
530 while it is not None:
531 cruft = model.get_value(it, CRUFT_COL)
532 if self.app.state.is_enabled(cruft.get_name()):
533 cleanable_cruft.append(cruft)
534 it = model.iter_next(it)
535 return cleanable_cruft
536
537 def really_cleanup(self):
538 """Ask user if they really mean to clean up.
539
540 Be especially insistent (in wording) if they are removing
541 packages.
542
543 """
544
545 crufts = self.get_crufts()
546 crufts = [c
547 for c in crufts
548 if self.app.state.is_enabled(c.get_name())]
549 packages = [c
550 for c in crufts
551 if isinstance(c, computerjanitor.PackageCruft)]
552 others = [c for c in crufts if c not in packages]
553
554 # The following messages are a bit vague, since we need to handle
555 # cases where we remove packages, and don't remove packages, and
556 # so on, given that "clean up cruft" is such a general concept.
557 # My apologies to anyone who thinks this is confusing. Please
558 # provide a patch that a) works b) is not specific to removing
559 # packages.
560
561 msg = _('Are you sure you want to clean up?')
562 dialog = self.gtk.MessageDialog(parent=self.widgets['window'],
563 type=self.gtk.MESSAGE_WARNING,
564 buttons=self.gtk.BUTTONS_NONE,
565 message_format=msg)
566 dialog.set_title(_('Clean up'))
567
568 if packages:
569 msg = (_('You have chosen to <b>remove %d software packages.</b> '
570 'Removing packages that are still needed can cause '
571 'errors.') %
572 len(packages))
573 else:
574 msg = _('Do you want to continue?')
575 dialog.format_secondary_markup(msg)
576
577 dialog.add_button(self.gtk.STOCK_CANCEL, self.gtk.RESPONSE_CLOSE)
578 if others:
579 dialog.add_button(_('Clean up'), self.gtk.RESPONSE_YES)
580 else:
581 dialog.add_button(_('Remove packages'), self.gtk.RESPONSE_YES)
582
583 dialog.show_all()
584 response = dialog.run()
585 dialog.hide()
586
587 return response == self.gtk.RESPONSE_YES
588
589 # The rest of this class is callbacks for GTK signals.
590
591 def on_about_menuitem_activate(self, *args):
592 w = self.widgets['about_dialog']
593 w.set_name(_('Computer Janitor'))
594 w.set_version(computerjanitorapp.VERSION)
595 w.show()
596 w.run()
597 w.hide()
598
599 def on_do_button_clicked(self, *args):
600 if self.really_cleanup():
601 Cleaner(self).start()
602 self.glib.timeout_add(150, self.pulse)
603
604 def on_show_previously_ignored_toggled(self, menuitem):
605 self.show_previously_ignored = menuitem.get_active()
606 iter = self.store.get_iter_first()
607 while iter:
608 cruft = self.store.get_value(iter, CRUFT_COL)
609 name = cruft.get_name()
610 shown = (self.show_previously_ignored or
611 not self.app.state.was_previously_ignored(name))
612 self.store.set_value(iter, SHOW_COL, shown)
613 iter = self.store.iter_next(iter)
614
615 def treeview_size_allocate(self, treeview, *args):
616 column = treeview.get_column(NAME_COL)
617 name_cr = column.get_cell_renderers()[0]
618 x, y, width, height = name_cr.get_size(treeview, None)
619 width = column.get_width()
620 name_cr.set_property("wrap-width", width)
621
622 on_unused_treeview_size_allocate = treeview_size_allocate
623 on_recommended_treeview_size_allocate = treeview_size_allocate
624 on_optimize_treeview_size_allocate = treeview_size_allocate
625
626 def on_quit_menuitem_activate(self, *args):
627 self.gtk.main_quit()
628
629 on_window_delete_event = on_quit_menuitem_activate
630
631 def on_window_map_event(self, *args):
632 if self.first_map:
633 self.first_map = False
634 self.require_root()
635 self.require_working_apt_cache()
636 self.show_cruft()
637
638 def treeview_button_press_event(self, treeview, event):
639 # We handle mouse button presses ourselves so that we can either
640 # toggle the long description (button 1, typically left) or
641 # pop up a menu (button 3, typically right).
642 #
643 # This is slightly tricky and probably a source of bugs.
644 # Oh well.
645
646 if event.button == 1:
647 # Select row being clicked on. Also show/hide its long
648 # description. But only if click is on the name
649 # portion of the column, not the toggle button.
650 x = int(event.x)
651 y = int(event.y)
652 time = event.time
653 pathinfo = treeview.get_path_at_pos(x, y)
654 if pathinfo:
655 path, col, cellx, celly = pathinfo
656 if col in self.name_cols:
657 treeview.set_cursor(path, col, False)
658 self.toggle_long_description(treeview)
659 else:
660 return False
661 return True
662 if event.button == 3:
663 # Popup a menu
664 x = int(event.x)
665 y = int(event.y)
666 time = event.time
667 pathinfo = treeview.get_path_at_pos(x, y)
668 if pathinfo:
669 path, col, cellx, celly = pathinfo
670 treeview.grab_focus()
671 treeview.set_cursor(path, col, False)
672 menu = self.popup_menus[treeview]
673 menu.popup(None, None, None, event.button, time)
674 return True
675
676 on_unused_treeview_button_press_event = treeview_button_press_event
677 on_recommended_treeview_button_press_event = treeview_button_press_event
678 on_optimize_treeview_button_press_event = treeview_button_press_event
679
680 def popup_menu_select_all(self, menuitem, treeview):
681 self.foreach_set_state(treeview, True)
682
683 def popup_menu_unselect_all(self, menuitem, treeview):
684 self.foreach_set_state(treeview, False)
685
686 def toggled(self, cr, path, treeview):
687 model = treeview.get_model()
688 filter_iter = model.get_iter(path)
689 iter = model.convert_iter_to_child_iter(filter_iter)
690 cruft = self.store.get_value(iter, CRUFT_COL)
691 cruft_name = cruft.get_name()
692 enabled = self.app.state.is_enabled(cruft.get_name())
693 enabled = not enabled
694 if enabled:
695 self.app.state.enable(cruft_name)
696 else:
697 self.app.state.disable(cruft_name)
698 self.app.state.save(self.options.state_file)
699 self.store.set_value(iter, STATE_COL, enabled)
700 self.set_sensitive_unlocked()
701
702 def on_sort_by_name_toggled(self, menuitem):
703 if menuitem.get_active():
704 self.sort_crufts_by_current_order = self.sort_crufts_by_name
705 else:
706 self.sort_crufts_by_current_order = self.sort_crufts_by_size
707 self.sort_crufts_by_current_order()
708
709 def on_borednow_messagedialog_close(self, dialog):
710 dialog.hide()
711
712 def on_borednow_messagedialog_response(self, dialog, response):
713 dialog.hide()
714
7150
=== removed file 'computerjanitorapp/ui_tests.py'
--- computerjanitorapp/ui_tests.py 2009-02-11 16:25:03 +0000
+++ computerjanitorapp/ui_tests.py 1970-01-01 00:00:00 +0000
@@ -1,36 +0,0 @@
1# ui_tests.py - unit tests for ui.py
2# Copyright (C) 2008, 2009 Canonical, Ltd.
3#
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation, version 3 of the License.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16
17import unittest
18
19import computerjanitor
20import computerjanitorapp
21
22
23class UserInterfaceTests(unittest.TestCase):
24
25 def setUp(self):
26 self.ui = computerjanitorapp.UserInterface("app", "pm")
27
28 def testReturnsCorrectApp(self):
29 self.assertEqual(self.ui.app, "app")
30
31 def testReturnsCorrectPluginManager(self):
32 self.assertEqual(self.ui.pm, "pm")
33
34 def testRunRaisesUnimplemented(self):
35 self.assertRaises(computerjanitor.UnimplementedMethod,
36 self.ui.run, None, None)
370
=== added file 'computerjanitorapp/uigtk.py'
--- computerjanitorapp/uigtk.py 1970-01-01 00:00:00 +0000
+++ computerjanitorapp/uigtk.py 2010-03-10 21:40:30 +0000
@@ -0,0 +1,515 @@
1# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, version 3 of the License.
6#
7# This program is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License
13# along with this program. If not, see <http://www.gnu.org/licenses/>.
14
15"""Gtk user interface for computer janitor."""
16
17from __future__ import absolute_import, unicode_literals
18
19__metaclass__ = type
20__all__ = [
21 'UserInterface',
22 ]
23
24
25import os
26import gtk
27import dbus
28import glib
29import pango
30import gobject
31
32from computerjanitorapp import __version__, setup_gettext
33from computerjanitorapp.areyousure import AreYouSure
34from computerjanitorapp.store import (
35 ListStoreColumns, Store, optimize, unused)
36from computerjanitorapp.utilities import format_size
37from computerjanitord.service import DBUS_INTERFACE_NAME
38
39_ = setup_gettext()
40
41GLADE = '/usr/share/computer-janitor/ComputerJanitor.ui'
42ROOT_WIDTH = 900
43ROOT_HEIGHT = 500
44NL = '\n'
45
46# Keys are lower-cased cruft types, i.e. class name of cruft instances.
47ACTIONS = dict(
48 packagecruft=_('Package will be <b>removed</b>.'),
49 filecruft=_('Package will be <b>installed</b>.'),
50 missingpackagecruft=_('File will be <b>removed</b>.'),
51 )
52
53SENSITIVE_WIDGETS = (
54 'do_button',
55 'optimize_treeview',
56 'quit_menuitem',
57 'show_previously_ignored',
58 'sort_by_name',
59 'sort_by_size',
60 'unused_treeview',
61 )
62
63
64class UserInterface:
65 """Implementation of the Gtk user interface."""
66
67 def __init__(self):
68 # Connect to the dbus service.
69 system_bus = dbus.SystemBus()
70 proxy = system_bus.get_object(DBUS_INTERFACE_NAME, '/')
71 self.janitord = dbus.Interface(
72 proxy, dbus_interface=DBUS_INTERFACE_NAME)
73 # Create the model on which the TreeViews will be based.
74 self.store = Store(self.janitord)
75 self.popup_menus = {}
76 self.cruft_name_columns = set()
77 # Sort by name by default.
78 self._sort_key = self._by_name
79 # Work is happening asynchronously on the dbus service.
80 self.working = False
81 # Connect to the signal the server will emit when cleaning up.
82 self.janitord.connect_to_signal('cleanup_status', self._clean_working)
83
84 def run(self):
85 """Set up the widgets and run the main loop."""
86 builder = gtk.Builder()
87 builder.set_translation_domain('computerjanitor')
88 # Load the glade ui file, which can be overridden from the environment
89 # for testing purposes.
90 glade_file = os.environ.get('COMPUTER_JANITOR_GLADE', GLADE)
91 builder.add_from_file(glade_file)
92 # Bind widgets to callbacks.
93 self.find_and_bind_widgets(builder)
94 # Do the initial search for cruft and set up the TreeView model.
95 self.store.find_cruft()
96 self.sort_cruft()
97 # Now hook the TreeViews up to there view of the model.
98 self.create_column('unused_treeview', unused)
99 self.create_column('optimize_treeview', optimize)
100 # Set the dimensions of the root window.
101 root = self.widgets['window']
102 root.set_default_size(ROOT_WIDTH, ROOT_HEIGHT)
103 # Map the root window and go!
104 root.show()
105 gtk.main()
106
107 def find_and_bind_widgets(self, builder):
108 """Bind widgets and callbacks."""
109 # Start by extracting all the bindable widgets from the ui builder,
110 # keeping track of them as mapped to their name.
111 self.widgets = {}
112 for ui_object in builder.get_objects():
113 if issubclass(type(ui_object), gtk.Buildable):
114 widget_name = gtk.Buildable.get_name(ui_object)
115 self.widgets[widget_name] = ui_object
116 # Search through the attributes of this instance looking for
117 # callbacks for this widget. We use the naming convention
118 # 'on_<widget>_<signal>' for such methods. Both the widget
119 # name and signal (or event) can contain underscores. Connect
120 # the widget to the callback.
121 prefix = 'on_{0}_'.format(widget_name)
122 for method_name in dir(self):
123 if method_name.startswith(prefix):
124 signal_name = method_name[len(prefix):]
125 ui_object.connect(
126 signal_name, getattr(self, method_name))
127
128 def create_column(self, widget_name, filter_func):
129 """Set up a column in the named TreeView."""
130 treeview = self.widgets[widget_name]
131 # XXX 2010-03-04 barry: This is a bit of an abuse because it's
132 # supposed to specify whether users are to read across the rows. As a
133 # side effect it renders the columns with alternating row colors, but
134 # that's not it's primary function.
135 treeview.set_rules_hint(True)
136 # Each TreeView contains two columns. The leftmost one is a toggle
137 # that when select tells c-j to act on that cruft. Deselecting the
138 # toggle ignores the package for next time.
139 toggle_cr = gtk.CellRendererToggle()
140 toggle_cr.connect('toggled', self._toggled, treeview)
141 toggle_cr.set_property('yalign', 0)
142 toggle_col = gtk.TreeViewColumn()
143 toggle_col.pack_start(toggle_cr)
144 toggle_col.add_attribute(toggle_cr, 'active', ListStoreColumns.active)
145 treeview.append_column(toggle_col)
146 # The rightmost column contains the details of the cruft. It will
147 # always contain the cruft name and can be expanded to display cruft
148 # details. Tell the column to get its toggle's active state from the
149 # model.
150 name_cr = gtk.CellRendererText()
151 name_cr.set_property('yalign', 0)
152 name_cr.set_property('wrap-mode', pango.WRAP_WORD)
153 name_col = gtk.TreeViewColumn()
154 name_col.pack_start(name_cr)
155 name_col.add_attribute(name_cr, 'markup', ListStoreColumns.text)
156 treeview.append_column(name_col)
157 self.cruft_name_columns.add(name_col)
158 # The individual crufts may or may not be visible in this TreeView.
159 # It's the filter function that controls this, so set that now.
160 filter_store = self.store.filter_new()
161 filter_store.set_visible_func(filter_func)
162 treeview.set_model(filter_store)
163 # Each TreeView has a popup menu for select or deselecting all visible
164 # cruft.
165 self.create_popup_menu_for_treeview(treeview)
166
167 def create_popup_menu_for_treeview(self, treeview):
168 """The tree views have a popup menu to select/deselect everything.
169
170 :param treeview: The `TreeView` to attach the menu to.
171 """
172 select_all = gtk.MenuItem(label='Select all')
173 select_all.connect('activate', self.popup_menu_select_all, treeview)
174 unselect_all = gtk.MenuItem(label='Unselect all')
175 unselect_all.connect('activate',
176 self.popup_menu_unselect_all, treeview)
177 menu = gtk.Menu()
178 menu.append(select_all)
179 menu.append(unselect_all)
180 menu.show_all()
181 self.popup_menus[treeview] = menu
182
183 def _by_name(self, cruft_name):
184 """Sort by cruft name."""
185 return cruft_name
186
187 def _by_size(self, cruft_name):
188 """Sort by cruft size, from largest to smallest."""
189 # Return negative size to sort from largest to smallest.
190 return -self.janitord.get_details(cruft_name)[1]
191
192 def sort_cruft(self):
193 """Sort the cruft displays, either by name or size."""
194 # The way reordering (not technically 'sorting') works in gtk is that
195 # you give a list of integer indexes to the the ListStore. These
196 # indexes are in sorted order, and refer to the pre-sort indexes of
197 # the items in the store. IOW, the ListStore knows that if the first
198 # integer in the list is 7, it will move the 7th item to the top.
199 #
200 # Start by getting the indexes and names of the currenly sorted cruft.
201 # We'll fill this list with 2-tuples of the format:
202 # (sort-key, current-index).
203 cruft_data = []
204 def get(model, path, iter, crufts):
205 cruft_name = model.get_value(iter, ListStoreColumns.name)
206 crufts.append((self._sort_key(cruft_name), len(crufts)))
207 # Continue iterating.
208 return False
209 self.store.foreach(get, cruft_data)
210 cruft_data.sort()
211 cruft_indexes = [index for key, index in cruft_data]
212 self.store.reorder(cruft_indexes)
213
214 def get_cleanable_cruft(self):
215 """Return the list of cleanable cruft candidates.
216
217 :return: List of cleanable cruft.
218 :rtype: list of 2-tuples of (cruft_name, is_package_cruft)
219 """
220 cleanable_cruft = []
221 def collect(model, path, iter, crufts):
222 # Only clean up active cruft, i.e. those that are specifically
223 # checked as ready for cleaning.
224 cruft_active = model.get_value(iter, ListStoreColumns.active)
225 if cruft_active:
226 cruft_name = model.get_value(iter, ListStoreColumns.name)
227 cruft_is_package_cruft = model.get_value(
228 iter, ListStoreColumns.is_package_cruft)
229 crufts.append((cruft_name, cruft_is_package_cruft))
230 # Continue iterating.
231 return False
232 self.store.foreach(collect, cleanable_cruft)
233 return cleanable_cruft
234
235 def toggle_long_description(self, treeview):
236 """Toggle the currently selected cruft's long description.
237
238 :param treeview: The TreeView
239 """
240 selection = treeview.get_selection()
241 filter_model, selected = selection.get_selected()
242 if not selected:
243 return
244 model = filter_model.get_model()
245 iter = filter_model.convert_iter_to_child_iter(selected)
246 cruft_name = model.get_value(iter, ListStoreColumns.name)
247 expanded = model.get_value(iter, ListStoreColumns.expanded)
248 shortname = model.get_value(iter, ListStoreColumns.short_name)
249 if expanded:
250 # Collapse it.
251 value = gobject.markup_escape_text(shortname)
252 else:
253 cruft_type, size = self.janitord.get_details(cruft_name)
254 lines = [gobject.markup_escape_text(shortname)]
255 action = ACTIONS.get(cruft_type.lower())
256 if action is not None:
257 lines.append(action)
258 lines.append('Size: {0}'.format(format_size(size)))
259 lines.append('')
260 description = self.janitord.get_description(cruft_name)
261 lines.append(gobject.markup_escape_text(description))
262 value = NL.join(lines)
263 model.set_value(iter, ListStoreColumns.text, value)
264 model.set_value(iter, ListStoreColumns.expanded, not expanded)
265
266 def desensitize(self):
267 """Make certain ui elements insensitive during work."""
268 for widget in SENSITIVE_WIDGETS:
269 self.widgets[widget].set_sensitive(False)
270
271 def sensitize(self):
272 """Make certain ui elements sensitive after work."""
273 for widget in SENSITIVE_WIDGETS:
274 self.widgets[widget].set_sensitive(True)
275
276 # Popup menu support.
277
278 def popup_menu_foreach_set_state(self, treeview, enabled):
279 """Set the state of the cruft 'active' flag for all cruft.
280
281 :param treeview: The `TreeView` to set cruft state on.
282 :param enabled: The new state flag for all cruft. True means enabled.
283 :type enabled: bool
284 """
285 def set_state(model, path, iter, user_data):
286 # Set the state on an individual piece of cruft. Start by
287 # changing the state of the cruft on the dbus service.
288 child_iter = model.convert_iter_to_child_iter(iter)
289 cruft_name = self.store.get_value(
290 child_iter, ListStoreColumns.name)
291 if enabled:
292 self.janitord.unignore(cruft_name)
293 else:
294 self.janitord.ignore(cruft_name)
295 # Now set the active state in the model.
296 self.store.set_value(child_iter, ListStoreColumns.active, enabled)
297 treeview.get_model().foreach(set_state, None)
298 # Save the updated state on the dbus service.
299 self.janitord.save()
300
301 def popup_menu_select_all(self, menuitem, treeview):
302 self.popup_menu_foreach_set_state(treeview, True)
303
304 def popup_menu_unselect_all(self, menuitem, treeview):
305 self.popup_menu_foreach_set_state(treeview, False)
306
307 # Progress bar
308
309 def pulse(self):
310 """Progress bar callback, showing that something is happening."""
311 progress = self.widgets['progressbar_status']
312 if self.working:
313 progress.show()
314 progress.pulse()
315 return True
316 else:
317 # All done. Hide the progress bar, make the ui elements sensitive
318 # again, update the store, and kill the timer.
319 progress.hide()
320 self.store.clear()
321 self.store.find_cruft()
322 self.sensitize()
323 return False
324
325 def _clean_working(self, cruft):
326 """dbus signal handler; the 'clean' operation is in progress.
327
328 :param done: The cruft that is being cleaned up.
329 :type done: string
330 """
331 # Just mark the status here. The progress bar pulsar will handle
332 # doing the actual work.
333 self.working = (cruft != '')
334 if self.working:
335 self.widgets['progressbar_status'].set_text(
336 _('Processing {0}').format(cruft))
337
338 # Callbacks
339
340 def _toggled(self, widget, path, treeview):
341 """Handle the toggle button in a TreeView cell.
342
343 :param widget: The CellRendererToggle
344 :param path: The cell's path.
345 :param treeview: The TreeView
346 """
347 # Find out which cruft's toggle was clicked.
348 model = treeview.get_model()
349 filter_iter = model.get_iter(path)
350 child_iter = model.convert_iter_to_child_iter(filter_iter)
351 cruft_name = self.store.get_value(child_iter, ListStoreColumns.name)
352 state = self.store.get_value(child_iter, ListStoreColumns.active)
353 # Toggle the current state.
354 new_state = not state
355 if new_state:
356 self.janitord.unignore(cruft_name)
357 else:
358 self.janitord.ignore(cruft_name)
359 self.store.set_value(child_iter, ListStoreColumns.active, new_state)
360 self.store.set_value(
361 child_iter, ListStoreColumns.server_ignored, not new_state)
362 # Save the new ignored state on the dbus service.
363 self.janitord.save()
364
365 # Signal and event handlers.
366
367 def on_quit_menuitem_activate(self, *args):
368 """Signal and event handlers for quitting.
369
370 Since we just want things to go away, we don't really care about the
371 arguments. Just tell the main loop to exit.
372 """
373 # Don't quit while we're working.
374 if self.working:
375 return True
376 gtk.main_quit()
377
378 on_window_delete_event = on_quit_menuitem_activate
379
380 def treeview_button_press_event(self, treeview, event):
381 """Handle mouse button press events on the TreeView ourselves.
382
383 We handle mouse button presses ourselves so that we can either
384 toggle the long description (button 1, typically left) or
385 pop up a menu (button 3, typically right).
386 """
387 # Original comment: This is slightly tricky and probably a source of
388 # bugs. Oh well.
389 if event.button == 1:
390 # Left button event. Select the row being clicked on. If the
391 # click is on the cruft name, show or hide its long description.
392 # If the click the click is elsewhere do not handle it. This
393 # allows the toggle button event to be handled separately.
394 x = int(event.x)
395 y = int(event.y)
396 time = event.time
397 pathinfo = treeview.get_path_at_pos(x, y)
398 if pathinfo is None:
399 # The click was not in a cell, but we've handled it anyway.
400 return True
401 path, column, cell_x, cell_y = pathinfo
402 if column in self.cruft_name_columns:
403 treeview.set_cursor(path, column, False)
404 self.toggle_long_description(treeview)
405 return True
406 else:
407 # We are not handling this event so that the toggle button
408 # handling can occur.
409 return False
410 elif event.button == 3:
411 # Right button event. Pop up the select/deselect all menu.
412 treeview.grab_focus()
413 x = int(event.x)
414 y = int(event.y)
415 time = event.time
416 pathinfo = treeview.get_path_at_pos(x, y)
417 if pathinfo is not None:
418 path, column, cell_x, cell_y = pathinfo
419 treeview.set_cursor(path, column, False)
420 menu = self.popup_menus[treeview]
421 menu.popup(None, None, None, event.button, time)
422 return True
423 else:
424 # No other events are handled by us.
425 return False
426
427 # The actual event handler is totally generic. Alias it to names
428 # recognized by the automatic event binding scheme.
429 on_unused_treeview_button_press_event = treeview_button_press_event
430 on_optimize_treeview_button_press_event = treeview_button_press_event
431
432 def treeview_size_allocate(self, treeview, *args):
433 """Allocate space for the tree view and set wrap width.
434
435 :param treeview: The TreeView
436 :param args: Additional ignored positional arguments
437 """
438 # Get the rightmost of the two columns in the TreeView, i.e. the one
439 # containing the text.
440 column = treeview.get_column(1)
441 name_cr = column.get_cell_renderers()[0]
442 # Wrap to the entire width of the column.
443 width = column.get_width()
444 name_cr.set_property('wrap-width', width)
445
446 on_unused_treeview_size_allocate = treeview_size_allocate
447 on_optimize_treeview_size_allocate = treeview_size_allocate
448
449 def on_sort_by_name_toggled(self, menuitem):
450 """Reorder the crufts to be sorted by name or size."""
451 if menuitem.get_active():
452 self._sort_key = self._by_name
453 else:
454 self._sort_key = self._by_size
455 self.sort_cruft()
456
457 def on_about_menuitem_activate(self, *args):
458 dialog = self.widgets['about_dialog']
459 dialog.set_name(_('Computer Janitor'))
460 dialog.set_version(__version__)
461 dialog.show()
462 dialog.run()
463 dialog.hide()
464
465 def on_show_previously_ignored_toggled(self, menuitem):
466 """Show all cruft, even those being ignored.
467
468 Normally, we only show cruft that wasn't explicitly ignored. By
469 toggling this menu item, the janitor can also display cruft that is
470 marked as ignored on the dbus service.
471 """
472 show_ignored_cruft = menuitem.get_active()
473 iter = self.store.get_iter_first()
474 while iter:
475 cruft_name = self.store.get_value(iter, ListStoreColumns.name)
476 server_ignored = self.store.get_value(
477 iter, ListStoreColumns.server_ignored)
478 show = (show_ignored_cruft or not server_ignored)
479 self.store.set_value(iter, ListStoreColumns.show, show)
480 iter = self.store.iter_next(iter)
481
482 def on_do_button_clicked(self, *args):
483 """JFDI, well almost."""
484 self.count = 0
485 response = AreYouSure(self).verify()
486 if not response:
487 return
488 # This can take a long time. Make an asynchronous call to the dbus
489 # service and arrange for it to occasionally provide us with status.
490 # This isn't great ui, but OTOH, the package cruft cleaners themselves
491 # don't provide much granularity, so there's little we can do anyway
492 # without a major rewrite of the plugin architecture.
493 self.working = True
494 glib.timeout_add(150, self.pulse)
495 # Make various ui elements insensitive.
496 self.desensitize()
497 cleanable = [cruft for cruft, ispkg in self.get_cleanable_cruft()]
498 # XXX 2010-03-08 barry: Do better than this.
499 def error(exception):
500 print exception
501 self.working = False
502 def reply():
503 pass
504 # Make the asynchronous call because this can take a long time. We'll
505 # get status updates periodically. Note however that even though this
506 # is asynchronous, dbus still expects a response within a certain
507 # amount of time. We have no idea how long it will take to clean up
508 # the cruft though, so just crank the timeout up to some insanely huge
509 # number (of seconds).
510 self.widgets['progressbar_status'].set_text('Authenticating...')
511 self.janitord.clean(cleanable,
512 reply_handler=reply,
513 error_handler=error,
514 # If it takes longer than an hour, we're screwed.
515 timeout=3600)
0516
=== added file 'computerjanitorapp/utilities.py'
--- computerjanitorapp/utilities.py 1970-01-01 00:00:00 +0000
+++ computerjanitorapp/utilities.py 2010-03-10 21:40:30 +0000
@@ -0,0 +1,51 @@
1# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, version 3 of the License.
6#
7# This program is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License
13# along with this program. If not, see <http://www.gnu.org/licenses/>.
14
15"""Common utilities."""
16
17from __future__ import absolute_import, unicode_literals
18
19__metaclass__ = type
20__all__ = [
21 'format_size',
22 ]
23
24
25from math import log10
26
27
28TABLE = (
29 'B',
30 'kB',
31 'MB',
32 'GB',
33 'TB',
34 )
35
36
37def format_size(bytes):
38 """Format size in bytes.
39
40 :param bytes: Integer size in bytes.
41 :type bytes: integer
42 :return: Formatted size
43 :rtype: string
44 """
45 assert bytes >= 0, 'Cannot have negative sizes'
46 if bytes == 0:
47 return '0B'
48 if bytes > 10**12:
49 return '>1TB'
50 key = divmod(int(log10(bytes)), 3)[0]
51 return '{0}{1}'.format(bytes // 10**(key * 3), TABLE[key])
052
=== added directory 'computerjanitord'
=== added file 'computerjanitord/__init__.py'
=== added file 'computerjanitord/application.py'
--- computerjanitord/application.py 1970-01-01 00:00:00 +0000
+++ computerjanitord/application.py 2010-03-10 21:40:30 +0000
@@ -0,0 +1,111 @@
1# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, version 3 of the License.
6#
7# This program is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License
13# along with this program. If not, see <http://www.gnu.org/licenses/>.
14
15"""Application interface for use by plugins.
16
17This primarily makes certain apt functionality available to the plugin.
18"""
19
20from __future__ import absolute_import, unicode_literals
21
22__metaclass__ = type
23__all__ = [
24 'Application',
25 'SourcesListError',
26 ]
27
28
29import apt
30
31import computerjanitor
32import computerjanitorapp
33
34_ = computerjanitorapp.setup_gettext()
35
36SYNTAPTIC_PREFERENCES_FILE = '/var/lib/synaptic/preferences'
37# There really isn't anything special about these packages. These really just
38# represent landmarks in the package namespace that we look for to try to
39# judge the sanity of the apt cache. If these are missing, things are really
40# messed up and we can't actually figure out how to continue.
41LANDMARK_PACKAGES = [
42 'dash',
43 'gzip',
44 ]
45
46
47class LandmarkPackageError(computerjanitor.Exception):
48 """Base class for problems with the landmark packages."""
49
50 _errmsg = None
51
52 def __init__(self, package):
53 self.package = package
54
55 def __str__(self):
56 # gettext translation needs to be called at run time.
57 return _(self._errmsg).format(self)
58
59
60class MissingLandmarkError(LandmarkPackageError):
61 """A landmark package could not be found."""
62
63 _errmsg = _('Landmark package {0.package} is missing')
64
65
66class NonDownloadableError(LandmarkPackageError):
67 """A landmark package is not downloadable."""
68
69 _errmsg = _('Landmark package {0.package} is not downloadable')
70
71
72class Application:
73 """Interface for plugins requesting apt actions."""
74
75 def __init__(self, apt_cache=None):
76 """Create the application interface.
77
78 :param apt_cache: Alternative apt cache for testing purposes. When
79 `None` use the default apt cache.
80 """
81 if apt_cache is None:
82 # Use the real apt cache.
83 self.apt_cache = apt.Cache()
84 else:
85 self.apt_cache = apt_cache
86 self.refresh_apt_cache()
87
88 def refresh_apt_cache(self):
89 """Refresh the apt cache.
90
91 This API is used by plugins.
92 """
93 self.apt_cache.open()
94 # For historical purposes, Synaptic has a different way of pinning
95 # packages than apt, so we have to load its preferences file in order
96 # to know what it's pinning.
97 self.apt_cache._depcache.ReadPinFile(SYNTAPTIC_PREFERENCES_FILE)
98
99 def verify_apt_cache(self):
100 """Verify that essential packages are available in the apt cache.
101
102 This API is used by plugins.
103
104 :raises SourcesListProblem: when an essential package is not
105 available.
106 """
107 for name in LANDMARK_PACKAGES:
108 if name not in self.apt_cache:
109 raise MissingLandmarkError(name)
110 if not any(v.downloadable for v in self.apt_cache[name].versions):
111 raise NonDownloadableError(name)
0112
=== added file 'computerjanitord/authenticator.py'
--- computerjanitord/authenticator.py 1970-01-01 00:00:00 +0000
+++ computerjanitord/authenticator.py 2010-03-10 21:40:30 +0000
@@ -0,0 +1,85 @@
1# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, version 3 of the License.
6#
7# This program is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License
13# along with this program. If not, see <http://www.gnu.org/licenses/>.
14
15"""Authentication for Computer Janitor backend services."""
16
17from __future__ import absolute_import, unicode_literals
18
19__metaclass__ = type
20__all__ = [
21 'Authenticator',
22 ]
23
24
25import dbus
26
27
28PK_AUTHORITY_BUS_NAME = 'org.freedesktop.PolicyKit1'
29PK_AUTHORITY_OBJECT_PATH = '/org/freedesktop/PolicyKit1/Authority'
30PK_AUTHORITY_INTERFACE = 'org.freedesktop.PolicyKit1.Authority'
31# From the PolicyKit API.
32# http://hal.freedesktop.org/docs/polkit/
33# eggdbus-interface-org.freedesktop.PolicyKit1.Authority.html
34AllowUserInteraction = 0x00000001
35
36
37class Authenticator:
38 """PolicyKit authenticator."""
39
40 def authenticate(self, sender, connection, privilege):
41 """Authenticate with PolicyKit.
42
43 :param sender: The initiator of the action.
44 :param connection: The dbus connection that initiated the action.
45 :param privilege: The privilege being requested.
46 :return: Whether the subject is authorized or not.
47 :rtype: bool
48 """
49 policykit = self._get_policykit_proxy()
50 sender_pid = self._get_sender_pid(connection, sender)
51 # This is the CheckAuthorization() 'subject' structure.
52 subject = (
53 'unix-process', {
54 'pid': sender_pid,
55 'start-time': 0,
56 })
57 # No details or cancellation_id needed.
58 details = {'': ''}
59 cancellation_id = ''
60 flags = AllowUserInteraction
61 # CheckAuthorization returns an AuthorizationResult structure, modeled
62 # as a 3-tuple. The only thing we care about though is the boolean
63 # describing whether we got authorized or not.
64 is_authorized, is_challenge, details = policykit.CheckAuthorization(
65 subject, privilege, details, flags, cancellation_id)
66 return is_authorized
67
68 def _get_policykit_proxy(self):
69 """Contact the system bus to get a PolicyKit proxy."""
70 system_bus = dbus.SystemBus()
71 pk_proxy = system_bus.get_object(
72 PK_AUTHORITY_BUS_NAME, PK_AUTHORITY_OBJECT_PATH)
73 return dbus.Interface(pk_proxy, PK_AUTHORITY_INTERFACE)
74
75 def _get_sender_pid(self, connection, sender):
76 """Contact the system bus to get the sender connection PID."""
77 # Since we're going to authorize a Unix process, we need to get the
78 # sender's process id. This is available on the dbus. The
79 # CheckAuthorization() method also requires us to have a start-time,
80 # but it's not clear what the semantics are for that, so we'll just
81 # put a zero there.
82 db_proxy = connection.get_object(
83 dbus.BUS_DAEMON_NAME, dbus.BUS_DAEMON_PATH, introspect=False)
84 info = dbus.Interface(db_proxy, dbus.BUS_DAEMON_IFACE)
85 return info.GetConnectionUnixProcessID(sender)
086
=== added file 'computerjanitord/collector.py'
--- computerjanitord/collector.py 1970-01-01 00:00:00 +0000
+++ computerjanitord/collector.py 2010-03-10 21:40:30 +0000
@@ -0,0 +1,149 @@
1# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, version 3 of the License.
6#
7# This program is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License
13# along with this program. If not, see <http://www.gnu.org/licenses/>.
14
15"""A cruft collector."""
16
17from __future__ import absolute_import, unicode_literals
18
19__metaclass__ = type
20__all__ = [
21 'Collector',
22 ]
23
24
25import os
26import time
27import logging
28
29from computerjanitor import PluginManager
30from computerjanitord.errors import DuplicateCruftError
31from computerjanitord.whitelist import Whitelist
32
33
34log = logging.getLogger('computerjanitor')
35MISSING = object()
36DEFAULT_PLUGINS_DIRS = "/usr/share/computerjanitor/plugins"
37
38# For testing purposes.
39SLEEPY_TIME = float(os.environ.get('COMPUTER_JANITOR_SLEEPY_TIME', '1.0'))
40
41
42class Collector:
43 """A cruft collector."""
44
45 def __init__(self, application, plugin_manager_class=None,
46 whitelist_dirs=None, service=None):
47 """Create a cruft collector.
48
49 :param application: The `Application` class.
50 :type application: This object is used by plugins to access the apt
51 database. It must have an attribute named `apt_cache` and a
52 method named `refresh_apt_cache()`.
53 :param plugin_manager_class: The plugin manager class. If None (the
54 default), then `computerjanitor.PluginManager` is used.
55 :type plugin_manager_class: callable accepting a single argument,
56 which is a sequence of plugin directories.
57 :param whitelist_dirs: Sequence of directories to search for
58 '.whitelist' files. Passed directly to
59 `computerjanitord.whitelist.Whitelist`.
60 :param service: The dbus service; when doing plugin post-cleanup, this
61 will be used to emit a progress signal.
62 """
63 self.application = application
64 self.service = service
65 self.whitelist = Whitelist(whitelist_dirs)
66 # Keep track of cruft and map between the cruft's name and its Cruft
67 # instance. We'll use the latter when cruft cleanup is requested
68 # through the dbus API.
69 self.cruft = None
70 self.cruft_by_name = None
71 # Set up the plugin manager.
72 plugin_path = os.environ.get('COMPUTER_JANITOR_PLUGINS',
73 DEFAULT_PLUGINS_DIRS)
74 plugin_dirs = plugin_path.split(':')
75 if plugin_manager_class is None:
76 plugin_manager_class = PluginManager
77 self.plugin_manager = plugin_manager_class(application, plugin_dirs)
78 self.load()
79
80 def load(self):
81 """Reload all cruft."""
82 self.cruft = []
83 self.cruft_by_name = {}
84 # Ask all the plugins to find their cruft, filtering out whitelisted
85 # cruft.
86 for plugin in self.plugin_manager.get_plugins():
87 for cruft in plugin.get_cruft():
88 if not self.whitelist.is_whitelisted(cruft):
89 # Different plugins can give us duplicate cruft names,
90 # however the Cruft class better be the same, otherwise we
91 # won't actually know how to map the name back to a cruft
92 # instance for proper cleanup.
93 if cruft.get_name() in self.cruft_by_name:
94 my_cruft = self.cruft_by_name[cruft.get_name()]
95 if cruft.__class__ is my_cruft.__class__:
96 # We only need one instance of this cruft.
97 continue
98 else:
99 raise DuplicateCruftError(cruft.get_name())
100 #print ' ', cruft.get_name()
101 self.cruft.append(cruft)
102 self.cruft_by_name[cruft.get_name()] = cruft
103
104 def clean(self, names, dry_run=False):
105 """Clean up the named cruft.
106
107 :param names: The names of the cruft to clean up.
108 :type names: list of strings
109 :param dry_run: Flag indicating whether to do permanent changes.
110 :type dry_run: bool
111 """
112 # Ensure that all named cruft is known.
113 for name in names:
114 cruft = self.cruft_by_name.get(name, MISSING)
115 if cruft is MISSING:
116 log.error('No such cruft: {0}'.format(name))
117 raise NoSuchCruftError(name)
118 log.info('cleaning cruft: {0}'.format(cruft.get_name()))
119 if not dry_run:
120 cruft.cleanup()
121 # Do plugin-specific post-cleanup.
122 for plugin in self.plugin_manager.get_plugins():
123 logging.info('post-cleanup: {0}'.format(plugin))
124 if self.service is not None:
125 # Notify the client that we're not done yet.
126 #
127 # 2010-02-09 barry: this actually kind of sucks because the
128 # granularity is too coarse. Some plugins will post_cleanup()
129 # very quickly, others will take a long time. Unfortunately,
130 # the computerjanitor.Plugin API doesn't support a more
131 # granular feedback. Plugin.get_plugin(..., callback=foo)
132 # doesn't really cut it because that only gets called during
133 # get_plugins().
134 self.service.cleanup_status(plugin.__class__.__name__)
135 if dry_run:
136 # For testing purposes.
137 time.sleep(SLEEPY_TIME)
138 else:
139 try:
140 plugin.post_cleanup()
141 except Exception:
142 logging.exception('plugin: {0}'.format(plugin))
143 # Keep going.
144 # Now we're done.
145 if self.service is not None:
146 self.service.cleanup_status('')
147 # Reload list of crufts.
148 self.application.refresh_apt_cache()
149 self.load()
0150
=== added directory 'computerjanitord/data'
=== added file 'computerjanitord/data/com.ubuntu.ComputerJanitor.conf'
--- computerjanitord/data/com.ubuntu.ComputerJanitor.conf 1970-01-01 00:00:00 +0000
+++ computerjanitord/data/com.ubuntu.ComputerJanitor.conf 2010-03-10 21:40:30 +0000
@@ -0,0 +1,15 @@
1<!DOCTYPE busconfig PUBLIC
2 "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
3 "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
4<busconfig>
5 <policy user="root">
6 <allow own="com.ubuntu.ComputerJanitor"/>
7 </policy>
8
9 <policy context="default">
10 <allow send_interface="com.ubuntu.ComputerJanitor"/>
11 <allow receive_interface="com.ubuntu.ComputerJanitor"
12 receive_sender="com.ubuntu.ComputerJanitor"/>
13 </policy>
14
15</busconfig>
016
=== added file 'computerjanitord/data/com.ubuntu.computerjanitor.policy'
--- computerjanitord/data/com.ubuntu.computerjanitor.policy 1970-01-01 00:00:00 +0000
+++ computerjanitord/data/com.ubuntu.computerjanitor.policy 2010-03-10 21:40:30 +0000
@@ -0,0 +1,19 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<!DOCTYPE policyconfig PUBLIC
3 "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
4 "http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">
5<policyconfig>
6
7 <vendor>ComputerJanitor</vendor>
8 <vendor_url>https://launchpad.net/computer-janitor</vendor_url>
9
10 <action id="com.ubuntu.computerjanitor.updatesystem">
11 <description>Clean up packages that are no longer necessary</description>
12 <message>Removing unused packages requires authentication</message>
13 <defaults>
14 <allow_inactive>no</allow_inactive>
15 <allow_active>auth_admin_keep</allow_active>
16 </defaults>
17 </action>
18
19</policyconfig>
020
=== added file 'computerjanitord/errors.py'
--- computerjanitord/errors.py 1970-01-01 00:00:00 +0000
+++ computerjanitord/errors.py 2010-03-10 21:40:30 +0000
@@ -0,0 +1,57 @@
1# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, version 3 of the License.
6#
7# This program is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License
13# along with this program. If not, see <http://www.gnu.org/licenses/>.
14
15"""Exceptions for the Computer Janitor daemon."""
16
17
18from __future__ import absolute_import, unicode_literals
19
20__metaclass__ = type
21__all__ = [
22 'DuplicateCruftError',
23 'NoSuchCruftError',
24 'PermissionDeniedError',
25 ]
26
27
28import dbus
29
30from computerjanitorapp import setup_gettext
31_ = setup_gettext()
32
33
34class PermissionDeniedError(dbus.DBusException):
35 """Permission denied by policy"""
36
37
38class CruftError(dbus.DBusException):
39 MSG = None
40
41 def __init__(self, cruft_name):
42 self.cruft_name = cruft_name
43
44 def __str__(self):
45 return _(self.MSG).format(self)
46
47
48class DuplicateCruftError(CruftError):
49 """Duplicate cruft name with different cleanup."""
50
51 MSG = _('Duplicate cruft with different cleanup: {0.cruft_name}')
52
53
54class NoSuchCruftError(CruftError):
55 """There is no cruft by the given name."""
56
57 MSG = _('No such cruft: {0.cruft_name}')
058
=== added file 'computerjanitord/main.py'
--- computerjanitord/main.py 1970-01-01 00:00:00 +0000
+++ computerjanitord/main.py 2010-03-10 21:40:30 +0000
@@ -0,0 +1,97 @@
1# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, version 3 of the License.
6#
7# This program is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License
13# along with this program. If not, see <http://www.gnu.org/licenses/>.
14
15"""Main entry point for Computer Janitor dbus daemon."""
16
17from __future__ import absolute_import, unicode_literals
18
19__metaclass__ = type
20__all__ = [
21 'main',
22 ]
23
24
25import os
26import gobject
27import logging
28import argparse
29import warnings
30import dbus.mainloop.glib
31import logging.handlers
32
33from computerjanitorapp import __version__, setup_gettext
34from computerjanitord.service import Service
35
36_ = setup_gettext()
37
38
39# 2010-02-09 barry: computerjanitor.package_cruft has a DeprecationWarning,
40# but that really needs to be fixed in that package, which in turn needs to be
41# ripped out of update-manager.
42warnings.filterwarnings('ignore', category=DeprecationWarning,
43 module='computerjanitor.package_cruft')
44
45
46class Options:
47 """Command line options."""
48
49 def __init__(self):
50 self.parser = argparse.ArgumentParser(
51 description=_('Computer janitor dbus daemon'),
52 version=__version__)
53 self.parser.add_argument(
54 '-n', '--dry-run', action='store_true',
55 help=_("""\
56 Only pretend to do anything permanent. This is useful for testing
57 and debugging."""))
58 self.parser.add_argument(
59 '-f', '--state-file', metavar='FILE',
60 help=_('Store ignored state in FILE instead of the default.'))
61 self.arguments = self.parser.parse_args()
62
63
64class ASCIIFormatter(logging.Formatter):
65 """Force the log messages to ASCII."""
66 def format(self, record):
67 message = logging.Formatter.format(self, record)
68 return message.encode('ascii', 'replace')
69
70
71def main():
72 """Main entry point."""
73 # Set up logging.
74 if os.environ.get('COMPUTER_JANITOR_DEBUG') is not None:
75 level = logging.DEBUG
76 else:
77 level = logging.INFO
78 logging.basicConfig(level=level, format='%(levelname)s: %(message)s')
79 log = logging.getLogger('computerjanitor')
80 # SysLogHandler does not recognize unicode arguments.
81 syslog = logging.handlers.SysLogHandler(str('/dev/log'))
82 # syslog will provide a timestamp.
83 formatter = ASCIIFormatter('computerjanitord:%(levelname)s: %(message)s')
84 syslog.setFormatter(formatter)
85 syslog.setLevel(level)
86 log.addHandler(syslog)
87 options = Options()
88 dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
89 server = Service(options)
90 try:
91 gobject.MainLoop().run()
92 except KeyboardInterrupt:
93 pass
94
95
96if __name__ == '__main__':
97 main()
098
=== added file 'computerjanitord/service.py'
--- computerjanitord/service.py 1970-01-01 00:00:00 +0000
+++ computerjanitord/service.py 2010-03-10 21:40:30 +0000
@@ -0,0 +1,260 @@
1# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, version 3 of the License.
6#
7# This program is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License
13# along with this program. If not, see <http://www.gnu.org/licenses/>.
14
15"""dbus service for cleaning up crufty packages that are no longer needed."""
16
17
18from __future__ import absolute_import, unicode_literals
19
20__metaclass__ = type
21__all__ = [
22 'Service',
23 ]
24
25
26import atexit
27import logging
28
29import dbus.service
30
31from computerjanitor import PackageCruft
32from computerjanitord.application import Application
33from computerjanitord.authenticator import Authenticator
34from computerjanitord.collector import Collector
35from computerjanitord.errors import NoSuchCruftError, PermissionDeniedError
36from computerjanitord.state import State, DEFAULT_STATE_FILE
37
38
39log = logging.getLogger('computerjanitor')
40MISSING = object()
41
42DBUS_INTERFACE_NAME = 'com.ubuntu.ComputerJanitor'
43PRIVILEGE = 'com.ubuntu.computerjanitor.updatesystem'
44
45
46class Service(dbus.service.Object):
47 """Backend dbus service that handles removing crufty packages."""
48
49 def __init__(self, options):
50 """Create the dbus service.
51
52 :param options: The command line options class.
53 :type options: `Options`
54 """
55 self.dry_run = options.arguments.dry_run
56 self.state_file = (DEFAULT_STATE_FILE
57 if options.arguments.state_file is None
58 else options.arguments.state_file)
59 self.application = Application()
60 self.state = State()
61 self.state.load(self.state_file)
62 self.collector = Collector(self.application, service=self)
63 self.authenticator = Authenticator()
64 bus_name = dbus.service.BusName(
65 DBUS_INTERFACE_NAME, bus=dbus.SystemBus())
66 dbus.service.Object.__init__(self, bus_name, '/')
67 # We can't use the decorator because that doesn't work with methods;
68 # self doesn't get passed to the handler.
69 atexit.register(self._exit_handler)
70
71 def _exit_handler(self):
72 """Ensure that the state file is saved at exit."""
73 if not self.dry_run:
74 self.state.save(self.state_file)
75
76 def _authenticate(self, sender, connection):
77 """Authenticate via PolicyKit.
78
79 :param sender: The dbus client sender.
80 :param connection: The dbus client connection.
81 :raises PermissionDeniedError: when the authentication fails.
82 """
83 if not self.authenticator.authenticate(sender, connection, PRIVILEGE):
84 log.error('Permission denied: {0} for {1} on {2}'.format(
85 PRIVILEGE, sender, connection))
86 raise PermissionDeniedError(PRIVILEGE)
87 log.debug('Permission granted: {0} for {1} on {2}'.format(
88 PRIVILEGE, sender, connection))
89
90 @dbus.service.method(DBUS_INTERFACE_NAME,
91 out_signature='as')
92 def find(self):
93 """Find all the non-whitelisted cruft on the system.
94
95 Because this is a read-only interface it does not need authorization
96 to be called.
97
98 :return: A list of matching cruft names.
99 """
100 return list(cruft.get_name() for cruft in self.collector.cruft)
101
102 @dbus.service.method(DBUS_INTERFACE_NAME,
103 out_signature='as',
104 # Must wrap these in str() because Python < 2.6.5
105 # does not like unicode keyword arguments.
106 sender_keyword=str('sender'),
107 connection_keyword=str('connection'))
108 def load(self, sender=None, connection=None):
109 """Load the state file."""
110 self._authenticate(sender, connection)
111 self.state.load(self.state_file)
112 return list(self.state.ignore)
113
114 @dbus.service.method(DBUS_INTERFACE_NAME,
115 # Must wrap these in str() because Python < 2.6.5
116 # does not like unicode keyword arguments.
117 sender_keyword=str('sender'),
118 connection_keyword=str('connection'))
119 def save(self, sender=None, connection=None):
120 """Save the state file."""
121 self._authenticate(sender, connection)
122 if not self.dry_run:
123 self.state.save(self.state_file)
124
125 @dbus.service.method(DBUS_INTERFACE_NAME,
126 in_signature='s',
127 # Must wrap these in str() because Python < 2.6.5
128 # does not like unicode keyword arguments.
129 sender_keyword=str('sender'),
130 connection_keyword=str('connection'))
131 def ignore(self, name, sender=None, connection=None):
132 """Ignore the named cruft.
133
134 :param name: The name of the cruft to ignore.
135 :type filename: string
136 """
137 # Make sure this is known cruft first.
138 cruft = self.collector.cruft_by_name.get(name, MISSING)
139 if cruft is MISSING:
140 log.error('ignore(): No such cruft: {0}'.format(name))
141 raise NoSuchCruftError(name)
142 self._authenticate(sender, connection)
143 if not self.dry_run:
144 self.state.ignore.add(name)
145
146 @dbus.service.method(DBUS_INTERFACE_NAME,
147 in_signature='s',
148 # Must wrap these in str() because Python < 2.6.5
149 # does not like unicode keyword arguments.
150 sender_keyword=str('sender'),
151 connection_keyword=str('connection'))
152 def unignore(self, name, sender=None, connection=None):
153 """Unignore the named cruft.
154
155 :param name: The name of the cruft to unignore.
156 :type filename: string
157 """
158 cruft = self.collector.cruft_by_name.get(name, MISSING)
159 if cruft is MISSING:
160 log.error('ignore(): No such cruft: {0}'.format(name))
161 raise NoSuchCruftError(name)
162 self._authenticate(sender, connection)
163 if not self.dry_run:
164 # Don't worry if we're already not ignoring the cruft (i.e. don't
165 # raise a KeyError here if 'name' is not in the set).
166 self.state.ignore.discard(name)
167
168 @dbus.service.method(DBUS_INTERFACE_NAME,
169 out_signature='as')
170 def ignored(self):
171 """Return the list of ignored cruft.
172
173 :return: The names of the ignored cruft.
174 :rtype: list of strings
175 """
176 return list(self.state.ignore)
177
178 @dbus.service.method(DBUS_INTERFACE_NAME,
179 in_signature='s',
180 out_signature='s')
181 def get_description(self, name):
182 """Return the description of the named cruft.
183
184 :param name: The cruft name.
185 :type name: string
186 :return: The description of the cruft.
187 :rtype: string
188 """
189 cruft = self.collector.cruft_by_name.get(name, MISSING)
190 if cruft is MISSING:
191 log.error('get_description(): No such cruft: {0}'.format(name))
192 raise NoSuchCruftError(name)
193 return cruft.get_description()
194
195 @dbus.service.method(DBUS_INTERFACE_NAME,
196 in_signature='s',
197 out_signature='s')
198 def get_shortname(self, name):
199 """Return the short name of the named cruft.
200
201 :param name: The cruft name.
202 :type name: string
203 :return: The short nameof the cruft.
204 :rtype: string
205 """
206 cruft = self.collector.cruft_by_name.get(name, MISSING)
207 if cruft is MISSING:
208 log.error('get_shortname(): No such cruft: {0}'.format(name))
209 raise NoSuchCruftError(name)
210 return cruft.get_shortname()
211
212 @dbus.service.method(DBUS_INTERFACE_NAME,
213 in_signature='s',
214 out_signature='st')
215 def get_details(self, name):
216 """Return some extra details about the named cruft.
217
218 :param name: The cruft name.
219 :type name: string
220 :return: Some extra details about the named cruft, specifically its
221 'type' and the amount of disk space it consumes. The type is
222 simply the name of the cruft instance's class.
223 :rtype: string, uint64
224 """
225 cruft = self.collector.cruft_by_name.get(name, MISSING)
226 if cruft is MISSING:
227 log.error('get_shortname(): No such cruft: {0}'.format(name))
228 raise NoSuchCruftError(name)
229 return cruft.__class__.__name__, cruft.get_disk_usage()
230
231 @dbus.service.method(DBUS_INTERFACE_NAME,
232 in_signature='as', # array of strings
233 # Must wrap these in str() because Python < 2.6.5
234 # does not like unicode keyword arguments.
235 sender_keyword=str('sender'),
236 connection_keyword=str('connection'))
237 def clean(self, names, sender=None, connection=None):
238 """Clean the named crufts.
239
240 :param names: The names of the cruft to clean.
241 :type names: list of strings
242 """
243 self._authenticate(sender, connection)
244 self.collector.clean(names, self.dry_run)
245
246 @dbus.service.signal(DBUS_INTERFACE_NAME,
247 signature='s')
248 def cleanup_status(self, cruft):
249 """Signal cleanup status.
250
251 This signal is used to incrementally inform clients that some cleanup
252 work is being done. It is called at the beginning of the cleanup
253 process and after each plugin has completed its `post_cleanup()`
254 method.
255
256 :param done: The name of the next piece of cruft to be cleaned up, or
257 the empty string when there's nothing left to do.
258 :type done: string
259 """
260 log.debug('cleanup_status: {0}'.format(cruft))
0261
=== added file 'computerjanitord/state.py'
--- computerjanitord/state.py 1970-01-01 00:00:00 +0000
+++ computerjanitord/state.py 2010-03-10 21:40:30 +0000
@@ -0,0 +1,92 @@
1# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, version 3 of the License.
6#
7# This program is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License
13# along with this program. If not, see <http://www.gnu.org/licenses/>.
14
15"""Maintaining package ignored state."""
16
17from __future__ import absolute_import, unicode_literals
18
19__metaclass__ = type
20__all__ = [
21 'DEFAULT_STATE_FILE',
22 'State',
23 ]
24
25
26import ConfigParser
27import textwrap
28
29
30DEFAULT_STATE_FILE = '/var/lib/computer-janitor/state.dat'
31
32
33class State:
34 """Maintain the state of cruft which should be ignored.
35
36 The file's format is a `ConfigParser` style .ini file. Each section is a
37 cruft's `.get_name()` value and contains a single boolean setting
38 `ignore`. If true, then the cruft is ignored.
39
40 For backward compatibility purposes, the setting `enabled` is also
41 recognized (on read only). If `enabled` is false then the cruft is
42 ignored.
43 """
44
45 def __init__(self):
46 self.ignore = set()
47
48 def load(self, filename):
49 """Load ignored state from a file.
50
51 This reset any previously determined state and re-initializes it with
52 the state stored in the file.
53
54 :param filename: The file to load.
55 :type filename: string
56 """
57 parser = ConfigParser.ConfigParser()
58 parser.read(filename)
59 # Reset the set of ignored packages.
60 self.ignore = set()
61 for cruft_name in parser.sections():
62 # For backwards compatibility, recognize both the 'ignore' setting
63 # and the 'enabled' setting. We only write the former.
64 try:
65 ignore = parser.getboolean(cruft_name, 'ignore')
66 except ConfigParser.NoOptionError:
67 try:
68 ignore = not parser.getboolean(cruft_name, 'enabled')
69 except ConfigParser.NoOptionError:
70 # No other settings are recognized.
71 ignore = False
72 if ignore:
73 self.ignore.add(cruft_name)
74
75 def save(self, filename):
76 """Save the ignored state to a file.
77
78 Only the packages being ignored are stored to the file, and writing
79 overwrites the previous contents of the file.
80
81 :param filename: The file to load.
82 :type filename: string
83 """
84 # It's easier just to write the .ini file directly instead of using
85 # the ConfigParser interface. This way we can guarantee sort order
86 # and can automatically cull unignored packages from the file.
87 with open(filename, 'w') as fp:
88 for cruft_name in self.ignore:
89 print >> fp, textwrap.dedent("""\
90 [{0}]
91 ignore: true
92 """.format(cruft_name))
093
=== added directory 'computerjanitord/tests'
=== added file 'computerjanitord/tests/__init__.py'
=== added directory 'computerjanitord/tests/data'
=== added file 'computerjanitord/tests/data/empty'
=== added directory 'computerjanitord/tests/data/etc'
=== added directory 'computerjanitord/tests/data/etc/apt'
=== added file 'computerjanitord/tests/data/etc/apt/sources.list'
--- computerjanitord/tests/data/etc/apt/sources.list 1970-01-01 00:00:00 +0000
+++ computerjanitord/tests/data/etc/apt/sources.list 2010-03-10 21:40:30 +0000
@@ -0,0 +1,1 @@
1deb http://archive.ubuntu.com/ubuntu intrepid main restricted
02
=== added directory 'computerjanitord/tests/data/var'
=== added directory 'computerjanitord/tests/data/var/cache'
=== added directory 'computerjanitord/tests/data/var/cache/apt'
=== added directory 'computerjanitord/tests/data/var/cache/apt/archives'
=== added directory 'computerjanitord/tests/data/var/cache/apt/archives/partial'
=== added directory 'computerjanitord/tests/data/var/lib'
=== added directory 'computerjanitord/tests/data/var/lib/apt'
=== added directory 'computerjanitord/tests/data/var/lib/apt/lists'
=== added file 'computerjanitord/tests/data/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_intrepid_restricted_binary-i386_Packages'
--- computerjanitord/tests/data/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_intrepid_restricted_binary-i386_Packages 1970-01-01 00:00:00 +0000
+++ computerjanitord/tests/data/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_intrepid_restricted_binary-i386_Packages 2010-03-10 21:40:30 +0000
@@ -0,0 +1,54 @@
1Package: dash
2Priority: required
3Section: shells
4Installed-Size: 236
5Maintainer: Ubuntu Core Developers <ubuntu-devel-discuss@lists.ubuntu.com>
6Original-Maintainer: Gerrit Pape <pape@smarden.org>
7Architecture: all
8Version: 0.5.5.1-3ubuntu1
9Depends: debianutils (>= 2.15), dpkg (>= 1.15.0)
10Pre-Depends: libc6 (>= 2.11~20100104-0ubuntu2)
11Filename: pool/main/d/dash/dash_0.5.5.1-3ubuntu1_all.deb
12Size: 104190
13MD5sum: a7f08fe3ee941d06c0d98e5e99c02190
14SHA1: d2dda78f9a6f82c58c01d34f18b94aebfcb33f19
15SHA256: 69709747f854ac1bd671dff37b47c18e589b095ecb8f3116beaed7fc0eeb657e
16Description: POSIX-compliant shell
17 The Debian Almquist Shell (dash) is a POSIX-compliant shell derived
18 from ash.
19 .
20 Since it executes scripts faster than bash, and has fewer library
21 dependencies (making it more robust against software or hardware
22 failures), it is used as the default system shell on Debian systems.
23Homepage: http://gondor.apana.org.au/~herbert/dash/
24Bugs: https://bugs.launchpad.net/ubuntu/+filebug
25Origin: Ubuntu
26Supported: 5y
27Task: minimal
28
29Package: gzip
30Essential: yes
31Priority: required
32Section: utils
33Installed-Size: 284
34Maintainer: Ubuntu Core Developers <ubuntu-devel-discuss@lists.ubuntu.com>
35Original-Maintainer: Bdale Garbee <bdale@gag.com>
36Architecture: all
37Version: 1.3.12-9ubuntu1
38Pre-Depends: libc6 (>= 2.4)
39Suggests: less
40Filename: pool/main/g/gzip/gzip_1.3.12-9ubuntu1_all.deb
41Size: 107030
42MD5sum: f64beb93d2d1a3348cfc47f1fd176ee1
43SHA1: 3dd3e56f551fb85ba2ad385df463adeff1fff2d9
44SHA256: 2545f0a28514535006adf9ee8576ca3be2aa6da3d890047f92eeda18c0e3aa57
45Description: GNU compression utilities
46 This package provides the standard GNU file compression utilities, which
47 are also the default compression tools for Debian. They typically operate
48 on files with names ending in '.gz', but can also decompress files ending
49 in '.Z' created with 'compress'.
50Bugs: https://bugs.launchpad.net/ubuntu/+filebug
51Origin: Ubuntu
52Supported: 5y
53Task: minimal
54
055
=== added directory 'computerjanitord/tests/data/var/lib/apt/lists/partial'
=== added directory 'computerjanitord/tests/data/var/lib/dpkg'
=== added file 'computerjanitord/tests/data/var/lib/dpkg/status'
--- computerjanitord/tests/data/var/lib/dpkg/status 1970-01-01 00:00:00 +0000
+++ computerjanitord/tests/data/var/lib/dpkg/status 2010-03-10 21:40:30 +0000
@@ -0,0 +1,28 @@
1Package: dash-nodownload
2Priority: required
3Section: shells
4Installed-Size: 236
5Maintainer: Ubuntu Core Developers <ubuntu-devel-discuss@lists.ubuntu.com>
6Original-Maintainer: Gerrit Pape <pape@smarden.org>
7Architecture: all
8Version: 0.5.5.1-3ubuntu1
9Depends: debianutils (>= 2.15), dpkg (>= 1.15.0)
10Pre-Depends: libc6 (>= 2.11~20100104-0ubuntu2)
11Filename: pool/main/d/dash/dash_0.5.5.1-3ubuntu1_all.deb
12Size: 104190
13MD5sum: a7f08fe3ee941d06c0d98e5e99c02190
14SHA1: d2dda78f9a6f82c58c01d34f18b94aebfcb33f19
15SHA256: 69709747f854ac1bd671dff37b47c18e589b095ecb8f3116beaed7fc0eeb657e
16Description: POSIX-compliant shell
17 The Debian Almquist Shell (dash) is a POSIX-compliant shell derived
18 from ash.
19 .
20 Since it executes scripts faster than bash, and has fewer library
21 dependencies (making it more robust against software or hardware
22 failures), it is used as the default system shell on Debian systems.
23Homepage: http://gondor.apana.org.au/~herbert/dash/
24Bugs: https://bugs.launchpad.net/ubuntu/+filebug
25Origin: Ubuntu
26Supported: 5y
27Task: minimal
28
029
=== added file 'computerjanitord/tests/test_all.py'
--- computerjanitord/tests/test_all.py 1970-01-01 00:00:00 +0000
+++ computerjanitord/tests/test_all.py 2010-03-10 21:40:30 +0000
@@ -0,0 +1,41 @@
1# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, version 3 of the License.
6#
7# This program is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License
13# along with this program. If not, see <http://www.gnu.org/licenses/>.
14
15"""Test suite for Computer Janitor daemon (dbus backend)."""
16
17from __future__ import absolute_import, unicode_literals
18
19__metaclass__ = type
20__all__ = [
21 'test_suite',
22 ]
23
24
25import unittest
26
27from computerjanitord.tests import test_application
28from computerjanitord.tests import test_authenticator
29from computerjanitord.tests import test_collector
30from computerjanitord.tests import test_state
31from computerjanitord.tests import test_whitelist
32
33
34def test_suite():
35 suite = unittest.TestSuite()
36 suite.addTests(test_application.test_suite())
37 suite.addTests(test_authenticator.test_suite())
38 suite.addTests(test_collector.test_suite())
39 suite.addTests(test_state.test_suite())
40 suite.addTests(test_whitelist.test_suite())
41 return suite
042
=== added file 'computerjanitord/tests/test_application.py'
--- computerjanitord/tests/test_application.py 1970-01-01 00:00:00 +0000
+++ computerjanitord/tests/test_application.py 2010-03-10 21:40:30 +0000
@@ -0,0 +1,106 @@
1# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, version 3 of the License.
6#
7# This program is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License
13# along with this program. If not, see <http://www.gnu.org/licenses/>.
14
15"""Test the plugin application interfaces."""
16
17from __future__ import absolute_import, unicode_literals
18
19__metaclass__ = type
20__all__ = [
21 'ApplicationTestSetupMixin',
22 'test_suite',
23 ]
24
25
26import os
27import apt
28import apt_pkg
29import unittest
30import warnings
31import pkg_resources
32
33from contextlib import contextmanager
34
35import computerjanitor
36import computerjanitord.application
37
38from computerjanitord.application import (
39 Application, MissingLandmarkError, NonDownloadableError)
40
41
42@contextmanager
43def landmarks(*packages):
44 # Hack the module global list of known landmark packages.
45 old_landmarks = computerjanitord.application.LANDMARK_PACKAGES[:]
46 computerjanitord.application.LANDMARK_PACKAGES[:] = packages
47 yield
48 computerjanitord.application.LANDMARK_PACKAGES[:] = old_landmarks
49
50
51class MockCruft:
52 def __init__(self, name):
53 self.name = name
54
55 def get_name(self):
56 warnings.warn('.get_name() is deprecated; use .name',
57 DeprecationWarning)
58 return self.name
59
60
61class ApplicationTestSetupMixin:
62 """Set up an `Application` instance with test data in its apt_cache."""
63
64 def setUp(self):
65 self.data_dir = os.path.abspath(
66 pkg_resources.resource_filename('computerjanitord.tests', 'data'))
67 # Make the test insensitive to the platform's architecture.
68 apt_pkg.Config.Set('APT::Architecture', 'i386')
69 self.cache = apt.Cache(rootdir=self.data_dir)
70 self.app = Application(self.cache)
71
72 def tearDown(self):
73 # Clear the cache.
74 cache_dir = os.path.join(self.data_dir, 'var', 'cache', 'apt')
75 for filename in os.listdir(cache_dir):
76 if filename.endswith('.bin'):
77 os.remove(os.path.join(cache_dir, filename))
78
79
80class TestApplication(unittest.TestCase, ApplicationTestSetupMixin):
81 """Test the `Application` interface."""
82
83 def setUp(self):
84 ApplicationTestSetupMixin.setUp(self)
85
86 def tearDown(self):
87 ApplicationTestSetupMixin.tearDown(self)
88
89 def test_verify_apt_cache_good_path(self):
90 # All essential packages are in the cache by default.
91 self.assertEqual(self.app.verify_apt_cache(), None)
92
93 def test_verify_apt_cache_with_nondownloadable_landmark(self):
94 # Test that a missing landmark file causes an exception.
95 with landmarks('gzip', 'dash-nodownload'):
96 self.assertRaises(NonDownloadableError, self.app.verify_apt_cache)
97
98 def test_verify_apt_cache_with_missing_landmark(self):
99 with landmarks('gzip', 'dash', 'i-am-not-here'):
100 self.assertRaises(MissingLandmarkError, self.app.verify_apt_cache)
101
102
103def test_suite():
104 suite = unittest.TestSuite()
105 suite.addTests(unittest.makeSuite(TestApplication))
106 return suite
0107
=== added file 'computerjanitord/tests/test_authenticator.py'
--- computerjanitord/tests/test_authenticator.py 1970-01-01 00:00:00 +0000
+++ computerjanitord/tests/test_authenticator.py 2010-03-10 21:40:30 +0000
@@ -0,0 +1,97 @@
1# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, version 3 of the License.
6#
7# This program is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License
13# along with this program. If not, see <http://www.gnu.org/licenses/>.
14
15"""Test the authenticator."""
16
17import unittest
18
19from computerjanitord.authenticator import Authenticator
20
21SUCCESS_PID = 801
22FAILURE_PID = 999
23
24AUTHENTICATED_USER = 'desktop-user'
25IMPOSTER_USER = 'imposter'
26EXPECTED_PRIVILEGE = 'com.ubuntu.computerjanitor.cleanpackages'
27BOGUS_PRIVILEGE = 'com.example.evil-corp.killsystem'
28
29
30class MockPolicyKit(object):
31 """Mock the PolicyKit's CheckAuthorization() method."""
32
33 def CheckAuthorization(self, subject, privilege, details, flags,
34 cancellation_id):
35 """See `policykit.CheckAuthorization()`.
36
37 :return: (is_authorized, is_challenge, details)
38 """
39 if privilege == BOGUS_PRIVILEGE:
40 return False, False, ''
41 assert isinstance(subject, tuple) and len(subject) == 2, (
42 'subject is not a 2-tuple')
43 assert subject[0] == 'unix-process', 'Badly formed subject'
44 assert isinstance(subject[1], dict), 'Badly formed subject details'
45 assert subject[1]['start-time'] == 0, 'subject missing start-time'
46 if subject[1]['pid'] == SUCCESS_PID:
47 return True, False, ''
48 else:
49 return False, False, ''
50
51
52class TestableAuthenticator(Authenticator):
53 """See `Authenticator`."""
54
55 def _get_policykit_proxy(self):
56 """See `Authenticator`."""
57 return MockPolicyKit()
58
59 def _get_sender_pid(self, connection, sender):
60 """See `Authenticator`."""
61 if sender == AUTHENTICATED_USER:
62 return SUCCESS_PID
63 else:
64 return FAILURE_PID
65
66
67class TestAuthenticator(unittest.TestCase):
68 """Tests of the PolicyKit authenticator."""
69
70 def setUp(self):
71 """See `unittest.TestCase`."""
72 self.authenticator = TestableAuthenticator()
73 self.connection = object()
74
75 def tearDown(self):
76 """See `unittest.TestCase`."""
77
78 def test_good_path(self):
79 # Test for successful authentication.
80 self.assertTrue(self.authenticator.authenticate(
81 AUTHENTICATED_USER, self.connection, EXPECTED_PRIVILEGE))
82
83 def test_bogus_privilege(self):
84 # Test for bogus privilege fails.
85 self.assertFalse(self.authenticator.authenticate(
86 AUTHENTICATED_USER, self.connection, BOGUS_PRIVILEGE))
87
88 def test_unauthorized(self):
89 # Test for some imposter not being able to authenticate.
90 self.assertFalse(self.authenticator.authenticate(
91 IMPOSTER_USER, self.connection, EXPECTED_PRIVILEGE))
92
93
94def test_suite():
95 suite = unittest.TestSuite()
96 suite.addTests(unittest.makeSuite(TestAuthenticator))
97 return suite
098
=== added file 'computerjanitord/tests/test_collector.py'
--- computerjanitord/tests/test_collector.py 1970-01-01 00:00:00 +0000
+++ computerjanitord/tests/test_collector.py 2010-03-10 21:40:30 +0000
@@ -0,0 +1,187 @@
1# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, version 3 of the License.
6#
7# This program is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License
13# along with this program. If not, see <http://www.gnu.org/licenses/>.
14
15"""Test the cruft collector."""
16
17from __future__ import absolute_import, unicode_literals
18
19__metaclass__ = type
20__all__ = [
21 'test_suite',
22 ]
23
24
25import os
26import shutil
27import tempfile
28import unittest
29
30from computerjanitor.plugin import Plugin
31from computerjanitord.collector import Collector
32from computerjanitord.errors import DuplicateCruftError
33from computerjanitord.tests.test_application import ApplicationTestSetupMixin
34
35
36class MockCruft:
37 """Mock cruft that supports the required `get_name()` interface."""
38
39 def __init__(self, name):
40 self.name = name
41
42 def get_name(self):
43 return self.name
44
45
46class MockPlugin(Plugin):
47 cruft_class = MockCruft
48
49 def __init__(self, prefix, shortnames):
50 super(MockPlugin, self).__init__()
51 self.prefix = prefix
52 self.shortnames = shortnames
53
54 def get_cruft(self):
55 for shortname in self.shortnames:
56 yield self.cruft_class('{0}:{1}'.format(self.prefix, shortname))
57
58
59class SawAppPlugin(Plugin):
60 """All this plugin does is set a marker attribute on the `Application`.
61
62 This proves that access to the application through the plugin works.
63 """
64 def get_cruft(self):
65 self.app.saw_app_plugin = True
66 return []
67
68
69class MockPluginManager:
70 def __init__(self, app, plugin_dirs):
71 self.app = app
72 # Ignore plugin_dirs
73
74 def get_plugins(self):
75 shortnames = ('one', 'two', 'three')
76 for prefix in ('foo', 'bar', 'baz'):
77 plugin = MockPlugin(prefix, shortnames)
78 plugin.set_application(self.app)
79 yield plugin
80 plugin = SawAppPlugin()
81 plugin.set_application(self.app)
82 yield plugin
83
84
85class MockCruftExtra(MockCruft):
86 """Cruft with a different class."""
87
88
89class MockPluginExtra(MockPlugin):
90 """A mock plugin that returns cruft with a different class."""
91
92 cruft_class = MockCruftExtra
93
94
95class IgnoredDuplicateCruftPluginManager(MockPluginManager):
96 """Add an additional piece of ignorable duplication cruft."""
97
98 def __init__(self, app, plugin_dirs):
99 self.app = app
100 # Ignore plugin_dirs
101
102 def get_plugins(self):
103 yield MockPlugin('one', ('foo', 'bar'))
104 yield MockPlugin('one', ('baz', 'foo'))
105
106
107class BadDuplicateCruftPluginManager(MockPluginManager):
108 """Add an additional piece of bad duplicate cruft."""
109 def __init__(self, app, plugin_dirs):
110 self.app = app
111 # Ignore plugin_dirs
112
113 def get_plugins(self):
114 yield MockPlugin('one', ('foo', 'bar'))
115 yield MockPluginExtra('one', ('baz', 'foo'))
116
117
118class TestCollector(unittest.TestCase, ApplicationTestSetupMixin):
119 """Test the cruft collector."""
120
121 def setUp(self):
122 # Set up the test data Application.
123 ApplicationTestSetupMixin.setUp(self)
124 self.tempdir = tempfile.mkdtemp()
125 whitelist_dirs = (self.tempdir,)
126 with open(os.path.join(self.tempdir, 'one.whitelist'), 'w') as fp:
127 print >> fp, 'foo:two'
128 print >> fp, 'bar:one'
129 print >> fp, 'baz:three'
130 self.collector = Collector(self.app, MockPluginManager, whitelist_dirs)
131
132 def tearDown(self):
133 shutil.rmtree(self.tempdir)
134 ApplicationTestSetupMixin.tearDown(self)
135
136 def test_cruft_collector(self):
137 cruft_names = set(cruft.get_name() for cruft in self.collector.cruft)
138 self.assertEqual(cruft_names, set(('foo:one', 'foo:three',
139 'bar:two', 'bar:three',
140 'baz:one', 'baz:two')))
141
142 def test_plugin_needs_application(self):
143 # SawAppPlugin returned by the MockPluginManager sets this attribute
144 # on the Application.
145 self.assertTrue(self.app.saw_app_plugin)
146
147 def test_collector_name_mapping(self):
148 cruft_keys = set(self.collector.cruft_by_name)
149 cruft_names = set(cruft.get_name() for cruft in self.collector.cruft)
150 self.assertEqual(cruft_keys, cruft_names)
151
152
153class TestDuplicateCruftCollector(
154 unittest.TestCase, ApplicationTestSetupMixin):
155
156 def setUp(self):
157 # Set up the test data Application.
158 ApplicationTestSetupMixin.setUp(self)
159
160 def tearDown(self):
161 ApplicationTestSetupMixin.tearDown(self)
162
163 def test_duplicate_cruft_error(self):
164 self.assertRaises(DuplicateCruftError, Collector,
165 self.app, BadDuplicateCruftPluginManager, [])
166
167 def test_duplicate_cruft_error_message(self):
168 try:
169 Collector(self.app, BadDuplicateCruftPluginManager, [])
170 except DuplicateCruftError as error:
171 self.assertEqual(
172 str(error),
173 'Duplicate cruft with different cleanup: one:foo')
174 else:
175 raise AssertionError('DuplicateCruftError expected')
176
177 def test_ignored_duplicate_cruft(self):
178 collector = Collector(self.app, IgnoredDuplicateCruftPluginManager, [])
179 self.assertEqual(list(cruft.get_name() for cruft in collector.cruft),
180 ['one:foo', 'one:bar', 'one:baz'])
181
182
183def test_suite():
184 suite = unittest.TestSuite()
185 suite.addTests(unittest.makeSuite(TestCollector))
186 suite.addTests(unittest.makeSuite(TestDuplicateCruftCollector))
187 return suite
0188
=== added file 'computerjanitord/tests/test_state.py'
--- computerjanitord/tests/test_state.py 1970-01-01 00:00:00 +0000
+++ computerjanitord/tests/test_state.py 2010-03-10 21:40:30 +0000
@@ -0,0 +1,141 @@
1# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, version 3 of the License.
6#
7# This program is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License
13# along with this program. If not, see <http://www.gnu.org/licenses/>.
14
15"""Test the package state."""
16
17from __future__ import absolute_import, unicode_literals
18
19__metaclass__ = type
20__all__ = [
21 'test_suite',
22 ]
23
24
25import os
26import difflib
27import tempfile
28import textwrap
29import unittest
30
31from computerjanitord.state import State
32
33NL = '\n'
34
35
36class TestState(unittest.TestCase):
37 """Test the `State` class."""
38
39 def setUp(self):
40 self.state = State()
41 # Create a temporary file with some enabled and disabled packages.
42 fd, self._state_file = tempfile.mkstemp()
43 os.close(fd)
44 with open(self._state_file, 'w') as fp:
45 print >> fp, textwrap.dedent("""\
46 [deb:foo]
47 ignore: false
48 [deb:bar]
49 ignore: true
50 [deb:baz]
51 ignore: true
52 """)
53 fd, self._state_file_old = tempfile.mkstemp()
54 os.close(fd)
55 with open(self._state_file_old, 'w') as fp:
56 print >> fp, textwrap.dedent("""\
57 [deb:qux]
58 enabled: false
59 [deb:fno]
60 enabled: true
61 [deb:bla]
62 enabled: true
63 """)
64 fd, self._write_file = tempfile.mkstemp()
65 os.close(fd)
66
67 def tearDown(self):
68 os.remove(self._state_file)
69 os.remove(self._state_file_old)
70 os.remove(self._write_file)
71
72 def assertEqualNdiff(self, expected, got):
73 expected_lines = expected.splitlines()
74 got_lines = got.splitlines()
75 self.assertEqual(
76 expected, got,
77 '\n' + NL.join(difflib.ndiff(expected_lines, got_lines)))
78
79 def test_initially_no_previously_ignored(self):
80 self.assertEqual(self.state.ignore, set())
81
82 def test_load_state(self):
83 self.state.load(self._state_file)
84 self.assertEqual(self.state.ignore, set(('deb:bar', 'deb:baz')))
85
86 def test_backward_compatibility_file_format(self):
87 # Here, enabled:false means to ignore the package.
88 self.state.load(self._state_file_old)
89 self.assertEqual(self.state.ignore, set(('deb:qux',)))
90
91 def test_ignore(self):
92 self.state.ignore.add('deb:buz')
93 self.state.ignore.add('deb:baz')
94 self.assertEqual(self.state.ignore, set(('deb:buz', 'deb:baz')))
95
96 def test_more_ignores(self):
97 self.state.load(self._state_file)
98 self.state.ignore.add('deb:buz')
99 self.state.ignore.add('deb:baz')
100 self.assertEqual(self.state.ignore,
101 set(('deb:bar', 'deb:buz', 'deb:baz')))
102
103 def test_unignore(self):
104 self.state.load(self._state_file)
105 self.state.ignore.remove('deb:bar')
106 self.assertEqual(self.state.ignore, set(('deb:baz',)))
107
108 def test_write(self):
109 self.state.load(self._state_file)
110 self.state.ignore.add('deb:buz')
111 self.state.ignore.remove('deb:baz')
112 self.state.save(self._write_file)
113 with open(self._write_file) as fp:
114 got = fp.read()
115 expected = textwrap.dedent("""\
116 [deb:bar]
117 ignore: true
118
119 [deb:buz]
120 ignore: true
121
122 """)
123 self.assertEqualNdiff(expected, got)
124
125 def test_write_new_format(self):
126 self.state.load(self._state_file_old)
127 self.state.save(self._write_file)
128 with open(self._write_file) as fp:
129 got = fp.read()
130 expected = textwrap.dedent("""\
131 [deb:qux]
132 ignore: true
133
134 """)
135 self.assertEqualNdiff(expected, got)
136
137
138def test_suite():
139 suite = unittest.TestSuite()
140 suite.addTests(unittest.makeSuite(TestState))
141 return suite
0142
=== added file 'computerjanitord/tests/test_whitelist.py'
--- computerjanitord/tests/test_whitelist.py 1970-01-01 00:00:00 +0000
+++ computerjanitord/tests/test_whitelist.py 2010-03-10 21:40:30 +0000
@@ -0,0 +1,119 @@
1# Copyright (C) 2008, 2009, 2010 Canonical, Ltd.
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, version 3 of the License.
6#
7# This program is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License
13# along with this program. If not, see <http://www.gnu.org/licenses/>.
14
15"""Test the whitelister."""
16
17from __future__ import absolute_import, unicode_literals
18
19__metaclass__ = type
20__all__ = [
21 'test_suite',
22 ]
23
24
25import os
26import shutil
27import tempfile
28import unittest
29
30from computerjanitord.whitelist import Whitelist
31
32
33class MockCruft:
34 """Mock cruft that supports the required `get_name()` interface."""
35
36 def __init__(self, shortname):
37 self.name = 'foo:' + shortname
38
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches