Ticket #715: basedir-option-improvements.dpatch

File basedir-option-improvements.dpatch, 19.5 KB (added by davidsarah, at 2010-07-22T01:07:56Z)

Basedir/node directory option improvements. addresses #188, #706, #715, #772, #890

Line 
1Thu Jul 22 00:15:07 GMT Daylight Time 2010  david-sarah@jacaranda.org
2  * util.fileutil, test.test_util: add abspath_expanduser_unicode function, to work around <http://bugs.python.org/issue3426>. util.encodingutil: add a convenience function argv_to_abspath.
3
4Thu Jul 22 00:48:34 GMT Daylight Time 2010  david-sarah@jacaranda.org
5  * Basedir/node directory option improvements. addresses #188, #706, #715, #772, #890
6
7New patches:
8
9[util.fileutil, test.test_util: add abspath_expanduser_unicode function, to work around <http://bugs.python.org/issue3426>. util.encodingutil: add a convenience function argv_to_abspath.
10david-sarah@jacaranda.org**20100721231507
11 Ignore-this: eee6904d1f65a733ff35190879844d08
12] {
13hunk ./src/allmydata/test/test_util.py 4
14 
15 def foo(): pass # keep the line number constant
16 
17-import os, time
18+import os, time, sys
19 from StringIO import StringIO
20 from twisted.trial import unittest
21 from twisted.internet import defer, reactor
22hunk ./src/allmydata/test/test_util.py 473
23         used = fileutil.du(basedir)
24         self.failUnlessEqual(10+11+12+13, used)
25 
26+    def test_abspath_expanduser_unicode(self):
27+        self.failUnlessRaises(AssertionError, fileutil.abspath_expanduser_unicode, "bytestring")
28+
29+        saved_cwd = os.path.normpath(os.getcwdu())
30+        abspath_cwd = fileutil.abspath_expanduser_unicode(u".")
31+        self.failUnless(isinstance(saved_cwd, unicode), saved_cwd)
32+        self.failUnless(isinstance(abspath_cwd, unicode), abspath_cwd)
33+        self.failUnlessEqual(abspath_cwd, saved_cwd)
34+
35+        # adapted from <http://svn.python.org/view/python/branches/release26-maint/Lib/test/test_posixpath.py?view=markup&pathrev=78279#test_abspath>
36+
37+        self.failUnlessIn(u"foo", fileutil.abspath_expanduser_unicode(u"foo"))
38+        self.failIfIn(u"~", fileutil.abspath_expanduser_unicode(u"~"))
39+
40+        cwds = ['cwd']
41+        try:
42+            cwds.append(u'\xe7w\xf0'.encode(sys.getfilesystemencoding()
43+                                            or 'ascii'))
44+        except UnicodeEncodeError:
45+            pass # the cwd can't be encoded -- test with ascii cwd only
46+
47+        for cwd in cwds:
48+            try:
49+                os.mkdir(cwd)
50+                os.chdir(cwd)
51+                for upath in (u'', u'fuu', u'f\xf9\xf9', u'/fuu', u'U:\\', u'~'):
52+                    uabspath = fileutil.abspath_expanduser_unicode(upath)
53+                    self.failUnless(isinstance(uabspath, unicode), uabspath)
54+            finally:
55+                os.chdir(saved_cwd)
56+
57 class PollMixinTests(unittest.TestCase):
58     def setUp(self):
59         self.pm = pollmixin.PollMixin()
60hunk ./src/allmydata/util/encodingutil.py 13
61 from twisted.python import usage
62 import locale
63 from allmydata.util import log
64+from allmydata.util.fileutil import abspath_expanduser_unicode
65 
66 
67 def _canonical_encoding(encoding):
68hunk ./src/allmydata/util/encodingutil.py 95
69         raise usage.UsageError("Argument %s cannot be decoded as %s." %
70                                (quote_output(s), argv_encoding))
71 
72+def argv_to_abspath(s):
73+    """
74+    Convenience function to decode an argv element to an absolute path, with ~ expanded.
75+    If this fails, raise a UsageError.
76+    """
77+    return abspath_expanduser_unicode(argv_to_unicode(s))
78+
79 def unicode_to_url(s):
80     """
81     Encode an unicode object used in an URL.
82hunk ./src/allmydata/util/fileutil.py 276
83     finally:
84         outf.close()
85 
86+
87+# Work around <http://bugs.python.org/issue3426>. This code is adapted from
88+# <http://svn.python.org/view/python/trunk/Lib/ntpath.py?revision=78247&view=markup>
89+# with some simplifications.
90+
91+_getfullpathname = None
92+try:
93+    from nt import _getfullpathname
94+except ImportError:
95+    pass
96+
97+def abspath_expanduser_unicode(path):
98+    """Return the absolute version of a path."""
99+    assert isinstance(path, unicode), path
100+
101+    path = os.path.expanduser(path)
102+
103+    if _getfullpathname:
104+        # On Windows, os.path.isabs will return True for paths without a drive letter,
105+        # e.g. "\\". See <http://bugs.python.org/issue1669539>.
106+        try:
107+            path = _getfullpathname(path or u".")
108+        except WindowsError:
109+            pass
110+
111+    if not os.path.isabs(path):
112+        path = os.path.join(os.getcwdu(), path)
113+
114+    # We won't hit <http://bugs.python.org/issue5827> because
115+    # there is always at least one Unicode path component.
116+    return os.path.normpath(path)
117+
118}
119[Basedir/node directory option improvements. addresses #188, #706, #715, #772, #890
120david-sarah@jacaranda.org**20100721234834
121 Ignore-this: 92d52f3af4acb0d659cb49e3306fef6c
122] {
123hunk ./src/allmydata/scripts/cli.py 3
124 import os.path, re, sys, fnmatch
125 from twisted.python import usage
126-from allmydata.scripts.common import BaseOptions, get_aliases
127-from allmydata.util.encodingutil import argv_to_unicode
128+from allmydata.scripts.common import BaseOptions, get_aliases, get_default_nodedir, DEFAULT_ALIAS
129+from allmydata.util.encodingutil import argv_to_unicode, argv_to_abspath, quote_output
130 
131 NODEURL_RE=re.compile("http(s?)://([^:]*)(:([1-9][0-9]*))?")
132 
133hunk ./src/allmydata/scripts/cli.py 8
134-class VDriveOptions(BaseOptions, usage.Options):
135+_default_nodedir = get_default_nodedir()
136+
137+class VDriveOptions(BaseOptions):
138     optParameters = [
139hunk ./src/allmydata/scripts/cli.py 12
140-        ["node-directory", "d", "~/.tahoe",
141-         "Look here to find out which Tahoe node should be used for all "
142-         "operations. The directory should either contain a full Tahoe node, "
143-         "or a file named node.url which points to some other Tahoe node. "
144-         "It should also contain a file named private/aliases which contains "
145-         "the mapping from alias name to root dirnode URI."
146-         ],
147+        ["node-directory", "d", None,
148+         "Specify which Tahoe node directory should be used. The directory "
149+         "should either contain a full Tahoe node, or a file named node.url "
150+         "that points to some other Tahoe node. It should also contain a file "
151+         "named private/aliases which contains the mapping from alias name "
152+         "to root dirnode URI." + (
153+            _default_nodedir and (" [default for most commands: " + quote_output(_default_nodedir) + "]") or "")],
154         ["node-url", "u", None,
155          "URL of the tahoe node to use, a URL like \"http://127.0.0.1:3456\". "
156          "This overrides the URL found in the --node-directory ."],
157hunk ./src/allmydata/scripts/cli.py 27
158         ]
159 
160     def postOptions(self):
161-        # TODO: allow Unicode node-dir
162-        # compute a node-url from the existing options, put in self['node-url']
163         if self['node-directory']:
164hunk ./src/allmydata/scripts/cli.py 28
165-            if sys.platform == 'win32' and self['node-directory'] == '~/.tahoe':
166-                from allmydata.windows import registry
167-                self['node-directory'] = registry.get_base_dir_path()
168-            else:
169-                self['node-directory'] = os.path.expanduser(self['node-directory'])
170+            self['node-directory'] = argv_to_abspath(self['node-directory'])
171+        else:
172+            self['node-directory'] = _default_nodedir
173+
174+        # compute a node-url from the existing options, put in self['node-url']
175         if self['node-url']:
176             if (not isinstance(self['node-url'], basestring)
177                 or not NODEURL_RE.match(self['node-url'])):
178hunk ./src/allmydata/scripts/cli.py 48
179 
180         aliases = get_aliases(self['node-directory'])
181         if self['dir-cap']:
182-            aliases["tahoe"] = self['dir-cap']
183+            aliases[DEFAULT_ALIAS] = self['dir-cap']
184         self.aliases = aliases # maps alias name to dircap
185 
186 
187hunk ./src/allmydata/scripts/common.py 5
188 import os, sys, urllib
189 import codecs
190 from twisted.python import usage
191-from allmydata.util.encodingutil import unicode_to_url, quote_output
192 from allmydata.util.assertutil import precondition
193hunk ./src/allmydata/scripts/common.py 6
194+from allmydata.util.encodingutil import unicode_to_url, quote_output, argv_to_abspath
195+from allmydata.util.fileutil import abspath_expanduser_unicode
196 
197hunk ./src/allmydata/scripts/common.py 9
198-class BaseOptions:
199+
200+_default_nodedir = None
201+if sys.platform == 'win32':
202+    from allmydata.windows import registry
203+    path = registry.get_base_dir_path()
204+    if path:
205+        precondition(isinstance(path, unicode), path)
206+        _default_nodedir = abspath_expanduser_unicode(path)
207+
208+if _default_nodedir is None:
209+    path = abspath_expanduser_unicode(u"~/.tahoe")
210+    precondition(isinstance(path, unicode), path)
211+    _default_nodedir = path
212+
213+def get_default_nodedir():
214+    return _default_nodedir
215+
216+
217+class BaseOptions(usage.Options):
218     # unit tests can override these to point at StringIO instances
219     stdin = sys.stdin
220     stdout = sys.stdout
221hunk ./src/allmydata/scripts/common.py 37
222         ["quiet", "q", "Operate silently."],
223         ["version", "V", "Display version numbers and exit."],
224         ["version-and-path", None, "Display version numbers and paths to their locations and exit."],
225-        ]
226-
227+    ]
228+    optParameters = [
229+        ["node-directory", "d", None, "Specify which Tahoe node directory should be used." + (
230+            _default_nodedir and (" [default for most commands: " + quote_output(_default_nodedir) + "]") or "")],
231+    ]
232+   
233     def opt_version(self):
234         import allmydata
235         print >>self.stdout, allmydata.get_package_versions_string()
236hunk ./src/allmydata/scripts/common.py 55
237 
238 
239 class BasedirMixin:
240-    optFlags = [
241-        ["multiple", "m", "allow multiple basedirs to be specified at once"],
242-        ]
243+    default_nodedir = _default_nodedir
244+    allow_multiple = True
245 
246hunk ./src/allmydata/scripts/common.py 58
247-    def postOptions(self):
248-        if not self.basedirs:
249-            raise usage.UsageError("<basedir> parameter is required")
250-        if self['basedir']:
251-            del self['basedir']
252-        self['basedirs'] = [os.path.abspath(os.path.expanduser(b)) for b in self.basedirs]
253+    optParameters = [
254+        ["basedir", "C", None, "Same as --node-directory."],
255+    ]
256+    optFlags = [
257+        ["multiple", "m", "Specify multiple node directories at once"],
258+    ]
259 
260     def parseArgs(self, *args):
261hunk ./src/allmydata/scripts/common.py 66
262-        self.basedirs = []
263-        if self['basedir']:
264-            precondition(isinstance(self['basedir'], (str, unicode)), self['basedir'])
265-            self.basedirs.append(self['basedir'])
266-        if self['multiple']:
267-            precondition(not [x for x in args if not isinstance(x, (str, unicode))], args)
268-            self.basedirs.extend(args)
269+        if self['node-directory'] and self['basedir']:
270+            raise usage.UsageError("The --node-directory (or -d) and --basedir (or -C) "
271+                                   "options cannot both be used.")
272+
273+        if self['node-directory'] or self['basedir']:
274+            self.basedirs = [argv_to_abspath(self['node-directory'] or self['basedir'])]
275         else:
276hunk ./src/allmydata/scripts/common.py 73
277-            if len(args) == 0 and not self.basedirs:
278-                if sys.platform == 'win32':
279-                    from allmydata.windows import registry
280-                    rbdp = registry.get_base_dir_path()
281-                    if rbdp:
282-                        precondition(isinstance(registry.get_base_dir_path(), (str, unicode)), registry.get_base_dir_path())
283-                        self.basedirs.append(rbdp)
284-                else:
285-                    precondition(isinstance(os.path.expanduser("~/.tahoe"), (str, unicode)), os.path.expanduser("~/.tahoe"))
286-                    self.basedirs.append(os.path.expanduser("~/.tahoe"))
287-            if len(args) > 0:
288-                precondition(isinstance(args[0], (str, unicode)), args[0])
289-                self.basedirs.append(args[0])
290-            if len(args) > 1:
291-                raise usage.UsageError("I wasn't expecting so many arguments")
292+            self.basedirs = []
293 
294hunk ./src/allmydata/scripts/common.py 75
295-class NoDefaultBasedirMixin(BasedirMixin):
296-    def parseArgs(self, *args):
297-        # create-client won't default to --basedir=~/.tahoe
298-        self.basedirs = []
299-        if self['basedir']:
300-            precondition(isinstance(self['basedir'], (str, unicode)), self['basedir'])
301-            self.basedirs.append(self['basedir'])
302-        if self['multiple']:
303-            precondition(not [x for x in args if not isinstance(x, (str, unicode))], args)
304-            self.basedirs.extend(args)
305+        if self.allow_multiple and self['multiple']:
306+            self.basedirs.extend(map(argv_to_abspath, args))
307         else:
308hunk ./src/allmydata/scripts/common.py 78
309-            if len(args) > 0:
310-                precondition(isinstance(args[0], (str, unicode)), args[0])
311-                self.basedirs.append(args[0])
312             if len(args) > 1:
313hunk ./src/allmydata/scripts/common.py 79
314-                raise usage.UsageError("I wasn't expecting so many arguments")
315+                raise usage.UsageError("I wasn't expecting so many arguments." +
316+                    (self.allow_multiple and
317+                     " Use the --multiple option to specify more than one node directory." or ""))
318+
319+            if len(args) == 0 and self.default_nodedir and not self.basedirs:
320+                self.basedirs.append(self.default_nodedir)
321+            elif len(args) > 0:
322+                self.basedirs.append(argv_to_abspath(args[0]))
323+
324+    def postOptions(self):
325         if not self.basedirs:
326hunk ./src/allmydata/scripts/common.py 90
327-            raise usage.UsageError("--basedir must be provided")
328+            raise usage.UsageError("A base directory for the node must be provided.")
329+        del self['basedir']
330+        self['basedirs'] = self.basedirs
331+
332 
333 DEFAULT_ALIAS = u"tahoe"
334 
335hunk ./src/allmydata/scripts/common.py 107
336         f = open(rootfile, "r")
337         rootcap = f.read().strip()
338         if rootcap:
339-            aliases[u"tahoe"] = uri.from_string_dirnode(rootcap).to_string()
340+            aliases[DEFAULT_ALIAS] = uri.from_string_dirnode(rootcap).to_string()
341     except EnvironmentError:
342         pass
343     try:
344hunk ./src/allmydata/scripts/create_node.py 3
345 
346 import os, sys
347-from twisted.python import usage
348-from allmydata.scripts.common import BasedirMixin, NoDefaultBasedirMixin
349+from allmydata.scripts.common import BasedirMixin, BaseOptions
350+from allmydata.util.assertutil import precondition
351+from allmydata.util.encodingutil import listdir_unicode, argv_to_unicode, quote_output
352 
353hunk ./src/allmydata/scripts/create_node.py 7
354-class CreateClientOptions(BasedirMixin, usage.Options):
355+class CreateClientOptions(BasedirMixin, BaseOptions):
356     optParameters = [
357         ("basedir", "C", None, "which directory to create the node in"),
358         # we provide 'create-node'-time options for the most common
359hunk ./src/allmydata/scripts/create_node.py 24
360         ("no-storage", None, "do not offer storage service to other nodes"),
361         ]
362 
363-class CreateIntroducerOptions(NoDefaultBasedirMixin, usage.Options):
364+class CreateIntroducerOptions(BasedirMixin, BaseOptions):
365+    default_nodedir = None
366+
367     optParameters = [
368         ["basedir", "C", None, "which directory to create the introducer in"],
369         ]
370hunk ./src/allmydata/scripts/create_node.py 73
371     c.write("\n\n")
372 
373     c.write("[node]\n")
374-    c.write("nickname = %s\n" % config.get("nickname", "")) #TODO: utf8 in argv?
375-    webport = config.get("webport", "none")
376+    nickname = argv_to_unicode(config.get("nickname") or "")
377+    c.write("nickname = %s\n" % (nickname.encode('utf-8'),))
378+
379+    # TODO: validate webport
380+    webport = argv_to_unicode(config.get("webport") or "none")
381     if webport.lower() == "none":
382         webport = ""
383hunk ./src/allmydata/scripts/create_node.py 80
384-    c.write("web.port = %s\n" % webport)
385+    c.write("web.port = %s\n" % (webport.encode('utf-8'),))
386     c.write("web.static = public_html\n")
387     c.write("#tub.port =\n")
388     c.write("#tub.location = \n")
389hunk ./src/allmydata/scripts/keygen.py 3
390 
391 import os, sys
392-from twisted.python import usage
393-#from allmydata.scripts.common import BasedirMixin, NoDefaultBasedirMixin
394+from allmydata.scripts.common import BasedirMixin, BaseOptions
395+from allmydata.util.encodingutil import listdir_unicode, quote_output
396+
397+class CreateKeyGeneratorOptions(BasedirMixin, BaseOptions):
398+    default_nodedir = None
399+    allow_multiple = False
400 
401hunk ./src/allmydata/scripts/keygen.py 10
402-class CreateKeyGeneratorOptions(usage.Options):
403     optParameters = [
404         ["basedir", "C", None, "which directory to create the key-generator in"],
405hunk ./src/allmydata/scripts/keygen.py 12
406-        ]
407+    ]
408 
409 keygen_tac = """
410 # -*- python -*-
411hunk ./src/allmydata/scripts/keygen.py 30
412 """
413 
414 def create_key_generator(config, out=sys.stdout, err=sys.stderr):
415-    basedir = config['basedir']
416-    if not basedir:
417-        print >>err, "a basedir was not provided, please use --basedir or -C"
418-        return -1
419+    basedir = config['basedirs'][0]
420     if os.path.exists(basedir):
421         if os.listdir(basedir):
422             print >>err, "The base directory \"%s\", which is \"%s\" is not empty." % (basedir, os.path.abspath(basedir))
423hunk ./src/allmydata/scripts/startstop_node.py 3
424 
425 import os, sys, signal, time
426-from twisted.python import usage
427-from allmydata.scripts.common import BasedirMixin
428+from allmydata.scripts.common import BasedirMixin, BaseOptions
429 from allmydata.util import fileutil, find_exe
430 
431hunk ./src/allmydata/scripts/startstop_node.py 6
432-class StartOptions(BasedirMixin, usage.Options):
433+class StartOptions(BasedirMixin, BaseOptions):
434     optParameters = [
435         ["basedir", "C", None, "which directory to start the node in"],
436         ]
437hunk ./src/allmydata/scripts/startstop_node.py 15
438         ["syslog", None, "tell the node to log to syslog, not a file"],
439         ]
440 
441-class StopOptions(BasedirMixin, usage.Options):
442+class StopOptions(BasedirMixin, BaseOptions):
443     optParameters = [
444         ["basedir", "C", None, "which directory to stop the node in"],
445         ]
446hunk ./src/allmydata/scripts/startstop_node.py 20
447 
448-class RestartOptions(BasedirMixin, usage.Options):
449+class RestartOptions(BasedirMixin, BaseOptions):
450     optParameters = [
451         ["basedir", "C", None, "which directory to restart the node in"],
452         ]
453hunk ./src/allmydata/scripts/startstop_node.py 29
454         ["syslog", None, "tell the node to log to syslog, not a file"],
455         ]
456 
457-class RunOptions(usage.Options):
458+class RunOptions(BasedirMixin, BaseOptions):
459+    default_nodedir = u"."
460+
461     optParameters = [
462         ["basedir", "C", None, "which directory to run the node in, CWD by default"],
463         ]
464hunk ./src/allmydata/test/test_runner.py 206
465 
466         # make sure it rejects a missing basedir specification
467         argv = ["create-key-generator"]
468-        rc, out, err = self.run_tahoe(argv)
469-        self.failIfEqual(rc, 0, str((out, err, rc)))
470-        self.failUnlessEqual(out, "")
471-        self.failUnless("a basedir was not provided" in err)
472+        self.failUnlessRaises(usage.UsageError,
473+                              runner.runner, argv,
474+                              run_by_human=False)
475 
476     def test_stats_gatherer(self):
477         basedir = self.workdir("test_stats_gatherer")
478}
479
480Context:
481
482[__init__.py: silence DeprecationWarning about BaseException.message globally. fixes #1129
483david-sarah@jacaranda.org**20100720011939
484 Ignore-this: 38808986ba79cb2786b010504a22f89
485] 
486[test_runner: test that 'tahoe --version' outputs no noise (e.g. DeprecationWarnings).
487david-sarah@jacaranda.org**20100720011345
488 Ignore-this: dd358b7b2e5d57282cbe133e8069702e
489] 
490[TAG allmydata-tahoe-1.7.1
491zooko@zooko.com**20100719131352
492 Ignore-this: 6942056548433dc653a746703819ad8c
493] 
494Patch bundle hash:
4958eb45d45724a0668578ea30a8f2ca4699bf50008