[merge] 0.61 is stable

authorAurelien Campeas <aurelien.campeas@logilab.fr>
changesetf5ad02c22df0
branchstable
phasepublic
hiddenno
parent revision#03abf06e88d7 update Changelog, #1150609fa17d Added tag logilab-common-version-0.61.0, logilab-common-debian-version-0.61.0-1, logilab-common-centos-version-0.61.0-1 for changeset 56b1d20168f8
child revision#a89e548d2187 default was stable, #8f32856bba64 [pytest] drop coverage option, broken for a while
files modified by this revision
.hgtags
ChangeLog
README
__pkginfo__.py
bin/pytest
configuration.py
date.py
debian.lenny/python-logilab-common.preinst
debian/changelog
deprecation.py
graph.py
modutils.py
optik_ext.py
pdf_ext.py
python-logilab-common.spec
setup.py
shellutils.py
table.py
tasksqueue.py
test/unittest_configuration.py
test/unittest_date.py
test/unittest_deprecation.py
testlib.py
# HG changeset patch
# User Aurelien Campeas <aurelien.campeas@logilab.fr>
# Date 1396358802 -7200
# Tue Apr 01 15:26:42 2014 +0200
# Branch stable
# Node ID f5ad02c22df04b74023b8b34cbfe017f570e63c9
# Parent 03abf06e88d7a880a65653b53a0e7b704e42976e
# Parent 1150609fa17db66ddf6efba542fe1dfdcec48d28
[merge] 0.61 is stable

