GTG

Merge lp:~jml/gtg/test-framework into lp:~gtg/gtg/old-trunk

Proposed by Jonathan Lange
Status: Merged
Merged at revision: not available
Proposed branch: lp:~jml/gtg/test-framework
Merge into: lp:~gtg/gtg/old-trunk
Diff against target: None lines
To merge this branch: bzr merge lp:~jml/gtg/test-framework
Reviewer Review Type Date Requested Status
Gtg developers Pending
Review via email: mp+8743@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Jonathan Lange (jml) wrote :

This branch puts a traditional Python unit testing framework in place.

It moves the top-level 'tests' into a package called GTG.tests. This is partly so that tests can import from each other, partly convention and partly so tools like trial and nosetests can be used to run the tests.

While I was looking at backends.py, I fixed up all of the PEP 8 violations and tidied up some of the docstrings. backends.py is now called test_backends.py, again to help discovery of tests.

Once this is done, the unit.sh script is no longer needed, so that's removed.

The branch also moves the debug.sh script out to 'scripts' and changes the debug directory to debug_data. It adds a bzrignore for debug_data too.

If you want to run tests now, you can use 'make check'. I've documented this and a bunch of other things in the new HACKING file.

This branch was branched from lp:~jml/gtg/better-lint-checking, so the diff you see on Launchpad is bigger than the actual changes in the branch. I'll reply to this post attaching the real diff.

Thanks,
jml

Revision history for this message
Jonathan Lange (jml) wrote :

On Tue, Jul 14, 2009 at 10:25 PM, Jonathan Lange<email address hidden> wrote:
> Jonathan Lange has proposed merging lp:~jml/gtg/test-framework into lp:gtg.
>
> Requested reviews:
>    Gtg developers (gtg)
>
> This branch puts a traditional Python unit testing framework in place.
>
...
> This branch was branched from lp:~jml/gtg/better-lint-checking, so the diff you see on Launchpad is bigger than the actual changes in the branch. I'll reply to this post attaching the real diff.
>

Attached.

1=== modified file '.bzrignore'
2--- .bzrignore 2009-05-12 07:36:21 +0000
3+++ .bzrignore 2009-07-14 12:13:22 +0000
4@@ -33,3 +33,5 @@
5 .settings
6 .bzrignore
7
8+debug_data
9+_trial_temp
10
11=== renamed directory 'tests' => 'GTG/tests'
12=== added file 'GTG/tests/__init__.py'
13--- GTG/tests/__init__.py 1970-01-01 00:00:00 +0000
14+++ GTG/tests/__init__.py 2009-07-14 12:09:40 +0000
15@@ -0,0 +1,30 @@
16+# -*- coding: utf-8 -*-
17+# -----------------------------------------------------------------------------
18+# Gettings Things Gnome! - a personnal organizer for the GNOME desktop
19+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
20+#
21+# This program is free software: you can redistribute it and/or modify it under
22+# the terms of the GNU General Public License as published by the Free Software
23+# Foundation, either version 3 of the License, or (at your option) any later
24+# version.
25+#
26+# This program is distributed in the hope that it will be useful, but WITHOUT
27+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
28+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
29+# details.
30+#
31+# You should have received a copy of the GNU General Public License along with
32+# this program. If not, see <http://www.gnu.org/licenses/>.
33+# -----------------------------------------------------------------------------
34+
35+"""Unit tests for GTG."""
36+
37+import unittest
38+
39+from GTG.tests import test_backends
40+
41+
42+def test_suite():
43+ return unittest.TestSuite([
44+ test_backends.test_suite(),
45+ ])
46
47=== renamed file 'tests/backends.py' => 'GTG/tests/test_backends.py'
48--- tests/backends.py 2009-03-17 18:56:53 +0000
49+++ GTG/tests/test_backends.py 2009-07-14 11:28:26 +0000
50@@ -1,4 +1,3 @@
51-#!/usr/bin/env python
52 # -*- coding: utf-8 -*-
53 # -----------------------------------------------------------------------------
54 # Gettings Things Gnome! - a personnal organizer for the GNOME desktop
55@@ -18,8 +17,9 @@
56 # this program. If not, see <http://www.gnu.org/licenses/>.
57 # -----------------------------------------------------------------------------
58
59-"""Unittests for GTG
60-Some of these tests will generated files in
61+"""Tests for GTG backends.
62+
63+Some of these tests will generate files in
64 xdg.BaseDirectory.xdg_data_home/gtg directory.
65 """
66
67@@ -35,7 +35,7 @@
68
69
70 class GtgBackendsUniTests(unittest.TestCase):
71- """Unittests for GTG backends"""
72+ """Tests for GTG backends."""
73
74 def __init__(self, test):
75 unittest.TestCase.__init__(self, test)
76@@ -43,7 +43,6 @@
77 self.datafile = ''
78 self.taskpath = ''
79 self.datapath = ''
80-
81
82 def test_localfile_get_name(self):
83 """Tests for localfile/get_name function :
84@@ -58,12 +57,12 @@
85 - a string is expected.
86 """
87 res = localfile.get_description()
88- expectedres = "Your tasks are saved in an XML file located in \
89-your HOME folder"
90+ expectedres = ("Your tasks are saved in an XML file located in "
91+ "your HOME folder")
92 self.assertEqual(res, expectedres)
93
94 def test_localfile_get_parameters(self):
95- """Tests for localfile/get_parameters function :
96+ """Tests for localfile/get_parameters function:
97 - a string is expected.
98 """
99 res = localfile.get_parameters()
100@@ -71,7 +70,7 @@
101 self.assertEqual(res['filename'], expectedres)
102
103 def test_localfile_get_features(self):
104- """Tests for localfile/get_features function :
105+ """Tests for localfile/get_features function:
106 - an empty dictionary is expected.
107 """
108 res = localfile.get_features()
109@@ -79,7 +78,7 @@
110 self.assertEqual(res, expectedres)
111
112 def test_localfile_get_type(self):
113- """Tests for localfile/get_type function :
114+ """Tests for localfile/get_type function:
115 - a string is expected.
116 """
117 res = localfile.get_type()
118@@ -87,7 +86,7 @@
119 self.assertEqual(res, expectedres)
120
121 def test_localfile_backend(self):
122- """Tests for localfile/Backend Class :
123+ """Tests for localfile/Backend Class:
124 - an empty list is expected
125 """
126 res = localfile.Backend({})
127@@ -95,7 +94,7 @@
128 self.assertEqual(res.get_tasks_list(), expectedres)
129
130 def test_localfile_backend_method1(self):
131- """Tests for localfile/Backend/new_task_id method :
132+ """Tests for localfile/Backend/new_task_id method:
133 - None value is expected.
134 """
135 res = localfile.Backend({})
136@@ -103,7 +102,7 @@
137 self.assertEqual(res.new_task_id(), expectedres)
138
139 def test_localfile_backend_method2(self):
140- """Tests for localfile/Backend/get_tasks_list method :
141+ """Tests for localfile/Backend/get_tasks_list method:
142 - an integer value is expected.
143 """
144 self.create_test_environment()
145@@ -111,7 +110,7 @@
146 xmlproject = doc.getElementsByTagName('backend')
147 for domobj in xmlproject:
148 dic = {}
149- if domobj.hasAttribute("module") :
150+ if domobj.hasAttribute("module"):
151 dic["module"] = str(domobj.getAttribute("module"))
152 dic["pid"] = str(domobj.getAttribute("pid"))
153 dic["xmlobject"] = domobj
154@@ -121,7 +120,7 @@
155 self.assertEqual(len(res.get_tasks_list()), expectedres)
156
157 def test_localfile_backend_method3(self):
158- """Tests for localfile/Backend/remove_task method :
159+ """Tests for localfile/Backend/remove_task method:
160 - parse task file to check if task has been removed.
161 """
162 self.create_test_environment()
163@@ -129,7 +128,7 @@
164 xmlproject = doc.getElementsByTagName('backend')
165 for domobj in xmlproject:
166 dic = {}
167- if domobj.hasAttribute("module") :
168+ if domobj.hasAttribute("module"):
169 dic["module"] = str(domobj.getAttribute("module"))
170 dic["pid"] = str(domobj.getAttribute("pid"))
171 dic["xmlobject"] = domobj
172@@ -146,7 +145,7 @@
173 self.assertEqual(res, expectedres)
174
175 def test_localfile_backend_method4(self):
176- """Tests for localfile/Backend/get_task method :
177+ """Tests for localfile/Backend/get_task method:
178 - Compares task titles to check if method works.
179 """
180 self.create_test_environment()
181@@ -154,7 +153,7 @@
182 xmlproject = doc.getElementsByTagName('backend')
183 for domobj in xmlproject:
184 dic = {}
185- if domobj.hasAttribute("module") :
186+ if domobj.hasAttribute("module"):
187 dic["module"] = str(domobj.getAttribute("module"))
188 dic["pid"] = str(domobj.getAttribute("pid"))
189 dic["xmlobject"] = domobj
190@@ -166,7 +165,7 @@
191 self.assertEqual(newtask.get_title(), u"Ceci est un test")
192
193 def test_localfile_backend_method5(self):
194- """Tests for localfile/Backend/set_task method :
195+ """Tests for localfile/Backend/set_task method:
196 - parses task file to check if new task has been stored.
197 """
198 self.create_test_environment()
199@@ -174,7 +173,7 @@
200 xmlproject = doc.getElementsByTagName('backend')
201 for domobj in xmlproject:
202 dic = {}
203- if domobj.hasAttribute("module") :
204+ if domobj.hasAttribute("module"):
205 dic["module"] = str(domobj.getAttribute("module"))
206 dic["pid"] = str(domobj.getAttribute("pid"))
207 dic["xmlobject"] = domobj
208@@ -195,13 +194,22 @@
209 """Create the test environment"""
210 self.taskfile = 'test.xml'
211 self.datafile = 'projectstest.xml'
212- tasks = ['<?xml version="1.0" ?>\n', '<project>\n',
213- '\t<task id="0@1" status="Active" tags="">\n', '\t\t<title>\n',
214- '\t\t\tCeci est un test\n', '\t\t</title>\n', '\t</task>\n',
215- '</project>\n']
216- data = ['<?xml version="1.0" ?>\n', '<config>\n',
217- '\t<backend filename="test.xml" module="localfile" pid="1"/>\n',
218- '</config>\n']
219+ tasks = [
220+ '<?xml version="1.0" ?>\n',
221+ '<project>\n',
222+ '\t<task id="0@1" status="Active" tags="">\n',
223+ '\t\t<title>\n',
224+ '\t\t\tCeci est un test\n',
225+ '\t\t</title>\n',
226+ '\t</task>\n',
227+ '</project>\n',
228+ ]
229+ data = [
230+ '<?xml version="1.0" ?>\n',
231+ '<config>\n',
232+ '\t<backend filename="test.xml" module="localfile" pid="1"/>\n',
233+ '</config>\n',
234+ ]
235 testdir = os.path.join(xdg.BaseDirectory.xdg_data_home, 'gtg')
236 if not os.path.exists(testdir):
237 os.makedirs(testdir)
238@@ -210,6 +218,6 @@
239 open(self.taskpath, 'w').writelines(tasks)
240 open(self.datapath, 'w').writelines(data)
241
242-if __name__ == '__main__':
243- unittest.main()
244
245+def test_suite():
246+ return unittest.TestLoader().loadTestsFromName(__name__)
247
248=== added file 'HACKING'
249--- HACKING 1970-01-01 00:00:00 +0000
250+++ HACKING 2009-07-14 12:12:59 +0000
251@@ -0,0 +1,124 @@
252+=================
253+GTG Hacking Guide
254+=================
255+
256+Testing
257+-------
258+
259+You can run the unit tests for GTG with::
260+
261+ make check
262+
263+If you are so inclined and have the right software installed, you can also run
264+the tests with ``trial GTG``.
265+
266+You can also manually test your changes with debugging data with::
267+
268+ ./scripts/debug.sh
269+
270+Using ``debug.sh`` will prevent GTG from messing with your real data. Instead,
271+the debug GTG will store data in ``debug_data/``.
272+
273+Unit tests live in ``GTG/tests/``, and are all named ``test_foo``. When you
274+add a new test module, make sure it has a ``test_suite()`` method that returns
275+the suite of all the tests in that module. Also make sure that the new module
276+is imported in ``GTG.tests`` and returned from the ``test_suite()`` function
277+there.
278+
279+For example, GTG/tests/test_newthing.py::
280+
281+ import unittest
282+
283+ class TestNewThing(unittest.TestCase):
284+ # ...
285+
286+ def test_suite():
287+ return unittest.TestLoader().loadTestsFromName(__name__)
288+
289+
290+And GTG/tests/__init__.py::
291+
292+ import unittest
293+
294+ from GTG.tests import test_backends, test_newthing
295+
296+ def test_suite():
297+ return unittest.TestSuite([
298+ test_backends.test_suite(),
299+ test_newthing.test_suite(),
300+ ])
301+
302+When in doubt, copy from an existing test module!
303+
304+
305+Coding style
306+------------
307+
308+In general, follow PEP 8 <http://www.python.org/dev/peps/pep-0008/>.
309+
310+Not all code in GTG currently follows PEP 8. If you are changing a section of
311+code, please update it to follow PEP 8.
312+
313+You should also avoid adding any 'flakes', simple Python mistakes caught by
314+Pyflakes <http://www.divmod.org/trac/wiki/DivmodPyflakes>.
315+
316+To check the cleanliness of your code, run::
317+
318+ make lint
319+
320+The ``make`` will fail if Pyflakes emits any warnings. You can the Pyflakes
321+checker separately using ``make pyflakes`` and the PEP 8 checker using ``make
322+pep8``. If you wish to find all PEP 8 violations in a particular file, use::
323+
324+ ./scripts/pep8.py --repeat FILENAME
325+
326+
327+Copyright
328+---------
329+
330+All modules should begin with the following header::
331+
332+# -*- coding: utf-8 -*-
333+# -----------------------------------------------------------------------------
334+# Gettings Things Gnome! - a personnal organizer for the GNOME desktop
335+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
336+#
337+# This program is free software: you can redistribute it and/or modify it under
338+# the terms of the GNU General Public License as published by the Free Software
339+# Foundation, either version 3 of the License, or (at your option) any later
340+# version.
341+#
342+# This program is distributed in the hope that it will be useful, but WITHOUT
343+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
344+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
345+# details.
346+#
347+# You should have received a copy of the GNU General Public License along with
348+# this program. If not, see <http://www.gnu.org/licenses/>.
349+# -----------------------------------------------------------------------------
350+
351+
352+Submitting Patches
353+------------------
354+
355+For information about contributing code to GTG, see
356+<http://live.gnome.org/gtg/contributing>.
357+
358+
359+Landing Branches
360+----------------
361+
362+ 1. Get the branch.
363+
364+ 2. Run the tests, ``make check``.
365+
366+ 3. Run ``make lint``, check that the number of PEP 8 warnings is lower than
367+ trunk and that there are no new pyflakes warnings.
368+
369+ 4. Launch GTG with debugging data, just in case, ``./scripts/debug.sh``.
370+
371+ 5. Update ``CHANGELOG`` if it isn't already.
372+
373+ 6. Update ``AUTHORS`` if the patch author is not already in there.
374+
375+ 7. Merge the branch into trunk, then commit!
376
377=== modified file 'Makefile'
378--- Makefile 2009-07-14 11:18:05 +0000
379+++ Makefile 2009-07-14 12:15:02 +0000
380@@ -1,4 +1,15 @@
381
382+# Run all of the tests.
383+check:
384+ ./run-tests
385+
386+# Get rid of stale files or files made during testing.
387+clean:
388+ rm -rf _trial_temp
389+ rm -rf debug_data
390+ find . -name '*.pyc' -print0 | xargs -0 rm -f
391+ find . -name '*~' -print0 | xargs -0 rm -f
392+
393 # Check for common & easily catchable Python mistakes.
394 pyflakes:
395 pyflakes GTG
396@@ -11,4 +22,4 @@
397 # Check for coding standard violations & flakes.
398 lint: pyflakes pep8
399
400-.PHONY: lint pyflakes pep8
401+.PHONY: check lint pyflakes pep8
402
403=== added file 'run-tests'
404--- run-tests 1970-01-01 00:00:00 +0000
405+++ run-tests 2009-07-14 11:33:37 +0000
406@@ -0,0 +1,41 @@
407+#!/usr/bin/env python
408+# -*- coding: utf-8 -*-
409+# -----------------------------------------------------------------------------
410+# Gettings Things Gnome! - a personnal organizer for the GNOME desktop
411+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
412+#
413+# This program is free software: you can redistribute it and/or modify it under
414+# the terms of the GNU General Public License as published by the Free Software
415+# Foundation, either version 3 of the License, or (at your option) any later
416+# version.
417+#
418+# This program is distributed in the hope that it will be useful, but WITHOUT
419+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
420+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
421+# details.
422+#
423+# You should have received a copy of the GNU General Public License along with
424+# this program. If not, see <http://www.gnu.org/licenses/>.
425+# -----------------------------------------------------------------------------
426+
427+"""Runs the GTG unit tests."""
428+
429+import sys
430+import unittest
431+
432+from GTG.tests import test_suite
433+
434+
435+def main(args):
436+ runner = unittest.TextTestRunner(
437+ stream=sys.stdout, descriptions=False, verbosity=1)
438+ test = test_suite()
439+ result = runner.run(test)
440+ if result.wasSuccessful():
441+ return 0
442+ else:
443+ return 1
444+
445+
446+if __name__ == '__main__':
447+ sys.exit(main(sys.argv[1:]))
448
449=== renamed file 'tests/debug.sh' => 'scripts/debug.sh'
450--- tests/debug.sh 2009-03-01 13:42:07 +0000
451+++ scripts/debug.sh 2009-07-14 11:40:08 +0000
452@@ -1,4 +1,4 @@
453 #!/bin/bash
454-export XDG_DATA_HOME="./tests/xdg/data"
455-export XDG_CONFIG_HOME="./tests/xdg/config"
456+export XDG_DATA_HOME="./debug_data/xdg/data"
457+export XDG_CONFIG_HOME="./debug_data/xdg/config"
458 ./gtg
459
460=== removed file 'tests/unit.sh'
461--- tests/unit.sh 2009-03-17 18:56:53 +0000
462+++ tests/unit.sh 1970-01-01 00:00:00 +0000
463@@ -1,6 +0,0 @@
464-#!/bin/bash
465-#Launch unit tests
466-#Argument is name of one existing test
467-export XDG_DATA_HOME="./tests/xdg/data"
468-export XDG_CONFIG_HOME="./tests/xdg/config"
469-python ./tests/$1.py
Revision history for this message
Lionel Dricot (ploum-deactivatedaccount) wrote :

