Merge lp:~nataliabidart/ubuntuone-control-panel/default-folders into lp:ubuntuone-control-panel

Proposed by Natalia Bidart on 2012-03-16
Status: Merged
Approved by: Natalia Bidart on 2012-03-16
Approved revision: 290
Merged at revision: 287
Proposed branch: lp:~nataliabidart/ubuntuone-control-panel/default-folders
Merge into: lp:ubuntuone-control-panel
Diff against target: 339 lines (+235/-8)
6 files modified
ubuntuone/controlpanel/logger.py (+2/-4)
ubuntuone/controlpanel/utils/__init__.py (+5/-2)
ubuntuone/controlpanel/utils/linux.py (+59/-0)
ubuntuone/controlpanel/utils/tests/test_linux.py (+101/-1)
ubuntuone/controlpanel/utils/tests/test_windows.py (+41/-0)
ubuntuone/controlpanel/utils/windows.py (+27/-1)
To merge this branch: bzr merge lp:~nataliabidart/ubuntuone-control-panel/default-folders
Reviewer Review Type Date Requested Status
dobey (community) Approve on 2012-03-16
Diego Sarmentero (community) Approve on 2012-03-16
Brian Curtin (community) 2012-03-16 Approve on 2012-03-16
Review via email: mp+97949@code.launchpad.net

Commit Message

- Implemented a method to list the user's default folders in every platform (part of LP: #933697).

To post a comment you must log in.
Brian Curtin (brian.curtin) wrote :

94 + with open(dirs_path) as f:
95 + while True:
96 + line = f.readline()

The file object you get back (f) is already iterable, so you could replace the "while True" with "for line in f"

Other than this it looks alright.

review: Needs Fixing
Natalia Bidart (nataliabidart) wrote :

> 94 + with open(dirs_path) as f:
> 95 + while True:
> 96 + line = f.readline()
>
> The file object you get back (f) is already iterable, so you could replace the
> "while True" with "for line in f"
>
> Other than this it looks alright.

Fixed and pushed, great catch! thanks

289. By Natalia Bidart on 2012-03-16

- Inprove default_folders file content iteration (thanks brian.curtin).

dobey (dobey) wrote :

86 + if dirs_path is None:
87 + dirs_path = os.path.join(user_home, '.config', 'user-dirs.dirs')

We probably want to use xdg_config_home from dirspec.basedir here, instead of assuming ~/.config/.

With the suggestion from Brian, you can also drop the if line is None: break check.

Do we need to check that translated names for directories get handled correctly as well here, and ensure that unicode handling doesn't break again? I don't see a test for that case in your tests.

Also, on Windows, for the special folders you check for, which aren't supported on XP for example, how is that error handled? I suppose there should be a test which causes and checks for the error when that happens, to ensure we handle it properly? The linked MSDN page seems to suggest that _MYVIDEO doesn't exist on 5.0 (XP) for example. It might also be worth noting in the comment that if/when we drop XP support, we probably want to switch to _MYDOCUMENTS instead of _PERSONAL for that folder; as the documentation seems to suggest we should do.

review: Needs Fixing
Brian Curtin (brian.curtin) wrote :

+1

review: Approve
Diego Sarmentero (diegosarmentero) wrote :

+1

review: Approve
Natalia Bidart (nataliabidart) wrote :

> 86 + if dirs_path is None:
> 87 + dirs_path = os.path.join(user_home, '.config', 'user-dirs.dirs')
>
> We probably want to use xdg_config_home from dirspec.basedir here, instead of
> assuming ~/.config/.

Added.

> With the suggestion from Brian, you can also drop the if line is None: break
> check.

Done already.

> Do we need to check that translated names for directories get handled
> correctly as well here, and ensure that unicode handling doesn't break again?
> I don't see a test for that case in your tests.

Added tests for this.

> Also, on Windows, for the special folders you check for, which aren't
> supported on XP for example, how is that error handled? I suppose there should
> be a test which causes and checks for the error when that happens, to ensure
> we handle it properly? The linked MSDN page seems to suggest that _MYVIDEO
> doesn't exist on 5.0 (XP) for example. It might also be worth noting in the
> comment that if/when we drop XP support, we probably want to switch to
> _MYDOCUMENTS instead of _PERSONAL for that folder; as the documentation seems
> to suggest we should do.

Good catch, made the code more robust and added tests for it.

290. By Natalia Bidart on 2012-03-16

Better unicode management, and non existent folders.

