Merge lp:~ivo-kracht/launchpad/bug-425934 into lp:launchpad

Proposed by Ivo Kracht on 2012-06-18
Status: Merged
Approved by: Abel Deuring on 2012-06-20
Approved revision: no longer in the source branch.
Merged at revision: 15458
Proposed branch: lp:~ivo-kracht/launchpad/bug-425934
Merge into: lp:launchpad
Diff against target: 246 lines (+83/-20)
7 files modified
lib/lp/bugs/mail/commands.py (+2/-0)
lib/lp/bugs/mail/handler.py (+4/-3)
lib/lp/code/mail/codehandler.py (+1/-1)
lib/lp/services/mail/commands.py (+7/-2)
lib/lp/services/mail/helpers.py (+9/-2)
lib/lp/services/mail/tests/test_commands.py (+34/-1)
lib/lp/services/mail/tests/test_helpers.py (+26/-11)
To merge this branch: bzr merge lp:~ivo-kracht/launchpad/bug-425934
Reviewer Review Type Date Requested Status
Raphaël Badin (community) 2012-06-20 Approve on 2012-06-20
Francesco Banconi (community) code* Approve on 2012-06-20
Ivo Kracht (community) Resubmit on 2012-06-20
Benji York (community) code 2012-06-18 Approve on 2012-06-18
Review via email: mp+110786@code.launchpad.net

Commit Message

Fix for bug-425934 - email commands are now case insensitive.

Description of the Change

I changed one line in the helper.py so that it converts all uppercase characters to lowercase characters. Incoming commands and args are now case insensitive. Converting everything to lowercase in the parse.commands function may be a little bit rough but should not create any problems. The only potential problem i can see is when project names and/or usernames are passed to the function but as far as I can see they have to be lowercase anyway.

Pre-imp call with adeuring

tests:

./bin/test services -vvt test_helpers

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/services/mail/helpers.py
  lib/lp/services/mail/tests/test_helpers.py

To post a comment you must log in.
Benji York (benji) wrote :

This branch looks good.

It might be slightly better to move the comment about commands having to be indented to just before the "if" because the "lower" line doesn't have anything to do with indentation. You could optionally also add a comment about the fact that we ignore capitalisation in commands just before the "lower" line.

review: Approve (code)
Ivo Kracht (ivo-kracht) wrote :

An EC2 test failed because some email command have to be uppercase (summary, cve) so I had to except those from being converted to lowercase. I did that by adding a new value called lowercase_args that can be set to False if a command should be case sensitive. Also i changed the names function into parsingParameters and wrote a unit test for its new task.

Pre-imp call with adeuring

tests:
lib/lp/services/mail/tests/test_helpers.py
lib/lp/services/mail/tests/test_commands.py

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/bugs/mail/commands.py
  lib/lp/bugs/mail/handler.py
  lib/lp/code/mail/codehandler.py
  lib/lp/services/mail/commands.py
  lib/lp/services/mail/helpers.py
  lib/lp/services/mail/tests/test_commands.py
  lib/lp/services/mail/tests/test_helpers.py

review: Resubmit
Francesco Banconi (frankban) wrote :

Another great contribution Ivo, thank you.
Suggestions follow.

#8
+ lowercase_args = False
Just an idea, and just a naming doubt: it seems that what we want to express here is the concept of a command with case insensitive args, and that converting them to lowercase is just an implementation detail. Maybe you could use a `case_insensitive_args` class attribute instead?

#97 and #104
if len(words) > 0:
Isn't it more "pythonic" to just check `if words:`?

#106
+ if command_names[first]:
+ words = words.lower()
+ words = words.split(' ')
                     commands.append((first, words))
This is really a personal opinion, and maybe I am wrong, but I think the code could be more readable if written as::
    case_insensitive_args = command_names[first]
    if case_insensitive_args:
        words = words.lower()
    commands.append((first, words.split(' ')))

review: Approve (code*)
Raphaël Badin (rvb) wrote :

Looks good.

[0]

I agree with Francesco's suggestions. Well, all suggestions but one: I think you should keep "if len(words) > 0:" because it's more specific than using "if words". For the same reason, if you want to test if "something" is None, we would rather write explicitly "if something is not None:" rather than "if something:".

[1]

147 + self.assertEqual(
148 + {'one': True,
149 + 'two': False,
150 + },
151 + SampleCommandCollection.parsingParameters())

This is really a detail but I think you could write this as

    self.assertEqual(
        {'one': True, 'two': False},
        SampleCommandCollection.parsingParameters())