I merged your branch because everything seems at least not worst than before. Make lint is not working for me but we will solve that.

Could you verify if everything in fine in trunk ? (I solved a couple of conflicts manually).

Could you update the wiki then ?

It looks like a real nice work, as seen in the HACKING file :-)

Thanks a lot for that, we wait your next contribution !!

Lionel

Revision history for this message
Jonathan Lange (jml) wrote :

On Tue, Jul 14, 2009 at 11:19 PM, Lionel Dricot<email address hidden> wrote:
> I merged your branch because everything seems at least not worst than before. Make lint is not working for me but we will solve that.
>

Not working for you? What's the behaviour you're seeing?

> Could you verify if everything in fine in trunk ? (I solved a couple of conflicts manually).
>

Done. Everything's good.

> Could you update the wiki then ?
>

Done.

> It looks like a real nice work, as seen in the HACKING file :-)
>

Thanks for saying so :)

jml

Revision history for this message
Lionel Dricot (ploum-deactivatedaccount) wrote :

> On Tue, Jul 14, 2009 at 11:19 PM, Lionel Dricot<email address hidden> wrote:
>> I merged your branch because everything seems at least not worst than
>> before. Make lint is not working for me but we will solve that.
>>
>
> Not working for you? What's the behaviour you're seeing?

9:26 ploum@nout ~/gtg% make lint
pyflakes GTG
GTG/core/dbuswrapper.py:2: redefinition of unused 'dbus' from line 1
GTG/core/dbuswrapper.py:3: redefinition of unused 'dbus' from line 2
make: *** [pyflakes] Erreur 2
zsh: exit 2 make lint

>
>> Could you verify if everything in fine in trunk ? (I solved a couple of
>> conflicts manually).
>>
>
> Done. Everything's good.
>
>> Could you update the wiki then ?
>>
>
> Done.
>
>> It looks like a real nice work, as seen in the HACKING file :-)
>>
>
> Thanks for saying so :)
>
> jml
> --
> https://code.launchpad.net/~jml/gtg/test-framework/+merge/8743
> You are subscribed to branch lp:gtg.
>

Revision history for this message
Jonathan Lange (jml) wrote :

On Wed, Jul 15, 2009 at 5:27 PM, Lionel Dricot<email address hidden> wrote:
>> On Tue, Jul 14, 2009 at 11:19 PM, Lionel Dricot<email address hidden> wrote:
>>> I merged your branch because everything seems at least not worst than
>>> before. Make lint is not working for me but we will solve that.
>>>
>>
>> Not working for you? What's the behaviour you're seeing?
>
> 9:26 ploum@nout ~/gtg% make lint
> pyflakes GTG
> GTG/core/dbuswrapper.py:2: redefinition of unused 'dbus' from line 1
> GTG/core/dbuswrapper.py:3: redefinition of unused 'dbus' from line 2
> make: *** [pyflakes] Erreur 2
> zsh: exit 2     make lint
>

That bug has been fixed in Pyflakes, but it hasn't been released:
- http://divmod.org/trac/ticket/1826
- http://divmod.org/trac/changeset/17490

I recommend running from trunk, which you can get from Launchpad @
lp:pyflakes. I'll poke the maintainer to do another release.

However, the broader idea is that 'make lint' *should* fail if there's
lint. We can change this if you'd like.

jml

Revision history for this message
Lionel Dricot (ploum-deactivatedaccount) wrote :

> However, the broader idea is that 'make lint' *should* fail if there's
> lint. We can change this if you'd like.

I'm not sure I really understand this sentence. I thought that make lint
would produce a number of errors that should be fixed.

Revision history for this message
Jonathan Lange (jml) wrote :

On Wed, Jul 15, 2009 at 11:06 PM, Lionel Dricot<email address hidden> wrote:
>
>> However, the broader idea is that 'make lint' *should* fail if there's
>> lint. We can change this if you'd like.
>
> I'm not sure I really understand this sentence.  I thought that make lint
> would produce a number of errors that should be fixed.
>

That's what it does. However, like any script or any make process, it
can succeed or fail. In make, if a step fails, it says so. The "Error
2" thing you see is make bubbling up the error code of pyflakes.

So, one problem is that old versions of pyflakes think that
dbuswrapper is buggy.

The other problem (which is only a problem of expectations) is that
'make lint' says "Error" and doesn't run the PEP 8 checker if there
are any errors found.

