Merge lp:~renamer-developers/renamer/config-file into lp:renamer
- config-file
- Merge into trunk
Proposed by
Jonathan Jacobs
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Tristan Seligmann | ||||
Approved revision: | 127 | ||||
Merged at revision: | 87 | ||||
Proposed branch: | lp:~renamer-developers/renamer/config-file | ||||
Merge into: | lp:renamer | ||||
Prerequisite: | lp:~renamer-developers/renamer/undo-command | ||||
Diff against target: |
731 lines (+435/-47) 11 files modified
renamer/_version.py (+1/-1) renamer/application.py (+74/-25) renamer/config.py (+132/-0) renamer/irenamer.py (+7/-3) renamer/plugin.py (+4/-4) renamer/plugins/undo.py (+11/-13) renamer/test/data/test.conf (+8/-0) renamer/test/test_actions.py (+1/-1) renamer/test/test_config.py (+127/-0) renamer/test/test_util.py (+50/-0) renamer/util.py (+20/-0) |
||||
To merge this branch: | bzr merge lp:~renamer-developers/renamer/config-file | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Tristan Seligmann | Approve | ||
Review via email:
|
Commit message
Description of the change
To post a comment you must log in.
- 126. By Jonathan Jacobs
-
Tweak function name.
- 127. By Jonathan Jacobs
-
Merge trunk.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'renamer/_version.py' |
2 | --- renamer/_version.py 2009-04-29 00:06:49 +0000 |
3 | +++ renamer/_version.py 2010-10-24 17:47:46 +0000 |
4 | @@ -1,3 +1,3 @@ |
5 | from twisted.python.versions import Version |
6 | |
7 | -version = Version('Renamer', 1, 0, 0) |
8 | +version = Version('Renamer', 0, 1, 0) |
9 | |
10 | === modified file 'renamer/application.py' |
11 | --- renamer/application.py 2010-10-20 00:38:33 +0000 |
12 | +++ renamer/application.py 2010-10-24 17:47:46 +0000 |
13 | @@ -9,10 +9,10 @@ |
14 | from axiom.store import Store |
15 | |
16 | from twisted.internet import defer |
17 | -from twisted.python import usage, log |
18 | +from twisted.python import usage, log, versions |
19 | from twisted.python.filepath import FilePath |
20 | |
21 | -from renamer import logging, plugin, util |
22 | +from renamer import config, logging, plugin, util, version |
23 | from renamer.irenamer import IRenamingCommand |
24 | from renamer.history import History |
25 | |
26 | @@ -28,12 +28,14 @@ |
27 | |
28 | |
29 | optParameters = [ |
30 | + ('config', 'c', '~/.renamer/renamer.conf', |
31 | + 'Configuration file path.'), |
32 | ('name', 'e', None, |
33 | 'Formatted filename.', string.Template), |
34 | ('prefix', 'p', None, |
35 | 'Formatted path to prefix to files before renaming.', string.Template), |
36 | ('concurrent', 'l', 10, |
37 | - 'Maximum number of concurrent tasks to perform at a time.', int)] |
38 | + 'Maximum number of asynchronous tasks to perform concurrently.', int)] |
39 | |
40 | |
41 | @property |
42 | @@ -42,14 +44,19 @@ |
43 | plugin.getRenamingCommands(), plugin.getCommands()) |
44 | for plg in commands: |
45 | try: |
46 | - yield plg.name, None, plg, plg.description |
47 | + yield ( |
48 | + plg.name, |
49 | + None, |
50 | + config.defaultsFromConfigFactory(self.config, plg), |
51 | + plg.description) |
52 | except AttributeError: |
53 | raise RuntimeError('Malformed plugin: %r' % (plg,)) |
54 | |
55 | |
56 | - def __init__(self): |
57 | - usage.Options.__init__(self) |
58 | + def __init__(self, config): |
59 | + super(Options, self).__init__() |
60 | self['verbosity'] = 1 |
61 | + self.config = config |
62 | |
63 | |
64 | @property |
65 | @@ -76,6 +83,14 @@ |
66 | opt_q = opt_quiet |
67 | |
68 | |
69 | + def opt_version(self): |
70 | + """ |
71 | + Display version information. |
72 | + """ |
73 | + print versions.getVersionString(version) |
74 | + sys.exit(0) |
75 | + |
76 | + |
77 | def parseArgs(self, *args): |
78 | args = (self.decodeCommandLine(arg) for arg in args) |
79 | if self['glob']: |
80 | @@ -101,24 +116,53 @@ |
81 | @ivar command: Renamer command being executed. |
82 | """ |
83 | def __init__(self): |
84 | - obs = logging.RenamerObserver() |
85 | - log.startLoggingWithObserver(obs.emit, setStdout=False) |
86 | + self._obs = logging.RenamerObserver() |
87 | + log.startLoggingWithObserver(self._obs.emit, setStdout=False) |
88 | |
89 | + self.options = self.parseOptions() |
90 | self.store = Store(os.path.expanduser('~/.renamer/renamer.axiom')) |
91 | # XXX: One day there might be more than one History item. |
92 | self.history = self.store.findOrCreate(History) |
93 | |
94 | - self.options = Options() |
95 | - self.options.parseOptions() |
96 | - obs.verbosity = self.options['verbosity'] |
97 | - self.command = self.getCommand() |
98 | - |
99 | - |
100 | - def getCommand(self): |
101 | + self.args = getattr(self.options, 'args', []) |
102 | + self.command = self.getCommand(self.options) |
103 | + |
104 | + |
105 | + def parseOptions(self): |
106 | + """ |
107 | + Parse configuration file and command-line options. |
108 | + """ |
109 | + _options = Options({}) |
110 | + _options.parseOptions() |
111 | + self._obs.verbosity = _options['verbosity'] |
112 | + |
113 | + self._configFile = config.ConfigFile( |
114 | + FilePath(os.path.expanduser(_options['config']))) |
115 | + command = self.getCommand(_options) |
116 | + |
117 | + options = Options(self._configFile) |
118 | + # Apply global defaults. |
119 | + options.update(self._configFile.get('renamer', options)) |
120 | + # Apply command-specific overrides for the global config. |
121 | + options.update( |
122 | + (k, v) for k, v in |
123 | + self._configFile.get(command.name, options).iteritems() |
124 | + if k in options) |
125 | + # Command-line options trump the config file. |
126 | + options.parseOptions() |
127 | + |
128 | + logging.msg( |
129 | + 'Global options: %r' % (options,), |
130 | + verbosity=5) |
131 | + |
132 | + return options |
133 | + |
134 | + |
135 | + def getCommand(self, options): |
136 | """ |
137 | Get the L{twisted.python.usage.Options} command that was invoked. |
138 | """ |
139 | - command = getattr(self.options, 'subOptions', None) |
140 | + command = getattr(options, 'subOptions', None) |
141 | if command is None: |
142 | raise usage.UsageError('At least one command must be specified') |
143 | |
144 | @@ -132,8 +176,7 @@ |
145 | """ |
146 | Perform a file rename. |
147 | """ |
148 | - options = self.options |
149 | - if options['no-act']: |
150 | + if self.options['no-act']: |
151 | logging.msg('Simulating: %s => %s' % (src.path, dst.path)) |
152 | return |
153 | |
154 | @@ -141,25 +184,31 @@ |
155 | logging.msg('Skipping noop "%s"' % (src.path,), verbosity=2) |
156 | return |
157 | |
158 | - if options['link-dst']: |
159 | + if self.options['link-dst']: |
160 | self.changeset.do( |
161 | self.changeset.newAction(u'symlink', src, dst), |
162 | - options) |
163 | + self.options) |
164 | else: |
165 | self.changeset.do( |
166 | self.changeset.newAction(u'move', src, dst), |
167 | - options) |
168 | - if options['link-src']: |
169 | + self.options) |
170 | + if self.options['link-src']: |
171 | self.changeset.do( |
172 | self.changeset.newAction(u'symlink', dst, src), |
173 | - options) |
174 | + self.options) |
175 | |
176 | |
177 | def runCommand(self, command): |
178 | """ |
179 | Run a generic command. |
180 | """ |
181 | - return defer.maybeDeferred(command.process, self) |
182 | + logging.msg( |
183 | + 'Using command "%s"' % (command.name,), |
184 | + verbosity=4) |
185 | + logging.msg( |
186 | + 'Command options: %r' % (command,), |
187 | + verbosity=5) |
188 | + return defer.maybeDeferred(command.process, self, self.options) |
189 | |
190 | |
191 | def runRenamingCommand(self, command): |
192 | @@ -178,7 +227,7 @@ |
193 | self.options['concurrent'],), |
194 | verbosity=3) |
195 | return util.parallel( |
196 | - self.options.args, self.options['concurrent'], _processOne) |
197 | + self.args, self.options['concurrent'], _processOne) |
198 | |
199 | |
200 | def run(self): |
201 | |
202 | === added file 'renamer/config.py' |
203 | --- renamer/config.py 1970-01-01 00:00:00 +0000 |
204 | +++ renamer/config.py 2010-10-24 17:47:46 +0000 |
205 | @@ -0,0 +1,132 @@ |
206 | +from ConfigParser import SafeConfigParser |
207 | + |
208 | +from renamer import util |
209 | + |
210 | + |
211 | + |
212 | +_identity = lambda x: x |
213 | + |
214 | + |
215 | + |
216 | +def flag(value): |
217 | + """ |
218 | + Transform the flag C{value} into a C{bool}. |
219 | + """ |
220 | + if isinstance(value, str): |
221 | + value = unicode(value, 'ascii') |
222 | + if isinstance(value, unicode): |
223 | + return value.lower() in (u'true', u'yes', u'1') |
224 | + return bool(value) |
225 | + |
226 | + |
227 | + |
228 | +def transformersFromOptions(options): |
229 | + """ |
230 | + Extract option transformers from an C{Options} subclass. |
231 | + |
232 | + @rtype: C{iterable} of C{(str, callable)} |
233 | + @return: Iterable of C{(longOption, transformer)}. |
234 | + """ |
235 | + flags = getattr(options, 'optFlags', []) |
236 | + for long, short, desc in flags: |
237 | + yield long, flag |
238 | + |
239 | + parameters = getattr(options, 'optParameters', []) |
240 | + parameters = (util.padIterable(ps, _identity, 5) for ps in parameters) |
241 | + global _identity |
242 | + for long, short, default, desc, coerce in parameters: |
243 | + if coerce is None: |
244 | + coerce = _identity |
245 | + yield long, coerce |
246 | + |
247 | + |
248 | + |
249 | +def defaultsFromConfigFactory(config, commandClass): |
250 | + """ |
251 | + Create a factory function that will create an C{ICommand} provider instance |
252 | + and apply defaults from a config file. |
253 | + |
254 | + @type config: L{renamer.config.ConfigFile} |
255 | + @param config: Config file to use defaults from. |
256 | + |
257 | + @type commandClass: C{type} |
258 | + @param commandClass: L{renamer.irenamer.ICommand} provider to apply config |
259 | + defaults to. |
260 | + |
261 | + @return: C{callable} suitable for passing as the third (parser class) |
262 | + argument to C{"subCommands"}. |
263 | + """ |
264 | + def initWrapper(): |
265 | + conf = config.get(commandClass.name, {}) |
266 | + |
267 | + def _params(): |
268 | + parameters = getattr(commandClass, 'optParameters', []) |
269 | + parameters = (util.padIterable(p, None, 5) for p in parameters) |
270 | + for long, short, default, desc, coerce in parameters: |
271 | + if long in conf: |
272 | + default = conf.get(long) |
273 | + if default is not None and coerce is not None: |
274 | + default = coerce(default) |
275 | + yield long, short, default, desc, coerce |
276 | + |
277 | + commandClass.optParameters = list(_params()) |
278 | + cmd = commandClass() |
279 | + |
280 | + # Since we can't set defaults for flags, we have to apply the |
281 | + # defaults after __init__ and before parseOptions. |
282 | + flags = getattr(commandClass, 'optFlags', []) |
283 | + for long, short, desc in flags: |
284 | + if long in conf: |
285 | + cmd[long] = flag(conf[long]) |
286 | + |
287 | + return cmd |
288 | + |
289 | + return initWrapper |
290 | + |
291 | + |
292 | + |
293 | +class ConfigFile(object): |
294 | + """ |
295 | + Configuration file. |
296 | + |
297 | + @type sections: C{dict} mapping C{str} to C{dict} mapping C{str} to C{str}. |
298 | + @ivar sections: Mapping of section names to mappings of configuration |
299 | + options to values. |
300 | + """ |
301 | + def __init__(self, path): |
302 | + config = SafeConfigParser() |
303 | + if path.exists(): |
304 | + fd = path.open() |
305 | + config.readfp(fd) |
306 | + fd.close() |
307 | + |
308 | + self.sections = {} |
309 | + for section in config.sections(): |
310 | + options = self.sections.setdefault(section, {}) |
311 | + for option in config.options(section): |
312 | + options[option] = unicode(config.get(section, option)) |
313 | + |
314 | + |
315 | + def get(self, section, options=None): |
316 | + """ |
317 | + Get and coerce configuration values. |
318 | + |
319 | + @type section: C{str} |
320 | + @param section: Section name to retrieve configuration values for. |
321 | + |
322 | + @type options: L{twisted.python.usage.Options} |
323 | + @param options: Options to use for determining coercion types, or |
324 | + C{None} to perform no coercions. |
325 | + |
326 | + @return: Mapping of configuration option names to their coerced values. |
327 | + """ |
328 | + section = self.sections.get(section, {}) |
329 | + if options is not None: |
330 | + for opt, optType in transformersFromOptions(options): |
331 | + value = section.get(opt) |
332 | + if value is not None: |
333 | + if optType is not None: |
334 | + value = optType(value) |
335 | + section[opt] = value |
336 | + |
337 | + return section |
338 | |
339 | === modified file 'renamer/irenamer.py' |
340 | --- renamer/irenamer.py 2010-10-16 16:35:10 +0000 |
341 | +++ renamer/irenamer.py 2010-10-24 17:47:46 +0000 |
342 | @@ -16,9 +16,13 @@ |
343 | """) |
344 | |
345 | |
346 | - def process(renamer): |
347 | + def process(renamer, options): |
348 | """ |
349 | Called once command line parsing is complete. |
350 | + |
351 | + @type renamer: L{renamer.application.Renamer} |
352 | + |
353 | + @type options: C{dict} |
354 | """ |
355 | |
356 | |
357 | @@ -67,7 +71,7 @@ |
358 | """ |
359 | Perform the action. |
360 | |
361 | - @type options: L{twisted.python.usage.Options} |
362 | + @type options: C{dict} |
363 | """ |
364 | |
365 | |
366 | @@ -75,5 +79,5 @@ |
367 | """ |
368 | Perform the reverse action. |
369 | |
370 | - @type options: L{twisted.python.usage.Options} |
371 | + @type options: C{dict} |
372 | """ |
373 | |
374 | === modified file 'renamer/plugin.py' |
375 | --- renamer/plugin.py 2010-10-16 16:35:10 +0000 |
376 | +++ renamer/plugin.py 2010-10-24 17:47:46 +0000 |
377 | @@ -72,7 +72,7 @@ |
378 | |
379 | # ICommand |
380 | |
381 | - def process(self, renamer): |
382 | + def process(self, renamer, options): |
383 | raise NotImplementedError('Commands must implement "process"') |
384 | |
385 | |
386 | @@ -137,7 +137,7 @@ |
387 | @param mapping: Mapping of template variables, used for template |
388 | substitution. |
389 | |
390 | - @type options: L{twisted.python.usage.Options} |
391 | + @type options: C{dict} |
392 | |
393 | @type src: L{twisted.python.filepath.FilePath} |
394 | @param src: Source path. |
395 | @@ -179,12 +179,12 @@ |
396 | |
397 | # ICommand |
398 | |
399 | - def process(self, renamer): |
400 | + def process(self, renamer, options): |
401 | arg = renamer.currentArgument |
402 | logging.msg('Processing "%s"' % (arg.path,), |
403 | verbosity=3) |
404 | d = defer.maybeDeferred(self.processArgument, arg) |
405 | - d.addCallback(self.buildDestination, renamer.options, arg) |
406 | + d.addCallback(self.buildDestination, options, arg) |
407 | return d |
408 | |
409 | |
410 | |
411 | === modified file 'renamer/plugins/undo.py' |
412 | --- renamer/plugins/undo.py 2010-10-18 15:12:54 +0000 |
413 | +++ renamer/plugins/undo.py 2010-10-24 17:47:46 +0000 |
414 | @@ -30,19 +30,19 @@ |
415 | ('ignore-errors', None, 'Do not stop the process when encountering OS errors')] |
416 | |
417 | |
418 | - def undoActions(self, renamer, changeset, actions): |
419 | + def undoActions(self, options, changeset, actions): |
420 | """ |
421 | Undo specific actions from a changeset. |
422 | """ |
423 | for action in actions: |
424 | msg = 'Simulating undo' |
425 | - if not renamer.options['no-act']: |
426 | + if not options['no-act']: |
427 | msg = 'Undo' |
428 | |
429 | logging.msg('%s: %s' % (msg, action.asHumanly()), verbosity=3) |
430 | - if not renamer.options['no-act']: |
431 | + if not options['no-act']: |
432 | try: |
433 | - changeset.undo(action, renamer.options) |
434 | + changeset.undo(action, options) |
435 | except OSError, e: |
436 | if not self['ignore-errors']: |
437 | raise e |
438 | @@ -67,10 +67,9 @@ |
439 | self['action'] = action |
440 | |
441 | |
442 | - def process(self, renamer): |
443 | + def process(self, renamer, options): |
444 | action = getItem(renamer.store, self['action'], Action) |
445 | - self.undoActions( |
446 | - renamer, action.changeset, [action]) |
447 | + self.undoActions(options, action.changeset, [action]) |
448 | |
449 | |
450 | |
451 | @@ -90,13 +89,12 @@ |
452 | self['changeset'] = changeset |
453 | |
454 | |
455 | - def process(self, renamer): |
456 | + def process(self, renamer, options): |
457 | changeset = getItem(renamer.store, self['changeset'], Changeset) |
458 | logging.msg('Undoing: %s' % (changeset.asHumanly(),), |
459 | verbosity=3) |
460 | actions = list(changeset.getActions()) |
461 | - self.undoActions( |
462 | - renamer, changeset, reversed(actions)) |
463 | + self.undoActions(options, changeset, reversed(actions)) |
464 | |
465 | |
466 | |
467 | @@ -109,7 +107,7 @@ |
468 | """ |
469 | |
470 | |
471 | - def process(self, renamer): |
472 | + def process(self, renamer, options): |
473 | changesets = list(renamer.history.getChangesets()) |
474 | for cs in changesets: |
475 | print 'Changeset ID=%d: %s' % (cs.storeID, cs.asHumanly()) |
476 | @@ -139,9 +137,9 @@ |
477 | self['identifier'] = identifier |
478 | |
479 | |
480 | - def process(self, renamer): |
481 | + def process(self, renamer, options): |
482 | item = getItem(renamer.store, self['identifier'], (Action, Changeset)) |
483 | - if not renamer.options['no-act']: |
484 | + if not options['no-act']: |
485 | logging.msg('Forgetting: %s' % (item.asHumanly(),), verbosity=2) |
486 | item.deleteFromStore() |
487 | |
488 | |
489 | === added file 'renamer/test/data/test.conf' |
490 | --- renamer/test/data/test.conf 1970-01-01 00:00:00 +0000 |
491 | +++ renamer/test/data/test.conf 2010-10-24 17:47:46 +0000 |
492 | @@ -0,0 +1,8 @@ |
493 | +[foo] |
494 | +bar=1 |
495 | +baz=apple |
496 | + |
497 | +[test] |
498 | +aardvark=hello |
499 | +bobcat=3 |
500 | +donut=no |
501 | |
502 | === modified file 'renamer/test/test_actions.py' |
503 | --- renamer/test/test_actions.py 2010-10-16 20:46:40 +0000 |
504 | +++ renamer/test/test_actions.py 2010-10-24 17:47:46 +0000 |
505 | @@ -14,7 +14,7 @@ |
506 | def setUp(self): |
507 | self.path = FilePath(self.mktemp()) |
508 | self.path.makedirs() |
509 | - self.options = Options() |
510 | + self.options = Options(None) |
511 | self.src, self.dst = self.createFiles() |
512 | |
513 | |
514 | |
515 | === added file 'renamer/test/test_config.py' |
516 | --- renamer/test/test_config.py 1970-01-01 00:00:00 +0000 |
517 | +++ renamer/test/test_config.py 2010-10-24 17:47:46 +0000 |
518 | @@ -0,0 +1,127 @@ |
519 | +from twisted.python.filepath import FilePath |
520 | +from twisted.trial.unittest import TestCase |
521 | + |
522 | +from renamer import config, plugin |
523 | + |
524 | + |
525 | + |
526 | +class TestCommand(plugin.Command): |
527 | + name = 'test' |
528 | + |
529 | + |
530 | + optParameters = [ |
531 | + ('aardvark', 'a', None, 'desc'), |
532 | + ('bobcat', 'b', 5, 'desc', int), |
533 | + ('chocolate', 'c', None, 'desc')] |
534 | + |
535 | + |
536 | + optFlags = [ |
537 | + ('donut', 'd', 'desc')] |
538 | + |
539 | + |
540 | + |
541 | +class ConfigTests(TestCase): |
542 | + """ |
543 | + Tests for L{renamer.config}. |
544 | + """ |
545 | + def setUp(self): |
546 | + path = FilePath(__file__).sibling('data').child('test.conf') |
547 | + self.config = config.ConfigFile(path) |
548 | + |
549 | + |
550 | + def test_flag(self): |
551 | + """ |
552 | + L{renamer.config.flag} returns C{True} for true flag values such as |
553 | + C{'yes'}, C{'true'}, C{'1'} and otherwise C{False}. |
554 | + """ |
555 | + truths = [ |
556 | + 'yes', u'yes', 'YES', u'true', '1', 1] |
557 | + for value in truths: |
558 | + self.assertTrue(config.flag(value)) |
559 | + |
560 | + falsehoods = [ |
561 | + 'no', u'NO', 'arst', 0, []] |
562 | + for value in falsehoods: |
563 | + self.assertFalse(config.flag(value)) |
564 | + |
565 | + |
566 | + def test_transformersFromOptions(self): |
567 | + """ |
568 | + L{renamer.config.transformersFromOptions} extracts coercion functions |
569 | + from a L{renamer.irenamer.ICommand} provider. |
570 | + """ |
571 | + expected = { |
572 | + 'aardvark': config._identity, |
573 | + 'bobcat': int, |
574 | + 'chocolate': config._identity, |
575 | + 'donut': config.flag} |
576 | + res = dict(config.transformersFromOptions(TestCommand())) |
577 | + self.assertEquals(expected, res) |
578 | + |
579 | + |
580 | + def test_defaultsFromConfigFactory(self): |
581 | + """ |
582 | + L{renamer.config.defaultsFromConfigFactory} creates a wrapper function |
583 | + around a L{renamer.irenamer.ICommand} that, when called, instantiates |
584 | + the L{renamer.irenamer.ICommand} and sets default values from a config |
585 | + file from a section with a name matched |
586 | + L{renamer.irenamer.ICommand.name}. Options that are explicitly provided |
587 | + to the command trump values from the config file. |
588 | + """ |
589 | + wrapper = config.defaultsFromConfigFactory(self.config, TestCommand) |
590 | + cmd = wrapper() |
591 | + cmd.parseOptions([]) |
592 | + self.assertEquals(cmd['aardvark'], u'hello') |
593 | + self.assertEquals(cmd['bobcat'], 3) |
594 | + self.assertIdentical(cmd['chocolate'], None) |
595 | + self.assertEquals(cmd['donut'], False) |
596 | + |
597 | + cmd = wrapper() |
598 | + cmd.parseOptions(['--bobcat=7', '--donut']) |
599 | + self.assertEquals(cmd['aardvark'], u'hello') |
600 | + self.assertEquals(cmd['bobcat'], 7) |
601 | + self.assertIdentical(cmd['chocolate'], None) |
602 | + self.assertEquals(cmd['donut'], True) |
603 | + |
604 | + |
605 | + |
606 | +class ConfigFileTests(TestCase): |
607 | + """ |
608 | + Tests for L{renamer.config.ConfigFile}. |
609 | + """ |
610 | + def setUp(self): |
611 | + path = FilePath(__file__).sibling('data').child('test.conf') |
612 | + self.config = config.ConfigFile(path) |
613 | + |
614 | + |
615 | + def test_nonexistentConfig(self): |
616 | + """ |
617 | + Specifying a nonexistent path to L{renamer.config.ConfigFile} results |
618 | + in an empty config object. |
619 | + """ |
620 | + conf = config.ConfigFile(FilePath('no_such_config')) |
621 | + self.assertEquals(conf.sections, {}) |
622 | + |
623 | + |
624 | + def test_getNoCoercion(self): |
625 | + """ |
626 | + Getting a config section without specifying an C{'options'} argument |
627 | + simply returns a mapping of strings to strings. |
628 | + """ |
629 | + expected = { |
630 | + 'bar': u'1', |
631 | + 'baz': u'apple'} |
632 | + self.assertEquals(expected, self.config.get('foo')) |
633 | + |
634 | + |
635 | + def test_getCoercion(self): |
636 | + """ |
637 | + Specifying an C{'options'} argument when getting a config section will |
638 | + coerce recognized options to their corresponding type in the |
639 | + C{'options'} argument. |
640 | + """ |
641 | + expected = { |
642 | + 'aardvark': u'hello', |
643 | + 'bobcat': 3, |
644 | + 'donut': False} |
645 | + self.assertEquals(expected, self.config.get('test', TestCommand)) |
646 | |
647 | === modified file 'renamer/test/test_util.py' |
648 | --- renamer/test/test_util.py 2010-10-16 16:16:58 +0000 |
649 | +++ renamer/test/test_util.py 2010-10-24 17:47:46 +0000 |
650 | @@ -140,3 +140,53 @@ |
651 | Interfaces are not provided by subclasses. |
652 | """ |
653 | self.assertTrue(IThing.providedBy(Thing)) |
654 | + |
655 | + |
656 | + |
657 | +class PadIterableTests(TestCase): |
658 | + """ |
659 | + Tests for L{renamer.util.util.padIterable}. |
660 | + """ |
661 | + def test_needsPadding(self): |
662 | + """ |
663 | + Iterables that need padding are padded to the specified length with the |
664 | + padding value. |
665 | + """ |
666 | + padded = list(util.padIterable(xrange(3), -1, 5)) |
667 | + self.assertEquals(len(padded), 5) |
668 | + self.assertEquals(padded, [0, 1, 2, -1, -1]) |
669 | + |
670 | + |
671 | + def test_exceedsPadding(self): |
672 | + """ |
673 | + Iterables that exceed the padding amount are clipped to the specified |
674 | + length. |
675 | + """ |
676 | + padded = list(util.padIterable(xrange(10), -1, 5)) |
677 | + self.assertEquals(len(padded), 5) |
678 | + self.assertEquals(padded, [0, 1, 2, 3, 4]) |
679 | + |
680 | + |
681 | + def test_negativeCount(self): |
682 | + """ |
683 | + Negative padding counts raise C{ValueError}. |
684 | + """ |
685 | + self.assertRaises(ValueError, util.padIterable, xrange(5), -1, -1) |
686 | + |
687 | + |
688 | + def test_exact(self): |
689 | + """ |
690 | + Iterables that are the same length as the padding are not padded. |
691 | + """ |
692 | + padded = list(util.padIterable(xrange(5), -1, 5)) |
693 | + self.assertEquals(len(padded), 5) |
694 | + self.assertEquals(padded, [0, 1, 2, 3, 4]) |
695 | + |
696 | + |
697 | + def test_empty(self): |
698 | + """ |
699 | + Empty iterables are filled entirely with the padding value. |
700 | + """ |
701 | + padded = list(util.padIterable([], -1, 5)) |
702 | + self.assertEquals(len(padded), 5) |
703 | + self.assertEquals(padded, [-1, -1, -1, -1, -1]) |
704 | |
705 | === modified file 'renamer/util.py' |
706 | --- renamer/util.py 2010-10-24 11:06:45 +0000 |
707 | +++ renamer/util.py 2010-10-24 17:47:46 +0000 |
708 | @@ -97,3 +97,23 @@ |
709 | if platform == 'win32': |
710 | globbing = _iglobWin32 |
711 | return _glob(globbing) |
712 | + |
713 | + |
714 | + |
715 | +def padIterable(iterable, padding, count): |
716 | + """ |
717 | + Pad C{iterable}, with C{padding}, to C{count} elements. |
718 | + |
719 | + Iterables containing more than C{count} elements are clipped to C{count} |
720 | + elements. |
721 | + |
722 | + @param iterable: The iterable to iterate. |
723 | + |
724 | + @param padding: Padding object. |
725 | + |
726 | + @param count: The padded length. |
727 | + |
728 | + @return: An iterable. |
729 | + """ |
730 | + return itertools.islice( |
731 | + itertools.chain(iterable, itertools.repeat(padding)), count) |
> 36 ('concurrent', 'l', 10,
I think a better long name for this option might be "concurrency".