It would (artificially) reduce the SLOC and I think it's more readable. But again, it's really a detail so feel to change it or not.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/mail/commands.py'
2--- lib/lp/bugs/mail/commands.py 2012-05-22 12:05:51 +0000
3+++ lib/lp/bugs/mail/commands.py 2012-06-20 12:21:30 +0000
4@@ -371,6 +371,7 @@
5 implements(IBugEditEmailCommand)
6 _numberOfArguments = 1
7 RANK = 1
8+ case_insensitive_args = False
9
10 def execute(self, bug, current_event):
11 """See IEmailCommand."""
12@@ -451,6 +452,7 @@
13
14 _numberOfArguments = 1
15 RANK = 5
16+ case_insensitive_args = False
17
18 def execute(self, bug, current_event):
19 """See IEmailCommand."""
20
21=== modified file 'lib/lp/bugs/mail/handler.py'
22--- lib/lp/bugs/mail/handler.py 2012-01-01 02:58:52 +0000
23+++ lib/lp/bugs/mail/handler.py 2012-06-20 12:21:30 +0000
24@@ -188,9 +188,10 @@
25 content = get_main_body(signed_msg)
26 if content is None:
27 return []
28- return [BugEmailCommands.get(name=name, string_args=args) for
29- name, args in parse_commands(content,
30- BugEmailCommands.names())]
31+ return [
32+ BugEmailCommands.get(name=name, string_args=args) for
33+ name, args in parse_commands(
34+ content, BugEmailCommands.parsingParameters())]
35
36 def extractAndAuthenticateCommands(self, signed_msg, to_addr):
37 """Extract commands and handle special destinations.
38
39=== modified file 'lib/lp/code/mail/codehandler.py'
40--- lib/lp/code/mail/codehandler.py 2012-06-14 05:18:22 +0000
41+++ lib/lp/code/mail/codehandler.py 2012-06-20 12:21:30 +0000
42@@ -209,7 +209,7 @@
43 return []
44 commands = [klass.get(name=name, string_args=args) for
45 name, args in parse_commands(message_body,
46- klass._commands.keys())]
47+ klass.parsingParameters())]
48 return sorted(commands, key=operator.attrgetter('sort_order'))
49
50 @classmethod
51
52=== modified file 'lib/lp/services/mail/commands.py'
53--- lib/lp/services/mail/commands.py 2011-08-12 14:39:51 +0000
54+++ lib/lp/services/mail/commands.py 2012-06-20 12:21:30 +0000
55@@ -63,6 +63,9 @@
56 """
57 _numberOfArguments = None
58
59+ # Should command arguments be converted to lowercase?
60+ case_insensitive_args = True
61+
62 def __init__(self, name, string_args):
63 self.name = name
64 self.string_args = normalize_arguments(string_args)
65@@ -139,9 +142,11 @@
66 """A collection of email commands."""
67
68 @classmethod
69- def names(klass):
70+ def parsingParameters(klass):
71 """Returns all the command names."""
72- return klass._commands.keys()
73+ return dict(
74+ (command_name, command.case_insensitive_args)
75+ for command_name, command in klass._commands.items())
76
77 @classmethod
78 def get(klass, name, string_args):
79
80=== modified file 'lib/lp/services/mail/helpers.py'
81--- lib/lp/services/mail/helpers.py 2012-03-02 19:45:41 +0000
82+++ lib/lp/services/mail/helpers.py 2012-06-20 12:21:30 +0000
83@@ -108,6 +108,7 @@
84 A list of (command, args) tuples is returned.
85 """
86 commands = []
87+
88 for line in content.splitlines():
89 # All commands have to be indented.
90 if line.startswith(' ') or line.startswith('\t'):
91@@ -116,12 +117,18 @@
92 # If the 'done' statement is encountered,
93 # stop reading any more commands.
94 break
95- words = command_string.split(' ')
96+ words = command_string.split(' ', 1)
97 if len(words) > 0:
98- first = words.pop(0)
99+ # Capitalization gets ignored
100+ first = words.pop(0).lower()
101 if first.endswith(':'):
102 first = first[:-1]
103 if first in command_names:
104+ if len(words) > 0:
105+ words = words[0]
106+ if command_names[first]:
107+ words = words.lower()
108+ words = words.split(' ')
109 commands.append((first, words))
110 return commands
111
112
113=== modified file 'lib/lp/services/mail/tests/test_commands.py'
114--- lib/lp/services/mail/tests/test_commands.py 2011-08-11 21:24:58 +0000
115+++ lib/lp/services/mail/tests/test_commands.py 2012-06-20 12:21:30 +0000
116@@ -2,7 +2,40 @@
117 # GNU Affero General Public License version 3 (see the file LICENSE).
118
119 from doctest import DocTestSuite
120+import unittest
121+
122+from lp.testing import TestCase
123+
124+from lp.services.mail.commands import(
125+ EmailCommand,
126+ EmailCommandCollection,
127+ )
128+
129+
130+class CommandOne(EmailCommand):
131+ pass
132+
133+
134+class CommandTwo(EmailCommand):
135+ case_insensitive_args = False
136+
137+
138+class SampleCommandCollection(EmailCommandCollection):
139+ _commands = {
140+ 'one': CommandOne,
141+ 'two': CommandTwo,
142+ }
143+
144+
145+class TestEmailCommandCollection(TestCase):
146+ def test_parsingParameters(self):
147+ self.assertEqual(
148+ {'one': True, 'two': False},
149+ SampleCommandCollection.parsingParameters())
150
151
152 def test_suite():
153- return DocTestSuite('lp.services.mail.commands')
154+ suite = unittest.TestSuite()
155+ suite.addTest(DocTestSuite('lp.services.mail.commands'))
156+ suite.addTest(unittest.TestLoader().loadTestsFromName(__name__))
157+ return suite
158
159=== modified file 'lib/lp/services/mail/tests/test_helpers.py'
160--- lib/lp/services/mail/tests/test_helpers.py 2012-03-05 20:13:15 +0000
161+++ lib/lp/services/mail/tests/test_helpers.py 2012-06-20 12:21:30 +0000
162@@ -39,28 +39,42 @@
163
164 def test_parse_commandsEmpty(self):
165 """Empty messages have no commands."""
166- self.assertEqual([], parse_commands('', ['command']))
167+ self.assertEqual([], parse_commands('', {'command': True}))
168
169 def test_parse_commandsNoIndent(self):
170 """Commands with no indent are not commands."""
171- self.assertEqual([], parse_commands('command', ['command']))
172+ self.assertEqual([], parse_commands('command', {'command': True}))
173
174 def test_parse_commandsSpaceIndent(self):
175 """Commands indented with spaces are recognized."""
176 self.assertEqual(
177- [('command', [])], parse_commands(' command', ['command']))
178+ [('command', [])], parse_commands(' command', {'command': True}))
179
180 def test_parse_commands_args(self):
181 """Commands indented with spaces are recognized."""
182 self.assertEqual(
183 [('command', ['arg1', 'arg2'])],
184- parse_commands(' command arg1 arg2', ['command']))
185+ parse_commands(' command arg1 arg2', {'command': True}))
186+
187+ def test_parse_commands_args_uppercase_unchanged(self):
188+ """Commands and args containing uppercase letters are not
189+ converted to lowercase if the flag is False."""
190+ self.assertEqual(
191+ [('command', ['Arg1', 'aRg2'])],
192+ parse_commands(' comMand Arg1 aRg2', {'command': False}))
193+
194+ def test_parse_commands_args_uppercase_to_lowercase(self):
195+ """Commands and args containing uppercase letters are converted to
196+ lowercase."""
197+ self.assertEqual(
198+ [('command', ['arg1', 'arg2'])],
199+ parse_commands(' comMand Arg1 aRg2', {'command': True}))
200
201 def test_parse_commands_args_quoted(self):
202 """Commands indented with spaces are recognized."""
203 self.assertEqual(
204 [('command', ['"arg1', 'arg2"'])],
205- parse_commands(' command "arg1 arg2"', ['command']))
206+ parse_commands(' command "arg1 arg2"', {'command': True}))
207
208 def test_parse_commandsTabIndent(self):
209 """Commands indented with tabs are recognized.
210@@ -68,29 +82,30 @@
211 (Tabs? What are we, make?)
212 """
213 self.assertEqual(
214- [('command', [])], parse_commands('\tcommand', ['command']))
215+ [('command', [])], parse_commands('\tcommand', {'command': True}))
216
217 def test_parse_commandsDone(self):
218 """The 'done' pseudo-command halts processing."""
219 self.assertEqual(
220 [('command', []), ('command', [])],
221- parse_commands(' command\n command', ['command']))
222+ parse_commands(' command\n command', {'command': True}))
223 self.assertEqual(
224 [('command', [])],
225- parse_commands(' command\n done\n command', ['command']))
226+ parse_commands(' command\n done\n command', {'command': True}))
227 # done takes no arguments.
228 self.assertEqual(
229 [('command', []), ('command', [])],
230- parse_commands(' command\n done commands\n command', ['command']))
231+ parse_commands(
232+ ' command\n done commands\n command', {'command': True}))
233
234 def test_parse_commands_optional_colons(self):
235 """Colons at the end of commands are accepted and stripped."""
236 self.assertEqual(
237 [('command', ['arg1', 'arg2'])],
238- parse_commands(' command: arg1 arg2', ['command']))
239+ parse_commands(' command: arg1 arg2', {'command': True}))
240 self.assertEqual(
241 [('command', [])],
242- parse_commands(' command:', ['command']))
243+ parse_commands(' command:', {'command': True}))
244
245
246 class TestEnsureSaneSignatureTimestamp(unittest.TestCase):