diff --git a/.hgtags b/.hgtags
@@ -131,5 +131,12 @@
1  a93679c86bfd724bea7aeff098af51c18b6234c4 logilab-common-version-0.59.0
2  60a9c75910c3e54bd99bf66ab82dcb82fbdf68b8 logilab-common-debian-version-0.59.0
3  d71a70531f517481d4fd8ef97fe0dbcc7e5467ef logilab-common-centos-version-0.59.0-1
4  271abd1bc556f3fbbd8d9531894b2af617bec146 logilab-common-version-0.59.1
5  787675c645b27390c74d83825fecadbc18595e84 logilab-common-debian-version-0.59.1-1
6 +1ad9bfc058cf19eb453ff558415bc62c1ca158e1 logilab-common-version-0.60.0
7 +5723c613242e2232d583a184e425566793829a8d logilab-common-debian-version-0.60.0-1
8 +f52d0719609cb5eda9431f4cccfd307f9e5485a1 logilab-common-version-0.60.1
9 +368d5403b82bda6055d3871bac0b594c5d2593d2 logilab-common-debian-version-0.60.1-1
10 +56b1d20168f8fb7e4b599b84d4a82dd26c1e9ed2 logilab-common-version-0.61.0
11 +56b1d20168f8fb7e4b599b84d4a82dd26c1e9ed2 logilab-common-debian-version-0.61.0-1
12 +56b1d20168f8fb7e4b599b84d4a82dd26c1e9ed2 logilab-common-centos-version-0.61.0-1
diff --git a/ChangeLog b/ChangeLog
@@ -1,55 +1,87 @@
13  ChangeLog for logilab.common
14  ============================
15 
diff --git a/README b/README
@@ -121,12 +121,10 @@
16 
17  * `corbautils`, useful functions for use with the OmniORB_ CORBA library.
18 
19  * `hg`, some Mercurial_ utility functions.
20 
21 -* `pdf_ext`, pdf and fdf file manipulations, with pdftk.
22 -
23  * `pyro_ext`, some Pyro_ utility functions.
24 
25  * `sphinx_ext`, Sphinx_ plugin defining a `autodocstring` directive.
26 
27  * `vcgutils` , utilities functions to generate file readable with Georg Sander's
diff --git a/__pkginfo__.py b/__pkginfo__.py
@@ -23,11 +23,11 @@
28  distname = 'logilab-common'
29  modname = 'common'
30  subpackage_of = 'logilab'
31  subpackage_master = True
32 
33 -numversion = (0, 59, 1)
34 +numversion = (0, 61, 0)
35  version = '.'.join([str(num) for num in numversion])
36 
37  license = 'LGPL' # 2.1 or later
38  description = "collection of low-level Python packages and modules used by Logilab projects"
39  web = "http://www.logilab.org/project/%s" % distname
diff --git a/bin/pytest b/bin/pytest
@@ -1,6 +1,6 @@
40 -#!/usr/bin/python -u
41 +#!/usr/bin/env python
42 
43  import warnings
44  warnings.simplefilter('default', DeprecationWarning)
45 
46  from logilab.common.pytest import run
diff --git a/configuration.py b/configuration.py
@@ -112,15 +112,15 @@
47  from ConfigParser import ConfigParser, NoOptionError, NoSectionError, \
48       DuplicateSectionError
49  from warnings import warn
50 
51  from logilab.common.compat import callable, raw_input, str_encode as _encode
52 -
53 +from logilab.common.deprecation import deprecated
54  from logilab.common.textutils import normalize_text, unquote
55 -from logilab.common import optik_ext as optparse
56 +from logilab.common import optik_ext
57 
58 -OptionError = optparse.OptionError
59 +OptionError = optik_ext.OptionError
60 
61  REQUIRED = []
62 
63  class UnsupportedAction(Exception):
64      """raised by set_option when it doesn't know what to do for an action"""
@@ -134,67 +134,70 @@
65      return encoding
66 
67 
68  # validation functions ########################################################
69 
70 +# validators will return the validated value or raise optparse.OptionValueError
71 +# XXX add to documentation
72 +
73  def choice_validator(optdict, name, value):
74      """validate and return a converted value for option of type 'choice'
75      """
76      if not value in optdict['choices']:
77          msg = "option %s: invalid value: %r, should be in %s"
78 -        raise optparse.OptionValueError(msg % (name, value, optdict['choices']))
79 +        raise optik_ext.OptionValueError(msg % (name, value, optdict['choices']))
80      return value
81 
82  def multiple_choice_validator(optdict, name, value):
83      """validate and return a converted value for option of type 'choice'
84      """
85      choices = optdict['choices']
86 -    values = optparse.check_csv(None, name, value)
87 +    values = optik_ext.check_csv(None, name, value)
88      for value in values:
89          if not value in choices:
90              msg = "option %s: invalid value: %r, should be in %s"
91 -            raise optparse.OptionValueError(msg % (name, value, choices))
92 +            raise optik_ext.OptionValueError(msg % (name, value, choices))
93      return values
94 
95  def csv_validator(optdict, name, value):
96      """validate and return a converted value for option of type 'csv'
97      """
98 -    return optparse.check_csv(None, name, value)
99 +    return optik_ext.check_csv(None, name, value)
100 
101  def yn_validator(optdict, name, value):
102      """validate and return a converted value for option of type 'yn'
103      """
104 -    return optparse.check_yn(None, name, value)
105 +    return optik_ext.check_yn(None, name, value)
106 
107  def named_validator(optdict, name, value):
108      """validate and return a converted value for option of type 'named'
109      """
110 -    return optparse.check_named(None, name, value)
111 +    return optik_ext.check_named(None, name, value)
112 
113  def file_validator(optdict, name, value):
114      """validate and return a filepath for option of type 'file'"""
115 -    return optparse.check_file(None, name, value)
116 +    return optik_ext.check_file(None, name, value)
117 
118  def color_validator(optdict, name, value):
119      """validate and return a valid color for option of type 'color'"""
120 -    return optparse.check_color(None, name, value)
121 +    return optik_ext.check_color(None, name, value)
122 
123  def password_validator(optdict, name, value):
124      """validate and return a string for option of type 'password'"""
125 -    return optparse.check_password(None, name, value)
126 +    return optik_ext.check_password(None, name, value)
127 
128  def date_validator(optdict, name, value):
129      """validate and return a mx DateTime object for option of type 'date'"""
130 -    return optparse.check_date(None, name, value)
131 +    return optik_ext.check_date(None, name, value)
132 
133  def time_validator(optdict, name, value):
134      """validate and return a time object for option of type 'time'"""
135 -    return optparse.check_time(None, name, value)
136 +    return optik_ext.check_time(None, name, value)
137 
138  def bytes_validator(optdict, name, value):
139      """validate and return an integer for option of type 'bytes'"""
140 -    return optparse.check_bytes(None, name, value)
141 +    return optik_ext.check_bytes(None, name, value)
142 
143 
144  VALIDATORS = {'string': unquote,
145                'int': int,
146                'float': float,
@@ -220,18 +223,22 @@
147      try:
148          return VALIDATORS[opttype](optdict, option, value)
149      except TypeError:
150          try:
151              return VALIDATORS[opttype](value)
152 -        except optparse.OptionValueError:
153 +        except optik_ext.OptionValueError:
154              raise
155          except:
156 -            raise optparse.OptionValueError('%s value (%r) should be of type %s' %
157 +            raise optik_ext.OptionValueError('%s value (%r) should be of type %s' %
158                                     (option, value, opttype))
159 
160  # user input functions ########################################################
161 
162 +# user input functions will ask the user for input on stdin then validate
163 +# the result and return the validated value or raise optparse.OptionValueError
164 +# XXX add to documentation
165 +
166  def input_password(optdict, question='password:'):
167      from getpass import getpass
168      while True:
169          value = getpass(question)
170          value2 = getpass('confirm: ')
@@ -249,11 +256,11 @@
171              value = raw_input(question)
172              if not value.strip():
173                  return None
174              try:
175                  return _call_validator(opttype, optdict, None, value)
176 -            except optparse.OptionValueError, ex:
177 +            except optik_ext.OptionValueError, ex:
178                  msg = str(ex).split(':', 1)[-1].strip()
179                  print 'bad value: %s' % msg
180      return input_validator
181 
182  INPUT_FUNCTIONS = {
@@ -262,10 +269,12 @@
183      }
184 
185  for opttype in VALIDATORS.keys():
186      INPUT_FUNCTIONS.setdefault(opttype, _make_input_function(opttype))
187 
188 +# utility functions ############################################################
189 +
190  def expand_default(self, option):
191      """monkey patch OptionParser.expand_default since we have a particular
192      way to handle defaults to avoid overriding values in the configuration
193      file
194      """
@@ -276,29 +285,32 @@
195          provider = self.parser.options_manager._all_options[optname]
196      except KeyError:
197          value = None
198      else:
199          optdict = provider.get_option_def(optname)
200 -        optname = provider.option_name(optname, optdict)
201 +        optname = provider.option_attrname(optname, optdict)
202          value = getattr(provider.config, optname, optdict)
203          value = format_option_value(optdict, value)
204 -    if value is optparse.NO_DEFAULT or not value:
205 +    if value is optik_ext.NO_DEFAULT or not value:
206          value = self.NO_DEFAULT_VALUE
207      return option.help.replace(self.default_tag, str(value))
208 
209 
210 -def convert(value, optdict, name=''):
211 +def _validate(value, optdict, name=''):
212      """return a validated value for an option according to its type
213 
214      optional argument name is only used for error message formatting
215      """
216      try:
217          _type = optdict['type']
218      except KeyError:
219          # FIXME
220          return value
221      return _call_validator(_type, optdict, name, value)
222 +convert = deprecated('[0.60] convert() was renamed _validate()')(_validate)
223 +
224 +# format and output functions ##################################################
225 
226  def comment(string):
227      """return string as a comment"""
228      lines = [line.strip() for line in string.splitlines()]
229      return '# ' + ('%s# ' % os.linesep).join(lines)
@@ -399,10 +411,11 @@
230          if value:
231              value = _encode(format_option_value(optdict, value), encoding)
232              print >> stream, ''
233              print >> stream, '  Default: ``%s``' % value.replace("`` ", "```` ``")
234 
235 +# Options Manager ##############################################################
236 
237  class OptionsManagerMixIn(object):
238      """MixIn to handle a configuration from both a configuration file and
239      command line options
240      """
@@ -423,11 +436,11 @@
241 
242      def reset_parsers(self, usage='', version=None):
243          # configuration file parser
244          self.cfgfile_parser = ConfigParser()
245          # command line parser
246 -        self.cmdline_parser = optparse.OptionParser(usage=usage, version=version)
247 +        self.cmdline_parser = optik_ext.OptionParser(usage=usage, version=version)
248          self.cmdline_parser.options_manager = self
249          self._optik_option_attrs = set(self.cmdline_parser.option_class.ATTRS)
250 
251      def register_options_provider(self, provider, own_group=True):
252          """register an options provider"""
@@ -459,11 +472,11 @@
253          assert options
254          # add option group to the command line parser
255          if group_name in self._mygroups:
256              group = self._mygroups[group_name]
257          else:
258 -            group = optparse.OptionGroup(self.cmdline_parser,
259 +            group = optik_ext.OptionGroup(self.cmdline_parser,
260                                           title=group_name.capitalize())
261              self.cmdline_parser.add_option_group(group)
262              group.level = provider.level
263              self._mygroups[group_name] = group
264              # add section to the config file
@@ -495,13 +508,13 @@
265              optdict['action'] = 'callback'
266              optdict['callback'] = self.cb_set_provider_option
267          # default is handled here and *must not* be given to optik if you
268          # want the whole machinery to work
269          if 'default' in optdict:
270 -            if (optparse.OPTPARSE_FORMAT_DEFAULT and 'help' in optdict and
271 -                optdict.get('default') is not None and
272 -                not optdict['action'] in ('store_true', 'store_false')):
273 +            if ('help' in optdict
274 +                and optdict.get('default') is not None
275 +                and not optdict['action'] in ('store_true', 'store_false')):
276                  optdict['help'] += ' [current: %default]'
277              del optdict['default']
278          args = ['--' + str(opt)]
279          if 'short' in optdict:
280              self._short_options[optdict['short']] = opt
@@ -564,11 +577,11 @@
281          """write a man page for the current configuration into the given
282          stream or stdout
283          """
284          self._monkeypatch_expand_default()
285          try:
286 -            optparse.generate_manpage(self.cmdline_parser, pkginfo,
287 +            optik_ext.generate_manpage(self.cmdline_parser, pkginfo,
288                                        section, stream=stream or sys.stdout,
289                                        level=self._maxlevel)
290          finally:
291              self._unmonkeypatch_expand_default()
292 
@@ -684,30 +697,30 @@
293 
294      # help methods ############################################################
295 
296      def add_help_section(self, title, description, level=0):
297          """add a dummy option section for help purpose """
298 -        group = optparse.OptionGroup(self.cmdline_parser,
299 +        group = optik_ext.OptionGroup(self.cmdline_parser,
300                                       title=title.capitalize(),
301                                       description=description)
302          group.level = level
303          self._maxlevel = max(self._maxlevel, level)
304          self.cmdline_parser.add_option_group(group)
305 
306      def _monkeypatch_expand_default(self):
307 -        # monkey patch optparse to deal with our default values
308 +        # monkey patch optik_ext to deal with our default values
309          try:
310 -            self.__expand_default_backup = optparse.HelpFormatter.expand_default
311 -            optparse.HelpFormatter.expand_default = expand_default
312 +            self.__expand_default_backup = optik_ext.HelpFormatter.expand_default
313 +            optik_ext.HelpFormatter.expand_default = expand_default
314          except AttributeError:
315              # python < 2.4: nothing to be done
316              pass
317      def _unmonkeypatch_expand_default(self):
318          # remove monkey patch
319 -        if hasattr(optparse.HelpFormatter, 'expand_default'):
320 -            # unpatch optparse to avoid side effects
321 -            optparse.HelpFormatter.expand_default = self.__expand_default_backup
322 +        if hasattr(optik_ext.HelpFormatter, 'expand_default'):
323 +            # unpatch optik_ext to avoid side effects
324 +            optik_ext.HelpFormatter.expand_default = self.__expand_default_backup
325 
326      def help(self, level=0):
327          """return the usage string for available options """
328          self.cmdline_parser.formatter.output_level = level
329          self._monkeypatch_expand_default()
@@ -732,10 +745,11 @@
330 
331      def __call__(self, *args, **kwargs):
332          assert self._inst, 'unbound method'
333          return getattr(self._inst, self.method)(*args, **kwargs)
334 
335 +# Options Provider #############################################################
336 
337  class OptionsProviderMixIn(object):
338      """Mixin to provide options to an OptionsManager"""
339 
340      # those attributes should be overridden
@@ -743,11 +757,11 @@
341      name = 'default'
342      options = ()
343      level = 0
344 
345      def __init__(self):
346 -        self.config = optparse.Values()
347 +        self.config = optik_ext.Values()
348          for option in self.options:
349              try:
350                  option, optdict = option
351              except ValueError:
352                  raise Exception('Bad option: %r' % option)
@@ -775,45 +789,45 @@
353          default = optdict.get('default')
354          if callable(default):
355              default = default()
356          return default
357 
358 -    def option_name(self, opt, optdict=None):
359 +    def option_attrname(self, opt, optdict=None):
360          """get the config attribute corresponding to opt
361          """
362          if optdict is None:
363              optdict = self.get_option_def(opt)
364          return optdict.get('dest', opt.replace('-', '_'))
365 +    option_name = deprecated('[0.60] OptionsProviderMixIn.option_name() was renamed to option_attrname()')(option_attrname)
366 
367      def option_value(self, opt):
368          """get the current value for the given option"""
369 -        return getattr(self.config, self.option_name(opt), None)
370 +        return getattr(self.config, self.option_attrname(opt), None)
371 
372      def set_option(self, opt, value, action=None, optdict=None):
373          """method called to set an option (registered in the options list)
374          """
375 -        # print "************ setting option", opt," to value", value
376          if optdict is None:
377              optdict = self.get_option_def(opt)
378          if value is not None:
379 -            value = convert(value, optdict, opt)
380 +            value = _validate(value, optdict, opt)
381          if action is None:
382              action = optdict.get('action', 'store')
383          if optdict.get('type') == 'named': # XXX need specific handling
384 -            optname = self.option_name(opt, optdict)
385 +            optname = self.option_attrname(opt, optdict)
386              currentvalue = getattr(self.config, optname, None)
387              if currentvalue:
388                  currentvalue.update(value)
389                  value = currentvalue
390          if action == 'store':
391 -            setattr(self.config, self.option_name(opt, optdict), value)
392 +            setattr(self.config, self.option_attrname(opt, optdict), value)
393          elif action in ('store_true', 'count'):
394 -            setattr(self.config, self.option_name(opt, optdict), 0)
395 +            setattr(self.config, self.option_attrname(opt, optdict), 0)
396          elif action == 'store_false':
397 -            setattr(self.config, self.option_name(opt, optdict), 1)
398 +            setattr(self.config, self.option_attrname(opt, optdict), 1)
399          elif action == 'append':
400 -            opt = self.option_name(opt, optdict)
401 +            opt = self.option_attrname(opt, optdict)
402              _list = getattr(self.config, opt, None)
403              if _list is None:
404                  if isinstance(value, (list, tuple)):
405                      _list = value
406                  elif value is not None:
@@ -891,10 +905,11 @@
407          if options is None:
408              options = self.options
409          for optname, optdict in options:
410              yield (optname, optdict, self.option_value(optname))
411 
412 +# configuration ################################################################
413 
414  class ConfigurationMixIn(OptionsManagerMixIn, OptionsProviderMixIn):
415      """basic mixin for simple configurations which don't need the
416      manager / providers model
417      """
@@ -911,11 +926,11 @@
418                      gdef = (optdict['group'].upper(), '')
419                  except KeyError:
420                      continue
421                  if not gdef in self.option_groups:
422                      self.option_groups.append(gdef)
423 -        self.register_options_provider(self, own_group=0)
424 +        self.register_options_provider(self, own_group=False)
425 
426      def register_options(self, options):
427          """add some options to the configuration"""
428          options_by_group = {}
429          for optname, optdict in options:
@@ -930,20 +945,20 @@
430      def __iter__(self):
431          return iter(self.config.__dict__.iteritems())
432 
433      def __getitem__(self, key):
434          try:
435 -            return getattr(self.config, self.option_name(key))
436 -        except (optparse.OptionValueError, AttributeError):
437 +            return getattr(self.config, self.option_attrname(key))
438 +        except (optik_ext.OptionValueError, AttributeError):
439              raise KeyError(key)
440 
441      def __setitem__(self, key, value):
442          self.set_option(key, value)
443 
444      def get(self, key, default=None):
445          try:
446 -            return getattr(self.config, self.option_name(key))
447 +            return getattr(self.config, self.option_attrname(key))
448          except (OptionError, AttributeError):
449              return default
450 
451 
452  class Configuration(ConfigurationMixIn):
@@ -975,24 +990,25 @@
453          return getattr(self.config, key)
454 
455      def __getitem__(self, key):
456          provider = self.config._all_options[key]
457          try:
458 -            return getattr(provider.config, provider.option_name(key))
459 +            return getattr(provider.config, provider.option_attrname(key))
460          except AttributeError:
461              raise KeyError(key)
462 
463      def __setitem__(self, key, value):
464 -        self.config.global_set_option(self.config.option_name(key), value)
465 +        self.config.global_set_option(self.config.option_attrname(key), value)
466 
467      def get(self, key, default=None):
468          provider = self.config._all_options[key]
469          try:
470 -            return getattr(provider.config, provider.option_name(key))
471 +            return getattr(provider.config, provider.option_attrname(key))
472          except AttributeError:
473              return default
474 
475 +# other functions ##############################################################
476 
477  def read_old_config(newconfig, changes, configfile):
478      """initialize newconfig from a deprecated configuration file
479 
480      possible changes:
diff --git a/date.py b/date.py
@@ -186,12 +186,12 @@
481      assert not (incday and incmonth)
482      begin = todate(begin)
483      end = todate(end)
484      if incmonth:
485          while begin < end:
486 +            yield begin
487              begin = next_month(begin, incmonth)
488 -            yield begin
489      else:
490          incr = get_step(begin, incday or 1)
491          while begin < end:
492             yield begin
493             begin += incr
diff --git a/debian.lenny/python-logilab-common.preinst b/debian.lenny/python-logilab-common.preinst
@@ -1,23 +0,0 @@
494 -#!/bin/sh -e
495 -
496 -case "$1" in
497 -	install)
498 -		;;
499 -	upgrade)
500 -		pycentral pkgremove python-logilab-common 2>/dev/null || true
501 -		rm -vrf /usr/lib/$(pyversions -d)/site-packages/logilab/common
502 -		if [[ $(find /usr/lib/$(pyversions -d)/site-packages/logilab/ -maxdepth 1 -type d | wc -l) = '1' ]]; then
503 -			rm -vrf /usr/lib/$(pyversions -d)/site-packages/logilab/
504 -	   	fi
505 -		;;
506 -	abort-upgrade)
507 -		;;
508 -	*)
509 -		echo "preinst called with unknown argument '$1'" >&2
510 -		exit 1
511 -		;;
512 -esac
513 -
514 -#DEBHELPER#
515 -
516 -exit 0
diff --git a/debian/changelog b/debian/changelog
@@ -1,5 +1,23 @@
517 +logilab-common (0.61.0-1) unstable; urgency=low
518 +
519 +  * new upstream release
520 +
521 + -- Julien Cristau <julien.cristau@logilab.fr>  Tue, 11 Feb 2014 15:37:02 +0100
522 +
523 +logilab-common (0.60.1-1) unstable; urgency=low
524 +
525 +  * new upstream release
526 +
527 + -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Mon, 16 Dec 2013 12:15:51 +0100
528 +
529 +logilab-common (0.60.0-1) unstable; urgency=low
530 +
531 +  * new upstream release
532 +
533 + -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Fri, 26 Jul 2013 10:30:04 +0200
534 +
535  logilab-common (0.59.1-1) unstable; urgency=low
536 
537    * new upstream release
538 
539   -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Tue, 16 Apr 2013 13:01:04 +0200
diff --git a/deprecation.py b/deprecation.py
@@ -20,97 +20,11 @@
540  __docformat__ = "restructuredtext en"
541 
542  import sys
543  from warnings import warn
544 
545 -class class_deprecated(type):
546 -    """metaclass to print a warning on instantiation of a deprecated class"""
547 -
548 -    def __call__(cls, *args, **kwargs):
549 -        msg = getattr(cls, "__deprecation_warning__",
550 -                      "%(cls)s is deprecated") % {'cls': cls.__name__}
551 -        warn(msg, DeprecationWarning, stacklevel=2)
552 -        return type.__call__(cls, *args, **kwargs)
553 -
554 -
555 -def class_renamed(old_name, new_class, message=None):
556 -    """automatically creates a class which fires a DeprecationWarning
557 -    when instantiated.
558 -
559 -    >>> Set = class_renamed('Set', set, 'Set is now replaced by set')
560 -    >>> s = Set()
561 -    sample.py:57: DeprecationWarning: Set is now replaced by set
562 -      s = Set()
563 -    >>>
564 -    """
565 -    clsdict = {}
566 -    if message is None:
567 -        message = '%s is deprecated, use %s' % (old_name, new_class.__name__)
568 -    clsdict['__deprecation_warning__'] = message
569 -    try:
570 -        # new-style class
571 -        return class_deprecated(old_name, (new_class,), clsdict)
572 -    except (NameError, TypeError):
573 -        # old-style class
574 -        class DeprecatedClass(new_class):
575 -            """FIXME: There might be a better way to handle old/new-style class
576 -            """
577 -            def __init__(self, *args, **kwargs):
578 -                warn(message, DeprecationWarning, stacklevel=2)
579 -                new_class.__init__(self, *args, **kwargs)
580 -        return DeprecatedClass
581 -
582 -
583 -def class_moved(new_class, old_name=None, message=None):
584 -    """nice wrapper around class_renamed when a class has been moved into
585 -    another module
586 -    """
587 -    if old_name is None:
588 -        old_name = new_class.__name__
589 -    if message is None:
590 -        message = 'class %s is now available as %s.%s' % (
591 -            old_name, new_class.__module__, new_class.__name__)
592 -    return class_renamed(old_name, new_class, message)
593 -
594 -def deprecated(reason=None, stacklevel=2, name=None, doc=None):
595 -    """Decorator that raises a DeprecationWarning to print a message
596 -    when the decorated function is called.
597 -    """
598 -    def deprecated_decorator(func):
599 -        message = reason or 'The function "%s" is deprecated'
600 -        if '%s' in message:
601 -            message = message % func.func_name
602 -        def wrapped(*args, **kwargs):
603 -            warn(message, DeprecationWarning, stacklevel=stacklevel)
604 -            return func(*args, **kwargs)
605 -        try:
606 -            wrapped.__name__ = name or func.__name__
607 -        except TypeError: # readonly attribute in 2.3
608 -            pass
609 -        wrapped.__doc__ = doc or func.__doc__
610 -        return wrapped
611 -    return deprecated_decorator
612 -
613 -def moved(modpath, objname):
614 -    """use to tell that a callable has been moved to a new module.
615 -
616 -    It returns a callable wrapper, so that when its called a warning is printed
617 -    telling where the object can be found, import is done (and not before) and
618 -    the actual object is called.
619 -
620 -    NOTE: the usage is somewhat limited on classes since it will fail if the
621 -    wrapper is use in a class ancestors list, use the `class_moved` function
622 -    instead (which has no lazy import feature though).
623 -    """
624 -    def callnew(*args, **kwargs):
625 -        from logilab.common.modutils import load_module_from_name
626 -        message = "object %s has been moved to module %s" % (objname, modpath)
627 -        warn(message, DeprecationWarning, stacklevel=2)
628 -        m = load_module_from_name(modpath)
629 -        return getattr(m, objname)(*args, **kwargs)
630 -    return callnew
631 -
632 +from logilab.common.changelog import Version
633 
634 
635  class DeprecationWrapper(object):
636      """proxy to print a warning on access to any attribute of the wrapped object
637      """
@@ -126,5 +40,149 @@
638          if attr in ('_proxied', '_msg'):
639              self.__dict__[attr] = value
640          else:
641              warn(self._msg, DeprecationWarning, stacklevel=2)
642              setattr(self._proxied, attr, value)
643 +
644 +
645 +class DeprecationManager(object):
646 +    """Manage the deprecation message handling. Messages are dropped for
647 +    versions more recent than the 'compatible' version. Example::
648 +
649 +        deprecator = deprecation.DeprecationManager("module_name")
650 +        deprecator.compatibility('1.3')
651 +
652 +        deprecator.warn('1.2', "message.")
653 +
654 +        @deprecator.deprecated('1.2', 'Message')
655 +        def any_func():
656 +            pass
657 +
658 +        class AnyClass(object):
659 +            __metaclass__ = deprecator.class_deprecated('1.2')
660 +    """
661 +    def __init__(self, module_name=None):
662 +        """
663 +        """
664 +        self.module_name = module_name
665 +        self.compatible_version = None
666 +
667 +    def compatibility(self, compatible_version):
668 +        """Set the compatible version.
669 +        """
670 +        self.compatible_version = Version(compatible_version)
671 +
672 +    def deprecated(self, version=None, reason=None, stacklevel=2, name=None, doc=None):
673 +        """Display a deprecation message only if the version is older than the
674 +        compatible version.
675 +        """
676 +        def decorator(func):
677 +            message = reason or 'The function "%s" is deprecated'
678 +            if '%s' in message:
679 +                message %= func.func_name
680 +            def wrapped(*args, **kwargs):
681 +                self.warn(version, message, stacklevel+1)
682 +                return func(*args, **kwargs)
683 +            return wrapped
684 +        return decorator
685 +
686 +    def class_deprecated(self, version=None):
687 +        class metaclass(type):
688 +            """metaclass to print a warning on instantiation of a deprecated class"""
689 +
690 +            def __call__(cls, *args, **kwargs):
691 +                msg = getattr(cls, "__deprecation_warning__",
692 +                              "%(cls)s is deprecated") % {'cls': cls.__name__}
693 +                self.warn(version, msg, stacklevel=3)
694 +                return type.__call__(cls, *args, **kwargs)
695 +        return metaclass
696 +
697 +    def moved(self, version, modpath, objname):
698 +        """use to tell that a callable has been moved to a new module.
699 +
700 +        It returns a callable wrapper, so that when its called a warning is printed
701 +        telling where the object can be found, import is done (and not before) and
702 +        the actual object is called.
703 +
704 +        NOTE: the usage is somewhat limited on classes since it will fail if the
705 +        wrapper is use in a class ancestors list, use the `class_moved` function
706 +        instead (which has no lazy import feature though).
707 +        """
708 +        def callnew(*args, **kwargs):
709 +            from logilab.common.modutils import load_module_from_name
710 +            message = "object %s has been moved to module %s" % (objname, modpath)
711 +            self.warn(version, message)
712 +            m = load_module_from_name(modpath)
713 +            return getattr(m, objname)(*args, **kwargs)
714 +        return callnew
715 +
716 +    def class_renamed(self, version, old_name, new_class, message=None):
717 +        clsdict = {}
718 +        if message is None:
719 +            message = '%s is deprecated, use %s' % (old_name, new_class.__name__)
720 +        clsdict['__deprecation_warning__'] = message
721 +        try:
722 +            # new-style class
723 +            return self.class_deprecated(version)(old_name, (new_class,), clsdict)
724 +        except (NameError, TypeError):
725 +            # old-style class
726 +            class DeprecatedClass(new_class):
727 +                """FIXME: There might be a better way to handle old/new-style class
728 +                """
729 +                def __init__(self, *args, **kwargs):
730 +                    self.warn(version, message, stacklevel=3)
731 +                    new_class.__init__(self, *args, **kwargs)
732 +            return DeprecatedClass
733 +
734 +    def class_moved(self, version, new_class, old_name=None, message=None):
735 +        """nice wrapper around class_renamed when a class has been moved into
736 +        another module
737 +        """
738 +        if old_name is None:
739 +            old_name = new_class.__name__
740 +        if message is None:
741 +            message = 'class %s is now available as %s.%s' % (
742 +                old_name, new_class.__module__, new_class.__name__)
743 +        return self.class_renamed(version, old_name, new_class, message)
744 +
745 +    def warn(self, version=None, reason="", stacklevel=2):
746 +        """Display a deprecation message only if the version is older than the
747 +        compatible version.
748 +        """
749 +        if (self.compatible_version is None
750 +            or version is None
751 +            or Version(version) < self.compatible_version):
752 +            if self.module_name and version:
753 +                reason = '[%s %s] %s' % (self.module_name, version, reason)
754 +            elif self.module_name:
755 +                reason = '[%s] %s' % (self.module_name, reason)
756 +            elif version:
757 +                reason = '[%s] %s' % (version, reason)
758 +            warn(reason, DeprecationWarning, stacklevel=stacklevel)
759 +
760 +_defaultdeprecator = DeprecationManager()
761 +
762 +def deprecated(reason=None, stacklevel=2, name=None, doc=None):
763 +    return _defaultdeprecator.deprecated(None, reason, stacklevel, name, doc)
764 +
765 +class_deprecated = _defaultdeprecator.class_deprecated()
766 +
767 +def moved(modpath, objname):
768 +    return _defaultdeprecator.moved(None, modpath, objname)
769 +moved.__doc__ = _defaultdeprecator.moved.__doc__
770 +
771 +def class_renamed(old_name, new_class, message=None):
772 +    """automatically creates a class which fires a DeprecationWarning
773 +    when instantiated.
774 +
775 +    >>> Set = class_renamed('Set', set, 'Set is now replaced by set')
776 +    >>> s = Set()
777 +    sample.py:57: DeprecationWarning: Set is now replaced by set
778 +    s = Set()
779 +    >>>
780 +    """
781 +    return _defaultdeprecator.class_renamed(None, old_name, new_class, message)
782 +
783 +def class_moved(new_class, old_name=None, message=None):
784 +    return _defaultdeprecator.class_moved(None, new_class, old_name, message)
785 +class_moved.__doc__ = _defaultdeprecator.class_moved.__doc__
786 +
diff --git a/graph.py b/graph.py
@@ -132,18 +132,18 @@
787          """emit an edge from <name1> to <name2>.
788          edge properties: see http://www.graphviz.org/doc/info/attrs.html
789          """
790          attrs = ['%s="%s"' % (prop, value) for prop, value in props.items()]
791          n_from, n_to = normalize_node_id(name1), normalize_node_id(name2)
792 -        self.emit('%s -> %s [%s];' % (n_from, n_to, ", ".join(attrs)) )
793 +        self.emit('%s -> %s [%s];' % (n_from, n_to, ', '.join(sorted(attrs))) )
794 
795      def emit_node(self, name, **props):
796          """emit a node with given properties.
797          node properties: see http://www.graphviz.org/doc/info/attrs.html
798          """
799          attrs = ['%s="%s"' % (prop, value) for prop, value in props.items()]
800 -        self.emit('%s [%s];' % (normalize_node_id(name), ", ".join(attrs)))
801 +        self.emit('%s [%s];' % (normalize_node_id(name), ', '.join(sorted(attrs))))
802 
803  def normalize_node_id(nid):
804      """Returns a suitable DOT node id for `nid`."""
805      return '"%s"' % nid
806 
diff --git a/modutils.py b/modutils.py
@@ -25,10 +25,12 @@
807  :var STD_LIB_DIR: directory where standard modules are located
808 
809  :type BUILTIN_MODULES: dict
810  :var BUILTIN_MODULES: dictionary with builtin module names has key
811  """
812 +from __future__ import with_statement
813 +
814  __docformat__ = "restructuredtext en"
815 
816  import sys
817  import os
818  from os.path import splitext, join, abspath, isdir, dirname, exists, basename
@@ -654,18 +656,23 @@
819              if mtype != PKG_DIRECTORY:
820                  raise ImportError('No module %s in %s' % ('.'.join(modpath),
821                                                            '.'.join(imported)))
822              # XXX guess if package is using pkgutil.extend_path by looking for
823              # those keywords in the first four Kbytes
824 -            data = open(join(mp_filename, '__init__.py')).read(4096)
825 -            if 'pkgutil' in data and 'extend_path' in data:
826 -                # extend_path is called, search sys.path for module/packages of this name
827 -                # see pkgutil.extend_path documentation
828 -                path = [join(p, modname) for p in sys.path
829 -                        if isdir(join(p, modname))]
830 +            try:
831 +                with open(join(mp_filename, '__init__.py')) as stream:
832 +                    data = stream.read(4096)
833 +            except IOError:
834 +                path = [mp_filename]
835              else:
836 -                path = [mp_filename]
837 +                if 'pkgutil' in data and 'extend_path' in data:
838 +                    # extend_path is called, search sys.path for module/packages
839 +                    # of this name see pkgutil.extend_path documentation
840 +                    path = [join(p, *imported) for p in sys.path
841 +                            if isdir(join(p, *imported))]
842 +                else:
843 +                    path = [mp_filename]
844      return mtype, mp_filename
845 
846  def _is_python_file(filename):
847      """return true if the given filename should be considered as a python file
848 
diff --git a/optik_ext.py b/optik_ext.py
@@ -63,13 +63,10 @@
849      from mx import DateTime
850      HAS_MX_DATETIME = True
851  except ImportError:
852      HAS_MX_DATETIME = False
853 
854 -
855 -OPTPARSE_FORMAT_DEFAULT = sys.version_info >= (2, 4)
856 -
857  from logilab.common.textutils import splitstrip
858 
859  def check_regexp(option, opt, value):
860      """check a regexp value by trying to compile it
861      return the compiled regexp
@@ -225,14 +222,11 @@
862 
863 
864      def process(self, opt, value, values, parser):
865          # First, convert the value(s) to the right type.  Howl if any
866          # value(s) are bogus.
867 -        try:
868 -            value = self.convert_value(opt, value)
869 -        except AttributeError: # py < 2.4
870 -            value = self.check_value(opt, value)
871 +        value = self.convert_value(opt, value)
872          if self.type == 'named':
873              existant = getattr(values, self.dest)
874              if existant:
875                  existant.update(value)
876                  value = existant
diff --git a/pdf_ext.py b/pdf_ext.py
@@ -1,111 +0,0 @@
877 -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
878 -# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
879 -#
880 -# This file is part of logilab-common.
881 -#
882 -# logilab-common is free software: you can redistribute it and/or modify it under
883 -# the terms of the GNU Lesser General Public License as published by the Free
884 -# Software Foundation, either version 2.1 of the License, or (at your option) any
885 -# later version.
886 -#
887 -# logilab-common is distributed in the hope that it will be useful, but WITHOUT
888 -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
889 -# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
890 -# details.
891 -#
892 -# You should have received a copy of the GNU Lesser General Public License along
893 -# with logilab-common.  If not, see <http://www.gnu.org/licenses/>.
894 -"""Manipulate pdf and fdf files (pdftk recommended).
895 -
896 -Notes regarding pdftk, pdf forms and fdf files (form definition file)
897 -fields names can be extracted with:
898 -
899 -    pdftk orig.pdf generate_fdf output truc.fdf
900 -
901 -to merge fdf and pdf:
902 -
903 -    pdftk orig.pdf fill_form test.fdf output result.pdf [flatten]
904 -
905 -without flatten, one could further edit the resulting form.
906 -with flatten, everything is turned into text.
907 -
908 -
909 -
910 -
911 -"""
912 -__docformat__ = "restructuredtext en"
913 -# XXX seems very unix specific
914 -# TODO: check availability of pdftk at import
915 -
916 -
917 -import os
918 -
919 -HEAD="""%FDF-1.2
920 -%\xE2\xE3\xCF\xD3
921 -1 0 obj
922 -<<
923 -/FDF
924 -<<
925 -/Fields [
926 -"""
927 -
928 -TAIL="""]
929 ->>
930 ->>
931 -endobj
932 -trailer
933 -
934 -<<
935 -/Root 1 0 R
936 ->>
937 -%%EOF
938 -"""
939 -
940 -def output_field( f ):
941 -    return "\xfe\xff" + "".join( [ "\x00"+c for c in f ] )
942 -
943 -def extract_keys(lines):
944 -    keys = []
945 -    for line in lines:
946 -        if line.startswith('/V'):
947 -            pass #print 'value',line
948 -        elif line.startswith('/T'):
949 -            key = line[7:-2]
950 -            key = ''.join(key.split('\x00'))
951 -            keys.append( key )
952 -    return keys
953 -
954 -def write_field(out, key, value):
955 -    out.write("<<\n")
956 -    if value:
957 -        out.write("/V (%s)\n" %value)
958 -    else:
959 -        out.write("/V /\n")
960 -    out.write("/T (%s)\n" % output_field(key) )
961 -    out.write(">> \n")
962 -
963 -def write_fields(out, fields):
964 -    out.write(HEAD)
965 -    for (key, value, comment) in fields:
966 -        write_field(out, key, value)
967 -        write_field(out, key+"a", value) # pour copie-carbone sur autres pages
968 -    out.write(TAIL)
969 -
970 -def extract_keys_from_pdf(filename):
971 -    # what about using 'pdftk filename dump_data_fields' and parsing the output ?
972 -    os.system('pdftk %s generate_fdf output /tmp/toto.fdf' % filename)
973 -    lines = file('/tmp/toto.fdf').readlines()
974 -    return extract_keys(lines)
975 -
976 -
977 -def fill_pdf(infile, outfile, fields):
978 -    write_fields(file('/tmp/toto.fdf', 'w'), fields)
979 -    os.system('pdftk %s fill_form /tmp/toto.fdf output %s flatten' % (infile, outfile))
980 -
981 -def testfill_pdf(infile, outfile):
982 -    keys = extract_keys_from_pdf(infile)
983 -    fields = []
984 -    for key in keys:
985 -        fields.append( (key, key, '') )
986 -    fill_pdf(infile, outfile, fields)
987 -
diff --git a/python-logilab-common.spec b/python-logilab-common.spec
@@ -8,11 +8,11 @@
988  %define __python /usr/bin/python
989  %endif
990  %{!?_python_sitelib: %define _python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")}
991 
992  Name:           %{python}-logilab-common
993 -Version:        0.59.1
994 +Version:        0.61.0
995  Release:        logilab.1%{?dist}
996  Summary:        Common libraries for Logilab projects
997 
998  Group:          Development/Libraries
999  License:        LGPLv2.1+
diff --git a/setup.py b/setup.py
@@ -137,15 +137,16 @@
1000                  dest = join(basedir, directory)
1001                  shutil.rmtree(dest, ignore_errors=True)
1002                  shutil.copytree(directory, dest)
1003                  if sys.version_info >= (3, 0):
1004                      # process manually python file in include_dirs (test data)
1005 -                    from subprocess import check_call
1006 +                    from distutils.util import run_2to3
1007                      # brackets are NOT optional here for py3k compat
1008                      print('running 2to3 on', dest)
1009 -                    # Needs `shell=True` to run on Windows.
1010 -                    check_call(['2to3', '-wn', dest], shell=True)
1011 +                    run_2to3([dest])
1012 +
1013 +
1014 
1015 
1016  def install(**kwargs):
1017      """setup entry point"""
1018      if USE_SETUPTOOLS:
diff --git a/shellutils.py b/shellutils.py
@@ -29,15 +29,17 @@
1019  import time
1020  import fnmatch
1021  import errno
1022  import string
1023  import random
1024 +import subprocess
1025  from os.path import exists, isdir, islink, basename, join
1026 
1027  from logilab.common import STD_BLACKLIST, _handle_blacklist
1028  from logilab.common.compat import raw_input
1029  from logilab.common.compat import str_to_bytes
1030 +from logilab.common.deprecation import deprecated
1031 
1032  try:
1033      from logilab.common.proc import ProcInfo, NoSuchProcess
1034  except ImportError:
1035      # windows platform
@@ -222,24 +224,21 @@
1036          else:
1037              outfile = open(join(destdir, name), 'wb')
1038              outfile.write(zfobj.read(name))
1039              outfile.close()
1040 
1041 +@deprecated('Use subprocess.Popen instead')
1042  class Execute:
1043      """This is a deadlock safe version of popen2 (no stdin), that returns
1044      an object with errorlevel, out and err.
1045      """
1046 
1047      def __init__(self, command):
1048 -        outfile = tempfile.mktemp()
1049 -        errfile = tempfile.mktemp()
1050 -        self.status = os.system("( %s ) >%s 2>%s" %
1051 -                                (command, outfile, errfile)) >> 8
1052 -        self.out = open(outfile, "r").read()
1053 -        self.err = open(errfile, "r").read()
1054 -        os.remove(outfile)
1055 -        os.remove(errfile)
1056 +        cmd = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1057 +        self.out, self.err = cmd.communicate()
1058 +        self.status = os.WEXITSTATUS(cmd.returncode)
1059 +
1060 
1061  def acquire_lock(lock_file, max_try=10, delay=10, max_delay=3600):
1062      """Acquire a lock represented by a file on the file system
1063 
1064      If the process written in lock file doesn't exist anymore, we remove the
diff --git a/table.py b/table.py
@@ -46,10 +46,12 @@
1065          if other is None:
1066              return False
1067          else:
1068              return list(self) == list(other)
1069 
1070 +    __hash__ = object.__hash__
1071 +
1072      def __ne__(self, other):
1073          return not self == other
1074 
1075      def __len__(self):
1076          return len(self.row_names)
diff --git a/tasksqueue.py b/tasksqueue.py
@@ -92,7 +92,9 @@
1077          return self.priority < other.priority
1078 
1079      def __eq__(self, other):
1080          return self.id == other.id
1081 
1082 +    __hash__ = object.__hash__
1083 +
1084      def merge(self, other):
1085          pass
diff --git a/test/unittest_configuration.py b/test/unittest_configuration.py
@@ -16,30 +16,32 @@
1086  # You should have received a copy of the GNU Lesser General Public License along
1087  # with logilab-common.  If not, see <http://www.gnu.org/licenses/>.
1088  import tempfile
1089  import os
1090  from os.path import join, dirname, abspath
1091 +import re
1092 
1093  from cStringIO import StringIO
1094  from sys import version_info
1095 
1096  from logilab.common.testlib import TestCase, unittest_main
1097  from logilab.common.optik_ext import OptionValueError
1098  from logilab.common.configuration import Configuration, \
1099 -     OptionsManagerMixIn, OptionsProviderMixIn, Method, read_old_config
1100 +     OptionsManagerMixIn, OptionsProviderMixIn, Method, read_old_config, \
1101 +     merge_options
1102 
1103  DATA = join(dirname(abspath(__file__)), 'data')
1104 
1105 -options = [('dothis', {'type':'yn', 'action': 'store', 'default': True, 'metavar': '<y or n>'}),
1106 +OPTIONS = [('dothis', {'type':'yn', 'action': 'store', 'default': True, 'metavar': '<y or n>'}),
1107             ('value', {'type': 'string', 'metavar': '<string>', 'short': 'v'}),
1108 -           ('multiple', {'type': 'csv', 'default': ('yop', 'yep'),
1109 +           ('multiple', {'type': 'csv', 'default': ['yop', 'yep'],
1110                           'metavar': '<comma separated values>',
1111                           'help': 'you can also document the option'}),
1112             ('number', {'type': 'int', 'default':2, 'metavar':'<int>', 'help': 'boom'}),
1113             ('choice', {'type': 'choice', 'default':'yo', 'choices': ('yo', 'ye'),
1114                         'metavar':'<yo|ye>'}),
1115 -           ('multiple-choice', {'type': 'multiple_choice', 'default':('yo', 'ye'),
1116 +           ('multiple-choice', {'type': 'multiple_choice', 'default':['yo', 'ye'],
1117                                  'choices': ('yo', 'ye', 'yu', 'yi', 'ya'),
1118                                  'metavar':'<yo|ye>'}),
1119             ('named', {'type':'named', 'default':Method('get_named'),
1120                        'metavar': '<key=val>'}),
1121 
@@ -54,20 +56,20 @@
1122          return {'key': 'val'}
1123 
1124  class ConfigurationTC(TestCase):
1125 
1126      def setUp(self):
1127 -        self.cfg = MyConfiguration(name='test', options=options, usage='Just do it ! (tm)')
1128 +        self.cfg = MyConfiguration(name='test', options=OPTIONS, usage='Just do it ! (tm)')
1129 
1130      def test_default(self):
1131          cfg = self.cfg
1132          self.assertEqual(cfg['dothis'], True)
1133          self.assertEqual(cfg['value'], None)
1134 -        self.assertEqual(cfg['multiple'], ('yop', 'yep'))
1135 +        self.assertEqual(cfg['multiple'], ['yop', 'yep'])
1136          self.assertEqual(cfg['number'], 2)
1137          self.assertEqual(cfg['choice'], 'yo')
1138 -        self.assertEqual(cfg['multiple-choice'], ('yo', 'ye'))
1139 +        self.assertEqual(cfg['multiple-choice'], ['yo', 'ye'])
1140          self.assertEqual(cfg['named'], {'key': 'val'})
1141 
1142      def test_base(self):
1143          cfg = self.cfg
1144          cfg.set_option('number', '0')
@@ -199,18 +201,21 @@
1145  [AGROUP]
1146 
1147  diffgroup=pouet""")
1148 
1149 
1150 -    def test_loopback(self):
1151 +    def test_roundtrip(self):
1152          cfg = self.cfg
1153          f = tempfile.mktemp()
1154          stream = open(f, 'w')
1155          try:
1156 +            self.cfg['dothis'] = False
1157 +            self.cfg['multiple'] = ["toto", "tata"]
1158 +            self.cfg['number'] = 3
1159              cfg.generate_config(stream)
1160              stream.close()
1161 -            new_cfg = MyConfiguration(name='testloop', options=options)
1162 +            new_cfg = MyConfiguration(name='test', options=OPTIONS)
1163              new_cfg.load_file_configuration(f)
1164              self.assertEqual(cfg['dothis'], new_cfg['dothis'])
1165              self.assertEqual(cfg['multiple'], new_cfg['multiple'])
1166              self.assertEqual(cfg['number'], new_cfg['number'])
1167              self.assertEqual(cfg['choice'], new_cfg['choice'])
@@ -231,22 +236,23 @@
1168          # at least in python 2.4.2 the output is:
1169          # '  -v <string>, --value=<string>'
1170          # it is not unlikely some optik/optparse versions do print -v<string>
1171          # so accept both
1172          help = help.replace(' -v <string>, ', ' -v<string>, ')
1173 +        help = re.sub('[ ]*(\r?\n)', '\\1', help)
1174          USAGE = """Usage: Just do it ! (tm)
1175 
1176  Options:
1177    -h, --help            show this help message and exit
1178 -  --dothis=<y or n>     
1179 +  --dothis=<y or n>
1180    -v<string>, --value=<string>
1181    --multiple=<comma separated values>
1182                          you can also document the option [current: yop,yep]
1183    --number=<int>        boom [current: 2]
1184 -  --choice=<yo|ye>      
1185 +  --choice=<yo|ye>
1186    --multiple-choice=<yo|ye>
1187 -  --named=<key=val>     
1188 +  --named=<key=val>
1189 
1190    Agroup:
1191      --diffgroup=<key=val>
1192 
1193    Bonus:
@@ -327,8 +333,27 @@
1194 
1195      def test_load_defaults(self):
1196          self.linter.load_command_line_configuration([])
1197          self.assertEqual(self.linter.config.profile, False)
1198 
1199 +class MergeTC(TestCase):
1200 +
1201 +    def test_merge1(self):
1202 +        merged = merge_options([('dothis', {'type':'yn', 'action': 'store', 'default': True,  'metavar': '<y or n>'}),
1203 +                                ('dothis', {'type':'yn', 'action': 'store', 'default': False, 'metavar': '<y or n>'}),
1204 +                                ])
1205 +        self.assertEqual(len(merged), 1)
1206 +        self.assertEqual(merged[0][0], 'dothis')
1207 +        self.assertEqual(merged[0][1]['default'], True)
1208 +
1209 +    def test_merge2(self):
1210 +        merged = merge_options([('dothis', {'type':'yn', 'action': 'store', 'default': True,  'metavar': '<y or n>'}),
1211 +                                ('value', {'type': 'string', 'metavar': '<string>', 'short': 'v'}),
1212 +                                ('dothis', {'type':'yn', 'action': 'store', 'default': False, 'metavar': '<y or n>'}),
1213 +                                ])
1214 +        self.assertEqual(len(merged), 2)
1215 +        self.assertEqual(merged[0][0], 'value')
1216 +        self.assertEqual(merged[1][0], 'dothis')
1217 +        self.assertEqual(merged[1][1]['default'], True)
1218 
1219  if __name__ == '__main__':
1220      unittest_main()
diff --git a/test/unittest_date.py b/test/unittest_date.py
@@ -136,10 +136,17 @@
1221      def test_ticks2datetime_before_1900(self):
1222          ticks = -2209075200000
1223          date = ticks2datetime(ticks)
1224          self.assertEqual(ustrftime(date, '%Y-%m-%d'), u'1899-12-31')
1225 
1226 +    def test_month(self):
1227 +        """enumerate months"""
1228 +        r = list(date_range(self.datecls(2006, 5, 6), self.datecls(2006, 8, 27),
1229 +                            incmonth=True))
1230 +        expected = [self.datecls(2006, 5, 6), self.datecls(2006, 6, 1), self.datecls(2006, 7, 1), self.datecls(2006, 8, 1)]
1231 +        self.assertListEqual(expected, r)
1232 +
1233 
1234  class MxDateTC(DateTC):
1235      datecls = mxDate
1236      datetimecls = mxDateTime
1237      timedeltacls = RelativeDateTime
diff --git a/test/unittest_deprecation.py b/test/unittest_deprecation.py
@@ -76,7 +76,68 @@
1238          any_func = deprecation.moved(module, 'moving_target')
1239          any_func()
1240          self.assertEqual(self.messages,
1241                           ['object moving_target has been moved to module data.deprecation'])
1242 
1243 +    def test_deprecated_manager(self):
1244 +        deprecator = deprecation.DeprecationManager("module_name")
1245 +        deprecator.compatibility('1.3')
1246 +        # This warn should be printed.
1247 +        deprecator.warn('1.1', "Major deprecation message.", 1)
1248 +        deprecator.warn('1.1')
1249 +
1250 +        @deprecator.deprecated('1.2', 'Major deprecation message.')
1251 +        def any_func():
1252 +            pass
1253 +        any_func()
1254 +
1255 +        @deprecator.deprecated('1.2')
1256 +        def other_func():
1257 +            pass
1258 +        other_func()
1259 +
1260 +        self.assertListEqual(self.messages,
1261 +                             ['[module_name 1.1] Major deprecation message.',
1262 +                              '[module_name 1.1] ',
1263 +                              '[module_name 1.2] Major deprecation message.',
1264 +                              '[module_name 1.2] The function "other_func" is deprecated'])
1265 +
1266 +    def test_class_deprecated_manager(self):
1267 +        deprecator = deprecation.DeprecationManager("module_name")
1268 +        deprecator.compatibility('1.3')
1269 +        class AnyClass:
1270 +            __metaclass__ = deprecator.class_deprecated('1.2')
1271 +        AnyClass()
1272 +        self.assertEqual(self.messages,
1273 +                         ['[module_name 1.2] AnyClass is deprecated'])
1274 +
1275 +
1276 +    def test_deprecated_manager_noprint(self):
1277 +        deprecator = deprecation.DeprecationManager("module_name")
1278 +        deprecator.compatibility('1.3')
1279 +        # This warn should not be printed.
1280 +        deprecator.warn('1.3', "Minor deprecation message.", 1)
1281 +
1282 +        @deprecator.deprecated('1.3', 'Minor deprecation message.')
1283 +        def any_func():
1284 +            pass
1285 +        any_func()
1286 +
1287 +        @deprecator.deprecated('1.20')
1288 +        def other_func():
1289 +            pass
1290 +        other_func()
1291 +
1292 +        @deprecator.deprecated('1.4')
1293 +        def other_func():
1294 +            pass
1295 +        other_func()
1296 +
1297 +        class AnyClass(object):
1298 +            __metaclass__ = deprecator.class_deprecated((1,5))
1299 +        AnyClass()
1300 +
1301 +        self.assertFalse(self.messages)
1302 +
1303 +
1304  if __name__ == '__main__':
1305      unittest_main()
diff --git a/testlib.py b/testlib.py
@@ -548,10 +548,13 @@
1306      def quiet_run(self, result, func, *args, **kwargs):
1307          try:
1308              func(*args, **kwargs)
1309          except (KeyboardInterrupt, SystemExit):
1310              raise
1311 +        except unittest.SkipTest, e:
1312 +            self._addSkip(result, str(e))
1313 +            return False
1314          except:
1315              result.addError(self, self.__exc_info())
1316              return False
1317          return True
1318 
@@ -1185,10 +1188,13 @@
1319 
1320      if sys.version_info >= (3,2):
1321          assertItemsEqual = unittest.TestCase.assertCountEqual
1322      else:
1323          assertCountEqual = unittest.TestCase.assertItemsEqual
1324 +        if sys.version_info < (2,7):
1325 +            def assertIsNotNone(self, value, *args, **kwargs):
1326 +                self.assertNotEqual(None, value, *args, **kwargs)
1327 
1328  TestCase.assertItemsEqual = deprecated('assertItemsEqual is deprecated, use assertCountEqual')(
1329      TestCase.assertItemsEqual)
1330 
1331  import doctest