jml

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2009-05-12 07:36:21 +0000
3+++ .bzrignore 2009-07-14 12:13:25 +0000
4@@ -33,3 +33,5 @@
5 .settings
6 .bzrignore
7
8+debug_data
9+_trial_temp
10
11=== modified file 'GTG/__init__.py'
12--- GTG/__init__.py 2009-07-12 22:05:07 +0000
13+++ GTG/__init__.py 2009-07-13 10:22:44 +0000
14@@ -96,8 +96,8 @@
15 translation = gettext.translation(GETTEXT_DOMAIN, LOCALE_PATH,
16 languages=languages_used,
17 fallback=True)
18-import __builtin__
19-__builtin__._ = translation.gettext
20+
21+_ = translation.gettext
22
23 #GTG directories setup
24 if not os.path.isdir( os.path.join(LOCAL_ROOTDIR,'data') ) :
25
26=== modified file 'GTG/backends/localfile.py'
27--- GTG/backends/localfile.py 2009-04-02 22:03:26 +0000
28+++ GTG/backends/localfile.py 2009-07-13 09:20:43 +0000
29@@ -21,7 +21,7 @@
30 #Localfile is a read/write backend that will store your tasks in an XML file
31 #This file will be in your $XDG_DATA_DIR/gtg folder.
32
33-import os,shutil
34+import os
35 import uuid
36
37 from GTG.core import CoreConfig
38
39=== modified file 'GTG/core/__init__.py'
40--- GTG/core/__init__.py 2009-06-18 08:56:33 +0000
41+++ GTG/core/__init__.py 2009-07-13 10:26:28 +0000
42@@ -45,11 +45,10 @@
43
44 #=== IMPORT ====================================================================
45 import os
46-from xdg.BaseDirectory import *
47+from xdg.BaseDirectory import xdg_data_home, xdg_config_home
48 from GTG.tools import cleanxml
49 from configobj import ConfigObj
50
51-import GTG
52 from GTG.core import firstrun_tasks
53
54 class CoreConfig:
55
56=== modified file 'GTG/core/dbuswrapper.py'
57--- GTG/core/dbuswrapper.py 2009-07-09 20:03:12 +0000
58+++ GTG/core/dbuswrapper.py 2009-07-13 09:13:22 +0000
59@@ -1,120 +1,133 @@
60-import gobject, dbus.glib, dbus, dbus.service
61+import dbus
62+import dbus.glib
63+import dbus.service
64+
65
66 BUSNAME = "org.GTG"
67
68 def dsanitize(data):
69- # Clean up a dict so that it can be transmitted through D-Bus
70- for k, v in data.items():
71- # Manually specify an arbitrary content type for empty Python arrays
72- # because D-Bus can't handle the type conversion for empty arrays
73- if not v and isinstance(v, list):
74- data[k] = dbus.Array([], "s")
75- # D-Bus has no concept of a null or empty value so we have to convert None
76- # types to something else. I use an empty string because it has the same
77- # behavior as None in a Python conditional expression
78- elif v == None:
79- data[k] = ""
80-
81- return data
82+ # Clean up a dict so that it can be transmitted through D-Bus
83+ for k, v in data.items():
84+ # Manually specify an arbitrary content type for empty Python arrays
85+ # because D-Bus can't handle the type conversion for empty arrays
86+ if not v and isinstance(v, list):
87+ data[k] = dbus.Array([], "s")
88+ # D-Bus has no concept of a null or empty value so we have to convert
89+ # None types to something else. I use an empty string because it has
90+ # the same behavior as None in a Python conditional expression
91+ elif v == None:
92+ data[k] = ""
93+
94+ return data
95+
96
97 def task_to_dict(task):
98- # Translate a task object into a D-Bus dictionary
99- return dbus.Dictionary(dsanitize({
100- "id": task.get_id(),
101- "status": task.get_status(),
102- "title": task.get_title(),
103- "duedate": task.get_due_date(),
104- "startdate": task.get_start_date(),
105- "donedate": task.get_closed_date(),
106- "tags": task.get_tags_name(),
107- "text": task.get_text(),
108- "subtask": task.get_subtasks_tid(),
109- }), signature="sv")
110+ # Translate a task object into a D-Bus dictionary
111+ return dbus.Dictionary(dsanitize({
112+ "id": task.get_id(),
113+ "status": task.get_status(),
114+ "title": task.get_title(),
115+ "duedate": task.get_due_date(),
116+ "startdate": task.get_start_date(),
117+ "donedate": task.get_closed_date(),
118+ "tags": task.get_tags_name(),
119+ "text": task.get_text(),
120+ "subtask": task.get_subtasks_tid(),
121+ }), signature="sv")
122+
123
124 class DBusTaskWrapper(dbus.service.Object):
125- # D-Bus service object that exposes GTG's task store to third-party apps
126- def __init__(self, req, ui):
127- # Attach the object to D-Bus
128- self.bus = dbus.SessionBus()
129- bus_name = dbus.service.BusName(BUSNAME, bus=self.bus)
130- dbus.service.Object.__init__(self, bus_name, "/org/GTG")
131- self.req = req
132- self.ui = ui
133-
134- @dbus.service.method(BUSNAME)
135- def get_task_ids(self):
136- # Retrieve a list of task ID values
137- return self.req.get_tasks_list(status=["Active", "Done"], started_only=False)
138-
139- @dbus.service.method(BUSNAME)
140- def get_task(self, tid):
141- # Retrieve a specific task by ID and return the data
142- return task_to_dict(self.req.get_task(tid))
143-
144- @dbus.service.method(BUSNAME)
145- def get_tasks(self):
146- # Retrieve a list of task data dicts
147- return [self.get_task(id) for id in self.get_task_ids()]
148-
149- @dbus.service.method(BUSNAME, in_signature="asasbb")
150- def get_task_ids_filtered(self, tags, status, started_only, is_root):
151- # Retrieve a list of task IDs filtered by specified parameters
152- ids = self.req.get_tasks_list(tags, status, False, started_only, is_root)
153- # If there are no matching tasks, return an empty D-Bus array
154- return ids if ids else dbus.Array([], "s")
155-
156- @dbus.service.method(BUSNAME, in_signature="asasbb")
157- def get_tasks_filtered(self, tags, status, started_only, is_root):
158- # Retrieve a list of task data discts filtered by specificed parameters
159- tasks = self.get_task_ids_filtered(tags, status, started_only, is_root)
160- # If no tasks match the filter, return an empty D-Bus array
161- return [self.get_task(id) for id in tasks] if tasks else dbus.Array([], "s")
162-
163- @dbus.service.method(BUSNAME)
164- def has_task(self, tid):
165- return self.req.has_task(tid)
166-
167- @dbus.service.method(BUSNAME)
168- def delete_task(self, tid):
169- self.req.delete_task(tid)
170-
171- @dbus.service.method(BUSNAME, in_signature="sssssassas")
172- def new_task(self, status, title, duedate, startdate, donedate, tags, text, subtasks):
173- # Generate a new task object and return the task data as a dict
174- nt = self.req.new_task()
175- for sub in subtasks: nt.add_subtask(sub)
176- for tag in tags: nt.add_tag(tag)
177- nt.set_status(status, donedate=donedate)
178- nt.set_title(title)
179- nt.set_due_date(duedate)
180- nt.set_start_date(startdate)
181- nt.set_text(text)
182- return task_to_dict(nt)
183-
184- @dbus.service.method(BUSNAME)
185- def modify_task(self, tid, task_data):
186- # Apply supplied task data to the task object with the specified ID
187- task = self.req.get_task(tid)
188- task.set_status(task_data["status"], donedate=task_data["donedate"])
189- task.set_title(task_data["title"])
190- task.set_due_date(task_data["duedate"])
191- task.set_start_date(task_data["startdate"])
192- task.set_text(task_data["text"])
193-
194- for tag in task_data["tags"]: task.add_tag(tag)
195- for sub in task_data["subtask"]: task.add_subtask(sub)
196- return task_to_dict(task)
197-
198- @dbus.service.method(BUSNAME)
199- def open_task_editor(self, tid):
200- self.ui.open_task(tid)
201-
202- @dbus.service.method(BUSNAME)
203- def hide_task_browser(self):
204- self.ui.window.hide()
205-
206- @dbus.service.method(BUSNAME)
207- def show_task_browser(self):
208- self.ui.window.present()
209- self.ui.window.move(self.ui.priv["window_xpos"], self.ui.priv["window_ypos"])
210-
211+
212+ # D-Bus service object that exposes GTG's task store to third-party apps
213+ def __init__(self, req, ui):
214+ # Attach the object to D-Bus
215+ self.bus = dbus.SessionBus()
216+ bus_name = dbus.service.BusName(BUSNAME, bus=self.bus)
217+ dbus.service.Object.__init__(self, bus_name, "/org/GTG")
218+ self.req = req
219+ self.ui = ui
220+
221+ @dbus.service.method(BUSNAME)
222+ def get_task_ids(self):
223+ # Retrieve a list of task ID values
224+ return self.req.get_tasks_list(
225+ status=["Active", "Done"], started_only=False)
226+
227+ @dbus.service.method(BUSNAME)
228+ def get_task(self, tid):
229+ # Retrieve a specific task by ID and return the data
230+ return task_to_dict(self.req.get_task(tid))
231+
232+ @dbus.service.method(BUSNAME)
233+ def get_tasks(self):
234+ # Retrieve a list of task data dicts
235+ return [self.get_task(id) for id in self.get_task_ids()]
236+
237+ @dbus.service.method(BUSNAME, in_signature="asasbb")
238+ def get_task_ids_filtered(self, tags, status, started_only, is_root):
239+ # Retrieve a list of task IDs filtered by specified parameters
240+ ids = self.req.get_tasks_list(
241+ tags, status, False, started_only, is_root)
242+ # If there are no matching tasks, return an empty D-Bus array
243+ return ids if ids else dbus.Array([], "s")
244+
245+ @dbus.service.method(BUSNAME, in_signature="asasbb")
246+ def get_tasks_filtered(self, tags, status, started_only, is_root):
247+ # Retrieve a list of task data discts filtered by specificed parameters
248+ tasks = self.get_task_ids_filtered(
249+ tags, status, started_only, is_root)
250+ # If no tasks match the filter, return an empty D-Bus array
251+ if tasks:
252+ return [self.get_task(id) for id in tasks]
253+ else:
254+ return dbus.Array([], "s")
255+
256+ @dbus.service.method(BUSNAME)
257+ def has_task(self, tid):
258+ return self.req.has_task(tid)
259+
260+ @dbus.service.method(BUSNAME)
261+ def delete_task(self, tid):
262+ self.req.delete_task(tid)
263+
264+ @dbus.service.method(BUSNAME, in_signature="sssssassas")
265+ def new_task(self, status, title, duedate, startdate, donedate, tags,
266+ text, subtasks):
267+ # Generate a new task object and return the task data as a dict
268+ nt = self.req.new_task()
269+ for sub in subtasks: nt.add_subtask(sub)
270+ for tag in tags: nt.add_tag(tag)
271+ nt.set_status(status, donedate=donedate)
272+ nt.set_title(title)
273+ nt.set_due_date(duedate)
274+ nt.set_start_date(startdate)
275+ nt.set_text(text)
276+ return task_to_dict(nt)
277+
278+ @dbus.service.method(BUSNAME)
279+ def modify_task(self, tid, task_data):
280+ # Apply supplied task data to the task object with the specified ID
281+ task = self.req.get_task(tid)
282+ task.set_status(task_data["status"], donedate=task_data["donedate"])
283+ task.set_title(task_data["title"])
284+ task.set_due_date(task_data["duedate"])
285+ task.set_start_date(task_data["startdate"])
286+ task.set_text(task_data["text"])
287+
288+ for tag in task_data["tags"]: task.add_tag(tag)
289+ for sub in task_data["subtask"]: task.add_subtask(sub)
290+ return task_to_dict(task)
291+
292+ @dbus.service.method(BUSNAME)
293+ def open_task_editor(self, tid):
294+ self.ui.open_task(tid)
295+
296+ @dbus.service.method(BUSNAME)
297+ def hide_task_browser(self):
298+ self.ui.window.hide()
299+
300+ @dbus.service.method(BUSNAME)
301+ def show_task_browser(self):
302+ self.ui.window.present()
303+ self.ui.window.move(
304+ self.ui.priv["window_xpos"], self.ui.priv["window_ypos"])
305
306=== modified file 'GTG/core/firstrun_tasks.py'
307--- GTG/core/firstrun_tasks.py 2009-06-18 08:37:25 +0000
308+++ GTG/core/firstrun_tasks.py 2009-07-13 10:22:44 +0000
309@@ -1,3 +1,4 @@
310+from GTG import _
311 from GTG.tools import cleanxml
312
313 def populate():
314
315=== modified file 'GTG/core/requester.py'
316--- GTG/core/requester.py 2009-07-13 08:57:44 +0000
317+++ GTG/core/requester.py 2009-07-13 10:26:28 +0000
318@@ -18,7 +18,7 @@
319 # -----------------------------------------------------------------------------
320
321
322-from GTG.tools.listes import *
323+from GTG.tools.listes import returnlist
324
325 #Requester is a pure View object. It will not do anything but it will
326 #be used by any Interface to handle the requests to the datastore
327
328=== modified file 'GTG/core/task.py'
329--- GTG/core/task.py 2009-07-13 08:57:44 +0000
330+++ GTG/core/task.py 2009-07-13 10:26:28 +0000
331@@ -20,8 +20,10 @@
332 from datetime import date
333 import xml.dom.minidom
334
335-from GTG.tools.dates import *
336-from GTG.tools.listes import *
337+from GTG import _
338+from GTG.tools.dates import strtodate
339+from GTG.tools.listes import returnlist
340+
341
342 #This class represent a task in GTG.
343 #You should never create a Task directly. Use the datastore.new_task() function.
344
345=== modified file 'GTG/gtg.py'
346--- GTG/gtg.py 2009-07-09 08:31:53 +0000
347+++ GTG/gtg.py 2009-07-13 10:22:44 +0000
348@@ -46,6 +46,7 @@
349 import sys, os
350
351 #our own imports
352+from GTG import _
353 from GTG.taskbrowser.browser import TaskBrowser
354 from GTG.core.datastore import DataStore
355 from GTG.core.dbuswrapper import DBusTaskWrapper
356
357=== modified file 'GTG/taskbrowser/__init__.py'
358--- GTG/taskbrowser/__init__.py 2009-04-03 15:00:18 +0000
359+++ GTG/taskbrowser/__init__.py 2009-07-13 10:22:44 +0000
360@@ -22,6 +22,8 @@
361 #simple, HIG compliant and well integrated with Gnome.
362 import os
363
364+from GTG import _
365+
366 class GnomeConfig :
367 current_rep = os.path.dirname(os.path.abspath(__file__))
368 GLADE_FILE = os.path.join(current_rep,"taskbrowser.glade")
369
370=== modified file 'GTG/taskbrowser/browser.py'
371--- GTG/taskbrowser/browser.py 2009-07-13 02:40:17 +0000
372+++ GTG/taskbrowser/browser.py 2009-07-13 10:41:18 +0000
373@@ -1,4 +1,5 @@
374 # -*- coding: utf-8 -*-
375+# pylint: disable-msg=W0201
376 # -----------------------------------------------------------------------------
377 # Gettings Things Gnome! - a personnal organizer for the GNOME desktop
378 # Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
379@@ -33,6 +34,7 @@
380
381 #our own imports
382 import GTG
383+from GTG import _
384 from GTG.taskeditor.editor import TaskEditor
385 from GTG.taskbrowser.CellRendererTags import CellRendererTags
386 from GTG.taskbrowser import GnomeConfig
387@@ -507,15 +509,15 @@
388 }
389 wTree.signal_autoconnect(dic)
390 window = wTree.get_widget("ColorChooser")
391- # Get previous color
392+ # Get previous color
393 tags,notag_only = self.get_selected_tags() #pylint: disable-msg=W0612
394 if len(tags) == 1:
395- color = tags[0].get_attribute("color")
396- if color != None:
397- colorspec = gtk.gdk.Color(color)
398- colorsel = window.colorsel
399- colorsel.set_previous_color(colorspec)
400- colorsel.set_current_color(colorspec)
401+ color = tags[0].get_attribute("color")
402+ if color != None:
403+ colorspec = gtk.gdk.Color(color)
404+ colorsel = window.colorsel
405+ colorsel.set_previous_color(colorspec)
406+ colorsel.set_current_color(colorspec)
407 window.show()
408
409 def on_color_response(self,widget,response) :
410@@ -653,7 +655,7 @@
411 return False
412 year,month,day = splited_date
413 try :
414- date = datetime.date(int(year),int(month),int(day))
415+ datetime.date(int(year),int(month),int(day))
416 except ValueError :
417 return False
418 else :
419
420=== modified file 'GTG/taskeditor/__init__.py'
421--- GTG/taskeditor/__init__.py 2009-06-10 13:17:47 +0000
422+++ GTG/taskeditor/__init__.py 2009-07-13 10:22:44 +0000
423@@ -20,6 +20,8 @@
424
425 import os
426
427+from GTG import _
428+
429 class GnomeConfig :
430 current_rep = os.path.dirname(os.path.abspath(__file__))
431 GLADE_FILE = os.path.join(current_rep,"taskeditor.glade")
432
433=== modified file 'GTG/taskeditor/editor.py'
434--- GTG/taskeditor/editor.py 2009-06-10 13:17:47 +0000
435+++ GTG/taskeditor/editor.py 2009-07-13 10:41:18 +0000
436@@ -26,18 +26,19 @@
437 import time
438 from datetime import date
439
440+from GTG import _
441 from GTG.taskeditor import GnomeConfig
442 from GTG.tools import dates
443 from GTG.taskeditor.taskview import TaskView
444 try:
445 import pygtk
446 pygtk.require("2.0")
447-except:
448+except: # pylint: disable-msg=W0702
449 sys.exit(1)
450 try:
451 import gtk
452 from gtk import gdk
453-except:
454+except: # pylint: disable-msg=W0702
455 sys.exit(1)
456
457 date_separator = "/"
458
459=== modified file 'GTG/taskeditor/taskview.py'
460--- GTG/taskeditor/taskview.py 2009-06-04 19:59:15 +0000
461+++ GTG/taskeditor/taskview.py 2009-07-13 09:20:43 +0000
462@@ -909,11 +909,11 @@
463 for ta in itera.get_toggled_tags(False) :
464 if ta.get_data('is_subtask') :
465 subtask_nbr = ta.get_data('child')
466-
467+
468 #New line : the user pressed enter !
469 #If the line begins with "-", it's a new subtask !
470 if tex == '\n' :
471- insert_point = self.buff.create_mark("insert_point",itera,True)
472+ self.buff.create_mark("insert_point", itera, True)
473 #First, we close tag tags.
474 #If we are at the end of a tag, we look for closed tags
475 closed_tag = None
476@@ -923,7 +923,6 @@
477 #Or maybe we are in the middle of a tag
478 else :
479 list_stag = itera.get_tags()
480- stag = None
481 for t in list_stag :
482 if t.get_data('is_tag') :
483 closed_tag = t.get_data('tagname')
484@@ -984,17 +983,15 @@
485 self.buff.create_mark(anchor,itera,True)
486 self.buff.create_mark("/%s"%anchor,itera,False)
487 self.insert_sigid = self.buff.connect('insert-text', self._insert_at_cursor)
488- self.keypress_sigid = self.connect('key_press_event', self._keypress)
489+ self.connect('key_press_event', self._keypress)
490 self.modified_sigid = self.buff.connect("changed" , self.modified)
491
492 def _keypress(self, widget, event):
493 # Check for Ctrl-Return/Enter
494 if event.state & gtk.gdk.CONTROL_MASK and event.keyval in (gtk.keysyms.Return, gtk.keysyms.KP_Enter):
495- buff = self.buff
496+ buff = self.buff
497 cursor_mark = buff.get_insert()
498 cursor_iter = buff.get_iter_at_mark(cursor_mark)
499- table = buff.get_tag_table()
500-
501 local_start = cursor_iter.copy()
502
503 for tag in local_start.get_tags():
504@@ -1005,7 +1002,7 @@
505 self.open_task(anchor)
506 elif typ == "http" :
507 openurl.openurl(anchor)
508-
509+
510 return True
511
512 #Deindent the current line of one level
513
514=== renamed directory 'tests' => 'GTG/tests'
515=== added file 'GTG/tests/__init__.py'
516--- GTG/tests/__init__.py 1970-01-01 00:00:00 +0000
517+++ GTG/tests/__init__.py 2009-07-14 12:09:55 +0000
518@@ -0,0 +1,30 @@
519+# -*- coding: utf-8 -*-
520+# -----------------------------------------------------------------------------
521+# Gettings Things Gnome! - a personnal organizer for the GNOME desktop
522+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
523+#
524+# This program is free software: you can redistribute it and/or modify it under
525+# the terms of the GNU General Public License as published by the Free Software
526+# Foundation, either version 3 of the License, or (at your option) any later
527+# version.
528+#
529+# This program is distributed in the hope that it will be useful, but WITHOUT
530+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
531+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
532+# details.
533+#
534+# You should have received a copy of the GNU General Public License along with
535+# this program. If not, see <http://www.gnu.org/licenses/>.
536+# -----------------------------------------------------------------------------
537+
538+"""Unit tests for GTG."""
539+
540+import unittest
541+
542+from GTG.tests import test_backends
543+
544+
545+def test_suite():
546+ return unittest.TestSuite([
547+ test_backends.test_suite(),
548+ ])
549
550=== renamed file 'tests/backends.py' => 'GTG/tests/test_backends.py'
551--- tests/backends.py 2009-03-17 18:56:53 +0000
552+++ GTG/tests/test_backends.py 2009-07-14 12:09:55 +0000
553@@ -1,4 +1,3 @@
554-#!/usr/bin/env python
555 # -*- coding: utf-8 -*-
556 # -----------------------------------------------------------------------------
557 # Gettings Things Gnome! - a personnal organizer for the GNOME desktop
558@@ -18,8 +17,9 @@
559 # this program. If not, see <http://www.gnu.org/licenses/>.
560 # -----------------------------------------------------------------------------
561
562-"""Unittests for GTG
563-Some of these tests will generated files in
564+"""Tests for GTG backends.
565+
566+Some of these tests will generate files in
567 xdg.BaseDirectory.xdg_data_home/gtg directory.
568 """
569
570@@ -35,7 +35,7 @@
571
572
573 class GtgBackendsUniTests(unittest.TestCase):
574- """Unittests for GTG backends"""
575+ """Tests for GTG backends."""
576
577 def __init__(self, test):
578 unittest.TestCase.__init__(self, test)
579@@ -43,7 +43,6 @@
580 self.datafile = ''
581 self.taskpath = ''
582 self.datapath = ''
583-
584
585 def test_localfile_get_name(self):
586 """Tests for localfile/get_name function :
587@@ -58,12 +57,12 @@
588 - a string is expected.
589 """
590 res = localfile.get_description()
591- expectedres = "Your tasks are saved in an XML file located in \
592-your HOME folder"
593+ expectedres = ("Your tasks are saved in an XML file located in "
594+ "your HOME folder")
595 self.assertEqual(res, expectedres)
596
597 def test_localfile_get_parameters(self):
598- """Tests for localfile/get_parameters function :
599+ """Tests for localfile/get_parameters function:
600 - a string is expected.
601 """
602 res = localfile.get_parameters()
603@@ -71,7 +70,7 @@
604 self.assertEqual(res['filename'], expectedres)
605
606 def test_localfile_get_features(self):
607- """Tests for localfile/get_features function :
608+ """Tests for localfile/get_features function:
609 - an empty dictionary is expected.
610 """
611 res = localfile.get_features()
612@@ -79,7 +78,7 @@
613 self.assertEqual(res, expectedres)
614
615 def test_localfile_get_type(self):
616- """Tests for localfile/get_type function :
617+ """Tests for localfile/get_type function:
618 - a string is expected.
619 """
620 res = localfile.get_type()
621@@ -87,7 +86,7 @@
622 self.assertEqual(res, expectedres)
623
624 def test_localfile_backend(self):
625- """Tests for localfile/Backend Class :
626+ """Tests for localfile/Backend Class:
627 - an empty list is expected
628 """
629 res = localfile.Backend({})
630@@ -95,7 +94,7 @@
631 self.assertEqual(res.get_tasks_list(), expectedres)
632
633 def test_localfile_backend_method1(self):
634- """Tests for localfile/Backend/new_task_id method :
635+ """Tests for localfile/Backend/new_task_id method:
636 - None value is expected.
637 """
638 res = localfile.Backend({})
639@@ -103,7 +102,7 @@
640 self.assertEqual(res.new_task_id(), expectedres)
641
642 def test_localfile_backend_method2(self):
643- """Tests for localfile/Backend/get_tasks_list method :
644+ """Tests for localfile/Backend/get_tasks_list method:
645 - an integer value is expected.
646 """
647 self.create_test_environment()
648@@ -111,7 +110,7 @@
649 xmlproject = doc.getElementsByTagName('backend')
650 for domobj in xmlproject:
651 dic = {}
652- if domobj.hasAttribute("module") :
653+ if domobj.hasAttribute("module"):
654 dic["module"] = str(domobj.getAttribute("module"))
655 dic["pid"] = str(domobj.getAttribute("pid"))
656 dic["xmlobject"] = domobj
657@@ -121,7 +120,7 @@
658 self.assertEqual(len(res.get_tasks_list()), expectedres)
659
660 def test_localfile_backend_method3(self):
661- """Tests for localfile/Backend/remove_task method :
662+ """Tests for localfile/Backend/remove_task method:
663 - parse task file to check if task has been removed.
664 """
665 self.create_test_environment()
666@@ -129,7 +128,7 @@
667 xmlproject = doc.getElementsByTagName('backend')
668 for domobj in xmlproject:
669 dic = {}
670- if domobj.hasAttribute("module") :
671+ if domobj.hasAttribute("module"):
672 dic["module"] = str(domobj.getAttribute("module"))
673 dic["pid"] = str(domobj.getAttribute("pid"))
674 dic["xmlobject"] = domobj
675@@ -146,7 +145,7 @@
676 self.assertEqual(res, expectedres)
677
678 def test_localfile_backend_method4(self):
679- """Tests for localfile/Backend/get_task method :
680+ """Tests for localfile/Backend/get_task method:
681 - Compares task titles to check if method works.
682 """
683 self.create_test_environment()
684@@ -154,7 +153,7 @@
685 xmlproject = doc.getElementsByTagName('backend')
686 for domobj in xmlproject:
687 dic = {}
688- if domobj.hasAttribute("module") :
689+ if domobj.hasAttribute("module"):
690 dic["module"] = str(domobj.getAttribute("module"))
691 dic["pid"] = str(domobj.getAttribute("pid"))
692 dic["xmlobject"] = domobj
693@@ -166,7 +165,7 @@
694 self.assertEqual(newtask.get_title(), u"Ceci est un test")
695
696 def test_localfile_backend_method5(self):
697- """Tests for localfile/Backend/set_task method :
698+ """Tests for localfile/Backend/set_task method:
699 - parses task file to check if new task has been stored.
700 """
701 self.create_test_environment()
702@@ -174,7 +173,7 @@
703 xmlproject = doc.getElementsByTagName('backend')
704 for domobj in xmlproject:
705 dic = {}
706- if domobj.hasAttribute("module") :
707+ if domobj.hasAttribute("module"):
708 dic["module"] = str(domobj.getAttribute("module"))
709 dic["pid"] = str(domobj.getAttribute("pid"))
710 dic["xmlobject"] = domobj
711@@ -195,13 +194,22 @@
712 """Create the test environment"""
713 self.taskfile = 'test.xml'
714 self.datafile = 'projectstest.xml'
715- tasks = ['<?xml version="1.0" ?>\n', '<project>\n',
716- '\t<task id="0@1" status="Active" tags="">\n', '\t\t<title>\n',
717- '\t\t\tCeci est un test\n', '\t\t</title>\n', '\t</task>\n',
718- '</project>\n']
719- data = ['<?xml version="1.0" ?>\n', '<config>\n',
720- '\t<backend filename="test.xml" module="localfile" pid="1"/>\n',
721- '</config>\n']
722+ tasks = [
723+ '<?xml version="1.0" ?>\n',
724+ '<project>\n',
725+ '\t<task id="0@1" status="Active" tags="">\n',
726+ '\t\t<title>\n',
727+ '\t\t\tCeci est un test\n',
728+ '\t\t</title>\n',
729+ '\t</task>\n',
730+ '</project>\n',
731+ ]
732+ data = [
733+ '<?xml version="1.0" ?>\n',
734+ '<config>\n',
735+ '\t<backend filename="test.xml" module="localfile" pid="1"/>\n',
736+ '</config>\n',
737+ ]
738 testdir = os.path.join(xdg.BaseDirectory.xdg_data_home, 'gtg')
739 if not os.path.exists(testdir):
740 os.makedirs(testdir)
741@@ -210,6 +218,6 @@
742 open(self.taskpath, 'w').writelines(tasks)
743 open(self.datapath, 'w').writelines(data)
744
745-if __name__ == '__main__':
746- unittest.main()
747
748+def test_suite():
749+ return unittest.TestLoader().loadTestsFromName(__name__)
750
751=== added file 'HACKING'
752--- HACKING 1970-01-01 00:00:00 +0000
753+++ HACKING 2009-07-14 12:13:11 +0000
754@@ -0,0 +1,124 @@
755+=================
756+GTG Hacking Guide
757+=================
758+
759+Testing
760+-------
761+
762+You can run the unit tests for GTG with::
763+
764+ make check
765+
766+If you are so inclined and have the right software installed, you can also run
767+the tests with ``trial GTG``.
768+
769+You can also manually test your changes with debugging data with::
770+
771+ ./scripts/debug.sh
772+
773+Using ``debug.sh`` will prevent GTG from messing with your real data. Instead,
774+the debug GTG will store data in ``debug_data/``.
775+
776+Unit tests live in ``GTG/tests/``, and are all named ``test_foo``. When you
777+add a new test module, make sure it has a ``test_suite()`` method that returns
778+the suite of all the tests in that module. Also make sure that the new module
779+is imported in ``GTG.tests`` and returned from the ``test_suite()`` function
780+there.
781+
782+For example, GTG/tests/test_newthing.py::
783+
784+ import unittest
785+
786+ class TestNewThing(unittest.TestCase):
787+ # ...
788+
789+ def test_suite():
790+ return unittest.TestLoader().loadTestsFromName(__name__)
791+
792+
793+And GTG/tests/__init__.py::
794+
795+ import unittest
796+
797+ from GTG.tests import test_backends, test_newthing
798+
799+ def test_suite():
800+ return unittest.TestSuite([
801+ test_backends.test_suite(),
802+ test_newthing.test_suite(),
803+ ])
804+
805+When in doubt, copy from an existing test module!
806+
807+
808+Coding style
809+------------
810+
811+In general, follow PEP 8 <http://www.python.org/dev/peps/pep-0008/>.
812+
813+Not all code in GTG currently follows PEP 8. If you are changing a section of
814+code, please update it to follow PEP 8.
815+
816+You should also avoid adding any 'flakes', simple Python mistakes caught by
817+Pyflakes <http://www.divmod.org/trac/wiki/DivmodPyflakes>.
818+
819+To check the cleanliness of your code, run::
820+
821+ make lint
822+
823+The ``make`` will fail if Pyflakes emits any warnings. You can the Pyflakes
824+checker separately using ``make pyflakes`` and the PEP 8 checker using ``make
825+pep8``. If you wish to find all PEP 8 violations in a particular file, use::
826+
827+ ./scripts/pep8.py --repeat FILENAME
828+
829+
830+Copyright
831+---------
832+
833+All modules should begin with the following header::
834+
835+# -*- coding: utf-8 -*-
836+# -----------------------------------------------------------------------------
837+# Gettings Things Gnome! - a personnal organizer for the GNOME desktop
838+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
839+#
840+# This program is free software: you can redistribute it and/or modify it under
841+# the terms of the GNU General Public License as published by the Free Software
842+# Foundation, either version 3 of the License, or (at your option) any later
843+# version.
844+#
845+# This program is distributed in the hope that it will be useful, but WITHOUT
846+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
847+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
848+# details.
849+#
850+# You should have received a copy of the GNU General Public License along with
851+# this program. If not, see <http://www.gnu.org/licenses/>.
852+# -----------------------------------------------------------------------------
853+
854+
855+Submitting Patches
856+------------------
857+
858+For information about contributing code to GTG, see
859+<http://live.gnome.org/gtg/contributing>.
860+
861+
862+Landing Branches
863+----------------
864+
865+ 1. Get the branch.
866+
867+ 2. Run the tests, ``make check``.
868+
869+ 3. Run ``make lint``, check that the number of PEP 8 warnings is lower than
870+ trunk and that there are no new pyflakes warnings.
871+
872+ 4. Launch GTG with debugging data, just in case, ``./scripts/debug.sh``.
873+
874+ 5. Update ``CHANGELOG`` if it isn't already.
875+
876+ 6. Update ``AUTHORS`` if the patch author is not already in there.
877+
878+ 7. Merge the branch into trunk, then commit!
879
880=== added file 'Makefile'
881--- Makefile 1970-01-01 00:00:00 +0000
882+++ Makefile 2009-07-14 12:15:18 +0000
883@@ -0,0 +1,25 @@
884+
885+# Run all of the tests.
886+check:
887+ ./run-tests
888+
889+# Get rid of stale files or files made during testing.
890+clean:
891+ rm -rf _trial_temp
892+ rm -rf debug_data
893+ find . -name '*.pyc' -print0 | xargs -0 rm -f
894+ find . -name '*~' -print0 | xargs -0 rm -f
895+
896+# Check for common & easily catchable Python mistakes.
897+pyflakes:
898+ pyflakes GTG
899+
900+# Check for coding standard violations.
901+pep8:
902+ find . -name '*.py' -print0 | xargs -0 ./scripts/pep8.py
903+ find . -name '*.py' -print0 | xargs -0 ./scripts/pep8.py --repeat | wc -l
904+
905+# Check for coding standard violations & flakes.
906+lint: pyflakes pep8
907+
908+.PHONY: check lint pyflakes pep8
909
910=== added file 'run-tests'
911--- run-tests 1970-01-01 00:00:00 +0000
912+++ run-tests 2009-07-14 11:36:28 +0000
913@@ -0,0 +1,41 @@
914+#!/usr/bin/env python
915+# -*- coding: utf-8 -*-
916+# -----------------------------------------------------------------------------
917+# Gettings Things Gnome! - a personnal organizer for the GNOME desktop
918+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
919+#
920+# This program is free software: you can redistribute it and/or modify it under
921+# the terms of the GNU General Public License as published by the Free Software
922+# Foundation, either version 3 of the License, or (at your option) any later
923+# version.
924+#
925+# This program is distributed in the hope that it will be useful, but WITHOUT
926+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
927+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
928+# details.
929+#
930+# You should have received a copy of the GNU General Public License along with
931+# this program. If not, see <http://www.gnu.org/licenses/>.
932+# -----------------------------------------------------------------------------
933+
934+"""Runs the GTG unit tests."""
935+
936+import sys
937+import unittest
938+
939+from GTG.tests import test_suite
940+
941+
942+def main(args):
943+ runner = unittest.TextTestRunner(
944+ stream=sys.stdout, descriptions=False, verbosity=1)
945+ test = test_suite()
946+ result = runner.run(test)
947+ if result.wasSuccessful():
948+ return 0
949+ else:
950+ return 1
951+
952+
953+if __name__ == '__main__':
954+ sys.exit(main(sys.argv[1:]))
955
956=== removed file 'scripts/codecheck.sh'
957--- scripts/codecheck.sh 2009-03-31 11:04:14 +0000
958+++ scripts/codecheck.sh 1970-01-01 00:00:00 +0000
959@@ -1,63 +0,0 @@
960-#!/bin/bash
961-#You have to install pychecker, pyflakes and pylint
962-#
963-#This script will only check for important error
964-#To a more in-depth check, launch "pylint folder_name" on any folder
965-#
966-#It will give you a list of error(E), warning(W),coding style rules(C)
967-#And programing rules (R).
968-#At the end, you will have statistics with the ID of each error.
969-#Use,for example, pylint --help-msg=W0613 to get information about
970-#This error
971-#If this is a false positive, in the code add the following comment
972-#at the error line : #pylint: disable-msg=W0613
973-#
974-#
975-#We do not check the following.
976-#We might do in the future
977-#C0324 : space after a comma (this one is buggy)
978-#C0103 : variable name should match a given regexp
979-#C0301 : lines too long (maybe we should care)
980-#C0111 : Missing docstring (we should care but later)
981-#R0914 : too many variable (why should we care ?)
982-#R0903 : class had too few methods
983-#R0915 : function has too many statements
984-#R0904 : too many public methods
985-#R0912 : function has too many branches
986-#R0201 : method could be a function
987-#R0913 : too many arguments
988-#C0323 : operator not followed by a space
989-#R0902 : too many attributes in the class
990-#W0102 : [] or {} as argument (don't understand why it's bad)
991-#W0232 : no __init__() (if I don't write it, I don't need it)
992-#W0105 : string statement has no effect(=documentation)
993-#C0321 : more than one statement on the same line (is that bad ?)
994-#W0401 : * wildcard import (yes, we use them)
995-#W0142 : use of * and ** arguments (yes, I find that handy)
996-#I0011 : Locally disabling. We don't care if we disabled locally
997-
998-#pylint argument :
999-disabled="C0324,C0103,C0301,C0111,R0914,R0903,R0915,R0904,R0912,R0201,R0913,C0323,R0902,W0102,W0232,W0105,C0321,W0401,W0142,I0011"
1000-args="--rcfile=/dev/null --include-ids=y --reports=n"
1001-#grepped_out="Locally disabling"
1002-#pylint doesn't recognize gettext _()
1003-grepped_out="Undefined variable '_'"
1004-
1005-echo "Running pychecker"
1006-echo "#################"
1007-pychecker -9 -T -8 -# 200 --changetypes GTG/gtg.py
1008-
1009-echo "Running pyflakes"
1010-echo "#################"
1011-#pyflakes triggers a false positive with import * and with gettext _()
1012-pyflakes GTG|grep -v "import \*"|grep -v "undefined name '_'"
1013-
1014-echo "Running pylint"
1015-echo "#################"
1016-pylint --rcfile=/dev/null --include-ids=y --reports=n --disable-msg=$disabled GTG/gtg.py|grep -v "$grepped_out"
1017-for i in GTG/*; do
1018- if test -d $i && [ $i != "data" ]; then
1019- #echo $i
1020- pylint --rcfile=/dev/null --include-ids=y --reports=n --disable-msg=$disabled $i |grep -v "$grepped_out"
1021- fi
1022-done
1023
1024=== renamed file 'tests/debug.sh' => 'scripts/debug.sh'
1025--- tests/debug.sh 2009-03-01 13:42:07 +0000
1026+++ scripts/debug.sh 2009-07-14 11:40:41 +0000
1027@@ -1,4 +1,4 @@
1028 #!/bin/bash
1029-export XDG_DATA_HOME="./tests/xdg/data"
1030-export XDG_CONFIG_HOME="./tests/xdg/config"
1031+export XDG_DATA_HOME="./debug_data/xdg/data"
1032+export XDG_CONFIG_HOME="./debug_data/xdg/config"
1033 ./gtg
1034
1035=== added file 'scripts/pep8.py'
1036--- scripts/pep8.py 1970-01-01 00:00:00 +0000
1037+++ scripts/pep8.py 2009-07-14 11:05:54 +0000
1038@@ -0,0 +1,863 @@
1039+#!/usr/bin/python
1040+# pep8.py - Check Python source code formatting, according to PEP 8
1041+# Copyright (C) 2006 Johann C. Rocholl <johann@browsershots.org>
1042+#
1043+# Permission is hereby granted, free of charge, to any person
1044+# obtaining a copy of this software and associated documentation files
1045+# (the "Software"), to deal in the Software without restriction,
1046+# including without limitation the rights to use, copy, modify, merge,
1047+# publish, distribute, sublicense, and/or sell copies of the Software,
1048+# and to permit persons to whom the Software is furnished to do so,
1049+# subject to the following conditions:
1050+#
1051+# The above copyright notice and this permission notice shall be
1052+# included in all copies or substantial portions of the Software.
1053+#
1054+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
1055+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
1056+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
1057+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
1058+# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
1059+# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
1060+# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1061+# SOFTWARE.
1062+
1063+"""
1064+Check Python source code formatting, according to PEP 8:
1065+http://www.python.org/dev/peps/pep-0008/
1066+
1067+For usage and a list of options, try this:
1068+$ python pep8.py -h
1069+
1070+This program and its regression test suite live here:
1071+http://svn.browsershots.org/trunk/devtools/pep8/
1072+http://trac.browsershots.org/browser/trunk/devtools/pep8/
1073+
1074+Groups of errors and warnings:
1075+E errors
1076+W warnings
1077+100 indentation
1078+200 whitespace
1079+300 blank lines
1080+400 imports
1081+500 line length
1082+600 deprecation
1083+700 statements
1084+
1085+You can add checks to this program by writing plugins. Each plugin is
1086+a simple function that is called for each line of source code, either
1087+physical or logical.
1088+
1089+Physical line:
1090+- Raw line of text from the input file.
1091+
1092+Logical line:
1093+- Multi-line statements converted to a single line.
1094+- Stripped left and right.
1095+- Contents of strings replaced with 'xxx' of same length.
1096+- Comments removed.
1097+
1098+The check function requests physical or logical lines by the name of
1099+the first argument:
1100+
1101+def maximum_line_length(physical_line)
1102+def extraneous_whitespace(logical_line)
1103+def blank_lines(logical_line, blank_lines, indent_level, line_number)
1104+
1105+The last example above demonstrates how check plugins can request
1106+additional information with extra arguments. All attributes of the
1107+Checker object are available. Some examples:
1108+
1109+lines: a list of the raw lines from the input file
1110+tokens: the tokens that contribute to this logical line
1111+line_number: line number in the input file
1112+blank_lines: blank lines before this one
1113+indent_char: first indentation character in this file (' ' or '\t')
1114+indent_level: indentation (with tabs expanded to multiples of 8)
1115+previous_indent_level: indentation on previous line
1116+previous_logical: previous logical line
1117+
1118+The docstring of each check function shall be the relevant part of
1119+text from PEP 8. It is printed if the user enables --show-pep8.
1120+
1121+"""
1122+
1123+import os
1124+import sys
1125+import re
1126+import time
1127+import inspect
1128+import tokenize
1129+from optparse import OptionParser
1130+from keyword import iskeyword
1131+from fnmatch import fnmatch
1132+
1133+__version__ = '0.2.0'
1134+__revision__ = '$Rev: 2208 $'
1135+
1136+default_exclude = '.svn,CVS,*.pyc,*.pyo'
1137+
1138+indent_match = re.compile(r'([ \t]*)').match
1139+raise_comma_match = re.compile(r'raise\s+\w+\s*(,)').match
1140+
1141+operators = """
1142++ - * / % ^ & | = < > >> <<
1143++= -= *= /= %= ^= &= |= == <= >= >>= <<=
1144+!= <> :
1145+in is or not and
1146+""".split()
1147+
1148+options = None
1149+args = None
1150+
1151+
1152+##############################################################################
1153+# Plugins (check functions) for physical lines
1154+##############################################################################
1155+
1156+
1157+def tabs_or_spaces(physical_line, indent_char):
1158+ """
1159+ Never mix tabs and spaces.
1160+
1161+ The most popular way of indenting Python is with spaces only. The
1162+ second-most popular way is with tabs only. Code indented with a mixture
1163+ of tabs and spaces should be converted to using spaces exclusively. When
1164+ invoking the Python command line interpreter with the -t option, it issues
1165+ warnings about code that illegally mixes tabs and spaces. When using -tt
1166+ these warnings become errors. These options are highly recommended!
1167+ """
1168+ indent = indent_match(physical_line).group(1)
1169+ for offset, char in enumerate(indent):
1170+ if char != indent_char:
1171+ return offset, "E101 indentation contains mixed spaces and tabs"
1172+
1173+
1174+def tabs_obsolete(physical_line):
1175+ """
1176+ For new projects, spaces-only are strongly recommended over tabs. Most
1177+ editors have features that make this easy to do.
1178+ """
1179+ indent = indent_match(physical_line).group(1)
1180+ if indent.count('\t'):
1181+ return indent.index('\t'), "W191 indentation contains tabs"
1182+
1183+
1184+def trailing_whitespace(physical_line):
1185+ """
1186+ JCR: Trailing whitespace is superfluous.
1187+ """
1188+ physical_line = physical_line.rstrip('\n') # chr(10), newline
1189+ physical_line = physical_line.rstrip('\r') # chr(13), carriage return
1190+ physical_line = physical_line.rstrip('\x0c') # chr(12), form feed, ^L
1191+ stripped = physical_line.rstrip()
1192+ if physical_line != stripped:
1193+ return len(stripped), "W291 trailing whitespace"
1194+
1195+
1196+def trailing_blank_lines(physical_line, lines, line_number):
1197+ """
1198+ JCR: Trailing blank lines are superfluous.
1199+ """
1200+ if physical_line.strip() == '' and line_number == len(lines):
1201+ return 0, "W391 blank line at end of file"
1202+
1203+
1204+def missing_newline(physical_line):
1205+ """
1206+ JCR: The last line should have a newline.
1207+ """
1208+ if physical_line.rstrip() == physical_line:
1209+ return len(physical_line), "W292 no newline at end of file"
1210+
1211+
1212+def maximum_line_length(physical_line):
1213+ """
1214+ Limit all lines to a maximum of 79 characters.
1215+
1216+ There are still many devices around that are limited to 80 character
1217+ lines; plus, limiting windows to 80 characters makes it possible to have
1218+ several windows side-by-side. The default wrapping on such devices looks
1219+ ugly. Therefore, please limit all lines to a maximum of 79 characters.
1220+ For flowing long blocks of text (docstrings or comments), limiting the
1221+ length to 72 characters is recommended.
1222+ """
1223+ length = len(physical_line.rstrip())
1224+ if length > 79:
1225+ return 79, "E501 line too long (%d characters)" % length
1226+
1227+
1228+##############################################################################
1229+# Plugins (check functions) for logical lines
1230+##############################################################################
1231+
1232+
1233+def blank_lines(logical_line, blank_lines, indent_level, line_number,
1234+ previous_logical):
1235+ """
1236+ Separate top-level function and class definitions with two blank lines.
1237+
1238+ Method definitions inside a class are separated by a single blank line.
1239+
1240+ Extra blank lines may be used (sparingly) to separate groups of related
1241+ functions. Blank lines may be omitted between a bunch of related
1242+ one-liners (e.g. a set of dummy implementations).
1243+
1244+ Use blank lines in functions, sparingly, to indicate logical sections.
1245+ """
1246+ if line_number == 1:
1247+ return # Don't expect blank lines before the first line
1248+ if previous_logical.startswith('@'):
1249+ return # Don't expect blank lines after function decorator
1250+ if (logical_line.startswith('def ') or
1251+ logical_line.startswith('class ') or
1252+ logical_line.startswith('@')):
1253+ if indent_level > 0 and blank_lines != 1:
1254+ return 0, "E301 expected 1 blank line, found %d" % blank_lines
1255+ if indent_level == 0 and blank_lines != 2:
1256+ return 0, "E302 expected 2 blank lines, found %d" % blank_lines
1257+ if blank_lines > 2:
1258+ return 0, "E303 too many blank lines (%d)" % blank_lines
1259+
1260+
1261+def extraneous_whitespace(logical_line):
1262+ """
1263+ Avoid extraneous whitespace in the following situations:
1264+
1265+ - Immediately inside parentheses, brackets or braces.
1266+
1267+ - Immediately before a comma, semicolon, or colon.
1268+ """
1269+ line = logical_line
1270+ for char in '([{':
1271+ found = line.find(char + ' ')
1272+ if found > -1:
1273+ return found + 1, "E201 whitespace after '%s'" % char
1274+ for char in '}])':
1275+ found = line.find(' ' + char)
1276+ if found > -1 and line[found - 1] != ',':
1277+ return found, "E202 whitespace before '%s'" % char
1278+ for char in ',;:':
1279+ found = line.find(' ' + char)
1280+ if found > -1:
1281+ return found, "E203 whitespace before '%s'" % char
1282+
1283+
1284+def missing_whitespace(logical_line):
1285+ """
1286+ JCR: Each comma, semicolon or colon should be followed by whitespace.
1287+ """
1288+ line = logical_line
1289+ for index in range(len(line) - 1):
1290+ char = line[index]
1291+ if char in ',;:' and line[index + 1] != ' ':
1292+ before = line[:index]
1293+ if char == ':' and before.count('[') > before.count(']'):
1294+ continue # Slice syntax, no space required
1295+ return index, "E231 missing whitespace after '%s'" % char
1296+
1297+
1298+def indentation(logical_line, previous_logical, indent_char,
1299+ indent_level, previous_indent_level):
1300+ """
1301+ Use 4 spaces per indentation level.
1302+
1303+ For really old code that you don't want to mess up, you can continue to
1304+ use 8-space tabs.
1305+ """
1306+ if indent_char == ' ' and indent_level % 4:
1307+ return 0, "E111 indentation is not a multiple of four"
1308+ indent_expect = previous_logical.endswith(':')
1309+ if indent_expect and indent_level <= previous_indent_level:
1310+ return 0, "E112 expected an indented block"
1311+ if indent_level > previous_indent_level and not indent_expect:
1312+ return 0, "E113 unexpected indentation"
1313+
1314+
1315+def whitespace_before_parameters(logical_line, tokens):
1316+ """
1317+ Avoid extraneous whitespace in the following situations:
1318+
1319+ - Immediately before the open parenthesis that starts the argument
1320+ list of a function call.
1321+
1322+ - Immediately before the open parenthesis that starts an indexing or
1323+ slicing.
1324+ """
1325+ prev_type = tokens[0][0]
1326+ prev_text = tokens[0][1]
1327+ prev_end = tokens[0][3]
1328+ for index in range(1, len(tokens)):
1329+ token_type, text, start, end, line = tokens[index]
1330+ if (token_type == tokenize.OP and
1331+ text in '([' and
1332+ start != prev_end and
1333+ prev_type == tokenize.NAME and
1334+ (index < 2 or tokens[index - 2][1] != 'class') and
1335+ (not iskeyword(prev_text))):
1336+ return prev_end, "E211 whitespace before '%s'" % text
1337+ prev_type = token_type
1338+ prev_text = text
1339+ prev_end = end
1340+
1341+
1342+def whitespace_around_operator(logical_line):
1343+ """
1344+ Avoid extraneous whitespace in the following situations:
1345+
1346+ - More than one space around an assignment (or other) operator to
1347+ align it with another.
1348+ """
1349+ line = logical_line
1350+ for operator in operators:
1351+ found = line.find(' ' + operator)
1352+ if found > -1:
1353+ return found, "E221 multiple spaces before operator"
1354+ found = line.find(operator + ' ')
1355+ if found > -1:
1356+ return found, "E222 multiple spaces after operator"
1357+ found = line.find('\t' + operator)
1358+ if found > -1:
1359+ return found, "E223 tab before operator"
1360+ found = line.find(operator + '\t')
1361+ if found > -1:
1362+ return found, "E224 tab after operator"
1363+
1364+
1365+def whitespace_around_comma(logical_line):
1366+ """
1367+ Avoid extraneous whitespace in the following situations:
1368+
1369+ - More than one space around an assignment (or other) operator to
1370+ align it with another.
1371+
1372+ JCR: This should also be applied around comma etc.
1373+ """
1374+ line = logical_line
1375+ for separator in ',;:':
1376+ found = line.find(separator + ' ')
1377+ if found > -1:
1378+ return found + 1, "E241 multiple spaces after '%s'" % separator
1379+ found = line.find(separator + '\t')
1380+ if found > -1:
1381+ return found + 1, "E242 tab after '%s'" % separator
1382+
1383+
1384+def imports_on_separate_lines(logical_line):
1385+ """
1386+ Imports should usually be on separate lines.
1387+ """
1388+ line = logical_line
1389+ if line.startswith('import '):
1390+ found = line.find(',')
1391+ if found > -1:
1392+ return found, "E401 multiple imports on one line"
1393+
1394+
1395+def compound_statements(logical_line):
1396+ """
1397+ Compound statements (multiple statements on the same line) are
1398+ generally discouraged.
1399+ """
1400+ line = logical_line
1401+ found = line.find(':')
1402+ if -1 < found < len(line) - 1:
1403+ before = line[:found]
1404+ if (before.count('{') <= before.count('}') and # {'a': 1} (dict)
1405+ before.count('[') <= before.count(']') and # [1:2] (slice)
1406+ not re.search(r'\blambda\b', before)): # lambda x: x
1407+ return found, "E701 multiple statements on one line (colon)"
1408+ found = line.find(';')
1409+ if -1 < found:
1410+ return found, "E702 multiple statements on one line (semicolon)"
1411+
1412+
1413+def python_3000_has_key(logical_line):
1414+ """
1415+ The {}.has_key() method will be removed in the future version of
1416+ Python. Use the 'in' operation instead, like:
1417+ d = {"a": 1, "b": 2}
1418+ if "b" in d:
1419+ print d["b"]
1420+ """
1421+ pos = logical_line.find('.has_key(')
1422+ if pos > -1:
1423+ return pos, "W601 .has_key() is deprecated, use 'in'"
1424+
1425+
1426+def python_3000_raise_comma(logical_line):
1427+ """
1428+ When raising an exception, use "raise ValueError('message')"
1429+ instead of the older form "raise ValueError, 'message'".
1430+
1431+ The paren-using form is preferred because when the exception arguments
1432+ are long or include string formatting, you don't need to use line
1433+ continuation characters thanks to the containing parentheses. The older
1434+ form will be removed in Python 3000.
1435+ """
1436+ match = raise_comma_match(logical_line)
1437+ if match:
1438+ return match.start(1), "W602 deprecated form of raising exception"
1439+
1440+
1441+##############################################################################
1442+# Helper functions
1443+##############################################################################
1444+
1445+
1446+def expand_indent(line):
1447+ """
1448+ Return the amount of indentation.
1449+ Tabs are expanded to the next multiple of 8.
1450+
1451+ >>> expand_indent(' ')
1452+ 4
1453+ >>> expand_indent('\\t')
1454+ 8
1455+ >>> expand_indent(' \\t')
1456+ 8
1457+ >>> expand_indent(' \\t')
1458+ 8
1459+ >>> expand_indent(' \\t')
1460+ 16
1461+ """
1462+ result = 0
1463+ for char in line:
1464+ if char == '\t':
1465+ result = result / 8 * 8 + 8
1466+ elif char == ' ':
1467+ result += 1
1468+ else:
1469+ break
1470+ return result
1471+
1472+
1473+##############################################################################
1474+# Framework to run all checks
1475+##############################################################################
1476+
1477+
1478+def message(text):
1479+ """Print a message."""
1480+ # print >> sys.stderr, options.prog + ': ' + text
1481+ # print >> sys.stderr, text
1482+ print text
1483+
1484+
1485+def find_checks(argument_name):
1486+ """
1487+ Find all globally visible functions where the first argument name
1488+ starts with argument_name.
1489+ """
1490+ checks = []
1491+ function_type = type(find_checks)
1492+ for name, function in globals().iteritems():
1493+ if type(function) is function_type:
1494+ args = inspect.getargspec(function)[0]
1495+ if len(args) >= 1 and args[0].startswith(argument_name):
1496+ checks.append((name, function, args))
1497+ checks.sort()
1498+ return checks
1499+
1500+
1501+def mute_string(text):
1502+ """
1503+ Replace contents with 'xxx' to prevent syntax matching.
1504+
1505+ >>> mute_string('"abc"')
1506+ '"xxx"'
1507+ >>> mute_string("'''abc'''")
1508+ "'''xxx'''"
1509+ >>> mute_string("r'abc'")
1510+ "r'xxx'"
1511+ """
1512+ start = 1
1513+ end = len(text) - 1
1514+ # String modifiers (e.g. u or r)
1515+ if text.endswith('"'):
1516+ start += text.index('"')
1517+ elif text.endswith("'"):
1518+ start += text.index("'")
1519+ # Triple quotes
1520+ if text.endswith('"""') or text.endswith("'''"):
1521+ start += 2
1522+ end -= 2
1523+ return text[:start] + 'x' * (end - start) + text[end:]
1524+
1525+
1526+class Checker:
1527+ """
1528+ Load a Python source file, tokenize it, check coding style.
1529+ """
1530+
1531+ def __init__(self, filename):
1532+ self.filename = filename
1533+ self.lines = file(filename).readlines()
1534+ self.physical_checks = find_checks('physical_line')
1535+ self.logical_checks = find_checks('logical_line')
1536+ options.counters['physical lines'] = \
1537+ options.counters.get('physical lines', 0) + len(self.lines)
1538+
1539+ def readline(self):
1540+ """
1541+ Get the next line from the input buffer.
1542+ """
1543+ self.line_number += 1
1544+ if self.line_number > len(self.lines):
1545+ return ''
1546+ return self.lines[self.line_number - 1]
1547+
1548+ def readline_check_physical(self):
1549+ """
1550+ Check and return the next physical line. This method can be
1551+ used to feed tokenize.generate_tokens.
1552+ """
1553+ line = self.readline()
1554+ if line:
1555+ self.check_physical(line)
1556+ return line
1557+
1558+ def run_check(self, check, argument_names):
1559+ """
1560+ Run a check plugin.
1561+ """
1562+ arguments = []
1563+ for name in argument_names:
1564+ arguments.append(getattr(self, name))
1565+ return check(*arguments)
1566+
1567+ def check_physical(self, line):
1568+ """
1569+ Run all physical checks on a raw input line.
1570+ """
1571+ self.physical_line = line
1572+ if self.indent_char is None and len(line) and line[0] in ' \t':
1573+ self.indent_char = line[0]
1574+ for name, check, argument_names in self.physical_checks:
1575+ result = self.run_check(check, argument_names)
1576+ if result is not None:
1577+ offset, text = result
1578+ self.report_error(self.line_number, offset, text, check)
1579+
1580+ def build_tokens_line(self):
1581+ """
1582+ Build a logical line from tokens.
1583+ """
1584+ self.mapping = []
1585+ logical = []
1586+ length = 0
1587+ previous = None
1588+ for token in self.tokens:
1589+ token_type, text = token[0:2]
1590+ if token_type in (tokenize.COMMENT, tokenize.NL,
1591+ tokenize.INDENT, tokenize.DEDENT,
1592+ tokenize.NEWLINE):
1593+ continue
1594+ if token_type == tokenize.STRING:
1595+ text = mute_string(text)
1596+ if previous:
1597+ end_line, end = previous[3]
1598+ start_line, start = token[2]
1599+ if end_line != start_line: # different row
1600+ if self.lines[end_line - 1][end - 1] not in '{[(':
1601+ logical.append(' ')
1602+ length += 1
1603+ elif end != start: # different column
1604+ fill = self.lines[end_line - 1][end:start]
1605+ logical.append(fill)
1606+ length += len(fill)
1607+ self.mapping.append((length, token))
1608+ logical.append(text)
1609+ length += len(text)
1610+ previous = token
1611+ self.logical_line = ''.join(logical)
1612+ assert self.logical_line.lstrip() == self.logical_line
1613+ assert self.logical_line.rstrip() == self.logical_line
1614+
1615+ def check_logical(self):
1616+ """
1617+ Build a line from tokens and run all logical checks on it.
1618+ """
1619+ options.counters['logical lines'] = \
1620+ options.counters.get('logical lines', 0) + 1
1621+ self.build_tokens_line()
1622+ first_line = self.lines[self.mapping[0][1][2][0] - 1]
1623+ indent = first_line[:self.mapping[0][1][2][1]]
1624+ self.previous_indent_level = self.indent_level
1625+ self.indent_level = expand_indent(indent)
1626+ if options.verbose >= 2:
1627+ print self.logical_line[:80].rstrip()
1628+ for name, check, argument_names in self.logical_checks:
1629+ if options.verbose >= 3:
1630+ print ' ', name
1631+ result = self.run_check(check, argument_names)
1632+ if result is not None:
1633+ offset, text = result
1634+ if type(offset) is tuple:
1635+ original_number, original_offset = offset
1636+ else:
1637+ for token_offset, token in self.mapping:
1638+ if offset >= token_offset:
1639+ original_number = token[2][0]
1640+ original_offset = (token[2][1]
1641+ + offset - token_offset)
1642+ self.report_error(original_number, original_offset,
1643+ text, check)
1644+ self.previous_logical = self.logical_line
1645+
1646+ def check_all(self):
1647+ """
1648+ Run all checks on the input file.
1649+ """
1650+ self.file_errors = 0
1651+ self.line_number = 0
1652+ self.indent_char = None
1653+ self.indent_level = 0
1654+ self.previous_logical = ''
1655+ self.blank_lines = 0
1656+ self.tokens = []
1657+ parens = 0
1658+ for token in tokenize.generate_tokens(self.readline_check_physical):
1659+ # print tokenize.tok_name[token[0]], repr(token)
1660+ self.tokens.append(token)
1661+ token_type, text = token[0:2]
1662+ if token_type == tokenize.OP and text in '([{':
1663+ parens += 1
1664+ if token_type == tokenize.OP and text in '}])':
1665+ parens -= 1
1666+ if token_type == tokenize.NEWLINE and not parens:
1667+ self.check_logical()
1668+ self.blank_lines = 0
1669+ self.tokens = []
1670+ if token_type == tokenize.NL and not parens:
1671+ self.blank_lines += 1
1672+ self.tokens = []
1673+ if token_type == tokenize.COMMENT:
1674+ source_line = token[4]
1675+ token_start = token[2][1]
1676+ if source_line[:token_start].strip() == '':
1677+ self.blank_lines = 0
1678+ return self.file_errors
1679+
1680+ def report_error(self, line_number, offset, text, check):
1681+ """
1682+ Report an error, according to options.
1683+ """
1684+ if options.quiet == 1 and not self.file_errors:
1685+ message(self.filename)
1686+ self.file_errors += 1
1687+ code = text[:4]
1688+ options.counters[code] = options.counters.get(code, 0) + 1
1689+ options.messages[code] = text[5:]
1690+ if options.quiet:
1691+ return
1692+ if options.testsuite:
1693+ base = os.path.basename(self.filename)[:4]
1694+ if base == code:
1695+ return
1696+ if base[0] == 'E' and code[0] == 'W':
1697+ return
1698+ if ignore_code(code):
1699+ return
1700+ if options.counters[code] == 1 or options.repeat:
1701+ message("%s:%s:%d: %s" %
1702+ (self.filename, line_number, offset + 1, text))
1703+ if options.show_source:
1704+ line = self.lines[line_number - 1]
1705+ message(line.rstrip())
1706+ message(' ' * offset + '^')
1707+ if options.show_pep8:
1708+ message(check.__doc__.lstrip('\n').rstrip())
1709+
1710+
1711+def input_file(filename):
1712+ """
1713+ Run all checks on a Python source file.
1714+ """
1715+ if excluded(filename) or not filename_match(filename):
1716+ return {}
1717+ if options.verbose:
1718+ message('checking ' + filename)
1719+ options.counters['files'] = options.counters.get('files', 0) + 1
1720+ errors = Checker(filename).check_all()
1721+ if options.testsuite and not errors:
1722+ message("%s: %s" % (filename, "no errors found"))
1723+
1724+
1725+def input_dir(dirname):
1726+ """
1727+ Check all Python source files in this directory and all subdirectories.
1728+ """
1729+ dirname = dirname.rstrip('/')
1730+ if excluded(dirname):
1731+ return
1732+ for root, dirs, files in os.walk(dirname):
1733+ if options.verbose:
1734+ message('directory ' + root)
1735+ options.counters['directories'] = \
1736+ options.counters.get('directories', 0) + 1
1737+ dirs.sort()
1738+ for subdir in dirs:
1739+ if excluded(subdir):
1740+ dirs.remove(subdir)
1741+ files.sort()
1742+ for filename in files:
1743+ input_file(os.path.join(root, filename))
1744+
1745+
1746+def excluded(filename):
1747+ """
1748+ Check if options.exclude contains a pattern that matches filename.
1749+ """
1750+ basename = os.path.basename(filename)
1751+ for pattern in options.exclude:
1752+ if fnmatch(basename, pattern):
1753+ # print basename, 'excluded because it matches', pattern
1754+ return True
1755+
1756+
1757+def filename_match(filename):
1758+ """
1759+ Check if options.filename contains a pattern that matches filename.
1760+ If options.filename is unspecified, this always returns True.
1761+ """
1762+ if not options.filename:
1763+ return True
1764+ for pattern in options.filename:
1765+ if fnmatch(filename, pattern):
1766+ return True
1767+
1768+
1769+def ignore_code(code):
1770+ """
1771+ Check if options.ignore contains a prefix of the error code.
1772+ """
1773+ for ignore in options.ignore:
1774+ if code.startswith(ignore):
1775+ return True
1776+
1777+
1778+def get_error_statistics():
1779+ """Get error statistics."""
1780+ return get_statistics("E")
1781+
1782+
1783+def get_warning_statistics():
1784+ """Get warning statistics."""
1785+ return get_statistics("W")
1786+
1787+
1788+def get_statistics(prefix=''):
1789+ """
1790+ Get statistics for message codes that start with the prefix.
1791+
1792+ prefix='' matches all errors and warnings
1793+ prefix='E' matches all errors
1794+ prefix='W' matches all warnings
1795+ prefix='E4' matches all errors that have to do with imports
1796+ """
1797+ stats = []
1798+ keys = options.messages.keys()
1799+ keys.sort()
1800+ for key in keys:
1801+ if key.startswith(prefix):
1802+ stats.append('%-7s %s %s' %
1803+ (options.counters[key], key, options.messages[key]))
1804+ return stats
1805+
1806+
1807+def print_statistics(prefix=''):
1808+ """Print overall statistics (number of errors and warnings)."""
1809+ for line in get_statistics(prefix):
1810+ print line
1811+
1812+
1813+def print_benchmark(elapsed):
1814+ """
1815+ Print benchmark numbers.
1816+ """
1817+ print '%-7.2f %s' % (elapsed, 'seconds elapsed')
1818+ keys = ['directories', 'files',
1819+ 'logical lines', 'physical lines']
1820+ for key in keys:
1821+ if key in options.counters:
1822+ print '%-7d %s per second (%d total)' % (
1823+ options.counters[key] / elapsed, key,
1824+ options.counters[key])
1825+
1826+
1827+def process_options(arglist=None):
1828+ """
1829+ Process options passed either via arglist or via command line args.
1830+ """
1831+ global options, args
1832+ usage = "%prog [options] input ..."
1833+ parser = OptionParser(usage)
1834+ parser.add_option('-v', '--verbose', default=0, action='count',
1835+ help="print status messages, or debug with -vv")
1836+ parser.add_option('-q', '--quiet', default=0, action='count',
1837+ help="report only file names, or nothing with -qq")
1838+ parser.add_option('--exclude', metavar='patterns', default=default_exclude,
1839+ help="skip matches (default %s)" % default_exclude)
1840+ parser.add_option('--filename', metavar='patterns',
1841+ help="only check matching files (e.g. *.py)")
1842+ parser.add_option('--ignore', metavar='errors', default='',
1843+ help="skip errors and warnings (e.g. E4,W)")
1844+ parser.add_option('--repeat', action='store_true',
1845+ help="show all occurrences of the same error")
1846+ parser.add_option('--show-source', action='store_true',
1847+ help="show source code for each error")
1848+ parser.add_option('--show-pep8', action='store_true',
1849+ help="show text of PEP 8 for each error")
1850+ parser.add_option('--statistics', action='store_true',
1851+ help="count errors and warnings")
1852+ parser.add_option('--benchmark', action='store_true',
1853+ help="measure processing speed")
1854+ parser.add_option('--testsuite', metavar='dir',
1855+ help="run regression tests from dir")
1856+ parser.add_option('--doctest', action='store_true',
1857+ help="run doctest on myself")
1858+ options, args = parser.parse_args(arglist)
1859+ if options.testsuite:
1860+ args.append(options.testsuite)
1861+ if len(args) == 0:
1862+ parser.error('input not specified')
1863+ options.prog = os.path.basename(sys.argv[0])
1864+ options.exclude = options.exclude.split(',')
1865+ for index in range(len(options.exclude)):
1866+ options.exclude[index] = options.exclude[index].rstrip('/')
1867+ if options.filename:
1868+ options.filename = options.filename.split(',')
1869+ if options.ignore:
1870+ options.ignore = options.ignore.split(',')
1871+ else:
1872+ options.ignore = []
1873+ options.counters = {}
1874+ options.messages = {}
1875+
1876+ return options, args
1877+
1878+
1879+def _main():
1880+ """
1881+ Parse options and run checks on Python source.
1882+ """
1883+ options, args = process_options()
1884+ if options.doctest:
1885+ import doctest
1886+ return doctest.testmod()
1887+ start_time = time.time()
1888+ for path in args:
1889+ if os.path.isdir(path):
1890+ input_dir(path)
1891+ else:
1892+ input_file(path)
1893+ elapsed = time.time() - start_time
1894+ if options.statistics:
1895+ print_statistics()
1896+ if options.benchmark:
1897+ print_benchmark(elapsed)
1898+
1899+
1900+if __name__ == '__main__':
1901+ _main()
1902
1903=== modified file 'setup.py'
1904--- setup.py 2009-07-04 13:31:04 +0000
1905+++ setup.py 2009-07-13 09:13:22 +0000
1906@@ -43,7 +43,7 @@
1907 if file.endswith(".png") or file.endswith(".svg"):
1908 dirList.append(os.path.join(root,file))
1909 if len(dirList)!=0:
1910- newroot = root.replace("data/","")
1911+ newroot = root.replace("data/","")
1912 fileList.append( (os.path.join(DATA_DIR,newroot),dirList) )
1913 return fileList
1914
1915
1916=== removed file 'tests/unit.sh'
1917--- tests/unit.sh 2009-03-17 18:56:53 +0000
1918+++ tests/unit.sh 1970-01-01 00:00:00 +0000
1919@@ -1,6 +0,0 @@
1920-#!/bin/bash
1921-#Launch unit tests
1922-#Argument is name of one existing test
1923-export XDG_DATA_HOME="./tests/xdg/data"
1924-export XDG_CONFIG_HOME="./tests/xdg/config"
1925-python ./tests/$1.py

Subscribers

People subscribed via source and target branches

to status/vote changes: