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