dobey (dobey) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'ubuntuone/controlpanel/logger.py'
2--- ubuntuone/controlpanel/logger.py 2012-02-06 18:09:58 +0000
3+++ ubuntuone/controlpanel/logger.py 2012-03-16 20:00:25 +0000
4@@ -1,8 +1,6 @@
5 # -*- coding: utf-8 -*-
6-
7-# Authors: Natalia B Bidart <natalia.bidart@canonical.com>
8 #
9-# Copyright 2010 Canonical Ltd.
10+# Copyright 2010-2012 Canonical Ltd.
11 #
12 # This program is free software: you can redistribute it and/or modify it
13 # under the terms of the GNU General Public License version 3, as published
14@@ -30,7 +28,7 @@
15 from ubuntuone.platform.xdg_base_directory import ubuntuone_log_dir
16
17
18-if os.environ.get('DEBUG'):
19+if os.environ.get('U1_DEBUG'):
20 LOG_LEVEL = logging.DEBUG
21 else:
22 # Only log this level and above
23
24=== modified file 'ubuntuone/controlpanel/utils/__init__.py'
25--- ubuntuone/controlpanel/utils/__init__.py 2012-03-09 13:44:53 +0000
26+++ ubuntuone/controlpanel/utils/__init__.py 2012-03-16 20:00:25 +0000
27@@ -1,7 +1,6 @@
28 # -*- coding: utf-8 -*-
29-
30 #
31-# Copyright 2010 Canonical Ltd.
32+# Copyright 2010-2012 Canonical Ltd.
33 #
34 # This program is free software: you can redistribute it and/or modify it
35 # under the terms of the GNU General Public License version 3, as published
36@@ -37,10 +36,14 @@
37 if sys.platform == 'win32':
38 from ubuntuone.controlpanel.utils import windows
39 are_updates_present = windows.are_updates_present
40+ default_folders = windows.default_folders
41 perform_update = windows.perform_update
42 else:
43+ from ubuntuone.controlpanel.utils import linux
44 are_updates_present = lambda *args, **kwargs: False
45+ default_folders = linux.default_folders
46 perform_update = lambda *args, **kwargs: None
47+
48 # pylint: enable=C0103
49
50
51
52=== added file 'ubuntuone/controlpanel/utils/linux.py'
53--- ubuntuone/controlpanel/utils/linux.py 1970-01-01 00:00:00 +0000
54+++ ubuntuone/controlpanel/utils/linux.py 2012-03-16 20:00:25 +0000
55@@ -0,0 +1,59 @@
56+# -*- coding: utf-8 -*-
57+#
58+# Copyright 2012 Canonical Ltd.
59+#
60+# This program is free software: you can redistribute it and/or modify it
61+# under the terms of the GNU General Public License version 3, as published
62+# by the Free Software Foundation.
63+#
64+# This program is distributed in the hope that it will be useful, but
65+# WITHOUT ANY WARRANTY; without even the implied warranties of
66+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
67+# PURPOSE. See the GNU General Public License for more details.
68+#
69+# You should have received a copy of the GNU General Public License along
70+# with this program. If not, see <http://www.gnu.org/licenses/>.
71+
72+"""Miscelaneous functions and constants for linux."""
73+
74+import codecs
75+import os
76+
77+from dirspec.basedir import xdg_config_home
78+from ubuntuone.controlpanel.logger import setup_logging
79+
80+
81+logger = setup_logging('utils.linux')
82+
83+
84+def default_folders(user_home='', dirs_path=None):
85+ """Return a list of the folders to add by default."""
86+ result = []
87+
88+ if dirs_path is None:
89+ dirs_path = os.path.join(xdg_config_home, u'user-dirs.dirs')
90+
91+ if not os.path.exists(dirs_path):
92+ logger.warning('default_folders: dirs_path %r does not exist.',
93+ dirs_path)
94+ return result
95+
96+ # pylint: disable=W0702
97+
98+ try:
99+ with codecs.open(dirs_path, encoding='utf-8') as f:
100+ for line in f:
101+ if line.startswith(u'#'):
102+ continue
103+
104+ try:
105+ _, value = line.strip().split(u'=')
106+ value = value.strip(u'"').replace(u'$HOME', user_home)
107+ except:
108+ logger.exception('default_folders: can not row %r:', line)
109+ else:
110+ result.append(value)
111+ except:
112+ logger.exception('default_folders: can not load file %r:', dirs_path)
113+
114+ return result
115
116=== modified file 'ubuntuone/controlpanel/utils/tests/test_linux.py'
117--- ubuntuone/controlpanel/utils/tests/test_linux.py 2011-10-26 10:45:54 +0000
118+++ ubuntuone/controlpanel/utils/tests/test_linux.py 2012-03-16 20:00:25 +0000
119@@ -17,11 +17,35 @@
120
121 """Test the linux utils functions."""
122
123+import codecs
124+import os
125
126 from twisted.internet import defer
127
128 from ubuntuone.controlpanel import utils
129-from ubuntuone.controlpanel.tests import TestCase
130+from ubuntuone.controlpanel.tests import TestCase, USER_HOME
131+
132+REAL_CONTENT = u"""
133+XDG_DESKTOP_DIR="$HOME/Desktop"
134+XDG_DOWNLOAD_DIR="$HOME/Downloads"
135+XDG_PUBLICSHARE_DIR="$HOME/Public"
136+XDG_DOCUMENTS_DIR="$HOME/Documents"
137+XDG_MUSIC_DIR="$HOME/Music"
138+XDG_PICTURES_DIR="$HOME/Pictures"
139+XDG_VIDEOS_DIR="$HOME/Videos"
140+"""
141+
142+NON_ASCII = u"""
143+XDG_DESKTOP_DIR="$HOME/桌面"
144+XDG_DOWNLOAD_DIR="$HOME/下载"
145+XDG_DOCUMENTS_DIR="$HOME/文档"
146+"""
147+
148+LATIN_1 = u"""
149+XDG_DESKTOP_DIR="$HOME/Súbeme"
150+XDG_DOWNLOAD_DIR="$HOME/Bájame"
151+XDG_DOCUMENTS_DIR="$HOME/Mis ñoños Documentos"
152+"""
153
154
155 class AutoupdaterTestCase(TestCase):
156@@ -36,3 +60,79 @@
157 def test_perform_update(self):
158 """Test the method that performs the update."""
159 self.assertEqual(None, utils.perform_update())
160+
161+
162+class DefaultFoldersTestCase(TestCase):
163+ """Test suite for the default_folders retriever."""
164+
165+ def _create_faked_file(self, content='', encoding=None):
166+ """Create a faked file to be parsed and clean it up afterwards."""
167+ filename = './dirs-parser-test.dirs'
168+ if os.path.exists(filename):
169+ os.remove(filename)
170+
171+ if encoding is None:
172+ encoding = 'utf-8'
173+
174+ with codecs.open(filename, 'w', encoding=encoding) as f:
175+ f.write(content)
176+
177+ self.addCleanup(os.remove, filename)
178+
179+ return filename
180+
181+ def test_default_folders_not_file(self):
182+ """Default folders are empty if the file does not exist."""
183+ path = 'foo/bar/baz/yadda/yo'
184+ assert not os.path.exists(path)
185+ actual = utils.default_folders(dirs_path=path)
186+
187+ self.assertEqual(actual, [])
188+
189+ def test_default_folders_empty_file(self):
190+ """Default folders are empty if the file empty."""
191+ filename = self._create_faked_file(content='')
192+ actual = utils.default_folders(dirs_path=filename)
193+
194+ self.assertEqual(actual, [])
195+
196+ def test_default_folders_only_comments(self):
197+ """Default folders are empty if only comments in the file."""
198+ content = u"# yadda yadda\n# foo bar baz\n#puipuipui"
199+ filename = self._create_faked_file(content=content)
200+ actual = utils.default_folders(dirs_path=filename)
201+
202+ self.assertEqual(actual, [])
203+
204+ def test_default_folders_syntax_error(self):
205+ """Default folders do not explode on syntax error."""
206+ content = u"foo ~ bar\nYADDA=DOO"
207+ filename = self._create_faked_file(content=content)
208+ actual = utils.default_folders(dirs_path=filename)
209+
210+ self.assertEqual(actual, [u'DOO'])
211+
212+ def test_default_folders_parsed(self):
213+ """Default folders parses the content of the file."""
214+ filename = self._create_faked_file(content=REAL_CONTENT)
215+ actual = utils.default_folders(user_home=USER_HOME, dirs_path=filename)
216+
217+ expected = (i.split(u'=')[1].strip(u'"') for i in REAL_CONTENT.split())
218+ expected = [i.replace(u'$HOME', USER_HOME) for i in expected]
219+ self.assertEqual(actual, expected)
220+
221+ def test_default_folders_non_ascii(self):
222+ """Default folders parses the content if it's non-ascii."""
223+ filename = self._create_faked_file(content=NON_ASCII)
224+ actual = utils.default_folders(user_home=USER_HOME, dirs_path=filename)
225+
226+ expected = (i.split(u'=')[1].strip(u'"') for i in NON_ASCII.split())
227+ expected = [i.replace(u'$HOME', USER_HOME) for i in expected]
228+ self.assertEqual(actual, expected)
229+
230+ def test_default_folders_bad_encoding(self):
231+ """Default folders parses the content if it's non-ascii."""
232+ filename = self._create_faked_file(content=LATIN_1, encoding='latin-1')
233+ actual = utils.default_folders(user_home=USER_HOME, dirs_path=filename)
234+
235+ self.assertEqual(actual, [])
236
237=== modified file 'ubuntuone/controlpanel/utils/tests/test_windows.py'
238--- ubuntuone/controlpanel/utils/tests/test_windows.py 2011-11-15 12:36:15 +0000
239+++ ubuntuone/controlpanel/utils/tests/test_windows.py 2012-03-16 20:00:25 +0000
240@@ -87,6 +87,47 @@
241 self.assertEqual(0, self._called[0][5])
242
243
244+class DefaultFoldersTestCase(TestCase):
245+ """Test the default_folders method."""
246+
247+ def test_special_folders(self, names=None):
248+ """Test that default_folders does the right thing."""
249+ folders = utils.default_folders()
250+
251+ if names is None:
252+ names = ('PERSONAL', 'MYMUSIC', 'MYPICTURES', 'MYVIDEO')
253+
254+ self.assertEqual(len(folders), len(names))
255+ self.assertEqual(folders, [os.path.abspath(f) for f in folders])
256+
257+ expected = []
258+ for name in names:
259+ name = getattr(utils.windows.shellcon, 'CSIDL_%s' % name)
260+ folder = utils.windows.shell.SHGetFolderPath(0, name, None, 0)
261+ expected.append(folder.encode('utf8'))
262+
263+ self.assertEqual(sorted(folders), sorted(expected))
264+
265+ def test_special_folders_with_error(self):
266+ """Test that default_folders handles errors."""
267+ failing_name = 'MYVIDEO'
268+ original_func = utils.windows.shell.SHGetFolderPath
269+
270+ class SomeError(Exception):
271+ """For test only."""
272+
273+ def fail_for_some(code, name, parent, flag):
274+ """Custom failer."""
275+ fail = getattr(utils.windows.shellcon, 'CSIDL_%s' % failing_name)
276+ if name == fail:
277+ raise SomeError('The parameter is incorrect.')
278+ else:
279+ return original_func(code, name, parent, flag)
280+
281+ self.patch(utils.windows.shell, 'SHGetFolderPath', fail_for_some)
282+ self.test_special_folders(names=('PERSONAL', 'MYMUSIC', 'MYPICTURES'))
283+
284+
285 class GetPathTestCase(TestCase):
286 """Test the code that is used for the auto update process."""
287
288
289=== modified file 'ubuntuone/controlpanel/utils/windows.py'
290--- ubuntuone/controlpanel/utils/windows.py 2012-03-09 00:47:47 +0000
291+++ ubuntuone/controlpanel/utils/windows.py 2012-03-16 20:00:25 +0000
292@@ -23,13 +23,15 @@
293 # Avoid pylint error on Linux
294 # pylint: disable=F0401
295 import win32api
296+
297+from win32com.shell import shell, shellcon
298 # pylint: enable=F0401
299 from twisted.internet import defer
300 from twisted.internet.utils import getProcessValue
301
302 from ubuntuone.controlpanel.logger import setup_logging
303
304-logger = setup_logging('utils')
305+logger = setup_logging('utils.windows')
306 AUTOUPDATE_EXE_NAME = 'autoupdate-windows.exe'
307
308
309@@ -62,6 +64,30 @@
310 defer.returnValue(result)
311
312
313+def default_folders(user_home=None):
314+ """Return a list of the folders to add by default."""
315+ # as per http://msdn.microsoft.com/en-us/library/windows/desktop/bb762181,
316+ # SHGetFolderPath is deprecated, we should migrate to SHGetKnownFolderPath
317+ # (http://msdn.microsoft.com/en-us/library/windows/desktop/bb762188)
318+ # but the latter does not support XP
319+ # (Minimum supported client: Windows Vista)
320+ get_path = lambda name: shell.SHGetFolderPath(
321+ 0, getattr(shellcon, name), None, 0).encode('utf8')
322+
323+ folders = []
324+ # More information on these constants at
325+ # http://msdn.microsoft.com/en-us/library/bb762494
326+ for name in (u'PERSONAL', u'MYMUSIC', u'MYPICTURES', u'MYVIDEO'):
327+ name = u'CSIDL_%s' % name
328+ try:
329+ folders.append(get_path(name))
330+ except: # pylint: disable=W0702
331+ logger.exception('default_folders: could not retrieve folder info '
332+ 'for name %r.', name)
333+
334+ return folders
335+
336+
337 def perform_update():
338 """Spawn the autoupdate process and call the stop function."""
339 update_path = _get_update_path()

Subscribers

People subscribed via source and target branches